mcpField
U
using-ui-bundle-salesforce-data

using-ui-bundle-salesforce-data

github.com/forcedotcom开发1.1k3.6k 次安装1 天前更新
代码仓库

关于 using-ui-bundle-salesforce-data

用于在 React UI bundle 中使用 `@salesforce/sdk-data` 构建 Salesforce 数据访问的技能。它优先采用经 Schema 验证的 GraphQL 查询记录,并将 REST 调用限制在明确支持的端点。

主要功能

  1. 01使用 Salesforce Data SDK
  2. 02依据 GraphQL Schema 验证字段
  3. 03通过 GraphQL 查询和修改记录
  4. 04应用分页与字段访问防护
  5. 05仅调用支持的 REST 端点

适用场景

  1. 01为 Salesforce UI bundle 构建记录查询
  2. 02添加记录创建、更新或删除操作
  3. 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方法端点 / 使用场景
GraphQLsdk.graphql通过 uiapi { } 命名空间查询和变更所有记录
UI API RESTsdk.fetch/services/data/v{ver}/ui-api/records/{id} — GraphQL 无法满足需求时获取记录元数据
Apex RESTsdk.fetch/services/apexrest/{resource} — 自定义服务器端逻辑、聚合、多步骤事务
Connect RESTsdk.fetch/services/data/v{ver}/connect/file/upload/config — 文件上传配置
Einstein LLMsdk.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.graphqlAccount、Contact、自定义对象
当前用户信息sdk.graphqluiapi { currentUser { Id Name { value } } }
UI API 记录元数据sdk.fetch/ui-api/records/{id}
Connect RESTsdk.fetch/connect/file/upload/config
Apex RESTsdk.fetch/services/apexrest/auth/login
Einstein LLMsdk.fetch/einstein/llm/prompt/generations

记录操作优先使用 GraphQL。只有在 GraphQL 无法满足使用场景时才使用 REST。


GraphQL 不可妥协规则

这些规则的存在是因为 Salesforce GraphQL 具有不同于标准 GraphQL 的平台特定行为。违反规则会导致运行时静默失败。

  1. HTTP 200 不代表成功 — 即使操作失败,Salesforce 也会返回 HTTP 200。务必解析响应正文中的 errors 数组。

  2. Schema 是唯一可信来源 — 在查询中使用任何实体名称、字段名称和类型之前,都必须通过 schema 搜索脚本确认。切勿猜测——Salesforce 字段名称区分大小写,关系可能是多态的,自定义对象使用后缀(__c、__e)。在 v60+ 中添加到 UI API 的对象可能使用 _Record 后缀(例如,使用 FeedItem_Record 而非 FeedItem)。

  3. 所有记录字段都加上 @optional(读取查询)— 如果用户缺少哪怕一个字段的访问权限,Salesforce 字段级安全性(FLS)就会导致整个查询失败。@optional 指令(v65+)会告知服务端省略无法访问的字段,而不是让查询失败。对每个标量字段、父级关系和子级关系都应用此指令。调用代码必须使用可选链(?.)和空值合并(??)。

  4. 使用正确的 mutation 语法 — Mutation 必须封装在 uiapi(input: { allOrNone: true/false }) 下,而不是直接使用 uiapi { ... }。务必显式设置 allOrNone。输出字段不能包含子级关系或导航引用字段。

  5. 显式分页 — 每个查询都必须包含 first:。如果省略,服务端会静默默认为 10 条记录。任何可能需要分页的查询都要包含 pageInfo { hasNextPage endCursor }。仅支持向前分页(first/after)——不支持 last/before。

  6. 基于 SOQL 的执行限制 — 每个请求最多 10 个子查询,子级到父级最多遍历 5 层,父级到子级最多遍历 1 层(不支持孙级),每个子查询最多返回 2,000 条记录。如果查询会超出这些限制,请拆分为多个请求。

  7. 仅使用所请求的字段 — 只生成用户明确要求的字段。切勿添加额外字段。

  8. 复合字段 — 进行筛选或排序时,使用组成字段(例如,BillingCity、BillingCountry),而不是复合封装字段(BillingAddress)。复合封装字段仅用于选择。


GraphQL 工作流

步骤操作关键输出
1获取 schemaschema.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

脚本会为每个实体输出七个部分:

  1. 类型定义 — 所有可查询字段和关系
  2. 筛选选项 — where: 条件可用的字段
  3. 排序选项 — orderBy: 可用的字段
  4. 创建 mutation 封装 — <Entity>CreateInput
  5. 创建 mutation 字段 — <Entity>CreateRepresentation(创建 mutation 接受的字段)
  6. 更新 mutation 封装 — <Entity>UpdateInput
  7. 更新 mutation 字段 — <Entity>UpdateRepresentation(更新 mutation 接受的字段)

脚本最多运行 2 次。 如果仍找不到实体,请询问用户——该对象可能尚未部署。

实体识别

如果候选项不匹配:

  • 自定义对象尝试使用 __c 后缀,平台事件尝试使用 __e 后缀
  • 尝试使用 _Record 后缀 — 在 v60+ 中添加的对象可能使用 <EntityName>_Record
  • 如果仍无法确定,请询问用户 — 不要猜测
迭代式内省(最多 3 个周期)
  1. 内省 — 为每个未确定的实体运行脚本
  2. 字段 — 从类型定义中提取所请求字段的名称和类型
  3. 引用 — 识别引用字段。如果是多态字段(多种类型),使用内联片段。将新发现的实体类型添加到工作列表。
  4. 子关系 — 识别 Connection 类型。将子实体类型添加到工作列表。
  5. 如果仍有未确定的实体,则重复(最多 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 访问:

包装类型底层类型包装类型底层类型
StringValueStringBooleanValueBoolean
IntValueIntDoubleValueDouble
CurrencyValueCurrencyPercentValuePercent
DateTimeValueDateTimeDateValueDate
PicklistValuePicklistLongValueLong
IDValueIDTextAreaValueTextArea
EmailValueEmailPhoneNumberValuePhoneNumber
UrlValueUrl

所有包装类型还会公开 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:验证和测试
  1. Lint:在 UI bundle 目录中运行 npx eslint <file>
  2. 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:schemaUI bundle 目录脚本位于 UI bundle 的 package.json 中
npm run graphql:codegenUI 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>

评论

支持 Markdown