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 f439b7ad31.

* fix: update graphql schema search script

* fix: update skill name for deploying to salesforce

---------

Co-authored-by: Hemant Singh Bisht <hsinghbisht@salesforce.com>
This commit is contained in:
Ciaran E Hannigan 2026-03-30 05:22:05 -04:00 committed by GitHub
parent f39ac26c0d
commit c14da0844e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 916 additions and 38 deletions

View File

@ -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](references/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](references/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](references/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](references/read-query-generation.md). For mutation chaining, input/output constraints, and transactional semantics, see [Mutation Query Generation](references/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 <EntityName>
bash scripts/graphql-search.sh <EntityName>
```
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](references/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";
@ -242,8 +265,9 @@ const GET_ACCOUNTS = gql`
edges {
node {
Id
Name @optional { value }
Industry @optional { value }
Name @optional {
value
}
}
}
}
@ -254,14 +278,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](references/webapp-integration.md).
---
## REST API Patterns
@ -320,7 +344,7 @@ const response = await sdk.graphql?.(GET_CURRENT_USER);
|---------|----------|-----|
| `npm run graphql:schema` | webapp dir | Script in webapp's package.json |
| `npx eslint <file>` | webapp dir | Reads eslint.config.js |
| `bash .a4drules/skills/using-salesforce-data/graphql-search.sh <Entity>` | project root | Schema lookup |
| `bash scripts/graphql-search.sh <Entity>` | skill root | Schema lookup |
| `sf api request rest` | project root | Needs sfdx-project.json |
---
@ -332,7 +356,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 <EntityName>
bash scripts/graphql-search.sh <EntityName>
```
| Script Output Section | Used For |
@ -358,6 +382,9 @@ bash .a4drules/skills/using-salesforce-data/graphql-search.sh <EntityName>
### 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 <file>`

View File

@ -0,0 +1,140 @@
# Mutation Query Generation
## Mutation Types
The GraphQL engine supports three mutation operations:
- **Create** — Insert a new record
- **Update** — Modify an existing record (Id-based)
- **Delete** — Remove an existing record (Id-based)
Mutations are GA in API v66+. They live under `mutation { uiapi { ... } }` and only support UI API-available objects.
## Generation Rules
1. **Input fields validation** — Validate that input fields satisfy the constraints for the operation type
2. **Output fields validation** — Validate that output fields satisfy the constraints for the operation type
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
4. **Input arguments**`input` is the default argument name unless otherwise specified
5. **Output field** — For `Create` and `Update`, the output field is always named `Record` (type: EntityName)
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
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.
## Transactional Semantics: `allOrNone`
The `uiapi` mutation input accepts an `allOrNone` argument that controls rollback behavior:
- **`allOrNone: true` (default)** — If any operation fails, all operations in the request are rolled back. Use when operations must succeed or fail together.
- **`allOrNone: false`** — Independent operations can succeed individually. However, dependent operations (those using `@{alias}` references) still roll back together with their dependencies.
Always set `allOrNone` explicitly to make transactional intent clear.
## Mutation Schema Patterns
Replace `EntityName` with the actual entity name (e.g., Account, Case). `Delete` operations use generic `Record` types.
```graphql
input EntityNameCreateRepresentation {
# Subset of EntityName fields
}
input EntityNameCreateInput { EntityName: EntityNameCreateRepresentation! }
type EntityNameCreatePayload { Record: EntityName! }
input EntityNameUpdateRepresentation {
# Subset of EntityName fields
}
input EntityNameUpdateInput { Id: IdOrRef! EntityName: EntityNameUpdateRepresentation! }
type EntityNameUpdatePayload { Record: EntityName! }
input RecordDeleteInput { Id: IdOrRef! }
type RecordDeletePayload { Id: ID }
type UIAPIMutations {
EntityNameCreate(input: EntityNameCreateInput!): EntityNameCreatePayload
EntityNameDelete(input: RecordDeleteInput!): RecordDeletePayload
EntityNameUpdate(input: EntityNameUpdateInput!): EntityNameUpdatePayload
}
```
## Input Field Constraints
### Create
- **Must** include all required fields (unless `defaultedOnCreate` is `true` and not explicitly requested)
- **Must** only include `createable` fields
- Child relationships cannot be set — exclude them
- Reference fields (`REFERENCE` type) can only be assigned IDs through their `ApiName` name
- **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)).
### Update
- **Must** include the `Id` of the entity to update
- **Must** only include `updateable` fields
- Child relationships cannot be set — exclude them
- Reference fields (`REFERENCE` type) can only be assigned IDs through their `ApiName` name
### Delete
- **Must** include the `Id` of the entity to delete
## Output Field Constraints
### Create and Update
- **Must** exclude all child relationships (child relationships cannot be queried in mutations)
- **Must** exclude all `REFERENCE` fields unless accessed through their `ApiName` member (no navigation to referenced entity, no sub fields)
- Inaccessible fields are reported in the `errors` attribute of the returned payload
### Delete
- **Must** only include the `Id` field
## Mutation Chaining
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.
1. **Ordering** — Mutation `B` can reference mutation `A` only if `A` comes first in the query
2. **Notation** — Use `SomeId: "@{A}"` in mutation `B` to set a field to the `Id` produced by mutation `A`
3. **IDs only**`@{A}` is always interpreted as the `Id` from mutation `A`
4. **Restrictions**`A` must be a `Create` or `Delete` mutation (chaining from `Update` will fail)
### Chaining Example
```graphql
mutation CreateAccountAndContact {
uiapi(input: { allOrNone: true }) {
AccountCreate(input: { Account: { Name: "Acme" } }) {
Record { Id }
}
ContactCreate(input: { Contact: { LastName: "Smith", AccountId: "@{AccountCreate}" } }) {
Record { Id }
}
}
}
```
## Mutation Query Template
```graphql
mutation mutateEntityName(
# arguments
) {
uiapi(input: { allOrNone: true }) {
EntityNameOperation(input: {
# For Create and Update only:
EntityName: {
# Input fields — use raw values, no formatting
}
# For Update and Delete only:
Id: ... # id here
}) {
# For Create and Update only:
Record {
# Output fields
}
# For Delete only:
Id
}
}
}
```

View File

@ -0,0 +1,78 @@
# Query Testing
## Testing Method
Use `sf api request rest` to POST the query to the GraphQL endpoint. Run from the **SFDX project root** (where `sfdx-project.json` lives).
```bash
sf api request rest /services/data/v66.0/graphql \
--method POST \
--body '{"query":"query GetData { uiapi { query { EntityName { edges { node { Id } } } } } }"}'
```
- Use the API version of the target org (v66.0+ for mutation support, v65.0+ for `@optional`)
- Replace the `query` value with the generated query string
- If the query uses variables, include them in the JSON body as a `variables` key
## Critical: HTTP 200 Does Not Mean Success
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.
## Testing Workflow
This workflow applies to both read and mutation queries:
1. **Report method** — State the exact method: `sf api request rest` POST to `/services/data/vXX.0/graphql` from the project root
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.
3. **Execute test** — Only if the user explicitly agrees. Run `sf api request rest` with the query, variables, and correct API version
4. **Report result** — Classify the result using the status definitions below. Always check the `errors` array in the response, even on HTTP 200.
## Result Status Definitions
| Status | Condition | Meaning |
| --------- | ----------------------------------------------- | --------------------------------------------- |
| `SUCCESS` | `errors` is absent or empty | Query is valid (even if no data is returned) |
| `FAILED` | `data` is empty or null | Query is invalid |
| `PARTIAL` | `data` is present **and** `errors` is not empty | Some fields are inaccessible (mutations only) |
## FAILED Status Handling
The query is invalid. Follow this sequence:
### 1. Error Analysis
Parse the `errors` array and check `errors[].extensions.ErrorType` for Salesforce-specific error classification. Categorize into:
| Category | ErrorType / Message Contains | Resolution |
| --------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| **Syntax** | `InvalidSyntax` | Fix syntax errors using the error message details |
| **Validation** | `ValidationError` | Field name is likely invalid — re-run the schema search script, ask user if still unclear |
| **Type** | `VariableTypeMismatch` or `UnknownType` | Use error details and schema to correct the argument type; adjust variables |
| **Execution** | `DataFetchingException`, `invalid cross reference id` | Entity is unknown/deleted — create entity first if possible, or ask for a valid Id |
| **Navigation** | `is not currently available in mutation results` | Field cannot be in mutation output — apply PARTIAL status handling |
| **Unsupported** | `OperationNotSupported` | The operation is not supported — check object availability and API version |
| **API Version** | `Cannot invoke JsonElement.isJsonObject()` (on update mutations) | `Record` selection requires API version 64+ — report and retry with version 64 |
### 2. Targeted Resolution
Apply the resolution from the table above based on the error category. Update the query accordingly.
### 3. Test Again
Re-run the testing workflow with the updated query. Increment and track the attempt counter.
## PARTIAL Status Handling
The query executed but some fields are inaccessible (mutations only):
1. Report the fields listed in the `errors` attribute
2. Explain that these fields cannot be queried as part of a mutation
3. Explain that the query will report errors if these fields remain
4. Offer to remove the offending fields
5. **STOP and WAIT** for the user's answer. Do NOT remove fields without explicit consent.
6. If the user agrees, restart the mutation generation workflow with the updated field list
## Retry and Escalation
- **Maximum 2 test attempts** per generated query
- 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

View File

@ -0,0 +1,307 @@
# Read Query Generation
## Generation Rules
1. **No proliferation** — Only generate for explicitly requested fields, nothing else. Do NOT add fields the user did not ask for.
2. **Unique query** — Leverage child relationships to query entities in one single query
3. **Navigate entities** — Always use `relationshipName` to access reference fields and child entities. Exception: if `relationshipName` is null, return the `Id` itself
4. **Leverage fragments** — Generate one fragment per possible type on polymorphic fields (fields with `dataType="REFERENCE"` and more than one entry in `referenceToInfos`)
5. **Type consistency** — Variables used as query arguments and their related fields must share the same GraphQL type. Verify types against the schema search script output — do not assume types
6. **Type enforcement** — Use field type information from introspection and the GraphQL schema to generate correct field access
7. **Field name validation** — Every field name in the generated query **MUST** match a field confirmed via the schema search script. Do NOT guess or assume field names exist
8. **@optional for FLS** — Apply `@optional` on all Salesforce record fields when possible (see [Field-Level Security and @optional](#field-level-security-and-optional)). This lets the query succeed when the user lacks field-level access; the server omits inaccessible fields instead of failing
9. **Consuming code defense** — When generating or modifying code that consumes read query results, defend against missing fields (see [Field-Level Security and @optional](#field-level-security-and-optional)). Use optional chaining (`?.`), nullish coalescing (`??`), and null/undefined checks — never assume optional fields are present
10. **Semi and anti joins** — Use the semi-join or anti-join templates to filter an entity with conditions on child entities
11. **Explicit pagination** — Always include `first:` in every query to control page size (see [Pagination](#pagination)). Default is 10 if omitted.
12. **Respect execution limits** — Stay within SOQL-derived limits: max 10 subqueries per request, max 5 child-to-parent relationship levels, max 1 parent-to-child level (no grandchildren), max 55 child-to-parent relationships, max 20 parent-to-child relationships per query
13. **Compound fields** — When filtering, ordering, or aggregating, use constituent fields (e.g., `BillingCity`, `BillingCountry`) not the compound wrapper (`BillingAddress`). The compound wrapper is only for selection.
14. **`_Record` suffix awareness** — Objects added to UI API in v60+ may use a `_Record` suffix for their type name (e.g., `FeedItem_Record` instead of `FeedItem`). Always verify type names via schema lookup — do not assume type name equals sObject API name.
15. **Query generation** — Use the read query template below
## Field-Level Security and @optional
Field-level security (FLS) restricts which fields different users can see. Use the `@optional` directive on Salesforce record fields when possible. The server omits the field when the user lacks access, allowing the query to succeed instead of failing. Available in API v65.0+.
Apply `@optional` to:
- Scalar fields and value-type fields (e.g. `Name { value }`)
- Parent relationships
- Child relationships
**Consuming code must defend against missing fields.** When a field is omitted due to FLS, it will be `undefined` (or absent) in the response. Use optional chaining (`?.`), nullish coalescing (`??`), and explicit null/undefined checks when reading query results. Never assume an optional field is present.
```ts
// Defend against missing fields
const name = node.Name?.value ?? '';
const relatedName = node.RelationshipName?.Name?.value ?? 'N/A';
// Unsafe — will throw if field omitted due to FLS
const name = node.Name.value;
```
## Pagination
Salesforce GraphQL uses Relay Cursor Connections with **forward-only pagination**. There is no backward pagination (`last`/`before` are not supported).
### Core Rules
- **Always specify `first:`** — If omitted, the server defaults to 10 records. Be explicit.
- **Forward-only** — Use `first` and `after` only. Do **not** use `last` or `before` — they are unsupported and will fail.
- **Maximum without upperBound** — Standard pagination allows up to 4,000 total records across pages.
- **Use `pageInfo`** — Select `pageInfo { hasNextPage endCursor }` for any query that may need pagination.
### UpperBound Pagination (v59+)
When you need more than 200 records per page or more than 4,000 total records, switch to upperBound mode:
- **`first` must be 2002000** when `upperBound` is set. Values below 200 are invalid.
- **`upperBound`** declares the estimated total record count and enables extended pagination.
```graphql
# Standard pagination
Account(first: 50, after: $cursor) {
edges { node { Id Name @optional { value } } }
pageInfo { hasNextPage endCursor }
}
# UpperBound pagination for large result sets
Account(first: 2000, after: $cursor, upperBound: 10000) {
edges { node { Id Name @optional { value } } }
pageInfo { hasNextPage endCursor }
}
```
## Ordering
Use the `orderBy:` argument with generated `<Object>_OrderBy` input types. Run the schema search script to verify sortable fields.
### Rules
- Use `orderBy:` with the generated OrderBy type: `orderBy: { FieldName: { order: ASC } }`
- **Multi-column sorting** is supported by combining fields in the orderBy input
- **Unsupported field types** for ordering: multi-select picklist, rich text, long text area, encrypted fields. Do not order by these.
- **Locale sensitivity** — Sort order depends on user locale. For deterministic ordering, add `Id` as a tie-breaker field.
- **Compound fields** — Use constituent fields for ordering (e.g., `BillingCity`), not the compound wrapper.
```graphql
Account(
first: 10,
orderBy: { Name: { order: ASC }, CreatedDate: { order: DESC } }
) { ... }
```
## Filtering
### Boolean Filter Composition
Filter types include `AND`, `OR`, and `NOT` fields for combining conditions. Multiple filter fields at the same level combine with implicit AND.
```graphql
# Implicit AND — both conditions must match
Account(where: { Industry: { eq: "Technology" }, AnnualRevenue: { gt: 1000000 } })
# Explicit OR
Account(where: { OR: [
{ Industry: { eq: "Technology" } },
{ Industry: { eq: "Finance" } }
] })
# NOT
Account(where: { NOT: { Industry: { eq: "Technology" } } })
```
### Date and DateTime Filtering
Date and DateTime fields use special input objects (`DateInput`/`DateTimeInput`) that support both literal values and SOQL-style relative date semantics.
```graphql
# Literal date
Opportunity(where: { CloseDate: { eq: { value: "2024-12-31" } } })
# Relative date literal
Opportunity(where: { CloseDate: { gte: { literal: TODAY } } })
```
Verify exact literal enum values (e.g., `TODAY`, `THIS_MONTH`) via the schema search script.
### String Equality Is Case-Insensitive
String comparisons with `eq` are case-insensitive in Salesforce GraphQL. Do not rely on case sensitivity for string equality filters.
### Relationship Filters
Filter through parent relationships using nested filter objects (not dot notation):
```graphql
# Correct — nested filter objects
Contact(where: { Account: { Name: { like: "Acme%" } } })
# Wrong — dot notation is not supported
Contact(where: { "Account.Name": { like: "Acme%" } })
```
### Polymorphic Relationship Filters
Polymorphic relationships use union-aware filter input types named `<Object>_<RelationshipName>_Filters`. Filter by specific concrete types within the union:
```graphql
# Filter by polymorphic Owner (which is a union of User, Group, etc.)
Account(where: { Owner: { User: { Username: { like: "admin%" } } } })
```
Verify exact filter input type names and available concrete types via the schema search script.
### ID Filtering
Salesforce accepts both 15-character and 18-character record IDs for `Id` filtering. Do not reject or "correct" either form.
## Semi-Join and Anti-Join Templates
Semi-joins and anti-joins filter a parent entity using conditions on child entities. They use `inq` (semi-join) and `ninq` (anti-join) operators on the parent entity's `Id`.
The operator accepts:
- The child entity camelCase name with conditions
- The `ApiName` field containing the parent entity `Id` (`fieldName` from `childRelationships`)
If the only condition is child entity existence, use `Id: { ne: null }`.
### Restrictions
Semi-join and anti-join queries have SOQL-derived restrictions:
- **Limited count** — There are limits on the number of `inq`/`ninq` operators per query
- **No `ne` with joins** — Cannot use `ne` operator in combination with join operators
- **No `or` in subquery** — The join subquery conditions cannot use `OR`
- **No `orderBy` in subquery** — Join subqueries do not support ordering
- **Nesting restrictions** — Semi/anti-joins cannot be nested within each other
### Semi-Join Example
Filter `ParentEntity` to include only those with at least one matching `ChildEntity`:
```graphql
query testSemiJoin {
uiapi {
query {
ParentEntity(
where: {
Id: {
inq: {
ChildEntity: {
Name: { like: "test%" }
Type: { eq: "some value" }
}
ApiName: "parentIdFieldInChild"
}
}
}
) {
edges {
node {
Id
Name @optional {
value
}
}
}
}
}
}
}
```
### Anti-Join Example
Same as the semi-join example, but replace `inq` with `ninq` to filter `ParentEntity` with **no** matching `ChildEntity`.
## Current User Exception
To retrieve **current user**, **connected user**, or **authenticated user** information, use `uiapi.currentUser` instead of the standard query pattern. This field takes **no arguments** and returns a `User` type.
## Conditional Field Selection
For dynamic fieldsets with **known** fields, use `@include(if: $condition)` and `@skip(if: $condition)` directives in `.graphql` files. See GraphQL spec for details.
## Read Query Template
```graphql
query QueryName($after: String) {
uiapi {
query {
EntityName(
first: 10 # Always specify — default is 10 if omitted
after: $after # For pagination
where: { ... } # Filter conditions
orderBy: { ... } # Sort order
) {
edges {
node {
# Direct fields — use @optional for FLS resilience
FieldName @optional { value }
# Non-polymorphic reference (single type)
RelationshipName @optional {
Id
Name { value }
}
# Polymorphic reference (multiple types)
PolymorphicRelationshipName @optional {
...TypeAInfo
...TypeBInfo
}
# Child relationship (subquery) — max 1 level deep, no grandchildren
RelationshipName @optional (
first: 10 # Always specify
) {
edges {
node {
# fields
}
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
fragment TypeAInfo on TypeA {
Id
SpecificFieldA @optional { value }
}
fragment TypeBInfo on TypeB {
Id
SpecificFieldB @optional { value }
}
```
## Field Value Wrappers
Schema fields use typed wrappers. Access the underlying value via `.value`:
| Wrapper Type | Underlying Type | Access Pattern |
| ----------------- | --------------- | --------------------- |
| `StringValue` | `String` | `field { value }` |
| `IntValue` | `Int` | `field { value }` |
| `CurrencyValue` | `Currency` | `field { value }` |
| `DateTimeValue` | `DateTime` | `field { value }` |
| `PicklistValue` | `Picklist` | `field { value }` |
| `BooleanValue` | `Boolean` | `field { value }` |
| `DoubleValue` | `Double` | `field { value }` |
| `PercentValue` | `Percent` | `field { value }` |
| `IDValue` | `ID` | `field { value }` |
| `EmailValue` | `Email` | `field { value }` |
| `PhoneNumberValue`| `PhoneNumber` | `field { value }` |
| `UrlValue` | `Url` | `field { value }` |
| `DateValue` | `Date` | `field { value }` |
| `LongValue` | `Long` | `field { value }` |
| `TextAreaValue` | `TextArea` | `field { value }` |
All wrappers also expose `displayValue: String` for formatted display. `displayValue` is server-rendered using SOQL `toLabel()` or `format()` depending on field type — use it for UI display instead of formatting values client-side.

View File

@ -0,0 +1,53 @@
# Schema Introspection
## Schema Access Policy
The `schema.graphql` file is **265,000+ lines**. Loading it into context or opening it in an editor will overwhelm the context window or crash tools.
Do not use cat, less, more, head, tail, editors (VS Code, vim, nano), or programmatic parsers (node, python, awk, sed, jq) on `schema.graphql`. Use the schema search script or targeted grep calls only.
## Schema Lookup
Run the search script from the **SFDX project root** to get all relevant schema info in one step:
```bash
bash scripts/graphql-search.sh <EntityName>
# Multiple entities:
bash scripts/graphql-search.sh Account Contact Opportunity
```
**Maximum 2 script runs.** If the entity still can't be found after checking naming variations, ask the user.
## Entity Identification
Map user intent to PascalCase entity names:
1. Convert natural language to PascalCase (e.g., "accounts" → `Account`, "case comments" → `CaseComment`, "custom objects" → `CustomObject__c`)
2. Run the schema search script to validate the entity exists
3. If a candidate does not match, try:
- `__c` suffix for custom objects, `__e` for platform events
- **`_Record` suffix** — Objects added to UI API in API v60+ may use `<EntityName>_Record` as their type name (e.g., `FeedItem_Record` instead of `FeedItem`)
4. If an entity cannot be resolved, **ask the user** for the correct name — do not guess
## Iterative Introspection
Use a maximum of **3 introspection cycles** to resolve all entities and their dependencies:
1. **Introspect** — Run the schema search script for each unresolved entity
2. **Fields** — Extract requested field names and types from the type definition output
3. **References** — Identify reference fields. If a reference resolves to multiple types, mark it as **polymorphic** (use inline fragments in the generated query). Add newly discovered entity types to the working list.
4. **Child relationships** — Identify Connection types (e.g., `Contacts: ContactConnection`). Add child entity types to the working list.
5. **Next cycle** — If unresolved entities remain and the cycle limit hasn't been reached, repeat from step 1
### Hard Stop Rules
- If no introspection data is returned for an entity, **stop** — the entity may not be deployed
- If unknown entities remain after 3 cycles, **stop** — ask the user for clarification
- Do not proceed with query generation until all entities and requested fields are confirmed in the schema
## Deployment Prerequisites
The schema reflects the **current org state**. Custom objects and fields appear only after metadata is deployed.
- **Before** running `npm run graphql:schema`: Deploy all metadata and assign permission sets. Invoke the `deploying-webapp-to-salesforce` skill for the full sequence.
- **After** any metadata deployment: Re-run `npm run graphql:schema` and `npm run graphql:codegen` so types and queries stay in sync.

View File

@ -0,0 +1,221 @@
# Webapp Integration
## When to Use
This guide applies when integrating GraphQL queries into a React webapp using `createDataSDK` + codegen from `@salesforce/sdk-data`.
## Core Types & Function Signatures
### createDataSDK and graphql
```typescript
import { createDataSDK } from "@salesforce/sdk-data";
const sdk = await createDataSDK();
const response = await sdk.graphql?.<ResponseType, VariablesType>(query, variables);
```
`createDataSDK()` returns a `DataSDK` instance. The `graphql` method uses optional chaining (`?.`) because not all surfaces support GraphQL.
### gql Template Tag
```typescript
import { gql } from "@salesforce/sdk-data";
const MY_QUERY = gql`
query MyQuery {
uiapi { ... }
}
`;
```
The `gql` tag enables ESLint validation against the schema. Plain template strings bypass validation.
### NodeOfConnection
```typescript
import { type NodeOfConnection } from "@salesforce/sdk-data";
type AccountNode = NodeOfConnection<GetHighRevenueAccountsQuery["uiapi"]["query"]["Account"]>;
```
Use `NodeOfConnection` to extract the node type from a Connection type for cleaner typing.
## Query Patterns
Choose the pattern based on query complexity:
- **Pattern 1 — External `.graphql` file**: Recommended for complex queries with variables, fragments, or shared across files. Full codegen support, syntax highlighting, shareable. Requires codegen step after changes. Does NOT support dynamic queries.
- **Pattern 2 — Inline `gql` tag**: Recommended for simple queries. Supports dynamic queries (field set varies at runtime). **MUST use `gql` tag** — plain template strings bypass `@graphql-eslint` validation.
## Pattern 1: External .graphql File
Create a `.graphql` file, run `npm run graphql:codegen`, import with `?raw` suffix, and use generated types.
**Required imports:**
```typescript
import { createDataSDK, type NodeOfConnection } from "@salesforce/sdk-data";
import MY_QUERY from "./query/myQuery.graphql?raw"; // ?raw suffix required
import type { GetMyDataQuery, GetMyDataQueryVariables } from "../graphql-operations-types";
```
**Example usage:**
```typescript
const sdk = await createDataSDK();
const response = await sdk.graphql?.<GetMyDataQuery, GetMyDataQueryVariables>(
MY_QUERY,
variables
);
if (response?.errors?.length) {
throw new Error(response.errors.map((e) => e.message).join("; "));
}
const nodes = response?.data?.uiapi?.query?.EntityName?.edges?.map((e) => e.node) ?? [];
```
## Pattern 2: Inline gql Tag
**Required imports:**
```typescript
import { createDataSDK, gql } from "@salesforce/sdk-data";
import { type CurrentUserQuery } from "../graphql-operations-types";
const MY_QUERY = gql`
query CurrentUser {
uiapi { ... }
}
`;
```
> **MUST use `gql` tag** — plain template strings bypass the `@graphql-eslint` processor entirely, meaning no lint validation against the schema.
## Error Handling Strategies
**Strategy A — Strict (default):** Treat any errors as failure.
```typescript
if (response?.errors?.length) {
throw new Error(response.errors.map((e) => e.message).join("; "));
}
const result = response?.data;
```
**Strategy B — Tolerant:** Log errors but use available data.
```typescript
if (response?.errors?.length) {
console.warn("GraphQL partial errors:", response.errors);
}
const result = response?.data;
```
**Strategy C — Discriminated:** Fail only when no data is returned. Useful for mutations where some return fields may be inaccessible.
```typescript
if (!response?.data && response?.errors?.length) {
throw new Error(response.errors.map((e) => e.message).join("; "));
}
const result = response?.data;
```
Responses follow `uiapi.query.ObjectName.edges[].node`; fields use `{ value }`.
## Conditional Field Selection
For dynamic fieldsets with **known** fields, use `@include(if: $condition)` and `@skip(if: $condition)` directives in `.graphql` files. See GraphQL spec for details.
## ESLint Validation
After writing the query into a source file, validate it against the schema:
```bash
# Run from webapp dir (force-app/main/default/webapplications/<app-name>/)
npx eslint <path-to-file-containing-query>
```
**How it works:** The ESLint config uses `@graphql-eslint/eslint-plugin` with its `processor`, which extracts GraphQL operations from `gql` template literals in `.ts`/`.tsx` files and validates the extracted `.graphql` virtual files against `schema.graphql`.
**Rules enforced:** `no-anonymous-operations`, `no-duplicate-fields`, `known-fragment-names`, `no-undefined-variables`, `no-unused-variables`
**On failure:** Fix the reported issues, re-run `npx eslint <file>` until clean, then proceed to testing.
> **Prerequisites**: The `schema.graphql` file must exist and project dependencies must be installed (`npm install`).
## Codegen
Generate TypeScript types from `.graphql` files and inline `gql` queries:
```bash
# Run from webapp dir (force-app/main/default/webapplications/<app-name>/)
npm run graphql:codegen
```
Output: `src/api/graphql-operations-types.ts`
Naming conventions:
- `<OperationName>Query` / `<OperationName>Mutation` — response types
- `<OperationName>QueryVariables` / `<OperationName>MutationVariables` — variable types
## Anti-Patterns
### Direct API Calls
```typescript
// NOT RECOMMENDED: Direct axios/fetch calls for GraphQL
// PREFERRED: Use the Data SDK
const sdk = await createDataSDK();
const response = await sdk.graphql?.<ResponseType>(query, variables);
```
### Missing Type Definitions
```typescript
// NOT RECOMMENDED: Untyped GraphQL calls
// PREFERRED: Provide response type
const response = await sdk.graphql?.<GetMyDataQuery>(query);
```
### Plain String Queries (Without gql Tag)
```typescript
// NOT RECOMMENDED: Plain strings bypass ESLint validation
const query = `query { ... }`;
// PREFERRED: Use gql tag for inline queries
const QUERY = gql`query { ... }`;
```
## Quality Checklists
### For Pattern 1 (.graphql files):
1. [ ] All field names verified via schema search script
2. [ ] Create `.graphql` file for the query/mutation
3. [ ] Run `npm run graphql:codegen` to generate types
4. [ ] Import query with `?raw` suffix
5. [ ] Import generated types from `graphql-operations-types.ts`
6. [ ] Use `sdk.graphql?.<ResponseType>()` with proper generic
7. [ ] Handle `response.errors` and destructure `response.data`
8. [ ] Use `NodeOfConnection` for cleaner node types when needed
9. [ ] Run `npx eslint <file>` from webapp dir — fix all GraphQL errors
### For Pattern 2 (inline with gql):
1. [ ] All field names verified via schema search script
2. [ ] Define query using `gql` template tag (NOT a plain string)
3. [ ] Ensure query name matches generated types in `graphql-operations-types.ts`
4. [ ] Import generated types for the query
5. [ ] Use `sdk.graphql?.<ResponseType>()` with proper generic
6. [ ] Handle `response.errors` and destructure `response.data`
7. [ ] Run `npx eslint <file>` from webapp dir — fix all GraphQL errors
### General:
- [ ] Lint validation passes (`npx eslint <file>` reports no GraphQL errors)
- [ ] Query field names match the schema exactly (case-sensitive)
- [ ] Response type generic is provided to `sdk.graphql?.<T>()`
- [ ] Optional chaining is used for nested response data

View File

@ -1,20 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail # exit on error (-e), undefined vars (-u), and propagate pipeline failures (-o pipefail)
# graphql-search.sh — Look up one or more Salesforce entities in schema.graphql.
#
# Run from the SFDX project root (where schema.graphql lives):
# bash .a4drules/skills/using-salesforce-data/graphql-search.sh Account
# bash .a4drules/skills/using-salesforce-data/graphql-search.sh Account Contact Opportunity
# bash scripts/graphql-search.sh Account
# bash scripts/graphql-search.sh Account Contact Opportunity
#
# Pass a custom schema path with -s / --schema:
# bash .a4drules/skills/using-salesforce-data/graphql-search.sh -s /path/to/schema.graphql Account
# bash .a4drules/skills/using-salesforce-data/graphql-search.sh --schema ./other/schema.graphql Account Contact
# bash scripts/graphql-search.sh -s /path/to/schema.graphql Account
# bash scripts/graphql-search.sh --schema ./other/schema.graphql Account Contact
#
# Output sections per entity:
# 1. Type definition — all fields and relationships
# 2. Filter options — <Entity>_Filter input (for `where:`)
# 3. Sort options — <Entity>_OrderBy input (for `orderBy:`)
# 4. Create input — <Entity>CreateRepresentation (for create mutations)
# 5. Update input — <Entity>UpdateRepresentation (for update mutations)
# 1. Type definition — all fields and relationships
# 2. Filter options — <Entity>_Filter input (for `where:`)
# 3. Sort options — <Entity>_OrderBy input (for `orderBy:`)
# 4. Create mutation wrapper — <Entity>CreateInput
# 5. Create mutation fields — <Entity>CreateRepresentation (for create mutations)
# 6. Update mutation wrapper — <Entity>UpdateInput
# 7. Update mutation fields — <Entity>UpdateRepresentation (for update mutations)
SCHEMA="./schema.graphql"
@ -62,6 +65,19 @@ if [ ! -f "$SCHEMA" ]; then
exit 1
fi
if [ ! -r "$SCHEMA" ]; then
echo "ERROR: schema.graphql is not readable at $SCHEMA"
echo " Check file permissions: ls -la $SCHEMA"
exit 1
fi
if [ ! -s "$SCHEMA" ]; then
echo "ERROR: schema.graphql is empty at $SCHEMA"
echo " Regenerate it from the webapp dir:"
echo " cd force-app/main/default/webapplications/<app-name> && npm run graphql:schema"
exit 1
fi
# ── Helper: extract lines from a grep match through the closing brace ────────
# Prints up to MAX_LINES lines after (and including) the first match of PATTERN.
# Uses a generous line count — blocks are always closed by a "}" line.
@ -72,68 +88,104 @@ extract_block() {
local max_lines="$3"
local match
match=$(grep -nE "$pattern" "$SCHEMA" | head -1)
local grep_exit=0
match=$(grep -nE "$pattern" "$SCHEMA" | head -1) || grep_exit=$?
if [ "$grep_exit" -eq 2 ]; then
echo " ERROR: grep failed on pattern: $pattern" >&2
return 1
fi
if [ -z "$match" ]; then
echo " (not found: $pattern)"
return
return 3
fi
echo "### $label"
grep -E "$pattern" "$SCHEMA" -A "$max_lines" | \
awk '/^\}$/{print; exit} {print}' | \
head -n "$max_lines"
head -n "$max_lines" || true
echo ""
}
# ── Main loop ────────────────────────────────────────────────────────────────
for ENTITY in "$@"; do
# Validate entity name: must be a valid PascalCase identifier (letters, digits, underscores)
if [[ ! "$ENTITY" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
echo "ERROR: Invalid entity name: '$ENTITY'"
echo " Entity names must start with a letter or underscore, followed by letters, digits, or underscores."
echo " Examples: Account, My_Custom_Object__c"
continue
fi
echo ""
echo "======================================================================"
echo " SCHEMA LOOKUP: $ENTITY"
echo "======================================================================"
echo ""
found=0
# Helper: call extract_block, track matches, surface errors
try_extract() {
local rc=0
extract_block "$@" || rc=$?
if [ "$rc" -eq 0 ]; then
found=$((found + 1))
elif [ "$rc" -eq 1 ]; then
echo " Aborting lookup for '$ENTITY' due to grep error" >&2
fi
# rc=3 is not-found — continue silently (already printed by extract_block)
}
# 1. Type definition — all fields and relationships
extract_block \
try_extract \
"Type definition — fields and relationships" \
"^type ${ENTITY} implements Record" \
200
# 2. Filter input — used in `where:` arguments
extract_block \
try_extract \
"Filter options — use in where: { ... }" \
"^input ${ENTITY}_Filter" \
100
# 3. OrderBy input — used in `orderBy:` arguments
extract_block \
try_extract \
"Sort options — use in orderBy: { ... }" \
"^input ${ENTITY}_OrderBy" \
60
# 4. Create mutation inputs
extract_block \
# 4. Create mutation wrapper
try_extract \
"Create mutation wrapper — ${ENTITY}CreateInput" \
"^input ${ENTITY}CreateInput" \
10
extract_block \
# 5. Create mutation fields
try_extract \
"Create mutation fields — ${ENTITY}CreateRepresentation" \
"^input ${ENTITY}CreateRepresentation" \
100
# 5. Update mutation inputs
extract_block \
# 6. Update mutation wrapper
try_extract \
"Update mutation wrapper — ${ENTITY}UpdateInput" \
"^input ${ENTITY}UpdateInput" \
10
extract_block \
# 7. Update mutation fields
try_extract \
"Update mutation fields — ${ENTITY}UpdateRepresentation" \
"^input ${ENTITY}UpdateRepresentation" \
100
if [ "$found" -eq 0 ]; then
echo "WARNING: No schema entries found for '$ENTITY'."
echo " - Names are PascalCase (e.g., 'Account' not 'account')"
echo " - Custom objects may need deployment first"
fi
echo ""
done