Note: This release is in preview. Features described here don’t become generally available until the latest general availability date that Salesforce announces for this release. Before then, and where features are noted as beta, pilot, or developer preview, we can’t guarantee general availability within any particular time frame or at all. Make your purchase decisions only on the basis of generally available products and features.
Stack Overflow Custom Adapter for Salesforce Connect
This example illustrates how to support external lookup relationships and multiple
tables. An external lookup relationship links a child standard, custom, or external object
to a parent external object. Each table can become an external object in the Salesforce
org.
For this example to work, create a custom field on the Contact standard object. Name the custom field “github_username” and select the External ID and Unique attributes.
StackOverflowDataSourceConnection Class
1/**
2 * Defines the connection to Stack Exchange API v2.2 to support
3 * querying of Stack Overflow users (stackoverflowUser)
4 * and posts (stackoverflowPost).
5 * Extends the DataSource.Connection class to enable
6 * Salesforce to sync the external system’s schema
7 * and to handle queries of the external data.
8 **/
9global class StackOverflowDataSourceConnection extends
10 DataSource.Connection {
11 private DataSource.ConnectionParams connectionInfo;
12
13 /**
14 * Constructor for StackOverflowDataSourceConnection
15 **/
16 global StackOverflowDataSourceConnection(
17 DataSource.ConnectionParams connectionInfo) {
18 this.connectionInfo = connectionInfo;
19 }
20
21 /**
22 * Defines the schema for the external system.
23 * Called when the administrator clicks “Validate and Sync”
24 * in the user interface for the external data source.
25 **/
26 override global List<DataSource.Table> sync() {
27 List<DataSource.Table> tables =
28 new List<DataSource.Table>();
29
30 // Defines columns for the table of Stack OverFlow posts
31 List<DataSource.Column> postColumns =
32 new List<DataSource.Column>();
33
34 // Defines the external lookup field.
35 postColumns.add(DataSource.Column.externalLookup(
36 'owner_id', 'stackoverflowUser__x'));
37 postColumns.add(DataSource.Column.text('title', 255));
38 postColumns.add(DataSource.Column.text('view_count', 255));
39 postColumns.add(DataSource.Column.text('question_id',255));
40 postColumns.add(DataSource.Column.text('creation_date',255));
41 postColumns.add(DataSource.Column.text('score',255));
42 postColumns.add(DataSource.Column.url('link'));
43 postColumns.add(DataSource.Column.url('DisplayUrl'));
44 postColumns.add(DataSource.Column.text('ExternalId',255));
45
46 tables.add(DataSource.Table.get('stackoverflowPost','title',
47 postColumns));
48
49 // Defines columns for the table of Stack OverFlow users
50 List<DataSource.Column> userColumns =
51 new List<DataSource.Column>();
52 userColumns.add(DataSource.Column.text('user_id', 255));
53 userColumns.add(DataSource.Column.text('display_name', 255));
54 userColumns.add(DataSource.Column.text('location',255));
55 userColumns.add(DataSource.Column.text('creation_date',255));
56 userColumns.add(DataSource.Column.url('website_url',255));
57 userColumns.add(DataSource.Column.text('reputation',255));
58 userColumns.add(DataSource.Column.url('link'));
59 userColumns.add(DataSource.Column.url('DisplayUrl'));
60 userColumns.add(DataSource.Column.text('ExternalId',255));
61
62 tables.add(DataSource.Table.get('stackoverflowUser',
63 'Display_name', userColumns));
64
65 return tables;
66 }
67
68 /**
69 * Called to query and get results from the external
70 * system for SOQL queries, list views, and detail pages
71 * for an external object that’s associated with the
72 * external data source.
73 *
74 * The QueryContext argument represents the query to run
75 * against a table in the external system.
76 *
77 * Returns a list of rows as the query results.
78 **/
79 override global DataSource.TableResult query(
80 DataSource.QueryContext context) {
81 DataSource.Filter filter = context.tableSelection.filter;
82 String url;
83
84 // Sets the URL to query Stack Overflow posts
85 if (context.tableSelection.tableSelected
86.equals('stackoverflowPost')) {
87 if (filter != null) {
88 String thisColumnName = filter.columnName;
89 if (thisColumnName != null &&
90 thisColumnName.equals('ExternalId'))
91 url = 'https://api.stackexchange.com/2.2/'
92 + 'questions/' + filter.columnValue
93 + '?order=desc&sort=activity'
94 + '&site=stackoverflow';
95 else
96 url = 'https://api.stackexchange.com/2.2/'
97 + 'questions'
98 + '?order=desc&sort=activity'
99 + '&site=stackoverflow';
100 } else {
101 url = 'https://api.stackexchange.com/2.2/'
102 + 'questions'
103 + '?order=desc&sort=activity'
104 + '&site=stackoverflow';
105 }
106 // Sets the URL to query Stack Overflow users
107 } else if (context.tableSelection.tableSelected
108.equals('stackoverflowUser')) {
109 if (filter != null) {
110 String thisColumnName = filter.columnName;
111 if (thisColumnName != null &&
112 thisColumnName.equals('ExternalId'))
113 url = 'https://api.stackexchange.com/2.2/'
114 + 'users/' + filter.columnValue
115 + '?order=desc&sort=reputation'
116 + '&site=stackoverflow';
117 else
118 url = 'https://api.stackexchange.com/2.2/'
119 + 'users' +
120'?order=desc&sort=reputation&site=stackoverflow';
121 } else {
122 url = 'https://api.stackexchange.com/2.2/'
123 + 'users' + '?order=desc&sort=reputation'
124 + '&site=stackoverflow';
125 }
126 }
127
128 /**
129 * Filters, sorts, and applies limit and offset clauses.
130 **/
131 List<Map<String, Object>> rows =
132 DataSource.QueryUtils.process(context, getData(url));
133 return DataSource.TableResult.get(true, null,
134 context.tableSelection.tableSelected, rows);
135 }
136
137 /**
138 * Helper method to parse the data.
139 * The url argument is the URL of the external system.
140 * Returns a list of rows from the external system.
141 **/
142 public List<Map<String, Object>> getData(String url) {
143 String response = getResponse(url);
144
145 List<Map<String, Object>> rows =
146 new List<Map<String, Object>>();
147
148 Map<String, Object> responseBodyMap = (Map<String, Object>)
149 JSON.deserializeUntyped(response);
150
151 /**
152 * Checks errors.
153 **/
154 Map<String, Object> error =
155 (Map<String, Object>)responseBodyMap.get('error');
156 if (error!=null) {
157 List<Object> errorsList =
158 (List<Object>)error.get('errors');
159 Map<String, Object> errors =
160 (Map<String, Object>)errorsList[0];
161 String errorMessage = (String)errors.get('message');
162 throw new
163 DataSource.OAuthTokenExpiredException(errorMessage);
164 }
165
166 List<Object> fileItems=
167 (List<Object>)responseBodyMap.get('items');
168 if (fileItems != null) {
169 for (Integer i=0; i < fileItems.size(); i++) {
170 Map<String, Object> item =
171 (Map<String, Object>)fileItems[i];
172 rows.add(createRow(item));
173 }
174 } else {
175 rows.add(createRow(responseBodyMap));
176 }
177
178 return rows;
179 }
180
181 /**
182 * Helper method to populate the External ID and Display
183 * URL fields on external object records based on the 'id'
184 * value that’s sent by the external system.
185 *
186 * The Map<String, Object> item parameter maps to the data
187 * that represents a row.
188 *
189 * Returns an updated map with the External ID and
190 * Display URL values.
191 **/
192 public Map<String, Object> createRow(
193 Map<String, Object> item) {
194 Map<String, Object> row = new Map<String, Object>();
195 for ( String key : item.keySet() ) {
196 if (key.equals('question_id') || key.equals('user_id')) {
197 row.put('ExternalId', item.get(key));
198 } else if (key.equals('link')) {
199 row.put('DisplayUrl', item.get(key));
200 } else if (key.equals('owner')) {
201 Map<String, Object> ownerMap =
202 (Map<String, Object>)item.get(key);
203 row.put('owner_id', ownerMap.get('user_id'));
204 }
205
206 row.put(key, item.get(key));
207 }
208 return row;
209 }
210
211 /**
212 * Helper method to make the HTTP GET call.
213 * The url argument is the URL of the external system.
214 * Returns the response from the external system.
215 **/
216 public String getResponse(String url) {
217 // Perform callouts for production (non-test) results.
218 Http httpProtocol = new Http();
219 HttpRequest request = new HttpRequest();
220 request.setEndPoint(url);
221 request.setMethod('GET');
222 HttpResponse response = httpProtocol.send(request);
223 return response.getBody();
224 }
225}StackOverflowPostDataSourceProvider 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 StackOverflowPostDataSourceProvider
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
45 StackOverflowDataSourceConnection(connectionParams);
46 }
47}