@salesforce/webapp-template-feature-graphql-experimental 1.56.0 → 1.57.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/README.md +3 -3
- package/package.json +2 -2
- package/rules/graphql-data-access-rule.md +103 -44
- package/skills/graphql-data-access/SKILL.md +21 -10
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ The base React app (`@salesforce/webapp-template-base-react-app-experimental`) i
|
|
|
15
15
|
- `@salesforce/sdk-data` dependency
|
|
16
16
|
- GraphQL ESLint plugin and rules
|
|
17
17
|
- GraphQL codegen dependencies and configuration (`codegen.yml`)
|
|
18
|
-
- `
|
|
18
|
+
- `getDataSDK()` + `data.graphql?.()` pattern and `gql` template tag
|
|
19
19
|
- Example components: `AccountsTable`, `AccountsPage`
|
|
20
20
|
- Type-safe GraphQL patterns with codegen
|
|
21
21
|
- `npm run graphql:schema` — download schema from a connected Salesforce org
|
|
@@ -54,7 +54,7 @@ The skill references the base app's `npm run graphql:codegen` and `npm run graph
|
|
|
54
54
|
|
|
55
55
|
This package includes **rules** that define conventions for GraphQL data access:
|
|
56
56
|
|
|
57
|
-
- Use `
|
|
57
|
+
- Use `getDataSDK()` + `data.graphql?.()` (never raw fetch/axios)
|
|
58
58
|
- Always provide response type generics
|
|
59
59
|
- Use `gql` tag or `.graphql` files (never plain strings)
|
|
60
60
|
- Follow the two-pattern workflow (external file vs. inline)
|
|
@@ -74,7 +74,7 @@ cp rules/graphql-data-access-rule.md ~/.clinerules/
|
|
|
74
74
|
|
|
75
75
|
The base React app includes reference examples:
|
|
76
76
|
|
|
77
|
-
- `src/api/graphql.ts` — Core `
|
|
77
|
+
- `src/api/graphql.ts` — Core `getDataSDK()` singleton and GraphQL utilities
|
|
78
78
|
- `src/api/utils/accounts.ts` — Pattern 1 (external `.graphql` file + codegen types)
|
|
79
79
|
- `src/api/utils/user.ts` — Pattern 2 (inline `gql` tag)
|
|
80
80
|
- `src/components/AccountsTable.tsx` — Component using GraphQL to fetch and render Accounts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/webapp-template-feature-graphql-experimental",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.57.0",
|
|
4
4
|
"description": "GraphQL documentation, agent skills, and rules for Salesforce web applications",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"author": "",
|
|
@@ -16,5 +16,5 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"clean": "echo 'No build artifacts to clean'"
|
|
18
18
|
},
|
|
19
|
-
"gitHead": "
|
|
19
|
+
"gitHead": "79d48250f085dbf33512c356b0ff5fb6acd239d3"
|
|
20
20
|
}
|
|
@@ -34,26 +34,25 @@ For a GraphQL operation named `GetHighRevenueAccounts`:
|
|
|
34
34
|
|
|
35
35
|
## Core Types & Function Signatures
|
|
36
36
|
|
|
37
|
-
###
|
|
37
|
+
### getDataSDK Function
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
Available from `@salesforce/webapp-experimental/api`:
|
|
40
40
|
|
|
41
41
|
```typescript
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
)
|
|
42
|
+
import { getDataSDK } from "@salesforce/webapp-experimental/api";
|
|
43
|
+
|
|
44
|
+
const data = await getDataSDK();
|
|
45
|
+
const response = await data.graphql?.<ResponseType, VariablesType>(query, variables);
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
- `
|
|
49
|
-
- `InputVariables` - The variables type (e.g., `GetHighRevenueAccountsQueryVariables`)
|
|
48
|
+
`getDataSDK()` returns a lazily-initialized `DataSDK` singleton. The `graphql` method uses optional chaining (`?.`) because not all surfaces support GraphQL.
|
|
50
49
|
|
|
51
50
|
### gql Template Tag
|
|
52
51
|
|
|
53
|
-
Also
|
|
52
|
+
Also available from `@salesforce/webapp-experimental/api` for inline query definitions:
|
|
54
53
|
|
|
55
54
|
```typescript
|
|
56
|
-
import { gql } from "
|
|
55
|
+
import { gql } from "@salesforce/webapp-experimental/api";
|
|
57
56
|
|
|
58
57
|
const MY_QUERY = gql`
|
|
59
58
|
query MyQuery {
|
|
@@ -68,7 +67,7 @@ The `gql` tag is a template literal that allows defining GraphQL queries inline
|
|
|
68
67
|
|
|
69
68
|
### GraphQLResponse Shape
|
|
70
69
|
|
|
71
|
-
|
|
70
|
+
`data.graphql?.()` returns `GraphQLResponse<T>`. Callers destructure `{ data, errors }` and handle errors themselves:
|
|
72
71
|
|
|
73
72
|
```typescript
|
|
74
73
|
interface GraphQLResponse<T> {
|
|
@@ -81,12 +80,47 @@ interface GraphQLResponse<T> {
|
|
|
81
80
|
}
|
|
82
81
|
```
|
|
83
82
|
|
|
83
|
+
### Handling Mixed Responses (Partial Success)
|
|
84
|
+
|
|
85
|
+
GraphQL can return **both `data` and `errors`** in the same response (partial success). For example, some fields may resolve while others fail due to field-level security. Choose a strategy per use case:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// Strategy A: Strict — treat any errors as failure (default for most queries)
|
|
89
|
+
if (response?.errors?.length) {
|
|
90
|
+
throw new Error(response.errors.map((e) => e.message).join("; "));
|
|
91
|
+
}
|
|
92
|
+
const result = response?.data;
|
|
93
|
+
|
|
94
|
+
// Strategy B: Tolerant — log errors but use partial data
|
|
95
|
+
if (response?.errors?.length) {
|
|
96
|
+
console.warn("GraphQL partial errors:", response.errors);
|
|
97
|
+
}
|
|
98
|
+
const result = response?.data;
|
|
99
|
+
|
|
100
|
+
// Strategy C: Discriminated — fail only when no data came back
|
|
101
|
+
if (response?.errors?.length && !response?.data) {
|
|
102
|
+
throw new Error(response.errors.map((e) => e.message).join("; "));
|
|
103
|
+
}
|
|
104
|
+
if (response?.errors?.length) {
|
|
105
|
+
console.warn("Partial success with errors:", response.errors);
|
|
106
|
+
}
|
|
107
|
+
const result = response?.data;
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**When to use each:**
|
|
111
|
+
|
|
112
|
+
- **Strategy A** — Default. Use for queries where incomplete data would be misleading (e.g., financial summaries, approval workflows).
|
|
113
|
+
- **Strategy B** — Use when partial data is still useful and the UI can degrade gracefully (e.g., dashboard tiles, optional fields).
|
|
114
|
+
- **Strategy C** — Use for mutations where the operation may succeed but some return fields are inaccessible.
|
|
115
|
+
|
|
116
|
+
For mutation-specific partial responses, see `docs/generate-mutation-query.md` which covers `PARTIAL` and `FAILED` status handling workflows.
|
|
117
|
+
|
|
84
118
|
### NodeOfConnection Utility Type
|
|
85
119
|
|
|
86
120
|
Extract the node type from a connection (edges/node pattern):
|
|
87
121
|
|
|
88
122
|
```typescript
|
|
89
|
-
import { type NodeOfConnection } from "
|
|
123
|
+
import { type NodeOfConnection } from "@salesforce/webapp-experimental/api";
|
|
90
124
|
|
|
91
125
|
// Extract Account node type from the query response
|
|
92
126
|
type AccountNode = NodeOfConnection<GetHighRevenueAccountsQuery["uiapi"]["query"]["Account"]>;
|
|
@@ -162,22 +196,31 @@ This generates types in `graphql-operations-types.ts`:
|
|
|
162
196
|
#### Step 3: Import and Use
|
|
163
197
|
|
|
164
198
|
```typescript
|
|
165
|
-
import {
|
|
199
|
+
import { getDataSDK, type NodeOfConnection } from "@salesforce/webapp-experimental/api";
|
|
166
200
|
import MY_QUERY from "./query/myQuery.graphql?raw";
|
|
167
201
|
import type { GetMyDataQuery, GetMyDataQueryVariables } from "../graphql-operations-types";
|
|
168
202
|
|
|
169
|
-
// Extract node type for cleaner return types
|
|
170
203
|
type MyNode = NodeOfConnection<GetMyDataQuery["uiapi"]["query"]["MyObject"]>;
|
|
171
204
|
|
|
172
205
|
export async function getMyData(variables: GetMyDataQueryVariables): Promise<MyNode[]> {
|
|
173
|
-
const
|
|
174
|
-
|
|
206
|
+
const data = await getDataSDK();
|
|
207
|
+
const response = await data.graphql?.<GetMyDataQuery, GetMyDataQueryVariables>(
|
|
208
|
+
MY_QUERY,
|
|
209
|
+
variables,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
if (response?.errors?.length) {
|
|
213
|
+
const errorMessages = response.errors.map((e) => e.message).join("; ");
|
|
214
|
+
throw new Error(`GraphQL Error: ${errorMessages}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return response?.data?.uiapi?.query?.MyObject?.edges?.map((edge) => edge?.node) || [];
|
|
175
218
|
}
|
|
176
219
|
```
|
|
177
220
|
|
|
178
221
|
**Key imports for Pattern 1:**
|
|
179
222
|
|
|
180
|
-
- `
|
|
223
|
+
- `getDataSDK` - Get the DataSDK singleton
|
|
181
224
|
- `NodeOfConnection` - Extract node types from connection responses
|
|
182
225
|
- Query from `.graphql` file with `?raw` suffix
|
|
183
226
|
- Generated types from `graphql-operations-types.ts`
|
|
@@ -206,7 +249,7 @@ export async function getMyData(variables: GetMyDataQueryVariables): Promise<MyN
|
|
|
206
249
|
For simpler queries without variables or when colocation is preferred:
|
|
207
250
|
|
|
208
251
|
```typescript
|
|
209
|
-
import {
|
|
252
|
+
import { getDataSDK, gql } from "@salesforce/webapp-experimental/api";
|
|
210
253
|
import { type CurrentUserQuery } from "../graphql-operations-types";
|
|
211
254
|
|
|
212
255
|
const CURRENT_USER_QUERY = gql`
|
|
@@ -229,9 +272,14 @@ interface User {
|
|
|
229
272
|
|
|
230
273
|
export async function getCurrentUser(): Promise<User | null> {
|
|
231
274
|
try {
|
|
232
|
-
const
|
|
275
|
+
const data = await getDataSDK();
|
|
276
|
+
const response = await data.graphql?.<CurrentUserQuery>(CURRENT_USER_QUERY);
|
|
277
|
+
|
|
278
|
+
if (response?.errors?.length) {
|
|
279
|
+
throw new Error(response.errors.map((e) => e.message).join("; "));
|
|
280
|
+
}
|
|
233
281
|
|
|
234
|
-
const userData = response
|
|
282
|
+
const userData = response?.data?.uiapi.currentUser;
|
|
235
283
|
|
|
236
284
|
if (!userData) {
|
|
237
285
|
throw new Error("No user data found");
|
|
@@ -250,7 +298,7 @@ export async function getCurrentUser(): Promise<User | null> {
|
|
|
250
298
|
|
|
251
299
|
**Key imports for Pattern 2:**
|
|
252
300
|
|
|
253
|
-
- `
|
|
301
|
+
- `getDataSDK` - Get the DataSDK singleton
|
|
254
302
|
- `gql` - Template tag for inline query definition
|
|
255
303
|
- Generated types from `graphql-operations-types.ts`
|
|
256
304
|
|
|
@@ -321,18 +369,22 @@ fragment ContactFields on Account {
|
|
|
321
369
|
### Usage
|
|
322
370
|
|
|
323
371
|
```typescript
|
|
324
|
-
import {
|
|
372
|
+
import { getDataSDK } from "@salesforce/webapp-experimental/api";
|
|
325
373
|
import QUERY from "./query/getAccountDetails.graphql?raw";
|
|
326
374
|
import type {
|
|
327
375
|
GetAccountDetailsQuery,
|
|
328
376
|
GetAccountDetailsQueryVariables,
|
|
329
377
|
} from "../graphql-operations-types";
|
|
330
378
|
|
|
331
|
-
const data = await
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
379
|
+
const data = await getDataSDK();
|
|
380
|
+
const response = await data.graphql?.<GetAccountDetailsQuery, GetAccountDetailsQueryVariables>(
|
|
381
|
+
QUERY,
|
|
382
|
+
{
|
|
383
|
+
id: accountId,
|
|
384
|
+
includeFinancials: userWantsFinancials,
|
|
385
|
+
includeContacts: userWantsContacts,
|
|
386
|
+
},
|
|
387
|
+
);
|
|
336
388
|
```
|
|
337
389
|
|
|
338
390
|
## Anti-Patterns (Not Recommended)
|
|
@@ -343,18 +395,21 @@ const data = await executeGraphQL<GetAccountDetailsQuery>(QUERY, {
|
|
|
343
395
|
// NOT RECOMMENDED: Direct axios/fetch calls for GraphQL
|
|
344
396
|
const response = await axios.post("/graphql", { query });
|
|
345
397
|
|
|
346
|
-
// PREFERRED: Use
|
|
347
|
-
const data = await
|
|
398
|
+
// PREFERRED: Use the DataSDK
|
|
399
|
+
const data = await getDataSDK();
|
|
400
|
+
const response = await data.graphql?.<ResponseType>(query, variables);
|
|
348
401
|
```
|
|
349
402
|
|
|
350
403
|
### Missing Type Definitions
|
|
351
404
|
|
|
352
405
|
```typescript
|
|
353
406
|
// NOT RECOMMENDED: Untyped GraphQL calls
|
|
354
|
-
const data = await
|
|
407
|
+
const data = await getDataSDK();
|
|
408
|
+
await data.graphql?.(query);
|
|
355
409
|
|
|
356
410
|
// PREFERRED: Provide response type
|
|
357
|
-
const data = await
|
|
411
|
+
const data = await getDataSDK();
|
|
412
|
+
const response = await data.graphql?.<GetMyDataQuery>(query);
|
|
358
413
|
```
|
|
359
414
|
|
|
360
415
|
### Plain String Queries (Without gql Tag)
|
|
@@ -362,23 +417,26 @@ const data = await executeGraphQL<GetMyDataQuery>(query);
|
|
|
362
417
|
```typescript
|
|
363
418
|
// NOT RECOMMENDED: Plain string queries without gql tag
|
|
364
419
|
const query = `query { ... }`;
|
|
365
|
-
const data = await
|
|
420
|
+
const data = await getDataSDK();
|
|
421
|
+
await data.graphql?.(query);
|
|
366
422
|
|
|
367
423
|
// PREFERRED: Use gql tag for inline queries
|
|
368
424
|
const QUERY = gql`query { ... }`;
|
|
369
|
-
const data = await
|
|
425
|
+
const data = await getDataSDK();
|
|
426
|
+
const response = await data.graphql?.<ResponseType>(QUERY);
|
|
370
427
|
|
|
371
428
|
// OR: Use .graphql file for complex queries
|
|
372
429
|
import QUERY from "./query/myQuery.graphql?raw";
|
|
373
|
-
const data = await
|
|
430
|
+
const data = await getDataSDK();
|
|
431
|
+
const response = await data.graphql?.<ResponseType>(QUERY);
|
|
374
432
|
```
|
|
375
433
|
|
|
376
|
-
## Benefits of
|
|
434
|
+
## Benefits of the DataSDK GraphQL API
|
|
377
435
|
|
|
378
|
-
-
|
|
436
|
+
- Uses the DataSDK with proper authentication and CSRF token handling
|
|
379
437
|
- Consistent typing with `GraphQLResponse<T>` interface
|
|
380
|
-
-
|
|
381
|
-
-
|
|
438
|
+
- Optional chaining (`?.`) safely handles surfaces where GraphQL is unavailable
|
|
439
|
+
- Callers get full `GraphQLResponse<T>` for flexible error handling
|
|
382
440
|
|
|
383
441
|
## Quality Checklist
|
|
384
442
|
|
|
@@ -390,18 +448,19 @@ Before completing GraphQL data access code:
|
|
|
390
448
|
2. [ ] Run `npm run graphql:codegen` to generate types
|
|
391
449
|
3. [ ] Import query with `?raw` suffix
|
|
392
450
|
4. [ ] Import generated types from `graphql-operations-types.ts`
|
|
393
|
-
5. [ ] Use `
|
|
394
|
-
6. [ ]
|
|
395
|
-
7. [ ]
|
|
396
|
-
8. [ ]
|
|
451
|
+
5. [ ] Use `data.graphql?.<ResponseType>()` with proper generic
|
|
452
|
+
6. [ ] Handle `response.errors` and destructure `response.data`
|
|
453
|
+
7. [ ] Use `NodeOfConnection` for cleaner node types when needed
|
|
454
|
+
8. [ ] Handle optional chaining for nested response data
|
|
455
|
+
9. [ ] Follow the pattern in `accounts.ts`
|
|
397
456
|
|
|
398
457
|
### For Pattern 2 (inline with gql):
|
|
399
458
|
|
|
400
459
|
1. [ ] Define query using `gql` template tag
|
|
401
460
|
2. [ ] Ensure query name matches generated types in `graphql-operations-types.ts`
|
|
402
461
|
3. [ ] Import generated types for the query
|
|
403
|
-
4. [ ] Use `
|
|
404
|
-
5. [ ] Handle errors
|
|
462
|
+
4. [ ] Use `data.graphql?.<ResponseType>()` with proper generic
|
|
463
|
+
5. [ ] Handle `response.errors` and destructure `response.data`
|
|
405
464
|
6. [ ] Handle optional chaining for nested response data
|
|
406
465
|
7. [ ] Follow the pattern in `user.ts`
|
|
407
466
|
|
|
@@ -5,7 +5,7 @@ description: Add or modify Salesforce GraphQL data access code. Use when the use
|
|
|
5
5
|
|
|
6
6
|
# GraphQL Data Access
|
|
7
7
|
|
|
8
|
-
Add or modify Salesforce GraphQL data access code using
|
|
8
|
+
Add or modify Salesforce GraphQL data access code using `getDataSDK()` + `data.graphql?.()` and codegen tooling.
|
|
9
9
|
|
|
10
10
|
## When to Use
|
|
11
11
|
|
|
@@ -18,14 +18,14 @@ Add or modify Salesforce GraphQL data access code using the established `execute
|
|
|
18
18
|
|
|
19
19
|
The base React app (`base-react-app`) ships with all GraphQL dependencies and tooling pre-configured:
|
|
20
20
|
|
|
21
|
-
- `@salesforce/sdk-data` — runtime SDK for `
|
|
21
|
+
- `@salesforce/sdk-data` — runtime SDK for `getDataSDK` and `gql`
|
|
22
22
|
- `@graphql-codegen/cli` + plugins — type generation from `.graphql` files and inline `gql` queries
|
|
23
23
|
- `@graphql-eslint/eslint-plugin` — linting for `.graphql` files and `gql` template literals
|
|
24
24
|
- `graphql` — shared by codegen, ESLint, and schema introspection
|
|
25
25
|
|
|
26
26
|
Before using this skill, ensure:
|
|
27
27
|
|
|
28
|
-
1. The
|
|
28
|
+
1. The `@salesforce/webapp-experimental/api` package is available (provides `getDataSDK`, `gql`, `NodeOfConnection`)
|
|
29
29
|
2. A `schema.graphql` file exists at the project root. If missing, generate it:
|
|
30
30
|
```bash
|
|
31
31
|
npm run graphql:schema
|
|
@@ -96,21 +96,31 @@ This updates `src/api/graphql-operations-types.ts` with:
|
|
|
96
96
|
|
|
97
97
|
```typescript
|
|
98
98
|
// Pattern 1
|
|
99
|
-
import {
|
|
99
|
+
import { getDataSDK, type NodeOfConnection } from "@salesforce/webapp-experimental/api";
|
|
100
100
|
import MY_QUERY from "./query/myQuery.graphql?raw";
|
|
101
101
|
import type { GetMyDataQuery, GetMyDataQueryVariables } from "../graphql-operations-types";
|
|
102
102
|
|
|
103
103
|
type MyNode = NodeOfConnection<GetMyDataQuery["uiapi"]["query"]["MyObject"]>;
|
|
104
104
|
|
|
105
105
|
export async function getMyData(variables: GetMyDataQueryVariables): Promise<MyNode[]> {
|
|
106
|
-
const
|
|
107
|
-
|
|
106
|
+
const data = await getDataSDK();
|
|
107
|
+
const response = await data.graphql?.<GetMyDataQuery, GetMyDataQueryVariables>(
|
|
108
|
+
MY_QUERY,
|
|
109
|
+
variables,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if (response?.errors?.length) {
|
|
113
|
+
const errorMessages = response.errors.map((e) => e.message).join("; ");
|
|
114
|
+
throw new Error(`GraphQL Error: ${errorMessages}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return response?.data?.uiapi?.query?.MyObject?.edges?.map((edge) => edge?.node) || [];
|
|
108
118
|
}
|
|
109
119
|
```
|
|
110
120
|
|
|
111
121
|
```typescript
|
|
112
122
|
// Pattern 2
|
|
113
|
-
import {
|
|
123
|
+
import { getDataSDK, gql } from "@salesforce/webapp-experimental/api";
|
|
114
124
|
import type { MySimpleQuery } from "../graphql-operations-types";
|
|
115
125
|
|
|
116
126
|
const MY_QUERY = gql`
|
|
@@ -120,15 +130,16 @@ const MY_QUERY = gql`
|
|
|
120
130
|
`;
|
|
121
131
|
|
|
122
132
|
export async function getSimpleData(): Promise<SomeType> {
|
|
123
|
-
const
|
|
124
|
-
|
|
133
|
+
const data = await getDataSDK();
|
|
134
|
+
const response = await data.graphql?.<MySimpleQuery>(MY_QUERY);
|
|
135
|
+
// check response.errors, then extract response.data
|
|
125
136
|
}
|
|
126
137
|
```
|
|
127
138
|
|
|
128
139
|
### Step 6: Verify
|
|
129
140
|
|
|
130
141
|
- [ ] Query field names match the schema exactly (case-sensitive)
|
|
131
|
-
- [ ] Response type generic is provided to `
|
|
142
|
+
- [ ] Response type generic is provided to `data.graphql?.<T>()`
|
|
132
143
|
- [ ] Optional chaining is used for nested response data
|
|
133
144
|
- [ ] Pattern 1: `.graphql` file imported with `?raw` suffix
|
|
134
145
|
- [ ] Pattern 2: Query uses `gql` tag (not plain string)
|