/** * Copyright (c) 2026, Salesforce, Inc., * All rights reserved. * For full license text, see the LICENSE.txt file */ import { buildOutput } from "./build-output.js"; import { getSchemaWithPriming } from "./get-schema-with-priming.js"; import { selectChildRelationship } from "./select-child-relationship.js"; import { type ListSpec, type ToolOutput } from "./types.js"; import { assertDottedGraphqlName, assertGraphqlName } from "../lib/graphql-name.js"; import { selectDottedFieldPath } from "../lib/path-selection.js"; import { type PrimeDeps } from "../lib/prime-schema.js"; import { addVariable, createSession, deepSetArg, selectLeaf } from "../lib/session.js"; import { normalizeOrderBy, promoteArg, promoteVariables } from "../lib/variable-promotion.js"; /** * Build a UIAPI list query against a Salesforce org. Implements `sf_gql_list` * intent for the graphiti MCP server. * * Implicit behaviors not visible in the signature: * - Scalar fields are auto-wrapped with `{ value }` per UIAPI; `Id` is selected bare. * - Dotted paths in `fields` / `parentFields` expand polymorphic unions into * per-member inline fragments; members lacking the field are silently skipped. * - `childRelationships` render as ` { edges { node { ... } } }` connections. * - `$varName` leaves anywhere in `filter` / `orderBy` / `scope` promote to typed * query variables (inferred from the schema; nullable; `String` on inference miss). * - `orderBy` arrays are collapsed to the first element (UIAPI expects a singleton). * - Cursor pagination is always on: declares `$after: String`, selects * `pageInfo { hasNextPage endCursor }`, defaults `first: 10`. * - Operation name defaults to `List`. * * Throws on invalid `object` or `operationName` (must be valid GraphQL Names), * auth-missing, or introspection failure (via `getSchemaWithPriming`); never * throws on validation or codegen failure (those surface as `warnings[]`). */ export async function buildList(spec: ListSpec, deps?: PrimeDeps): Promise { assertGraphqlName(spec.object, "buildList", "object"); const { schema, primingNote, instanceUrl } = await getSchemaWithPriming(spec.org, deps); const session = createSession(spec.org, "query", instanceUrl); session.operationName = spec.operationName ?? `${spec.object}List`; assertGraphqlName(session.operationName, "buildList", "operationName"); const connectionPath = ["uiapi", "query", spec.object]; const nodePath = [...connectionPath, "edges", "node"]; for (const field of spec.fields) { assertDottedGraphqlName(field, "buildList", "fields entry"); selectDottedFieldPath(session, schema, nodePath, field); } if (spec.parentFields) { for (const pf of spec.parentFields) { assertDottedGraphqlName(pf, "buildList", "parentFields entry"); selectDottedFieldPath(session, schema, nodePath, pf); } } const extraWarnings: string[] = []; // Declare the reserved cursor variable before promoting ANY user variables — // including childRelationship filters — so a filter reusing $after keeps String // (first-wins) and warns, instead of overwriting the pagination arg's type (W-22697670). addVariable(session, "after", "String"); deepSetArg(session, connectionPath, "after", [], "$after"); if (spec.childRelationships) { for (const child of spec.childRelationships) { selectChildRelationship(session, schema, nodePath, child, extraWarnings); } } const first = spec.first ?? 10; const firstRendered = promoteArg(session, schema, connectionPath, "first", first).rendered; deepSetArg(session, connectionPath, "first", [], firstRendered); selectLeaf(session, [...connectionPath, "pageInfo", "hasNextPage"]); selectLeaf(session, [...connectionPath, "pageInfo", "endCursor"]); if (spec.filter) { const { rendered } = promoteArg( session, schema, connectionPath, "where", spec.filter, extraWarnings, ); deepSetArg(session, connectionPath, "where", [], rendered); } const orderBy = normalizeOrderBy(spec.orderBy); if (orderBy) { const { rendered } = promoteArg( session, schema, connectionPath, "orderBy", orderBy, extraWarnings, ); deepSetArg(session, connectionPath, "orderBy", [], rendered); } if (spec.scope) { // `scope` renders into an argument position and is stored verbatim, so an // unconstrained string is a selection-set / argument breakout (W-22735537). // Allow a bare enum token (e.g. MINE) or a $varName; reject anything else. const scopeName = spec.scope.startsWith("$") ? spec.scope.slice(1) : spec.scope; assertGraphqlName(scopeName, "buildList", "scope"); promoteVariables(session, schema, connectionPath, "scope", spec.scope, extraWarnings); deepSetArg(session, connectionPath, "scope", [], spec.scope); } return buildOutput(session, schema, primingNote, extraWarnings); }