afv-library/skills/experience-ui-bundle-salesforce-data-access/references/sdk-api.md

8.6 KiB

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; for old→new conversion see migration.md.

Import

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:

const sdk = await createDataSDK();

// QUERY — reactive, cached on WebApp
const result = await sdk.graphql!.query<TData, TVariables>({
  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<TData, TVariables>({
  mutation: MY_MUTATION,  // NOTE: the key is `mutation`, not `query`
  variables,
  operationName,
});

Type signatures (from core/data.ts):

query<T, V>(options: QueryOptions<V>): Promise<QueryResult<T>>;
mutate<T, V>(options: MutateOptions<V>): Promise<MutationResult<T>>;

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:

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

// `!` 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<T> — the reactive query handle

query() resolves to a QueryResult<T>, which IS a snapshot AND carries two reactive methods:

interface QuerySnapshot<T> { data: T | undefined; errors?: GraphQLError[]; }
type QuerySubscriber<T> = (snapshot: QuerySnapshot<T>) => void;
type Unsubscribe = () => void;

interface QueryResult<T> extends QuerySnapshot<T> {
  subscribe(cb: QuerySubscriber<T>): Unsubscribe;
  refresh(): Promise<void>;
}
  • 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<void>.

When to reach for subscribe/refresh over the default cache, plus the uncached-surface caveats, is a strategy call — see 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:

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: <seconds> } 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:

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.

MutationResult<T> — one-shot, NO subscribe/refresh

interface MutationResult<T> { 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).

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:

// accounts.ts pattern
if (result.errors?.length) {
  throw new Error(result.errors.map((e) => e.message).join("; "));
}
// graphqlClient.ts (canonical scaffold) — strict wrapper
export async function executeGraphQL<TData, TVariables>(
  query: string,
  variables?: TVariables,
): Promise<TData> {
  const sdk = await createDataSDK();
  const result = await sdk.graphql!.query<TData, TVariables>({ 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):

// 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 <Op>Query / <Op>QueryVariables types and pass them as the type parameters. The call shape changed — types now go on .query(), not the old callable:

import type { GetAccountsQuery, GetAccountsQueryVariables } from "../graphql-operations-types";

const result = await sdk.graphql!.query<GetAccountsQuery, GetAccountsQueryVariables>({
  query: GET_ACCOUNTS,
  variables,
});

NodeOfConnection<T> extracts a node type from a Connection for cleaner typing:

import { type NodeOfConnection } from "@salesforce/platform-sdk";
type AccountNode = NodeOfConnection<GetAccountsQuery["uiapi"]["query"]["Account"]>;