Apex 代码可以通过任何 Salesforce Connect 适配器访问外部对象数据。使用 Apex Connector Framework,用于为 Salesforce Connect 开发自定义适配器。自定义适配器可以从外部系统检索数据并在本地合成数据。 Salesforce Connect 在 Salesforce 外部对象中表示该数据,使用户和 Lightning Platform 可与存储在 Salesforce 外部的数据无缝交互 组织。
- Salesforce Connect 外部对象的 Apex 注意事项 Apex 代码可以通过任何 Salesforce Connect 适配器访问外部对象
数据,但存在一些要求和限制。 - 可写的外部对象 默认情况下,外部对象
是只读的,但您可以使它们可写。这样,Salesforce 用户和 API 就可以通过与组织内的外部对象进行交互来创建、更新和删除存储在组织外部的数据。例如,用户可以查看驻留在 SAP 系统中与 Salesforce 中的帐户关联的所有订单。然后,在不离开 Salesforce 用户界面的情况下,他们可以下新订单或路由现有订单。相关数据在SAP系统中自动创建或更新。 - 外部变更数据捕获打包和测试 您可以在托管包中分发外部变更数据捕获组件,包括用于测试
Apex 触发器的框架。特殊行为和限制适用于包装和包装安装。 - Apex 连接器框架
入门 要开始使用 Salesforce Connect 的第一个自定义适配器,请创建两个 Apex 类:一个用于扩展类,另一个用于扩展该类。DataSource.ConnectionDataSource.Provider - 关于 Apex 连接器框架的关键概念 命名空间提供 Apex 连接器框架
的类。使用 Apex 连接器框架为 Salesforce Connect 开发自定义适配器。然后,通过 Salesforce Connect 自定义适配器将您的 Salesforce 组织连接到任何位置的任何数据。DataSource - Apex Connector 框架
的注意事项 了解使用 Apex Connector Framework 创建 Salesforce Connect 自定义适配器的限制和注意事项。 - Apex 连接器框架示例 这些示例
说明如何使用 Apex 连接器框架为 Salesforce Connect 创建自定义适配器。
Salesforce Connect 外部的 Apex 注意事项 对象
Apex 代码可以通过任何 Salesforce Connect 适配器访问外部对象数据,但某些 适用要求和限制。
- 这些功能不适用于外部对象。
- Apex 托管共享
- Apex 触发器(但是,您可以针对外部更改数据创建触发器 从 OData 4.0 连接捕获事件。
- 当开发人员使用 Apex 操作外部对象记录时,异步 计时和活动后台队列可最大程度地减少潜在的保存冲突。一个 专门的 Apex 方法和关键字集可处理潜在的计时问题 具有写入执行功能。Apex 还允许您检索删除和 更新插入操作。使用 BackgroundOperation 对象监视作业进度 用于通过 API 或 SOQL 进行写入操作。
- Database.insertAsync()方法不能在 门户用户的上下文,即使门户用户是社区成员也是如此。 要通过 Apex 添加外部对象记录,请使用方法。Database.insertImmediate()
重要
针对外部数据源运行可迭代的批处理 Apex 作业时, 作业运行时,外部记录存储在 Salesforce 中。数据将从 作业完成时的存储,无论作业是否成功。没有外部数据 在使用 .Database.QueryLocator
- 如果使用批处理 Apex 来访问外部对象 通过适用于 Salesforce 的 OData 适配器:Database.QueryLocator
可写的外部对象
默认情况下,外部对象是只读的,但您可以使它们可写。这样做 允许 Salesforce 用户和 API 创建、更新和删除存储在组织外部的数据 通过与组织内的外部对象进行交互。例如,用户可以看到所有 驻留在 SAP 系统中且与 Salesforce 中的帐户关联的订单。然后 在不离开 Salesforce 用户界面的情况下,他们可以下新订单或路由 现有订单。相关数据在SAP中自动创建或更新 系统。
对外部数据的访问取决于 Salesforce 与外部数据之间的连接 存储数据的系统。网络延迟和外部可用性 系统可能会在外部执行 Apex 写入或删除操作时引入计时问题 对象。
由于这些连接的复杂性,Apex 无法执行标准、或操作 在外部对象上。相反,Apex 提供了一组专门的数据库方法和 关键字来解决写入执行的潜在问题。DML 插入、更新、 对外部对象的创建和删除操作是异步的或执行的 当满足特定标准时。insert()update()create()此示例使用该方法将新订单异步插入数据库表中。它返回一个包含唯一标识符的对象 用于插入物 工作。
Database.insertAsync()SaveResult
public void createOrder () {
SalesOrder__x order = new SalesOrder__x ();
Database.SaveResult sr = Database.insertAsync (order);
if (! sr.isSuccess ()) {
String locator = Database.getAsyncLocator ( sr );
completeOrderCreation(locator);
}
}
注意
通过 Salesforce 用户界面或 API 对外部对象执行写入 是同步的,其工作方式与标准对象和自定义对象相同。
您可以异步对外部对象执行以下 DML 操作 或基于条件:插入记录、更新记录、更新插入记录或删除记录。 使用命名空间中的类获取 异步作业的唯一标识符,或检索 upsert 的结果列表, 删除或保存操作。DataSource
在外部对象上启动 Apex 方法时,将调度作业并将其放置在 后台作业队列。使用 BackgroundOperation 对象可以查看作业状态 用于通过 API 或 SOQL 进行写入操作。监视作业进度和相关错误 组织、提取统计信息、处理批处理作业,或查看指定中发生的错误数 时间段。
有关使用信息和示例,请参阅数据库命名空间和数据源命名空间。
外部变更数据捕获、打包和测试
您可以在托管包中分发外部变更数据捕获组件, 包括用于测试 Apex 触发器的框架。特殊行为和限制 适用于包装和包装安装。
- 通过选择 将外部更改数据跟踪组件包含在托管包中 Apex 类组件类型列表中的测试。触发器、测试、外部数据 源、外部对象和其他相关资源被引入到包中 分配。
- 证书不可打包。如果打包外部数据源 指定证书,请确保订阅者组织具有有效的证书 同名。
为了帮助您测试外部变更数据捕获触发的 Apex 类,下面是一个单元 测试触发器对模拟外部更改做出反应的代码示例。
例 触发
trigger OnExternalProductChangeEventForAudit on Products__ChangeEvent (after insert) {
if (Trigger.new.size() != 1) return;
for (Products__ChangeEvent event: Trigger.new) {
Product_Audit__c audit = new Product_Audit__c();
audit.Name = 'ProductChangeOn' + event.ExternalId;
audit.Change_Type__c = event.ChangeEventHeader.getChangeType();
audit.Audit_Price__c = event.Price__c;
audit.Product_Name__c = event.Name__c;
insert(audit);
}
}
顶点测试
@isTest
public class testOnExternalProductChangeEventForAudit {
static testMethod void testExternalProductChangeTrigger() {
// Create Change Event
Products__ChangeEvent event = new Products__ChangeEvent();
// Set Change Event Header Fields
EventBus.ChangeEventHeader header = new EventBus.ChangeEventHeader();
header.changeType='CREATE';
header.entityName='Products__x';
header.changeOrigin='here';
header.transactionKey = 'some';
header.commitUser = 'me';
event.changeEventHeader = header;
event.put('ExternalId', 'ParentExternalId');
event.put('Price__c', 5500);
event.put('Name__c', 'Coat');
// Publish the event to the EventBus
EventBus.publish(event);
Test.getEventBus().deliver();
// Perform assertion that the trigger was run
Product_Audit__c audit = [SELECT name, Audit_Price__c, Product_Name__c FROM Product_Audit__c WHERE name = : 'ProductChangeOn'+ event.ExternalId LIMIT 1];
System.assertEquals('ProductChangeOn'+ event.ExternalId, audit.Name);
System.assertEquals(5500, audit.Audit_Price__c);
System.assertEquals('Coat', audit.Product_Name__c);
}
}
Apex Connector 框架入门
要开始使用 Salesforce Connect 的第一个自定义适配器,请创建两个 Apex 类:一个用于扩展类,一个用于扩展类。
DataSource.ConnectionDataSource.Provider
让我们逐步完成示例自定义适配器的代码。
- 创建示例 DataSource.Connection 类 首先,创建一个类
,使 Salesforce 能够获取外部系统的架构,并处理外部数据的查询和搜索。DataSource.Connection - 创建示例 DataSource.Provider 类
现在,您需要一个类来扩展和覆盖 中的几个方法。DataSource.Provider - 设置 Salesforce Connect 以使用您的自定义适配器 创建 和 类后,Salesforce Connect 自定义适配器
将在“设置”中可用。DataSource.ConnectionDataSource.Provider
创建示例 DataSource.Connection 类
首先,创建一个类 使 Salesforce 能够获取外部系统的架构并处理查询,以及 搜索外部数据。
DataSource.Connection
global class SampleDataSourceConnection
extends DataSource.Connection {
global SampleDataSourceConnection(DataSource.ConnectionParams
connectionParams) {
}
// Add implementation of abstract methods
// ...
该类包含这些方法。
DataSource.Connection
- 查询
- 搜索
- 同步
- upsert行
- deleteRows
同步
当管理员单击外部数据源上的“验证并同步”按钮时,将调用该方法 详情页面。它返回描述结构元数据的信息 外部系统。sync()
注意
更改类的方法不会 自动重新同步任何外部对象。syncDataSource.Connection
// ...
override global List<DataSource.Table> sync() {
List<DataSource.Table> tables =
new List<DataSource.Table>();
List<DataSource.Column> columns;
columns = new List<DataSource.Column>();
columns.add(DataSource.Column.text('Name', 255));
columns.add(DataSource.Column.text('ExternalId', 255));
columns.add(DataSource.Column.url('DisplayUrl'));
tables.add(DataSource.Table.get('Sample', 'Title',
columns));
return tables;
}
// ...
查询
当 SOQL 查询时调用该方法 在外部对象上执行。将自动生成 SOQL 查询,并 当用户在 Salesforce的。是 始终只针对单个表。queryDataSource.QueryContext
此示例自定义适配器使用类中的帮助程序方法对结果进行筛选和排序,并根据 SOQL 查询中的 and 子句。DataSource.QueryUtilsWHEREORDER BY
类及其 帮助程序方法可以在 Salesforce 组织内本地处理查询结果。这 提供课程是为了您的方便,以简化您的 用于初始测试的 Salesforce Connect 自定义适配器。但是,该类及其方法 不支持在使用标注进行检索的生产环境中使用 来自外部系统的数据。完成对外部的过滤和排序 系统,然后再将查询结果发送到 Salesforce。如果可能,请使用 服务器驱动的分页或其他技术,让外部系统确定 根据查询中的 limit 和 offset 子句设置适当的数据子集。DataSource.QueryUtilsDataSource.QueryUtils
// ...
override global DataSource.TableResult query(
DataSource.QueryContext context) {
if (context.tableSelection.columnsSelected.size() == 1 &&
context.tableSelection.columnsSelected.get(0).aggregation ==
DataSource.QueryAggregation.COUNT) {
List<Map<String,Object>> rows = getRows(context);
List<Map<String,Object>> response =
DataSource.QueryUtils.filter(context, getRows(context));
List<Map<String, Object>> countResponse =
new List<Map<String, Object>>();
Map<String, Object> countRow =
new Map<String, Object>();
countRow.put(
context.tableSelection.columnsSelected.get(0).columnName,
response.size());
countResponse.add(countRow);
return DataSource.TableResult.get(context,
countResponse);
} else {
List<Map<String,Object>> filteredRows =
DataSource.QueryUtils.filter(context, getRows(context));
List<Map<String,Object>> sortedRows =
DataSource.QueryUtils.sort(context, filteredRows);
List<Map<String,Object>> limitedRows =
DataSource.QueryUtils.applyLimitAndOffset(context,
sortedRows);
return DataSource.TableResult.get(context, limitedRows);
}
}
// ...
搜索
该方法由 SOSL 查询调用 外部对象或当用户执行 Salesforce 全局搜索时,该搜索还 搜索外部对象。由于搜索可以针对多个对象进行联合, 可以有 已选择多个表。但是,在此示例中,自定义适配器知道 只有一张表。searchDataSource.SearchContext
// ...
override global List<DataSource.TableResult> search(
DataSource.SearchContext context) {
List<DataSource.TableResult> results =
new List<DataSource.TableResult>();
for (DataSource.TableSelection tableSelection :
context.tableSelections) {
results.add(DataSource.TableResult.get(tableSelection,
getRows(context)));
}
return results;
}
// ...
以下是 helper 方法 搜索示例调用以从外部系统获取行值。该方法利用其他帮助程序 方法:
getRowsgetRows
- makeGetCallout向 外部系统。
- foundRow根据 标注结果中的值。该方法用于对 返回的字段值,例如更改字段名称或修改字段 价值。foundRow
这些方法不包含在此代码片段中,但在完整示例中可用 包含在连接类中。通常,过滤器 from 或将用于减少结果 set,但为简单起见,此示例不使用 context 对象。SearchContextQueryContext
// ...
// Helper method to get record values from the external system for the Sample table.
private List<Map<String, Object>> getRows () {
// Get row field values for the Sample table from the external system via a callout.
HttpResponse response = makeGetCallout();
// Parse the JSON response and populate the rows.
Map<String, Object> m = (Map<String, Object>)JSON.deserializeUntyped(
response.getBody());
Map<String, Object> error = (Map<String, Object>)m.get('error');
if (error != null) {
throwException(string.valueOf(error.get('message')));
}
List<Map<String,Object>> rows = new List<Map<String,Object>>();
List<Object> jsonRows = (List<Object>)m.get('value');
if (jsonRows == null) {
rows.add(foundRow(m));
} else {
for (Object jsonRow : jsonRows) {
Map<String,Object> row = (Map<String,Object>)jsonRow;
rows.add(foundRow(row));
}
}
return rows;
}
// ...
upsert行
该方法在以下情况下调用 创建或更新外部对象记录。您可以创建或更新外部 通过 Salesforce 用户界面或 DML 进行对象记录。以下示例 提供该方法的示例实现。该示例使用传入的表来确定哪个表是 selected 并仅在所选表的名称为 时执行更新插入。upsert 操作分为 插入新记录或更新现有记录。这些 使用标注在外部系统中执行操作。数组从 从标注响应中获得的结果。请注意,因为标注是针对 每一行,此示例可能会达到 Apex 标注限制。upsertRowsupsertRowsUpsertContextSampleDataSource.UpsertResult
// ...
global override List<DataSource.UpsertResult> upsertRows(DataSource.UpsertContext
context) {
if (context.tableSelected == 'Sample') {
List<DataSource.UpsertResult> results = new List<DataSource.UpsertResult>();
List<Map<String, Object>> rows = context.rows;
for (Map<String, Object> row : rows){
// Make a callout to insert or update records in the external system.
HttpResponse response;
// Determine whether to insert or update a record.
if (row.get('ExternalId') == null){
// Send a POST HTTP request to insert new external record.
// Make an Apex callout and get HttpResponse.
response = makePostCallout(
'{"name":"' + row.get('Name') + '","ExternalId":"' +
row.get('ExternalId') + '"');
}
else {
// Send a PUT HTTP request to update an existing external record.
// Make an Apex callout and get HttpResponse.
response = makePutCallout(
'{"name":"' + row.get('Name') + '","ExternalId":"' +
row.get('ExternalId') + '"',
String.valueOf(row.get('ExternalId')));
}
// Check the returned response.
// Deserialize the response.
Map<String, Object> m = (Map<String, Object>)JSON.deserializeUntyped(
response.getBody());
if (response.getStatusCode() == 200){
results.add(DataSource.UpsertResult.success(
String.valueOf(m.get('id'))));
}
else {
results.add(DataSource.UpsertResult.failure(
String.valueOf(m.get('id')),
'The callout resulted in an error: ' +
response.getStatusCode()));
}
}
return results;
}
return null;
}
// ...
deleteRows
该方法在以下情况下调用 外部对象记录将被删除。您可以通过以下方式删除外部对象记录 Salesforce 用户界面或 DML。以下示例提供了一个示例 方法的实现。 该示例使用传入的表来确定选择了哪个表,并且仅当名称 所选表为 。删除是 在外部系统中使用每个外部 ID 的标注执行。填充 从标注响应获得的结果。请注意,因为标注是 针对每个 ID 创建,此示例可能会达到 Apex 标注限制。deleteRowsdeleteRowsDeleteContextSampleDataSource.DeleteResult
// ...
global override List<DataSource.DeleteResult> deleteRows(DataSource.DeleteContext
context) {
if (context.tableSelected == 'Sample'){
List<DataSource.DeleteResult> results = new List<DataSource.DeleteResult>();
for (String externalId : context.externalIds){
HttpResponse response = makeDeleteCallout(externalId);
if (response.getStatusCode() == 200){
results.add(DataSource.DeleteResult.success(externalId));
}
else {
results.add(DataSource.DeleteResult.failure(externalId,
'Callout delete error:'
+ response.getBody()));
}
}
return results;
}
return null;
}
// ...
创建示例 DataSource.Provider 类
现在,您需要一个类来扩展和覆盖 中的一些方法。
DataSource.Provider
您的班级通知 Salesforce 支持或需要的功能和身份验证功能 连接到外部系统。DataSource.Provider
global class SampleDataSourceProvider extends DataSource.Provider {
如果外部系统需要身份验证,Salesforce 可以提供身份验证 来自外部数据源定义或用户个人设置的凭据。为 但是,简单来说,此示例声明外部系统不需要 认证。为此,它将作为列表中的唯一条目返回 身份验证功能。AuthenticationCapability.ANONYMOUS
override global List<DataSource.AuthenticationCapability>
getAuthenticationCapabilities() {
List<DataSource.AuthenticationCapability> capabilities =
new List<DataSource.AuthenticationCapability>();
capabilities.add(
DataSource.AuthenticationCapability.ANONYMOUS);
return capabilities;
}
此示例还声明外部系统允许 SOQL 查询、SOSL 查询、 Salesforce 搜索、更新插入数据和删除数据。
- 为了允许 SOQL,该示例声明了该功能。DataSource.Capability.ROW_QUERY
- 为了允许 SOSL 和 Salesforce 搜索,该示例声明了该功能。DataSource.Capability.SEARCH
- 为了允许更新插入外部数据,该示例声明了 和 功能。DataSource.Capability.ROW_CREATEDataSource.Capability.ROW_UPDATE
- 为了允许删除外部数据,该示例声明了该功能。DataSource.Capability.ROW_DELETE
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);
capabilities.add(DataSource.Capability.ROW_CREATE);
capabilities.add(DataSource.Capability.ROW_UPDATE);
capabilities.add(DataSource.Capability.ROW_DELETE);
return capabilities;
}
最后,该示例标识获取外部系统架构的类,以及 处理外部数据的查询和搜索。SampleDataSourceConnection
override global DataSource.Connection getConnection(
DataSource.ConnectionParams connectionParams) {
return new SampleDataSourceConnection(connectionParams);
}
}
设置 Salesforce Connect 以使用您的自定义适配器
创建 和 类后,Salesforce Connect 自定义适配器在安装程序中可用。
DataSource.ConnectionDataSource.Provider
完成“设置 Salesforce Connect to Access”中描述的任务 Salesforce 帮助中的“使用自定义适配器的外部数据”。要将外部对象的写入功能添加到适配器,请执行以下操作:
- 使此适配器的外部数据源可写。请参阅“定义 Salesforce 帮助中的 Salesforce Connect – 自定义适配器”。
- 实现 和 方法 适配器。有关详细信息,请参阅连接类。DataSource.Connection.upsertRows()DataSource.Connection.deleteRows()
关于 Apex 连接器框架的关键概念
命名空间提供类 用于 Apex Connector 框架。使用 Apex 连接器框架开发自定义适配器 适用于 Salesforce Connect。然后,通过 Salesforce 将您的 Salesforce 组织连接到任何地方的任何数据 连接自定义适配器。
DataSource
我们建议您了解一些关键概念,以帮助您使用 Apex 连接器 有效的框架。
- Salesforce Connect 外部对象的外部 ID 当您使用 Salesforce Connect 的自定义适配器访问外部数据时,外部对象
上的外部 ID 标准字段的值来自命名的 .DataSource.ColumnExternalId - Salesforce Connect 自定义适配器的身份验证
您的类声明哪些类型的凭据可用于向外部系统进行身份验证。DataSource.Provider - Salesforce Connect 自定义适配器的标注
就像任何其他 Apex 代码一样,Salesforce Connect 自定义适配器可以进行标注。如果与外部系统的连接需要身份验证,请将身份验证参数合并到标注中。 - 使用 Apex 连接器框架
进行分页 在用户界面中显示大量记录时,Salesforce 会将记录集分成多个批次并显示一个批次。然后,您可以分页浏览这些批次。但是,Salesforce Connect 的自定义适配器不会自动支持任何类型的分页。若要支持通过自定义适配器获取的外部对象数据进行分页,请实现服务器驱动或客户端驱动的分页。 - queryMore with the Apex Connector Framework
适用于 Salesforce Connect 的自定义适配器不会自动支持 API 查询中的方法。但是,您的实现必须能够将大型结果集分解为批处理,并使用 SOAP API 中的方法循环访问它们。默认批处理大小为 500 条记录,但查询开发人员可以在查询调用中以编程方式调整该值。queryMorequeryMore - Salesforce Connect 自定义适配器的聚合
如果收到查询,则所选列的属性中具有该值。所选列在 for 的属性中提供。COUNT()QueryAggregation.COUNTaggregationcolumnsSelectedtableSelectionDataSource.QueryContext - Apex 连接器框架
中的过滤器包含一个 .可以有多个 .每个属性都有一个属性,该属性表示 SOQL 或 SOSL 查询中的子句。DataSource.QueryContextDataSource.TableSelectionDataSource.SearchContextTableSelectionTableSelectionfilterWHERE
Salesforce Connect 外部对象的外部 ID
当您使用 Salesforce Connect 的自定义适配器访问外部数据时, 外部对象上的“外部 ID”标准字段的值来自命名的 .
DataSource.ColumnExternalId
每个外部对象都有一个外部 ID 标准字段。它的价值观 唯一标识组织中的每个外部对象记录。当外部对象是 外部查找关系中的父项,外部 ID 标准字段用于 标识子记录。
重要
- 自定义适配器的 Apex 代码必须声明命名并提供其值。DataSource.ColumnExternalId
- 不要使用敏感数据作为 外部 ID 标准字段或指定为名称字段的字段,因为 Salesforce 有时会存储这些值。
- 外部查找 子记录上的关系字段存储并显示外部 父记录的 ID 值。
- 为 仅供内部使用,Salesforce 存储每个 ID 的外部 ID 值 从外部系统检索到的行。 此行为不会 应用于与高数据量关联的外部对象 外部数据源。
例
示例类的摘录显示了名为 .
DataSource.ConnectionDataSource.ColumnExternalId
override global List<DataSource.Table> sync() {
List<DataSource.Table> tables =
new List<DataSource.Table>();
List<DataSource.Column> columns;
columns = new List<DataSource.Column>();
columns.add(DataSource.Column.text('title', 255));
columns.add(DataSource.Column.text('description',255));
columns.add(DataSource.Column.text('createdDate',255));
columns.add(DataSource.Column.text('modifiedDate',255));
columns.add(DataSource.Column.url('selfLink'));
columns.add(DataSource.Column.url('DisplayUrl'));
columns.add(DataSource.Column.text('ExternalId',255));
tables.add(DataSource.Table.get('googleDrive','title',
columns));
return tables;
}
Salesforce Connect 自定义适配器的身份验证
你的班级声明了什么 凭据类型可用于向外部系统进行身份验证。
DataSource.Provider
如果 DataSource.Provider 类的扩展返回 DataSource.AuthenticationCapability 值,这些值指示 支持身份验证,DataSource.Connection 类在构造函数中使用 DataSource.ConnectionParams 实例进行实例化。实例中的身份验证凭据取决于身份 Salesforce 中外部数据源定义的 Type 字段。
DataSource.ConnectionParams
- 如果“身份类型”设置为 ,则凭据来自外部数据 源定义。Named Principal
- 如果“身份类型”设置为:Per User
- 适用于 Salesforce Connect 自定义适配器的
OAuth 如果您使用 OAuth 2.0 访问外部数据,请了解如何避免访问令牌过期导致的访问中断。
Salesforce Connect 自定义适配器的 OAuth
如果您使用 OAuth 2.0 访问外部数据,请了解如何避免访问中断 由过期的访问令牌导致。某些外部系统使用过期且需要刷新的 OAuth 访问令牌。我们 在以下情况下,可以根据需要自动刷新访问令牌:
- 用户或外部数据源具有来自先前 OAuth 的有效刷新令牌 流。
- 类中的 sync、query 或 search 方法会引发 .DataSource.ConnectionDataSource.OAuthTokenExpiredException
我们使用用户或外部数据源的相关 OAuth 凭据进行协商 并刷新令牌。该类是使用 我们提供的 到构造函数。然后重新调用搜索或查询。DataSource.ConnectionDataSource.ConnectionParams
如果身份验证提供程序未提供刷新令牌,则访问外部 当当前访问令牌过期时,系统将丢失。如果出现警告消息 外部数据源详细信息页面,请咨询您的 OAuth 提供商以获取有关以下内容的信息 请求脱机访问或刷新令牌。
对于某些身份验证提供程序,请求脱机访问就像添加 范围。例如,要从 Salesforce 身份验证提供商请求脱机访问, 添加到 Salesforce 组织中身份验证提供商定义的默认范围字段。refresh_token
对于其他身份验证提供程序,必须在身份验证中请求脱机访问 URL 作为查询参数。例如,使用 Google 时,将 Append 附加到 Authorize Endpoint Salesforce 中身份验证提供商定义上的 URL 字段 组织。若要编辑授权终结点,请选择“打开 ID” 在 Provider Type 字段中连接 身份验证提供程序。有关详细信息,请参阅“配置 OpenID Connect 身份验证” 提供商”。?access_type=offline
Salesforce Connect 自定义适配器的标注
就像任何其他 Apex 代码一样,Salesforce Connect 自定义适配器可以进行标注。 如果与外部系统的连接需要身份验证,请合并 身份验证参数添加到标注中。
身份验证参数封装在对象中,并提供给类的构造函数。ConnectionParamsDataSource.Connection例如,如果您的连接需要 OAuth 访问令牌,请使用类似于 以后。
public HttpResponse getResponse(String url) {
Http httpProtocol = new Http();
HttpRequest request = new HttpRequest();
request.setEndPoint(url);
request.setMethod('GET');
request.setHeader('Authorization', 'Bearer ' +
this.connectionInfo.oauthToken);
HttpResponse response = httpProtocol.send(request);
return response;
}
如果您的连接需要基本密码身份验证,请使用类似于 以后。
public HttpResponse getResponse(String url) {
Http httpProtocol = new Http();
HttpRequest request = new HttpRequest();
request.setEndPoint(url);
request.setMethod('GET');
string encodedHeaderValue = EncodingUtil.base64Encode(Blob.valueOf(
this.connectioninfo.username + ':' +
this.connectionInfo.password));
request.setHeader('Authorization', 'Basic ' + encodedHeaderValue);
HttpResponse response = httpProtocol.send(request);
return response;
}
命名凭据作为 Salesforce Connect Custom 的标注端点 适配器
Salesforce Connect 自定义适配器获取存储的相关凭据 在 Salesforce 中,只要有需要。但是,您的 Apex 代码必须应用这些 所有标注的凭据,但将命名凭据指定为 标注端点。命名凭据允许 Salesforce 处理身份验证 逻辑,这样你的代码就不必这样做了。
如果所有自定义适配器的标注都使用命名凭据,则可以设置外部 数据源的“身份验证协议”字段设置为“否” 身份验证。命名凭据会添加相应的 证书,并且可以将标准授权标头添加到标注中。你也 不需要为定义为 命名凭据。
使用 Apex 连接器框架进行分页
在用户界面中显示大量记录时,Salesforce 会破坏 设置为批次并显示一个批次。然后,您可以分页浏览这些批次。然而 Salesforce Connect 的自定义适配器不会自动支持任何类型的分页。自 支持通过自定义适配器获取的外部对象数据进行分页,实现 服务器驱动或客户端驱动的分页。
使用服务器驱动的分页,外部系统控制分页并忽略任何批处理 在查询中指定的边界或页面大小。若要启用服务器驱动的分页,请执行以下操作: 在类中声明功能。 此外,您的 Apex 代码必须生成一个查询令牌,并使用它来确定和获取 下一批结果。QUERY_PAGINATION_SERVER_DRIVENDataSource.Provider
使用客户端驱动的分页,可以使用 and 子句对结果集进行分页。因素 和属性来确定要返回的行。例如 假设结果集有 20 行,数值介于 1 到 20 之间。如果我们要求 of 和 of ,我们希望得到带有 ID – 的行。我们建议您执行所有操作 在外部系统中,在 Apex 之外,使用外部方法进行过滤 系统支持。LIMITOFFSEToffsetmaxResultsDataSource.QueryContextExternalIDoffset5maxResults5610
queryMore 与 Apex 连接器框架
Salesforce Connect 的自定义适配器不会自动支持 API 查询中的方法。但是,您的 实现必须能够将大型结果集分解为多个批次并对其进行迭代 通过使用 SOAP API 中的方法。这 默认批大小为 500 条记录,但查询开发人员可以调整该值 在查询调用中以编程方式。
queryMorequeryMore要支持 ,您的实现必须 指示存在的数据是否多于当前批处理中的数据。当闪电 平台知道存在更多数据,您的API查询将返回一个类似于 以后。
queryMoreQueryResult
{
"totalSize" => -1,
"done" => false,
"nextRecordsUrl" => "/services/data/v32.0/query/01gxx000000B5OgAAK-2000",
"records" => [
[ 0] {
"attributes" => {
"type" => "Sample__x",
"url" =>
"/services/data/v32.0/sobjects/Sample__x/x06xx0000000001AAA"
},
"ExternalId" => "id0"
},
[ 1] {
"attributes" => {
"type" => "Sample__x",
"url" =>
"/services/data/v32.0/sobjects/Sample__x/x06xx0000000002AAA"
},
…
}
- 使用服务器驱动的分页
支持 queryMore 使用服务器驱动的分页时,外部系统控制分页并忽略查询中指定的任何批处理边界或页面大小。若要启用服务器驱动的分页,请在类中声明该功能。QUERY_PAGINATION_SERVER_DRIVENDataSource.Provider - 通过使用客户端驱动的分页
支持 queryMore 在客户端驱动的分页中,可以使用 and 子句对结果集进行分页。LIMITOFFSET
支持查询More by Using 服务器驱动的寻呼
使用服务器驱动的分页时,外部系统控制分页并忽略任何 查询中指定的批处理边界或页面大小。启用服务器驱动 分页,在类中声明功能。
QUERY_PAGINATION_SERVER_DRIVENDataSource.Provider
当返回的没有 包含整个结果集,必须提供一个值。查询 token 是我们临时存储的任意字符串。当我们要求下一批时 中,我们将查询令牌传递回 .您的 Apex 代码必须使用 该查询令牌,用于确定哪些行属于下一批结果。DataSource.TableResultTableResultqueryMoreTokenDataSource.QueryContext
当自定义适配器返回最终批处理时,它不得返回 中的值。queryMoreTokenTableResult
使用客户端驱动支持 queryMore 寻呼
使用客户端驱动的分页,您可以使用 and 子句对结果进行分页 集。
LIMITOFFSET
如果外部系统可以返回每个查询的结果集的总大小, 在类中声明功能。确保 每个搜索或查询都返回值 在。如果总大小 大于批处理中返回的行数,我们生成一个链接并将标志设置为 .我们还将 到你的值 供应。QUERY_TOTAL_SIZEDataSource.ProvidertotalSizeDataSource.TableResultnextRecordsUrldonefalsetotalSizeTableResult
如果外部系统无法返回每个查询的总大小,请不要在类中声明该功能。每当我们进行查询时 通过您的自定义适配器,我们要求增加一行。例如,如果您运行查询,我们调用 具有 属性设置为 6 的对象上的方法。存在或 结果集中缺少第六行表示是否有更多数据可用。我们 但是,假设我们查询的数据集在查询之间不会更改。如果 数据集在查询之间发生变化,您可能会看到重复的行或无法获取所有行 结果。QUERY_TOTAL_SIZEDataSource.ProviderSELECT ExternalId FROM Sample LIMIT 5queryDataSource.ConnectionDataSource.QueryContextmaxResults
归根结底,当您检索小数据时,访问外部数据最有效 数据量和您查询的数据集很少更改。
Salesforce Connect 自定义适配器的聚合
如果您收到查询,则选中 列在其属性中具有值。所选列是 在 for 的属性中提供。
COUNT()QueryAggregation.COUNTaggregationcolumnsSelectedtableSelectionDataSource.QueryContext
下面的示例演示如何应用属性的值来处理查询。aggregationCOUNT()
// Handle COUNT() queries
if (context.tableSelection.columnsSelected.size() == 1 &&
context.tableSelection.columnsSelected.get(0).aggregation ==
QueryAggregation.COUNT) {
List<Map<String, Object>> countResponse = new List<Map<String, Object>>();
Map<String, Object> countRow = new Map<String, Object>();
countRow.put(context.tableSelection.columnsSelected.get(0).columnName,
response.size());
countResponse.add(countRow);
return countResponse;
}
聚合查询仍然可以具有筛选器,因此查询方法可以实现如下方式 以下示例支持基本查询,无论是否使用筛选器。aggregation
override global DataSource.TableResult query(DataSource.QueryContext context) {
List<Map<String,Object>> rows = retrieveData(context);
List<Map<String,Object>> response = postFilterRecords(
context.tableSelection.filter, rows);
if (context.tableSelection.columnsSelected.size() == 1 &&
context.tableSelection.columnsSelected.get(0).aggregation ==
DataSource.QueryAggregation.COUNT) {
List<Map<String, Object>> countResponse = new List<Map<String,
Object>>();
Map<String, Object> countRow = new Map<String, Object>();
countRow.put(context.tableSelection.columnsSelected.get(0).columnName,
response.size());
countResponse.add(countRow);
return DataSource.TableResult.get(context, countResponse);
}
return DataSource.TableResult.get(context, response);
}
Apex 连接器框架中的过滤器
包含一个 .可以有多个 .每个都有一个属性 表示 SOQL 或 SOSL 中的子句 查询。
DataSource.QueryContextDataSource.TableSelectionDataSource.SearchContextTableSelectionTableSelectionfilterWHERE
例如,当用户转到外部对象的记录详细信息页面时,将执行 your 。背后 场景中,我们生成一个类似于以下内容的 SOQL 查询。DataSource.Connection
SELECT columnNames
FROM externalObjectApiName
WHERE ExternalId = 'selectedExternalObjectExternalId'
此 SOQL 查询会导致调用类上的方法。 以下代码可以检测到这种情况。queryDataSource.Connection
if (context.tableSelection.filter != null) {
if (context.tableSelection.filter.type == DataSource.FilterType.EQUALS
&& 'ExternalId' == context.tableSelection.filter.columnName
&& context.tableSelection.filter.columnValue instanceOf String) {
String selection = (String)context.tableSelection.filter.columnValue;
return DataSource.TableResult.get(true, null,
tableSelection.tableSelected, findSingleResult(selection));
}
}
此代码示例假定您实现了一个返回单个记录的方法,该方法给定选定的 .确保您的代码获取 与请求的 .findSingleResultExternalIdExternalId
- 在 Apex 连接器框架
中评估过滤器 如果某行与过滤器描述的条件匹配,则过滤器的计算结果为 true。 - Apex 连接器框架
筛选器中的复合筛选器可以具有子筛选器,这些子筛选器存储在属性中。subfilters
评估 Apex 连接器框架中的过滤器
如果某行与以下条件匹配,则筛选器将该行的计算结果为 true: 过滤器描述。
例如,假设 a 已设置为 、 和 设置为 。将返回远程表中列条目等于 42 的任何行。DataSource.FiltercolumnNamemeaningOfLifecolumnValue42typeEQUALSmeaningOfLife
相反,假设筛选器已设置为 、 和 设置为 。我们将构造一个对象,其中包含值小于 3 的所有行。typeLESS_THANcolumnValue3columnNamenumericColDataSource.TableResultnumericCol
若要提高性能,请在外部系统中执行所有筛选。你可以,为了 例如,将对象转换为 SQL 或 OData 查询,或将其映射到 SOAP 查询上的参数。如果外部系统返回 大量数据,您在 Apex 代码中进行过滤,您很快就会超过 调速器限制。Filter
如果无法在外部系统中执行所有过滤,请尽可能多地在那里执行 并返回尽可能少的数据。然后筛选较小的数据集合 您的 Apex 代码。
Apex 连接器框架中的复合过滤器
筛选器可以具有子筛选器,这些子筛选器存储在属性中。
subfilters
如果筛选器具有子项,则筛选器必须 以下其中一项。type
过滤器类型 | 描述 |
---|---|
AND_ | 我们返回与所有子筛选器匹配的所有行。 |
OR_ | 我们返回与任何子筛选器匹配的所有行。 |
NOT_ | 筛选器反转其子筛选器计算行的方式。过滤器 此类型只能有一个子筛选器。 |
此代码示例演示如何处理复合筛选器。
override global DataSource.TableResult query(DataSource.QueryContext context) {
// Call out to an external data source and retrieve a set of records.
// We should attempt to get as much information as possible about the
// query from the QueryContext, to minimize the number of records
// that we return.
List<Map<String,Object>> rows = retrieveData(context);
// This only filters the results. Anything in the query that we don’t
// currently support, such as aggregation or sorting, is ignored.
return DataSource.TableResult.get(context, postFilterRecords(
context.tableSelection.filter, rows));
}
private List<Map<String,Object>> retrieveData(DataSource.QueryContext context) {
// Call out to an external data source. Form the callout so that
// it filters as much as possible on the remote site,
// based on the parameters in the QueryContext.
return ...;
}
private List<Map<String,Object>> postFilterRecords(
DataSource.Filter filter, List<Map<String,Object>> rows) {
if (filter == null) {
return rows;
}
DataSource.FilterType type = filter.type;
List<Map<String,Object>> retainedRows = new List<Map<String,Object>>();
if (type == DataSource.FilterType.NOT_) {
// We expect one Filter in the subfilters.
DataSource.Filter subfilter = filter.subfilters.get(0);
for (Map<String,Object> row : rows) {
if (!evaluate(filter, row)) {
retainedRows.add(row);
}
}
return retainedRows;
} else if (type == DataSource.FilterType.AND_) {
// For each filter, find all matches; anything that matches ALL filters
// is returned.
retainedRows = rows;
for (DataSource.Filter subfilter : filter.subfilters) {
retainedRows = postFilterRecords(subfilter, retainedRows);
}
return retainedRows;
} else if (type == DataSource.FilterType.OR_) {
// For each filter, find all matches. Anything that matches
// at least one filter is returned.
for (DataSource.Filter subfilter : filter.subfilters) {
List<Map<String,Object>> matchedRows = postFilterRecords(
subfilter, rows);
retainedRows.addAll(matchedRows);
}
return retainedRows;
} else {
// Find all matches for this filter in our collection of records.
for (Map<String,Object> row : rows) {
if (evaluate(filter, row)) {
retainedRows.add(row);
}
}
return retainedRows;
}
}
private Boolean evaluate(DataSource.Filter filter, Map<String,Object> row) {
if (filter.type == DataSource.FilterType.EQUALS) {
String columnName = filter.columnName;
Object expectedValue = filter.columnValue;
Object foundValue = row.get(columnName);
return expectedValue.equals(foundValue);
} else {
// Throw an exception; implementing other filter types is left
// as an exercise for the reader.
throwException('Unexpected filter type: ' + filter.type);
}
return false;
}
Apex 连接器框架的注意事项
了解创建 Salesforce Connect 自定义的限制和注意事项 具有 Apex 连接器框架的适配器。
- 如果更改并保存类,请重新保存 相应的类。 否则,在定义外部数据源时,自定义适配器不会 显示为“类型”字段的选项。DataSource.ConnectionDataSource.Provider此外, 关联的外部对象的自定义选项卡不再显示在 Salesforce UI 中。
- 包含自定义的 Apex 代码中不允许 DML 操作 适配器。
- 确保您了解外部系统 API 的限制。例如 某些外部系统仅接受最多 40 行的请求。
- Apex 数据类型限制:
- 双精度 – 超过 18 位有效数字时,该值将失去精度。为 精度更高,使用小数而不是双精度。
- 字符串 – 如果长度大于 255 个字符,则字符串为 映射到 Salesforce 中的长文本区域字段。
- Salesforce Connect 的自定义适配器受到与任何适配器相同的限制 其他 Apex 代码。例如:
- 所有 Apex 调速器限制均适用。
- 测试方法不支持 Web 服务标注。执行 Web 的测试 服务标注失败。对于演示如何避免这些失败的示例 通过返回模拟响应进行测试,请参阅适用于 Salesforce 的 Google 云端硬盘™自定义适配器 连接。
- 在 Apex 测试中,使用动态 SOQL 进行查询 外部对象。对外部对象执行静态 SOQL 查询的测试 失败。
Apex 连接器框架示例
这些示例说明了如何使用 Apex Connector Framework 创建自定义 Salesforce Connect 的适配器。
- 适用于 Salesforce Connect 的 Google Drive 自定义适配器 此示例说明如何使用标注和 OAuth 连接到外部系统,在本例中为 Google Drive™™ 在线存储服务。
该示例还演示了如何通过返回测试方法的模拟响应来避免 Web 服务标注的测试失败。 - 适用于 Salesforce Connect 的 Google 图书自定义适配器 此示例说明了如何解决外部系统 API 的要求和限制:在本例中为 Google 图书™ API 系列。
- Salesforce Connect 的环回自定义适配器 此示例说明如何处理查询中的筛选。
为简单起见,此示例将 Salesforce 组织作为外部系统连接到自身。 - 适用于 Salesforce Connect 的 GitHub 自定义适配器 此示例说明如何支持间接查找关系。
间接查找关系将子外部对象链接到父标准对象或自定义对象。 - 适用于 Salesforce Connect 的 Stack Overflow 自定义适配器 此示例说明如何支持外部查找关系和多个表。
外部查找关系将子标准对象、自定义对象或外部对象链接到父外部对象。每个表都可以成为 Salesforce 组织中的外部对象。
适用于 Salesforce Connect 的 Google Drive™ 自定义适配器
此示例说明如何使用标注和 OAuth 连接到外部 系统,在本例中为 Google Drive™ 在线存储服务。该示例还 演示如何通过返回 Web 服务标注的模拟响应来避免测试失败 测试方法。
要使此示例可靠地工作,请在设置 OAuth 时请求脱机访问,以便 Salesforce 可以获取并维护连接的刷新令牌。
DriveDataSourceConnection 类
/**
* Extends the DataSource.Connection class to enable
* Salesforce to sync the external system’s schema
* and to handle queries and searches of the external data.
**/
global class DriveDataSourceConnection extends
DataSource.Connection {
private DataSource.ConnectionParams connectionInfo;
/**
* Constructor for DriveDataSourceConnection.
**/
global DriveDataSourceConnection(
DataSource.ConnectionParams connectionInfo) {
this.connectionInfo = connectionInfo;
}
/**
* Called when an external object needs to get a list of
* schema from the external data source, for example when
* the administrator clicks “Validate and Sync” in the
* user interface for the external data source.
**/
override global List<DataSource.Table> sync() {
List<DataSource.Table> tables =
new List<DataSource.Table>();
List<DataSource.Column> columns;
columns = new List<DataSource.Column>();
columns.add(DataSource.Column.text('title', 255));
columns.add(DataSource.Column.text('description',255));
columns.add(DataSource.Column.text('createdDate',255));
columns.add(DataSource.Column.text('modifiedDate',255));
columns.add(DataSource.Column.url('selfLink'));
columns.add(DataSource.Column.url('DisplayUrl'));
columns.add(DataSource.Column.text('ExternalId',255));
tables.add(DataSource.Table.get('googleDrive','title',
columns));
return tables;
}
/**
* Called to query and get results from the external
* system for SOQL queries, list views, and detail pages
* for an external object that’s associated with the
* external data source.
*
* The QueryContext argument represents the query to run
* against a table in the external system.
*
* Returns a list of rows as the query results.
**/
override global DataSource.TableResult query(
DataSource.QueryContext context) {
DataSource.Filter filter = context.tableSelection.filter;
String url;
if (filter != null) {
String thisColumnName = filter.columnName;
if (thisColumnName != null &&
thisColumnName.equals('ExternalId'))
url = 'https://www.googleapis.com/drive/v2/'
+ 'files/' + filter.columnValue;
else
url = 'https://www.googleapis.com/drive/v2/'
+ 'files';
} else {
url = 'https://www.googleapis.com/drive/v2/'
+ 'files';
}
/**
* Filters, sorts, and applies limit and offset clauses.
**/
List<Map<String, Object>> rows =
DataSource.QueryUtils.process(context, getData(url));
return DataSource.TableResult.get(true, null,
context.tableSelection.tableSelected, rows);
}
/**
* Called to do a full text search and get results from
* the external system for SOSL queries and Salesforce
* global searches.
*
* The SearchContext argument represents the query to run
* against a table in the external system.
*
* Returns results for each table that the SearchContext
* requested to be searched.
**/
override global List<DataSource.TableResult> search(
DataSource.SearchContext context) {
List<DataSource.TableResult> results =
new List<DataSource.TableResult>();
for (Integer i =0;i< context.tableSelections.size();i++) {
String entity = context.tableSelections[i].tableSelected;
String url =
'https://www.googleapis.com/drive/v2/files'+
'?q=fullText+contains+\''+context.searchPhrase+'\'';
results.add(DataSource.TableResult.get(
true, null, entity, getData(url)));
}
return results;
}
/**
* Helper method to parse the data.
* The url argument is the URL of the external system.
* Returns a list of rows from the external system.
**/
public List<Map<String, Object>> getData(String url) {
String response = getResponse(url);
List<Map<String, Object>> rows =
new List<Map<String, Object>>();
Map<String, Object> responseBodyMap = (Map<String, Object>)
JSON.deserializeUntyped(response);
/**
* Checks errors.
**/
Map<String, Object> error =
(Map<String, Object>)responseBodyMap.get('error');
if (error!=null) {
List<Object> errorsList =
(List<Object>)error.get('errors');
Map<String, Object> errors =
(Map<String, Object>)errorsList[0];
String errorMessage = (String)errors.get('message');
throw new DataSource.OAuthTokenExpiredException(errorMessage);
}
List<Object> fileItems=(List<Object>)responseBodyMap.get('items');
if (fileItems != null) {
for (Integer i=0; i < fileItems.size(); i++) {
Map<String, Object> item =
(Map<String, Object>)fileItems[i];
rows.add(createRow(item));
}
} else {
rows.add(createRow(responseBodyMap));
}
return rows;
}
/**
* Helper method to populate the External ID and Display
* URL fields on external object records based on the 'id'
* value that’s sent by the external system.
*
* The Map<String, Object> item parameter maps to the data
* that represents a row.
*
* Returns an updated map with the External ID and
* Display URL values.
**/
public Map<String, Object> createRow(
Map<String, Object> item){
Map<String, Object> row = new Map<String, Object>();
for ( String key : item.keySet() ) {
if (key == 'id') {
row.put('ExternalId', item.get(key));
} else if (key=='selfLink') {
row.put(key, item.get(key));
row.put('DisplayUrl', item.get(key));
} else {
row.put(key, item.get(key));
}
}
return row;
}
static String mockResponse = '{' +
' "kind": "drive#file",' +
' "id": "12345",' +
' "selfLink": "files/12345",' +
' "title": "Mock File",' +
' "mimeType": "application/text",' +
' "description": "Mock response that’s used during tests",' +
' "createdDate": "2016-04-20",' +
' "modifiedDate": "2016-04-20",' +
' "version": 1' +
'}';
/**
* Helper method to make the HTTP GET call.
* The url argument is the URL of the external system.
* Returns the response from the external system.
**/
public String getResponse(String url) {
if (System.Test.isRunningTest()) {
// Avoid callouts during tests. Return mock data instead.
return mockResponse;
} else {
// Perform callouts for production (non-test) results.
Http httpProtocol = new Http();
HttpRequest request = new HttpRequest();
request.setEndPoint(url);
request.setMethod('GET');
request.setHeader('Authorization', 'Bearer '+
this.connectionInfo.oauthToken);
HttpResponse response = httpProtocol.send(request);
return response.getBody();
}
}
}
DriveDataSourceProvider Class
/**
* Extends the DataSource.Provider base class to create a
* custom adapter for Salesforce Connect. The class informs
* Salesforce of the functional and authentication
* capabilities that are supported by or required to connect
* to an external system.
**/
global class DriveDataSourceProvider
extends DataSource.Provider {
/**
* Declares the types of authentication that can be used
* to access the external system.
**/
override global List<DataSource.AuthenticationCapability>
getAuthenticationCapabilities() {
List<DataSource.AuthenticationCapability> capabilities =
new List<DataSource.AuthenticationCapability>();
capabilities.add(
DataSource.AuthenticationCapability.OAUTH);
capabilities.add(
DataSource.AuthenticationCapability.ANONYMOUS);
return capabilities;
}
/**
* Declares the functional capabilities that the
* external system supports.
**/
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;
}
/**
* Declares the associated DataSource.Connection class.
**/
override global DataSource.Connection getConnection(
DataSource.ConnectionParams connectionParams) {
return new DriveDataSourceConnection(connectionParams);
}
}
适用于 Salesforce Connect 的 Google 图书™自定义适配器
此示例说明如何解决 外部系统的 API:在本例中为 Google 图书 API 系列。为了与 Google 图书™服务集成,我们按如下方式设置 Salesforce Connect。
- Google Books API 最多允许返回 40 个结果,因此我们开发了 用于处理超过 40 行的结果集的自定义适配器。
- Google Books API 只能按搜索相关性和发布日期进行排序,因此我们 开发我们的自定义适配器以禁用列排序。
- 为了支持 OAuth,我们在 Salesforce 中设置了身份验证设置,以便 请求的访问令牌权限范围包括 。https://www.googleapis.com/auth/books
- 为了允许 Apex 标注,我们在 Salesforce 中定义了以下远程站点:
- https://www.googleapis.com
- https://books.google.com
BooksDataSourceConnection 类
/**
* Extends the DataSource.Connection class to enable
* Salesforce to sync the external system metadata
* schema and to handle queries and searches of the external
* data.
**/
global class BooksDataSourceConnection extends
DataSource.Connection {
private DataSource.ConnectionParams connectionInfo;
// Constructor for BooksDataSourceConnection.
global BooksDataSourceConnection(DataSource.ConnectionParams
connectionInfo) {
this.connectionInfo = connectionInfo;
}
/**
* Called when an external object needs to get a list of
* schema from the external data source, for example when
* the administrator clicks “Validate and Sync” in the
* user interface for the external data source.
**/
override global List<DataSource.Table> sync() {
List<DataSource.Table> tables =
new List<DataSource.Table>();
List<DataSource.Column> columns;
columns = new List<DataSource.Column>();
columns.add(getColumn('title'));
columns.add(getColumn('description'));
columns.add(getColumn('publishedDate'));
columns.add(getColumn('publisher'));
columns.add(DataSource.Column.url('DisplayUrl'));
columns.add(DataSource.Column.text('ExternalId', 255));
tables.add(DataSource.Table.get('googleBooks', 'title',
columns));
return tables;
}
/**
* Google Books API v1 doesn't support sorting,
* so we create a column with sortable = false.
**/
private DataSource.Column getColumn(String columnName) {
DataSource.Column column = DataSource.Column.text(columnName,
255);
column.sortable = false;
return column;
}
/**
* Called to query and get results from the external
* system for SOQL queries, list views, and detail pages
* for an external object that's associated with the
* external data source.
*
* The QueryContext argument represents the query to run
* against a table in the external system.
*
* Returns a list of rows as the query results.
**/
override global DataSource.TableResult query(
DataSource.QueryContext contexts) {
DataSource.Filter filter = contexts.tableSelection.filter;
String url;
if (contexts.tableSelection.columnsSelected.size() == 1 &&
contexts.tableSelection.columnsSelected.get(0).aggregation ==
DataSource.QueryAggregation.COUNT) {
return getCount(contexts);
}
if (filter != null) {
String thisColumnName = filter.columnName;
if (thisColumnName != null &&
thisColumnName.equals('ExternalId')) {
url = 'https://www.googleapis.com/books/v1/' +
'volumes?q=' + filter.columnValue +
'&maxResults=1&id=' + filter.columnValue;
return DataSource.TableResult.get(true, null,
contexts.tableSelection.tableSelected,
getData(url));
}
else {
url = 'https://www.googleapis.com/books/' +
'v1/volumes?q=' + filter.columnValue +
'&id=' + filter.columnValue +
'&maxResults=40' + '&startIndex=';
}
} else {
url = 'https://www.googleapis.com/books/v1/' +
'volumes?q=america&' + '&maxResults=40' +
'&startIndex=';
}
/**
* Google Books API v1 supports maxResults of 40
* so we handle pagination explicitly in the else statement
* when we handle more than 40 records per query.
**/
if (contexts.maxResults < 40) {
return DataSource.TableResult.get(true, null,
contexts.tableSelection.tableSelected,
getData(url + contexts.offset));
}
else {
return fetchData(contexts, url);
}
}
/**
* Helper method to fetch results when maxResults is
* greater than 40 (the max value for maxResults supported
* by Google Books API v1).
**/
private DataSource.TableResult fetchData(
DataSource.QueryContext contexts, String url) {
Integer fetchSlot = (contexts.maxResults / 40) + 1;
List<Map<String, Object>> data =
new List<Map<String, Object>>();
Integer startIndex = contexts.offset;
for(Integer count = 0; count < fetchSlot; count++) {
data.addAll(getData(url + startIndex));
if(count == 0)
contexts.offset = 41;
else
contexts.offset += 40;
}
return DataSource.TableResult.get(true, null,
contexts.tableSelection.tableSelected, data);
}
/**
* Helper method to execute count() query.
**/
private DataSource.TableResult getCount(
DataSource.QueryContext contexts) {
String url = 'https://www.googleapis.com/books/v1/' +
'volumes?q=america&projection=full';
List<Map<String,Object>> response =
DataSource.QueryUtils.filter(contexts, getData(url));
List<Map<String, Object>> countResponse =
new List<Map<String, Object>>();
Map<String, Object> countRow =
new Map<String, Object>();
countRow.put(
contexts.tableSelection.columnsSelected.get(0).columnName,
response.size());
countResponse.add(countRow);
return DataSource.TableResult.get(contexts, countResponse);
}
/**
* Called to do a full text search and get results from
* the external system for SOSL queries and Salesforce
* global searches.
*
* The SearchContext argument represents the query to run
* against a table in the external system.
*
* Returns results for each table that the SearchContext
* requested to be searched.
**/
override global List<DataSource.TableResult> search(
DataSource.SearchContext contexts) {
List<DataSource.TableResult> results =
new List<DataSource.TableResult>();
for (Integer i =0; i< contexts.tableSelections.size();i++) {
String entity = contexts.tableSelections[i].tableSelected;
String url = 'https://www.googleapis.com/books/v1' +
'/volumes?q=' + contexts.searchPhrase;
results.add(DataSource.TableResult.get(true, null,
entity,
getData(url)));
}
return results;
}
/**
* Helper method to parse the data.
* Returns a list of rows from the external system.
**/
public List<Map<String, Object>> getData(String url) {
HttpResponse response = getResponse(url);
String body = response.getBody();
List<Map<String, Object>> rows =
new List<Map<String, Object>>();
Map<String, Object> responseBodyMap =
(Map<String, Object>)JSON.deserializeUntyped(body);
/**
* Checks errors.
**/
Map<String, Object> error =
(Map<String, Object>)responseBodyMap.get('error');
if (error!=null) {
List<Object> errorsList =
(List<Object>)error.get('errors');
Map<String, Object> errors =
(Map<String, Object>)errorsList[0];
String messages = (String)errors.get('message');
throw new DataSource.OAuthTokenExpiredException(messages);
}
List<Object> sItems = (List<Object>)responseBodyMap.get('items');
if (sItems != null) {
for (Integer i=0; i< sItems.size(); i++) {
Map<String, Object> item =
(Map<String, Object>)sItems[i];
rows.add(createRow(item));
}
} else {
rows.add(createRow(responseBodyMap));
}
return rows;
}
/**
* Helper method to populate a row based on source data.
*
* The item argument maps to the data that
* represents a row.
*
* Returns an updated map with the External ID and
* Display URL values.
**/
public Map<String, Object> createRow(
Map<String, Object> item) {
Map<String, Object> row = new Map<String, Object>();
for ( String key : item.keySet() ){
if (key == 'id') {
row.put('ExternalId', item.get(key));
} else if (key == 'volumeInfo') {
Map<String, Object> volumeInfoMap =
(Map<String, Object>)item.get(key);
row.put('title', volumeInfoMap.get('title'));
row.put('description',
volumeInfoMap.get('description'));
row.put('DisplayUrl',
volumeInfoMap.get('infoLink'));
row.put('publishedDate',
volumeInfoMap.get('publishedDate'));
row.put('publisher',
volumeInfoMap.get('publisher'));
}
}
return row;
}
/**
* Helper method to make the HTTP GET call.
* The url argument is the URL of the external system.
* Returns the response from the external system.
**/
public HttpResponse getResponse(String url) {
Http httpProtocol = new Http();
HttpRequest request = new HttpRequest();
request.setEndPoint(url);
request.setMethod('GET');
request.setHeader('Authorization', 'Bearer '+
this.connectionInfo.oauthToken);
HttpResponse response = httpProtocol.send(request);
return response;
}
}
BooksDataSourceProvider Class
/**
* Extends the DataSource.Provider base class to create a
* custom adapter for Salesforce Connect. The class informs
* Salesforce of the functional and authentication
* capabilities that are supported by or required to connect
* to an external system.
**/
global class BooksDataSourceProvider extends
DataSource.Provider {
/**
* Declares the types of authentication that can be used
* to access the external system.
**/
override global List<DataSource.AuthenticationCapability>
getAuthenticationCapabilities() {
List<DataSource.AuthenticationCapability> capabilities =
new List<DataSource.AuthenticationCapability>();
capabilities.add(
DataSource.AuthenticationCapability.OAUTH);
capabilities.add(
DataSource.AuthenticationCapability.ANONYMOUS);
return capabilities;
}
/**
* Declares the functional capabilities that the
* external system supports.
**/
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;
}
/**
* Declares the associated DataSource.Connection class.
**/
override global DataSource.Connection getConnection(
DataSource.ConnectionParams connectionParams) {
return new BooksDataSourceConnection(connectionParams);
}
}
适用于 Salesforce Connect 的环回自定义适配器
此示例说明如何处理查询中的筛选。为简单起见,这里 示例将 Salesforce 组织作为外部系统连接到自身。
LoopbackDataSourceConnection 类
/**
* Extends the DataSource.Connection class to enable
* Salesforce to sync the external system’s schema
* and to handle queries and searches of the external data.
**/
global class LoopbackDataSourceConnection
extends DataSource.Connection {
/**
* Constructors.
**/
global LoopbackDataSourceConnection(
DataSource.ConnectionParams connectionParams) {
}
global LoopbackDataSourceConnection() {}
/**
* Called when an external object needs to get a list of
* schema from the external data source, for example when
* the administrator clicks “Validate and Sync†in the
* user interface for the external data source.
**/
override global List<DataSource.Table> sync() {
List<DataSource.Table> tables =
new List<DataSource.Table>();
List<DataSource.Column> columns;
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));
tables.add(
DataSource.Table.get('Looper', 'Name', columns));
return tables;
}
/**
* Called to query and get results from the external
* system for SOQL queries, list views, and detail pages
* for an external object that’s associated with the
* external data source.
*
* The QueryContext argument represents the query to run
* against a table in the external system.
*
* Returns a list of rows as the query results.
**/
override global DataSource.TableResult
query(DataSource.QueryContext context) {
if (context.tableSelection.columnsSelected.size() == 1 &&
context.tableSelection.columnsSelected.get(0).aggregation ==
DataSource.QueryAggregation.COUNT) {
integer count = execCount(getCountQuery(context));
List<Map<String, Object>> countResponse =
new List<Map<String, Object>>();
Map<String, Object> countRow =
new Map<String, Object>();
countRow.put(
context.tableSelection.columnsSelected.get(0).columnName,
count);
countResponse.add(countRow);
return DataSource.TableResult.get(context,countResponse);
} else {
List<Map<String,Object>> rows = execQuery(
getSoqlQuery(context));
return DataSource.TableResult.get(context,rows);
}
}
/**
* Called to do a full text search and get results from
* the external system for SOSL queries and Salesforce
* global searches.
*
* The SearchContext argument represents the query to run
* against a table in the external system.
*
* Returns results for each table that the SearchContext
* requested to be searched.
**/
override global List<DataSource.TableResult>
search(DataSource.SearchContext context) {
return DataSource.SearchUtils.searchByName(context, this);
}
/**
* Helper method to execute the SOQL query and
* return the results.
**/
private List<Map<String,Object>>
execQuery(String soqlQuery) {
List<Account> objs = Database.query(soqlQuery);
List<Map<String,Object>> rows =
new List<Map<String,Object>>();
for (Account obj : objs) {
Map<String,Object> row = new Map<String,Object>();
row.put('Name', obj.Name);
row.put('NumberOfEmployees', obj.NumberOfEmployees);
row.put('ExternalId', obj.Id);
row.put('DisplayUrl',
URL.getOrgDomainUrl().toExternalForm() +
obj.Id);
rows.add(row);
}
return rows;
}
/**
* Helper method to get aggregate count.
**/
private integer execCount(String soqlQuery) {
integer count = Database.countQuery(soqlQuery);
return count;
}
/**
* Helper method to create default aggregate query.
**/
private String getCountQuery(DataSource.QueryContext context) {
String baseQuery = 'SELECT COUNT() FROM Account';
String filter = getSoqlFilter('',
context.tableSelection.filter);
if (filter.length() > 0)
return baseQuery + ' WHERE ' + filter;
return baseQuery;
}
/**
* Helper method to create default query.
**/
private String getSoqlQuery(DataSource.QueryContext context) {
String baseQuery =
'SELECT Id,Name,NumberOfEmployees FROM Account';
String filter = getSoqlFilter('',
context.tableSelection.filter);
if (filter.length() > 0)
return baseQuery + ' WHERE ' + filter;
return baseQuery;
}
/**
* Helper method to handle query filter.
**/
private String getSoqlFilter(String query,
DataSource.Filter filter) {
if (filter == null) {
return query;
}
String append;
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);
append = getSoqlFilter('NOT', subfilter);
} else if (type == DataSource.FilterType.AND_) {
append =
getSoqlFilterCompound('AND', filter.subfilters);
} else if (type == DataSource.FilterType.OR_) {
append =
getSoqlFilterCompound('OR', filter.subfilters);
} else {
append = getSoqlFilterExpression(filter);
}
return query + ' ' + append;
}
/**
* Helper method to handle query subfilters.
**/
private String getSoqlFilterCompound(String operator,
List<DataSource.Filter> subfilters) {
String expression = ' (';
boolean first = true;
for (DataSource.Filter subfilter : subfilters) {
if (first)
first = false;
else
expression += ' ' + operator + ' ';
expression += getSoqlFilter('', subfilter);
}
expression += ') ';
return expression;
}
/**
* Helper method to handle query filter expressions.
**/
private String getSoqlFilterExpression(
DataSource.Filter filter) {
String columnName = filter.columnName;
String operator;
Object expectedValue = filter.columnValue;
if (filter.type == DataSource.FilterType.EQUALS) {
operator = '=';
} else if (filter.type ==
DataSource.FilterType.NOT_EQUALS) {
operator = '<>';
} else if (filter.type ==
DataSource.FilterType.LESS_THAN) {
operator = '<';
} else if (filter.type ==
DataSource.FilterType.GREATER_THAN) {
operator = '>';
} else if (filter.type ==
DataSource.FilterType.LESS_THAN_OR_EQUAL_TO) {
operator = '<=';
} else if (filter.type ==
DataSource.FilterType.GREATER_THAN_OR_EQUAL_TO) {
operator = '>=';
} 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 if (filter.type ==
DataSource.FilterType.LIKE_) {
return mapColumnName(columnName) +
' LIKE \'' + String.valueOf(expectedValue) + '\'';
} else {
throwException(
'Implementing other filter types is left as an exercise for the reader: '
+ filter.type);
}
return mapColumnName(columnName) +
' ' + operator + ' ' + wrapValue(expectedValue);
}
/**
* Helper method to map column names.
**/
private String mapColumnName(String apexName) {
if (apexName.equalsIgnoreCase('ExternalId'))
return 'Id';
if (apexName.equalsIgnoreCase('DisplayUrl'))
return 'Id';
return apexName;
}
/**
* Helper method to wrap expression Strings with quotes.
**/
private String wrapValue(Object foundValue) {
if (foundValue instanceof String)
return '\'' + String.valueOf(foundValue) + '\'';
return String.valueOf(foundValue);
}
}
LoopbackDataSourceProvider Class
/**
* Extends the DataSource.Provider base class to create a
* custom adapter for Salesforce Connect. The class informs
* Salesforce of the functional and authentication
* capabilities that are supported by or required to connect
* to an external system.
**/
global class LoopbackDataSourceProvider
extends DataSource.Provider {
/**
* Declares the types of authentication that can be used
* to access the external system.
**/
override global List<DataSource.AuthenticationCapability>
getAuthenticationCapabilities() {
List<DataSource.AuthenticationCapability> capabilities =
new List<DataSource.AuthenticationCapability>();
capabilities.add(
DataSource.AuthenticationCapability.ANONYMOUS);
capabilities.add(
DataSource.AuthenticationCapability.BASIC);
return capabilities;
}
/**
* Declares the functional capabilities that the
* external system supports.
**/
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;
}
/**
* Declares the associated DataSource.Connection class.
**/
override global DataSource.Connection
getConnection(DataSource.ConnectionParams connectionParams) {
return new LoopbackDataSourceConnection();
}
}
适用于 Salesforce Connect 的 GitHub 自定义适配器
此示例演示如何支持间接查找关系。间接 查找关系将子外部对象链接到父标准对象或自定义对象 对象。
若要使此示例正常工作,请在“联系人”标准对象上创建一个自定义字段。命名自定义项 字段,使其成为长度为 39 的文本字段,并且 选择 和 属性。另外,将 https://api.github.com 添加到 您的远程站点设置。github_usernameExternal IDUnique
GitHubDataSourceConnection 类
/**
* Defines the connection to GitHub REST API v3 to support
* querying of GitHub profiles.
* Extends the DataSource.Connection class to enable
* Salesforce to sync the external system’s schema
* and to handle queries and searches of the external data.
**/
global class GitHubDataSourceConnection extends
DataSource.Connection {
private DataSource.ConnectionParams connectionInfo;
/**
* Constructor for GitHubDataSourceConnection
**/
global GitHubDataSourceConnection(
DataSource.ConnectionParams connectionInfo) {
this.connectionInfo = connectionInfo;
}
/**
* Called to query and get results from the external
* system for SOQL queries, list views, and detail pages
* for an external object that’s associated with the
* external data source.
*
* The queryContext argument represents the query to run
* against a table in the external system.
*
* Returns a list of rows as the query results.
**/
override global DataSource.TableResult query(
DataSource.QueryContext context) {
DataSource.Filter filter = context.tableSelection.filter;
String url;
if (filter != null) {
String thisColumnName = filter.columnName;
if (thisColumnName != null &&
(thisColumnName.equals('ExternalId') ||
thisColumnName.equals('login')))
url = 'https://api.github.com/users/'
+ filter.columnValue;
else
url = 'https://api.github.com/users';
} else {
url = 'https://api.github.com/users';
}
/**
* Filters, sorts, and applies limit and offset clauses.
**/
List<Map<String, Object>> rows =
DataSource.QueryUtils.process(context, getData(url));
return DataSource.TableResult.get(true, null,
context.tableSelection.tableSelected, rows);
}
/**
* Defines the schema for the external system.
* Called when the administrator clicks “Validate and Sync”
* in the user interface for the external data source.
**/
override global List<DataSource.Table> sync() {
List<DataSource.Table> tables =
new List<DataSource.Table>();
List<DataSource.Column> columns;
columns = new List<DataSource.Column>();
// Defines the indirect lookup field. (For this to work,
// make sure your Contact standard object has a
// custom unique, external ID field called github_username.)
columns.add(DataSource.Column.indirectLookup(
'login', 'Contact', 'github_username__c'));
columns.add(DataSource.Column.text('id', 255));
columns.add(DataSource.Column.text('name',255));
columns.add(DataSource.Column.text('company',255));
columns.add(DataSource.Column.text('bio',255));
columns.add(DataSource.Column.text('followers',255));
columns.add(DataSource.Column.text('following',255));
columns.add(DataSource.Column.url('html_url'));
columns.add(DataSource.Column.url('DisplayUrl'));
columns.add(DataSource.Column.text('ExternalId',255));
tables.add(DataSource.Table.get('githubProfile','login',
columns));
return tables;
}
/**
* Called to do a full text search and get results from
* the external system for SOSL queries and Salesforce
* global searches.
*
* The SearchContext argument represents the query to run
* against a table in the external system.
*
* Returns results for each table that the SearchContext
* requested to be searched.
**/
override global List<DataSource.TableResult> search(
DataSource.SearchContext context) {
List<DataSource.TableResult> results =
new List<DataSource.TableResult>();
for (Integer i =0;i< context.tableSelections.size();i++) {
String entity = context.tableSelections[i].tableSelected;
// Search usernames
String url = 'https://api.github.com/users/'
+ context.searchPhrase;
results.add(DataSource.TableResult.get(
true, null, entity, getData(url)));
}
return results;
}
/**
* Helper method to parse the data.
* The url argument is the URL of the external system.
* Returns a list of rows from the external system.
**/
public List<Map<String, Object>> getData(String url) {
String response = getResponse(url);
// Standardize response string
if (!response.contains('"items":')) {
if (response.substring(0,1).equals('{')) {
response = '[' + response + ']';
}
response = '{"items": ' + response + '}';
}
List<Map<String, Object>> rows =
new List<Map<String, Object>>();
Map<String, Object> responseBodyMap = (Map<String, Object>)
JSON.deserializeUntyped(response);
/**
* Checks errors.
**/
Map<String, Object> error =
(Map<String, Object>)responseBodyMap.get('error');
if (error!=null) {
List<Object> errorsList =
(List<Object>)error.get('errors');
Map<String, Object> errors =
(Map<String, Object>)errorsList[0];
String errorMessage = (String)errors.get('message');
throw new
DataSource.OAuthTokenExpiredException(errorMessage);
}
List<Object> fileItems =
(List<Object>)responseBodyMap.get('items');
if (fileItems != null) {
for (Integer i=0; i < fileItems.size(); i++) {
Map<String, Object> item =
(Map<String, Object>)fileItems[i];
rows.add(createRow(item));
}
} else {
rows.add(createRow(responseBodyMap));
}
return rows;
}
/**
* Helper method to populate the External ID and Display
* URL fields on external object records based on the 'id'
* value that’s sent by the external system.
*
* The Map<String, Object> item parameter maps to the data
* that represents a row.
*
* Returns an updated map with the External ID and
* Display URL values.
**/
public Map<String, Object> createRow(
Map<String, Object> item){
Map<String, Object> row = new Map<String, Object>();
for ( String key : item.keySet() ) {
if (key == 'login') {
row.put('ExternalId', item.get(key));
} else if (key=='html_url') {
row.put('DisplayUrl', item.get(key));
}
row.put(key, item.get(key));
}
return row;
}
/**
* Helper method to make the HTTP GET call.
* The url argument is the URL of the external system.
* Returns the response from the external system.
**/
public String getResponse(String url) {
// Perform callouts for production (non-test) results.
Http httpProtocol = new Http();
HttpRequest request = new HttpRequest();
request.setEndPoint(url);
request.setMethod('GET');
HttpResponse response = httpProtocol.send(request);
return response.getBody();
}
}
GitHubDataSourceProvider 类
/**
* Extends the DataSource.Provider base class to create a
* custom adapter for Salesforce Connect. The class informs
* Salesforce of the functional and authentication
* capabilities that are supported by or required to connect
* to an external system.
**/
global class GitHubDataSourceProvider
extends DataSource.Provider {
/**
* For simplicity, this example declares that the external
* system doesn’t require authentication by returning
* AuthenticationCapability.ANONYMOUS as the sole entry
* in the list of authentication capabilities.
**/
override global List<DataSource.AuthenticationCapability>
getAuthenticationCapabilities() {
List<DataSource.AuthenticationCapability> capabilities =
new List<DataSource.AuthenticationCapability>();
capabilities.add(
DataSource.AuthenticationCapability.ANONYMOUS);
return capabilities;
}
/**
* Declares the functional capabilities that the
* external system supports, in this case
* only SOQL queries.
**/
override global List<DataSource.Capability>
getCapabilities() {
List<DataSource.Capability> capabilities =
new List<DataSource.Capability>();
capabilities.add(DataSource.Capability.ROW_QUERY);
return capabilities;
}
/**
* Declares the associated DataSource.Connection class.
**/
override global DataSource.Connection getConnection(
DataSource.ConnectionParams connectionParams) {
return new GitHubDataSourceConnection(connectionParams);
}
}
适用于 Salesforce Connect 的 Stack Overflow 自定义适配器
此示例说明如何支持外部查找关系和多个 表。外部查找关系链接子标准对象、自定义对象或外部对象 添加到父外部对象。每个表都可以成为 Salesforce 中的外部对象 组织。
若要使此示例正常工作,请在“联系人”标准对象上创建一个自定义字段。将 自定义字段“github_username”,然后选择 和 属性。External IDUnique
StackOverflowDataSourceConnection 类
/**
* Defines the connection to Stack Exchange API v2.2 to support
* querying of Stack Overflow users (stackoverflowUser)
* and posts (stackoverflowPost).
* Extends the DataSource.Connection class to enable
* Salesforce to sync the external system’s schema
* and to handle queries of the external data.
**/
global class StackOverflowDataSourceConnection extends
DataSource.Connection {
private DataSource.ConnectionParams connectionInfo;
/**
* Constructor for StackOverflowDataSourceConnection
**/
global StackOverflowDataSourceConnection(
DataSource.ConnectionParams connectionInfo) {
this.connectionInfo = connectionInfo;
}
/**
* Defines the schema for the external system.
* Called when the administrator clicks “Validate and Sync”
* in the user interface for the external data source.
**/
override global List<DataSource.Table> sync() {
List<DataSource.Table> tables =
new List<DataSource.Table>();
// Defines columns for the table of Stack OverFlow posts
List<DataSource.Column> postColumns =
new List<DataSource.Column>();
// Defines the external lookup field.
postColumns.add(DataSource.Column.externalLookup(
'owner_id', 'stackoverflowUser__x'));
postColumns.add(DataSource.Column.text('title', 255));
postColumns.add(DataSource.Column.text('view_count', 255));
postColumns.add(DataSource.Column.text('question_id',255));
postColumns.add(DataSource.Column.text('creation_date',255));
postColumns.add(DataSource.Column.text('score',255));
postColumns.add(DataSource.Column.url('link'));
postColumns.add(DataSource.Column.url('DisplayUrl'));
postColumns.add(DataSource.Column.text('ExternalId',255));
tables.add(DataSource.Table.get('stackoverflowPost','title',
postColumns));
// Defines columns for the table of Stack OverFlow users
List<DataSource.Column> userColumns =
new List<DataSource.Column>();
userColumns.add(DataSource.Column.text('user_id', 255));
userColumns.add(DataSource.Column.text('display_name', 255));
userColumns.add(DataSource.Column.text('location',255));
userColumns.add(DataSource.Column.text('creation_date',255));
userColumns.add(DataSource.Column.url('website_url',255));
userColumns.add(DataSource.Column.text('reputation',255));
userColumns.add(DataSource.Column.url('link'));
userColumns.add(DataSource.Column.url('DisplayUrl'));
userColumns.add(DataSource.Column.text('ExternalId',255));
tables.add(DataSource.Table.get('stackoverflowUser',
'Display_name', userColumns));
return tables;
}
/**
* Called to query and get results from the external
* system for SOQL queries, list views, and detail pages
* for an external object that’s associated with the
* external data source.
*
* The QueryContext argument represents the query to run
* against a table in the external system.
*
* Returns a list of rows as the query results.
**/
override global DataSource.TableResult query(
DataSource.QueryContext context) {
DataSource.Filter filter = context.tableSelection.filter;
String url;
// Sets the URL to query Stack Overflow posts
if (context.tableSelection.tableSelected
.equals('stackoverflowPost')) {
if (filter != null) {
String thisColumnName = filter.columnName;
if (thisColumnName != null &&
thisColumnName.equals('ExternalId'))
url = 'https://api.stackexchange.com/2.2/'
+ 'questions/' + filter.columnValue
+ '?order=desc&sort=activity'
+ '&site=stackoverflow';
else
url = 'https://api.stackexchange.com/2.2/'
+ 'questions'
+ '?order=desc&sort=activity'
+ '&site=stackoverflow';
} else {
url = 'https://api.stackexchange.com/2.2/'
+ 'questions'
+ '?order=desc&sort=activity'
+ '&site=stackoverflow';
}
// Sets the URL to query Stack Overflow users
} else if (context.tableSelection.tableSelected
.equals('stackoverflowUser')) {
if (filter != null) {
String thisColumnName = filter.columnName;
if (thisColumnName != null &&
thisColumnName.equals('ExternalId'))
url = 'https://api.stackexchange.com/2.2/'
+ 'users/' + filter.columnValue
+ '?order=desc&sort=reputation'
+ '&site=stackoverflow';
else
url = 'https://api.stackexchange.com/2.2/'
+ 'users' +
'?order=desc&sort=reputation&site=stackoverflow';
} else {
url = 'https://api.stackexchange.com/2.2/'
+ 'users' + '?order=desc&sort=reputation'
+ '&site=stackoverflow';
}
}
/**
* Filters, sorts, and applies limit and offset clauses.
**/
List<Map<String, Object>> rows =
DataSource.QueryUtils.process(context, getData(url));
return DataSource.TableResult.get(true, null,
context.tableSelection.tableSelected, rows);
}
/**
* Helper method to parse the data.
* The url argument is the URL of the external system.
* Returns a list of rows from the external system.
**/
public List<Map<String, Object>> getData(String url) {
String response = getResponse(url);
List<Map<String, Object>> rows =
new List<Map<String, Object>>();
Map<String, Object> responseBodyMap = (Map<String, Object>)
JSON.deserializeUntyped(response);
/**
* Checks errors.
**/
Map<String, Object> error =
(Map<String, Object>)responseBodyMap.get('error');
if (error!=null) {
List<Object> errorsList =
(List<Object>)error.get('errors');
Map<String, Object> errors =
(Map<String, Object>)errorsList[0];
String errorMessage = (String)errors.get('message');
throw new
DataSource.OAuthTokenExpiredException(errorMessage);
}
List<Object> fileItems=
(List<Object>)responseBodyMap.get('items');
if (fileItems != null) {
for (Integer i=0; i < fileItems.size(); i++) {
Map<String, Object> item =
(Map<String, Object>)fileItems[i];
rows.add(createRow(item));
}
} else {
rows.add(createRow(responseBodyMap));
}
return rows;
}
/**
* Helper method to populate the External ID and Display
* URL fields on external object records based on the 'id'
* value that’s sent by the external system.
*
* The Map<String, Object> item parameter maps to the data
* that represents a row.
*
* Returns an updated map with the External ID and
* Display URL values.
**/
public Map<String, Object> createRow(
Map<String, Object> item) {
Map<String, Object> row = new Map<String, Object>();
for ( String key : item.keySet() ) {
if (key.equals('question_id') || key.equals('user_id')) {
row.put('ExternalId', item.get(key));
} else if (key.equals('link')) {
row.put('DisplayUrl', item.get(key));
} else if (key.equals('owner')) {
Map<String, Object> ownerMap =
(Map<String, Object>)item.get(key);
row.put('owner_id', ownerMap.get('user_id'));
}
row.put(key, item.get(key));
}
return row;
}
/**
* Helper method to make the HTTP GET call.
* The url argument is the URL of the external system.
* Returns the response from the external system.
**/
public String getResponse(String url) {
// Perform callouts for production (non-test) results.
Http httpProtocol = new Http();
HttpRequest request = new HttpRequest();
request.setEndPoint(url);
request.setMethod('GET');
HttpResponse response = httpProtocol.send(request);
return response.getBody();
}
}
StackOverflowPostDataSourceProvider Class
/**
* Extends the DataSource.Provider base class to create a
* custom adapter for Salesforce Connect. The class informs
* Salesforce of the functional and authentication
* capabilities that are supported by or required to connect
* to an external system.
**/
global class StackOverflowPostDataSourceProvider
extends DataSource.Provider {
/**
* For simplicity, this example declares that the external
* system doesn’t require authentication by returning
* AuthenticationCapability.ANONYMOUS as the sole entry
* in the list of authentication capabilities.
**/
override global List<DataSource.AuthenticationCapability>
getAuthenticationCapabilities() {
List<DataSource.AuthenticationCapability> capabilities =
new List<DataSource.AuthenticationCapability>();
capabilities.add(
DataSource.AuthenticationCapability.ANONYMOUS);
return capabilities;
}
/**
* Declares the functional capabilities that the
* external system supports, in this case
* only SOQL queries.
**/
override global List<DataSource.Capability>
getCapabilities() {
List<DataSource.Capability> capabilities =
new List<DataSource.Capability>();
capabilities.add(DataSource.Capability.ROW_QUERY);
return capabilities;
}
/**
* Declares the associated DataSource.Connection class.
**/
override global DataSource.Connection getConnection(
DataSource.ConnectionParams connectionParams) {
return new
StackOverflowDataSourceConnection(connectionParams);
}
}