# The Data SDK call API (`@salesforce/platform-sdk`) The `query`/`mutate` namespace, typing, and error handling. For caching and the reactive refresh modes see [caching.md](caching.md); for old→new conversion see [migration.md](migration.md). ## Import ```typescript import { createDataSDK, gql, type CacheControl, type NodeOfConnection } from "@salesforce/platform-sdk"; ``` `createDataSDK`, `gql`, `CacheControl`, and `NodeOfConnection` all come from **`@salesforce/platform-sdk`**. The old `@salesforce/sdk-data` package name is dead — any `@salesforce/sdk-data` string you see in the repo is a stale `dist/` build artifact, not the canonical import. ## `sdk.graphql` is a NAMESPACE, not a callable The previous SDK exposed `sdk.graphql` as a callable: `await sdk.graphql?.(query, vars)`. That form is gone. `sdk.graphql` is now a namespace with two methods: ```typescript const sdk = await createDataSDK(); // QUERY — reactive, cached on WebApp const result = await sdk.graphql!.query({ query: MY_QUERY, // gql-tagged string (the `query` key) variables, // optional operationName, // optional cacheControl, // optional — see caching.md }); // MUTATE — request/response, never cached const { data, errors } = await sdk.graphql!.mutate({ mutation: MY_MUTATION, // NOTE: the key is `mutation`, not `query` variables, operationName, }); ``` Type signatures (from `core/data.ts`): ```typescript query(options: QueryOptions): Promise>; mutate(options: MutateOptions): Promise>; ``` ### `sdk.graphql!` vs guard `sdk.graphql` may still be `undefined` on surfaces that do not support data operations, so it is typed `graphql?: DataSDKGraphQL`. Real consumer code asserts it is present after `createDataSDK()` with the non-null assertion: ```typescript const result = await sdk.graphql!.query<...>({ query, variables }); ``` This replaces the old optional-call `sdk.graphql?.(...)`. Do **not** reach for the dead callable form. Which of `!` vs a guard you use is a **surface decision** (the spine's [Surfaces](../SKILL.md#surfaces--sdkgraphql-vs-guard) table is the routing): `!` only if the bundle is WebApp-exclusive; otherwise guard, because 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. ```typescript // `!` form — WebApp-only bundles; every shipped WebApp consumer uses it. const result = await sdk.graphql!.query<...>({ query, variables }); // Portable guard form — safe on every surface. Use when the bundle is not WebApp-exclusive. if (!sdk.graphql) { // No data SDK on this surface — degrade gracefully (render empty, throw, or feature-flag off). return; } const result = await sdk.graphql.query<...>({ query, variables }); ``` (The same `!`-vs-guard call applies to `sdk.fetch!`.) ## `QueryResult` — the reactive query handle `query()` resolves to a `QueryResult`, which IS a snapshot AND carries two reactive methods: ```typescript interface QuerySnapshot { data: T | undefined; errors?: GraphQLError[]; } type QuerySubscriber = (snapshot: QuerySnapshot) => void; type Unsubscribe = () => void; interface QueryResult extends QuerySnapshot { subscribe(cb: QuerySubscriber): Unsubscribe; refresh(): Promise; } ``` - `result.data` / `result.errors` — the initial snapshot at await-time (cached value on cached surfaces, else the network response). - `result.subscribe(cb)` — register a callback for every *subsequent* snapshot (a cache update, a stale-while-revalidate refill, or an explicit `refresh()`). Returns an `Unsubscribe`. Each `subscribe` is **independent** — unsubscribing one leaves the others live — and it does **not** fire on registration, only on later snapshots. - `result.refresh()` — re-issue the request bypassing the cache, write the fresh result back, and push the new snapshot to **all** current subscribers. Returns `Promise`. *When* to reach for `subscribe`/`refresh` over the default cache, plus the uncached-surface caveats, is a strategy call — see [caching.md](caching.md). ## `cacheControl` — the per-call cache policy `cacheControl` is an optional field on the query options bag. It overrides the default cache behavior for **one** call and does **not** change the cache key, so a `"no-cache"` call and a default call read/write the *same* cache slot. The type: ```typescript type CacheControlShorthand = "no-cache" | "only-if-cached"; interface CacheControlMaxAge { type: "max-age"; maxAge: number; } type CacheControl = CacheControlShorthand | CacheControlMaxAge; ``` | `cacheControl` | Behavior | |---|---| | *(omitted)* | Default — return the cached entry if fresh (300s TTL), else fetch and write back. | | `"no-cache"` | Skip the cache **read**, always hit the network. **Still writes the response back** for later default callers. | | `"only-if-cached"` | Read from cache **only**. Hit → return; **miss → `DataNotFoundError` on `result.errors`** (no network, no throw). | | `{ type: "max-age", maxAge: }` | Custom TTL instead of 300s. `maxAge: 0` = written but immediately stale; invalid values silently fall back to 300s. | An `only-if-cached` miss is not an exception — the Promise resolves and the miss surfaces on `result.errors` as a `DataNotFoundError`. This is offline-first: a miss is **expected, not an error**. Check `result.errors`, render an empty state, and **do not fall back to a network read** — a fallback defeats the point of `only-if-cached`: ```typescript const result = await sdk.graphql!.query({ query: GET_ACCOUNTS, variables, cacheControl: "only-if-cached" }); if (result.errors?.length) { // cold cache — render the empty state. Do NOT fall back to the network; that defeats offline-first. return renderEmptyState(); } ``` Which policy fits which goal (force-refresh button, offline-first, fast-changing data) is a strategy call — see the decision matrix in [caching.md](caching.md). ## `MutationResult` — one-shot, NO subscribe/refresh ```typescript interface MutationResult { data: T | undefined; errors?: GraphQLError[]; } ``` Mutations are request/response. The type deliberately has **no** `subscribe` and **no** `refresh` — mutating on subscribe is incoherent, and re-running a mutation on refresh is dangerous. To refresh data after a mutation, hold a query `result` and call `result.refresh()` (see [caching.md](caching.md)). ## HTTP 200 ≠ success — always read `errors` The Promise resolves even for GraphQL/parse errors; they surface on `result.errors`, never as a thrown exception from `query()`/`mutate()`. Both shipped consumers gate on `errors` first: ```typescript // accounts.ts pattern if (result.errors?.length) { throw new Error(result.errors.map((e) => e.message).join("; ")); } ``` ```typescript // graphqlClient.ts (canonical scaffold) — strict wrapper export async function executeGraphQL( query: string, variables?: TVariables, ): Promise { const sdk = await createDataSDK(); const result = await sdk.graphql!.query({ query, variables }); if (result.errors?.length) { throw new Error(`GraphQL Error: ${result.errors.map((e) => e.message).join("; ")}`); } if (result.data == null) { throw new Error("GraphQL response data is null"); } return result.data; } ``` Three error-handling stances (all read `result.errors`): ```typescript // Strict — any errors = failure if (result.errors?.length) throw new Error(result.errors.map((e) => e.message).join("; ")); // Tolerant — log, use whatever data came back (partial success) if (result.errors?.length) console.warn("GraphQL partial errors:", result.errors); // Discriminated — fail only when NO data returned if (!result.data && result.errors?.length) { throw new Error(result.errors.map((e) => e.message).join("; ")); } ``` ## Generated types on the call After `npm run graphql:codegen`, import the generated `Query` / `QueryVariables` types and pass them as the type parameters. The call shape changed — types now go on `.query()`, not the old callable: ```typescript import type { GetAccountsQuery, GetAccountsQueryVariables } from "../graphql-operations-types"; const result = await sdk.graphql!.query({ query: GET_ACCOUNTS, variables, }); ``` `NodeOfConnection` extracts a node type from a Connection for cleaner typing: ```typescript import { type NodeOfConnection } from "@salesforce/platform-sdk"; type AccountNode = NodeOfConnection; ```