experience-ui-bundle-salesforce-data-access
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSalesforce Data Access (UI bundles)
Salesforce数据访问(UI bundles)
All Salesforce data access in a UI bundle goes through the
data SDK. The SDK handles auth, CSRF, and base-URL resolution, and — on the WebApp
surface — caches every GraphQL query by default.
@salesforce/platform-sdkThis file is the workflow + guardrail spine. Depth lives in linked docs:
- references/graphiti-cli.md — the CLI (
graphiticommands) 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.sf-gql-* - references/sdk-api.md — the new call API: /
query,mutate, typing, error-handling stances.QueryResult - references/caching.md — on-by-default cache + the two refresh
modes (/
result.refreshvs per-callsubscribe).cacheControl - references/graphql-hand-authoring.md — schema lookup, read /
mutation templates, every platform guardrail (, pagination, limits, semi-join, wrappers, error table…).
@optional - references/rest-and-integration.md — , the supported-API allowlist, and the reactive/lifecycle integration patterns.
sdk.fetch - references/migration.md — old callable code → new namespace. The only place the dead API appears as usable code.
@salesforce/sdk-data
UI bundle中所有的Salesforce数据访问都需通过****数据SDK实现。该SDK负责处理认证、CSRF防护和基础URL解析,并且在WebApp环境下,默认会缓存所有GraphQL查询结果。
@salesforce/platform-sdk本文件是工作流规范与防护约束的核心,详细内容请参考关联文档:
- references/graphiti-cli.md —— CLI(
graphiti命令),可将小型JSON规范编译为符合Schema要求、带有防护约束的查询语句、变量及类型定义。这是下文步骤中编写GraphQL的首选方式;若无法使用,则可回退到schema-grep脚本。sf-gql-* - references/sdk-api.md —— 新的调用API:/
query、mutate、类型定义、错误处理策略。QueryResult - references/caching.md —— 默认启用的缓存机制,以及两种刷新模式(/
result.refreshvs 单次调用的subscribe)。cacheControl - references/graphql-hand-authoring.md —— Schema查询、读取/突变模板、所有平台防护约束(、分页、限制、半连接、包装器、错误表等)。
@optional - references/rest-and-integration.md —— 、支持的API白名单,以及响应式/生命周期集成模式。
sdk.fetch - references/migration.md —— 旧版可调用代码向新命名空间的迁移指南。这是唯一提及已废弃API可用代码的文档。
@salesforce/sdk-data
The one-paragraph mental model
核心认知模型
const sdk = await createDataSDK()sdk.graphqlsdk.graphql!.query({...})sdk.graphql!.mutate({...})query()result.errorsschema.graphqltypescript
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 .valueTyped call params (), the
type, and (extracts a node type from a Connection for clean typing) all
live in references/sdk-api.md.
query<GetAccountsQuery, GetAccountsQueryVariables>CacheControlNodeOfConnection<T>This changed (breaking — PR #502). The previous callableform 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 stalesdk.graphql(...)artifact), don't copy it; convert it per Working on existing code.dist/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 —sdk.graphql!vs guard below.!
const sdk = await createDataSDK() sdk.graphqlsdk.graphql!.query({...})sdk.graphql!.mutate({...})query()result.errorsschema.graphqltypescript
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读取字段值带类型的调用参数()、类型,以及(从Connection中提取节点类型以实现清晰的类型定义)均在references/sdk-api.md中有详细说明。
query<GetAccountsQuery, GetAccountsQueryVariables>CacheControlNodeOfConnection<T>此处已变更(破坏性更新——PR #502)。之前的可调用形式和旧包名已废弃——上述代码是唯一正确的调用形式。若在现有代码(或过期的sdk.graphql(...)产物)中遇到旧API,请勿复制使用;请按照现有代码迁移部分进行转换。dist/仅适用于WebApp。上述代码中的非空断言仅在bundle仅运行于WebApp环境时才是安全的;若在其他环境运行可能会崩溃——编写代码前需明确运行环境。详见下文**运行环境——sdk.graphql!断言与防护检查**。!
Surfaces — sdk.graphql!
vs guard
sdk.graphql!运行环境——sdk.graphql!
断言与防护检查
sdk.graphql!createDataSDK()sdk.graphqlsdk.fetchgraphql?: …!querymutate| Surface(s) | | Write |
|---|---|---|
| WebApp only | always present | |
| Mosaic / OpenAI / MCPApps (or any bundle that might run off-WebApp) | can be | guard first ( |
Rule of thumb: if you cannot prove the bundle is WebApp-only, guard. A bare
that later ships to another surface throws at runtime —
TypeScript won't catch it because silences exactly that check (same applies to ).
The portable guard snippet lives in references/sdk-api.md.
sdk.graphql!Cannot read properties of undefined!sdk.fetch!createDataSDK()sdk.graphqlsdk.fetchgraphql?: …!querymutate| 运行环境 | | 代码写法 |
|---|---|---|
| 仅WebApp | 始终存在 | |
| Mosaic / OpenAI / MCPApps(或任何可能在WebApp外运行的bundle) | 可能为 | 先做防护检查( |
经验法则:若无法证明bundle仅运行于WebApp环境,必须添加防护检查。若直接使用,后续部署到其他环境时会抛出运行时错误——TypeScript不会捕获该错误,因为恰好屏蔽了该检查(同理)。可移植的防护代码片段请参考references/sdk-api.md。
sdk.graphql!Cannot read properties of undefined!sdk.fetch!Step 0 — Route the task
步骤0——任务路由
| The task is… | Go to |
|---|---|
| Read records | Read workflow below |
| Create / update / delete records | Write workflow below |
| Object/field metadata, picklist values, related-list metadata, aggregations | Beyond 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 | Working on existing code below |
GraphQL covers far more than record reads and writes — prefer it for anything the
namespace exposes (see Beyond record CRUD). Reach for REST only when
the data genuinely lives outside (Apex REST, file upload, Einstein) — see
references/rest-and-integration.md.
uiapiuiapi| 任务类型 | 跳转至 |
|---|---|
| 读取记录 | 下文**读取流程** |
| 创建/更新/删除记录 | 下文**写入流程** |
| 对象/字段元数据、选择列表值、关联列表元数据、聚合操作 | 下文**记录CRUD之外的操作** |
| 数据过期/“添加刷新按钮”/“延长缓存时长” | 下文**数据新鲜度与缓存** |
| GraphQL无法表达的操作(Apex REST、文件上传、Einstein) | references/rest-and-integration.md |
迁移旧版 | 下文**现有代码迁移** |
GraphQL的覆盖范围远不止记录的读写操作——对于**命名空间暴露的所有功能**,优先使用GraphQL(详见记录CRUD之外的操作)。仅当数据确实位于之外时(如Apex REST、文件上传、Einstein),才使用REST——详见references/rest-and-integration.md。
uiapiuiapiPreconditions — verify before writing any query
前置条件——编写查询前需验证
<skill-dir>SKILL.mdschema.graphqlschema.graphql--schema <path>GRAPHQL_SCHEMA=<path>[graphql-search] using schema: …| # | Requirement | Verify | If missing |
|---|---|---|---|
| 1 | | | Tell user to install it; cannot proceed |
| 2 | A grounding tool resolves | Preferred: | No graphiti dep / org won't prime → use the script. Script can't find |
| 3 | Target objects/fields deployed | The object appears in | Entity absent usually means it isn't deployed (or the cache/schema is stale). Refresh: |
If preconditions aren't met you may still scaffold components, routes, and layout — but
use empty arrays / for data, mark query sites with
, and add a plan item to return. Do not
write GraphQL strings until the schema workflow is complete.
null// TODO: add query after schema verification下文的指本技能的安装目录(即本所在的目录)。Schema查询脚本随本技能一同发布。该脚本不会通过遍历目录树查找——上级目录的Schema可能属于其他组织,会导致字段验证错误。需显式指定Schema:从SFDX项目根目录(所在目录)运行脚本,或通过参数/设置环境变量指定。脚本会输出所使用的Schema路径(stderr中的)——请确认使用的是正确的Schema文件。
<skill-dir>SKILL.mdschema.graphqlschema.graphql--schema <path>GRAPHQL_SCHEMA=<path>[graphql-search] using schema: …| 序号 | 要求 | 验证方式 | 缺失时处理 |
|---|---|---|---|
| 1 | 已安装 | 检查UI bundle目录下的 | 告知用户安装该依赖;否则无法继续 |
| 2 | Schema查询工具可用 | 首选方式:在UI bundle目录下运行 | 无graphiti依赖/组织无法初始化 → 使用回退脚本。脚本无法找到 |
| 3 | 目标对象/字段已部署 | 对象出现在 | 对象不存在通常意味着未部署(或缓存/Schema过期)。刷新缓存:运行 |
若前置条件未满足,仍可搭建组件、路由和布局——但需使用空数组/填充数据,在查询位置标记,并添加后续完成计划。请勿在Schema验证完成前编写GraphQL字符串。
null// TODO: add query after schema verificationRead workflow
读取流程
-
Look up the schema first — never guess a name. Preferred (graphiti): when the exact API name is at all uncertain, list before you describe —to find the real name, then
npx graphiti sf-gql-discover '{"org":"<alias>","mode":"list_objects","search":"<intent>"}'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 ornpx graphiti sf-gql-discover '{"org":"<alias>","mode":"describe_object","object":"<Entity>"}'; 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):forceRefreshfrom the SFDX project root. (Full rules: references/graphql-hand-authoring.md.)bash <skill-dir>/scripts/graphql-search.sh <Entity> -
Write the query. Preferred — compile it with graphiti:returns a
npx graphiti sf-gql-list '{"org":"<alias>","object":"<Entity>","fields":[…],"first":N}'envelope with{ query, variables, types, warnings },@optional/value,displayValue, andedges/node/first:already applied. ConfirmpageInfo(a non-empty array means the object wasn't in the primed schema — the query is degraded; don't ship it), then paste thewarnings: []verbatim into inlinequery(simple) or an externalgqlfile (one operation per file, imported with the bundler's.graphqlsuffix —?rawbrings the file in as a plain string). Fallback — hand-author: applyimport Q from "./q.graphql?raw"to every selectable FLS-gated field — scalar leaf fields (@optional) and parent/child relationships and the fields inside them — but NOT onName @optional { value }, on connection plumbing (Id,edges, the connection field itself), or onnode; the graphiti output leaves those bare and is the canonical placement. Always setpageInfo, includefirst:if it may page. Either way, full mechanics and the primed-vs-degraded behavior: references/graphiti-cli.md.pageInfo -
Generate types —(from the UI bundle dir) →
npm run graphql:codegen.src/api/graphql-operations-types.ts -
Callwith the generated types:
query()typescriptimport 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 }); -
Handle the result.+
result.dataare the initial snapshot;result.errors/result.subscribeare the reactive handles. Always checkresult.refreshbefore readingerrors:datatypescriptif (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 can omit fields). Error-handling
stances (strict / tolerant / discriminated) and typing: references/sdk-api.md.
?.??@optionalNodeOfConnection-
先查询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>"}';请勿回退到脚本查询(见防护约束2)。回退方式仅适用于CLI确实无法运行的情况(无graphiti依赖/组织无法初始化):在SFDX项目根目录运行forceRefresh。(完整规则:references/graphql-hand-authoring.md)bash <skill-dir>/scripts/graphql-search.sh <Entity> -
编写查询语句。首选方式——使用graphiti编译:运行,返回包含
npx graphiti sf-gql-list '{"org":"<alias>","object":"<Entity>","fields":[…],"first":N}'、query、variables、types的结果包,其中已自动应用warnings、@optional/value、displayValue、edges/node/first:等约束。确认pageInfo(非空数组意味着对象不在初始化后的Schema中——查询语句存在缺陷;请勿发布),然后将warnings: []内容直接粘贴到内联query中(简单场景)或外部gql文件中(每个文件一个操作,通过打包工具的.graphql后缀导入——?raw将文件作为纯字符串导入)。回退方式——手动编写:为每个受FLS(字段级安全)限制的可选字段添加import Q from "./q.graphql?raw"——包括标量叶子字段(@optional)、父/子关系及其内部字段——但请勿为Name @optional { value }、连接结构(Id、edges、连接字段本身)或node添加;graphiti输出的内容已正确处理这些字段,是标准写法。始终设置pageInfo,若可能分页则需包含first:。两种方式的完整机制及初始化/降级行为详见:references/graphiti-cli.md。pageInfo -
生成类型定义——在UI bundle目录运行→ 生成
npm run graphql:codegen文件。src/api/graphql-operations-types.ts -
调用并使用生成的类型:
query()typescriptimport type { GetAccountsQuery, GetAccountsQueryVariables } from "../graphql-operations-types"; const result = await sdk.graphql!.query<GetAccountsQuery, GetAccountsQueryVariables>({ query: GET_ACCOUNTS, variables: { first: 20 }, // cacheControl, // 可选参数——详见数据新鲜度与缓存 }); -
处理查询结果。+
result.data是初始快照;result.errors/result.subscribe是响应式处理句柄。读取result.refresh前务必检查data:errorstypescriptif (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) ?? [];
使用/保护消费代码(因为可能会省略字段)。错误处理策略(严格/容忍/区分)和类型定义详见:references/sdk-api.md。
?.??@optionalNodeOfConnectionWrite workflow
写入流程
1–3 as above (schema lookup → write the mutation → codegen). To compile the mutation with
graphiti, use / / — they emit the
shape; the field tells you
the input shape. Details: references/graphiti-cli.md.
4. Call — note the option key is , not , and that
mutations are never cached. The runtime shape differs per operation —
values are raw (never -wrapped; that wrapper is a read-shape thing and breaks
writes) and nest under the entity key:
sf-gql-createsf-gql-updatesf-gql-deleteuiapi { <Object>Create(input: $input) { Record {…} } }typesmutate()mutationqueryvariables{value}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 shape the spine owns; the CLI -field interpretation is in
references/graphiti-cli.md and the GraphQL-document field constraints
(/, references, chaining) in
references/graphql-hand-authoring.md.
5. Re-freshen affected reads. has no . To update a live list
after a write, hold the from your earlier call (e.g.
) and call (forced re-fetch, pushes
to subscribers) — note this is the read's handle, not anything returns. See
Freshness & caching.
variablestypescreateableupdateableApiName@{alias}mutate()refreshQueryResultquery()accountsResultawait accountsResult.refresh()mutate()Mutation syntax is exacting: wrap under , only
/ fields, Create/Update output is always but Delete has no
field — select only. Full template + chaining + constraints:
references/graphql-hand-authoring.md.
uiapi(input: { allOrNone: ... })createableupdateableRecordRecordId1–3步骤与读取流程相同(查询Schema → 编写突变语句 → 生成类型定义)。使用graphiti编译突变语句时,使用 / / 命令——它们会生成格式的语句;字段会告知输入参数的结构。详情:references/graphiti-cli.md。
4. 调用——注意参数键为**,而非,且突变操作绝不会被缓存**。运行时的结构因操作而异——值为原始值(无需用包装;该包装仅适用于读取操作,会导致写入失败),并嵌套在实体键下:
sf-gql-createsf-gql-updatesf-gql-deleteuiapi { <Object>Create(input: $input) { Record {…} } }typesmutate()mutationqueryvariables{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("; "));这是核心规范定义的**结构**;CLI的字段解释详见references/graphiti-cli.md,GraphQL文档的字段约束(/、引用、链式调用)详见references/graphql-hand-authoring.md。
5. 刷新受影响的读取操作。没有方法。若要在写入后更新实时列表,请保留之前调用返回的(如),并调用(强制重新获取数据,并推送给订阅者)——注意这是读取操作的句柄,而非返回的内容。详见**数据新鲜度与缓存**。
variablestypescreateableupdateableApiName@{alias}mutate()refreshquery()QueryResultaccountsResultawait accountsResult.refresh()mutate()突变语法要求严格:需包裹在中,仅使用/字段;创建/更新操作的输出始终为,但删除操作没有字段——仅需选择。完整模板、链式调用及约束详见:references/graphql-hand-authoring.md。
uiapi(input: { allOrNone: ... })createableupdateableRecordRecordIdBeyond record CRUD
记录CRUD之外的操作
The namespace is not just record reads/writes. Before reaching for REST, check
whether GraphQL already covers it — the same call, different
sub-selection. The top-level fields:
uiapisdk.graphql!.query()uiapi| Need | Use | Returns |
|---|---|---|
| Query records | | records (the Read workflow) |
| Counts / sums / grouped rollups without pulling rows | | aggregated buckets |
Object/field metadata — labels, data types, | | |
| Picklist values (per record type) | | picklist values |
| Related-list metadata — display columns, ordering for a parent's related list | | |
Same rules as record reads: verify every type/field first, where FLS applies, check
. Aggregations can be compiled with (pass
+ ); object metadata / picklists / related lists are hand-authored —
templates: references/graphql-hand-authoring.md.
@optionalresult.errorsnpx graphiti sf-gql-aggregategroupByaggregationsTwo 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.
uiapisdk.graphql!.query()uiapi| 需求 | 使用方式 | 返回结果 |
|---|---|---|
| 查询记录 | | 记录(详见读取流程) |
| 无需获取记录即可统计/求和/分组汇总 | | 聚合分组结果 |
对象/字段元数据——标签、数据类型、 | | |
| 选择列表值(按记录类型) | | 选择列表值 |
| 关联列表元数据——父对象关联列表的显示列、排序方式 | | |
规则与记录读取相同:先验证所有类型/字段,在受FLS限制的字段添加,检查。聚合操作可通过编译(传入 + );对象元数据/选择列表/关联列表需手动编写——模板详见:references/graphql-hand-authoring.md。
@optionalresult.errorsnpx graphiti sf-gql-aggregategroupByaggregations两项相关功能(当前用户记录和布局交付)需先针对当前组织Schema确认后,本技能才会提供查询模板——目前已列为后续任务,暂未在此覆盖。
Freshness & caching
数据新鲜度与缓存
Caching is ON by default on WebApp. Every is cached with a
300-second TTL — no opt-in flag, no factory, no import subpath. Do not
build your own cache (no React Query, SWR, , or hand-rolled Map). The
cache is shared across SDK instances by : the same query+variables from a
different targeting the same host is a cache hit. Only non-empty,
error-free is cached. is never cached.
sdk.graphql!.query()max-agelocalStoragebaseUrlcreateDataSDK()datamutate()There are two distinct freshness tools — keep them separate:
- Per-call — a one-shot policy override on the query options bag (
cacheControl/"no-cache"/"only-if-cached"). The type and exact per-value behavior live in references/sdk-api.md. Take{ type: "max-age", maxAge: <seconds> }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. ForcacheControl:getAccounts(first, after?, cacheControl?)(and likewiseexport const refreshAccounts = () => getAccounts(20, undefined, "no-cache")→offlineAccounts,"only-if-cached"→shortLivedAccounts). Keep the policy in the data layer.{ type: "max-age", maxAge: 10 } - Reactive /
subscribe— a stateful handle on a liverefresh:QueryResultfires on every later snapshot,result.subscribe(cb)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.result.refresh()
| Want | Reach for |
|---|---|
| Freshness within ~5 min is fine | nothing (default cache) |
| This one read must bypass the cache (refresh button) | |
| Read only cached data, tolerate misses (offline-first) | |
| Tighter/looser TTL for this query | |
| Mounted component reflects updates over time | |
| Re-fetch now + notify all subscribers (e.g. after a mutation) | |
cacheControlsubscriberefreshWebApp环境下默认启用缓存。所有调用都会被缓存,默认TTL为300秒——无需启用标志、工厂函数或导入子路径。请勿自行构建缓存(如React Query、SWR、或手动实现的Map)。缓存按在SDK实例间共享:不同实例针对同一主机执行相同的查询+变量,会命中缓存。仅非空且无错误的会被缓存。操作绝不会被缓存。
sdk.graphql!.query()localStoragebaseUrlcreateDataSDK()datamutate()有两种独立的数据新鲜度工具——请勿混淆:
- 单次调用的——查询选项中的一次性策略覆盖(
cacheControl/"no-cache"/"only-if-cached")。类型及各值的具体行为详见references/sdk-api.md。将{ type: "max-age", maxAge: <seconds> }作为读取函数的可选参数,并在同一数据层文件中暴露不同策略的轻量命名导出——“调用点”是命名导出,而非新的React组件。例如cacheControl:getAccounts(first, after?, cacheControl?)(同理export const refreshAccounts = () => getAccounts(20, undefined, "no-cache")→offlineAccounts,"only-if-cached"→shortLivedAccounts)。将策略逻辑保留在数据层。{ type: "max-age", maxAge: 10 } - 响应式/
subscribe——实时refresh的状态句柄:QueryResult会在每次快照更新时触发回调,result.subscribe(cb)会绕过缓存重新获取数据并推送给订阅者。结构详见references/sdk-api.md;订阅生命周期(组件销毁时务必取消订阅)详见references/caching.md。result.refresh()
| 需求 | 使用工具 |
|---|---|
| 5分钟内的数据新鲜度即可满足 | 无需操作(默认缓存) |
| 本次读取必须绕过缓存(刷新按钮) | |
| 仅读取缓存数据,容忍缺失(离线优先) | |
| 为本次查询设置更短/更长的TTL | |
| 已挂载组件随时间反映数据更新 | |
| 立即重新获取数据并通知所有订阅者(如写入操作后) | |
cacheControlsubscriberefreshWorking on existing code (migration)
现有代码迁移
Only enter this path if the existing code actually uses the old API — i.e. it imports
or calls the callable form. For any new
read/write, ignore migration entirely and use the Read workflow /
Write workflow — those already show the only correct API.
@salesforce/sdk-datasdk.graphql(query, vars)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.
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.)
- HTTP 200 ≠ success — always parse ; the Promise resolves even on failure.
result.errors - Schema is the only source of truth — verify, never invent. Verify every
entity/field/type via graphiti (preferred) or
sf-gql-discoverbefore use. Case-sensitive;bash <skill-dir>/scripts/graphql-search.sh <Entity>/__c;__eentity suffix (v60+). When graphiti is primed, a "not found"/empty/_Recordanswer (including fromCannot query field/graphql-codegen, even when the message points at@graphql-eslint) 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)schema.graphql- assign perms + refresh (/
sf-gql-connect --forceRefresh). 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.npm run graphql:schemaand the codegen output (schema.graphql) are read-only generated mirrors — never open or edit them (honor anysrc/api/graphql-operations-types.tsmarker). 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 /# DO NOT EDIT).SCHEMA_PRIME_FAILED
- assign perms + refresh (
- 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
@optional, the connection plumbing (Id,edges, the connection field), ornode— those are not FLS-gated and the graphiti output leaves them bare. Consume withpageInfo/?.. Placement rules: references/graphql-hand-authoring.md.?? - Mutations wrap under ; set
uiapi(input: { allOrNone: ... })explicitly; output excludes child/navigated-reference fields; the output field is literally namedallOrNone(unrelated to theRecordentity suffix in rule 2) — Delete →_Recordonly. GA v66+.Id - Explicit pagination — always set , because the server silently caps at 10 and you'll drop rows with no error; forward-only (
first:/first, noafter/last);before(v59+) raises the per-request ceiling for large sets (when set,upperBoundmust be 200–2000).first - SOQL governor limits apply — 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.
uiapi - Field value wrappers — read the raw value via ;
.valueis the server-formatted string for UI. When a field is both shown and operated on (currency, dates, picklists), select bothdisplayValueandvalueso you don't reformat on the client. Display-only fields can take justdisplayValue.displayValue - Compound fields — filter/order on constituents (), not the wrapper (
BillingCity).BillingAddress - Supported APIs only — GraphQL (), UI API REST, Apex REST, Connect REST, Einstein LLM via
uiapi. NOT: Enterprise SOQLsdk.fetch, Aura-enabled Apex, Chatter (use/query). See references/rest-and-integration.md.uiapi.currentUser
One SDK convention lives in the workflows, not this list (it's not a platform behavior): always runand use the generated types after writing an operation (Read workflow step 3). Also in the Pre-flight checklist.npm run graphql:codegengraphiti applies most of these for you. When you compile a query withagainst an object that's in the primed schema, rules 3 (sf-gql-*), 4 (mutation@optionaloutput envelope and entity-keyed input — notRecord, which you still add yourself), 5 (allOrNone/first:), and 7 (pageInfo/valuewrappers) come out already satisfied — which is exactly why you paste thedisplayValueverbatim rather than re-deriving it. Rules 1 (checkquery), 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-emptyresult.errorsarray means it isn't, and the emitted query is degraded (bare fields, no guardrails) — see references/graphiti-cli.md.warnings
这些是Salesforce GraphQL平台的固有行为,与SDK无关。违反约束会导致静默的运行时失败。(详情及模板:references/graphql-hand-authoring.md)
- HTTP 200 ≠ 操作成功——务必解析;即使操作失败,Promise也会resolve。
result.errors - Schema是唯一的事实来源——必须验证,绝不臆造。使用graphiti (首选)或
sf-gql-discover验证所有实体/字段/类型后再使用。注意大小写敏感;bash <skill-dir>/scripts/graphql-search.sh <Entity>/__c后缀;__e实体后缀(v60+)。当graphiti已初始化时,“未找到”/空结果/_Record错误(包括来自Cannot query field/graphql-codegen的错误,即使错误信息指向@graphql-eslint)是组织的实际情况——名称错误或未部署/不可访问的元数据,而非工具故障:修正操作,或部署元数据(platform-metadata-deploy技能负责此操作)+分配权限+刷新缓存(schema.graphql/sf-gql-connect --forceRefresh)。请勿回退到脚本、手动绕过或臆造名称——臆造的实体或字段会导致整个查询在运行时静默失败;若查询工具无法得到一致结果,请询问用户而非继续尝试。npm run graphql:schema和代码生成输出(schema.graphql)是只读的生成镜像——请勿打开或编辑(遵守任何src/api/graphql-operations-types.ts标记)。手动添加缺失的类型可满足代码生成/ lint要求,但无法获得组织的访问权限;只会隐藏错误直到运行时。仅当CLI完全无法运行时(无依赖/# DO NOT EDIT),才回退到脚本。SCHEMA_PRIME_FAILED - 每个嵌套层级中受FLS限制的字段都需添加——包括标量叶子字段、每个父/子关系及其内部字段(否则FLS验证会导致整个查询失败,v65+)。请勿为
@optional、连接结构(Id、edges、连接字段)或node添加——这些不受FLS限制,graphiti输出的内容已正确处理。使用pageInfo/?.消费数据。放置规则详见:references/graphql-hand-authoring.md。?? - 突变操作需包裹在中;显式设置
uiapi(input: { allOrNone: ... });输出不包含子/导航引用字段;输出字段名称为allOrNone(与规则2中的Record实体后缀无关)——删除操作仅需选择_Record。GA版本v66+。Id - 显式分页——始终设置,因为服务器会静默限制为10条记录,且不会报错;仅支持向前分页(
first:/first,不支持after/last);before(v59+)可提高单次请求的记录上限(设置后,upperBound必须为200–2000)。first - SOQL governor限制适用——查询会编译为SOQL,因此继承相同的governor限制:子查询≤10个,子→父层级≤5个,父→子层级≤1个,每个子查询的记录数≤2000。若超出限制,请拆分为多个请求。
uiapi - 字段值包装——通过读取原始值;
.value是服务器格式化的UI显示字符串。当字段既要显示又要操作时(如货币、日期、选择列表),需同时选择**displayValue和value**,避免在客户端重新格式化。仅用于显示的字段可仅选择displayValue。displayValue - 复合字段——基于组成字段过滤/排序(如),而非包装字段(如
BillingCity)。BillingAddress - 仅使用支持的API——包括GraphQL()、UI API REST、Apex REST、Connect REST、通过
uiapi调用的Einstein LLM。禁止使用:Enterprise SOQLsdk.fetch、支持Aura的Apex、Chatter(使用/query)。详见references/rest-and-integration.md。uiapi.currentUser
graphiti会自动应用大部分约束。当针对已初始化Schema中的对象使用编译查询时,规则3(sf-gql-*)、4(突变@optional输出包和实体键输入——不包括Record,仍需手动设置)、5(allOrNone/first:)、7(pageInfo/value包装)会自动满足——这正是为什么要直接粘贴displayValue内容而非重新推导。规则1(检查query)、6(governor限制)、8(复合字段)、9(支持的API)仍需自行遵守。仅当对象已初始化时,自动化才会生效:非空result.errors数组意味着对象未初始化,生成的查询存在缺陷(无防护约束的裸字段)——详见references/graphiti-cli.md。warnings
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| Command | Run from | Purpose |
|---|---|---|
| UI bundle dir | Discover objects/fields against the live org (preferred grounding) |
| UI bundle dir | Compile a guardrail-applied query/mutation (references/graphiti-cli.md) |
| UI bundle dir | Refresh graphiti's schema cache after a deploy |
| project root (or pass | Schema lookup fallback (grep over local |
| UI bundle dir | Fetch/refresh |
| UI bundle dir | Generate operation types |
| UI bundle dir | Lint (catches |
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调用代码| 命令 | 运行目录 | 用途 |
|---|---|---|
| UI bundle目录 | 针对在线组织发现对象/字段(首选查询方式) |
| UI bundle目录 | 编译带有防护约束的查询/突变语句(references/graphiti-cli.md) |
| UI bundle目录 | 部署后刷新graphiti的Schema缓存 |
| 项目根目录(或通过 | Schema查询回退方式(在本地 |
| UI bundle目录 | 获取/刷新 |
| UI bundle目录 | 生成操作类型定义 |
| UI bundle目录 | 代码检查(捕获 |
Pre-flight checklist
预检查清单
- Surface decided: only if WebApp-only; otherwise guard with
sdk.graphql!(Surfaces)if (!sdk.graphql) … - Every field/entity verified — (preferred) or
sf-gql-discover(fallback, against the right schema)graphql-search.sh - If compiled with graphiti: confirmed (non-empty = degraded query, don't ship);
warnings: []pasted verbatimquery - on FLS-gated fields + relationships (NOT
@optional/Id/edges/node);pageInfo/?.in consuming code?? - checked before reading
result.errorsresult.data - Caching considered: default 300s OK, or /
cacheControlchosen deliberatelyrefresh - run; generated types used;
npm run graphql:codegenpassesnpx eslint
- 已确定运行环境:仅当仅运行于WebApp时使用;否则添加
sdk.graphql!防护检查(运行环境)if (!sdk.graphql) … - 所有字段/实体已验证——使用(首选)或
sf-gql-discover(回退方式,针对正确的Schema)graphql-search.sh - 若使用graphiti编译:已确认(非空=查询存在缺陷,请勿发布);已直接粘贴
warnings: []内容query - 受FLS限制的字段及关系已添加(
@optional/Id/edges/node除外);消费代码中使用pageInfo/?.?? - 读取前已检查
result.dataresult.errors - 已考虑缓存策略:默认300秒缓存满足需求,或已明确选择/
cacheControlrefresh - 已运行;已使用生成的类型;
npm run graphql:codegen检查通过npx eslint