@salesforce/afv-skills 1.24.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/commerce-b2b-open-code-components-replace/SKILL.md +244 -0
- package/skills/commerce-b2b-open-code-components-replace/assets/ootb-to-open-code-mapping.json +66 -0
- package/skills/dx-devops-test-failures-analyze/SKILL.md +89 -0
- package/skills/dx-devops-test-failures-analyze/references/code-analyzer-violations.md +26 -0
- package/skills/dx-devops-test-failures-analyze/references/failure-categories.md +85 -0
- package/skills/{checking-devops-prerequisites/SKILL.md → dx-devops-test-failures-analyze/references/prerequisite-checks.md} +8 -37
- package/skills/{creating-fix-work-item/SKILL.md → dx-devops-test-failures-analyze/references/work-item-creation.md} +8 -12
- package/skills/dx-devops-test-pipeline-configure/SKILL.md +72 -0
- package/skills/dx-devops-test-pipeline-configure/references/configuring-quality-gate.md +133 -0
- package/skills/dx-devops-test-pipeline-configure/references/configuring-test-provider.md +80 -0
- package/skills/dx-devops-test-pipeline-configure/references/error-handling.md +39 -0
- package/skills/dx-devops-test-pipeline-configure/references/gotchas.md +37 -0
- package/skills/dx-devops-test-pipeline-configure/references/prerequisite-checks.md +112 -0
- package/skills/dx-devops-test-pipeline-configure/references/syncing-test-providers.md +69 -0
- package/skills/dx-devops-test-suite-assignments-configure/SKILL.md +74 -0
- package/skills/dx-devops-test-suite-assignments-configure/references/api-endpoint.md +30 -0
- package/skills/dx-devops-test-suite-assignments-configure/references/error-handling.md +14 -0
- package/skills/dx-devops-test-suite-assignments-configure/references/prerequisite-checks.md +112 -0
- package/skills/{recommending-devops-tests/SKILL.md → dx-devops-test-suite-assignments-configure/references/recommendation-logic.md} +10 -26
- package/skills/dx-devops-test-suite-assignments-configure/references/suite-assignment-modes.md +99 -0
- package/skills/dx-devops-test-suite-run/SKILL.md +111 -0
- package/skills/dx-devops-test-suite-run/references/error-handling.md +31 -0
- package/skills/dx-devops-test-suite-run/references/polling-configuration.md +78 -0
- package/skills/dx-devops-test-suite-run/references/prerequisite-checks.md +112 -0
- package/skills/dx-devops-test-suite-run/references/retrigger-mode.md +51 -0
- package/skills/dx-org-manage/SKILL.md +192 -0
- package/skills/dx-org-manage/examples/README.md +45 -0
- package/skills/dx-org-manage/examples/scratch-orgs/error_no_devhub.json +9 -0
- package/skills/dx-org-manage/examples/scratch-orgs/error_timeout.json +13 -0
- package/skills/dx-org-manage/examples/scratch-orgs/success_definition_file.json +28 -0
- package/skills/dx-org-manage/examples/scratch-orgs/success_edition.json +26 -0
- package/skills/dx-org-manage/examples/scratch-orgs/success_snapshot.json +27 -0
- package/skills/dx-org-manage/examples/snapshots/error_output.json +9 -0
- package/skills/dx-org-manage/examples/snapshots/success_output.json +15 -0
- package/skills/dx-org-manage/references/cli_flags.md +67 -0
- package/skills/dx-org-manage/references/creating-scratch-org.md +164 -0
- package/skills/dx-org-manage/references/creating-snapshot.md +103 -0
- package/skills/dx-org-manage/references/definition_file_options.md +224 -0
- package/skills/dx-org-manage/references/edition_types.md +78 -0
- package/skills/dx-org-manage/references/opening-org.md +160 -0
- package/skills/dx-org-manage/references/snapshot_usage.md +74 -0
- package/skills/dx-org-permission-set-assign/SKILL.md +98 -0
- package/skills/dx-org-permission-set-assign/examples/error_output.json +19 -0
- package/skills/dx-org-permission-set-assign/examples/success_output.json +16 -0
- package/skills/dx-org-permission-set-assign/references/cli_flags.md +68 -0
- package/skills/experience-cms-brand-apply/SKILL.md +1 -1
- package/skills/experience-ui-bundle-app-coordinate/SKILL.md +31 -19
- package/skills/experience-ui-bundle-file-upload-generate/SKILL.md +1 -1
- package/skills/experience-ui-bundle-frontend-generate/implementation/header-footer.md +1 -1
- package/skills/experience-ui-bundle-salesforce-data-access/SKILL.md +336 -581
- package/skills/experience-ui-bundle-salesforce-data-access/references/caching.md +172 -0
- package/skills/experience-ui-bundle-salesforce-data-access/references/graphiti-cli.md +373 -0
- package/skills/experience-ui-bundle-salesforce-data-access/references/graphql-hand-authoring.md +376 -0
- package/skills/experience-ui-bundle-salesforce-data-access/references/migration.md +119 -0
- package/skills/experience-ui-bundle-salesforce-data-access/references/rest-and-integration.md +152 -0
- package/skills/experience-ui-bundle-salesforce-data-access/references/sdk-api.md +217 -0
- package/skills/experience-ui-bundle-salesforce-data-access/scripts/graphql-search.sh +36 -9
- package/skills/platform-agentsetup-categories-fetch/SKILL.md +109 -0
- package/skills/platform-agentsetup-categories-fetch/references/api-response-schema.md +121 -0
- package/skills/platform-custom-object-generate/SKILL.md +62 -7
- package/skills/platform-custom-object-generate/references/description-enrichment.md +125 -0
- package/skills/platform-metadata-retrieve/SKILL.md +121 -0
- package/skills/platform-metadata-retrieve/examples/error_output.json +10 -0
- package/skills/platform-metadata-retrieve/examples/success_output.json +27 -0
- package/skills/platform-metadata-retrieve/references/cli_flags.md +138 -0
- package/skills/platform-metadata-retrieve/references/retrieval_modes.md +181 -0
- package/skills/platform-sharing-rules-generate/SKILL.md +165 -0
- package/skills/platform-sharing-rules-generate/references/rule-types.md +199 -0
- package/skills/platform-tracing-agentforce-configure/SKILL.md +118 -0
- package/skills/platform-tracing-agentforce-configure/assets/AgentforcePlatformTracing-template.xml +4 -0
- package/skills/platform-tracing-configure/SKILL.md +118 -0
- package/skills/platform-tracing-configure/assets/EventSettings-template.xml +4 -0
- package/skills/platform-trust-archive-manage/SKILL.md +25 -11
- package/skills/platform-trust-archive-manage/examples/monitor-failed-jobs.md +2 -2
- package/skills/platform-trust-archive-manage/references/archive-activity-entity.md +1 -1
- package/skills/platform-trust-archive-manage/references/connect-api-operations.md +51 -12
- package/skills/analyzing-test-failures/SKILL.md +0 -159
- package/skills/configuring-quality-gate/SKILL.md +0 -120
- package/skills/configuring-test-provider/SKILL.md +0 -113
- package/skills/managing-suite-assignments/SKILL.md +0 -161
- package/skills/polling-test-results/SKILL.md +0 -72
- package/skills/running-devops-test-suite/SKILL.md +0 -144
- package/skills/syncing-test-providers/SKILL.md +0 -108
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Caching & the reactive query result
|
|
2
|
+
|
|
3
|
+
Deep reference for the WebApp resource cache and the two refresh tools. The
|
|
4
|
+
[SKILL.md](../SKILL.md#freshness--caching) Freshness & caching section is the summary; this is
|
|
5
|
+
the full behavior.
|
|
6
|
+
|
|
7
|
+
> **Surface caveat up front:** everything about *caching* below is the **WebApp surface**. On
|
|
8
|
+
> uncached surfaces (Mosaic, OpenAI) there is no cache — see
|
|
9
|
+
> [Uncached surfaces](#uncached-surfaces-mosaic-openai) at the end. The `query`/`mutate`
|
|
10
|
+
> namespace shape is identical on every surface, so call sites stay portable.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. Caching is ON by default (WebApp) — do NOT reinvent it
|
|
15
|
+
|
|
16
|
+
Every `sdk.graphql!.query()` on WebApp is cached automatically. There is **no opt-in flag, no
|
|
17
|
+
`createCachedClient` factory, and no `/cache` import subpath** — if you have heard of those,
|
|
18
|
+
they do not exist in `@salesforce/platform-sdk`. There is also **no need for React Query, SWR,
|
|
19
|
+
`localStorage`, or any hand-rolled memoization**. If you find yourself writing a cache, **stop**
|
|
20
|
+
— it already exists.
|
|
21
|
+
|
|
22
|
+
Default policy: **`max-age` with a 300-second TTL** (`DEFAULT_MAX_AGE_SECONDS = 300`).
|
|
23
|
+
|
|
24
|
+
| Situation | Behavior |
|
|
25
|
+
|-----------|----------|
|
|
26
|
+
| Cache hit (entry < 300s old) | Return cached `data` immediately — **no network call** |
|
|
27
|
+
| Stale (entry > 300s old) | Treated as a miss → fetch from network → write back (300s TTL) |
|
|
28
|
+
| Miss (no entry) | Fetch → write to cache (300s TTL) → return |
|
|
29
|
+
|
|
30
|
+
`mutate()` is **never** cached — it is a pass-through to the network.
|
|
31
|
+
|
|
32
|
+
### What gets cached
|
|
33
|
+
|
|
34
|
+
Only **successful responses with a non-empty `data` object** are written:
|
|
35
|
+
|
|
36
|
+
- `data` is `null`, missing, or `{}` → **NOT cached** (an empty `data` usually means a
|
|
37
|
+
transient server condition; caching it would poison the cache for the full TTL).
|
|
38
|
+
- Response carries a non-empty `errors` array → surfaced as an error and **NOT cached**.
|
|
39
|
+
|
|
40
|
+
### Cache key
|
|
41
|
+
|
|
42
|
+
The key is `stableJSONStringify({ query, variables, operationName })`.
|
|
43
|
+
|
|
44
|
+
- **`cacheControl` does NOT affect the key.** The same query + variables share **one** cache
|
|
45
|
+
entry no matter what policy each call passes. A `"no-cache"` call and a default call
|
|
46
|
+
read/write the *same* slot.
|
|
47
|
+
- The query is keyed by its **raw string** — two semantically identical queries with different
|
|
48
|
+
whitespace produce **different** entries. Reuse the same `gql`-tagged constant; do not
|
|
49
|
+
re-template the same query inline per call.
|
|
50
|
+
- Variables are deep-cloned at call time, so mutating your variables object afterward does not
|
|
51
|
+
desync the key from the request body. Variables must be JSON-serializable (circular refs /
|
|
52
|
+
BigInt throw a typed error).
|
|
53
|
+
|
|
54
|
+
### Shared across SDK instances by `baseUrl`
|
|
55
|
+
|
|
56
|
+
Cache bundles are deduped in a module-level registry keyed by the resolved `baseUrl`. **A query
|
|
57
|
+
run through one `createDataSDK()` instance is a cache hit on another instance targeting the same
|
|
58
|
+
host.** Independently-built features that each create their own SDK do not issue redundant
|
|
59
|
+
network requests for the same data.
|
|
60
|
+
|
|
61
|
+
The per-instance fetch pipeline (CSRF, `onStatus`) stays **isolated** — a cache *miss* routes
|
|
62
|
+
through the calling SDK's own fetch, so per-instance request behavior is preserved.
|
|
63
|
+
|
|
64
|
+
**Practical implication:** you do **not** need to hoist `createDataSDK()` into a singleton purely
|
|
65
|
+
to share cache. Calling it per-feature is fine; the cache is shared by host underneath. (A
|
|
66
|
+
singleton is still reasonable for other reasons.)
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 2. The two refresh tools (keep them distinct)
|
|
71
|
+
|
|
72
|
+
There are **two unrelated mechanisms** for getting fresh data. They have different shapes and
|
|
73
|
+
different mental models. Do not conflate `result.refresh()` (a method on a live handle) with
|
|
74
|
+
`cacheControl: "no-cache"` (a per-call option).
|
|
75
|
+
|
|
76
|
+
| | **Reactive refresh** | **Call-site cache control** |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| **API** | `result.subscribe(cb)` + `result.refresh()` | `cacheControl` on the query options bag |
|
|
79
|
+
| **Lifetime** | Long-lived handle; subscription persists until you unsubscribe | One-shot, fire-and-forget per call |
|
|
80
|
+
| **Pushes updates?** | Yes — `subscribe` fires on every subsequent snapshot | No — you read the returned value once |
|
|
81
|
+
| **PR** | #502 | #537 (W-22514759) |
|
|
82
|
+
| **Use when** | A mounted component should react to cache updates or re-fetch on demand | "This specific read must bypass / only-use / re-TTL the cache" |
|
|
83
|
+
|
|
84
|
+
### 2a. Reactive refresh — `subscribe` + `refresh`
|
|
85
|
+
|
|
86
|
+
`query()` resolves a **`QueryResult<T>`** — a snapshot (`data`/`errors`) plus `subscribe(cb)`
|
|
87
|
+
and `refresh()`. The contract (type shape, the independent-subscription and fire-on-subsequent
|
|
88
|
+
semantics) lives in [sdk-api.md](sdk-api.md#queryresultt--the-reactive-query-handle); this
|
|
89
|
+
section is about *when* and *how* to use it.
|
|
90
|
+
|
|
91
|
+
The lifecycle is: read the initial snapshot, register a subscriber for later snapshots, and
|
|
92
|
+
**always unsubscribe when the consumer goes away** (component unmount, effect re-run, view
|
|
93
|
+
teardown) so the subscription doesn't leak:
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
const result = await sdk.graphql!.query<GetAccountsQuery>({ query: GET_ACCOUNTS, variables });
|
|
97
|
+
render(result.data, result.errors); // initial snapshot
|
|
98
|
+
|
|
99
|
+
const unsub = result.subscribe(({ data, errors }) => render(data, errors)); // live updates
|
|
100
|
+
await result.refresh(); // force re-fetch → pushes to subscribers
|
|
101
|
+
// later, when the consumer tears down:
|
|
102
|
+
unsub();
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Managing the subscription in a framework.** Whatever reactive/lifecycle primitive your UI
|
|
106
|
+
layer uses — a React effect, a Vue/Svelte lifecycle hook, a web-component
|
|
107
|
+
`connected`/`disconnectedCallback`, a store teardown — the same three obligations hold:
|
|
108
|
+
|
|
109
|
+
- Kick off `query()` on mount/setup and store the resolved `result` so you can call
|
|
110
|
+
`refresh()` on it later (e.g. behind a "Refresh" button).
|
|
111
|
+
- Push each `subscribe` snapshot into your reactive state so the view re-renders.
|
|
112
|
+
- Run `unsub()` in the teardown path, and guard against a late-resolving `query()` writing state
|
|
113
|
+
after teardown (track a `cancelled` flag). The subscription does not fire on registration, so
|
|
114
|
+
set initial state from the awaited snapshot, not from the subscriber.
|
|
115
|
+
|
|
116
|
+
**Refresh after a mutation** — mutations have no `subscribe`/`refresh`. To make a list reflect a
|
|
117
|
+
write, hold the query `result` and call `result.refresh()` after `mutate()` resolves:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
await sdk.graphql!.mutate({ mutation: CREATE_ACCOUNT, variables: { input } });
|
|
121
|
+
await accountsResult.refresh(); // re-fetches, bypasses cache, pushes to subscribers
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 2b. Call-site cache control — the `cacheControl` option
|
|
125
|
+
|
|
126
|
+
`cacheControl` is a one-shot policy override on the query options bag. The type and the precise
|
|
127
|
+
per-value behavior (including how an `only-if-cached` miss surfaces as `DataNotFoundError`) are
|
|
128
|
+
the SDK contract — see
|
|
129
|
+
[sdk-api.md](sdk-api.md#cachecontrol--the-per-call-cache-policy). In short:
|
|
130
|
+
|
|
131
|
+
- `"no-cache"` — skip the cache read, always hit the network, still write back.
|
|
132
|
+
- `"only-if-cached"` — cache-only; a miss surfaces a `DataNotFoundError` on `result.errors` (no
|
|
133
|
+
network, no throw). Handle it by rendering an empty state — do **not** fall back to the network.
|
|
134
|
+
- `{ type: "max-age", maxAge: <seconds> }` — custom TTL instead of 300s.
|
|
135
|
+
|
|
136
|
+
It does **not** affect the cache key — the same query+variables share one slot regardless of the
|
|
137
|
+
policy each call passes. Which policy to reach for is the strategy table below.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 3. Choosing a strategy
|
|
142
|
+
|
|
143
|
+
| Goal | Reach for |
|
|
144
|
+
|------|-----------|
|
|
145
|
+
| Default reads, freshness within ~5 min is fine | Nothing — default 300s cache |
|
|
146
|
+
| Component stays mounted, should reflect cache updates / on-demand re-fetch | `subscribe` + `refresh` (2a) |
|
|
147
|
+
| Re-fetch a held query after a mutation | `result.refresh()` (2a) |
|
|
148
|
+
| One-off "this read must be fresh" (button, post-mutation one-shot) | `cacheControl: "no-cache"` (2b) |
|
|
149
|
+
| Offline-first — render only cached data, tolerate a miss as an empty state (no network fallback) | `cacheControl: "only-if-cached"` (2b) |
|
|
150
|
+
| Data changes faster than 5 min | `cacheControl: { type: "max-age", maxAge: N }` (2b) |
|
|
151
|
+
|
|
152
|
+
`no-cache` (2b) and `refresh()` (2a) both bypass the cache and write back; the difference is
|
|
153
|
+
**`refresh()` pushes to existing subscribers** and is a method on a live handle, while
|
|
154
|
+
`no-cache` is a fresh one-shot call with no subscribers.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Uncached surfaces (Mosaic, OpenAI)
|
|
159
|
+
|
|
160
|
+
- **No cache exists.** Every `query()` is a network request.
|
|
161
|
+
- **`cacheControl` is silently ignored** — `"no-cache"`, `"only-if-cached"`, and `max-age` have
|
|
162
|
+
no effect (notably, `only-if-cached` will **not** raise `DataNotFoundError` because there is no
|
|
163
|
+
cache layer to miss).
|
|
164
|
+
- **`subscribe` is real but only emits in response to `refresh()`** — there is no background
|
|
165
|
+
cache to push updates, so `refresh()` is the sole source of new snapshots, and each
|
|
166
|
+
`refresh()` costs one network request.
|
|
167
|
+
- PR #502 flags the uncached `subscribe`/`refresh` edge semantics (re-emit-to-all, error
|
|
168
|
+
fan-out, ordering vs concurrent refresh) as a **known soft spot / follow-up**. Document
|
|
169
|
+
conservatively; do not over-promise behavior there.
|
|
170
|
+
|
|
171
|
+
The namespace shape (`query`/`mutate`, options bag, `QueryResult`) is identical across surfaces,
|
|
172
|
+
so the same call site runs on WebApp and uncached surfaces alike.
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
# Building queries with the graphiti CLI
|
|
2
|
+
|
|
3
|
+
The base UI-bundle template ships **`@salesforce/graphiti`** as a devDependency, exposing a
|
|
4
|
+
`graphiti` CLI. Its `sf-gql-*` subcommands turn a small JSON spec into a **schema-correct
|
|
5
|
+
GraphQL document** — the query string, its typed variables, and a TypeScript shape — with
|
|
6
|
+
every platform guardrail (`@optional`, `value`/`displayValue` wrappers, `edges/node`,
|
|
7
|
+
pagination, the mutation `Record` envelope) already applied. This is the **preferred way to
|
|
8
|
+
author the GraphQL in [Read workflow](../SKILL.md#read-workflow) /
|
|
9
|
+
[Write workflow](../SKILL.md#write-workflow) step 2** — you get a query that's already
|
|
10
|
+
grounded against the org's live schema instead of hand-writing one and discovering field
|
|
11
|
+
errors at runtime.
|
|
12
|
+
|
|
13
|
+
> **The CLI is a query *compiler*, not a data fetcher.** Every `sf-gql-*` command returns a
|
|
14
|
+
> `{ query, variables, types, warnings }` envelope. It **never calls Salesforce for records
|
|
15
|
+
> and never returns rows** — execution still happens at runtime through
|
|
16
|
+
> `sdk.graphql!.query()` / `.mutate()` exactly as the Read/Write workflows describe. Think of
|
|
17
|
+
> it as "codegen for the query string itself": dev-time you compile the operation, persist it,
|
|
18
|
+
> generate types; runtime the SDK runs it.
|
|
19
|
+
|
|
20
|
+
> **Fallback only when the CLI genuinely *can't run*.** Fall back to the schema-grep path
|
|
21
|
+
> (`bash <skill-dir>/scripts/graphql-search.sh <Entity>`) and hand-author per
|
|
22
|
+
> [graphql-hand-authoring.md](graphql-hand-authoring.md) **only** when `@salesforce/graphiti` isn't
|
|
23
|
+
> installed or the org can't be primed (`SCHEMA_PRIME_FAILED`). The guardrails there are the same
|
|
24
|
+
> ones the CLI automates — you're just applying them by hand.
|
|
25
|
+
>
|
|
26
|
+
> **A primed CLI returning empty / "not found" / `Cannot query field` is NOT a fallback trigger.**
|
|
27
|
+
> That's a fact about the org — a wrong API name, or metadata that isn't deployed/refreshed — not a
|
|
28
|
+
> CLI failure. Re-`discover` (list before describe), `sf-gql-connect --forceRefresh` if you just
|
|
29
|
+
> deployed, or deploy the metadata; **do not switch to the script or hand-author around it**, and
|
|
30
|
+
> **never edit `schema.graphql`** to make the name resolve (it grants no org access — see
|
|
31
|
+
> [graphql-hand-authoring.md](graphql-hand-authoring.md)).
|
|
32
|
+
|
|
33
|
+
> **MCP, later.** These `sf-gql-*` commands mirror, one-for-one, the `sf_gql_*` tools of the
|
|
34
|
+
> graphiti MCP server (same args, same output, different transport). If graphiti is later
|
|
35
|
+
> approved as an MCP server, an agent calls the `sf_gql_*` tools directly and everything below
|
|
36
|
+
> about *shapes and behavior* still holds — only the invocation changes.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## How to invoke it
|
|
41
|
+
|
|
42
|
+
Run from the **UI bundle dir** (where `package.json` with the `@salesforce/graphiti` dep
|
|
43
|
+
lives). Each command takes one JSON argument (positional, or piped on stdin) and emits exactly
|
|
44
|
+
one JSON line on stdout:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx graphiti sf-gql-list '{"org":"myOrgAlias","object":"Account","fields":["Name","Industry"],"first":10}'
|
|
48
|
+
|
|
49
|
+
# stdin form (handy for large specs):
|
|
50
|
+
echo '{"org":"myOrgAlias","object":"Account","fields":["Name"]}' | npx graphiti sf-gql-list
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Always single-quote the JSON argument.** It contains `"` and may contain `$varName` tokens
|
|
54
|
+
(see [Variables](#variables--parameterising-a-query)); an unquoted `$foo` is expanded by the
|
|
55
|
+
shell to an empty string before graphiti ever sees it.
|
|
56
|
+
|
|
57
|
+
`org` is an **org alias from the local Salesforce CLI auth** (`~/.sf` / `~/.sfdx`) — the same
|
|
58
|
+
aliases `sf org list` shows. It is **required on every command**.
|
|
59
|
+
|
|
60
|
+
Exit code is `0` on success, `1` on any error (the error is also in the JSON envelope, so you
|
|
61
|
+
can parse stdout rather than branch on the code).
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## The three phases
|
|
66
|
+
|
|
67
|
+
### 1. Prepare — make sure the org's schema is available
|
|
68
|
+
|
|
69
|
+
The build commands **auto-prime**: the first `sf-gql-*` call for an org downloads and caches
|
|
70
|
+
its schema, so you usually don't need a separate step. You only call `sf-gql-connect`
|
|
71
|
+
explicitly to **refresh after a deploy** (new objects/fields/picklist values won't appear until
|
|
72
|
+
you do):
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npx graphiti sf-gql-connect '{"org":"myOrgAlias","forceRefresh":true}'
|
|
76
|
+
# → {"org":"myOrgAlias","instanceUrl":"https://…","refreshed":true,"cached":false,"durationMs":…}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
If priming fails, the command returns a `SCHEMA_PRIME_FAILED` error envelope (see
|
|
80
|
+
[Errors](#error-envelope)) — the org is unreachable, unauthed, or hitting a server-side
|
|
81
|
+
introspection issue. You can't build verified queries against an org you can't prime; surface
|
|
82
|
+
that to the user rather than guessing field names.
|
|
83
|
+
|
|
84
|
+
### 2. Discover — never guess an object or field
|
|
85
|
+
|
|
86
|
+
`sf-gql-discover` is how you ground intent against the org *before* building. Three modes:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# What objects exist (optional substring filter)?
|
|
90
|
+
npx graphiti sf-gql-discover '{"org":"myOrgAlias","mode":"list_objects","search":"Account"}'
|
|
91
|
+
# → {"mode":"list_objects","objects":[{"name":"Account"}, …]}
|
|
92
|
+
|
|
93
|
+
# What fields does an object have (+ type, filterable, sortable, picklist values)?
|
|
94
|
+
npx graphiti sf-gql-discover '{"org":"myOrgAlias","mode":"describe_object","object":"Hero__c"}'
|
|
95
|
+
# → {"mode":"describe_object","object":{"name":"Hero__c","fields":[
|
|
96
|
+
# {"name":"Class__c","label":"Class","type":"PICKLIST","filterable":true,"sortable":true,
|
|
97
|
+
# "picklistValues":["Warrior","Mage","Rogue","Cleric"], …}, …]}}
|
|
98
|
+
|
|
99
|
+
# Drill into one field
|
|
100
|
+
npx graphiti sf-gql-discover '{"org":"myOrgAlias","mode":"describe_field","object":"Hero__c","field":"Class__c"}'
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`mode` is **required**; `object` is required for `describe_object`/`describe_field`; `field`
|
|
104
|
+
is required for `describe_field`. Use this output to pick exact API names, valid picklist
|
|
105
|
+
values, and which fields are filterable/sortable — the same facts that otherwise cause silent
|
|
106
|
+
runtime failures.
|
|
107
|
+
|
|
108
|
+
### 3. Build — compile the operation
|
|
109
|
+
|
|
110
|
+
Pick the command for the task, pass the spec, read the `query` + `variables` + `types` out of
|
|
111
|
+
the envelope. The next section is the catalogue.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## The commands
|
|
116
|
+
|
|
117
|
+
| Command | Builds | Key spec fields |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| `sf-gql-list` | List query (`uiapi.query`) | `object`, `fields[]`, `first?`, `filter?`, `orderBy?`, `parentFields?`, `childRelationships?`, `scope?` |
|
|
120
|
+
| `sf-gql-detail` | Single-record-by-Id query | `object`, `fields[]`, `idVariable?` (default `id`) |
|
|
121
|
+
| `sf-gql-aggregate` | Aggregate query (`uiapi.aggregate`) | `object`, `groupBy?[]`, `aggregations?[]`, `filter?`, `first?` |
|
|
122
|
+
| `sf-gql-create` | Create mutation | `object`, `returnFields?` (default `["Id"]`), `inputVariable?` (default `input`) |
|
|
123
|
+
| `sf-gql-update` | Update mutation | `object`, `returnFields?`, `inputVariable?` |
|
|
124
|
+
| `sf-gql-delete` | Delete mutation | `object`, `inputVariable?` |
|
|
125
|
+
| `sf-gql-raw` | Arbitrary query from CLI-style `select`/`set`/`var` commands | `commands[]`, `operation?` (`query`\|`mutation`\|`aggregate`) |
|
|
126
|
+
| `sf-gql-discover` | Schema metadata (no GraphQL) | `mode`, `object?`, `field?`, `search?` |
|
|
127
|
+
| `sf-gql-connect` | Primes/refreshes the schema cache (no GraphQL) | `forceRefresh?` |
|
|
128
|
+
|
|
129
|
+
`operationName` (most commands) overrides the generated operation name — **set it to something
|
|
130
|
+
meaningful** (e.g. `"GetActiveHeroes"`) so the persisted `.graphql` file and the types codegen
|
|
131
|
+
generates off it read well. Defaults are derived (`<Object>List`, `<Object>Detail`, …).
|
|
132
|
+
|
|
133
|
+
### Output envelope
|
|
134
|
+
|
|
135
|
+
Every build command emits the same four-key envelope:
|
|
136
|
+
|
|
137
|
+
```jsonc
|
|
138
|
+
{
|
|
139
|
+
"query": "query …{ … }", // the GraphQL document — paste verbatim
|
|
140
|
+
"variables": [{ "name": "after", "type": "String", "required": false }],
|
|
141
|
+
"types": "export interface …", // TS shape of variables + result (a preview)
|
|
142
|
+
"warnings": [] // schema/semantic warnings — READ THESE
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- **`query`** — paste it verbatim into your `.graphql` file or inline `gql`. The guardrails are
|
|
147
|
+
baked in; don't "tidy" them out (that's the load-bearing part — see
|
|
148
|
+
[Primed vs degraded](#primed-vs-degraded--why-the-guardrails-sometimes-vanish)).
|
|
149
|
+
- **`variables`** — the GraphQL variables the operation declares, with types and nullability.
|
|
150
|
+
These map straight to the `variables` object you pass to `sdk.graphql!.query({ query,
|
|
151
|
+
variables })`.
|
|
152
|
+
- **`types`** — a TypeScript preview of the variables + result shape, with anonymized interface
|
|
153
|
+
names (`S37eaResult`). Useful to *see* the shape, but the canonical typed path is still the
|
|
154
|
+
bundle's `npm run graphql:codegen` over your saved `.graphql` (it produces **named** types
|
|
155
|
+
you import) — see [Wiring into runtime](#wiring-the-output-into-runtime).
|
|
156
|
+
- **`warnings`** — non-fatal, but **always read them**. An empty `[]` means the operation
|
|
157
|
+
validated cleanly against the live schema. A non-empty entry usually means a field/object
|
|
158
|
+
isn't in the primed schema or a selection is semantically off (examples below).
|
|
159
|
+
|
|
160
|
+
### Ground-truth examples
|
|
161
|
+
|
|
162
|
+
These are real outputs captured from the CLI (query strings pretty-printed for readability;
|
|
163
|
+
the CLI emits them as a single JSON line).
|
|
164
|
+
|
|
165
|
+
**`sf-gql-list`** — `{"org":"…","object":"Hero__c","fields":["Name","Level__c","Class__c"],"first":5,"orderBy":{"Level__c":{"order":"DESC"}}}`
|
|
166
|
+
|
|
167
|
+
```graphql
|
|
168
|
+
query Hero__cList($after: String) {
|
|
169
|
+
uiapi {
|
|
170
|
+
query {
|
|
171
|
+
Hero__c(first: 5, after: $after, orderBy: { Level__c: { order: DESC } }) {
|
|
172
|
+
edges {
|
|
173
|
+
node {
|
|
174
|
+
Name @optional { value displayValue }
|
|
175
|
+
Level__c @optional { value displayValue }
|
|
176
|
+
Class__c @optional { value displayValue }
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
pageInfo { hasNextPage endCursor }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
`variables: [{ "name": "after", "type": "String", "required": false }]` — note the CLI adds
|
|
186
|
+
forward-pagination (`$after` + `pageInfo`) for you. `orderBy` is a **singleton object**
|
|
187
|
+
(`{Field:{order:DESC}}`), not an array or `{field,direction}`.
|
|
188
|
+
|
|
189
|
+
**`sf-gql-detail`** — `{"org":"…","object":"Hero__c","fields":["Name","Level__c"]}`
|
|
190
|
+
|
|
191
|
+
```graphql
|
|
192
|
+
query Hero__cDetail($id: ID!) {
|
|
193
|
+
uiapi { query { Hero__c(where: { Id: { eq: $id } }, first: 1) {
|
|
194
|
+
edges { node {
|
|
195
|
+
Name @optional { value displayValue }
|
|
196
|
+
Level__c @optional { value displayValue }
|
|
197
|
+
} }
|
|
198
|
+
} } }
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
Injects `$id: ID!` and the `where: { Id: { eq: $id } }, first: 1` binding. Pass
|
|
202
|
+
`idVariable` to rename `$id`.
|
|
203
|
+
|
|
204
|
+
**`sf-gql-aggregate`** — `{"org":"…","object":"Hero__c","groupBy":["Class__c"],"aggregations":[{"function":"count","field":"Id","alias":"total"},{"function":"avg","field":"Level__c","alias":"avgLevel"}]}`
|
|
205
|
+
|
|
206
|
+
```graphql
|
|
207
|
+
query Hero__cAggregate($after: String) {
|
|
208
|
+
uiapi { aggregate { Hero__c(groupBy: { Class__c: { group: true } }, after: $after) {
|
|
209
|
+
edges { node { aggregate {
|
|
210
|
+
Class__c { value }
|
|
211
|
+
total: Id { count { value } }
|
|
212
|
+
avgLevel: Level__c { avg { value } }
|
|
213
|
+
} } }
|
|
214
|
+
pageInfo { hasNextPage endCursor }
|
|
215
|
+
} } }
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
`groupBy` is `["Field"]` (or `[{field,function}]` for date bucketing); aggregations carry a
|
|
219
|
+
`function` (`count`/`countDistinct`/`sum`/`avg`/`min`/`max`), an optional `field` (defaults to
|
|
220
|
+
`Id` for counts), and an `alias`. *Observed quirk:* this exact spec returns
|
|
221
|
+
`warnings: ["Validation: Field \"avg\" must not have a selection since type \"Double\" has no
|
|
222
|
+
subfields."]` — `avg` over a Double should be selected bare, not `avg { value }`. Treat such a
|
|
223
|
+
warning as a prompt to adjust the selection, not as a failed build.
|
|
224
|
+
|
|
225
|
+
**`sf-gql-create`** — `{"org":"…","object":"Hero__c","returnFields":["Id","Name"]}`
|
|
226
|
+
|
|
227
|
+
```graphql
|
|
228
|
+
mutation CreateHero__c($input: Hero__cCreateInput!) {
|
|
229
|
+
uiapi { Hero__cCreate(input: $input) { Record {
|
|
230
|
+
Id
|
|
231
|
+
Name @optional { value displayValue }
|
|
232
|
+
} } }
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
The `types` field describes the input you must supply at runtime — note the **entity-keyed
|
|
236
|
+
wrapper**, the single most common mutation-variables mistake:
|
|
237
|
+
```typescript
|
|
238
|
+
interface Hero__cCreateInput { Hero__c: Hero__cCreateRepresentation; }
|
|
239
|
+
interface Hero__cCreateRepresentation { Name?: string; Class__c?: string; Level__c?: number; … }
|
|
240
|
+
// → variables: { input: { Hero__c: { Name: "Aria", Level__c: 5 } } }
|
|
241
|
+
```
|
|
242
|
+
`sf-gql-update` is identical but the input type adds a sibling `Id: string`
|
|
243
|
+
(`{ input: { Hero__c: {…}, Id: "a0X…" } }`). `sf-gql-delete` uses the generic
|
|
244
|
+
`RecordDeleteInput { Id: string }` (`{ input: { Id: "a0X…" } }`) and selects only `Id` back —
|
|
245
|
+
there is no per-entity delete type. **Mutation inputs are raw values — never `{value}`-wrapped**
|
|
246
|
+
(that wrapper is a *read*-shape thing; mirroring it into a write is the classic failure).
|
|
247
|
+
|
|
248
|
+
> **One mutation guardrail the builder does NOT add: `allOrNone`.** The emitted document wraps the
|
|
249
|
+
> operation in a bare `uiapi { … }`, *not* `uiapi(input: { allOrNone: … })`. Before you ship, add
|
|
250
|
+
> the `allOrNone` wrapper yourself and set it explicitly — `uiapi(input: { allOrNone: true }) {
|
|
251
|
+
> Hero__cCreate(input: $input) { Record { … } } }` — per guardrail 4 in the spine and the
|
|
252
|
+
> [hand-authored templates](graphql-hand-authoring.md#mutations). graphiti gives you the `Record`
|
|
253
|
+
> output envelope and the entity-keyed input shape; the transaction policy is still on you.
|
|
254
|
+
|
|
255
|
+
> **You do not pass record values to the builder.** A create/update spec takes **only**
|
|
256
|
+
> `object`, `returnFields?`, `inputVariable?`, and `operationName?` — there is **no `fields`
|
|
257
|
+
> key** carrying the values to write. The builder emits the mutation *shape* (with the input
|
|
258
|
+
> declared as a `$variable`); the actual values become the **runtime variable** you pass to
|
|
259
|
+
> `sdk.graphql!.mutate({ mutation, variables })`. `returnFields` only controls what you read
|
|
260
|
+
> *back* after the write.
|
|
261
|
+
|
|
262
|
+
**`sf-gql-raw`** — for shapes the declarative commands don't cover, drive `select`/`set`/`var`:
|
|
263
|
+
```bash
|
|
264
|
+
npx graphiti sf-gql-raw '{"org":"…","commands":["select uiapi/query/Hero__c/edges/node/Name/value","set uiapi/query/Hero__c first=5"]}'
|
|
265
|
+
```
|
|
266
|
+
Same `@optional`/wrapper guardrails are still applied automatically.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Primed vs degraded — why the guardrails sometimes vanish
|
|
271
|
+
|
|
272
|
+
This is the one behavior that surprises people, and it changes how much you can trust a single
|
|
273
|
+
build. The guardrail automation (`@optional`, `value`/`displayValue`, `edges/node`, the typed
|
|
274
|
+
result shape) is **conditional on the object being in the primed schema**. When it is, you get
|
|
275
|
+
the clean output above. When it **isn't** — wrong API name, not deployed, or schema not yet
|
|
276
|
+
refreshed after a deploy — the CLI still emits *a* query, but a **degraded** one, and flags it:
|
|
277
|
+
|
|
278
|
+
`{"org":"…","object":"Account","fields":["Name","Industry"],"first":3}` against an org whose
|
|
279
|
+
cache doesn't contain `Account`:
|
|
280
|
+
|
|
281
|
+
```graphql
|
|
282
|
+
query AccountList($after: String) {
|
|
283
|
+
uiapi { query { Account(first: 3, after: $after) {
|
|
284
|
+
edges { node { Name Industry } } # ← bare fields: no @optional, no value/displayValue
|
|
285
|
+
pageInfo { hasNextPage endCursor }
|
|
286
|
+
} } }
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
…with `types` collapsing the result to `Account: unknown` and:
|
|
290
|
+
```json
|
|
291
|
+
"warnings": ["Validation: Cannot query field \"Account\" on type \"RecordQuery\"."]
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**So: a non-empty `warnings` array — especially `Cannot query field …` — means the build is
|
|
295
|
+
NOT trustworthy. Do not ship a degraded query.** Re-`discover` the correct API name, or
|
|
296
|
+
`sf-gql-connect` with `forceRefresh: true` if you just deployed, then rebuild until
|
|
297
|
+
`warnings` is `[]`. A clean build is your signal that the guardrails actually fired.
|
|
298
|
+
**Never edit `schema.graphql` to make a degraded build pass** — the mirror is a generated,
|
|
299
|
+
read-only reflection of the org; adding the missing field/type there silences the warning but
|
|
300
|
+
grants no org access, so the operation still fails at runtime. Fix the name or deploy + refresh.
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## Variables — parameterising a query
|
|
305
|
+
|
|
306
|
+
A `$varName` string **leaf inside `filter`** is promoted to a typed, nullable GraphQL variable,
|
|
307
|
+
with the type inferred from the schema:
|
|
308
|
+
|
|
309
|
+
`{"org":"…","object":"Hero__c","fields":["Name"],"filter":{"Class__c":{"eq":"$heroClass"}},"first":5}`
|
|
310
|
+
```graphql
|
|
311
|
+
query Hero__cList($after: String, $heroClass: Picklist) {
|
|
312
|
+
uiapi { query { Hero__c(first: 5, after: $after, where: { Class__c: { eq: $heroClass } }) { … } } }
|
|
313
|
+
}
|
|
314
|
+
# variables: [ {after, String, false}, {heroClass, Picklist, false} ]
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
What does **not** work on the declarative tools: passing a whole-arg `$var` for `filter` or
|
|
318
|
+
`orderBy` (e.g. `"filter":"$where"`). Those args are typed as objects and reject a bare string
|
|
319
|
+
with an `INVALID_ARGS` error — promote at the **leaf**, not the whole argument. `first` is a
|
|
320
|
+
number and likewise can't take a `$var` string.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Wiring the output into runtime
|
|
325
|
+
|
|
326
|
+
The CLI produces the operation; the rest is the existing
|
|
327
|
+
[Read workflow](../SKILL.md#read-workflow) / [Write workflow](../SKILL.md#write-workflow),
|
|
328
|
+
unchanged:
|
|
329
|
+
|
|
330
|
+
1. **Persist** the `query` string — inline `gql` for simple ops, or a `.graphql` file
|
|
331
|
+
(one operation per file, imported with `?raw`) for complex ones. Use a meaningful
|
|
332
|
+
`operationName` so the file/type names read well.
|
|
333
|
+
2. **Codegen** — `npm run graphql:codegen` (from the UI bundle dir) generates **named** types
|
|
334
|
+
into `src/api/graphql-operations-types.ts`. (The CLI's `types` field is a preview of the
|
|
335
|
+
same shape; codegen is the canonical import source.)
|
|
336
|
+
3. **Call** — `sdk.graphql!.query({ query, variables })` for reads,
|
|
337
|
+
`sdk.graphql!.mutate({ mutation, variables })` for writes, using the codegen'd types and the
|
|
338
|
+
`variables` the CLI listed. Surface decision (`!` vs guard), error handling, and caching are
|
|
339
|
+
exactly as the SKILL spine and [sdk-api.md](sdk-api.md) describe.
|
|
340
|
+
|
|
341
|
+
The CLI never executes anything — it has no part in step 3. It just makes step 1 produce a
|
|
342
|
+
query you can trust.
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Error envelope
|
|
347
|
+
|
|
348
|
+
Failures come back as a single JSON line and set exit code `1`:
|
|
349
|
+
|
|
350
|
+
```json
|
|
351
|
+
{"error":{"code":"INVALID_ARGS","message":"Input failed schema validation.","details":[{"path":["mode"],"message":"Required"}]}}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
| `code` | Means | Do |
|
|
355
|
+
|---|---|---|
|
|
356
|
+
| `INVALID_ARGS` | The spec failed Zod validation (missing/mis-typed field). `details[]` gives the JSON path. | Fix the spec per `details` — e.g. add the required `mode`, or stop passing a `$var` where an object is expected. |
|
|
357
|
+
| `AUTH_FAILED` | Org alias not authed / not found in local CLI auth. | Check `sf org list`; have the user authenticate the alias. |
|
|
358
|
+
| `SCHEMA_PRIME_FAILED` | Schema couldn't be downloaded/introspected (unreachable, or a server-side introspection error). | Retry; if it persists the org has an introspection problem — fall back to `graphql-search.sh` + hand-authoring, and tell the user. |
|
|
359
|
+
| `INTERNAL` | Anything else; verbatim message preserved. | Read the message. Set `GRAPHITI_DEBUG=1` for a stack trace. |
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Common pitfalls (failure-first)
|
|
364
|
+
|
|
365
|
+
| Symptom | Cause | Fix |
|
|
366
|
+
|---|---|---|
|
|
367
|
+
| Built query has bare fields, no `@optional`/wrappers; `types` shows `unknown` | Object not in the primed schema (typo, not deployed, stale cache) | Check `warnings` for `Cannot query field`; re-`discover` the name or `connect --forceRefresh`, rebuild until `warnings: []` |
|
|
368
|
+
| `$heroClass` came through as an empty string | Shell expanded `$…` in an unquoted arg | **Single-quote** the whole JSON argument |
|
|
369
|
+
| `INVALID_ARGS` on `"filter":"$where"` | Whole-arg `$var` not allowed on declarative tools | Promote at a leaf: `"filter":{"Field":{"eq":"$where"}}` |
|
|
370
|
+
| Mutation rejected at runtime / wrong shape | `{value}`-wrapped a mutation **input**, or dropped the entity-key wrapper | Inputs are raw values under the entity key: `{ input: { <Object>: { Field: v } } }`; read the `types` field |
|
|
371
|
+
| `Field "avg" must not have a selection…` warning | Selected `avg { value }` on a Double | Select the aggregate bare per the warning |
|
|
372
|
+
| New field/object missing from discover or build | Schema cached before the deploy | `sf-gql-connect` with `forceRefresh: true`, then rebuild |
|
|
373
|
+
| Used `graphiti new`/`cd`/`select`/`run` and it executed against the org | That's the **legacy** interactive/session flow, not the `sf-gql-*` mirror | Use the stateless `sf-gql-*` commands documented here — they compile, they don't execute |
|