afv-library/skills/using-ui-bundle-salesforce-data/references/read-query-generation.md
k-j-kim c611126c2f
Rename webapplication skills to ui-bundle @W-21338965@ (#136)
* Rename webapplication skills to ui-bundle

Rename all 7 webapplication skill directories and their content to use
the "ui-bundle" / "UIBundle" naming convention, aligning with the
New++ (TDX) naming strategy.

Directory renames:
- deploying-webapplication → deploying-ui-bundle
- generating-webapplication-{features,metadata,ui} → generating-ui-bundle-*
- implementing-webapplication-{agentforce-conversation-client,file-upload} → implementing-ui-bundle-*
- using-webapplication-salesforce-data → using-ui-bundle-salesforce-data

Content updates across all skills:
- Frontmatter name fields
- Prose: "web application" → "UI bundle"
- Metadata refs: WebApplication → UIBundle, webapplications/ → uiBundles/
- CLI: sf webapp → sf ui-bundle
- NPM packages per rename table
- Cross-skill references and generating-experience-react-site refs

* Rename generating-ui-bundle-ui skill to building-ui-bundle-frontend

The "ui-bundle-ui" name was confusing. "building-ui-bundle-frontend"
better conveys that this skill handles the React frontend layer
within a UI bundle.

---------

Co-authored-by: gbockus-sf <76090802+gbockus-sf@users.noreply.github.com>
2026-03-30 15:17:38 -05:00

13 KiB
Raw Blame History

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

// 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.
# 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.
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.

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

# 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):

# 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:

# 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:

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

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.