experience-ui-bundle-salesforce-data-access

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Salesforce Data Access (UI bundles)

Salesforce数据访问(UI bundles)

All Salesforce data access in a UI bundle goes through the
@salesforce/platform-sdk
data SDK. The SDK handles auth, CSRF, and base-URL resolution, and — on the WebApp surface — caches every GraphQL query by default.
This file is the workflow + guardrail spine. Depth lives in linked docs:
  • references/graphiti-cli.md — the
    graphiti
    CLI
    (
    sf-gql-*
    commands) that compiles a small JSON spec into a schema-correct, guardrail-applied query + variables + types. The preferred way to author the GraphQL in steps below; falls back to the schema-grep script when unavailable.
  • references/sdk-api.md — the new call API:
    query
    /
    mutate
    ,
    QueryResult
    , typing, error-handling stances.
  • references/caching.md — on-by-default cache + the two refresh modes (
    result.refresh
    /
    subscribe
    vs per-call
    cacheControl
    ).
  • references/graphql-hand-authoring.md — schema lookup, read / mutation templates, every platform guardrail (
    @optional
    , pagination, limits, semi-join, wrappers, error table…).
  • references/rest-and-integration.md
    sdk.fetch
    , the supported-API allowlist, and the reactive/lifecycle integration patterns.
  • references/migration.md — old
    @salesforce/sdk-data
    callable code → new namespace. The only place the dead API appears as usable code.
UI bundle中所有的Salesforce数据访问都需通过**
@salesforce/platform-sdk
**数据SDK实现。该SDK负责处理认证、CSRF防护和基础URL解析,并且在WebApp环境下,默认会缓存所有GraphQL查询结果。
本文件是工作流规范与防护约束的核心,详细内容请参考关联文档:
  • references/graphiti-cli.md ——
    graphiti
    CLI
    sf-gql-*
    命令),可将小型JSON规范编译为符合Schema要求、带有防护约束的查询语句、变量及类型定义。这是下文步骤中编写GraphQL的首选方式;若无法使用,则可回退到schema-grep脚本。
  • references/sdk-api.md —— 新的调用API:
    query
    /
    mutate
    QueryResult
    、类型定义、错误处理策略。
  • references/caching.md —— 默认启用的缓存机制,以及两种刷新模式
    result.refresh
    /
    subscribe
    vs 单次调用的
    cacheControl
    )。
  • references/graphql-hand-authoring.md —— Schema查询、读取/突变模板、所有平台防护约束(
    @optional
    、分页、限制、半连接、包装器、错误表等)。
  • references/rest-and-integration.md ——
    sdk.fetch
    、支持的API白名单,以及响应式/生命周期集成模式。
  • references/migration.md —— 旧版
    @salesforce/sdk-data
    可调用代码向新命名空间的迁移指南。这是唯一提及已废弃API可用代码的文档。

The one-paragraph mental model

核心认知模型

const sdk = await createDataSDK()
. Then
sdk.graphql
is a namespace, not a function:
sdk.graphql!.query({...})
for reads,
sdk.graphql!.mutate({...})
for writes. On WebApp, every
query()
is cached by default
(300s). HTTP 200 never means success — always check
result.errors
. Verify every entity and field against the schema before you query it: one unverified field fails the whole query at runtime, and
schema.graphql
is too large to eyeball — look it up.
typescript
import { createDataSDK, gql } from "@salesforce/platform-sdk"; // gql tags the query string so codegen + eslint validate it

const sdk = await createDataSDK();
const result = await sdk.graphql!.query({ query: GET_ACCOUNTS, variables });
if (result.errors?.length) throw new Error(result.errors.map((e) => e.message).join("; "));
const rows = result.data?.uiapi?.query?.Account?.edges?.map((e) => e.node) ?? []; // unwrap edges/node; read field values via .value
Typed call params (
query<GetAccountsQuery, GetAccountsQueryVariables>
), the
CacheControl
type, and
NodeOfConnection<T>
(extracts a node type from a Connection for clean typing) all live in references/sdk-api.md.
This changed (breaking — PR #502). The previous callable
sdk.graphql(...)
form and the previous package name are dead — the code above is the only correct form. If you encounter the old API in existing code (or a stale
dist/
artifact), don't copy it; convert it per Working on existing code.
sdk.graphql!
is WebApp-only.
The non-null assertion above is correct only if the bundle runs solely on WebApp. On other surfaces it can crash — decide before you write it. See Surfaces —
!
vs guard
below.

const sdk = await createDataSDK()
。随后
 sdk.graphql
是一个命名空间,而非函数:读取操作使用**
sdk.graphql!.query({...})
,写入操作使用
sdk.graphql!.mutate({...})
**。在WebApp环境下,所有
query()
调用默认都会被缓存
(缓存时长300秒)。HTTP 200状态码绝不代表操作成功——务必检查
result.errors
。在执行查询前,需验证所有实体和字段是否符合Schema要求:只要有一个未验证的字段,就会导致整个查询在运行时失败,而
schema.graphql
文件过大无法人工检查——必须通过工具查询确认。
typescript
import { createDataSDK, gql } from "@salesforce/platform-sdk"; // gql用于标记查询字符串,以便代码生成和eslint进行验证

const sdk = await createDataSDK();
const result = await sdk.graphql!.query({ query: GET_ACCOUNTS, variables });
if (result.errors?.length) throw new Error(result.errors.map((e) => e.message).join("; "));
const rows = result.data?.uiapi?.query?.Account?.edges?.map((e) => e.node) ?? []; // 解析edges/node;通过.value读取字段值
带类型的调用参数(
query<GetAccountsQuery, GetAccountsQueryVariables>
)、
CacheControl
类型,以及
NodeOfConnection<T>
(从Connection中提取节点类型以实现清晰的类型定义)均在references/sdk-api.md中有详细说明。
此处已变更(破坏性更新——PR #502)。之前的可调用形式
sdk.graphql(...)
和旧包名已废弃——上述代码是唯一正确的调用形式。若在现有代码(或过期的
dist/
产物)中遇到旧API,请勿复制使用;请按照现有代码迁移部分进行转换。
sdk.graphql!
仅适用于WebApp
。上述代码中的非空断言仅在bundle仅运行于WebApp环境时才是安全的;若在其他环境运行可能会崩溃——编写代码前需明确运行环境。详见下文**运行环境——
!
断言与防护检查
**。

Surfaces —
sdk.graphql!
vs guard

运行环境——
sdk.graphql!
断言与防护检查

createDataSDK()
runs on multiple surfaces, and
sdk.graphql
/
sdk.fetch
are genuinely optional
(typed
graphql?: …
). Whether you may assert them with
!
depends entirely on where the bundle runs — this is the one surface decision that turns into a runtime crash if you get it wrong, so make it explicitly before writing any
query
/
mutate
call:
Surface(s)
sdk.graphql
Write
WebApp onlyalways present
sdk.graphql!.query({...})
!
is safe; every shipped WebApp consumer uses it
Mosaic / OpenAI / MCPApps (or any bundle that might run off-WebApp)can be
undefined
guard first (
if (!sdk.graphql) return …
), then call
Rule of thumb: if you cannot prove the bundle is WebApp-only, guard. A bare
sdk.graphql!
that later ships to another surface throws
Cannot read properties of undefined
at runtime — TypeScript won't catch it because
!
silences exactly that check (same applies to
sdk.fetch!
). The portable guard snippet lives in references/sdk-api.md.

createDataSDK()
可在多种环境下运行,且**
sdk.graphql
/
sdk.fetch
是可选的**(类型定义为
graphql?: …
)。能否使用
!
进行非空断言完全取决于bundle的运行环境——这是唯一会导致运行时崩溃的环境决策,因此在编写任何
query
/
mutate
调用前,需明确确认运行环境:
运行环境
sdk.graphql
状态
代码写法
仅WebApp始终存在
sdk.graphql!.query({...})
——
!
是安全的;所有已发布的WebApp消费者均使用该写法
Mosaic / OpenAI / MCPApps(或任何可能在WebApp外运行的bundle)可能为
undefined
先做防护检查
if (!sdk.graphql) return …
),再调用方法
经验法则:若无法证明bundle仅运行于WebApp环境,必须添加防护检查。若直接使用
sdk.graphql!
,后续部署到其他环境时会抛出
Cannot read properties of undefined
运行时错误——TypeScript不会捕获该错误,因为
!
恰好屏蔽了该检查(
sdk.fetch!
同理)。可移植的防护代码片段请参考references/sdk-api.md

Step 0 — Route the task

步骤0——任务路由

The task is…Go to
Read recordsRead workflow below
Create / update / delete recordsWrite workflow below
Object/field metadata, picklist values, related-list metadata, aggregationsBeyond record CRUD below
Data is stale / "add a refresh button" / "cache it longer"Freshness & caching below
Something GraphQL can't express (Apex REST, file upload, Einstein)references/rest-and-integration.md
Migrating old
sdk.graphql?.(query, vars)
code
Working on existing code below
GraphQL covers far more than record reads and writes — prefer it for anything the
uiapi
namespace exposes
(see Beyond record CRUD). Reach for REST only when the data genuinely lives outside
uiapi
(Apex REST, file upload, Einstein) — see references/rest-and-integration.md.

任务类型跳转至
读取记录下文**读取流程**
创建/更新/删除记录下文**写入流程**
对象/字段元数据、选择列表值、关联列表元数据、聚合操作下文**记录CRUD之外的操作**
数据过期/“添加刷新按钮”/“延长缓存时长”下文**数据新鲜度与缓存**
GraphQL无法表达的操作(Apex REST、文件上传、Einstein)references/rest-and-integration.md
迁移旧版
sdk.graphql?.(query, vars)
代码
下文**现有代码迁移**
GraphQL的覆盖范围远不止记录的读写操作——对于**
uiapi
命名空间暴露的所有功能**,优先使用GraphQL(详见记录CRUD之外的操作)。仅当数据确实位于
uiapi
之外时(如Apex REST、文件上传、Einstein),才使用REST——详见references/rest-and-integration.md

Preconditions — verify before writing any query

前置条件——编写查询前需验证

<skill-dir>
below is wherever this skill is installed (the directory this
SKILL.md
loaded from). The schema-lookup script ships inside it. The script does not hunt for
schema.graphql
by walking up the tree — an ancestor schema can belong to a different org and would validate fields against the wrong one. Resolve the schema explicitly: run from the SFDX project root (where
schema.graphql
lives), or pass
--schema <path>
/ set
GRAPHQL_SCHEMA=<path>
. The script echoes the schema it resolved (
[graphql-search] using schema: …
on stderr) — glance at it to confirm you grounded against the right file.
#RequirementVerifyIf missing
1
@salesforce/platform-sdk
installed
package.json
in the UI bundle dir
Tell user to install it; cannot proceed
2A grounding tool resolvesPreferred:
npx graphiti sf-gql-discover '{"org":"<alias>","mode":"list_objects"}'
from the UI bundle dir returns objects. Fallback:
bash <skill-dir>/scripts/graphql-search.sh <Entity>
from the project root prints a lookup, not "schema.graphql not found"
No graphiti dep / org won't prime → use the script. Script can't find
schema.graphql
→ pass
--schema <path>
, or
npm run graphql:schema
from the UI bundle dir. (references/graphiti-cli.md covers CLI setup)
3Target objects/fields deployedThe object appears in
sf-gql-discover
(or
graphql-search.sh <Entity>
returns output)
Entity absent usually means it isn't deployed (or the cache/schema is stale). Refresh:
npx graphiti sf-gql-connect '{"org":"<alias>","forceRefresh":true}'
(CLI) or
npm run graphql:schema
(script). If still absent, deploy the metadata (the platform-metadata-deploy skill handles this) and assign the permission sets, then re-check
If preconditions aren't met you may still scaffold components, routes, and layout — but use empty arrays /
null
for data, mark query sites with
// TODO: add query after schema verification
, and add a plan item to return. Do not write GraphQL strings until the schema workflow is complete.

下文的
<skill-dir>
指本技能的安装目录(即本
SKILL.md
所在的目录)。Schema查询脚本随本技能一同发布。该脚本不会通过遍历目录树查找
schema.graphql
——上级目录的Schema可能属于其他组织,会导致字段验证错误。需显式指定Schema:从SFDX项目根目录(
schema.graphql
所在目录)运行脚本,或通过
--schema <path>
参数/设置
GRAPHQL_SCHEMA=<path>
环境变量指定。脚本会输出所使用的Schema路径(stderr中的
[graphql-search] using schema: …
)——请确认使用的是正确的Schema文件。
序号要求验证方式缺失时处理
1已安装
@salesforce/platform-sdk
检查UI bundle目录下的
package.json
告知用户安装该依赖;否则无法继续
2Schema查询工具可用首选方式:在UI bundle目录下运行
npx graphiti sf-gql-discover '{"org":"<alias>","mode":"list_objects"}'
,返回对象列表。回退方式:在项目根目录运行
bash <skill-dir>/scripts/graphql-search.sh <Entity>
,返回查询结果而非“schema.graphql not found”
无graphiti依赖/组织无法初始化 → 使用回退脚本。脚本无法找到
schema.graphql
→ 通过
--schema <path>
参数指定,或在UI bundle目录运行
npm run graphql:schema
。(references/graphiti-cli.md涵盖CLI配置说明)
3目标对象/字段已部署对象出现在
sf-gql-discover
的结果中(或
graphql-search.sh <Entity>
返回输出)
对象不存在通常意味着未部署(或缓存/Schema过期)。刷新缓存:运行
npx graphiti sf-gql-connect '{"org":"<alias>","forceRefresh":true}'
(CLI方式)或
npm run graphql:schema
(脚本方式)。若仍不存在,请部署元数据(platform-metadata-deploy技能负责此操作)并分配权限集,然后重新检查
若前置条件未满足,仍可搭建组件、路由和布局——但需使用空数组/
null
填充数据,在查询位置标记
// TODO: add query after schema verification
,并添加后续完成计划。请勿在Schema验证完成前编写GraphQL字符串。

Read workflow

读取流程

  1. Look up the schema first — never guess a name. Preferred (graphiti): when the exact API name is at all uncertain, list before you describe
    npx graphiti sf-gql-discover '{"org":"<alias>","mode":"list_objects","search":"<intent>"}'
    to find the real name, then
    npx graphiti sf-gql-discover '{"org":"<alias>","mode":"describe_object","object":"<Entity>"}'
    for exact field/type names, picklist values, filterable/sortable. An empty list or missing object is a fact about the org (wrong name or not deployed), not a tool failure — re-list or
    forceRefresh
    ; do not fall back to the script for this (see guardrail 2). Fallback is only for a CLI that genuinely can't run (no graphiti dep / org won't prime):
    bash <skill-dir>/scripts/graphql-search.sh <Entity>
    from the SFDX project root. (Full rules: references/graphql-hand-authoring.md.)
  2. Write the query. Preferred — compile it with graphiti:
    npx graphiti sf-gql-list '{"org":"<alias>","object":"<Entity>","fields":[…],"first":N}'
    returns a
    { query, variables, types, warnings }
    envelope with
    @optional
    ,
    value
    /
    displayValue
    ,
    edges/node
    , and
    first:
    /
    pageInfo
    already applied. Confirm
    warnings: []
    (a non-empty array means the object wasn't in the primed schema — the query is degraded; don't ship it), then paste the
    query
    verbatim into inline
    gql
    (simple) or an external
    .graphql
    file (one operation per file, imported with the bundler's
    ?raw
    suffix —
    import Q from "./q.graphql?raw"
    brings the file in as a plain string). Fallback — hand-author: apply
    @optional
    to every selectable FLS-gated field — scalar leaf fields (
    Name @optional { value }
    ) and parent/child relationships and the fields inside them — but NOT on
    Id
    , on connection plumbing (
    edges
    ,
    node
    , the connection field itself), or on
    pageInfo
    ; the graphiti output leaves those bare and is the canonical placement. Always set
    first:
    , include
    pageInfo
    if it may page. Either way, full mechanics and the primed-vs-degraded behavior: references/graphiti-cli.md.
  3. Generate types
    npm run graphql:codegen
    (from the UI bundle dir) →
    src/api/graphql-operations-types.ts
    .
  4. Call
    query()
    with the generated types:
    typescript
    import type { GetAccountsQuery, GetAccountsQueryVariables } from "../graphql-operations-types";
    
    const result = await sdk.graphql!.query<GetAccountsQuery, GetAccountsQueryVariables>({
      query: GET_ACCOUNTS,
      variables: { first: 20 },
      // cacheControl,            // optional — see Freshness & caching
    });
  5. Handle the result.
    result.data
    +
    result.errors
    are the initial snapshot;
    result.subscribe
    /
    result.refresh
    are the reactive handles. Always check
    errors
    before reading
    data
    :
    typescript
    if (result.errors?.length) throw new Error(result.errors.map((e) => e.message).join("; "));
    const rows = result.data?.uiapi?.query?.Account?.edges?.map((e) => e.node) ?? [];
Defend consuming code with
?.
/
??
(because
@optional
can omit fields). Error-handling stances (strict / tolerant / discriminated) and
NodeOfConnection
typing: references/sdk-api.md.

  1. 先查询Schema——绝不猜测名称首选方式(graphiti):若不确定API的确切名称,先列出对象再查询详情——运行
    npx graphiti sf-gql-discover '{"org":"<alias>","mode":"list_objects","search":"<intent>"}'
    查找真实名称,再运行
    npx graphiti sf-gql-discover '{"org":"<alias>","mode":"describe_object","object":"<Entity>"}'
    获取准确的字段/类型名称、选择列表值、可过滤/可排序属性。空列表或对象缺失是组织的实际情况(名称错误或未部署),并非工具故障——重新查询或
    forceRefresh
    请勿回退到脚本查询(见防护约束2)。回退方式仅适用于CLI确实无法运行的情况(无graphiti依赖/组织无法初始化):在SFDX项目根目录运行
    bash <skill-dir>/scripts/graphql-search.sh <Entity>
    。(完整规则:references/graphql-hand-authoring.md
  2. 编写查询语句首选方式——使用graphiti编译:运行
    npx graphiti sf-gql-list '{"org":"<alias>","object":"<Entity>","fields":[…],"first":N}'
    ,返回包含
    query
    variables
    types
    warnings
    的结果包,其中已自动应用
    @optional
    value
    /
    displayValue
    edges/node
    first:
    /
    pageInfo
    等约束。确认
    warnings: []
    (非空数组意味着对象不在初始化后的Schema中——查询语句存在缺陷;请勿发布),然后将
    query
    内容直接粘贴到内联
    gql
    中(简单场景)或外部
    .graphql
    文件中(每个文件一个操作,通过打包工具的
    ?raw
    后缀导入——
    import Q from "./q.graphql?raw"
    将文件作为纯字符串导入)。回退方式——手动编写:为每个受FLS(字段级安全)限制的可选字段添加
    @optional
    ——包括标量叶子字段(
    Name @optional { value }
    )、父/子关系及其内部字段——但请勿
    Id
    、连接结构(
    edges
    node
    、连接字段本身)或
    pageInfo
    添加;graphiti输出的内容已正确处理这些字段,是标准写法。始终设置
    first:
    ,若可能分页则需包含
    pageInfo
    。两种方式的完整机制及初始化/降级行为详见:references/graphiti-cli.md
  3. 生成类型定义——在UI bundle目录运行
    npm run graphql:codegen
    → 生成
    src/api/graphql-operations-types.ts
    文件。
  4. 调用
    query()
    并使用生成的类型
    typescript
    import type { GetAccountsQuery, GetAccountsQueryVariables } from "../graphql-operations-types";
    
    const result = await sdk.graphql!.query<GetAccountsQuery, GetAccountsQueryVariables>({
      query: GET_ACCOUNTS,
      variables: { first: 20 },
      // cacheControl,            // 可选参数——详见数据新鲜度与缓存
    });
  5. 处理查询结果
    result.data
    +
    result.errors
    是初始快照;
    result.subscribe
    /
    result.refresh
    是响应式处理句柄。读取
    data
    前务必检查
    errors
    typescript
    if (result.errors?.length) throw new Error(result.errors.map((e) => e.message).join("; "));
    const rows = result.data?.uiapi?.query?.Account?.edges?.map((e) => e.node) ?? [];
使用
?.
/
??
保护消费代码(因为
@optional
可能会省略字段)。错误处理策略(严格/容忍/区分)和
NodeOfConnection
类型定义详见:references/sdk-api.md

Write workflow

写入流程

1–3 as above (schema lookup → write the mutation → codegen). To compile the mutation with graphiti, use
sf-gql-create
/
sf-gql-update
/
sf-gql-delete
— they emit the
uiapi { <Object>Create(input: $input) { Record {…} } }
shape; the
types
field tells you the input shape. Details: references/graphiti-cli.md. 4. Call
mutate()
— note the option key is
mutation
, not
query
, and that mutations are never cached. The runtime
variables
shape differs per operation — values are raw (never
{value}
-wrapped; that wrapper is a read-shape thing and breaks writes) and nest under the entity key:
typescript
// create — input.<Entity> holds the new field values
variables: { input: { Account: { Name: "Acme", Industry: "Technology" } } }
// update — sibling Id alongside the entity key
variables: { input: { Id: "001…", Account: { Industry: "Finance" } } }
// delete — Id only, no entity key (generic RecordDeleteInput)
variables: { input: { Id: "001…" } }

const { data, errors } = await sdk.graphql!.mutate<CreateAccountMutation, CreateAccountMutationVariables>({
  mutation: CREATE_ACCOUNT,
  variables: { input: { Account: { Name: "Acme" } } },
});
if (errors?.length) throw new Error(errors.map((e) => e.message).join("; "));
This is the
variables
shape
the spine owns; the CLI
types
-field interpretation is in references/graphiti-cli.md and the GraphQL-document field constraints (
createable
/
updateable
,
ApiName
references,
@{alias}
chaining) in references/graphql-hand-authoring.md. 5. Re-freshen affected reads.
mutate()
has no
refresh
. To update a live list after a write, hold the
QueryResult
from your earlier
query()
call (e.g.
accountsResult
) and call
await accountsResult.refresh()
(forced re-fetch, pushes to subscribers) — note this is the read's handle, not anything
mutate()
returns. See Freshness & caching.
Mutation syntax is exacting: wrap under
uiapi(input: { allOrNone: ... })
, only
createable
/
updateable
fields, Create/Update output is always
Record
but Delete has no
Record
field — select
Id
only
. Full template + chaining + constraints: references/graphql-hand-authoring.md.

1–3步骤与读取流程相同(查询Schema → 编写突变语句 → 生成类型定义)。使用graphiti编译突变语句时,使用
sf-gql-create
/
sf-gql-update
/
sf-gql-delete
命令——它们会生成
uiapi { <Object>Create(input: $input) { Record {…} } }
格式的语句;
types
字段会告知输入参数的结构。详情:references/graphiti-cli.md。 4. 调用
mutate()
——注意参数键为**
mutation
,而非
query
,且突变操作
绝不会被缓存**。运行时
variables
的结构因操作而异——值为原始值(无需用
{value}
包装;该包装仅适用于读取操作,会导致写入失败),并嵌套在实体键下:
typescript
// 创建操作——input.<Entity>包含新字段值
variables: { input: { Account: { Name: "Acme", Industry: "Technology" } } }
// 更新操作——Id与实体键同级
variables: { input: { Id: "001…", Account: { Industry: "Finance" } } }
// 删除操作——仅需Id,无实体键(通用RecordDeleteInput)
variables: { input: { Id: "001…" } }

const { data, errors } = await sdk.graphql!.mutate<CreateAccountMutation, CreateAccountMutationVariables>({
  mutation: CREATE_ACCOUNT,
  variables: { input: { Account: { Name: "Acme" } } },
});
if (errors?.length) throw new Error(errors.map((e) => e.message).join("; "));
这是核心规范定义的**
variables
结构**;CLI的
types
字段解释详见references/graphiti-cli.md,GraphQL文档的字段约束(
createable
/
updateable
ApiName
引用、
@{alias}
链式调用)详见references/graphql-hand-authoring.md。 5. 刷新受影响的读取操作
mutate()
没有
refresh
方法。若要在写入后更新实时列表,请保留之前
query()
调用返回的
QueryResult
(如
accountsResult
),并调用
await accountsResult.refresh()
(强制重新获取数据,并推送给订阅者)——注意这是读取操作的句柄,而非
mutate()
返回的内容。详见**数据新鲜度与缓存**。
突变语法要求严格:需包裹在
uiapi(input: { allOrNone: ... })
中,仅使用
createable
/
updateable
字段;创建/更新操作的输出始终为
Record
,但删除操作没有
Record
字段——仅需选择
Id
。完整模板、链式调用及约束详见:references/graphql-hand-authoring.md

Beyond record CRUD

记录CRUD之外的操作

The
uiapi
namespace is not just record reads/writes. Before reaching for REST, check whether GraphQL already covers it — the same
sdk.graphql!.query()
call, different sub-selection. The top-level
uiapi
fields:
NeedUseReturns
Query records
uiapi { query { <Entity>(...) } }
records (the Read workflow)
Counts / sums / grouped rollups without pulling rows
uiapi { aggregate { <Entity>(groupBy: …) } }
aggregated buckets
Object/field metadata — labels, data types,
createable
/
updateable
, record types
uiapi { objectInfos(apiNames: […]) }
ObjectInfo[]
Picklist values (per record type)
uiapi { objectInfos(objectInfoInputs: […]) { fields … on PicklistField { … } } }
picklist values
Related-list metadata — display columns, ordering for a parent's related list
uiapi { relatedListByName(parentApiName, relatedListName) }
RelatedListInfo
Same rules as record reads: verify every type/field first,
@optional
where FLS applies, check
result.errors
. Aggregations can be compiled with
npx graphiti sf-gql-aggregate
(pass
groupBy
+
aggregations
); object metadata / picklists / related lists are hand-authored — templates: references/graphql-hand-authoring.md.
Two related capabilities (the current-user record and layout delivery) need confirmation against a current org schema before this skill documents a query shape — tracked as a follow-up, not yet covered here.

uiapi
命名空间不仅支持记录的读写操作。在使用REST之前,请先确认GraphQL是否已支持该功能——使用相同的
sdk.graphql!.query()
调用,仅需修改子选择字段。顶级
uiapi
字段包括:
需求使用方式返回结果
查询记录
uiapi { query { <Entity>(...) } }
记录(详见读取流程
无需获取记录即可统计/求和/分组汇总
uiapi { aggregate { <Entity>(groupBy: …) } }
聚合分组结果
对象/字段元数据——标签、数据类型、
createable
/
updateable
、记录类型
uiapi { objectInfos(apiNames: […]) }
ObjectInfo[]
选择列表值(按记录类型)
uiapi { objectInfos(objectInfoInputs: […]) { fields … on PicklistField { … } } }
选择列表值
关联列表元数据——父对象关联列表的显示列、排序方式
uiapi { relatedListByName(parentApiName, relatedListName) }
RelatedListInfo
规则与记录读取相同:先验证所有类型/字段,在受FLS限制的字段添加
@optional
,检查
result.errors
。聚合操作可通过
npx graphiti sf-gql-aggregate
编译(传入
groupBy
+
aggregations
);对象元数据/选择列表/关联列表需手动编写——模板详见:references/graphql-hand-authoring.md
两项相关功能(当前用户记录和布局交付)需先针对当前组织Schema确认后,本技能才会提供查询模板——目前已列为后续任务,暂未在此覆盖。

Freshness & caching

数据新鲜度与缓存

Caching is ON by default on WebApp. Every
sdk.graphql!.query()
is cached with a 300-second
max-age
TTL — no opt-in flag, no factory, no import subpath. Do not build your own cache (no React Query, SWR,
localStorage
, or hand-rolled Map). The cache is shared across SDK instances by
baseUrl
: the same query+variables from a different
createDataSDK()
targeting the same host is a cache hit. Only non-empty, error-free
data
is cached.
mutate()
is never cached.
There are two distinct freshness tools — keep them separate:
  1. Per-call
    cacheControl
    — a one-shot policy override on the query options bag (
    "no-cache"
    /
    "only-if-cached"
    /
    { type: "max-age", maxAge: <seconds> }
    ). The type and exact per-value behavior live in references/sdk-api.md. Take
    cacheControl
    as an optional param on the read function and expose each distinct policy as a thin named export in the same data-layer file — a "call site" is a named export, not a new React component. For
    getAccounts(first, after?, cacheControl?)
    :
    export const refreshAccounts = () => getAccounts(20, undefined, "no-cache")
    (and likewise
    offlineAccounts
    "only-if-cached"
    ,
    shortLivedAccounts
    { type: "max-age", maxAge: 10 }
    ). Keep the policy in the data layer.
  2. Reactive
    subscribe
    /
    refresh
    — a stateful handle on a live
    QueryResult
    :
    result.subscribe(cb)
    fires on every later snapshot,
    result.refresh()
    re-fetches bypassing the cache and pushes to subscribers. Shape in references/sdk-api.md; subscription lifecycle (always unsubscribe on teardown) in references/caching.md.
WantReach for
Freshness within ~5 min is finenothing (default cache)
This one read must bypass the cache (refresh button)
cacheControl: "no-cache"
Read only cached data, tolerate misses (offline-first)
cacheControl: "only-if-cached"
— a miss is expected, not an error: it surfaces a
DataNotFoundError
on
result.errors
(no network, no throw). Check
result.errors
, render empty state, do not throw and do not fall back to the network — that defeats offline-first.
Tighter/looser TTL for this query
cacheControl: { type: "max-age", maxAge: 60 }
(
maxAge
is in seconds)
Mounted component reflects updates over time
result.subscribe(cb)
Re-fetch now + notify all subscribers (e.g. after a mutation)
result.refresh()
cacheControl
is fire-and-forget at call time;
subscribe
/
refresh
is a live handle. Different mechanisms, different jobs — don't conflate "refresh" with "no-cache". Full behavior, the reactive-subscription lifecycle, and uncached-surface caveats: references/caching.md.

WebApp环境下默认启用缓存。所有
sdk.graphql!.query()
调用都会被缓存,默认TTL为300秒——无需启用标志、工厂函数或导入子路径。请勿自行构建缓存(如React Query、SWR、
localStorage
或手动实现的Map)。缓存
baseUrl
在SDK实例间共享
:不同
createDataSDK()
实例针对同一主机执行相同的查询+变量,会命中缓存。仅非空且无错误的
data
会被缓存。
mutate()
操作绝不会被缓存。
两种独立的数据新鲜度工具——请勿混淆:
  1. 单次调用的
    cacheControl
    ——查询选项中的一次性策略覆盖(
    "no-cache"
    /
    "only-if-cached"
    /
    { type: "max-age", maxAge: <seconds> }
    )。类型及各值的具体行为详见references/sdk-api.md。将
    cacheControl
    作为读取函数的可选参数,并在同一数据层文件中暴露不同策略的轻量命名导出——“调用点”是命名导出,而非新的React组件。例如
    getAccounts(first, after?, cacheControl?)
    export const refreshAccounts = () => getAccounts(20, undefined, "no-cache")
    (同理
    offlineAccounts
    "only-if-cached"
    shortLivedAccounts
    { type: "max-age", maxAge: 10 }
    )。将策略逻辑保留在数据层。
  2. 响应式
    subscribe
    /
    refresh
    ——实时
    QueryResult
    的状态句柄:
    result.subscribe(cb)
    会在每次快照更新时触发回调,
    result.refresh()
    会绕过缓存重新获取数据并推送给订阅者。结构详见references/sdk-api.md;订阅生命周期(组件销毁时务必取消订阅)详见references/caching.md
需求使用工具
5分钟内的数据新鲜度即可满足无需操作(默认缓存)
本次读取必须绕过缓存(刷新按钮)
cacheControl: "no-cache"
仅读取缓存数据,容忍缺失(离线优先)
cacheControl: "only-if-cached"
—— 缓存缺失是预期行为,而非错误:会在
result.errors
中返回
DataNotFoundError
(无网络时)。检查
result.errors
,渲染空状态,请勿抛出错误或回退到网络请求——否则违背离线优先原则。
为本次查询设置更短/更长的TTL
cacheControl: { type: "max-age", maxAge: 60 }
maxAge
单位为
已挂载组件随时间反映数据更新
result.subscribe(cb)
立即重新获取数据并通知所有订阅者(如写入操作后)
result.refresh()
cacheControl
是调用时的一次性设置;
subscribe
/
refresh
是实时句柄。两者机制不同,用途不同——请勿将“刷新”与“no-cache”混淆。完整行为、响应式订阅生命周期及非缓存环境注意事项详见:references/caching.md

Working on existing code (migration)

现有代码迁移

Only enter this path if the existing code actually uses the old API — i.e. it imports
@salesforce/sdk-data
or calls the callable
sdk.graphql(query, vars)
form. For any new read/write, ignore migration entirely and use the Read workflow / Write workflow — those already show the only correct API.
When you do have old code to convert, see references/migration.md for the before→after diff (imports, query/mutate calls, optional-chaining → non-null assertion, codegen type placement) and a checklist. The target API is exactly what the Read/Write workflows above prescribe — migrating is just swapping the old form for that.

仅当现有代码确实使用旧版API时,才需进入此流程——即代码导入
@salesforce/sdk-data
或调用可调用形式
sdk.graphql(query, vars)
。对于任何新的读写操作,请完全忽略迁移流程,直接使用读取流程/写入流程——这些流程已展示唯一正确的API
若需转换旧代码,请参考**references/migration.md**中的前后对比(导入语句、查询/突变调用、可选链→非空断言、代码生成类型位置)及检查清单。目标API与上述读写流程完全一致——迁移仅需将旧形式替换为新形式。

Platform guardrails — never regress these

平台防护约束——绝不能违反

These are Salesforce GraphQL platform behaviors, independent of the SDK. Violations cause silent runtime failures. (Details + templates: references/graphql-hand-authoring.md.)
  1. HTTP 200 ≠ success — always parse
    result.errors
    ; the Promise resolves even on failure.
  2. Schema is the only source of truth — verify, never invent. Verify every entity/field/type via graphiti
    sf-gql-discover
    (preferred) or
    bash <skill-dir>/scripts/graphql-search.sh <Entity>
    before use. Case-sensitive;
    __c
    /
    __e
    ;
    _Record
    entity suffix (v60+). When graphiti is primed, a "not found"/empty/
    Cannot query field
    answer (including from
    graphql-codegen
    /
    @graphql-eslint
    , even when the message points at
    schema.graphql
    ) is a fact about the org — wrong name or undeployed/inaccessible metadata, not a tool failure: fix the operation, or deploy the metadata (the platform-metadata-deploy skill)
    • assign perms + refresh (
      sf-gql-connect --forceRefresh
      /
      npm run graphql:schema
      ). Do not fall back to the script, hand-author around it, or guess a name — a guessed entity or field silently fails the whole query at runtime; if lookups aren't converging, ask the user rather than keep spiraling.
      schema.graphql
      and the codegen output (
      src/api/graphql-operations-types.ts
      ) are read-only generated mirrors — never open or edit them
      (honor any
      # DO NOT EDIT
      marker). Hand-adding a missing type satisfies codegen/lint but grants no org access; it just hides the failure until runtime. Fall back to the script only when the CLI can't run at all (no dep /
      SCHEMA_PRIME_FAILED
      ).
  3. @optional
    on every FLS-gated field at each nesting level
    — scalar leaf fields plus each parent/child relationship and the fields inside it (FLS fails the whole query otherwise, v65+). Do NOT decorate
    Id
    , the connection plumbing (
    edges
    ,
    node
    , the connection field), or
    pageInfo
    — those are not FLS-gated and the graphiti output leaves them bare. Consume with
    ?.
    /
    ??
    . Placement rules: references/graphql-hand-authoring.md.
  4. Mutations wrap under
    uiapi(input: { allOrNone: ... })
    ; set
    allOrNone
    explicitly; output excludes child/navigated-reference fields; the output field is literally named
    Record
    (unrelated to the
    _Record
    entity suffix in rule 2) — Delete →
    Id
    only. GA v66+.
  5. Explicit pagination — always set
    first:
    , because the server silently caps at 10 and you'll drop rows with no error; forward-only (
    first
    /
    after
    , no
    last
    /
    before
    );
    upperBound
    (v59+) raises the per-request ceiling for large sets (when set,
    first
    must be 200–2000).
  6. SOQL governor limits apply
    uiapi
    queries compile to SOQL, so the same governor limits are inherited: ≤10 subqueries, ≤5 child→parent levels, ≤1 parent→child level, ≤2,000 records/subquery. Split into multiple requests if you'd exceed them.
  7. Field value wrappers — read the raw value via
    .value
    ;
    displayValue
    is the server-formatted string for UI. When a field is both shown and operated on (currency, dates, picklists), select both
    value
    and
    displayValue
    so you don't reformat on the client. Display-only fields can take just
    displayValue
    .
  8. Compound fields — filter/order on constituents (
    BillingCity
    ), not the wrapper (
    BillingAddress
    ).
  9. Supported APIs only — GraphQL (
    uiapi
    ), UI API REST, Apex REST, Connect REST, Einstein LLM via
    sdk.fetch
    . NOT: Enterprise SOQL
    /query
    , Aura-enabled Apex, Chatter (use
    uiapi.currentUser
    ). See references/rest-and-integration.md.
One SDK convention lives in the workflows, not this list (it's not a platform behavior): always run
npm run graphql:codegen
and use the generated types after writing an operation (Read workflow step 3). Also in the Pre-flight checklist.
graphiti applies most of these for you. When you compile a query with
sf-gql-*
against an object that's in the primed schema, rules 3 (
@optional
), 4 (mutation
Record
output envelope and entity-keyed input — not
allOrNone
, which you still add yourself), 5 (
first:
/
pageInfo
), and 7 (
value
/
displayValue
wrappers) come out already satisfied — which is exactly why you paste the
query
verbatim
rather than re-deriving it. Rules 1 (check
result.errors
), 6 (governor limits), 8 (compound fields), and 9 (supported APIs) are still on you. And the automation only fires when the object is primed: a non-empty
warnings
array means it isn't, and the emitted query is degraded (bare fields, no guardrails) — see references/graphiti-cli.md.

这些是Salesforce GraphQL平台的固有行为,与SDK无关。违反约束会导致静默的运行时失败。(详情及模板:references/graphql-hand-authoring.md
  1. HTTP 200 ≠ 操作成功——务必解析
    result.errors
    ;即使操作失败,Promise也会resolve。
  2. Schema是唯一的事实来源——必须验证,绝不臆造。使用graphiti
    sf-gql-discover
    (首选)或
    bash <skill-dir>/scripts/graphql-search.sh <Entity>
    验证所有实体/字段/类型后再使用。注意大小写敏感;
    __c
    /
    __e
    后缀;
    _Record
    实体后缀(v60+)。当graphiti已初始化时,“未找到”/空结果/
    Cannot query field
    错误(包括来自
    graphql-codegen
    /
    @graphql-eslint
    的错误,即使错误信息指向
    schema.graphql
    )是组织的实际情况——名称错误或未部署/不可访问的元数据,而非工具故障:修正操作,或部署元数据(platform-metadata-deploy技能负责此操作)+分配权限+刷新缓存(
    sf-gql-connect --forceRefresh
    /
    npm run graphql:schema
    )。请勿回退到脚本、手动绕过或臆造名称——臆造的实体或字段会导致整个查询在运行时静默失败;若查询工具无法得到一致结果,请询问用户而非继续尝试
    schema.graphql
    和代码生成输出(
    src/api/graphql-operations-types.ts
    )是只读的生成镜像——请勿打开或编辑
    (遵守任何
    # DO NOT EDIT
    标记)。手动添加缺失的类型可满足代码生成/ lint要求,但无法获得组织的访问权限;只会隐藏错误直到运行时。仅当CLI完全无法运行时(无依赖/
    SCHEMA_PRIME_FAILED
    ),才回退到脚本。
  3. 每个嵌套层级中受FLS限制的字段都需添加
    @optional
    ——包括标量叶子字段、每个父/子关系及其内部字段(否则FLS验证会导致整个查询失败,v65+)。请勿
    Id
    、连接结构(
    edges
    node
    、连接字段)或
    pageInfo
    添加——这些不受FLS限制,graphiti输出的内容已正确处理。使用
    ?.
    /
    ??
    消费数据。放置规则详见:references/graphql-hand-authoring.md
  4. 突变操作需包裹在
    uiapi(input: { allOrNone: ... })
    中;显式设置
    allOrNone
    ;输出不包含子/导航引用字段;输出字段名称为
    Record
    (与规则2中的
    _Record
    实体后缀无关)——删除操作仅需选择
    Id
    。GA版本v66+。
  5. 显式分页——始终设置
    first:
    ,因为服务器会静默限制为10条记录,且不会报错;仅支持向前分页(
    first
    /
    after
    ,不支持
    last
    /
    before
    );
    upperBound
    (v59+)可提高单次请求的记录上限(设置后,
    first
    必须为200–2000)。
  6. SOQL governor限制适用——
    uiapi
    查询会编译为SOQL,因此继承相同的governor限制:子查询≤10个,子→父层级≤5个,父→子层级≤1个,每个子查询的记录数≤2000。若超出限制,请拆分为多个请求。
  7. 字段值包装——通过
    .value
    读取原始值;
    displayValue
    是服务器格式化的UI显示字符串。当字段既要显示又要操作时(如货币、日期、选择列表),需同时选择**
    value
    displayValue
    **,避免在客户端重新格式化。仅用于显示的字段可仅选择
    displayValue
  8. 复合字段——基于组成字段过滤/排序(如
    BillingCity
    ),而非包装字段(如
    BillingAddress
    )。
  9. 仅使用支持的API——包括GraphQL(
    uiapi
    )、UI API REST、Apex REST、Connect REST、通过
    sdk.fetch
    调用的Einstein LLM。禁止使用:Enterprise SOQL
    /query
    、支持Aura的Apex、Chatter(使用
    uiapi.currentUser
    )。详见references/rest-and-integration.md
有一个SDK约定已包含在流程中,未列入此列表(不属于平台行为):编写操作后,务必运行
npm run graphql:codegen
并使用生成的类型(读取流程步骤3)。同样包含在预检查清单中。
graphiti会自动应用大部分约束。当针对已初始化Schema中的对象使用
sf-gql-*
编译查询时,规则3(
@optional
)、4(突变
Record
输出包和实体键输入——不包括
allOrNone
,仍需手动设置
)、5(
first:
/
pageInfo
)、7(
value
/
displayValue
包装)会自动满足——这正是为什么要直接粘贴
query
内容
而非重新推导。规则1(检查
result.errors
)、6(governor限制)、8(复合字段)、9(支持的API)仍需自行遵守。仅当对象已初始化时,自动化才会生效:非空
warnings
数组意味着对象未初始化,生成的查询存在缺陷(无防护约束的裸字段)——详见references/graphiti-cli.md

Commands & layout

命令与目录结构

text
<skill-dir>/                            ← wherever this skill is installed
└── scripts/graphql-search.sh           ← schema lookup (ships with the skill)

<project-root>/                         ← SFDX project root; run the script from here
├── schema.graphql                      ← generated mirror; grep target (never open or edit; script reads ./schema.graphql)
└── force-app/main/default/uiBundles/<app>/   ← UI bundle dir
    ├── package.json                    ← npm scripts
    └── src/api/                        ← queries, generated types, SDK calls
CommandRun fromPurpose
npx graphiti sf-gql-discover '{…}'
UI bundle dirDiscover objects/fields against the live org (preferred grounding)
npx graphiti sf-gql-<list|detail|aggregate|create|update|delete|raw> '{…}'
UI bundle dirCompile a guardrail-applied query/mutation (references/graphiti-cli.md)
npx graphiti sf-gql-connect '{"org":"<alias>","forceRefresh":true}'
UI bundle dirRefresh graphiti's schema cache after a deploy
bash <skill-dir>/scripts/graphql-search.sh <Entity>
project root (or pass
--schema <path>
; no tree walk-up)
Schema lookup fallback (grep over local
schema.graphql
)
npm run graphql:schema
UI bundle dirFetch/refresh
schema.graphql
(for the fallback script)
npm run graphql:codegen
UI bundle dirGenerate operation types
npx eslint <file>
UI bundle dirLint (catches
gql
schema violations)
text
<skill-dir>/                            ← 本技能的安装目录
└── scripts/graphql-search.sh           ← Schema查询脚本(随技能发布)

<project-root>/                         ← SFDX项目根目录;在此运行脚本
├── schema.graphql                      ← 生成的镜像文件;查询目标(请勿打开或编辑;脚本读取./schema.graphql)
└── force-app/main/default/uiBundles/<app>/   ← UI bundle目录
    ├── package.json                    ← npm脚本
    └── src/api/                        ← 查询语句、生成的类型、SDK调用代码
命令运行目录用途
npx graphiti sf-gql-discover '{…}'
UI bundle目录针对在线组织发现对象/字段(首选查询方式)
npx graphiti sf-gql-<list|detail|aggregate|create|update|delete|raw> '{…}'
UI bundle目录编译带有防护约束的查询/突变语句(references/graphiti-cli.md
npx graphiti sf-gql-connect '{"org":"<alias>","forceRefresh":true}'
UI bundle目录部署后刷新graphiti的Schema缓存
bash <skill-dir>/scripts/graphql-search.sh <Entity>
项目根目录(或通过
--schema <path>
指定;不支持目录树遍历)
Schema查询回退方式(在本地
schema.graphql
中搜索)
npm run graphql:schema
UI bundle目录获取/刷新
schema.graphql
(用于回退脚本)
npm run graphql:codegen
UI bundle目录生成操作类型定义
npx eslint <file>
UI bundle目录代码检查(捕获
gql
的Schema违规)

Pre-flight checklist

预检查清单

  • Surface decided:
    sdk.graphql!
    only if WebApp-only; otherwise guard with
    if (!sdk.graphql) …
    (Surfaces)
  • Every field/entity verified —
    sf-gql-discover
    (preferred) or
    graphql-search.sh
    (fallback, against the right schema)
  • If compiled with graphiti:
    warnings: []
    confirmed (non-empty = degraded query, don't ship);
    query
    pasted verbatim
  • @optional
    on FLS-gated fields + relationships (NOT
    Id
    /
    edges
    /
    node
    /
    pageInfo
    );
    ?.
    /
    ??
    in consuming code
  • result.errors
    checked before reading
    result.data
  • Caching considered: default 300s OK, or
    cacheControl
    /
    refresh
    chosen deliberately
  • npm run graphql:codegen
    run; generated types used;
    npx eslint
    passes
  • 已确定运行环境:仅当仅运行于WebApp时使用
    sdk.graphql!
    ;否则添加
    if (!sdk.graphql) …
    防护检查(运行环境
  • 所有字段/实体已验证——使用
    sf-gql-discover
    (首选)或
    graphql-search.sh
    (回退方式,针对正确的Schema)
  • 若使用graphiti编译:已确认
    warnings: []
    (非空=查询存在缺陷,请勿发布);已直接粘贴
    query
    内容
  • 受FLS限制的字段及关系已添加
    @optional
    Id
    /
    edges
    /
    node
    /
    pageInfo
    除外);消费代码中使用
    ?.
    /
    ??
  • 读取
    result.data
    前已检查
    result.errors
  • 已考虑缓存策略:默认300秒缓存满足需求,或已明确选择
    cacheControl
    /
    refresh
  • 已运行
    npm run graphql:codegen
    ;已使用生成的类型;
    npx eslint
    检查通过