Newer Version Available

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

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}