global class SampleDataSourceConnection extends DataSource.Connection { global SampleDataSourceConnection(DataSource.ConnectionParams connectionParams) { } // ...
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.
// ... override global List<DataSource.Table> sync() { List<DataSource.Table> tables = new List<DataSource.Table>(); List<DataSource.Column> columns; columns = new List<DataSource.Column>(); columns.add(DataSource.Column.text('Name', 255)); columns.add(DataSource.Column.text('ExternalId', 255)); columns.add(DataSource.Column.url('DisplayUrl')); tables.add(DataSource.Table.get('Sample', 'Title', columns)); return tables; } // ...
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. The DataSource.QueryContext is always only for a single table.
This sample custom adapter uses a helper method in the DataSource.QueryUtils class to filter and sort the results based on the WHERE and ORDER BY clauses in the SOQL query.
The DataSource.QueryUtils class and its helper methods can process query results locally within your Salesforce org. This class is provided for your convenience to simplify the development of your Salesforce Connect custom adapter for initial tests. However, the DataSource.QueryUtils class and its methods aren’t supported for use in production environments that use callouts to retrieve data from external systems. Complete the filtering and sorting on the external system before sending the query results to Salesforce. When possible, use server-driven paging or another technique to have the external system determine the appropriate data subsets according to the limit and offset clauses in the query.
// ... override global DataSource.TableResult query( DataSource.QueryContext context) { if (context.tableSelection.columnsSelected.size() == 1 && context.tableSelection.columnsSelected.get(0).aggregation == DataSource.QueryAggregation.COUNT) { List<Map<String,Object>> rows = getRows(context); List<Map<String,Object>> response = DataSource.QueryUtils.filter(context, getRows(context)); List<Map<String, Object>> countResponse = new List<Map<String, Object>>(); Map<String, Object> countRow = new Map<String, Object>(); countRow.put( context.tableSelection.columnsSelected.get(0).columnName, response.size()); countResponse.add(countRow); return DataSource.TableResult.get(context, countResponse); } else { List<Map<String,Object>> filteredRows = DataSource.QueryUtils.filter(context, getRows(context)); List<Map<String,Object>> sortedRows = DataSource.QueryUtils.sort(context, filteredRows); List<Map<String,Object>> limitedRows = DataSource.QueryUtils.applyLimitAndOffset(context, sortedRows); return DataSource.TableResult.get(context, limitedRows); } } // ...
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.SearchContext can have multiple tables selected. In this example, however, the custom adapter knows about only one table.
// ... override global List<DataSource.TableResult> search( DataSource.SearchContext context) { List<DataSource.TableResult> results = new List<DataSource.TableResult>(); for (DataSource.TableSelection tableSelection : context.tableSelections) { results.add(DataSource.TableResult.get(tableSelection, getRows(context))); } return results; } // ...
These methods aren’t included in this snippet but are available in the full example included in Connection Class. Typically, the filter from SearchContext or QueryContext would be used to reduce the result set, but for simplicity this example doesn’t make use of the context object.
// ... // Helper method to get record values from the external system for the Sample table. private List<Map<String, Object>> getRows () { // Get row field values for the Sample table from the external system via a callout. HttpResponse response = makeGetCallout(); // Parse the JSON response and populate the rows. Map<String, Object> m = (Map<String, Object>)JSON.deserializeUntyped( response.getBody()); Map<String, Object> error = (Map<String, Object>)m.get('error'); if (error != null) { throwException(string.valueOf(error.get('message'))); } List<Map<String,Object>> rows = new List<Map<String,Object>>(); List<Object> jsonRows = (List<Object>)m.get('value'); if (jsonRows == null) { rows.add(foundRow(m)); } else { for (Object jsonRow : jsonRows) { Map<String,Object> row = (Map<String,Object>)jsonRow; rows.add(foundRow(row)); } } return rows; } // ...
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 following example provides a sample implementation for the upsertRows method. The example uses the passed-in UpsertContext to determine what table was selected and performs the upsert only if the name of the selected table is Sample. 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. Note that because a callout is made for each row, this example might hit the Apex callouts limit.
// ... global override List<DataSource.UpsertResult> upsertRows(DataSource.UpsertContext context) { if (context.tableSelected == 'Sample') { List<DataSource.UpsertResult> results = new List<DataSource.UpsertResult>(); List<Map<String, Object>> rows = context.rows; for (Map<String, Object> row : rows){ // Make a callout to insert or update records in the external system. HttpResponse response; // Determine whether to insert or update a record. if (row.get('ExternalId') == null){ // Send a POST HTTP request to insert new external record. // Make an Apex callout and get HttpResponse. response = makePostCallout( '{"name":"' + row.get('Name') + '","ExternalId":"' + row.get('ExternalId') + '"'); } else { // Send a PUT HTTP request to update an existing external record. // Make an Apex callout and get HttpResponse. response = makePutCallout( '{"name":"' + row.get('Name') + '","ExternalId":"' + row.get('ExternalId') + '"', String.valueOf(row.get('ExternalId'))); } // Check the returned response. // Deserialize the response. Map<String, Object> m = (Map<String, Object>)JSON.deserializeUntyped( response.getBody()); if (response.getStatusCode() == 200){ results.add(DataSource.UpsertResult.success( String.valueOf(m.get('id')))); } else { results.add(DataSource.UpsertResult.failure( String.valueOf(m.get('id')), 'The callout resulted in an error: ' + response.getStatusCode())); } } return results; } return null; } // ...
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 following example provides a sample implementation for the deleteRows method. 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. Note that because a callout is made for each ID, this example might hit the Apex callouts limit.
// ... global override List<DataSource.DeleteResult> deleteRows(DataSource.DeleteContext context) { if (context.tableSelected == 'Sample'){ List<DataSource.DeleteResult> results = new List<DataSource.DeleteResult>(); for (String externalId : context.externalIds){ HttpResponse response = makeDeleteCallout(externalId); if (response.getStatusCode() == 200){ results.add(DataSource.DeleteResult.success(externalId)); } else { results.add(DataSource.DeleteResult.failure(externalId, 'Callout delete error:' + response.getBody())); } } return results; } return null; } // ...