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
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.