Skip to content

Commit c14da08

Browse files
W-21685788: Expand using-webapp-salesforce-data guidance (#101)
* feat(using-webapp-salesforce-data): add supporting documentation Add detailed reference docs for schema introspection, read query generation, mutation query generation, query testing, and webapp integration patterns. Made-with: Cursor * refactor(using-webapp-salesforce-data): move graphql-search.sh to scripts/ Relocate the schema search script from the skill root into a dedicated scripts/ directory for better organization. Made-with: Cursor * feat(using-webapp-salesforce-data): align SKILL.md with using-salesforce-data - Add GraphQL Non-Negotiable Rules section (6 critical platform rules) - Fix mutation template to include allOrNone wrapper - Add doc cross-references (schema introspection, query generation, testing, webapp integration) - Add deploying-webapp-to-salesforce skill cross-reference - Update script paths from .a4drules/... to scripts/graphql-search.sh - Expand checklist with allOrNone, pagination, and error handling items - Add two named webapp integration patterns (external .graphql, inline gql) Made-with: Cursor * fix: remove negative constraints from frontmatter and body * fix: pr feedback * refactor(using-webapp-salesforce-data): rename docs to references Made-with: Cursor * Revert "fix: remove negative constraints from frontmatter and body" This reverts commit f439b7a. * fix: update graphql schema search script * fix: update skill name for deploying to salesforce --------- Co-authored-by: Hemant Singh Bisht <hsinghbisht@salesforce.com>
1 parent f39ac26 commit c14da08

7 files changed

Lines changed: 916 additions & 38 deletions

File tree

skills/using-webapp-salesforce-data/SKILL.md

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Use this skill when the user wants to:
1717

1818
## Data SDK Requirement
1919

20-
> **All Salesforce data access MUST use the Data SDK** (`@salesforce/sdk-data`). The SDK handles authentication, CSRF, and base URL resolution. Never use `fetch()` or `axios` directly.
20+
> **All Salesforce data access MUST use the Data SDK** (`@salesforce/sdk-data`). The SDK handles authentication, CSRF, and base URL resolution.
2121
2222
```typescript
2323
import { createDataSDK, gql } from "@salesforce/sdk-data";
@@ -67,6 +67,24 @@ const res = await sdk.fetch?.("/services/apexrest/my-resource");
6767

6868
---
6969

70+
## GraphQL Non-Negotiable Rules
71+
72+
These rules exist because Salesforce GraphQL has platform-specific behaviors that differ from standard GraphQL. Violations cause silent runtime failures.
73+
74+
1. **Schema is the single source of truth** — Every entity name, field name, and type must be confirmed via the schema search script before use in a query. Never guess — Salesforce field names are case-sensitive, relationships may be polymorphic, and custom objects use suffixes (`__c`, `__e`). See [Schema Introspection](references/schema-introspection.md) for entity identification and iterative lookup procedures.
75+
76+
2. **`@optional` on all record fields** (read queries) — Salesforce field-level security (FLS) causes queries to fail entirely if the user lacks access to even one field. The `@optional` directive (v65+) tells the server to omit inaccessible fields instead of failing. Apply it to every scalar field, parent relationship, and child relationship. Consuming code must use optional chaining (`?.`) and nullish coalescing (`??`).
77+
78+
3. **Correct mutation syntax** — Mutations wrap under `uiapi(input: { allOrNone: true/false })`, not bare `uiapi { ... }`. Always set `allOrNone` explicitly. Output fields cannot include child relationships or navigated reference fields. See [Mutation Query Generation](references/mutation-query-generation.md).
79+
80+
4. **Explicit pagination** — Always include `first:` in every query. If omitted, the server silently defaults to 10 records. Include `pageInfo { hasNextPage endCursor }` for any query that may need pagination.
81+
82+
5. **SOQL-derived execution limits** — Max 10 subqueries per request, max 5 levels of child-to-parent traversal, max 1 level of parent-to-child (no grandchildren), max 2,000 records per subquery. If a query would exceed these, split into multiple requests.
83+
84+
6. **HTTP 200 does not mean success** — Salesforce returns HTTP 200 even when operations fail. Always parse the `errors` array in the response body.
85+
86+
---
87+
7088
## GraphQL Workflow
7189

7290
### Step 1: Acquire Schema
@@ -75,18 +93,18 @@ The `schema.graphql` file (265K+ lines) is the source of truth. **Never open or
7593

7694
1. Check if `schema.graphql` exists at the SFDX project root
7795
2. If missing, run from the **webapp dir**: `npm run graphql:schema`
78-
3. Custom objects appear only after metadata is deployed
96+
3. Custom objects appear only after metadata is deployed — invoke the `deploying-webapp-to-salesforce` skill if deployment is needed
7997

8098
### Step 2: Look Up Entity Schema
8199

82100
Map user intent to PascalCase names ("accounts" → `Account`), then **run the search script from the project root**:
83101

84102
```bash
85-
# From project root — look up all relevant schema info for one or more entities
86-
bash .a4drules/skills/using-salesforce-data/graphql-search.sh Account
103+
# Look up all relevant schema info for one or more entities
104+
bash scripts/graphql-search.sh Account
87105

88106
# Multiple entities at once
89-
bash .a4drules/skills/using-salesforce-data/graphql-search.sh Account Contact Opportunity
107+
bash scripts/graphql-search.sh Account Contact Opportunity
90108
```
91109

92110
The script outputs five sections per entity:
@@ -96,11 +114,11 @@ The script outputs five sections per entity:
96114
4. **Create input** — fields accepted by create mutations
97115
5. **Update input** — fields accepted by update mutations
98116

99-
Use this output to determine exact field names before writing any query or mutation. **Maximum 2 script runs.** If the entity still can't be found, ask the user — the object may not be deployed.
117+
Use this output to determine exact field names before writing any query or mutation. **Maximum 2 script runs.** If the entity still can't be found, ask the user — the object may not be deployed. For entity identification procedures (`_Record` suffix, `__c` conventions) and iterative introspection cycles, see [Schema Introspection](references/schema-introspection.md).
100118

101119
### Step 3: Generate Query
102120

103-
Use the templates below. Every field name **must** be verified from the script output in Step 2.
121+
Use the templates below. Every field name **must** be verified from the script output in Step 2. For detailed generation rules, filtering, pagination, ordering, semi-joins, and field value wrappers, see [Read Query Generation](references/read-query-generation.md). For mutation chaining, input/output constraints, and transactional semantics, see [Mutation Query Generation](references/mutation-query-generation.md).
104122

105123
#### Read Query Template
106124

@@ -138,7 +156,7 @@ const name = node.Name?.value ?? "";
138156

139157
```graphql
140158
mutation CreateAccount($input: AccountCreateInput!) {
141-
uiapi {
159+
uiapi(input: { allOrNone: true }) {
142160
AccountCreate(input: $input) {
143161
Record { Id Name { value } }
144162
}
@@ -222,15 +240,20 @@ const fields = response?.data?.uiapi?.objectInfos?.[0]?.fields ?? [];
222240

223241
```bash
224242
# From project root — re-check the entity that caused the error
225-
bash .a4drules/skills/using-salesforce-data/graphql-search.sh <EntityName>
243+
bash scripts/graphql-search.sh <EntityName>
226244
```
227245

228-
Then fix the query using the exact names from the script output.
246+
Then fix the query using the exact names from the script output. For detailed error categories, status handling, and retry strategy, see [Query Testing](references/query-testing.md).
229247

230248
---
231249

232250
## Webapp Integration (React)
233251

252+
Two integration patterns are available:
253+
254+
- **Pattern 1 — External `.graphql` file** (recommended for complex queries): Create a `.graphql` file, run `npm run graphql:codegen`, import with `?raw` suffix
255+
- **Pattern 2 — Inline `gql` tag** (for simple queries): Use the `gql` template tag from `@salesforce/sdk-data`. **Must use `gql`** — plain template strings bypass ESLint schema validation.
256+
234257
```typescript
235258
import { createDataSDK, gql } from "@salesforce/sdk-data";
236259

@@ -242,8 +265,9 @@ const GET_ACCOUNTS = gql`
242265
edges {
243266
node {
244267
Id
245-
Name @optional { value }
246-
Industry @optional { value }
268+
Name @optional {
269+
value
270+
}
247271
}
248272
}
249273
}
@@ -254,14 +278,14 @@ const GET_ACCOUNTS = gql`
254278

255279
const sdk = await createDataSDK();
256280
const response = await sdk.graphql?.(GET_ACCOUNTS);
257-
258281
if (response?.errors?.length) {
259282
throw new Error(response.errors.map(e => e.message).join("; "));
260283
}
261-
262284
const accounts = response?.data?.uiapi?.query?.Account?.edges?.map(e => e.node) ?? [];
263285
```
264286

287+
For detailed patterns (external .graphql files, codegen, error handling strategies, quality checklists), see [Webapp Integration](references/webapp-integration.md).
288+
265289
---
266290

267291
## REST API Patterns
@@ -320,7 +344,7 @@ const response = await sdk.graphql?.(GET_CURRENT_USER);
320344
|---------|----------|-----|
321345
| `npm run graphql:schema` | webapp dir | Script in webapp's package.json |
322346
| `npx eslint <file>` | webapp dir | Reads eslint.config.js |
323-
| `bash .a4drules/skills/using-salesforce-data/graphql-search.sh <Entity>` | project root | Schema lookup |
347+
| `bash scripts/graphql-search.sh <Entity>` | skill root | Schema lookup |
324348
| `sf api request rest` | project root | Needs sfdx-project.json |
325349

326350
---
@@ -332,7 +356,7 @@ const response = await sdk.graphql?.(GET_CURRENT_USER);
332356
Run the search script to get all relevant schema info in one step:
333357

334358
```bash
335-
bash .a4drules/skills/using-salesforce-data/graphql-search.sh <EntityName>
359+
bash scripts/graphql-search.sh <EntityName>
336360
```
337361

338362
| Script Output Section | Used For |
@@ -358,6 +382,9 @@ bash .a4drules/skills/using-salesforce-data/graphql-search.sh <EntityName>
358382
### Checklist
359383

360384
- [ ] All field names verified via search script (Step 2)
361-
- [ ] `@optional` applied to record fields (reads)
385+
- [ ] `@optional` applied to all record fields (reads)
386+
- [ ] Mutations use `uiapi(input: { allOrNone: ... })` wrapper
387+
- [ ] `first:` specified in every query
362388
- [ ] Optional chaining in consuming code
389+
- [ ] `errors` array checked in response handling
363390
- [ ] Lint passes: `npx eslint <file>`
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Mutation Query Generation
2+
3+
## Mutation Types
4+
5+
The GraphQL engine supports three mutation operations:
6+
7+
- **Create** — Insert a new record
8+
- **Update** — Modify an existing record (Id-based)
9+
- **Delete** — Remove an existing record (Id-based)
10+
11+
Mutations are GA in API v66+. They live under `mutation { uiapi { ... } }` and only support UI API-available objects.
12+
13+
## Generation Rules
14+
15+
1. **Input fields validation** — Validate that input fields satisfy the constraints for the operation type
16+
2. **Output fields validation** — Validate that output fields satisfy the constraints for the operation type
17+
3. **Type consistency** — Variables used as query arguments and their related fields must share the same GraphQL type. Verify types via the schema search script — do NOT assume types
18+
4. **Input arguments**`input` is the default argument name unless otherwise specified
19+
5. **Output field** — For `Create` and `Update`, the output field is always named `Record` (type: EntityName)
20+
6. **Field name validation** — Every field name in the generated mutation **MUST** match a field confirmed via the schema search script. Do NOT guess or assume field names exist
21+
7. **Raw input values** — Numeric values must be raw numbers without commas, currency symbols, or locale formatting (e.g., `80000` not `"80,000"` or `"$80,000"`). Compound fields (like addresses) require constituent fields (e.g., `BillingCity`, `BillingStreet`) — do not attempt to set the compound wrapper itself.
22+
23+
## Transactional Semantics: `allOrNone`
24+
25+
The `uiapi` mutation input accepts an `allOrNone` argument that controls rollback behavior:
26+
27+
- **`allOrNone: true` (default)** — If any operation fails, all operations in the request are rolled back. Use when operations must succeed or fail together.
28+
- **`allOrNone: false`** — Independent operations can succeed individually. However, dependent operations (those using `@{alias}` references) still roll back together with their dependencies.
29+
30+
Always set `allOrNone` explicitly to make transactional intent clear.
31+
32+
## Mutation Schema Patterns
33+
34+
Replace `EntityName` with the actual entity name (e.g., Account, Case). `Delete` operations use generic `Record` types.
35+
36+
```graphql
37+
input EntityNameCreateRepresentation {
38+
# Subset of EntityName fields
39+
}
40+
input EntityNameCreateInput { EntityName: EntityNameCreateRepresentation! }
41+
type EntityNameCreatePayload { Record: EntityName! }
42+
43+
input EntityNameUpdateRepresentation {
44+
# Subset of EntityName fields
45+
}
46+
input EntityNameUpdateInput { Id: IdOrRef! EntityName: EntityNameUpdateRepresentation! }
47+
type EntityNameUpdatePayload { Record: EntityName! }
48+
49+
input RecordDeleteInput { Id: IdOrRef! }
50+
type RecordDeletePayload { Id: ID }
51+
52+
type UIAPIMutations {
53+
EntityNameCreate(input: EntityNameCreateInput!): EntityNameCreatePayload
54+
EntityNameDelete(input: RecordDeleteInput!): RecordDeletePayload
55+
EntityNameUpdate(input: EntityNameUpdateInput!): EntityNameUpdatePayload
56+
}
57+
```
58+
59+
## Input Field Constraints
60+
61+
### Create
62+
63+
- **Must** include all required fields (unless `defaultedOnCreate` is `true` and not explicitly requested)
64+
- **Must** only include `createable` fields
65+
- Child relationships cannot be setexclude them
66+
- Reference fields (`REFERENCE` type) can only be assigned IDs through their `ApiName` name
67+
- **No nested child creates** — Creating a record with child relationships in a single create operation is not supported. To create a parent and child together, use separate operations with `IdOrRef` chaining (see [Mutation Chaining](#mutation-chaining)).
68+
69+
### Update
70+
71+
- **Must** include the `Id` of the entity to update
72+
- **Must** only include `updateable` fields
73+
- Child relationships cannot be set — exclude them
74+
- Reference fields (`REFERENCE` type) can only be assigned IDs through their `ApiName` name
75+
76+
### Delete
77+
78+
- **Must** include the `Id` of the entity to delete
79+
80+
## Output Field Constraints
81+
82+
### Create and Update
83+
84+
- **Must** exclude all child relationships (child relationships cannot be queried in mutations)
85+
- **Must** exclude all `REFERENCE` fields unless accessed through their `ApiName` member (no navigation to referenced entity, no sub fields)
86+
- Inaccessible fields are reported in the `errors` attribute of the returned payload
87+
88+
### Delete
89+
90+
- **Must** only include the `Id` field
91+
92+
## Mutation Chaining
93+
94+
Chain related mutations in a single request using references to `Id` values from previous mutations. This is the required approach for creating parent-child records together, since nested child creates are not supported.
95+
96+
1. **Ordering** — Mutation `B` can reference mutation `A` only if `A` comes first in the query
97+
2. **Notation** — Use `SomeId: "@{A}"` in mutation `B` to set a field to the `Id` produced by mutation `A`
98+
3. **IDs only** — `@{A}` is always interpreted as the `Id` from mutation `A`
99+
4. **Restrictions** — `A` must be a `Create` or `Delete` mutation (chaining from `Update` will fail)
100+
101+
### Chaining Example
102+
103+
```graphql
104+
mutation CreateAccountAndContact {
105+
uiapi(input: { allOrNone: true }) {
106+
AccountCreate(input: { Account: { Name: "Acme" } }) {
107+
Record { Id }
108+
}
109+
ContactCreate(input: { Contact: { LastName: "Smith", AccountId: "@{AccountCreate}" } }) {
110+
Record { Id }
111+
}
112+
}
113+
}
114+
```
115+
116+
## Mutation Query Template
117+
118+
```graphql
119+
mutation mutateEntityName(
120+
# arguments
121+
) {
122+
uiapi(input: { allOrNone: true }) {
123+
EntityNameOperation(input: {
124+
# For Create and Update only:
125+
EntityName: {
126+
# Input fields — use raw values, no formatting
127+
}
128+
# For Update and Delete only:
129+
Id: ... # id here
130+
}) {
131+
# For Create and Update only:
132+
Record {
133+
# Output fields
134+
}
135+
# For Delete only:
136+
Id
137+
}
138+
}
139+
}
140+
```
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Query Testing
2+
3+
## Testing Method
4+
5+
Use `sf api request rest` to POST the query to the GraphQL endpoint. Run from the **SFDX project root** (where `sfdx-project.json` lives).
6+
7+
```bash
8+
sf api request rest /services/data/v66.0/graphql \
9+
--method POST \
10+
--body '{"query":"query GetData { uiapi { query { EntityName { edges { node { Id } } } } } }"}'
11+
```
12+
13+
- Use the API version of the target org (v66.0+ for mutation support, v65.0+ for `@optional`)
14+
- Replace the `query` value with the generated query string
15+
- If the query uses variables, include them in the JSON body as a `variables` key
16+
17+
## Critical: HTTP 200 Does Not Mean Success
18+
19+
Salesforce returns HTTP 200 even when the GraphQL operation has errors (e.g., invalid fields, permission failures, invalid IDs). **Always parse the `errors` array in the response body regardless of HTTP status code.** Do not treat HTTP 200 as confirmation that the query succeeded.
20+
21+
## Testing Workflow
22+
23+
This workflow applies to both read and mutation queries:
24+
25+
1. **Report method** — State the exact method: `sf api request rest` POST to `/services/data/vXX.0/graphql` from the project root
26+
2. **Ask user** — Ask the user whether they want to test the query. For mutations, also ask for input argument values — mutations modify real data, so explicit consent is essential. Wait for the user's answer before proceeding. Do not fabricate test data.
27+
3. **Execute test** — Only if the user explicitly agrees. Run `sf api request rest` with the query, variables, and correct API version
28+
4. **Report result** — Classify the result using the status definitions below. Always check the `errors` array in the response, even on HTTP 200.
29+
30+
## Result Status Definitions
31+
32+
| Status | Condition | Meaning |
33+
| --------- | ----------------------------------------------- | --------------------------------------------- |
34+
| `SUCCESS` | `errors` is absent or empty | Query is valid (even if no data is returned) |
35+
| `FAILED` | `data` is empty or null | Query is invalid |
36+
| `PARTIAL` | `data` is present **and** `errors` is not empty | Some fields are inaccessible (mutations only) |
37+
38+
## FAILED Status Handling
39+
40+
The query is invalid. Follow this sequence:
41+
42+
### 1. Error Analysis
43+
44+
Parse the `errors` array and check `errors[].extensions.ErrorType` for Salesforce-specific error classification. Categorize into:
45+
46+
| Category | ErrorType / Message Contains | Resolution |
47+
| --------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
48+
| **Syntax** | `InvalidSyntax` | Fix syntax errors using the error message details |
49+
| **Validation** | `ValidationError` | Field name is likely invalid — re-run the schema search script, ask user if still unclear |
50+
| **Type** | `VariableTypeMismatch` or `UnknownType` | Use error details and schema to correct the argument type; adjust variables |
51+
| **Execution** | `DataFetchingException`, `invalid cross reference id` | Entity is unknown/deleted — create entity first if possible, or ask for a valid Id |
52+
| **Navigation** | `is not currently available in mutation results` | Field cannot be in mutation output — apply PARTIAL status handling |
53+
| **Unsupported** | `OperationNotSupported` | The operation is not supported — check object availability and API version |
54+
| **API Version** | `Cannot invoke JsonElement.isJsonObject()` (on update mutations) | `Record` selection requires API version 64+ — report and retry with version 64 |
55+
56+
### 2. Targeted Resolution
57+
58+
Apply the resolution from the table above based on the error category. Update the query accordingly.
59+
60+
### 3. Test Again
61+
62+
Re-run the testing workflow with the updated query. Increment and track the attempt counter.
63+
64+
## PARTIAL Status Handling
65+
66+
The query executed but some fields are inaccessible (mutations only):
67+
68+
1. Report the fields listed in the `errors` attribute
69+
2. Explain that these fields cannot be queried as part of a mutation
70+
3. Explain that the query will report errors if these fields remain
71+
4. Offer to remove the offending fields
72+
5. **STOP and WAIT** for the user's answer. Do NOT remove fields without explicit consent.
73+
6. If the user agrees, restart the mutation generation workflow with the updated field list
74+
75+
## Retry and Escalation
76+
77+
- **Maximum 2 test attempts** per generated query
78+
- If targeted resolution fails after 2 attempts, ask the user for additional details and **restart the entire workflow from Step 1 (Acquire Schema)** to re-validate entity and field information

0 commit comments

Comments
 (0)