Newer Version Available
GitHub Custom Adapter for Salesforce Connect
This example illustrates how to support indirect lookup relationships. An indirect
lookup relationship links a child external object to a parent standard or custom
object.
For this example to work, create a custom field on the Contact standard object. Name the custom field github_username, make it a text field of length 39, and select the External ID and Unique attributes. Also, add https://api.github.com to your remote site settings.
GitHubDataSourceConnection Class
1/**
2 * Defines the connection to GitHub REST API v3 to support
3 * querying of GitHub profiles.
4 * Extends the DataSource.Connection class to enable
5 * Salesforce to sync the external system’s schema
6 * and to handle queries and searches of the external data.
7 **/
8global class GitHubDataSourceConnection extends
9 DataSource.Connection {
10 private DataSource.ConnectionParams connectionInfo;
11
12 /**
13 * Constructor for GitHubDataSourceConnection
14 **/
15 global GitHubDataSourceConnection(
16 DataSource.ConnectionParams connectionInfo) {
17 this.connectionInfo = connectionInfo;
18 }
19
20 /**
21 * Called to query and get results from the external
22 * system for SOQL queries, list views, and detail pages
23 * for an external object that’s associated with the
24 * external data source.
25 *
26 * The queryContext argument represents the query to run
27 * against a table in the external system.
28 *
29 * Returns a list of rows as the query results.
30 **/
31 override global DataSource.TableResult query(
32 DataSource.QueryContext context) {
33 DataSource.Filter filter = context.tableSelection.filter;
34 String url;
35 if (filter != null) {
36 String thisColumnName = filter.columnName;
37 if (thisColumnName != null &&
38 (thisColumnName.equals('ExternalId') ||
39 thisColumnName.equals('login')))
40 url = 'https://api.github.com/users/'
41 + filter.columnValue;
42 else
43 url = 'https://api.github.com/users';
44 } else {
45 url = 'https://api.github.com/users';
46 }
47
48 /**
49 * Filters, sorts, and applies limit and offset clauses.
50 **/
51 List<Map<String, Object>> rows =
52 DataSource.QueryUtils.process(context, getData(url));
53 return DataSource.TableResult.get(true, null,
54 context.tableSelection.tableSelected, rows);
55 }
56
57 /**
58 * Defines the schema for the external system.
59 * Called when the administrator clicks “Validate and Sync”
60 * in the user interface for the external data source.
61 **/
62 override global List<DataSource.Table> sync() {
63 List<DataSource.Table> tables =
64 new List<DataSource.Table>();
65 List<DataSource.Column> columns;
66 columns = new List<DataSource.Column>();
67
68 // Defines the indirect lookup field. (For this to work,
69 // make sure your Contact standard object has a
70 // custom unique, external ID field called github_username.)
71 columns.add(DataSource.Column.indirectLookup(
72 'login', 'Contact', 'github_username__c'));
73
74 columns.add(DataSource.Column.text('id', 255));
75 columns.add(DataSource.Column.text('name',255));
76 columns.add(DataSource.Column.text('company',255));
77 columns.add(DataSource.Column.text('bio',255));
78 columns.add(DataSource.Column.text('followers',255));
79 columns.add(DataSource.Column.text('following',255));
80 columns.add(DataSource.Column.url('html_url'));
81 columns.add(DataSource.Column.url('DisplayUrl'));
82 columns.add(DataSource.Column.text('ExternalId',255));
83 tables.add(DataSource.Table.get('githubProfile','login',
84 columns));
85 return tables;
86 }
87
88 /**
89 * Called to do a full text search and get results from
90 * the external system for SOSL queries and Salesforce
91 * global searches.
92 *
93 * The SearchContext argument represents the query to run
94 * against a table in the external system.
95 *
96 * Returns results for each table that the SearchContext
97 * requested to be searched.
98 **/
99 override global List<DataSource.TableResult> search(
100 DataSource.SearchContext context) {
101 List<DataSource.TableResult> results =
102 new List<DataSource.TableResult>();
103
104 for (Integer i =0;i< context.tableSelections.size();i++) {
105 String entity = context.tableSelections[i].tableSelected;
106
107 // Search usernames
108 String url = 'https://api.github.com/users/'
109 + context.searchPhrase;
110 results.add(DataSource.TableResult.get(
111 true, null, entity, getData(url)));
112 }
113
114 return results;
115 }
116
117 /**
118 * Helper method to parse the data.
119 * The url argument is the URL of the external system.
120 * Returns a list of rows from the external system.
121 **/
122 public List<Map<String, Object>> getData(String url) {
123 String response = getResponse(url);
124
125 // Standardize response string
126 if (!response.contains('"items":')) {
127 if (response.substring(0,1).equals('{')) {
128 response = '[' + response + ']';
129 }
130 response = '{"items": ' + response + '}';
131 }
132
133 List<Map<String, Object>> rows =
134 new List<Map<String, Object>>();
135
136 Map<String, Object> responseBodyMap = (Map<String, Object>)
137 JSON.deserializeUntyped(response);
138
139 /**
140 * Checks errors.
141 **/
142 Map<String, Object> error =
143 (Map<String, Object>)responseBodyMap.get('error');
144 if (error!=null) {
145 List<Object> errorsList =
146 (List<Object>)error.get('errors');
147 Map<String, Object> errors =
148 (Map<String, Object>)errorsList[0];
149 String errorMessage = (String)errors.get('message');
150 throw new
151 DataSource.OAuthTokenExpiredException(errorMessage);
152 }
153
154 List<Object> fileItems =
155 (List<Object>)responseBodyMap.get('items');
156 if (fileItems != null) {
157 for (Integer i=0; i < fileItems.size(); i++) {
158 Map<String, Object> item =
159 (Map<String, Object>)fileItems[i];
160 rows.add(createRow(item));
161 }
162 } else {
163 rows.add(createRow(responseBodyMap));
164 }
165
166 return rows;
167 }
168
169 /**
170 * Helper method to populate the External ID and Display
171 * URL fields on external object records based on the 'id'
172 * value that’s sent by the external system.
173 *
174 * The Map<String, Object> item parameter maps to the data
175 * that represents a row.
176 *
177 * Returns an updated map with the External ID and
178 * Display URL values.
179 **/
180 public Map<String, Object> createRow(
181 Map<String, Object> item){
182 Map<String, Object> row = new Map<String, Object>();
183 for ( String key : item.keySet() ) {
184 if (key == 'login') {
185 row.put('ExternalId', item.get(key));
186 } else if (key=='html_url') {
187 row.put('DisplayUrl', item.get(key));
188 }
189
190 row.put(key, item.get(key));
191 }
192 return row;
193 }
194
195 /**
196 * Helper method to make the HTTP GET call.
197 * The url argument is the URL of the external system.
198 * Returns the response from the external system.
199 **/
200 public String getResponse(String url) {
201 // Perform callouts for production (non-test) results.
202 Http httpProtocol = new Http();
203 HttpRequest request = new HttpRequest();
204 request.setEndPoint(url);
205 request.setMethod('GET');
206 HttpResponse response = httpProtocol.send(request);
207 return response.getBody();
208 }
209}GitHubDataSourceProvider 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 GitHubDataSourceProvider
9 extends DataSource.Provider {
10
11 /**
12 * For simplicity, this example declares that the external
13 * system doesn’t require authentication by returning
14 * AuthenticationCapability.ANONYMOUS as the sole entry
15 * in the list of authentication capabilities.
16 **/
17 override global List<DataSource.AuthenticationCapability>
18 getAuthenticationCapabilities() {
19 List<DataSource.AuthenticationCapability> capabilities =
20 new List<DataSource.AuthenticationCapability>();
21 capabilities.add(
22 DataSource.AuthenticationCapability.ANONYMOUS);
23 return capabilities;
24 }
25
26 /**
27 * Declares the functional capabilities that the
28 * external system supports, in this case
29 * only SOQL queries.
30 **/
31 override global List<DataSource.Capability>
32 getCapabilities() {
33 List<DataSource.Capability> capabilities =
34 new List<DataSource.Capability>();
35 capabilities.add(DataSource.Capability.ROW_QUERY);
36 return capabilities;
37 }
38
39 /**
40 * Declares the associated DataSource.Connection class.
41 **/
42 override global DataSource.Connection getConnection(
43 DataSource.ConnectionParams connectionParams) {
44 return new GitHubDataSourceConnection(connectionParams);
45 }
46}