/** * Copyright (c) 2026, Salesforce, Inc., * All rights reserved. * For full license text, see the LICENSE.txt file */ /* eslint-disable @typescript-eslint/no-explicit-any -- graphiti traverses untyped schema/introspection JSON; see follow-up to replace with `unknown` + narrowing */ import type { GraphQLSchema } from "graphql"; import { formatArgsDirectoryListing, formatDirectoryListing, formatInputDirectoryListing, formatVariablesListing, parseSearchTerms, parseSearchRegex, type AliasEntry, type FieldLongInfo, } from "../lib/formatter.js"; import { getCachedObjectInfo } from "../lib/object-info.js"; import { renderQuery } from "../lib/query-builder.js"; import { deepSetArg, deepSetVariableValue, formatPath, getArgsFieldPath, getChildren, getFocusedNodeAtPath, getInputSubPath, getNavigationContext, isAliasedSegment, isArgsSegment, isFragmentSegment, normalizeFragmentSegment, isInArgsContext, listInstancesAtPath, parseVariablePath, pathKey, queryNavToSchemaPath, selectLeaf, setVariableDefault, setVariableRuntimeValue, syncFocusFromNavigationPath, toSchemaPath, type FieldProjectionNode, type OperationType, type ProjectionNode, type QuerySession, } from "../lib/session.js"; import { getRootFields, getSchema, resolveArgByName, resolveFieldOnPath, resolveInputPath, resolvePath, type InputWalkerResult, type WalkerResult, } from "../lib/walker.js"; // Re-export for use by other modules and external consumers. export type { WalkerResult, InputWalkerResult, QuerySession, FieldProjectionNode, ProjectionNode }; // ── CommandError ────────────────────────────────────────────────────────────── export class CommandError extends Error { exitCode: number; constructor(message: string, exitCode = 1) { super(message); this.name = "CommandError"; this.exitCode = exitCode; } } export const EXIT_CODES = { SUCCESS: 0, USER_ERROR: 1, VALIDATION_FAILURE: 2, EXECUTION_FAILURE: 3, AUTH_FAILURE: 4, } as const; // ── Schema helper ───────────────────────────────────────────────────────────── export function getSessionSchema(session: QuerySession): GraphQLSchema { if (!session.instanceUrl) { throw new CommandError( `Session ${session.id} has no instanceUrl. Recreate it with \`graphiti query new \`.`, ); } return getSchema(session.instanceUrl); } // ── Internal helpers ────────────────────────────────────────────────────────── export function walkerResultAtPath( session: QuerySession, navPath = session.navigationPath, ): WalkerResult { const schema = getSessionSchema(session); const schemaPath = queryNavToSchemaPath(navPath); return resolvePath(schema, session.operation, schemaPath); } export function parsePathInput(currentPath: string[], rawPath: string): string[] { if (!rawPath || rawPath === ".") return [...currentPath]; const absolute = rawPath.startsWith("/"); const parts = rawPath.split("/").filter((part) => part.length > 0); const base = absolute ? [] : [...currentPath]; for (const part of parts) { if (part === ".") continue; if (part === "..") { base.pop(); continue; } if (part === "@args" && base.length > 0 && base[base.length - 1] === "@args") { continue; } base.push(normalizeFragmentSegment(part)); } return base; } export function resolveAliasSegments(session: QuerySession, navPath: string[]): string[] { const result = [...navPath]; const queryIdx = result.indexOf("query"); if (queryIdx === -1) return result; let parentId: string | null = null; for (let i = queryIdx + 1; i < result.length; i++) { const seg = result[i]; if (isArgsSegment(seg) || isFragmentSegment(seg) || isAliasedSegment(seg)) break; const children = getChildren(session, parentId); const aliasMatch = children.find( (n): n is FieldProjectionNode => n.kind === "field" && n.alias === seg, ); if (aliasMatch) { result[i] = `${aliasMatch.alias}(${aliasMatch.fieldName})`; const schemaPath = toSchemaPath(result.slice(queryIdx + 1, i + 1)); session.focusByPath[pathKey(schemaPath)] = aliasMatch.id; parentId = aliasMatch.id; continue; } const fieldName = seg; const schemaPath = toSchemaPath(result.slice(queryIdx + 1, i + 1)); const focusedId = session.focusByPath[pathKey(schemaPath)]; const node = children.find((n) => n.id === focusedId) ?? children.find((n) => n.kind === "field" && n.fieldName === fieldName); parentId = node?.id ?? null; } return result; } export function resolveDirectoryPathInSession(session: QuerySession, rawPath: string): string[] { let resolved = parsePathInput(session.navigationPath, rawPath); if (resolved.length === 0) return resolved; if (resolved.length > 0 && resolved[0] !== "query" && resolved[0] !== "variables") { resolved = ["query", ...resolved]; } const ctx = getNavigationContext(resolved); if (ctx === "root") { throw new Error("At root, navigate into `query/` or `variables/`."); } if (ctx === "variables") { return resolveVariablesPath(session, resolved); } resolved = resolveAliasSegments(session, resolved); if (isInArgsContext(resolved)) { return resolveArgsPath(session, resolved); } const schemaPath = queryNavToSchemaPath(resolved); if (schemaPath.length === 0) return resolved; const schema = getSessionSchema(session); const wr = resolvePath(schema, session.operation, schemaPath); if (wr.isLeaf) { const last = resolved[resolved.length - 1]; throw new Error( `Cannot cd into leaf node ${formatPath(resolved)} (${wr.typeName}). Use \`select ${last}\` instead.`, ); } return resolved; } export function resolveArgsPath(session: QuerySession, resolved: string[]): string[] { const schema = getSessionSchema(session); const fieldSchemaPath = getArgsFieldPath(resolved); const inputSubPath = getInputSubPath(resolved); const wr = resolvePath(schema, session.operation, fieldSchemaPath); if (wr.args.length === 0) { throw new Error(`Field at ${formatPath(fieldSchemaPath)} has no arguments.`); } if (inputSubPath.length === 0) return resolved; const argName = inputSubPath[0]; const argInfo = resolveArgByName(schema, wr, argName); const rawTypeName = argInfo.typeName; const remaining = inputSubPath.slice(1); if (remaining.length === 0) { const inputResult = resolveInputPath(schema, rawTypeName, []); if (inputResult.isLeaf && !inputResult.isList) { throw new Error( `Cannot cd into "${argName}" — it is a scalar (${inputResult.typeName}). Use \`assign ${argName} \` instead.`, ); } return resolved; } const inputResult = resolveInputPath(schema, rawTypeName, remaining); if (inputResult.isLeaf && !inputResult.isList) { const last = remaining[remaining.length - 1]; throw new Error( `Cannot cd into "${last}" — it is a scalar (${inputResult.typeName}). Use \`assign\` to set its value.`, ); } return resolved; } export function resolveVariablesPath(session: QuerySession, resolved: string[]): string[] { if (resolved.length === 1) return resolved; const varParsed = parseVariablePath(resolved); if (!varParsed) throw new Error("Invalid variables path."); const variable = session.variables.find((v) => v.name === varParsed.varName); if (!variable) { throw new Error( `Variable "$${varParsed.varName}" is not defined. Use \`define $${varParsed.varName} \` to create it.`, ); } if (varParsed.inputSubPath.length === 0) { const schema = getSessionSchema(session); const rawTypeName = variable.type.replace(/[![\]]/g, ""); const inputResult = resolveInputPath(schema, rawTypeName, []); if (inputResult.isLeaf && !inputResult.isList) { throw new Error( `Cannot cd into "$${varParsed.varName}" — it is a scalar type (${variable.type}). Use \`assign\` to set its value.`, ); } return resolved; } const schema = getSessionSchema(session); const rawTypeName = variable.type.replace(/[![\]]/g, ""); const inputResult = resolveInputPath(schema, rawTypeName, varParsed.inputSubPath); if (inputResult.isLeaf && !inputResult.isList) { const last = varParsed.inputSubPath[varParsed.inputSubPath.length - 1]; throw new Error( `Cannot cd into "${last}" — it is a scalar (${inputResult.typeName}). Use \`assign\` to set its value.`, ); } return resolved; } export function getCurrentInstances(session: QuerySession): ProjectionNode[] { if (session.navigationPath.length === 0) return []; return listInstancesAtPath(session, toSchemaPath(session.navigationPath)); } export function getActiveInstanceId(session: QuerySession): string | undefined { return session.focusByPath[pathKey(toSchemaPath(session.navigationPath))]; } export function buildSelectionInfo(session: QuerySession): { selectedFields: Set; optionalFields: Set; aliases: AliasEntry[]; } { const activeNode = getFocusedNodeAtPath( session, queryNavToSchemaPath(session.navigationPath), false, ); const parentId = activeNode?.id ?? null; const children = getChildren(session, parentId); const selectedFields = new Set(); const optionalFields = new Set(); const aliases: AliasEntry[] = []; for (const child of children) { if (child.kind === "field") { selectedFields.add(child.fieldName); if (child.directives.some((d) => d.name === "optional")) { optionalFields.add(child.fieldName); } if (child.alias) { aliases.push({ alias: child.alias, fieldName: child.fieldName, argCount: Object.keys(child.args).length, isActive: session.focusByPath[pathKey(child.schemaPath)] === child.id, }); } } } return { selectedFields, optionalFields, aliases }; } export function buildFieldLongInfo( session: QuerySession, wr: WalkerResult, ): Map { const map = new Map(); const sObjectName = detectSObjectName(session); const objInfo = sObjectName ? getCachedObjectInfo(session.orgAlias, sObjectName) : null; if (!objInfo) return map; const metaByApi = new Map(objInfo.fields.map((f) => [f.apiName, f])); const relByName = new Map( objInfo.fields.filter((f) => f.relationshipName).map((f) => [f.relationshipName!, f]), ); const childRelByName = new Map( objInfo.childRelationships .filter((cr) => cr.relationshipName) .map((cr) => [cr.relationshipName!, cr]), ); for (const field of wr.fields) { const info: FieldLongInfo = { required: false, createable: true, updateable: true, defaultedOnCreate: false, filterable: true, sortable: true, }; const meta = metaByApi.get(field.name); if (meta) { info.required = meta.required; info.createable = meta.createable; info.updateable = meta.updateable; info.defaultedOnCreate = meta.defaultedOnCreate; info.filterable = meta.filterable; info.sortable = meta.sortable; info.label = meta.label ?? undefined; info.dataType = meta.dataType ?? undefined; info.nameField = meta.nameField || undefined; info.compound = meta.compound || undefined; info.compoundFieldName = meta.compoundFieldName ?? undefined; info.extraTypeInfo = meta.extraTypeInfo ?? undefined; info.calculated = meta.calculated || undefined; info.custom = meta.custom || undefined; info.inlineHelpText = meta.inlineHelpText ?? undefined; if (meta.precision > 0) info.precision = meta.precision; if (meta.scale > 0) info.scale = meta.scale; if (meta.reference && meta.referenceToInfos.length > 0) { info.referenceTargets = meta.referenceToInfos.map((r) => r.apiName); const nameFields = meta.referenceToInfos.flatMap((r) => r.nameFields); if (nameFields.length > 0) info.referenceNameFields = nameFields; } } const relMeta = relByName.get(field.name); if (relMeta && relMeta.reference && relMeta.referenceToInfos.length > 0) { info.referenceTargets = relMeta.referenceToInfos.map((r) => r.apiName); const nameFields = relMeta.referenceToInfos.flatMap((r) => r.nameFields); if (nameFields.length > 0) info.referenceNameFields = nameFields; } const cr = childRelByName.get(field.name); if (cr) { info.childRelTarget = `${cr.childObjectApiName}.${cr.fieldName}`; } const picklist = objInfo.picklists.find((p) => p.apiName === field.name); if (picklist && picklist.values.length > 0) { info.picklistValues = picklist.values .map((v) => v.value) .filter((v): v is string => v !== null); } map.set(field.name, info); } return map; } export function formatAliasContext(session: QuerySession): string | null { if (session.navigationPath.length === 0) return null; const ctx = getNavigationContext(session.navigationPath); if (ctx !== "query" || isInArgsContext(session.navigationPath)) return null; const schemaPath = queryNavToSchemaPath(session.navigationPath); if (schemaPath.length === 0) return null; const instances = listInstancesAtPath(session, schemaPath); const fieldInstances = instances.filter((n): n is FieldProjectionNode => n.kind === "field"); if (fieldInstances.length === 0) return null; const hasAliases = fieldInstances.some((n) => n.alias); if (fieldInstances.length === 1 && !hasAliases) return null; const activeId = session.focusByPath[pathKey(schemaPath)]; if (fieldInstances.length === 1) { const inst = fieldInstances[0]; const argCount = Object.keys(inst.args).length; const argPart = argCount > 0 ? ` [${argCount} arg${argCount === 1 ? "" : "s"}]` : ""; return `Alias: ${inst.alias}${argPart}`; } const parts = fieldInstances.map((inst) => { const isActive = inst.id === activeId || (!activeId && inst === fieldInstances[0]); const name = inst.alias ?? "(unnamed)"; const argCount = Object.keys(inst.args).length; const argPart = argCount > 0 ? ` [${argCount} arg${argCount === 1 ? "" : "s"}]` : ""; return `${isActive ? "* " : " "}${name}${argPart}`; }); return `Aliases: ${parts.join(" | ")}`; } export function detectSObjectName(session: QuerySession, overridePath?: string[]): string | null { const schemaPath = overridePath ?? queryNavToSchemaPath(session.navigationPath); if (session.operation === "mutation") { const idx = schemaPath.indexOf("uiapi"); if (idx !== -1 && schemaPath[idx + 1]) { const match = schemaPath[idx + 1].match(/^(\w+?)(?:Create|Update|Delete)$/); if (match) return match[1]; } } const queryIdx = schemaPath.indexOf("query"); if (queryIdx !== -1 && schemaPath[queryIdx + 1]) { return schemaPath[queryIdx + 1]; } const aggIdx = schemaPath.indexOf("aggregate"); if (aggIdx !== -1 && schemaPath[aggIdx + 1]) { return schemaPath[aggIdx + 1]; } return null; } export function printDirectory( session: QuerySession, opts: { search?: string; regex?: string; long?: boolean; all?: boolean; showFields?: boolean; dataCloud?: boolean; } = {}, ): void { const ctx = getNavigationContext(session.navigationPath); if (session.navigationPath.length === 0 || ctx === "root") { console.log(" query/"); console.log(" variables/"); return; } if (ctx === "variables") { printVariablesDirectory(session, opts); return; } if (isInArgsContext(session.navigationPath)) { printArgsDirectory(session, opts); return; } const schemaPath = queryNavToSchemaPath(session.navigationPath); if (schemaPath.length === 0) { const schema = getSessionSchema(session); let rootFields = getRootFields(schema, session.operation); const rootMatcher = opts.regex ? parseSearchRegex(opts.regex) : opts.search ? parseSearchTerms(opts.search) : undefined; if (rootMatcher) { rootFields = rootFields.filter((f) => rootMatcher.test(f.name)); } for (const f of rootFields) { console.log(` ${f.name}/`); } return; } const walkerResult = walkerResultAtPath(session); const { selectedFields, optionalFields, aliases } = buildSelectionInfo(session); let fieldLongInfo: Map | undefined; if (opts.long) { fieldLongInfo = buildFieldLongInfo(session, walkerResult); } if (opts.showFields !== false) { console.log( formatDirectoryListing(walkerResult, { searchPattern: opts.search, regexPattern: opts.regex, long: opts.long, all: opts.all, hasArgs: walkerResult.args.length > 0, aliases, selectedFields, optionalFields, fieldLongInfo, dataCloud: opts.dataCloud, }), ); } } export function printArgsDirectory( session: QuerySession, opts: { search?: string; regex?: string; long?: boolean; all?: boolean; showFields?: boolean; } = {}, ): void { const schema = getSessionSchema(session); const fieldSchemaPath = getArgsFieldPath(session.navigationPath); const inputSubPath = getInputSubPath(session.navigationPath); const wr = resolvePath(schema, session.operation, fieldSchemaPath); const node = getFocusedNodeAtPath(session, fieldSchemaPath, false); const currentArgs = node && node.kind === "field" ? node.args : {}; if (inputSubPath.length === 0) { console.log( formatArgsDirectoryListing(wr.args, !!opts.long, currentArgs, opts.search, opts.regex), ); return; } const argName = inputSubPath[0]; const argInfo = resolveArgByName(schema, wr, argName); const rawTypeName = argInfo.typeName; const remaining = inputSubPath.slice(1); const inputResult = resolveInputPath(schema, rawTypeName, remaining); let currentValues: Record | undefined; let listElementCount: number | undefined; if (node && node.kind === "field") { const raw = node.args[argName]; if (raw) { try { let parsed = JSON.parse(raw); for (const seg of remaining) { if (parsed === null || parsed === undefined) break; if (/^\d+$/.test(seg)) { parsed = Array.isArray(parsed) ? parsed[Number(seg)] : undefined; } else { parsed = typeof parsed === "object" ? parsed[seg] : undefined; } } if (inputResult.isList && Array.isArray(parsed)) { listElementCount = parsed.length; currentValues = {}; for (let i = 0; i < parsed.length; i++) { currentValues[String(i)] = parsed[i]; } } else if (typeof parsed === "object" && parsed !== null) { currentValues = parsed as Record; } } catch { /* no current values */ } } } console.log( formatInputDirectoryListing( inputResult, !!opts.long, currentValues, listElementCount, opts.search, opts.regex, ), ); } export function printVariablesDirectory( session: QuerySession, opts: { search?: string; regex?: string; long?: boolean; all?: boolean; showFields?: boolean; } = {}, ): void { const varParsed = parseVariablePath(session.navigationPath); if (!varParsed) { console.log(formatVariablesListing(session.variables, !!opts.long)); return; } const variable = session.variables.find((v) => v.name === varParsed.varName); if (!variable) { console.log(`Variable "$${varParsed.varName}" is not defined.`); return; } const schema = getSessionSchema(session); const rawTypeName = variable.type.replace(/[![\]]/g, ""); const inputResult = resolveInputPath(schema, rawTypeName, varParsed.inputSubPath); let currentValues: Record | undefined; let listElementCount: number | undefined; if (variable.runtimeValue) { try { let parsed = JSON.parse(variable.runtimeValue); for (const seg of varParsed.inputSubPath) { if (parsed === null || parsed === undefined) break; if (/^\d+$/.test(seg)) { parsed = Array.isArray(parsed) ? parsed[Number(seg)] : undefined; } else { parsed = typeof parsed === "object" ? parsed[seg] : undefined; } } if (inputResult.isList && Array.isArray(parsed)) { listElementCount = parsed.length; currentValues = {}; for (let i = 0; i < parsed.length; i++) { currentValues[String(i)] = parsed[i]; } } else if (typeof parsed === "object" && parsed !== null) { currentValues = parsed as Record; } } catch { /* no current values */ } } console.log( formatInputDirectoryListing( inputResult, !!opts.long, currentValues, listElementCount, opts.search, opts.regex, ), ); } export function printQuery(session: QuerySession): void { console.log("Query:"); console.log(renderQuery(session)); } export function requireFieldDirectory(session: QuerySession): string[] { const ctx = getNavigationContext(session.navigationPath); if (ctx !== "query") { throw new Error("This command requires a query field directory. Navigate into query/ first."); } const schemaPath = queryNavToSchemaPath(session.navigationPath); if (schemaPath.length === 0) { throw new Error( "You are at the root of the query tree. Navigate into a field first (e.g. `cd uiapi`).", ); } const last = session.navigationPath[session.navigationPath.length - 1]; if (isFragmentSegment(last)) { throw new Error( "The current directory is an inline fragment. Arguments and aliases apply to field directories only.", ); } return schemaPath; } export function dotPathToSlashPath(dotPath: string): string { const segments: string[] = []; let i = 0; while (i < dotPath.length) { if (dotPath[i] === ".") { i++; continue; } if (dotPath[i] === "[") { const close = dotPath.indexOf("]", i); if (close === -1) { segments.push(dotPath.slice(i)); break; } segments.push(normalizeFragmentSegment(dotPath.slice(i, close + 1))); i = close + 1; } else { let end = dotPath.indexOf(".", i); if (end === -1) end = dotPath.length; segments.push(normalizeFragmentSegment(dotPath.slice(i, end))); i = end; } } return segments.join("/"); } export function selectLeafInSession(session: QuerySession, leafSpec: string, alias?: string): void { syncFocusFromNavigationPath(session); const schema = getSessionSchema(session); let normalizedSpec: string; if (leafSpec.includes(".") && !leafSpec.includes("/")) { normalizedSpec = dotPathToSlashPath(leafSpec); } else if (leafSpec.includes("/") && leafSpec.includes(".")) { const parts = leafSpec.split("/"); const lastPart = parts.pop()!; if (lastPart.includes(".")) { const dotParts = dotPathToSlashPath(lastPart); normalizedSpec = [...parts, ...dotParts.split("/")].join("/"); } else { normalizedSpec = leafSpec; } } else { normalizedSpec = leafSpec; } if ( normalizedSpec.includes("/") && !normalizedSpec.startsWith("/") && !normalizedSpec.startsWith("query/") && !normalizedSpec.startsWith("variables/") && getNavigationContext(session.navigationPath) === "root" ) { const firstSeg = normalizedSpec.split("/")[0]; const rootFields = getRootFields(schema, session.operation); if (rootFields.some((f) => f.name === firstSeg)) { normalizedSpec = `query/${normalizedSpec}`; } } if (normalizedSpec.includes("/")) { const parts = normalizedSpec.split("/"); const leafName = parts.pop()!; const dirPath = parts.join("/"); let resolved = parsePathInput(session.navigationPath, dirPath); resolved = resolveAliasSegments(session, resolved); const schemaPath = getNavigationContext(resolved) === "query" ? queryNavToSchemaPath(resolved) : toSchemaPath(resolved); let fieldResult: WalkerResult; try { fieldResult = resolveFieldOnPath(schema, session.operation, schemaPath, leafName); } catch (error: any) { if ((leafName === "value" || leafName === "displayValue") && parts.length >= 1) { try { const dirResult = resolvePath(schema, session.operation, schemaPath); if (dirResult.isLeaf) { const scalarFieldName = schemaPath[schemaPath.length - 1]; const parentSchemaPath = schemaPath.slice(0, -1); selectLeaf(session, [...parentSchemaPath, scalarFieldName], alias); console.log( ` (auto-corrected: ${scalarFieldName} is a ${dirResult.typeName} scalar — selected directly without .${leafName})`, ); return; } } catch { /* fall through to original error */ } } // Auto-inject Record/ for mutation payloads: when a field isn't found on a Payload type, // find the Payload ancestor in the schemaPath and inject Record/ after it if (session.operation === "mutation" && error.message?.includes("Payload")) { try { // Walk the schemaPath to find where the Payload type is for (let pi = 1; pi <= schemaPath.length; pi++) { const candidatePath = schemaPath.slice(0, pi); let parentResult: WalkerResult; try { parentResult = resolvePath(schema, session.operation, candidatePath); } catch { continue; } if ( parentResult.typeName.endsWith("Payload") && parentResult.fields.some((f) => f.name === "Record") ) { // Reconstruct path with Record/ injected after the Payload const afterPayload = schemaPath.slice(pi); const newSchemaPath = [...candidatePath, "Record", ...afterPayload]; const recordResult = resolveFieldOnPath( schema, session.operation, newSchemaPath, leafName, ); if (recordResult.isLeaf) { selectLeaf(session, [...newSchemaPath, leafName], alias); console.log( ` (auto-injected Record/: path adjusted to include Record/ inside ${parentResult.typeName})`, ); return; } if ( recordResult.typeName.endsWith("Value") && recordResult.fields.some((f) => f.name === "value") ) { selectLeaf(session, [...newSchemaPath, leafName, "value"], alias); console.log( ` (auto-injected Record/.../${leafName}/value inside ${parentResult.typeName})`, ); return; } break; } } } catch { /* fall through to original error */ } } if (session.navigationPath.length < 4 && parts[0] === "edges") { throw new Error( `${error.message}\nHint: "edges" is part of a connection type. Navigate to the connection first with \`cd query/uiapi/query/\`, then select fields.`, ); } throw error; } if (!fieldResult.isLeaf) { const leaves = fieldResult.fields .filter((f) => f.typeKind === "SCALAR" || f.typeKind === "ENUM") .map((f) => f.name); if (fieldResult.typeName.endsWith("Value") && leaves.includes("value")) { selectLeaf(session, [...schemaPath, leafName, "value"], alias); console.log( ` (auto-expanded: ${leafName} is a ${fieldResult.typeName} wrapper — selected ${leafName}/value)`, ); return; } const leafHint = leaves.length > 0 ? ` Available leaves: ${leaves.join(", ")}` : ""; throw new Error( `Cannot select "${leafName}" from ${formatPath(schemaPath)} because it resolves to ${fieldResult.typeName}. Navigate into it with \`cd ${normalizedSpec.replace(/\/[^/]+$/, "")}\` and select one of its leaf children.${leafHint}`, ); } selectLeaf(session, [...schemaPath, leafName], alias); return; } const schemaPath = queryNavToSchemaPath(session.navigationPath); const fieldResult = resolveFieldOnPath(schema, session.operation, schemaPath, normalizedSpec); if (!fieldResult.isLeaf) { const leaves = fieldResult.fields .filter((f) => f.typeKind === "SCALAR" || f.typeKind === "ENUM") .map((f) => f.name); if (fieldResult.typeName.endsWith("Value") && leaves.includes("value")) { selectLeaf(session, [...schemaPath, normalizedSpec, "value"], alias); console.log( ` (auto-expanded: ${normalizedSpec} is a ${fieldResult.typeName} wrapper — selected ${normalizedSpec}/value)`, ); return; } const leafHint = leaves.length > 0 ? ` Available leaves: ${leaves.join(", ")}` : ""; throw new Error( `Cannot select "${normalizedSpec}" from ${formatPath(schemaPath)} because it resolves to ${fieldResult.typeName}. Navigate into it with \`cd ${normalizedSpec}\` and select one of its leaf children.${leafHint}`, ); } selectLeaf(session, [...schemaPath, normalizedSpec], alias); } // ── Assignment / validation helpers ────────────────────────────────────────── export function pathPointsToArgs(p: string): boolean { return p.includes("@args") || (p.includes("/") && p.split("/").some((s) => s === "@args")); } export function resolveArgType( session: QuerySession, fieldSchemaPath: string[], argName: string, inputPath: string[], ): { expectedType: string; argTypeKind: string } { const schema = getSessionSchema(session); const wr = resolvePath(schema, session.operation, fieldSchemaPath); const argInfo = resolveArgByName(schema, wr, argName); if (inputPath.length > 0) { const inputResult = resolveInputPath(schema, argInfo.typeName, inputPath); return { expectedType: inputResult.typeName, argTypeKind: inputResult.kind }; } return { expectedType: argInfo.typeName, argTypeKind: argInfo.typeKind }; } export function validateVariableAssignment( session: QuerySession, fieldSchemaPath: string[], argName: string, inputPath: string[], varRef: string, ): void { const cleanName = varRef.replace(/^\$/, ""); const variable = session.variables.find((v) => v.name === cleanName); if (!variable) { throw new CommandError( `Variable "$${cleanName}" is not defined. Define it first with \`define $${cleanName} \`.`, ); } const { expectedType } = resolveArgType(session, fieldSchemaPath, argName, inputPath); const varBaseType = variable.type.replace(/[![\]]/g, ""); if (varBaseType !== expectedType) { throw new CommandError( `Type mismatch: $${cleanName} is ${variable.type}, but ${argName}${inputPath.length > 0 ? "." + inputPath.join(".") : ""} expects ${expectedType}.`, ); } } export function validateLiteralAssignment( session: QuerySession, fieldSchemaPath: string[], argName: string, inputPath: string[], value: string, contextHint: string, ): void { const schema = getSessionSchema(session); const wr = resolvePath(schema, session.operation, fieldSchemaPath); const argInfo = resolveArgByName(schema, wr, argName); const rawTypeName = argInfo.typeName; const isJsonLiteral = /^\s*[{[]/.test(value); if (inputPath.length > 0) { const inputResult = resolveInputPath(schema, rawTypeName, inputPath); if (!inputResult.isLeaf && !isJsonLiteral) { throw new CommandError( `Cannot assign to "${inputPath[inputPath.length - 1]}" — it is ${inputResult.typeName} (${inputResult.kind}). ` + contextHint, ); } } else if (argInfo.typeKind !== "SCALAR" && argInfo.typeKind !== "ENUM" && !isJsonLiteral) { throw new CommandError( `Cannot assign a literal to "${argName}" — it is ${rawTypeName} (${argInfo.typeKind}). ` + contextHint, ); } if (isJsonLiteral) { const sanitized = value.replace(/\$[a-zA-Z_]\w*/g, '"__var_placeholder__"'); try { JSON.parse(sanitized); } catch { throw new CommandError( `Invalid JSON for "${argName}": ${value.slice(0, 60)}${value.length > 60 ? "..." : ""}`, ); } } } export function assignViaPath(session: QuerySession, rawPath: string, value: string): void { const normalized = rawPath.replace(/\./g, "/"); let resolved = parsePathInput(session.navigationPath, normalized); resolved = resolveAliasSegments(session, resolved); if (!isInArgsContext(resolved)) { throw new CommandError( `Path "${rawPath}" does not point into an @args/ directory. ` + "Use a path like `@args/first` or `Account/@args/where`.", ); } 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/first` or `@args/where`.", ); } const argName = inputSubPath[0]; const inputPath = inputSubPath.slice(1); if (value.startsWith("$")) { validateVariableAssignment(session, fieldSchemaPath, argName, inputPath, value); } else { validateLiteralAssignment( session, fieldSchemaPath, argName, inputPath, value, `Assign to a deeper path (e.g. \`@args/${argName}/...\`) or assign a $variable reference.`, ); } deepSetArg(session, fieldSchemaPath, argName, inputPath, value); emitObjectInfoWarnings(session, fieldSchemaPath, argName, inputPath, value); } export function emitObjectInfoWarnings( session: QuerySession, fieldSchemaPath: string[], argName: string, inputPath: string[], value: string, ): void { const sObjectName = detectSObjectName(session, fieldSchemaPath); if (!sObjectName) return; const objInfo = getCachedObjectInfo(session.orgAlias, sObjectName); if (!objInfo) return; const metaByApi = new Map(objInfo.fields.map((f) => [f.apiName, f])); if ((argName === "where" || argName === "orderBy") && /^\s*[{[]/.test(value)) { try { const parsed = JSON.parse(value); const fieldNames = typeof parsed === "object" && parsed !== null ? Object.keys(parsed) : []; for (const fn of fieldNames) { if (fn === "and" || fn === "or" || fn === "not") continue; const meta = metaByApi.get(fn); if (!meta) continue; if (argName === "where" && !meta.filterable) { console.log( ` Warning: ${fn} is not filterable per ObjectInfo. This where clause may fail at runtime.`, ); } if (argName === "orderBy" && !meta.sortable) { console.log( ` Warning: ${fn} is not sortable per ObjectInfo. This orderBy clause may fail at runtime.`, ); } } } catch { /* not valid JSON — skip */ } return; } const fieldName = inputPath.length > 0 ? inputPath[0] : argName; const isInWhere = argName === "where" || inputPath.includes("where"); const isInOrderBy = argName === "orderBy" || inputPath.includes("orderBy"); const meta = metaByApi.get(fieldName); if (meta) { if (isInWhere && !meta.filterable) { console.log( ` Warning: ${fieldName} is not filterable per ObjectInfo. This where clause may fail at runtime.`, ); } if (isInOrderBy && !meta.sortable) { console.log( ` Warning: ${fieldName} is not sortable per ObjectInfo. This orderBy clause may fail at runtime.`, ); } // Create-only field guard: warn when assigning a create-only field in an update mutation context const isMutationUpdate = session.operation === "mutation" && fieldSchemaPath.some((s) => /Update$/.test(s)); if (isMutationUpdate && meta.createable && !meta.updateable) { console.log( ` Warning: ${fieldName} is create-only (not updateable). Setting it in an update mutation will fail at runtime.`, ); } } if (!value.startsWith("$") && !value.startsWith("{") && !value.startsWith("[")) { const picklist = objInfo.picklists.find((p) => p.apiName === fieldName); if (picklist && picklist.values.length > 0) { const cleanValue = value.replace(/^"|"$/g, ""); const validValues = picklist.values .map((v) => v.value) .filter((v): v is string => v !== null); if (validValues.length > 0 && !validValues.includes(cleanValue)) { console.log(` Warning: "${cleanValue}" is not a known picklist value for ${fieldName}.`); console.log(` Valid values: ${validValues.join(", ")}`); } } } } export function assignInArgsContext( session: QuerySession, segments: string[], value: string, ): void { const fieldSchemaPath = getArgsFieldPath(session.navigationPath); const currentInputSub = getInputSubPath(session.navigationPath); const fullInputPath = [...currentInputSub, ...segments]; if (fullInputPath.length === 0) { throw new CommandError("Usage: assign "); } const argName = fullInputPath[0]; const inputPath = fullInputPath.slice(1); if (value.startsWith("$")) { validateVariableAssignment(session, fieldSchemaPath, argName, inputPath, value); } else { validateLiteralAssignment( session, fieldSchemaPath, argName, inputPath, value, `Navigate into it with \`cd\` and assign its leaf fields, or assign a $variable reference.`, ); } deepSetArg(session, fieldSchemaPath, argName, inputPath, value); emitObjectInfoWarnings(session, fieldSchemaPath, argName, inputPath, value); } export function assignInVariablesContext( session: QuerySession, segments: string[], value: string, setDefault = false, ): void { const varParsed = parseVariablePath(session.navigationPath); if (!varParsed) { if (segments.length === 1 && segments[0].startsWith("$")) { const cleanName = segments[0].replace(/^\$/, ""); const variable = session.variables.find((v) => v.name === cleanName); if (!variable) throw new CommandError(`Variable "$${cleanName}" is not defined.`); if (setDefault) { setVariableDefault(session, cleanName, value); console.log(`Set default for $${cleanName} = ${value}`); } else { setVariableRuntimeValue(session, cleanName, value); } return; } throw new CommandError( "Navigate into a variable first (e.g. `cd $myVar`) or specify: `assign [--default] $varName `.", ); } if (setDefault) { throw new CommandError( "--default can only be used on a whole variable (e.g. `assign --default $varName `), not on nested paths.", ); } const fullPath = [...varParsed.inputSubPath, ...segments]; deepSetVariableValue(session, varParsed.varName, fullPath, value); } export function inferTypeFromArgsPath( schema: GraphQLSchema, operation: OperationType, fieldSchemaPath: string[], inputSubPath: string[], ): { inferredType: string; argName: string; argInputPath: string[] } { const argName = inputSubPath[0]; const wr = resolvePath(schema, operation, fieldSchemaPath); const argInfo = resolveArgByName(schema, wr, argName); let inferredType: string; if (inputSubPath.length === 1) { inferredType = argInfo.typeName; if (argInfo.isList) inferredType = `[${inferredType}]`; if (argInfo.isNonNull) inferredType += "!"; } else { const nestedPath = inputSubPath.slice(1); const inputResult = resolveInputPath(schema, argInfo.typeName, nestedPath); inferredType = inputResult.typeName; if (inputResult.isList) inferredType = `[${inferredType}]`; if (inputResult.isNonNull) inferredType += "!"; } return { inferredType, argName, argInputPath: inputSubPath.slice(1) }; }