From 316c36093fce2c1e73e1da05f37f22bd40338e4c Mon Sep 17 00:00:00 2001 From: Ciaran Hannigan Date: Tue, 24 Mar 2026 14:16:14 -0400 Subject: [PATCH] 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 --- skills/using-webapp-salesforce-data/SKILL.md | 64 +++++++++++++------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/skills/using-webapp-salesforce-data/SKILL.md b/skills/using-webapp-salesforce-data/SKILL.md index cd88681..5ba2bdd 100644 --- a/skills/using-webapp-salesforce-data/SKILL.md +++ b/skills/using-webapp-salesforce-data/SKILL.md @@ -17,7 +17,7 @@ Use this skill when the user wants to: ## Data SDK Requirement -> **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. +> **All Salesforce data access MUST use the Data SDK** (`@salesforce/sdk-data`). The SDK handles authentication, CSRF, and base URL resolution. ```typescript import { createDataSDK, gql } from "@salesforce/sdk-data"; @@ -67,6 +67,24 @@ const res = await sdk.fetch?.("/services/apexrest/my-resource"); --- +## GraphQL Non-Negotiable Rules + +These rules exist because Salesforce GraphQL has platform-specific behaviors that differ from standard GraphQL. Violations cause silent runtime failures. + +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](docs/schema-introspection.md) for entity identification and iterative lookup procedures. + +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 (`??`). + +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](docs/mutation-query-generation.md). + +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. + +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. + +6. **HTTP 200 does not mean success** — Salesforce returns HTTP 200 even when operations fail. Always parse the `errors` array in the response body. + +--- + ## GraphQL Workflow ### Step 1: Acquire Schema @@ -75,18 +93,18 @@ The `schema.graphql` file (265K+ lines) is the source of truth. **Never open or 1. Check if `schema.graphql` exists at the SFDX project root 2. If missing, run from the **webapp dir**: `npm run graphql:schema` -3. Custom objects appear only after metadata is deployed +3. Custom objects appear only after metadata is deployed — invoke the `deploying-webapp-to-salesforce` skill if deployment is needed ### Step 2: Look Up Entity Schema Map user intent to PascalCase names ("accounts" → `Account`), then **run the search script from the project root**: ```bash -# From project root — look up all relevant schema info for one or more entities -bash .a4drules/skills/using-salesforce-data/graphql-search.sh Account +# Look up all relevant schema info for one or more entities +bash scripts/graphql-search.sh Account # Multiple entities at once -bash .a4drules/skills/using-salesforce-data/graphql-search.sh Account Contact Opportunity +bash scripts/graphql-search.sh Account Contact Opportunity ``` The script outputs five sections per entity: @@ -96,11 +114,11 @@ The script outputs five sections per entity: 4. **Create input** — fields accepted by create mutations 5. **Update input** — fields accepted by update mutations -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. +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](docs/schema-introspection.md). ### Step 3: Generate Query -Use the templates below. Every field name **must** be verified from the script output in Step 2. +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](docs/read-query-generation.md). For mutation chaining, input/output constraints, and transactional semantics, see [Mutation Query Generation](docs/mutation-query-generation.md). #### Read Query Template @@ -138,7 +156,7 @@ const name = node.Name?.value ?? ""; ```graphql mutation CreateAccount($input: AccountCreateInput!) { - uiapi { + uiapi(input: { allOrNone: true }) { AccountCreate(input: $input) { Record { Id Name { value } } } @@ -222,15 +240,20 @@ const fields = response?.data?.uiapi?.objectInfos?.[0]?.fields ?? []; ```bash # From project root — re-check the entity that caused the error -bash .a4drules/skills/using-salesforce-data/graphql-search.sh +bash scripts/graphql-search.sh ``` -Then fix the query using the exact names from the script output. +Then fix the query using the exact names from the script output. For detailed error categories, status handling, and retry strategy, see [Query Testing](docs/query-testing.md). --- ## Webapp Integration (React) +Two integration patterns are available: + +- **Pattern 1 — External `.graphql` file** (recommended for complex queries): Create a `.graphql` file, run `npm run graphql:codegen`, import with `?raw` suffix +- **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. + ```typescript import { createDataSDK, gql } from "@salesforce/sdk-data"; @@ -239,13 +262,7 @@ const GET_ACCOUNTS = gql` uiapi { query { Account(first: 10) { - edges { - node { - Id - Name @optional { value } - Industry @optional { value } - } - } + edges { node { Id Name @optional { value } } } } } } @@ -254,14 +271,14 @@ const GET_ACCOUNTS = gql` const sdk = await createDataSDK(); const response = await sdk.graphql?.(GET_ACCOUNTS); - if (response?.errors?.length) { throw new Error(response.errors.map(e => e.message).join("; ")); } - const accounts = response?.data?.uiapi?.query?.Account?.edges?.map(e => e.node) ?? []; ``` +For detailed patterns (external .graphql files, codegen, error handling strategies, quality checklists), see [Webapp Integration](docs/webapp-integration.md). + --- ## REST API Patterns @@ -320,7 +337,7 @@ const response = await sdk.graphql?.(GET_CURRENT_USER); |---------|----------|-----| | `npm run graphql:schema` | webapp dir | Script in webapp's package.json | | `npx eslint ` | webapp dir | Reads eslint.config.js | -| `bash .a4drules/skills/using-salesforce-data/graphql-search.sh ` | project root | Schema lookup | +| `bash scripts/graphql-search.sh ` | skill root | Schema lookup | | `sf api request rest` | project root | Needs sfdx-project.json | --- @@ -332,7 +349,7 @@ const response = await sdk.graphql?.(GET_CURRENT_USER); Run the search script to get all relevant schema info in one step: ```bash -bash .a4drules/skills/using-salesforce-data/graphql-search.sh +bash scripts/graphql-search.sh ``` | Script Output Section | Used For | @@ -358,6 +375,9 @@ bash .a4drules/skills/using-salesforce-data/graphql-search.sh ### Checklist - [ ] All field names verified via search script (Step 2) -- [ ] `@optional` applied to record fields (reads) +- [ ] `@optional` applied to all record fields (reads) +- [ ] Mutations use `uiapi(input: { allOrNone: ... })` wrapper +- [ ] `first:` specified in every query - [ ] Optional chaining in consuming code +- [ ] `errors` array checked in response handling - [ ] Lint passes: `npx eslint `