Lightning Connect is a feature in the App Cloud that enables our customers to expose data outside of Salesforce as custom objects. Join us as we introduce an SDK that allows Force.com developers to create their own custom connectors.
2. 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
4. 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
5. 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
7. 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
9. 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
10. 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);
}
}
14. 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();
15. 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);
16. 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.’);
17. 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
18. 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;
}
21. 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_
22. 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
23. 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 … ]
}
24. 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 … ]
}
25. 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 … ]
}
26. 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
27. 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
28. 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
29. 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
30. 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
31. 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()) { … }
}
32. 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);
33. 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);