mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-08-01 04:16:17 +08:00
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:
parent
f39ac26c0d
commit
c14da0844e
@ -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>`
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -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
|
||||
@ -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 `<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.
|
||||
@ -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.
|
||||
@ -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
|
||||
94
skills/using-webapp-salesforce-data/graphql-search.sh → skills/using-webapp-salesforce-data/scripts/graphql-search.sh
Normal file → Executable file
94
skills/using-webapp-salesforce-data/graphql-search.sh → skills/using-webapp-salesforce-data/scripts/graphql-search.sh
Normal file → Executable 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
|
||||
Loading…
Reference in New Issue
Block a user