/** * Copyright (c) 2026, Salesforce, Inc., * All rights reserved. * For full license text, see the LICENSE.txt file */ import { CommandError, getSessionSchema, walkerResultAtPath, parsePathInput, resolveAliasSegments, pathPointsToArgs, assignViaPath, assignInArgsContext, assignInVariablesContext, inferTypeFromArgsPath, printQuery, } from "./query-helpers.js"; import { loadSession, saveSession, syncFocusFromNavigationPath, getNavigationContext, isInArgsContext, getArgsFieldPath, getInputSubPath, addVariable, deepSetArg, deepRemoveArg, formatPath, type QuerySession, } from "../lib/session.js"; import { resolvePath } from "../lib/walker.js"; export function queryAssign( sessionId: string, specs: { path: string; value: string }[], setDefault = false, ): void { const session = loadSession(sessionId); syncFocusFromNavigationPath(session); const ctx = getNavigationContext(session.navigationPath); for (const { path: dotPath, value } of specs) { if (pathPointsToArgs(dotPath)) { assignViaPath(session, dotPath, value); console.log(`Assigned ${dotPath} = ${value}`); continue; } const segments = dotPath.includes("/") ? dotPath.split("/") : dotPath.includes(".") ? dotPath.split(".") : [dotPath]; if (ctx === "variables") { assignInVariablesContext(session, segments, value, setDefault); } else if (isInArgsContext(session.navigationPath)) { assignInArgsContext(session, segments, value); } else if (ctx === "query") { const normalizedDotPath = dotPath.replace(/\./g, "/"); assignViaPath(session, `@args/${normalizedDotPath}`, value); const displayPath = segments.join("."); console.log(`Assigned @args/${displayPath} = ${value}`); continue; } else { throw new CommandError( "The `assign` command works inside `@args/` or `/variables/`, or with a path containing `@args/`.\n" + "Example: `assign @args/first 10` or `cd @args` first.", ); } const displayPath = segments.join("."); console.log(`Assigned ${displayPath} = ${value}`); } saveSession(session); console.log(""); printQuery(session); } export function queryUnassign(sessionId: string, rawPath: string): void { const session = loadSession(sessionId); syncFocusFromNavigationPath(session); const normalized = rawPath.replace(/\./g, "/"); const _resolved: string[] = []; let rawResolved = parsePathInput(session.navigationPath, normalized); if (rawResolved.length > 0 && rawResolved[0] !== "query" && rawResolved[0] !== "variables") { rawResolved = ["query", ...rawResolved]; } rawResolved = resolveAliasSegments(session, rawResolved); // Collect all candidate @args resolutions and try each until one succeeds. // This handles both explicit @args paths and shorthand paths like // "uiapi/query/Case/where/Status" → "uiapi/query/Case/@args/where/Status" const candidates: string[][] = []; if (isInArgsContext(rawResolved)) { candidates.push(rawResolved); } // Try injecting @args/ at each possible position const parts = normalized.split("/"); for (let i = parts.length - 1; i >= 1; i--) { const fieldPath = parts.slice(0, i).join("/"); const argPath = parts.slice(i).join("/"); const candidate = parsePathInput(session.navigationPath, `${fieldPath}/@args/${argPath}`); if (candidate.length > 0 && candidate[0] !== "query" && candidate[0] !== "variables") { candidate.unshift("query"); } if (isInArgsContext(candidate)) { candidates.push(resolveAliasSegments(session, candidate)); } } // Fall back: try prepending @args/ to whole path (works when navigated into a field) const withArgs = parsePathInput(session.navigationPath, `@args/${normalized}`); if (isInArgsContext(withArgs)) { candidates.push(withArgs); } if (candidates.length === 0) { throw new CommandError( `Path "${rawPath}" does not point into an @args/ directory. ` + "Use a path like `@args/first` or `unassign first`.", ); } // Try each candidate — use the first one where data actually exists let removed = false; for (const candidate of candidates) { const fieldSchemaPath = getArgsFieldPath(candidate); const inputSubPath = getInputSubPath(candidate); if (inputSubPath.length === 0) continue; const argName = inputSubPath[0]; const inputPath = inputSubPath.slice(1); if (deepRemoveArg(session, fieldSchemaPath, argName, inputPath)) { removed = true; break; } } if (!removed) { throw new CommandError(`No value set at "${rawPath}".`); } saveSession(session); console.log(`Removed ${rawPath}.`); console.log(""); printQuery(session); } export function queryDefine(sessionId: string, rest: string[]): void { const session = loadSession(sessionId); const varName = rest[0]; if (!varName) { throw new CommandError( "Usage: define $name [default]\n e.g. define $filter @args/where", ); } const cleanName = varName.replace(/^\$/, ""); const pathArg = rest[1]; const defaultValue = rest[2]; if (pathArg) { defineAtPath(session, cleanName, pathArg, defaultValue); saveSession(session); console.log(""); printQuery(session); return; } if (isInArgsContext(session.navigationPath)) { defineFromCurrentArgsPosition(session, cleanName); saveSession(session); console.log(""); printQuery(session); return; } const ctx = getNavigationContext(session.navigationPath); if (ctx === "query") { const schemaPath = getArgsFieldPath(session.navigationPath); if (schemaPath.length > 0) { const wr = walkerResultAtPath(session); if (wr.args.length === 0) { throw new CommandError( "This field has no arguments. Provide a path: `define $name @args/`.", ); } } throw new CommandError( "Provide a path: `define $name @args/where` or `define $name Account/@args/first`.", ); } if (ctx === "variables") { throw new CommandError( "Provide a path to an argument: `define $name /query/uiapi/query/Account/@args/where`.", ); } throw new CommandError("Usage: define $name [default]\n e.g. define $filter @args/where"); } /** * Declare a variable, or throw a CommandError if it collides with an existing * declaration of a different type (first-wins). Single-sources the collision * message for the two `define` entry points so they can't drift. The * variable-promotion path intentionally warns instead of throwing and does not * use this helper. */ function addVariableOrThrow( session: QuerySession, cleanName: string, type: string, defaultValue?: string, ): void { const collision = addVariable(session, cleanName, type, defaultValue); if (collision) { throw new CommandError( `$${cleanName} is already declared as ${collision.existingType}; cannot redefine it as ${collision.ignoredType}. Use a different variable name, or \`undo\` the earlier definition first.`, ); } } function defineFromCurrentArgsPosition(session: QuerySession, cleanName: string): void { const schema = getSessionSchema(session); const fieldSchemaPath = getArgsFieldPath(session.navigationPath); const inputSubPath = getInputSubPath(session.navigationPath); if (inputSubPath.length === 0) { throw new CommandError( "At @args/ root. Specify which argument: `define $name first` or `define $name where`.", ); } const { inferredType, argName, argInputPath } = inferTypeFromArgsPath( schema, session.operation, fieldSchemaPath, inputSubPath, ); addVariableOrThrow(session, cleanName, inferredType); deepSetArg(session, fieldSchemaPath, argName, argInputPath, `$${cleanName}`); const pathDesc = inputSubPath.join("."); console.log(`Defined $${cleanName}: ${inferredType}`); console.log(`Auto-assigned: ${pathDesc} = $${cleanName}`); console.log("Tip: use `undo` to remove this variable definition."); } // Exported for the MCP `sf_gql_raw` parser (lib/apply-command.ts), which needs the // pure var-define logic without queryDefine's disk load/save. Already pure — only // the `export` keyword is added. export function defineAtPath( session: QuerySession, cleanName: string, rawPath: string, defaultValue?: string, ): void { const schema = getSessionSchema(session); const normalized = rawPath.includes(".") && !rawPath.includes("/") ? rawPath.replace(/\./g, "/") : rawPath; let resolved = parsePathInput(session.navigationPath, normalized); if (resolved.length > 0 && resolved[0] !== "query" && resolved[0] !== "variables") { resolved = ["query", ...resolved]; } if (!isInArgsContext(resolved)) { // For mutations, auto-inject @args/input if the path points to a mutation field if (session.operation === "mutation") { const withArgsInput = [...resolved, "@args", "input"]; if (isInArgsContext(withArgsInput)) { resolved = withArgsInput; } else { throw new CommandError( `Path "${rawPath}" does not point into an @args/ directory. ` + `Hint: for mutations, use the path with /@args/input appended, e.g. \`${rawPath}/@args/input\`.`, ); } } else { throw new CommandError( `Path "${rawPath}" does not point into an @args/ directory. ` + "Use a path like `@args/where` or `Account/@args/first`.", ); } } const fieldSchemaPath = getArgsFieldPath(resolved); const inputSubPath = getInputSubPath(resolved); if (inputSubPath.length === 0) { throw new CommandError( "Path points to @args/ root. Specify a specific argument: e.g. `@args/where` or `@args/first`.", ); } const wr = resolvePath(schema, session.operation, fieldSchemaPath); if (wr.args.length === 0) { throw new CommandError(`Field at ${formatPath(fieldSchemaPath)} has no arguments.`); } const { inferredType, argName, argInputPath } = inferTypeFromArgsPath( schema, session.operation, fieldSchemaPath, inputSubPath, ); addVariableOrThrow(session, cleanName, inferredType, defaultValue); deepSetArg(session, fieldSchemaPath, argName, argInputPath, `$${cleanName}`); const pathDesc = inputSubPath.join("."); console.log(`Defined $${cleanName}: ${inferredType}${defaultValue ? ` = ${defaultValue}` : ""}`); console.log(`Auto-assigned: ${pathDesc} = $${cleanName}`); console.log("Tip: use `undo` to remove this variable definition."); }