Lightning Connect Custom Adapters: Connecting Anything with Salesforce

34
Lightning Connect Custom Adapters Connecting Anything with Salesforce Lawrence McAlpin Principal Member of Technical Staff [email protected] @lmcalpin

Transcript of Lightning Connect Custom Adapters: Connecting Anything with Salesforce

Page 1: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

Lightning Connect Custom Adapters Connecting Anything with Salesforce

 Lawrence McAlpin  Principal Member of Technical Staff  [email protected]  @lmcalpin  

Page 2: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

 Safe harbor statement under the Private Securities Litigation Reform Act of 1995:

 This presentation may contain forward-looking statements that involve risks, uncertainties, and assumptions. If any such uncertainties materialize or if any of the assumptions proves incorrect, the results of salesforce.com, inc. could differ materially from the results expressed or implied by the forward-looking statements we make. All statements other than statements of historical fact could be deemed forward-looking, including any projections of product or service availability, subscriber growth, earnings, revenues, or other financial items and any statements regarding strategies or plans of management for future operations, statements of belief, any statements concerning new, planned, or upgraded services or technology developments and customer contracts or use of our services.

 The risks and uncertainties referred to above include – but are not limited to – risks associated with developing and delivering new functionality for our service, new products and services, our new business model, our past operating losses, possible fluctuations in our operating results and rate of growth, interruptions or delays in our Web hosting, breach of our security measures, the outcome of any litigation, risks associated with completed and any possible mergers and acquisitions, the immature market in which we operate, our relatively limited operating history, our ability to expand, retain, and motivate our employees and manage our growth, new releases of our service and successful customer deployment, our limited history reselling non-salesforce.com products, and utilization and selling to larger enterprise customers. Further information on potential factors that could affect the financial results of salesforce.com, inc. is included in our annual report on Form 10-K for the most recent fiscal year and in our quarterly report on Form 10-Q for the most recent fiscal quarter. These documents and others containing important disclosures are available on the SEC Filings section of the Investor Information section of our Web site.

 Any unreleased services or features referenced in this or other presentations, press releases or public statements are not currently available and may not be delivered on time or at all. Customers who purchase our services should make the purchase decisions based upon features that are currently available. Salesforce.com, inc. assumes no obligation and does not intend to update these forward-looking statements.

Safe Harbor

Page 3: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

Agenda

  Background on External Data Sources   Custom Adapter Framework Overview

  Demo

  Provider Implementation

  Connection Implementation

  Pagination   Search

  DML

  Q&A

Page 4: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

Background

●  Simplifies integration with external systems

●  No magic ○  data is still remote

●  Ideal for use cases: ○  data is infrequently

accessed (such as archival data)

○  you do not want to present stale data

○  single point of truth

Page 5: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

External Data Sources

• Data must be in a format we understand

▪  OData v2

▪  OData v4

▪  Salesforce

• Apex Custom Adapter Framework

▪  write your own!

• Data must be accessible to Salesforce

Page 6: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

Custom Adapter Framework

Allows you to write your own Lightning Connect adapters

Page 7: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

Custom Adapter Framework

●  Standard Governor limits apply

●  No limit to the number of Apex custom adapter classes you can define

●  Need Lightning Connect license to configure an External Data Source to use the custom adapter

Page 8: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

Demo

Page 9: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

Custom Adapter Framework

●  DataSource.Provider o  describes the capabilities of the external data source o  creates the Connection class

●  DataSource.Connection o  called whenever you import the metadata o  called when you execute SOQL, SOSL, DML or equivalent UI interactions

Page 10: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

DataSource.Provider

global class DummyDataSourceProvider extends DataSource.Provider { override global List<DataSource.Capability> getCapabilities() { List<DataSource.Capability> capabilities = new List<DataSource.Capability>(); capabilities.add(DataSource.Capability.ROW_QUERY); capabilities.add(DataSource.Capability.SEARCH); return capabilities; } override global List<DataSource.AuthenticationCapability> getAuthenticationCapabilities() { List<DataSource.AuthenticationCapability> capabilities = new List<DataSource.AuthenticationCapability>(); capabilities.add(DataSource.AuthenticationCapability.ANONYMOUS); return capabilities; } override global DataSource.Connection getConnection(DataSource.ConnectionParams connectionParams) { return new DummyDataSourceConnection(connectionParams); } }

Page 11: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

DataSource.Provider

override global List<DataSource.Capability> getCapabilities()

  ROW_QUERY   ROW_CREATE   ROW_UPDATE   ROW_DELETE   SEARCH   REQUIRE_ENDPOINT   REQUIRE_HTTPS   QUERY_TOTAL_SIZE   QUERY_PAGINATION_SERVER_DRIVEN

Page 12: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

DataSource.Provider

getAuthenticationCapabilities ANONYMOUS BASIC CERTIFICATE OAUTH

Page 13: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

DataSource.Provider

override global DataSource.Connection getConnection(DataSource.ConnectionParams connectionParams) { return new DummyDataSourceConnection(connectionParams); }

ConnectionParams properties

String username

String password String oauthToken

AuthenticationProtocol protocol String endpoint

String repository IdentityType principalType

String certificateName

 

 

Page 14: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

Callouts

Data may be retrieved using HTTP or Web service callouts ●  Authentication must be handled manually

o  throw OAuthTokenExpiredException to refresh the stored access token o  all callout endpoints need to be registered in Remote Site Settings

HttpRequest req = new HttpRequest(); req.setEndpoint('http://www.wherever.com'); req.setMethod('GET'); if (protocol == DataSource.AuthenticationProtocol.PASSWORD) { String username = connectionParams.username; String password = connectionParams.password; Blob headerValue = Blob.valueOf(username + ':' + password); String authorizationHeader = 'BASIC ' + EncodingUtil.base64Encode(headerValue); req.setHeader('Authorization', authorizationHeader); } else if (protocol == DataSource.AuthenticationProtocol.OAUTH) { req.setHeader('Authorization', 'Bearer ' + connectionParams.oauthToken); } Http http = new Http(); HTTPResponse res = http.send(req); if (res.getStatusCode() == 401) throw new OAuthTokenExpiredException();

Page 15: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

Callouts with Named Credentials

Named Credentials are more flexible but require additional setup ●  no direct access to credentials ●  no need to add to Remote Site

Settings HttpRequest req = new HttpRequest(); req.setEndpoint(‘callout:test’); req.setMethod('GET'); Http http = new Http(); HTTPResponse res = http.send(req);

Page 16: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

Callouts with Named Credentials

Merge fields {!$CREDENTIAL.xxx} ●  USERNAME ●  PASSWORD ●  OAUTHTOKEN ●  AUTHORIZATIONMETHOD (BASIC, OAUTH) ●  AUTHORIZATIONHEADERVALUE (Base64 encoded username+password or Oauth token) ●  OAUTHCONSUMERKEY

// Concur expects OAuth to prefix the access token, instead of Bearer req.setHeader(‘Authorization’, ‘OAuth {!$Credential.OAuthToken}’); // non-standard authentication req.setHeader(‘X-Username’, ‘{!$Credential.UserName}’); req.setHeader(‘X-Password’, ‘{!$Credential.Password}’); // you can also use it in the body req.setBody(‘Dear {!$Credential.UserName}, I am a Salesforce Prince and as a Prince of Salesforce I naturally own a metric crap ton of RSUs. If you send me 10,000 of teh bitcoins now I will deliver my stock to you as it vests which wil be totes winwin.’);

Page 17: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

DataSource.Connection

override global List<DataSource.Table> sync() enumerates the list of Tables that this data source knows about

override global DataSource.TableResult query(DataSource.QueryContext c)

called when executing SOQL or visiting the List or Details pages in the UI

override global List<DataSource.TableResult> search(DataSource.SearchContext c)

called when executing SOSL or using the search functions in the UI

override global List<DataSource.UpsertResult> upsertRows(DataSource.UpsertContext c)

called when executing insert or update DML; also called when editing a record in the UI

override global List<DataSource.DeleteResult> deleteRows(DataSource.DeleteContext c)

called when executing delete DML; also called when deleting a record in the UI

Page 18: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

Sync - DataSource.Table override global List<DataSource.Table> sync() {

List<DataSource.Table> tables = new List<DataSource.Table>();

List<DataSource.Column> columns;

columns = new List<DataSource.Column>();

// next slide...

tables.add(DataSource.Table.get('Looper', 'Name', columns));

return tables;

}

Page 19: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

Sync - DataSource.Column columns = new List<DataSource.Column>();

columns.add(DataSource.Column.text('ExternalId', 255));

columns.add(DataSource.Column.url('DisplayUrl'));

columns.add(DataSource.Column.text('Name', 255));

columns.add(DataSource.Column.number('NumberOfEmployees', 18, 0));

Page 20: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

Query override global DataSource.TableResult query(DataSource.QueryContext c) { HttpRequest req = prepareCallout(c); List<Map<String,Object>> rows = getData(req); // don’t forget the standard fields, especially ExternalId for (Map<String,Object> row : rows) { row.put('ExternalId', row.get(‘key’)); row.put('DisplayUrl', connectionParams.url + ‘/record/’ + row.get(‘key’)); rows.add(row); } return DataSource.TableResult.get(c,rows); }

QueryContext properties TableSelection tableSelection Integer offset Integer maxResults TableSelection properties string tableSelected List<ColumnSelection> columnsSelected Filter filter List<Order> order TableResult properties boolean success String errorMessage String tableName List<Map<String,Object>> rows integer totalSize String queryMoreToken

Page 21: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

Query Filters /** Compound types **/

NOT_,

AND_,

OR_, private string getSoqlFilter(string query, DataSource.Filter filter) { if (filter == null) { return query; } DataSource.FilterType type = filter.type; List<Map<String,Object>> retainedRows = new List<Map<String,Object>>(); if (type == DataSource.FilterType.NOT_) { DataSource.Filter subfilter = filter.subfilters.get(0); return ‘NOT ‘ + getSoqlFilterExpression(subfilter); } else if (type == DataSource.FilterType.AND_) { return join('AND', filter.subfilters); } else if (type == DataSource.FilterType.OR_) { return join('OR', filter.subfilters); } return getSoqlFilterExpression(filter); }

/** Simple comparative types **/ EQUALS, NOT_EQUALS, LESS_THAN, GREATER_THAN,

private string getSoqlFilterExpression(DataSource.Filter filter) { string op; string columnName = filter.columnName; object expectedValue = filter.columnValue; if (filter.type == DataSource.FilterType.EQUALS) { op = '='; } else if (filter.type == DataSource.FilterType.NOT_EQUALS) { op = '<>'; } else if (filter.type == DataSource.FilterType.LESS_THAN) { op = '<'; } else if (filter.type == DataSource.FilterType.GREATER_THAN) { op = '>'; } else if (filter.type == DataSource.FilterType.LESS_THAN_OR_EQUAL_TO) { op = '<='; } else if (filter.type == DataSource.FilterType.GREATER_THAN_OR_EQUAL_TO) { op = '>='; } else if (filter.type == DataSource.FilterType.STARTS_WITH) { return mapColumnName(columnName) + ' LIKE \'' + String.valueOf(expectedValue) + '%\''; } else if (filter.type == DataSource.FilterType.ENDS_WITH) { return mapColumnName(columnName) + ' LIKE \'%' + String.valueOf(expectedValue) + '\''; } else { throwException('DF15SpeakerWasLazyException: unimplemented filter type' + filter.type); } return mapColumnName(columnName) + ' ' + op + ' ' + wrapValue(expectedValue); }

LESS_THAN_OR_EQUAL_TO, GREATER_THAN_OR_EQUAL_TO, STARTS_WITH, ENDS_WITH, CONTAINS, LIKE_

Page 22: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

QueryMore

  Can’t return all results in a single batch!   Not all external data sources handle pagination the same way

•  page number •  token •  limit, offset

  Three strategies to handle pagination: •  client driven, known total size •  client driven, unknown total size •  server driven

  Driven by comabination of capabilities: •  QUERY_TOTAL_SIZE •  QUERY_SERVER_DRIVEN_PAGINATION

Page 23: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

QueryMore - Client Driven, Known Total Size

  Provider class declares QUERY_TOTAL_SIZE   /services/data/v35.0/query?q=SELECT...FROM+Xds__x ⇒

  query •  QueryContext maxResults = 1000 •  TableResult should have 1000 records as requested!!!

  API call returns { "totalSize" => 1500, "done" => false, "nextRecordsUrl" => "/services/data/v35.0/query/01gxx000000B4WAAA0-1000", "records" => [ … 1000 RECORDS … ] }

  queryMore call to /services/data/v35.0/query/xxx ⇒   query

•  QueryContext maxResults = 1000, and offset = 1000 •  TableResult should have 500 records

  API call returns { "totalSize" => 1500, "done" => true, "records" => [ … 500 RECORDS … ] }

Page 24: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

QueryMore - Client Driven, Unknown Query Result Size

  Default strategy used when provider does not support QUERY_TOTAL_SIZE or QUERY_SERVER_DRIVEN_PAGINATION   /services/data/v35.0/query?q=SELECT...FROM+Xds__x ⇒

  query •  QueryContext maxResults = 1001 •  TableResult should return 1001 records as requested!!!

  SFDC API call returns { "totalSize" => -1, "done" => false, "nextRecordsUrl" => "/services/data/v35.0/query/01gxx000000B4WAAA0-1000", "records" => [ … 1000 RECORDS … ] }

  queryMore call to /services/data/v35.0/query/xxx ⇒   query

•  QueryContext maxResults = 1001, and offset = 1000 •  TableResult should have 500 records

  API call returns { "totalSize" => 1500, "done" => true, "records" => [ … 500 RECORDS … ] }

Page 25: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

QueryMore - Server Driven

  Provider class declares QUERY_SERVER_DRIVEN_PAGINATION   /services/data/v35.0/query?q=SELECT...FROM+Xds__x ⇒

  query •  QueryContext maxResults = 0 •  TableResult should have however many records you want •  TableResult must provide a queryMoreToken

  API call returns the following if queryMoreToken is not null { "totalSize" => -1 or 1500, # depends on QUERY_TOTAL_SIZE support "done" => false, "nextRecordsUrl" => "/services/data/v35.0/query/01gxx000000B4WAAA0-1000", "records" => [ … ??? RECORDS … ] }

  queryMore call to /services/data/v35.0/query/xxx ⇒   query

•  QueryContext queryMoreToken will be set to the token previously supplied •  TableResult should have however many records you want

  API call returns the following when queryMoreToken is null { "totalSize" => 1500, "done" => true, "records" => [ … ??? RECORDS … ] }

Page 26: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

Search

●  SOSL and Search UI operations invoke the search method on your Connection class ●  Multiple tables for a single connector may be searched in a single call ●  Display URL is intended to point to the search results in the external system

Page 27: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

 SearchContext properties  List<TableSelection> tableSelections  String searchPhrase global static List<DataSource.TableResult> search(SearchContext c) {

List<DataSource.TableResult> results = new List<DataSource.TableResult>(); for (DataSource.TableSelection tableSelection : c.tableSelections) {

QueryContext ctx = new QueryContext();

ctx.tableSelection = tableSelection; Table table = c.getTableMetadata(ctx.tableSelection);

tableSelection.filter = new Filter(FilterType.CONTAINS, ctx.tableSelection.tableSelected,

table.nameColumn, c.searchPhrase);

results.add(query(ctx)); }

return results;

}

Search

Page 28: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

Inserts, Updates, Deletes

Requires DML capabilities in DataSource.Provider ●  ROW_CREATE

●  ROW_UPDATE

●  ROW_DELETE

Requires ID mapping Requires Allow Create, Edit, and Delete selection on External Data Source

Page 29: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

Insert, Updates override global List<DataSource.UpsertResult> upsertRows(DataSource.UpsertContext c) { List<DataSource.UpsertResult> results = new List<DataSource.UpsertResult>(); List<Map<String,Object>> rows = c.rows; for (Map<String,Object> row : rows) { String externalId = String.valueOf(row.get('ExternalId')); // insert or update record in the external system boolean success = // insert or update record if (success) { results.add(DataSource.UpsertResult.success(id)); } else { results.add(DataSource.UpsertResult.failure(id, 'An error occurred updating this record')); } } return results; }

UpsertContext properties String tableSelected List<Map<string,object>> rows

UpsertResult properties Boolean success String errorMessage String externalId

Page 30: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

Deletes override global List<DataSource.DeleteResult> deleteRows(DataSource.DeleteContext c) {

Set<Id> externalIds = new Set<Id>();

List<DataSource.DeleteResult> results = new List<DataSource.DeleteResult>();

for (String externalId : c.externalIds) {

boolean success = // delete record in external system

if (result.success) {

results.add(DataSource.DeleteResult.success(id));

} else {

results.add(DataSource.DeleteResult.failure(id, 'An error occurred updating this record'));

}

}

return results;

}

DeleteContext properties String tableSelected List<String> externalIds

DeleteResult properties Boolean success String errorMessage String externalId

Page 31: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

DML

●  New Database methods used for external objects (only) ○  insertAsync ○  updateAsync ○  deleteAsync

●  DML operations are asynchronous ●  call getAsyncResult later to get results

Order__x x = new Order__x(); Database.SaveResult locator = Database.insertAsync(x); if (!locator.isSuccess() && locator.getAsyncLocator() != null) { // save was queued up for execution, when the result is ready, do some additional processing completeOrderCreation(asyncLocator); } // must be in another transaction!!! @future public void completeOrderCreation(String asyncLocator) { Database.SaveResult sr = Database.getAsyncResult(asyncLocator); if (sr.isSuccess()) { … } }

Page 32: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

DML Callbacks

●  Callbacks to handle post-save processing global class XdsSaveCallback extends DataSource.AsyncSaveCallback { virtual global void processSave(Database.SaveResult sr) { if (sr.isSuccess()) { … } } } XdsSaveCallback cb = new XdsSaveCallback(); Order__x x = new Order__x(); Database.insertAsync(x, cb);

Page 33: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

DML Callbacks

●  Callbacks to handle post-delete processing global class XdsDeleteCallback extends DataSource.AsyncDeleteCallback { virtual global void processDelete(Database.DeleteResult dr) {} } XdsDeleteCallback cb = new XdsDeleteCallback(); Xds__x x = [SELECT Id FROM Xds__x WHERE ExternalId = ‘...’]; Database.deleteAsync(x, cb);

Page 34: Lightning Connect Custom Adapters: Connecting Anything with Salesforce

Lawrence McAlpin Lawrence McAlpin Principal Member of Technical Staff

[email protected] @lmcalpin