/** * 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 { type CreateSpec, type ToolOutput, type UpdateSpec } 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 } from "../lib/session.js"; import { createInputTypeName, mutationFieldPath, mutationRecordPath, updateInputTypeName, } from "../lib/uiapi.js"; export type MutationOp = "Create" | "Update"; /** * Build a UIAPI Create/Update mutation against a Salesforce org. Shared * implementation for `sf_gql_create` and `sf_gql_update` (FR-5.6). * * Implicit behaviors: * - `returnFields` defaults to `["Id"]`. * - `inputVariable` defaults to `"input"`; a leading `$` is stripped. * - Operation name defaults to `` (e.g. `CreateAccount`, `UpdateAccount`). * - The `input` argument on the mutation field is bound to the declared variable. * * Throws on invalid `object`/`inputVariable`/`operationName` GraphQL Names, empty * `returnFields`, auth-missing, or introspection failure. Never throws on * validation/codegen failure (those surface as `warnings[]`). */ export async function buildMutation( spec: CreateSpec | UpdateSpec, op: MutationOp, deps?: PrimeDeps, ): Promise { assertGraphqlName(spec.object, "buildMutation", "object"); const inputVar = (spec.inputVariable ?? "input").replace(/^\$/, ""); assertGraphqlName(inputVar, "buildMutation", "inputVariable"); if (spec.returnFields !== undefined && spec.returnFields.length === 0) { throw new Error("buildMutation: returnFields must contain at least one field"); } const { schema, primingNote, instanceUrl } = await getSchemaWithPriming(spec.org, deps); const session = createSession(spec.org, "mutation", instanceUrl); session.operationName = spec.operationName ?? op + spec.object; assertGraphqlName(session.operationName, "buildMutation", "operationName"); const fieldPath = mutationFieldPath(spec.object, op); const recordPath = mutationRecordPath(spec.object, op); const inputTypeName = op === "Create" ? createInputTypeName(spec.object) : updateInputTypeName(spec.object); addVariable(session, inputVar, inputTypeName + "!"); deepSetArg(session, fieldPath, "input", [], "$" + inputVar); const extraWarnings: string[] = []; for (const field of spec.returnFields ?? ["Id"]) { // Validate the charset BEFORE the warn-swallowing try below, so a // selection-set breakout (W-22735537) hard-fails instead of being demoted // to a non-gating warning. A legit dot-path passes here and is then handled // (warn-and-skip) by selectDottedFieldPath as an unsupported mutation-result path. assertDottedGraphqlName(field, "buildMutation", "returnFields entry"); try { selectDottedFieldPath(session, schema, recordPath, field); } catch (err) { const msg = err instanceof Error ? err.message : String(err); extraWarnings.push(`returnFields: ${msg}`); } } return buildOutput(session, schema, primingNote, extraWarnings); }