@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.
- 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/{developing-datacloud-code-extension → data360-code-extension-generate}/SKILL.md +7 -7
- package/skills/{developing-datacloud-code-extension → data360-code-extension-generate}/references/README.md +7 -7
- package/skills/{developing-datacloud-code-extension → data360-code-extension-generate}/references/quick-reference.md +2 -2
- package/skills/{getting-datacloud-schema → data360-schema-get}/SKILL.md +26 -26
- package/skills/{getting-datacloud-schema → data360-schema-get}/references/README.md +9 -9
- 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/{getting-datacloud-schema → data360-schema-get}/scripts/get_dlo_schema.py +0 -0
- /package/skills/{getting-datacloud-schema → data360-schema-get}/scripts/get_dmo_schema.py +0 -0
package/skills/experience-ui-bundle-salesforce-data-access/references/graphql-hand-authoring.md
ADDED
|
@@ -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).
|