关于 using-ui-bundle-salesforce-data
用于在 React UI bundle 中使用 `@salesforce/sdk-data` 构建 Salesforce 数据访问的技能。它优先采用经 Schema 验证的 GraphQL 查询记录,并将 REST 调用限制在明确支持的端点。
主要功能
- 01使用 Salesforce Data SDK
- 02依据 GraphQL Schema 验证字段
- 03通过 GraphQL 查询和修改记录
- 04应用分页与字段访问防护
- 05仅调用支持的 REST 端点
适用场景
- 01为 Salesforce UI bundle 构建记录查询
- 02添加记录创建、更新或删除操作
- 03在 GraphQL 不足时获取记录元数据
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ forcedotcom/ sf-skills --skill using-ui-bundle-salesforce-data安装后在 Claude Code 中输入/using-ui-bundle-salesforce-data确认已加载。
附带文件scripts/graphql-search.sh
详细介绍
Salesforce 数据访问
Data SDK 要求
所有 Salesforce 数据访问都必须使用 Data SDK(
@salesforce/sdk-data)。该 SDK 会处理身份验证、CSRF 和基础 URL 解析。
import { createDataSDK, gql } from "@salesforce/sdk-data";
import type { ResponseTypeQuery } from "../graphql-operations-types";
const sdk = await createDataSDK();
// GraphQL for record queries/mutations (PREFERRED)
const response = await sdk.graphql?.<ResponseTypeQuery>(query, variables);
// REST for Connect REST, Apex REST, UI API (when GraphQL insufficient)
const res = await sdk.fetch?.("/services/apexrest/my-resource");
始终使用可选链(sdk.graphql?.()、sdk.fetch?.())——这些方法在某些界面中可能未定义。
前置条件 — 开始前请先验证
| # | 要求 | 验证方法 | 缺少时的处理 |
|---|---|---|---|
| 1 | 已安装 @salesforce/sdk-data | 检查 UI bundle 目录中的 package.json | 无法继续——告知用户安装该包 |
| 2 | 项目根目录中存在 schema.graphql | 检查文件是否存在 | 在 UI bundle 目录中运行 npm run graphql:schema |
| 3 | 已部署自定义对象/字段 | 运行 graphql-search.sh <Entity>——无输出表示尚未部署 | 请用户部署元数据并分配权限集 |
如果未满足前置条件,你可以搭建组件、路由、布局和 UI 逻辑,但数据请使用空数组 / null,并在查询位置标记 // TODO: add query after schema verification;同时在计划中注明后续需返回处理相关要求并编写 GraphQL。在完成 schema 工作流之前,不要编写 GraphQL 查询字符串。
支持的 API
**仅允许使用以下 API。**不得使用此处未列出的任何端点。
| API | 方法 | 端点 / 使用场景 |
|---|---|---|
| GraphQL | sdk.graphql | 通过 uiapi { } 命名空间查询和变更所有记录 |
| UI API REST | sdk.fetch | /services/data/v{ver}/ui-api/records/{id} — GraphQL 无法满足需求时获取记录元数据 |
| Apex REST | sdk.fetch | /services/apexrest/{resource} — 自定义服务器端逻辑、聚合、多步骤事务 |
| Connect REST | sdk.fetch | /services/data/v{ver}/connect/file/upload/config — 文件上传配置 |
| Einstein LLM | sdk.fetch | /services/data/v{ver}/einstein/llm/prompt/generations — AI 文本生成 |
不支持:
- Enterprise REST 查询端点(通过 SOQL 访问
/services/data/v*/query)——在代理层被拦截。读取记录请使用 GraphQL;如果需要服务器端 SOQL 聚合,请使用 Apex REST。 - 启用 Aura 的 Apex(
@AuraEnabled)——这是一种无法从 React UI bundle 调用的 LWC/Aura 模式。 - Chatter API(
/chatter/users/me)——请改用 GraphQL 查询中的uiapi { currentUser { ... } }。 - 以上支持列表中未列出的任何其他 Salesforce REST 端点。
决策:GraphQL 还是 REST
| 需求 | 方法 | 示例 |
|---|---|---|
| 查询/变更记录 | sdk.graphql | Account、Contact、自定义对象 |
| 当前用户信息 | sdk.graphql | uiapi { currentUser { Id Name { value } } } |
| UI API 记录元数据 | sdk.fetch | /ui-api/records/{id} |
| Connect REST | sdk.fetch | /connect/file/upload/config |
| Apex REST | sdk.fetch | /services/apexrest/auth/login |
| Einstein LLM | sdk.fetch | /einstein/llm/prompt/generations |
记录操作优先使用 GraphQL。只有在 GraphQL 无法满足使用场景时才使用 REST。
GraphQL 不可妥协规则
这些规则的存在是因为 Salesforce GraphQL 具有不同于标准 GraphQL 的平台特定行为。违反规则会导致运行时静默失败。
-
HTTP 200 不代表成功 — 即使操作失败,Salesforce 也会返回 HTTP 200。务必解析响应正文中的
errors数组。 -
Schema 是唯一可信来源 — 在查询中使用任何实体名称、字段名称和类型之前,都必须通过 schema 搜索脚本确认。切勿猜测——Salesforce 字段名称区分大小写,关系可能是多态的,自定义对象使用后缀(
__c、__e)。在 v60+ 中添加到 UI API 的对象可能使用_Record后缀(例如,使用FeedItem_Record而非FeedItem)。 -
所有记录字段都加上
@optional(读取查询)— 如果用户缺少哪怕一个字段的访问权限,Salesforce 字段级安全性(FLS)就会导致整个查询失败。@optional指令(v65+)会告知服务端省略无法访问的字段,而不是让查询失败。对每个标量字段、父级关系和子级关系都应用此指令。调用代码必须使用可选链(?.)和空值合并(??)。 -
使用正确的 mutation 语法 — Mutation 必须封装在
uiapi(input: { allOrNone: true/false })下,而不是直接使用uiapi { ... }。务必显式设置allOrNone。输出字段不能包含子级关系或导航引用字段。 -
显式分页 — 每个查询都必须包含
first:。如果省略,服务端会静默默认为 10 条记录。任何可能需要分页的查询都要包含pageInfo { hasNextPage endCursor }。仅支持向前分页(first/after)——不支持last/before。 -
基于 SOQL 的执行限制 — 每个请求最多 10 个子查询,子级到父级最多遍历 5 层,父级到子级最多遍历 1 层(不支持孙级),每个子查询最多返回 2,000 条记录。如果查询会超出这些限制,请拆分为多个请求。
-
仅使用所请求的字段 — 只生成用户明确要求的字段。切勿添加额外字段。
-
复合字段 — 进行筛选或排序时,使用组成字段(例如,
BillingCity、BillingCountry),而不是复合封装字段(BillingAddress)。复合封装字段仅用于选择。
GraphQL 工作流
| 步骤 | 操作 | 关键输出 |
|---|---|---|
| 1 | 获取 schema | schema.graphql 已存在 |
| 2 | 查找实体 | 已确认字段名称、类型和关系 |
| 3 | 生成查询 | .graphql 文件或内联 gql 标签 |
| 4 | 生成类型 | graphql-operations-types.ts |
| 5 | 验证 | Lint 和 codegen 通过 |
步骤 1:获取 Schema
schema.graphql 文件(超过 265K 行)是唯一可信来源。切勿直接打开或解析它——不得使用 cat、less、head、tail、编辑器或程序化解析器。
验证前置条件 1–3(参见前置条件 — 开始前进行验证),然后继续步骤 2。
步骤 2:查找实体 Schema
将用户意图映射到 PascalCase 名称(“accounts” → Account),然后从 sfdx-project 文件夹(项目根目录)运行搜索脚本:
bash scripts/graphql-search.sh Account
# Multiple entities:
bash scripts/graphql-search.sh Account Contact Opportunity
脚本会为每个实体输出七个部分:
- 类型定义 — 所有可查询字段和关系
- 筛选选项 —
where:条件可用的字段 - 排序选项 —
orderBy:可用的字段 - 创建 mutation 封装 —
<Entity>CreateInput - 创建 mutation 字段 —
<Entity>CreateRepresentation(创建 mutation 接受的字段) - 更新 mutation 封装 —
<Entity>UpdateInput - 更新 mutation 字段 —
<Entity>UpdateRepresentation(更新 mutation 接受的字段)
脚本最多运行 2 次。 如果仍找不到实体,请询问用户——该对象可能尚未部署。
实体识别
如果候选项不匹配:
- 自定义对象尝试使用
__c后缀,平台事件尝试使用__e后缀 - 尝试使用
_Record后缀 — 在 v60+ 中添加的对象可能使用<EntityName>_Record - 如果仍无法确定,请询问用户 — 不要猜测
迭代式内省(最多 3 个周期)
- 内省 — 为每个未确定的实体运行脚本
- 字段 — 从类型定义中提取所请求字段的名称和类型
- 引用 — 识别引用字段。如果是多态字段(多种类型),使用内联片段。将新发现的实体类型添加到工作列表。
- 子关系 — 识别 Connection 类型。将子实体类型添加到工作列表。
- 如果仍有未确定的实体,则重复(最多 3 个周期)
硬性停止条件: 如果未返回某个实体的数据,则停止 — 该实体可能尚未部署。如果 3 个周期后仍有未知实体,请询问用户。不要使用未经确认的实体或字段生成查询。
步骤 3:生成查询
每个字段名称都必须根据步骤 2 的脚本输出进行验证。
读取查询模板
query QueryName($after: String) {
uiapi {
query {
EntityName(
first: 10
after: $after
where: { ... }
orderBy: { ... }
) {
edges {
node {
Id
FieldName @optional { value }
# Parent relationship (non-polymorphic)
Owner @optional { Name { value } }
# Parent relationship (polymorphic — use fragments)
What @optional {
...WhatAccount
...WhatOpportunity
}
# Child relationship — max 1 level, no grandchildren
Contacts @optional(first: 10) {
edges { node { Name @optional { value } } }
}
}
}
pageInfo { hasNextPage endCursor }
}
}
}
}
fragment WhatAccount on Account {
Id
Name @optional { value }
}
fragment WhatOpportunity on Opportunity {
Id
Name @optional { value }
}
使用代码必须防范字段缺失:
const name = node.Name?.value ?? "";
const relatedName = node.Owner?.Name?.value ?? "N/A";
筛选
# Implicit AND
Account(where: { Industry: { eq: "Technology" }, AnnualRevenue: { gt: 1000000 } })
# Explicit OR
Account(where: { OR: [{ Industry: { eq: "Technology" } }, { Industry: { eq: "Finance" } }] })
# NOT
Account(where: { NOT: { Industry: { eq: "Technology" } } })
# Date literal
Opportunity(where: { CloseDate: { eq: { value: "2024-12-31" } } })
# Relative date
Opportunity(where: { CloseDate: { gte: { literal: TODAY } } })
# Relationship filter (nested objects, NOT dot notation)
Contact(where: { Account: { Name: { like: "Acme%" } } })
# Polymorphic relationship filter
Account(where: { Owner: { User: { Username: { like: "admin%" } } } })
字符串相等(eq)比较不区分大小写。接受 15 位和 18 位记录 ID。
排序
Account(
first: 10,
orderBy: { Name: { order: ASC }, CreatedDate: { order: DESC } }
) { ... }
不支持排序的字段:多选选项列表、富文本、长文本区域、加密字段。添加 Id 作为决胜字段,以确保排序结果确定。
UpperBound 分页(v59+)
每页超过 200 条记录或总计超过 4,000 条记录时,请使用 upperBound。设置时,first 必须介于 200–2000 之间。
Account(first: 2000, after: $cursor, upperBound: 10000) {
edges { node { Id Name @optional { value } } }
pageInfo { hasNextPage endCursor }
}
半连接和反连接
使用父实体 Id 上的 inq(半连接)或 ninq(反连接),根据子实体上的条件筛选父实体。如果唯一条件是子实体是否存在,请使用 Id: { ne: null }。
query SemiJoinExample {
uiapi {
query {
Account(where: {
Id: {
inq: {
Contact: { LastName: { like: "Smith%" } }
ApiName: "AccountId"
}
}
}, first: 10) {
edges { node { Id Name @optional { value } } }
}
}
}
}
反连接时,将 inq 替换为 ninq。限制:子查询中不能使用 OR,子查询中不能使用 orderBy,连接之间不能嵌套。
当前用户
请使用 uiapi.currentUser(不带参数),而非标准查询模式:
query CurrentUser {
uiapi { currentUser { Id Name { value } } }
}
字段值包装类型
架构字段使用有类型的包装类型 — 通过 .value 访问:
| 包装类型 | 底层类型 | 包装类型 | 底层类型 |
|---|---|---|---|
StringValue | String | BooleanValue | Boolean |
IntValue | Int | DoubleValue | Double |
CurrencyValue | Currency | PercentValue | Percent |
DateTimeValue | DateTime | DateValue | Date |
PicklistValue | Picklist | LongValue | Long |
IDValue | ID | TextAreaValue | TextArea |
EmailValue | Email | PhoneNumberValue | PhoneNumber |
UrlValue | Url |
所有包装类型还会公开 displayValue: String(通过 toLabel()/format() 在服务端渲染)— 用于 UI 显示,而不要在客户端进行格式化。
变更模板
变更操作在 API v66+ 中正式发布。三种操作:创建、更新、删除。
# Create
mutation CreateAccount($input: AccountCreateInput!) {
uiapi(input: { allOrNone: true }) {
AccountCreate(input: $input) {
Record { Id Name { value } }
}
}
}
# Update — must include Id
mutation UpdateAccount {
uiapi(input: { allOrNone: true }) {
AccountUpdate(input: { Id: "001xx000003GYkZAAW", Account: { Name: "New Name" } }) {
Record { Id Name { value } }
}
}
}
输入限制:
- 创建:必填字段(除非设置了
defaultedOnCreate),仅限createable字段,不支持子关系。引用字段通过ApiName设置(例如,AccountId)。 - 更新:必须包含
Id,仅限updateable字段,不支持子关系。 - 删除:仅限
Id。 IdOrRef类型:更新和删除输入中的Id字段使用IdOrRef类型,可接受字面记录 ID(例如,"001xx...")或变更链式引用("@{Alias}")。创建输入中的引用字段(例如,AccountId)也接受@{Alias},用于链式引用。- 原始值:不能包含逗号、货币符号或区域格式(例如,使用
80000,而非"$80,000")。
输出限制:
- 创建/更新:排除子关系,排除导航引用字段(仅允许
ApiName成员)。输出字段始终命名为Record。 - 删除:仅限
Id。
allOrNone 语义:
true(默认)——所有操作全部成功,否则全部回滚。false——各操作独立成功,但相互依赖的操作(使用@{alias})仍会一起回滚。
变更链式引用
使用 @{alias} 引用之前变更操作的 Id,以链接相关变更。创建父子记录时必须使用此方式(不支持嵌套子记录创建)。
mutation CreateAccountAndContact {
uiapi(input: { allOrNone: true }) {
AccountCreate(input: { Account: { Name: "Acme" } }) {
Record { Id }
}
ContactCreate(input: { Contact: { LastName: "Smith", AccountId: "@{AccountCreate}" } }) {
Record { Id }
}
}
}
规则:A 必须在查询中先于 B。@{A} 始终是变更操作 A 的 Id。只能从创建或删除操作链接(不能从更新操作链接)。
删除变更
删除使用通用的 RecordDeleteInput(非实体专用)。输出仅包含 Id,没有 Record 字段。
mutation DeleteAccount($id: ID!) {
uiapi(input: { allOrNone: true }) {
AccountDelete(input: { Id: $id }) {
Id
}
}
}
对象元数据和选项列表值
使用 uiapi { objectInfos(...) } 获取字段元数据或选项列表值。传入 apiNames 或 objectInfoInputs 其中之一,不能同时传入两者。
// Object metadata
const GET_OBJECT_INFO = gql`
query GetObjectInfo($apiNames: [String!]!) {
uiapi {
objectInfos(apiNames: $apiNames) {
ApiName
label
labelPlural
fields { ApiName label dataType updateable createable }
}
}
}
`;
// Picklist values (use objectInfoInputs + inline fragment)
const GET_PICKLIST_VALUES = gql`
query GetPicklistValues($objectInfoInputs: [ObjectInfoInput!]!) {
uiapi {
objectInfos(objectInfoInputs: $objectInfoInputs) {
ApiName
fields {
ApiName
... on PicklistField {
picklistValuesByRecordTypeIDs {
recordTypeID
picklistValues { label value }
}
}
}
}
}
}
`;
步骤 4:生成类型(codegen)
编写查询后(无论是在 .graphql 文件中,还是内联使用 gql),都要生成 TypeScript 类型:
# Run from UI bundle dir
npm run graphql:codegen
输出:src/api/graphql-operations-types.ts
生成的类型命名约定:
<OperationName>Query/<OperationName>Mutation—— 响应类型<OperationName>QueryVariables/<OperationName>MutationVariables—— 变量类型
调用 sdk.graphql 时始终导入并使用生成的类型:
import type { GetAccountsQuery, GetAccountsQueryVariables } from "../graphql-operations-types";
const response = await sdk.graphql?.<GetAccountsQuery, GetAccountsQueryVariables>(GET_ACCOUNTS, variables);
使用 NodeOfConnection<T> 从 Connection 中提取节点类型,以获得更清晰的类型标注:
import { type NodeOfConnection } from "@salesforce/sdk-data";
type AccountNode = NodeOfConnection<GetAccountsQuery["uiapi"]["query"]["Account"]>;
步骤 5:验证和测试
- Lint:在 UI bundle 目录中运行
npx eslint <file> - codegen:在 UI bundle 目录中运行
npm run graphql:codegen
常见错误模式
| 错误包含 | 解决方法 |
|---|---|
Cannot query field / ValidationError | 字段名称错误——重新运行 graphql-search.sh <Entity> |
Unknown type | 类型名称错误——通过脚本验证实体名称是否为 PascalCase |
Unknown argument | 参数错误——检查脚本输出中的 Filter/OrderBy 部分 |
invalid syntax / InvalidSyntax | 根据错误消息修正语法 |
VariableTypeMismatch / UnknownType | 根据架构修正参数类型 |
invalid cross reference id | 实体已删除——请求有效的 Id |
OperationNotSupported | 检查对象是否可用以及 API 版本 |
is not currently available in mutation results | 从变更输出中移除该字段 |
Cannot invoke JsonElement.isJsonObject() | 更新变更选择 Record 时使用 API 版本 64+ |
出现 PARTIAL 时 如果变更同时返回数据和错误(部分成功):报告无法访问的字段,说明这些字段不能出现在变更输出中,并询问是否移除。等待用户同意后再进行更改。
UI Bundle 集成(React)
两种集成模式:
模式 1 — 外部 .graphql 文件(复杂查询)
每个 .graphql 文件只包含一个操作。 每个文件必须且只能包含一个 query 或 mutation(以及其片段)。请勿在单个文件中组合多个操作。
import { createDataSDK, type NodeOfConnection } from "@salesforce/sdk-data";
import MY_QUERY from "./query/myQuery.graphql?raw"; // ?raw suffix required
import type { GetMyDataQuery, GetMyDataQueryVariables } from "../graphql-operations-types";
const sdk = await createDataSDK();
const response = await sdk.graphql?.<GetMyDataQuery, GetMyDataQueryVariables>(MY_QUERY, variables);
创建或更改 .graphql 文件后,运行 npm run graphql:codegen,将类型生成到 src/api/graphql-operations-types.ts。
模式 2 — 内联 gql 标签(简单查询)
必须使用 gql——普通模板字符串会绕过 ESLint 模式验证。
import { createDataSDK, gql } from "@salesforce/sdk-data";
import type { GetAccountsQuery } from "../graphql-operations-types";
const GET_ACCOUNTS = gql`
query GetAccounts {
uiapi {
query {
Account(first: 10) {
edges { node { Id Name @optional { value } } }
}
}
}
}
`;
const sdk = await createDataSDK();
const response = await sdk.graphql?.<GetAccountsQuery>(GET_ACCOUNTS);
错误处理
// Strict (default) — any errors = failure
if (response?.errors?.length) {
throw new Error(response.errors.map(e => e.message).join("; "));
}
// Tolerant — log errors, use available data
if (response?.errors?.length) {
console.warn("GraphQL partial errors:", response.errors);
}
// Discriminated — fail only when no data returned
if (!response?.data && response?.errors?.length) {
throw new Error(response.errors.map(e => e.message).join("; "));
}
const accounts = response?.data?.uiapi?.query?.Account?.edges?.map(e => e.node) ?? [];
REST API 模式
GraphQL 无法满足需求时使用 sdk.fetch。完整许可列表请参见 支持的 API 表格。
declare const __SF_API_VERSION__: string;
const API_VERSION = typeof __SF_API_VERSION__ !== "undefined" ? __SF_API_VERSION__ : "65.0";
// Connect — file upload config
const res = await sdk.fetch?.(`/services/data/v${API_VERSION}/connect/file/upload/config`);
// Apex REST (no version in path)
const res = await sdk.fetch?.("/services/apexrest/auth/login", {
method: "POST",
body: JSON.stringify({ email, password }),
headers: { "Content-Type": "application/json" },
});
// UI API — record with metadata (prefer GraphQL for simple reads)
const res = await sdk.fetch?.(`/services/data/v${API_VERSION}/ui-api/records/${recordId}`);
// Einstein LLM
const res = await sdk.fetch?.(`/services/data/v${API_VERSION}/einstein/llm/prompt/generations`, {
method: "POST",
body: JSON.stringify({ promptTextorId: prompt }),
});
当前用户:请勿使用 Chatter(/chatter/users/me)。请改用 GraphQL:
const GET_CURRENT_USER = gql`
query CurrentUser {
uiapi { currentUser { Id Name { value } } }
}
`;
const response = await sdk.graphql?.(GET_CURRENT_USER);
目录结构
<project-root>/ ← SFDX project root
├── schema.graphql ← grep target (lives here)
├── sfdx-project.json
├── scripts/graphql-search.sh ← schema lookup script
└── force-app/main/default/uiBundles/<app-name>/ ← UI bundle dir
├── package.json ← npm scripts
└── src/
| 命令 | 运行目录 | 用途 |
|---|---|---|
npm run graphql:schema | UI bundle 目录 | 脚本位于 UI bundle 的 package.json 中 |
npm run graphql:codegen | UI bundle 目录 | 生成 GraphQL 类型 |
npx eslint <file> | UI bundle 目录 | 读取 eslint.config.js |
bash scripts/graphql-search.sh <Entity> | 项目根目录 | 查询模式 |
快速参考
模式查询(从项目根目录运行)
运行搜索脚本,一步获取所有相关模式信息:
bash scripts/graphql-search.sh <EntityName>
| 脚本输出部分 | 用途 |
|---|---|
| 类型定义 | 字段名称、父子关系 |
| 筛选选项 | where: 条件 |
| 排序选项 | orderBy: |
| CreateRepresentation | 创建 mutation 字段列表 |
| UpdateRepresentation | 更新 mutation 字段列表 |
错误类别
| 错误包含内容 | 解决方法 |
|---|---|
Cannot query field | 字段名称错误——运行 graphql-search.sh <Entity>,并使用“类型定义”部分中的准确名称 |
Unknown type | 类型名称错误——运行 graphql-search.sh <Entity>,确认正确的 PascalCase 实体名称 |
Unknown argument | 参数名称错误——运行 graphql-search.sh <Entity>,检查筛选或 OrderBy 部分 |
invalid syntax | 根据错误消息修正语法 |
validation error | 字段名称错误——运行 graphql-search.sh <Entity> 进行验证 |
VariableTypeMismatch | 使用模式中的正确参数类型 |
invalid cross reference id | 实体已删除——请获取有效的 Id |
检查清单
- 已通过搜索脚本(第 2 步)验证所有字段名称
- 所有记录字段(读取操作)均已应用
@optional - Mutations 使用
uiapi(input: { allOrNone: ... })包装器 - 每个查询均指定了
first: - 消费代码中使用了可选链
- 响应处理时检查了
errors数组 - Lint 检查通过:
npx eslint <file>