Salesforce Custom adapter External Data Source

Introduction : –

In this blog, I am going to explain how to use Salesforce Apex connector framework to integrate your external data sources as salesforce external objects. In this example, I am using another salesforce org as a source org from where we are getting data for integration. Salesforce has cross-org  Adaptor out of the box but still, I am using integration through apex to the shown usage of the Apex connector framework.The Apex Salesforce Connect custom adapter can retrieve data from external systems and synthesize data locally. Salesforce Connect represents that data in Salesforce external objects, enabling users and the Lightning Platform to seamlessly interact with data that’s stored outside the Salesforce Org.Salesforce Connect provides seamless integration of data across system boundaries by letting your users view, search, and modify data that’s stored outside your Salesforce Org. In this example, I am going to show how to use apex connector framework to create, delete, update, search external data from the salesforce.In this example, we are going to sync book table data from one salesforce org to another salesforce as an external object by using apex connector framework. Here is the below image shows how to set up External Data Source by using apex connector adaptor .we are using named credentials for authenticating the salesforce source org.

 Using Apex Connector Framework

In order to create a custom adapter from the apex connector framework, you need to create two Apex classes: one that extends the DataSource.Connection class, and one that extends the DataSource.Provider class.DataSource.Connection class enables your Salesforce org to sync the external system’s schema and to handle queries, searches, and write operations (upsert and delete) of the external data.  DataSource.Provider to create a custom adapter for Salesforce Connect. The class informs Salesforce of the functional and authentication capabilities that are supported by or required to connect to the external system   DataSource.Connection class contains the flowing functions.

sync

The sync () method is invoked when an administrator clicks the Validate and Sync button on the external data source detail page. It returns information that describes the structural metadata on the external system.Here is the code for sync method to create a table

 override global List<DataSource.Table> sync() {
        List<DataSource.Table> tables =new List<DataSource.Table>();
        List<DataSource.Column> columns = new List<DataSource.Column>();
        columns.add(DataSource.Column.text('Name', 255));
        columns.add(DataSource.Column.text('Author_Name__c', 255));
        columns.add(DataSource.Column.text('Category__c', 255));
        columns.add(DataSource.Column.text('Publisher_Id__c', 255));
        columns.add(DataSource.Column.text('ExternalId', 255));
        columns.add(DataSource.Column.url('DisplayUrl'));
        tables.add(DataSource.Table.get('Salesforce Books', 'Title',columns));
        return tables;
        
    }

Validate and Sync button which will produce the table as shown below.

query

The query method is invoked when a SOQL query is executed on an external object. A SOQL query is automatically generated and executed when a user opens an external object’s list view or detail page in Salesforce.

override global DataSource.TableResult query(DataSource.QueryContext context) {
        String url = 'https://ltngdev-dev-ed.my.salesforce.com';
        DataSource.Filter filter = context.tableSelection.filter;
        List<Map<String, Object>> rows =
            DataSource.QueryUtils.process(context, getData());
        return DataSource.TableResult.get(true, null, context.tableSelection.tableSelected, rows);
        
    }

search

The search method is invoked by a SOSL query of an external object or when a user performs a Salesforce global search that also searches external objects. Because search can be federated over multiple objects, the DataSource.SearchContextcan have multiple tables selected. The following example allows you to search publisher id from the book table.

 override global List<DataSource.TableResult> search(DataSource.SearchContext context) {
        List<DataSource.TableResult> results =new List<DataSource.TableResult>();
        for (Integer i =0;i< context.tableSelections.size();i++) {
            String entity = context.tableSelections[i].tableSelected;
            String likeKey= context.searchPhrase;
            String url ='callout:Salesforce/services/data/v40.0/query?q=Select+Id+,Name,Author_Name__c,Category__c,Publisher_Id__c+from+Books__c+Where+Publisher_Id__c=\''+likeKey+'\'';
            results.add(DataSource.TableResult.get(true, null, entity, getData(url)));
        }
        return results;
    }

 

upsertRows

The upsertRows method is invoked when external object records are created or updated. You can create or update external object records through the Salesforce user interface or DML. The upsert operation is broken up into either an insert of a new record or an update of an existing record. These operations are performed in the external system using callouts. An array of DataSource.UpsertResult is populated from the results obtained from the callout responses.

 global override List<DataSource.UpsertResult> upsertRows(DataSource.UpsertContext context) {
        List<DataSource.UpsertResult> results = new List<DataSource.UpsertResult>();
        List<Map<String, Object>> rows = context.rows;
        for (Map<String, Object> row : rows){
            if (row.get('ExternalId') != null){
                String tempExtids = (String)row.get('ExternalId') ;
                String jsonTemp = '{"name":"' + (String)row.get('Name') + '","id":"' +(String)row.get('ExternalId') + '"}' ;
                String responsePatch = makePatchCallout(tempExtids ,jsonTemp);
                Map<String, Object> responseBodyMapTemp12 = (Map<String, Object>)JSON.deserializeUntyped(responsePatch);
                results.add(DataSource.UpsertResult.success(String.valueOf(responseBodyMapTemp12.get('id'))));
                return results ;
            }else{
                String jsontemp = '{"Name":"' + (String)row.get('Name') + '","Category__c":"' +(String)row.get('Category__c') + '"}' ;
                String  responsePost = makePostCallout(jsontemp);
                Map<String, Object> responseBodyMapTemp34 = (Map<String, Object>)JSON.deserializeUntyped(responsePost);
                results.add(DataSource.UpsertResult.success(String.valueOf(responseBodyMapTemp34.get('id'))));
                return results ;
            }
        }
        return results;
    }

 

deleteRows

The deleteRows method is invoked when external object records are deleted. You can delete external object records through the Salesforce user interface or DML.  The example uses the passed-in DeleteContext to determine what table was selected and performs the deletion only if the name of the selected table is Sample. The deletion is performed in the external system using callouts for each external ID. An array of DataSource.DeleteResult is populated from the results obtained from the callout responses.

 global override List<DataSource.DeleteResult> deleteRows(DataSource.DeleteContext context) {
        List<DataSource.DeleteResult> results = new List<DataSource.DeleteResult>();
        for (String externalId : context.externalIds){
            HttpResponse response = makeDeleteCallout(externalId);
            results.add(DataSource.DeleteResult.success(externalId));
        }
        return results;
    }

Here is the complete code Connection  class 

global class SFDCDataSourceConnection extends DataSource.Connection{
    global DataSource.ConnectionParams connectionInfo;
    global SFDCDataSourceConnection(DataSource.ConnectionParams connectionInfo) {
        this.connectionInfo = connectionInfo;
    }
    override global List<DataSource.Table> sync() {
        List<DataSource.Table> tables =new List<DataSource.Table>();
        List<DataSource.Column> columns = new List<DataSource.Column>();
        columns.add(DataSource.Column.text('Name', 255));
        columns.add(DataSource.Column.text('Author_Name__c', 255));
        columns.add(DataSource.Column.text('Category__c', 255));
        columns.add(DataSource.Column.text('Publisher_Id__c', 255));
        columns.add(DataSource.Column.text('ExternalId', 255));
        columns.add(DataSource.Column.url('DisplayUrl'));
        tables.add(DataSource.Table.get('Salesforce Books', 'Title',columns));
        return tables;
        
    }
    override global DataSource.TableResult query(DataSource.QueryContext context) {
        String url = 'https://ltngdev-dev-ed.my.salesforce.com';
        DataSource.Filter filter = context.tableSelection.filter;
        List<Map<String, Object>> rows =
            DataSource.QueryUtils.process(context, getData());
        return DataSource.TableResult.get(true, null, context.tableSelection.tableSelected, rows);
        
    }
    override global List<DataSource.TableResult> search(DataSource.SearchContext context) {
        List<DataSource.TableResult> results =new List<DataSource.TableResult>();
        for (Integer i =0;i< context.tableSelections.size();i++) {
            String entity = context.tableSelections[i].tableSelected;
            String likeKey= context.searchPhrase;
            String url ='callout:Salesforce/services/data/v40.0/query?q=Select+Id+,Name,Author_Name__c,Category__c,Publisher_Id__c+from+Books__c+Where+Publisher_Id__c=\''+likeKey+'\'';
            results.add(DataSource.TableResult.get(true, null, entity, getData(url)));
        }
        return results;
    }
    
    global override List<DataSource.UpsertResult> upsertRows(DataSource.UpsertContext context) {
        List<DataSource.UpsertResult> results = new List<DataSource.UpsertResult>();
        List<Map<String, Object>> rows = context.rows;
        for (Map<String, Object> row : rows){
            if (row.get('ExternalId') != null){
                String tempExtids = (String)row.get('ExternalId') ;
                String jsonTemp = '{"name":"' + (String)row.get('Name') + '","id":"' +(String)row.get('ExternalId') + '"}' ;
                String responsePatch = makePatchCallout(tempExtids ,jsonTemp);
                Map<String, Object> responseBodyMapTemp12 = (Map<String, Object>)JSON.deserializeUntyped(responsePatch);
                results.add(DataSource.UpsertResult.success(String.valueOf(responseBodyMapTemp12.get('id'))));
                return results ;
            }else{
                String jsontemp = '{"Name":"' + (String)row.get('Name') + '","Category__c":"' +(String)row.get('Category__c') + '"}' ;
                String  responsePost = makePostCallout(jsontemp);
                Map<String, Object> responseBodyMapTemp34 = (Map<String, Object>)JSON.deserializeUntyped(responsePost);
                results.add(DataSource.UpsertResult.success(String.valueOf(responseBodyMapTemp34.get('id'))));
                return results ;
            }
        }
        return results;
    }
    
    global override List<DataSource.DeleteResult> deleteRows(DataSource.DeleteContext context) {
        List<DataSource.DeleteResult> results = new List<DataSource.DeleteResult>();
        for (String externalId : context.externalIds){
            HttpResponse response = makeDeleteCallout(externalId);
            results.add(DataSource.DeleteResult.success(externalId));
        }
        return results;
    }
    global List<Map<String, Object>> getData(String url) {
        String response = getResponse(url);
        List<Map<String, Object>> rows =new List<Map<String, Object>>();
        Map<String, Object> responseBodyMap = (Map<String, Object>)JSON.deserializeUntyped(response);
        List<Object> fileItems=(List<Object>)responseBodyMap.get('records');
        if (fileItems != null) {
            for (Integer i=0; i < fileItems.size(); i++) {
                System.debug('iii'+fileItems[i]);
                Map<String, Object> item =(Map<String, Object>)fileItems[i];
                rows.add(foundRow(item));  
            }
        } else {
            //  rows.add(foundRow(responseBodyMap));
        }
        return rows;
    }
    
    private Map<String,Object> foundRow(Map<String,Object> foundRow) {
        Map<String,Object> row = new Map<String,Object>();
        row.put('ExternalId', string.valueOf(foundRow.get('Id')));
        row.put('DisplayUrl', string.valueOf(foundRow.get('DisplayUrl')));
        row.put('Name', string.valueOf(foundRow.get('Name')));       
        return row;
    }
    
    
    
    private List<Map<String, Object>> getData() {
        String response = getResponse();
        List<Map<String, Object>> rows =new List<Map<String, Object>>();
        Map<String, Object> responseBodyMap = (Map<String, Object>)JSON.deserializeUntyped(response);
        List<Object> fileItems=(List<Object>)responseBodyMap.get('records');
        if (fileItems != null) {
            for (Integer i=0; i < fileItems.size(); i++) {
                Map<String, Object> item =(Map<String, Object>)fileItems[i];
                rows.add(createRow(item));  
            }
        } else {
            rows.add(createRow(responseBodyMap));
        }
        return rows;
    }
    private  Map<String, Object> createRow(Map<String, Object> item){
        Map<String, Object> row = new Map<String, Object>();
        for ( String key : item.keySet() ) {
            if (key == 'id') {
                row.put('ExternalId', item.get(key));
                row.put('DisplayUrl', 'https://ltngdev-dev-ed.my.salesforce.com'+item.get(key));
            } else {
                row.put(key, item.get(key));
            }
        }
        return row;     
    }
    
    private  HttpResponse makeDeleteCallout(String recid) {
        String url ='callout:Salesforce/services/data/v41.0/sobjects/Books__c/'+recid;
        
        Http h = new Http();
        HttpRequest request = new HttpRequest();
        request.setEndPoint(url);
        request.setMethod('DELETE');
        request.setHeader('Authorization', this.connectionInfo.oauthToken);
        HttpResponse response = h.send(request);
        return response;
        
    }
    private  String makePatchCallout(String extenalId , String jsonBody) {
        String url ='callout:Salesforce/services/data/v41.0/sobjects/Books__c/'+extenalId;
        Http h = new Http();
        HttpRequest request = new HttpRequest();
        request.setEndPoint(url);
        request.setHeader('Content-Type', 'application/json');
        request.setBody(jsonBody);
        request.setHeader('Authorization', this.connectionInfo.oauthToken);
        request.setMethod('PATCH');
        HttpResponse response = h.send(request);
        return response.getBody();
        
    }
    private String makePostCallout( String jsonBody) {
        
        System.debug(' POST CALL --------');
        
        String url ='callout:Salesforce/services/data/v41.0/sobjects/Books__c';
        Http h = new Http();
        HttpRequest request = new HttpRequest();
        request.setEndPoint(url);
        request.setMethod('POST');
        request.setHeader('Content-Type', 'application/json');
        request.setBody(jsonBody);
        request.setHeader('Authorization', this.connectionInfo.oauthToken);
        HttpResponse response = h.send(request);
        System.debug(' IN POST CALL RESPONCE ---'+response.getBody());
        return response.getBody();
    }
    private String getResponse() {
        String url ='callout:Salesforce/services/data/v41.0/query?q=Select+Id+,Name,Author_Name__c,Category__c,Publisher_Id__c+from+Books__c';
        Http h = new Http();
        HttpRequest request = new HttpRequest();
        request.setEndPoint(url);
        request.setMethod('GET');
        request.setHeader('Authorization', this.connectionInfo.oauthToken);
        HttpResponse response = h.send(request);
        return response.getBody();
    } 
    private String getResponse(String url) {
        Http h = new Http();
        HttpRequest request = new HttpRequest();
        request.setEndPoint(url);
        request.setMethod('GET');
        request.setHeader('Authorization', this.connectionInfo.oauthToken);
        HttpResponse response = h.send(request);
        return response.getBody();
    } 
    
}

 

Create DataSource.Provider 

The second class you need to create an apex custom adaptor is by extends the DataSource.Provider which provides the functional and authentication capabilities that are supported by or required to connect to the external system. the authentication capabilities indicated how do you want to authenticate the external system like Basic, OAuth, and Anonymous and functional capabilities like create, delete, insert and update, search etc…
Here is the complete code.
global class SFDCSourceProvider extends DataSource.Provider{
    
    override global List<DataSource.AuthenticationCapability> getAuthenticationCapabilities() {
        List<DataSource.AuthenticationCapability> capabilities = new List<DataSource.AuthenticationCapability>();
        capabilities.add(DataSource.AuthenticationCapability.ANONYMOUS);
        capabilities.add(DataSource.AuthenticationCapability.OAUTH);
        capabilities.add(DataSource.AuthenticationCapability.BASIC);
        return capabilities;
    }
    
    override global List<DataSource.Capability> getCapabilities() {
        List<DataSource.Capability> capabilities =  new List<DataSource.Capability>();
       capabilities.add(DataSource.Capability.QUERY_PAGINATION_SERVER_DRIVEN);
        capabilities.add(DataSource.Capability.QUERY_TOTAL_SIZE);
        capabilities.add(DataSource.Capability.REQUIRE_ENDPOINT);
        capabilities.add(DataSource.Capability.REQUIRE_HTTPS);
        capabilities.add(DataSource.Capability.ROW_CREATE);
        capabilities.add(DataSource.Capability.ROW_DELETE);
        capabilities.add(DataSource.Capability.ROW_QUERY);
        capabilities.add(DataSource.Capability.ROW_UPDATE);
        capabilities.add(DataSource.Capability.SEARCH);
        
        return capabilities;
    }
    
    
    override global DataSource.Connection getConnection(DataSource.ConnectionParams connectionParams) {
        return new SFDCDataSourceConnection(connectionParams);
    }
    
}

 

Now go to external data source from the setup menu and create a new external data source by selecting the “Salesforce Connect: Custom” Type .then validate and Sync to create a new external object. after creating the external object, you can able to create or update or search the global search.