@salesforce/afv-skills 1.23.0 → 1.25.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.
Files changed (61) hide show
  1. package/package.json +1 -1
  2. package/skills/commerce-b2b-open-code-components-replace/SKILL.md +244 -0
  3. package/skills/commerce-b2b-open-code-components-replace/assets/ootb-to-open-code-mapping.json +66 -0
  4. package/skills/{developing-datacloud-code-extension → data360-code-extension-generate}/SKILL.md +7 -7
  5. package/skills/{developing-datacloud-code-extension → data360-code-extension-generate}/references/README.md +7 -7
  6. package/skills/{developing-datacloud-code-extension → data360-code-extension-generate}/references/quick-reference.md +2 -2
  7. package/skills/{getting-datacloud-schema → data360-schema-get}/SKILL.md +26 -26
  8. package/skills/{getting-datacloud-schema → data360-schema-get}/references/README.md +9 -9
  9. package/skills/dx-org-manage/SKILL.md +192 -0
  10. package/skills/dx-org-manage/examples/README.md +45 -0
  11. package/skills/dx-org-manage/examples/scratch-orgs/error_no_devhub.json +9 -0
  12. package/skills/dx-org-manage/examples/scratch-orgs/error_timeout.json +13 -0
  13. package/skills/dx-org-manage/examples/scratch-orgs/success_definition_file.json +28 -0
  14. package/skills/dx-org-manage/examples/scratch-orgs/success_edition.json +26 -0
  15. package/skills/dx-org-manage/examples/scratch-orgs/success_snapshot.json +27 -0
  16. package/skills/dx-org-manage/examples/snapshots/error_output.json +9 -0
  17. package/skills/dx-org-manage/examples/snapshots/success_output.json +15 -0
  18. package/skills/dx-org-manage/references/cli_flags.md +67 -0
  19. package/skills/dx-org-manage/references/creating-scratch-org.md +164 -0
  20. package/skills/dx-org-manage/references/creating-snapshot.md +103 -0
  21. package/skills/dx-org-manage/references/definition_file_options.md +224 -0
  22. package/skills/dx-org-manage/references/edition_types.md +78 -0
  23. package/skills/dx-org-manage/references/opening-org.md +160 -0
  24. package/skills/dx-org-manage/references/snapshot_usage.md +74 -0
  25. package/skills/dx-org-permission-set-assign/SKILL.md +98 -0
  26. package/skills/dx-org-permission-set-assign/examples/error_output.json +19 -0
  27. package/skills/dx-org-permission-set-assign/examples/success_output.json +16 -0
  28. package/skills/dx-org-permission-set-assign/references/cli_flags.md +68 -0
  29. package/skills/experience-cms-brand-apply/SKILL.md +1 -1
  30. package/skills/experience-ui-bundle-app-coordinate/SKILL.md +31 -19
  31. package/skills/experience-ui-bundle-file-upload-generate/SKILL.md +1 -1
  32. package/skills/experience-ui-bundle-frontend-generate/implementation/header-footer.md +1 -1
  33. package/skills/experience-ui-bundle-salesforce-data-access/SKILL.md +336 -581
  34. package/skills/experience-ui-bundle-salesforce-data-access/references/caching.md +172 -0
  35. package/skills/experience-ui-bundle-salesforce-data-access/references/graphiti-cli.md +373 -0
  36. package/skills/experience-ui-bundle-salesforce-data-access/references/graphql-hand-authoring.md +376 -0
  37. package/skills/experience-ui-bundle-salesforce-data-access/references/migration.md +119 -0
  38. package/skills/experience-ui-bundle-salesforce-data-access/references/rest-and-integration.md +152 -0
  39. package/skills/experience-ui-bundle-salesforce-data-access/references/sdk-api.md +217 -0
  40. package/skills/experience-ui-bundle-salesforce-data-access/scripts/graphql-search.sh +36 -9
  41. package/skills/platform-agentsetup-categories-fetch/SKILL.md +109 -0
  42. package/skills/platform-agentsetup-categories-fetch/references/api-response-schema.md +121 -0
  43. package/skills/platform-custom-object-generate/SKILL.md +62 -7
  44. package/skills/platform-custom-object-generate/references/description-enrichment.md +125 -0
  45. package/skills/platform-metadata-retrieve/SKILL.md +121 -0
  46. package/skills/platform-metadata-retrieve/examples/error_output.json +10 -0
  47. package/skills/platform-metadata-retrieve/examples/success_output.json +27 -0
  48. package/skills/platform-metadata-retrieve/references/cli_flags.md +138 -0
  49. package/skills/platform-metadata-retrieve/references/retrieval_modes.md +181 -0
  50. package/skills/platform-sharing-rules-generate/SKILL.md +165 -0
  51. package/skills/platform-sharing-rules-generate/references/rule-types.md +199 -0
  52. package/skills/platform-tracing-agentforce-configure/SKILL.md +118 -0
  53. package/skills/platform-tracing-agentforce-configure/assets/AgentforcePlatformTracing-template.xml +4 -0
  54. package/skills/platform-tracing-configure/SKILL.md +118 -0
  55. package/skills/platform-tracing-configure/assets/EventSettings-template.xml +4 -0
  56. package/skills/platform-trust-archive-manage/SKILL.md +25 -11
  57. package/skills/platform-trust-archive-manage/examples/monitor-failed-jobs.md +2 -2
  58. package/skills/platform-trust-archive-manage/references/archive-activity-entity.md +1 -1
  59. package/skills/platform-trust-archive-manage/references/connect-api-operations.md +51 -12
  60. /package/skills/{getting-datacloud-schema → data360-schema-get}/scripts/get_dlo_schema.py +0 -0
  61. /package/skills/{getting-datacloud-schema → data360-schema-get}/scripts/get_dmo_schema.py +0 -0
@@ -0,0 +1,376 @@
1
+ # Hand-authoring Salesforce GraphQL queries & mutations (fallback)
2
+
3
+ **Use this only when the graphiti CLI is genuinely unreachable** — `@salesforce/graphiti` isn't
4
+ installed, or the org can't be primed. When the CLI *is* available it authors the query for you
5
+ with these same guardrails already applied; see [graphiti-cli.md](graphiti-cli.md). This doc is
6
+ the hand-authoring path: the schema-grep lookup plus the document templates and platform rules
7
+ you'd otherwise lean on the CLI to apply.
8
+
9
+ These rules are independent of the SDK reshape — only the *call mechanics* changed (see
10
+ [sdk-api.md](sdk-api.md)). Verify every entity and field via
11
+ `bash <skill-dir>/scripts/graphql-search.sh <Entity>` before writing a query
12
+ (`<skill-dir>` = wherever this skill is installed).
13
+
14
+ ## Schema lookup (do this first)
15
+
16
+ Map intent to PascalCase ("accounts" → `Account`), then run the search script from the
17
+ SFDX project root (where `schema.graphql` lives). The script reads `./schema.graphql` and
18
+ does **not** walk up the tree — if the schema is elsewhere, pass `--schema <path>` (or set
19
+ `GRAPHQL_SCHEMA`). It prints the resolved schema path on stderr; confirm it's the right one.
20
+
21
+ ```bash
22
+ bash <skill-dir>/scripts/graphql-search.sh Account
23
+ bash <skill-dir>/scripts/graphql-search.sh Account Contact Opportunity # multiple
24
+ bash <skill-dir>/scripts/graphql-search.sh --schema path/to/schema.graphql Account # schema not at ./
25
+ ```
26
+
27
+ Output sections per entity: (1) Type definition, (2) Filter options, (3) Sort options,
28
+ (4) Create wrapper `<Entity>CreateInput`, (5) Create fields `<Entity>CreateRepresentation`,
29
+ (6) Update wrapper `<Entity>UpdateInput`, (7) Update fields `<Entity>UpdateRepresentation`.
30
+
31
+ If an entity isn't found: try `__c`/`__e`, try a `_Record` suffix (v60+); if it's still
32
+ unresolved it may not be deployed — **ask the user**. Introspect nested references iteratively
33
+ as you discover them; if the lookups aren't converging on what you need, ask the user rather
34
+ than keep guessing. Never generate a query with an unconfirmed entity or field. **Never open *or edit*
35
+ `schema.graphql`** (265K+ lines) — no cat, less, head, tail, editors, or programmatic
36
+ parsers. It is a generated, read-only mirror of the org; editing it (e.g. to add a field that
37
+ won't resolve) silences the validator but grants no org access, so the operation still fails at
38
+ runtime. To change what it contains, deploy metadata then regenerate (`npm run graphql:schema`).
39
+
40
+ ## Read query template
41
+
42
+ ```graphql
43
+ query QueryName($after: String) {
44
+ uiapi {
45
+ query {
46
+ EntityName(
47
+ first: 10
48
+ after: $after
49
+ where: { ... }
50
+ orderBy: { ... }
51
+ ) {
52
+ edges {
53
+ node {
54
+ Id
55
+ FieldName @optional { value }
56
+ # Parent relationship (non-polymorphic) — @optional on the relationship AND its fields
57
+ Owner @optional { Name @optional { value } }
58
+ # Parent relationship (polymorphic — use fragments)
59
+ What @optional {
60
+ ...WhatAccount
61
+ ...WhatOpportunity
62
+ }
63
+ # Child relationship — max 1 level, no grandchildren.
64
+ # `first:` is a FIELD ARGUMENT (parens on the field); `@optional` is a
65
+ # bare directive that takes no arguments. Keep them separate.
66
+ Contacts(first: 10) @optional {
67
+ edges { node { Name @optional { value } } }
68
+ }
69
+ }
70
+ }
71
+ pageInfo { hasNextPage endCursor }
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ fragment WhatAccount on Account { Id Name @optional { value } }
78
+ fragment WhatOpportunity on Opportunity { Id Name @optional { value } }
79
+ ```
80
+
81
+ Consuming code must defend against omitted (FLS-stripped) fields:
82
+
83
+ ```typescript
84
+ const name = node.Name?.value ?? "";
85
+ const relatedName = node.Owner?.Name?.value ?? "N/A";
86
+ ```
87
+
88
+ This is exactly how the shipped `accounts.ts` `toAccount()` mapper works —
89
+ `node.Name?.value ?? "Unknown"`, `node.Industry?.value ?? null`, etc.
90
+
91
+ ## `@optional` and FLS
92
+
93
+ Salesforce field-level security makes a query fail **entirely** if the user lacks access to
94
+ even one selected field. The `@optional` directive (v65+) tells the server to omit inaccessible
95
+ fields instead of failing the whole request. `@optional` is a per-field directive
96
+ (`directive @optional on FIELD`), so apply it at **every level of nesting**, not just the
97
+ outermost: decorate each scalar field, each parent relationship, **and the nested fields
98
+ inside that relationship** — `Owner @optional { Name @optional { value } }`, not
99
+ `Owner @optional { Name { value } }`. The template above does this throughout.
100
+
101
+ > Shipped code varies: `userProfileApi.ts` uses `FirstName @optional { value }`, while the
102
+ > `accounts.ts` demo selects bare `Name { value }` and leans entirely on defensive
103
+ > `?.value ?? fallback` downstream. Bare selection is only safe when every selected field is
104
+ > guaranteed-accessible; **decorate with `@optional` by default** so a single FLS-restricted
105
+ > field can't fail the whole query. Always pair it with `?.`/`??` in consuming code regardless.
106
+
107
+ ## Filtering
108
+
109
+ ```graphql
110
+ # Implicit AND
111
+ Account(where: { Industry: { eq: "Technology" }, AnnualRevenue: { gt: 1000000 } })
112
+ # OR
113
+ Account(where: { OR: [{ Industry: { eq: "Technology" } }, { Industry: { eq: "Finance" } }] })
114
+ # NOT
115
+ Account(where: { NOT: { Industry: { eq: "Technology" } } })
116
+ # Date literal / relative date
117
+ Opportunity(where: { CloseDate: { eq: { value: "2024-12-31" } } })
118
+ Opportunity(where: { CloseDate: { gte: { literal: TODAY } } })
119
+ # Relationship filter (nested object, NOT dot notation)
120
+ Contact(where: { Account: { Name: { like: "Acme%" } } })
121
+ # Polymorphic relationship filter
122
+ Account(where: { Owner: { User: { Username: { like: "admin%" } } } })
123
+ ```
124
+
125
+ String `eq` is case-insensitive. Both 15- and 18-char record IDs are accepted. **Compound
126
+ fields**: filter/order on constituents (`BillingCity`, `BillingCountry`), never the compound
127
+ wrapper (`BillingAddress`) — the wrapper is selection-only.
128
+
129
+ ## Ordering
130
+
131
+ ```graphql
132
+ Account(first: 10, orderBy: { Name: { order: ASC }, CreatedDate: { order: DESC } })
133
+ ```
134
+
135
+ Add `Id` as a tie-breaker for deterministic order. Unsupported for ordering: multi-select
136
+ picklist, rich text, long text area, encrypted fields.
137
+
138
+ ## Pagination
139
+
140
+ - **Always include `first:`** — the server silently defaults to 10 if omitted.
141
+ - Include `pageInfo { hasNextPage endCursor }` for anything paginatable.
142
+ - Forward-only (`first` / `after`); `last` / `before` are unsupported.
143
+ - `upperBound` (v59+) for large sets; when set, `first` must be 200–2000.
144
+
145
+ ```graphql
146
+ Account(first: 2000, after: $cursor, upperBound: 10000) {
147
+ edges { node { Id Name @optional { value } } }
148
+ pageInfo { hasNextPage endCursor }
149
+ }
150
+ ```
151
+
152
+ The shipped `accounts.ts` query parameterizes `$first` / `$after` and selects
153
+ `pageInfo { hasNextPage endCursor }`; `Accounts.tsx` pages via "Load more" using
154
+ `pageInfo.endCursor`.
155
+
156
+ ## SOQL-derived limits
157
+
158
+ Max 10 subqueries/request, 5 levels child→parent, 1 level parent→child (no grandchildren),
159
+ 2,000 records/subquery. Split into multiple requests if exceeded.
160
+
161
+ ## Field value wrappers
162
+
163
+ Schema fields use typed wrappers; access via `.value`. Use `displayValue` (a `String`,
164
+ server-rendered) for UI display instead of formatting client-side.
165
+
166
+ | Wrapper | Underlying | Wrapper | Underlying |
167
+ |---|---|---|---|
168
+ | `StringValue` | String | `BooleanValue` | Boolean |
169
+ | `IntValue` | Int | `DoubleValue` | Double |
170
+ | `CurrencyValue` | Currency | `PercentValue` | Percent |
171
+ | `DateTimeValue` | DateTime | `DateValue` | Date |
172
+ | `PicklistValue` | Picklist | `LongValue` | Long |
173
+ | `IDValue` | ID | `TextAreaValue` | TextArea |
174
+ | `EmailValue` | Email | `PhoneNumberValue` | PhoneNumber |
175
+ | `UrlValue` | Url | | |
176
+
177
+ ## Semi-join / anti-join
178
+
179
+ Filter a parent by conditions on children via `inq` (semi-join) / `ninq` (anti-join) on the
180
+ parent's `Id`. If the only condition is child existence, use `Id: { ne: null }`.
181
+
182
+ ```graphql
183
+ Account(where: { Id: { inq: { Contact: { LastName: { like: "Smith%" } } ApiName: "AccountId" } } }, first: 10) {
184
+ edges { node { Id Name @optional { value } } }
185
+ }
186
+ ```
187
+
188
+ Restrictions: no `OR` in subquery, no `orderBy` in subquery, no nested joins.
189
+
190
+ ## Current user
191
+
192
+ ```graphql
193
+ query CurrentUser { uiapi { currentUser { Id Name { value } } } }
194
+ ```
195
+
196
+ Do **not** use Chatter (`/chatter/users/me`).
197
+
198
+ ## Mutations
199
+
200
+ Mutations GA in v66+. Call via `sdk.graphql!.mutate({ mutation, variables })` (the document
201
+ goes under the `mutation` key — see [sdk-api.md](sdk-api.md)). Wrap under
202
+ `uiapi(input: { allOrNone: true | false })` and set `allOrNone` explicitly.
203
+
204
+ ```graphql
205
+ # Create
206
+ mutation CreateAccount($input: AccountCreateInput!) {
207
+ uiapi(input: { allOrNone: true }) {
208
+ AccountCreate(input: $input) { Record { Id Name { value } } }
209
+ }
210
+ }
211
+
212
+ # Update — must include Id
213
+ mutation UpdateAccount($input: AccountUpdateInput!) {
214
+ uiapi(input: { allOrNone: true }) {
215
+ AccountUpdate(input: $input) { Record { Id Name { value } } }
216
+ }
217
+ }
218
+
219
+ # Delete — generic RecordDeleteInput (NO per-entity delete type); Id is flat, not
220
+ # nested under an entity key. Payload is RecordDeletePayload with `Id` ONLY — there
221
+ # is no `Record` field to select back (selecting `Record` is a schema error).
222
+ # Declare the variable `$input: RecordDeleteInput!` (its `Id` is `IdOrRef!`, NOT `ID!`,
223
+ # so `$id: ID!` is rejected) — same `(input: $input)` call shape as Create/Update above.
224
+ mutation DeleteAccount($input: RecordDeleteInput!) {
225
+ uiapi(input: { allOrNone: true }) {
226
+ AccountDelete(input: $input) { Id }
227
+ }
228
+ }
229
+ # runtime: variables: { input: { Id: "001…" } } // flat Id — matches the spine's delete shape
230
+ ```
231
+
232
+ Real consumer call (`userProfileApi.ts`):
233
+
234
+ ```typescript
235
+ const result = await sdk.graphql!.mutate<UpdateResult>({
236
+ mutation: UPDATE_USER_PROFILE,
237
+ variables: { input: { Id: userId, User: { ...values } } },
238
+ });
239
+ if (result.errors?.length) throw new Error("An unexpected error occurred");
240
+ return result.data?.uiapi?.UserUpdate?.Record;
241
+ ```
242
+
243
+ > Evidence note: the snippet above is reproduced from the shipped `userProfileApi.ts` to show the
244
+ > *call mechanics* (the `mutate()` options bag and `result` handling) only — it predates the
245
+ > `allOrNone` guardrail and omits the `uiapi(input: { allOrNone })` wrapper. Always author new
246
+ > mutations with the wrapper set explicitly, as in the templates above; the guardrail wins over
247
+ > this older shipped example.
248
+
249
+ **Input constraints**
250
+ - Create: required fields (unless `defaultedOnCreate`), only `createable` fields, no child
251
+ relationships; reference fields set by `ApiName` (e.g. `AccountId`).
252
+ - Update: must include `Id`, only `updateable` fields, no child relationships.
253
+ - Delete: `Id` only.
254
+ - `IdOrRef` (Update/Delete `Id`, and Create reference fields) accepts a literal record ID or a
255
+ chaining reference `"@{Alias}"`.
256
+ - Raw values only — no commas, currency symbols, or locale formatting (`80000`, not `"$80,000"`).
257
+
258
+ **Output constraints**
259
+ - Create/Update: output is always named `Record`; exclude child relationships and navigated
260
+ reference fields (only the `ApiName` member is allowed).
261
+ - Delete: `Id` only — the payload is `RecordDeletePayload`, which has **no `Record` field**;
262
+ selecting `Record` (the Create/Update output) on a delete is a schema error.
263
+
264
+ **`allOrNone` semantics**
265
+ - `true` — all operations succeed or all roll back.
266
+ - `false` — independent operations succeed individually; dependent operations (chained via
267
+ `@{alias}`) still roll back together.
268
+
269
+ ### Mutation chaining
270
+
271
+ Chain related mutations with `@{alias}` references to an earlier mutation's `Id`. Required for
272
+ parent-child creation (nested child creates are unsupported).
273
+
274
+ ```graphql
275
+ mutation CreateAccountAndContact {
276
+ uiapi(input: { allOrNone: true }) {
277
+ AccountCreate(input: { Account: { Name: "Acme" } }) { Record { Id } }
278
+ ContactCreate(input: { Contact: { LastName: "Smith", AccountId: "@{AccountCreate}" } }) { Record { Id } }
279
+ }
280
+ }
281
+ ```
282
+
283
+ Rules: `A` must appear before `B`; `@{A}` is always `A`'s `Id`; only `Create` or `Delete` can
284
+ be chained *from* (not `Update`).
285
+
286
+ ## Object metadata & picklist values
287
+
288
+ Use `uiapi { objectInfos(...) }`. Pass **either** `apiNames` **or** `objectInfoInputs` — never both.
289
+
290
+ ```graphql
291
+ query GetObjectInfo($apiNames: [String!]!) {
292
+ uiapi { objectInfos(apiNames: $apiNames) {
293
+ ApiName label labelPlural
294
+ fields { ApiName label dataType updateable createable }
295
+ } }
296
+ }
297
+
298
+ query GetPicklistValues($objectInfoInputs: [ObjectInfoInput!]!) {
299
+ uiapi { objectInfos(objectInfoInputs: $objectInfoInputs) {
300
+ ApiName
301
+ fields { ApiName ... on PicklistField {
302
+ picklistValuesByRecordTypeIDs { recordTypeID picklistValues { label value } }
303
+ } }
304
+ } }
305
+ }
306
+ ```
307
+
308
+ ## Aggregations
309
+
310
+ `uiapi { aggregate { … } }` mirrors `query` (one entry per record type) but returns
311
+ aggregated buckets instead of rows — use it for counts/sums/grouped rollups so you don't
312
+ pull every record client-side. Pass `groupBy:` (each grouped field gets `{ group: true }`)
313
+ and select aggregate functions under `node { aggregate { … } }`. Scalar aggregates expose
314
+ `count`/`countDistinct`/`min`/`max`; numerics (`IntAggregate`, etc.) add `avg`/`sum`.
315
+
316
+ ```graphql
317
+ # Count + average employees, grouped by Industry
318
+ query AccountsByIndustry {
319
+ uiapi {
320
+ aggregate {
321
+ Account(groupBy: { Industry: { group: true } }, first: 50) {
322
+ edges {
323
+ node {
324
+ aggregate {
325
+ Industry @optional { value }
326
+ # aggregate functions are FieldValue wrappers — select { value }
327
+ # (count/sum are LongValue, avg is DoubleValue), not bare.
328
+ NumberOfEmployees @optional { count { value } avg { value } sum { value } }
329
+ }
330
+ }
331
+ }
332
+ totalCount
333
+ }
334
+ }
335
+ }
336
+ }
337
+ ```
338
+
339
+ `first:` is still required, `@optional` still applies, results page like a normal connection.
340
+
341
+ ## Related-list metadata
342
+
343
+ `uiapi { relatedListByName(parentApiName:, relatedListName:) }` returns the *shape* of a
344
+ parent object's related list — its display columns and ordering, not the child records
345
+ themselves (query those via the child relationship). Both args are required.
346
+
347
+ ```graphql
348
+ query AccountContactsRelatedList {
349
+ uiapi {
350
+ relatedListByName(parentApiName: "Account", relatedListName: "Contacts") {
351
+ label
352
+ childApiName
353
+ displayColumns { fieldApiName label sortable }
354
+ orderedByInfo { fieldApiName sortDirection }
355
+ }
356
+ }
357
+ }
358
+ ```
359
+
360
+ ## Error patterns
361
+
362
+ | Error contains | Resolution |
363
+ |---|---|
364
+ | `Cannot query field` / `ValidationError` / `validation error` | The *operation* is wrong or the type isn't accessible — re-ground the field (graphiti `sf-gql-discover`, or `graphql-search.sh <Entity>` on this fallback path) and fix the operation to the exact name from the Type definition; if it genuinely isn't in the org, deploy the metadata + assign perms then `npm run graphql:schema`. **Never edit `schema.graphql` to satisfy the validator** — it grants no org access and only hides the failure until runtime. |
365
+ | `Unknown type` | Type name wrong — verify PascalCase entity name via script |
366
+ | `Unknown argument` | Check Filter / OrderBy sections in script output |
367
+ | `invalid syntax` / `InvalidSyntax` | Fix syntax per message |
368
+ | `VariableTypeMismatch` / `UnknownType` | Correct argument type from schema |
369
+ | `invalid cross reference id` | Entity deleted — ask for a valid Id |
370
+ | `OperationNotSupported` | Check object availability and API version |
371
+ | `is not currently available in mutation results` | Remove the field from mutation output |
372
+ | `Cannot invoke JsonElement.isJsonObject()` | Use API v66+ for update-mutation `Record` selection |
373
+
374
+ **On PARTIAL** (mutation returns both data and errors): report inaccessible fields, explain
375
+ they cannot appear in mutation output, offer to remove them, and **wait for user consent**
376
+ before changing.
@@ -0,0 +1,119 @@
1
+ # Migration — old Data SDK → `@salesforce/platform-sdk`
2
+
3
+ The Data SDK changed in two breaking ways (PR #502, shipped in `@salesforce/platform-sdk`
4
+ v10.10.1):
5
+
6
+ 1. **Package renamed** — `@salesforce/sdk-data` → **`@salesforce/platform-sdk`**.
7
+ 2. **`sdk.graphql` reshaped** from a **callable method** into a **namespace** with `.query()`
8
+ and `.mutate()`, both taking an **options object**.
9
+
10
+ > Any remaining `@salesforce/sdk-data` strings in the webapps repo are **stale `dist/` build
11
+ > artifacts only** — not canonical. Always import from `@salesforce/platform-sdk`.
12
+
13
+ This is the **only** document where the dead callable API appears, for comparison. Everywhere
14
+ else in this skill uses the new API exclusively.
15
+
16
+ ---
17
+
18
+ ## 1. Import
19
+
20
+ ```diff
21
+ - import { createDataSDK, gql, type NodeOfConnection } from "@salesforce/sdk-data";
22
+ + import { createDataSDK, gql, type NodeOfConnection, type CacheControl } from "@salesforce/platform-sdk";
23
+ ```
24
+
25
+ `createDataSDK`, `gql`, `NodeOfConnection`, and `CacheControl` all export from
26
+ `@salesforce/platform-sdk`.
27
+
28
+ ## 2. Query call
29
+
30
+ ```diff
31
+ - // OLD — callable, positional args, returns a response object with .data / .errors
32
+ - const response = await sdk.graphql?.<GetAccountsQuery, GetAccountsQueryVariables>(GET_ACCOUNTS, variables);
33
+ - const accounts = response?.data?.uiapi?.query?.Account?.edges ?? [];
34
+ - if (response?.errors?.length) { /* ... */ }
35
+
36
+ + // NEW — namespace method, options bag, returns a reactive QueryResult
37
+ + const result = await sdk.graphql!.query<GetAccountsQuery, GetAccountsQueryVariables>({
38
+ + query: GET_ACCOUNTS,
39
+ + variables,
40
+ + });
41
+ + const accounts = result.data?.uiapi?.query?.Account?.edges ?? [];
42
+ + if (result.errors?.length) { /* ... */ }
43
+ ```
44
+
45
+ Passing the generated `<GetAccountsQuery>` type parameter makes `result.data` fully typed — no
46
+ `as any` cast needed.
47
+
48
+ Mapping:
49
+
50
+ | Old | New |
51
+ |-----|-----|
52
+ | `sdk.graphql?.(QUERY, vars)` | `sdk.graphql!.query({ query: QUERY, variables: vars })` |
53
+ | positional `<T, V>(query, variables)` | type params on `query<T, V>(options)` |
54
+ | `response.data` / `response.errors` | `result.data` / `result.errors` (same fields) |
55
+ | (none) | `result.subscribe(cb)` / `result.refresh()` — new reactive handle |
56
+ | (none) | `cacheControl` option — new per-call cache policy |
57
+
58
+ ## 3. Mutation call
59
+
60
+ The old callable form was used for mutations too. Mutations now have their own method, and the
61
+ operation key is **`mutation`**, not `query`:
62
+
63
+ ```diff
64
+ - const response = await sdk.graphql?.<CreateAccountMutation>(CREATE_ACCOUNT, { input });
65
+ + const { data, errors } = await sdk.graphql!.mutate<CreateAccountMutation, CreateAccountMutationVariables>({
66
+ + mutation: CREATE_ACCOUNT, // key is `mutation`, not `query`
67
+ + variables: { input },
68
+ + });
69
+ ```
70
+
71
+ `mutate()` returns `{ data, errors }` only — **no** `subscribe`/`refresh`, and it is **never
72
+ cached**. To refresh a list after a mutation, hold the query `result` and call
73
+ `result.refresh()` (see [caching.md](caching.md)).
74
+
75
+ ## 4. Optional chaining → non-null assertion
76
+
77
+ The old guidance was "always use optional chaining" (`sdk.graphql?.()`). Real consumer code now
78
+ uses the **non-null assertion** after `createDataSDK()`:
79
+
80
+ ```diff
81
+ - const response = await sdk.graphql?.(QUERY, vars);
82
+ + const result = await sdk.graphql!.query({ query: QUERY, variables: vars });
83
+ ```
84
+
85
+ `sdk.graphql` may still be `undefined` on some surfaces. Use `!` when you know the surface
86
+ supports data ops; use an explicit guard if it might not:
87
+
88
+ ```typescript
89
+ if (!sdk.graphql) throw new Error("GraphQL not available on this surface");
90
+ const result = await sdk.graphql.query({ query: QUERY, variables: vars });
91
+ ```
92
+
93
+ ## 5. Codegen types — unchanged generation, new call placement
94
+
95
+ `npm run graphql:codegen` and the generated type names (`<Op>Query` / `<Op>QueryVariables` /
96
+ `<Op>Mutation` / `<Op>MutationVariables`) are unchanged. Only **where** the type params attach
97
+ moves — from the callable to the namespace method:
98
+
99
+ ```diff
100
+ - await sdk.graphql?.<GetAccountsQuery, GetAccountsQueryVariables>(GET_ACCOUNTS, variables);
101
+ + await sdk.graphql!.query<GetAccountsQuery, GetAccountsQueryVariables>({ query: GET_ACCOUNTS, variables });
102
+ ```
103
+
104
+ ## 6. New capabilities to consider while migrating
105
+
106
+ The reshape also unlocked the freshness features (none required to get back to working). The one
107
+ migration-specific gotcha: **caching is now on by default** (300s, WebApp) — if any code relied
108
+ on every call hitting the network, add `cacheControl: "no-cache"` to it. The reactive
109
+ `subscribe`/`refresh` handle and the rest of `cacheControl` are in [caching.md](caching.md).
110
+
111
+ ## 7. Migration checklist
112
+
113
+ - [ ] Replace every `@salesforce/sdk-data` import with `@salesforce/platform-sdk`
114
+ - [ ] Convert every `sdk.graphql?.(q, v)` query to `sdk.graphql!.query({ query: q, variables: v })`
115
+ - [ ] Convert every mutation call to `sdk.graphql!.mutate({ mutation: m, variables: v })` (key is `mutation`)
116
+ - [ ] Move generated type params onto `query<T,V>` / `mutate<T,V>`
117
+ - [ ] Rename `response` → `result`; `response.data`/`.errors` → `result.data`/`.errors`
118
+ - [ ] Add a `cacheControl: "no-cache"` to any call that genuinely must always hit the network
119
+ - [ ] Re-run `npm run graphql:codegen` and `npx eslint <file>`
@@ -0,0 +1,152 @@
1
+ # REST (`sdk.fetch`) & UI bundle integration
2
+
3
+ Use `sdk.fetch` only when GraphQL doesn't cover the use case. The allowlist is in
4
+ [SKILL.md](../SKILL.md#platform-guardrails--never-regress-these) (guardrail 9). Every code shape
5
+ here is grounded in shipped consumer code.
6
+
7
+ ## Supported APIs (allowlist)
8
+
9
+ | API | Method | Endpoints / use case |
10
+ |---|---|---|
11
+ | GraphQL | `sdk.graphql` | all record queries/mutations via the `uiapi { }` namespace |
12
+ | UI API REST | `sdk.fetch` | `/services/data/v{ver}/ui-api/records/{id}` — record metadata when GraphQL insufficient |
13
+ | Apex REST | `sdk.fetch` | `/services/apexrest/{resource}` — custom server logic, aggregates, multi-step transactions |
14
+ | Connect REST | `sdk.fetch` | `/services/data/v{ver}/connect/file/upload/config` — file upload config |
15
+ | Einstein LLM | `sdk.fetch` | `/services/data/v{ver}/einstein/llm/prompt/generations` — AI text generation |
16
+
17
+ **Not supported:** Enterprise REST query (`/services/data/v*/query` with SOQL — blocked at the
18
+ proxy; use GraphQL for reads, Apex REST for server-side SOQL aggregates), Aura-enabled Apex
19
+ (`@AuraEnabled` — no invocation path from UI bundles), Chatter API (`/chatter/users/me` — use
20
+ `uiapi { currentUser { ... } }`), and any other Salesforce REST endpoint not in the table above.
21
+
22
+ > `sdk.fetch` is **not cached** — only `sdk.graphql!.query()` participates in the resource
23
+ > cache. REST reads hit the network every time.
24
+
25
+ ## `sdk.fetch` patterns
26
+
27
+ ```typescript
28
+ declare const __SF_API_VERSION__: string;
29
+ const API_VERSION = typeof __SF_API_VERSION__ !== "undefined" ? __SF_API_VERSION__ : "65.0";
30
+
31
+ // Connect — file upload config
32
+ const res = await sdk.fetch!(`/services/data/v${API_VERSION}/connect/file/upload/config`);
33
+
34
+ // Apex REST (no version in path)
35
+ const res = await sdk.fetch!("/services/apexrest/auth/login", {
36
+ method: "POST",
37
+ body: JSON.stringify({ email, password }),
38
+ headers: { "Content-Type": "application/json" },
39
+ });
40
+
41
+ // UI API — record with metadata (prefer GraphQL for simple reads)
42
+ const res = await sdk.fetch!(`/services/data/v${API_VERSION}/ui-api/records/${recordId}`);
43
+
44
+ // Einstein LLM
45
+ const res = await sdk.fetch!(`/services/data/v${API_VERSION}/einstein/llm/prompt/generations`, {
46
+ method: "POST",
47
+ body: JSON.stringify({ promptTextOrId: prompt }),
48
+ });
49
+ ```
50
+
51
+ Use `sdk.fetch!` (non-null assertion) just as `sdk.graphql!` — `fetch` is also optional on
52
+ `DataSDK`. When you call `sdk.fetch` you are handling raw HTTP, so check `response.ok` and then
53
+ parse the JSON body's `errors` array yourself (HTTP 200 still doesn't guarantee a successful
54
+ GraphQL/REST body):
55
+
56
+ ```typescript
57
+ const response = await sdk.fetch!(url, { method: "GET", headers: { Accept: "application/json" } });
58
+ if (!response.ok) throw new Error(`Request failed: ${response.statusText}`);
59
+ const result = (await response.json()) as { data?: unknown; errors?: { message: string }[] };
60
+ if (result.errors?.length) throw new Error(result.errors.map((e) => e.message).join("; "));
61
+ ```
62
+
63
+ > **Advanced / escape hatch — GraphQL over GET.** The supported path for record reads is
64
+ > `sdk.graphql!.query()` (POST, cached, typed) — see guardrail 9 in
65
+ > [SKILL.md](../SKILL.md#platform-guardrails--never-regress-these), which lists GraphQL as
66
+ > supported *only* via `sdk.graphql`. The shipped `accounts.ts` *demonstrates* issuing GraphQL
67
+ > as a `GET` through `sdk.fetch` — encoding the operation as a `queryInput` URL param against
68
+ > `/services/data/v{ver}/graphql` — but this is an example-app demonstration, not a sanctioned
69
+ > platform pattern. It sits below the supported-API contract: it bypasses the resource cache,
70
+ > the generated-type plumbing, and the supported-surface guarantee. Do not reach for it for
71
+ > ordinary record reads; stay on `sdk.graphql!.query()`.
72
+
73
+ ## UI bundle integration (reactive / lifecycle code)
74
+
75
+ The patterns below are framework-agnostic — the SDK calls are plain `async`; only *where* you
76
+ place them (a React effect, a Vue/Svelte lifecycle hook, a web-component `connectedCallback`,
77
+ a store action) varies by UI layer.
78
+
79
+ ### Pattern 1 — external `.graphql` file (complex queries)
80
+
81
+ **One operation per `.graphql` file** (one `query` or `mutation` plus its fragments). Import with
82
+ the `?raw` suffix; pass the imported string as `query`.
83
+
84
+ ```typescript
85
+ import { createDataSDK, type NodeOfConnection } from "@salesforce/platform-sdk";
86
+ import MY_QUERY from "./query/myQuery.graphql?raw"; // ?raw suffix required
87
+ import type { GetMyDataQuery, GetMyDataQueryVariables } from "../graphql-operations-types";
88
+
89
+ const sdk = await createDataSDK();
90
+ const result = await sdk.graphql!.query<GetMyDataQuery, GetMyDataQueryVariables>({
91
+ query: MY_QUERY,
92
+ variables,
93
+ });
94
+ ```
95
+
96
+ Run `npm run graphql:codegen` after creating/changing `.graphql` files.
97
+
98
+ ### Pattern 2 — inline `gql` tag (simple queries)
99
+
100
+ **Must use `gql`** — plain template strings bypass ESLint schema validation. This is exactly the
101
+ shape of the shipped `accounts.ts`.
102
+
103
+ ```typescript
104
+ import { createDataSDK, gql } from "@salesforce/platform-sdk";
105
+ import type { GetAccountsQuery, GetAccountsQueryVariables } from "../graphql-operations-types";
106
+
107
+ const GET_ACCOUNTS = gql`
108
+ query GetAccounts($first: Int, $after: String) {
109
+ uiapi { query {
110
+ Account(first: $first, after: $after, orderBy: { Name: { order: ASC } }) {
111
+ edges { node { Id Name @optional { value } Industry @optional { value } } }
112
+ pageInfo { hasNextPage endCursor }
113
+ }
114
+ } }
115
+ }
116
+ `;
117
+
118
+ const sdk = await createDataSDK();
119
+ const result = await sdk.graphql!.query<GetAccountsQuery, GetAccountsQueryVariables>({
120
+ query: GET_ACCOUNTS,
121
+ variables: { first: 20 },
122
+ });
123
+ ```
124
+
125
+ ### Canonical thin client (the scaffold)
126
+
127
+ The base-react-app template ships a thin wrapper (`graphqlClient.ts`) that every new app starts
128
+ from. Reuse it rather than re-implementing error handling:
129
+
130
+ ```typescript
131
+ import { createDataSDK } from "@salesforce/platform-sdk";
132
+
133
+ export async function executeGraphQL<TData, TVariables>(
134
+ query: string,
135
+ variables?: TVariables,
136
+ ): Promise<TData> {
137
+ const sdk = await createDataSDK();
138
+ const result = await sdk.graphql!.query<TData, TVariables>({ query, variables });
139
+ if (result.errors?.length) {
140
+ throw new Error(`GraphQL Error: ${result.errors.map((e) => e.message).join("; ")}`);
141
+ }
142
+ if (result.data == null) throw new Error("GraphQL response data is null");
143
+ return result.data;
144
+ }
145
+ ```
146
+
147
+ (To thread caching or use the reactive handle, call `sdk.graphql!.query()` directly with
148
+ `cacheControl` / `subscribe` — see [caching.md](caching.md).)
149
+
150
+ > Directory layout and the full command table (`graphql:schema` / `graphql:codegen` / `eslint` /
151
+ > `graphql-search.sh`, plus the graphiti commands) live in the spine — see
152
+ > [SKILL.md](../SKILL.md#commands--layout).