/** * Copyright (c) 2026, Salesforce, Inc., * All rights reserved. * For full license text, see the LICENSE.txt file */ /** * Salesforce UIAPI shape facts as a public library surface. * * The graphiti library below this module is GraphQL-generic — it knows * about sessions, projection nodes, schema walking, and validation, but * not about Salesforce. The MCP server above this module composes * intent-shaped builders (`buildList`, `buildDetail`, etc.) that bake * in product opinions like pagination defaults and mutation return * shapes. * * In between, every consumer that targets Salesforce UIAPI ends up * needing the same handful of facts: where the connection field lives, * how mutation results are nested, when to unwrap a value wrapper, * what the filter / orderBy / input type names look like. This module * collects those facts so the MCP, the CLI, the eval harness, and any * future surface can call into one place instead of re-discovering * each convention. * * What belongs here: * - Path constructors for UIAPI's nested shapes. * - Type-shape detection (value wrappers). * - Naming conventions for filter / orderBy / mutation input types. * * What does NOT belong here: * - Pagination defaults, "always select Id on mutations", "$id: ID!" * conventions — those are MCP-specific opinions. * - Spec types (`ListSpec`, `DetailSpec`, etc.) — those are MCP tool * surface, not graphiti API. * - Schema priming, codegen packaging, warning filtering — those * wrap library output for the MCP's `ToolOutput` and live in MCP. */ import { isInterfaceType, isObjectType, type GraphQLSchema } from "graphql"; import { type OperationType } from "./session.js"; import { resolvePath } from "./walker.js"; // ── Path constructors ───────────────────────────────────────────────────────── /** `["uiapi", "query", ]` — the connection field for an SObject. */ export function connectionPath(object: string): string[] { return ["uiapi", "query", object]; } /** * Given any UIAPI connection-field path, returns the per-record selection * scope inside it (`[...connection, "edges", "node"]`). Works for top-level * connections AND for nested child-relationship connections. */ export function connectionNodePath(connection: string[]): string[] { return [...connection, "edges", "node"]; } /** * `["uiapi", "query", , "edges", "node"]` — the per-record selection * scope for a top-level list query. Composition of `connectionNodePath` + * `connectionPath` for the common case. */ export function nodePath(object: string): string[] { return connectionNodePath(connectionPath(object)); } /** * `[...connection, "pageInfo"]` — the cursor-pagination metadata path on * any UIAPI connection field. Selecting fields here is a caller opinion; * the path itself is a UIAPI fact. */ export function pageInfoPath(connection: string[]): string[] { return [...connection, "pageInfo"]; } /** `["uiapi", ]` — the mutation field at the root of the operation. */ export function mutationFieldPath(object: string, op: "Create" | "Update" | "Delete"): string[] { return ["uiapi", `${object}${op}`]; } /** * `["uiapi", , "Record"]` — the per-record selection scope inside * a mutation result. Delete returns no record, so this only applies to * Create / Update. */ export function mutationRecordPath(object: string, op: "Create" | "Update"): string[] { return [...mutationFieldPath(object, op), "Record"]; } /** `["uiapi", "aggregate", ]` — the aggregate root for an SObject. */ export function aggregatePath(object: string): string[] { return ["uiapi", "aggregate", object]; } // ── Type-shape detection ────────────────────────────────────────────────────── /** * Returns true when the named type is a UIAPI value wrapper — an object * (or interface) type whose name ends in `Value` and that exposes a * `value` field (e.g. `StringValue`, `PicklistValue`, `DateTimeValue`). * * Callers use this to decide whether to select `field` directly or * `field { value }`. The check is permissive enough to catch the full * Salesforce wrapper family without false positives on unrelated * `*Value`-named types — those would also need a `value` field to * satisfy the second condition. */ export function isValueWrapperType(schema: GraphQLSchema, typeName: string): boolean { if (!typeName.endsWith("Value")) return false; const t = schema.getType(typeName); if (!t) return false; if (!isObjectType(t) && !isInterfaceType(t)) return false; return Object.prototype.hasOwnProperty.call(t.getFields(), "value"); } /** * Resolves the path that should actually be selected for a single * (non-dotted) scalar field on `basePath`. Encapsulates two UIAPI * facts in one call: * * - `Id` is a real scalar — return the path as-is. * - `Name`-shaped fields resolve to a value wrapper — append "value". * - Other shapes (raw scalars, leaves) — return the path as-is. * * Returns `null` when the path can't be resolved against the schema, * letting the caller decide whether to throw, warn, or fall back. The * helper deliberately does NOT call `selectLeaf` on the session — it * returns the path so callers stay in control of the projection * (alias handling, error reporting, etc.). * * For dotted paths that may traverse a polymorphic union, callers * should use `selectDottedFieldPath` from `./path-selection.js` * instead — that helper handles the union expansion AND calls into * the equivalent of this resolver internally. */ export function resolveScalarSelectionPath( schema: GraphQLSchema, operation: OperationType, basePath: string[], fieldName: string, ): string[] | null { const fullPath = [...basePath, fieldName]; if (fieldName === "Id") return fullPath; try { const wr = resolvePath(schema, operation, fullPath); if (wr.isLeaf) return fullPath; if (isValueWrapperType(schema, wr.typeName)) return [...fullPath, "value"]; return fullPath; } catch { return null; } } // ── Naming conventions ──────────────────────────────────────────────────────── /** `_Filter` — the connection field's `where` input type name. */ export function filterTypeName(object: string): string { return `${object}_Filter`; } /** `_OrderBy` — the connection field's `orderBy` singleton input type name. */ export function orderByTypeName(object: string): string { return `${object}_OrderBy`; } /** `CreateInput` — the create mutation's `input` argument type name. */ export function createInputTypeName(object: string): string { return `${object}CreateInput`; } /** `UpdateInput` — the update mutation's `input` argument type name. */ export function updateInputTypeName(object: string): string { return `${object}UpdateInput`; }