Newer Version Available

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

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}