afv-library/skills/using-ui-bundle-salesforce-data/references/read-query-generation.md

308 lines
13 KiB
Markdown
Raw Normal View History

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