首页/AI 应用构建与集成/experience-ui-bundle-salesforce-data-access
E

experience-ui-bundle-salesforce-data-access

by @forcedotcomv
4.4(120)

此技能能在UI bundle中高效实现Salesforce数据访问,通过@salesforce/platform-sdk处理读写、缓存与刷新操作,并自动应用GraphQL最佳实践与平台安全守卫。帮助开发者快速搭建可靠的数据层,避免常见错误。

salesforcedata-accessgraphqlplatform-sdkui-bundleGitHub
安装方式
npx skills add https://github.com/forcedotcom/sf-skills --skill experience-ui-bundle-salesforce-data-access
compare_arrows

Before / After 效果对比

1 组
使用前

在没有此技能前,开发者需手动处理Salesforce数据访问的认证、CSRF、缓存、GraphQL查询编写与错误处理,平均耗时60分钟,且错误率高达30%。

使用后

使用此技能后,通过标准化SDK和自动守卫,开发时间缩短至10分钟,错误率降至5%,数据层更可靠。

SKILL.md

Salesforce Data Access (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.

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.

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.


Surfaces — sdk.graphql! vs guard

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.graphqlWrite
WebApp onlyalways presentsdk.graphql!.query({...}) — ! is safe; every shipped WebApp consumer uses it
Mosaic / OpenAI / MCPApps (or any bundle that might run off-WebApp)can be undefinedguard 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.


Step 0 — Route the task

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) codeWorking 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.


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 installedpackage.json in the UI bundle dirTell 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.


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:

    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:

    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.


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:

// 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.


Beyond record 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 recordsuiapi { query { <Entity>(...) } }records (the Read workflow)
Counts / sums / grouped rollups without pulling rowsuiapi { aggregate { <Entity>(groupBy: …) } }aggregated buckets
Object/field metadata — labels, data types, createable/updateable, record typesuiapi { 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 listuiapi { 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 **curren

...

用户评价 (0)

发表评价

效果
易用性
文档
兼容性

暂无评价

统计数据

安装量5.2K
评分4.4 / 5.0
版本
更新日期2026年10月11日
对比案例1 组

用户评分

4.4(120)
5
37%
4
43%
3
13%
2
5%
1
2%

为此 Skill 评分

0.0

兼容平台

🤖claude-code

时间线

创建2026年7月26日
最后更新2026年10月11日
🎁 Agent 知识卡片
调研问卷