experience-ui-bundle-salesforce-data-access
このSkillは、UIバンドル内でSalesforceデータアクセスを効率的に実現し、@salesforce/platform-sdkを使用して読み取り、書き込み、キャッシュ、リフレッシュ操作を処理し、GraphQLのベストプラクティスとプラットフォームガードレールを自動適用します。開発者が迅速に信頼性の高いデータ層を構築し、一般的なミスを回避するのに役立ちます。
npx skills add https://github.com/forcedotcom/sf-skills --skill experience-ui-bundle-salesforce-data-accessBefore / After 効果比較
1 组このSkillがない場合、開発者は手動で認証、CSRF、キャッシュ、GraphQLクエリ作成、エラーハンドリングを処理する必要があり、平均60分かかり、エラー率は30%でした。
このSkillを使用すると、標準化されたSDKと自動ガードレールにより、開発時間が10分、エラー率が5%に短縮され、データ層がより信頼性の高いものになります。
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
graphitiCLI (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/subscribevs per-callcacheControl). - 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-datacallable 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 staledist/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.graphql | Write |
|---|---|---|
| WebApp only | always 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.
Step 0 — Route the task
| 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 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.
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.
| # | Requirement | Verify | If missing |
|---|---|---|---|
| 1 | @salesforce/platform-sdk installed | package.json in the UI bundle dir | Tell user to install it; cannot proceed |
| 2 | A grounding tool resolves | Preferred: 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) |
| 3 | Target objects/fields deployed | The 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
-
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, thennpx 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 orforceRefresh; 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.) -
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, andfirst:/pageInfoalready applied. Confirmwarnings: [](a non-empty array means the object wasn't in the primed schema — the query is degraded; don't ship it), then paste thequeryverbatim into inlinegql(simple) or an external.graphqlfile (one operation per file, imported with the bundler's?rawsuffix —import Q from "./q.graphql?raw"brings the file in as a plain string). Fallback — hand-author: apply@optionalto every selectable FLS-gated field — scalar leaf fields (Name @optional { value }) and parent/child relationships and the fields inside them — but NOT onId, on connection plumbing (edges,node, the connection field itself), or onpageInfo; the graphiti output leaves those bare and is the canonical placement. Always setfirst:, includepageInfoif it may page. Either way, full mechanics and the primed-vs-degraded behavior: references/graphiti-cli.md. -
Generate types —
npm run graphql:codegen(from the UI bundle dir) →src/api/graphql-operations-types.ts. -
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 }); -
Handle the result.
result.data+result.errorsare the initial snapshot;result.subscribe/result.refreshare the reactive handles. Always checkerrorsbefore readingdata: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:
| Need | Use | Returns |
|---|---|---|
| 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 **curren
...
ユーザーレビュー (0)
レビューを書く
レビューなし
統計データ
ユーザー評価
この Skill を評価