diff --git a/skills/using-webapp-salesforce-data/SKILL.md b/skills/using-webapp-salesforce-data/SKILL.md index cd88681..9a0a4bf 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](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 +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](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 ` | 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 +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 +bash scripts/graphql-search.sh ``` | Script Output Section | Used For | @@ -358,6 +382,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 ` diff --git a/skills/using-webapp-salesforce-data/references/mutation-query-generation.md b/skills/using-webapp-salesforce-data/references/mutation-query-generation.md new file mode 100644 index 0000000..5f0bb6e --- /dev/null +++ b/skills/using-webapp-salesforce-data/references/mutation-query-generation.md @@ -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 + } + } +} +``` diff --git a/skills/using-webapp-salesforce-data/references/query-testing.md b/skills/using-webapp-salesforce-data/references/query-testing.md new file mode 100644 index 0000000..3a441a5 --- /dev/null +++ b/skills/using-webapp-salesforce-data/references/query-testing.md @@ -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 diff --git a/skills/using-webapp-salesforce-data/references/read-query-generation.md b/skills/using-webapp-salesforce-data/references/read-query-generation.md new file mode 100644 index 0000000..5ba0aad --- /dev/null +++ b/skills/using-webapp-salesforce-data/references/read-query-generation.md @@ -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 200–2000** 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 `_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 `__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. diff --git a/skills/using-webapp-salesforce-data/references/schema-introspection.md b/skills/using-webapp-salesforce-data/references/schema-introspection.md new file mode 100644 index 0000000..d5cbf42 --- /dev/null +++ b/skills/using-webapp-salesforce-data/references/schema-introspection.md @@ -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 +# 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 `_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. diff --git a/skills/using-webapp-salesforce-data/references/webapp-integration.md b/skills/using-webapp-salesforce-data/references/webapp-integration.md new file mode 100644 index 0000000..7c1c023 --- /dev/null +++ b/skills/using-webapp-salesforce-data/references/webapp-integration.md @@ -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?.(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; +``` + +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?.( + 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//) +npx eslint +``` + +**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 ` 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//) +npm run graphql:codegen +``` + +Output: `src/api/graphql-operations-types.ts` + +Naming conventions: +- `Query` / `Mutation` — response types +- `QueryVariables` / `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?.(query, variables); +``` + +### Missing Type Definitions + +```typescript +// NOT RECOMMENDED: Untyped GraphQL calls +// PREFERRED: Provide response type +const response = await sdk.graphql?.(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?.()` with proper generic +7. [ ] Handle `response.errors` and destructure `response.data` +8. [ ] Use `NodeOfConnection` for cleaner node types when needed +9. [ ] Run `npx eslint ` 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?.()` with proper generic +6. [ ] Handle `response.errors` and destructure `response.data` +7. [ ] Run `npx eslint ` from webapp dir — fix all GraphQL errors + +### General: + +- [ ] Lint validation passes (`npx eslint ` reports no GraphQL errors) +- [ ] Query field names match the schema exactly (case-sensitive) +- [ ] Response type generic is provided to `sdk.graphql?.()` +- [ ] Optional chaining is used for nested response data diff --git a/skills/using-webapp-salesforce-data/graphql-search.sh b/skills/using-webapp-salesforce-data/scripts/graphql-search.sh old mode 100644 new mode 100755 similarity index 56% rename from skills/using-webapp-salesforce-data/graphql-search.sh rename to skills/using-webapp-salesforce-data/scripts/graphql-search.sh index 28f923c..e11f41e --- a/skills/using-webapp-salesforce-data/graphql-search.sh +++ b/skills/using-webapp-salesforce-data/scripts/graphql-search.sh @@ -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 — _Filter input (for `where:`) -# 3. Sort options — _OrderBy input (for `orderBy:`) -# 4. Create input — CreateRepresentation (for create mutations) -# 5. Update input — UpdateRepresentation (for update mutations) +# 1. Type definition — all fields and relationships +# 2. Filter options — _Filter input (for `where:`) +# 3. Sort options — _OrderBy input (for `orderBy:`) +# 4. Create mutation wrapper — CreateInput +# 5. Create mutation fields — CreateRepresentation (for create mutations) +# 6. Update mutation wrapper — UpdateInput +# 7. Update mutation fields — 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/ && 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