Newer Version Available

This content describes an older version of this product. View Latest

Loopback Custom Adapter for Salesforce Connect

This example illustrates how to handle filtering in queries. For simplicity, this example connects the Salesforce org to itself as the external system.

LoopbackDataSourceConnection Class

1/**
2 *   Extends the DataSource.Connection class to enable 
3 *   Salesforce to sync the external system’s schema 
4 *   and to handle queries and searches of the external data. 
5 **/
6global class LoopbackDataSourceConnection 
7    extends DataSource.Connection {
8
9    /**
10     *   Constructors.
11     **/
12    global LoopbackDataSourceConnection(
13        DataSource.ConnectionParams connectionParams) {
14    }
15    global LoopbackDataSourceConnection() {}
16    
17    /**
18     *   Called when an external object needs to get a list of 
19     *   schema from the external data source, for example when 
20     *   the administrator clicks “Validate and Sync” in the 
21     *   user interface for the external data source.   
22     **/
23    override global List<DataSource.Table> sync() {
24        List<DataSource.Table> tables = 
25            new List<DataSource.Table>();        
26        List<DataSource.Column> columns;
27        columns = new List<DataSource.Column>();
28        columns.add(DataSource.Column.text('ExternalId', 255));
29        columns.add(DataSource.Column.url('DisplayUrl'));
30        columns.add(DataSource.Column.text('Name', 255));
31        columns.add(
32            DataSource.Column.number('NumberOfEmployees', 18, 0));
33        tables.add(
34            DataSource.Table.get('Looper', 'Name', columns));
35        return tables;
36    }
37    
38    /**
39     *   Called to query and get results from the external 
40     *   system for SOQL queries, list views, and detail pages 
41     *   for an external object that’s associated with the 
42     *   external data source.
43     *   
44     *   The QueryContext argument represents the query to run 
45     *   against a table in the external system.
46     *   
47     *   Returns a list of rows as the query results.
48     **/
49    override global DataSource.TableResult 
50        query(DataSource.QueryContext context) {
51        if (context.tableSelection.columnsSelected.size() == 1 &&
52            context.tableSelection.columnsSelected.get(0).aggregation ==
53                DataSource.QueryAggregation.COUNT) {
54            integer count = execCount(getCountQuery(context));
55            List<Map<String, Object>> countResponse =
56                new List<Map<String, Object>>();
57            Map<String, Object> countRow =
58                new Map<String, Object>();
59            countRow.put(
60                context.tableSelection.columnsSelected.get(0).columnName,
61                count);
62            countResponse.add(countRow);
63            return DataSource.TableResult.get(context,countResponse);
64        } else {
65            List<Map<String,Object>> rows = execQuery(
66                getSoqlQuery(context));
67            return DataSource.TableResult.get(context,rows);
68        }
69    }
70    
71    /**
72     *   Called to do a full text search and get results from
73     *   the external system for SOSL queries and Salesforce
74     *   global searches.
75     *   
76     *   The SearchContext argument represents the query to run 
77     *   against a table in the external system.
78     *   
79     *   Returns results for each table that the SearchContext 
80     *   requested to be searched.
81     **/
82    override global List<DataSource.TableResult> 
83        search(DataSource.SearchContext context) {        
84        return DataSource.SearchUtils.searchByName(context, this);
85    }
86
87    /**
88     *   Helper method to execute the SOQL query and 
89     *   return the results.
90     **/
91    private List<Map<String,Object>> 
92        execQuery(String soqlQuery) {
93        List<Account> objs = Database.query(soqlQuery);
94        List<Map<String,Object>> rows = 
95            new List<Map<String,Object>>();
96        for (Account obj : objs) {
97            Map<String,Object> row = new Map<String,Object>();
98            row.put('Name', obj.Name);
99            row.put('NumberOfEmployees', obj.NumberOfEmployees);
100            row.put('ExternalId', obj.Id);
101            row.put('DisplayUrl', 
102                URL.getSalesforceBaseUrl().toExternalForm() + 
103                    obj.Id);
104            rows.add(row);
105        }
106        return rows;
107    }
108
109    /**
110     *   Helper method to get aggregate count.
111     **/
112    private integer execCount(String soqlQuery) {
113        integer count = Database.countQuery(soqlQuery);
114        return count;
115    }
116
117    /**
118     *   Helper method to create default aggregate query.
119     **/
120    private String getCountQuery(DataSource.QueryContext context) {
121        String baseQuery = 'SELECT COUNT() FROM Account';
122        String filter = getSoqlFilter('', 
123            context.tableSelection.filter);
124        if (filter.length() > 0)
125            return baseQuery + ' WHERE ' + filter;
126        return baseQuery;
127    }
128
129    /**
130     *   Helper method to create default query.
131     **/
132    private String getSoqlQuery(DataSource.QueryContext context) {
133        String baseQuery = 
134            'SELECT Id,Name,NumberOfEmployees FROM Account';
135        String filter = getSoqlFilter('', 
136            context.tableSelection.filter);
137        if (filter.length() > 0)
138            return baseQuery + ' WHERE ' + filter;
139        return baseQuery;
140    }
141
142    /**
143     *   Helper method to handle query filter.
144     **/
145    private String getSoqlFilter(String query, 
146        DataSource.Filter filter) {
147        if (filter == null) {
148            return query;
149        }
150        String append;
151        DataSource.FilterType type = filter.type;
152        List<Map<String,Object>> retainedRows = 
153            new List<Map<String,Object>>();
154        if (type == DataSource.FilterType.NOT_) {
155            DataSource.Filter subfilter = filter.subfilters.get(0);
156            append = getSoqlFilter('NOT', subfilter);
157        } else if (type == DataSource.FilterType.AND_) {
158            append =  
159                getSoqlFilterCompound('AND', filter.subfilters);
160        } else if (type == DataSource.FilterType.OR_) {
161            append = 
162                getSoqlFilterCompound('OR', filter.subfilters);
163        } else {
164            append = getSoqlFilterExpression(filter);
165        }
166        return query + ' ' + append;
167    }
168    
169    /**
170     *   Helper method to handle query subfilters.
171     **/
172    private String getSoqlFilterCompound(String operator, 
173        List<DataSource.Filter> subfilters) {
174        String expression = ' (';
175        boolean first = true;
176        for (DataSource.Filter subfilter : subfilters) {
177            if (first)
178                first = false;
179            else
180                expression += ' ' + operator + ' ';
181            expression += getSoqlFilter('', subfilter);
182        }
183        expression += ') ';
184        return expression;
185    }
186    
187    /**
188     *   Helper method to handle query filter expressions.
189     **/
190    private String getSoqlFilterExpression(
191        DataSource.Filter filter) {
192        String columnName = filter.columnName;
193        String operator;
194        Object expectedValue = filter.columnValue;
195        if (filter.type == DataSource.FilterType.EQUALS) {
196            operator = '=';
197        } else if (filter.type == 
198            DataSource.FilterType.NOT_EQUALS) {
199            operator = '<>';
200        } else if (filter.type == 
201            DataSource.FilterType.LESS_THAN) {
202            operator = '<';
203        } else if (filter.type == 
204            DataSource.FilterType.GREATER_THAN) {
205            operator = '>';
206        } else if (filter.type == 
207            DataSource.FilterType.LESS_THAN_OR_EQUAL_TO) {
208            operator = '<=';
209        } else if (filter.type == 
210            DataSource.FilterType.GREATER_THAN_OR_EQUAL_TO) {
211            operator = '>=';
212        } else if (filter.type == 
213            DataSource.FilterType.STARTS_WITH) {
214            return mapColumnName(columnName) + 
215            ' LIKE \'' + String.valueOf(expectedValue) + '%\'';
216        } else if (filter.type == 
217            DataSource.FilterType.ENDS_WITH) {
218            return mapColumnName(columnName) + 
219            ' LIKE \'%' + String.valueOf(expectedValue) + '\'';
220        } else if (filter.type == 
221            DataSource.FilterType.LIKE_) {
222            return mapColumnName(columnName) + 
223            ' LIKE \'' + String.valueOf(expectedValue) + '\'';
224        } else {
225            throwException(
226            'Implementing other filter types is left as an exercise for the reader: ' 
227            + filter.type);
228        }
229        return mapColumnName(columnName) + 
230            ' ' + operator + ' ' + wrapValue(expectedValue);
231    }
232    
233    /**
234     *   Helper method to map column names.
235     **/
236    private String mapColumnName(String apexName) {
237        if (apexName.equalsIgnoreCase('ExternalId'))
238            return 'Id';
239        if (apexName.equalsIgnoreCase('DisplayUrl'))
240            return 'Id';
241        return apexName;
242    }
243
244    /**
245    *   Helper method to wrap expression Strings with quotes.
246    **/
247    private String wrapValue(Object foundValue) {
248        if (foundValue instanceof String)
249            return '\'' + String.valueOf(foundValue) + '\'';
250        return String.valueOf(foundValue);
251    }
252}

LoopbackDataSourceProvider Class

1/**
2 *   Extends the DataSource.Provider base class to create a 
3 *   custom adapter for Salesforce Connect. The class informs 
4 *   Salesforce of the functional and authentication 
5 *   capabilities that are supported by or required to connect 
6 *   to an external system.
7 **/
8global class LoopbackDataSourceProvider 
9    extends DataSource.Provider {
10    
11    /**
12     *   Declares the types of authentication that can be used 
13     *   to access the external system.
14     **/
15    override global List<DataSource.AuthenticationCapability> 
16        getAuthenticationCapabilities() {
17        List<DataSource.AuthenticationCapability> capabilities = 
18            new List<DataSource.AuthenticationCapability>();
19        capabilities.add(
20            DataSource.AuthenticationCapability.ANONYMOUS);
21        capabilities.add(
22            DataSource.AuthenticationCapability.BASIC);
23        return capabilities;
24    }
25
26    /**
27     *   Declares the functional capabilities that the 
28     *   external system supports.
29     **/
30    override global List<DataSource.Capability> 
31        getCapabilities() {
32        List<DataSource.Capability> capabilities = 
33            new List<DataSource.Capability>();
34        capabilities.add(DataSource.Capability.ROW_QUERY);
35        capabilities.add(DataSource.Capability.SEARCH);
36        return capabilities;
37    }
38
39    /**
40     *   Declares the associated DataSource.Connection class.
41     **/
42    override global DataSource.Connection 
43        getConnection(DataSource.ConnectionParams connectionParams) {
44        return new LoopbackDataSourceConnection();
45    }
46}