/** * Copyright (c) 2026, Salesforce, Inc., * All rights reserved. * For full license text, see the LICENSE.txt file */ import { type GraphQLSchema, isInputObjectType } from "graphql"; import { getOrgAuth } from "../lib/auth.js"; import { formatTypeInfo, type TypeAnnotations } from "../lib/formatter.js"; import { getObjectInfo, getCachedObjectInfo, type ObjectInfoResult, type FieldMetadata, } from "../lib/object-info.js"; import { getSchema, inspectType, type TypeInfo } from "../lib/walker.js"; export async function typeCommand(orgAlias: string, typeName: string): Promise { const auth = await getOrgAuth(orgAlias); const schema = getSchema(auth.instanceUrl); const info = inspectType(schema, typeName); const annotations = await buildAnnotations(schema, orgAlias, typeName, info); console.log(formatTypeInfo(info, annotations)); } // ── Annotation builders ─────────────────────────────────────────────────────── async function buildAnnotations( schema: GraphQLSchema, orgAlias: string, typeName: string, info: TypeInfo, ): Promise { const ann: TypeAnnotations = {}; switch (info.kind) { case "OBJECT": case "INTERFACE": annotateObjectFields(schema, info, ann); await annotateObjectWithObjectInfo(schema, orgAlias, typeName, info, ann); break; case "INPUT_OBJECT": await annotateInputObject(schema, orgAlias, typeName, info, ann); break; } return ann; } /** * For OBJECT/INTERFACE types: annotate fields with wrapper-type leaf fields * (e.g. StringValue → { value, displayValue, label }) and child relationship args. */ function annotateObjectFields(schema: GraphQLSchema, info: TypeInfo, ann: TypeAnnotations): void { ann.fieldComments = ann.fieldComments ?? new Map(); for (const field of info.fields) { const comments: string[] = []; if (field.args.length > 0) { const argSummary = field.args .map((a) => { if (a.enumValues) return `${a.name}: ${a.enumValues.join(" | ")}`; return `${a.name}: ${a.typeName}`; }) .join(", "); comments.push(`child: ${argSummary}`); } if (comments.length > 0) { ann.fieldComments.set(field.name, comments); } } } /** * Enrich OBJECT types with ObjectInfo data: parent references, child * relationship targets, picklist values. */ async function annotateObjectWithObjectInfo( schema: GraphQLSchema, orgAlias: string, typeName: string, info: TypeInfo, ann: TypeAnnotations, ): Promise { const sObjectName = detectSObjectFromObjectType(typeName); if (!sObjectName) return; const objInfo = await fetchObjectInfoQuietly(orgAlias, sObjectName); if (!objInfo) return; ann.fieldComments = ann.fieldComments ?? new Map(); const fieldMap = new Map(objInfo.fields.map((f) => [f.apiName, f])); const relNameMap = new Map( objInfo.fields.filter((f) => f.relationshipName).map((f) => [f.relationshipName!, f]), ); for (const field of info.fields) { const existing = ann.fieldComments.get(field.name) ?? []; const meta = fieldMap.get(field.name); if (meta) { if (meta.reference && meta.referenceToInfos.length > 0) { const targets = meta.referenceToInfos.map((r) => r.apiName).join(", "); existing.unshift(`-> ${targets}`); } addPicklistComment(objInfo, meta.apiName, existing); } const relMeta = relNameMap.get(field.name); if (relMeta && relMeta.reference && relMeta.referenceToInfos.length > 0) { const targets = relMeta.referenceToInfos.map((r) => r.apiName).join(", "); if (!existing.some((c) => c.startsWith("->"))) { existing.unshift(`-> ${targets}`); } } if (existing.length > 0) { ann.fieldComments.set(field.name, existing); } } const childRelMap = new Map( objInfo.childRelationships .filter((cr) => cr.relationshipName) .map((cr) => [cr.relationshipName!, cr]), ); for (const field of info.fields) { const cr = childRelMap.get(field.name); if (cr) { const existing = ann.fieldComments.get(field.name) ?? []; if (!existing.some((c) => c.startsWith("child:"))) { existing.unshift(`child -> ${cr.childObjectApiName}.${cr.fieldName}`); } ann.fieldComments.set(field.name, existing); } } } /** * For INPUT_OBJECT types: detect whether it's a mutation input, filter, or * orderBy and annotate accordingly. */ async function annotateInputObject( schema: GraphQLSchema, orgAlias: string, typeName: string, info: TypeInfo, ann: TypeAnnotations, ): Promise { ann.fieldComments = ann.fieldComments ?? new Map(); if (isFilterType(typeName)) { annotateFilterFields(schema, info, ann); ann.filterExamples = buildFilterExamples(typeName, info, schema); return; } if (isOrderByType(typeName)) { annotateOrderByFields(schema, info, ann); return; } const mutationMatch = typeName.match(/^(\w+?)(Create|Update)(?:Input|Representation)$/); if (mutationMatch) { await annotateMutationInput( schema, orgAlias, typeName, info, ann, mutationMatch[1], mutationMatch[2], ); } } // ── Filter annotation ───────────────────────────────────────────────────────── function annotateFilterFields(schema: GraphQLSchema, info: TypeInfo, ann: TypeAnnotations): void { for (const field of info.inputFields) { const comments: string[] = []; const namedType = schema.getType(stripWrapping(field.typeName)); if (namedType && isInputObjectType(namedType)) { if (field.typeName.endsWith("Operators")) { const ops = Object.keys(namedType.getFields()).join(", "); comments.push(`${namedType.name}: ${ops}`); } else if (field.name === "and" || field.name === "or" || field.name === "not") { // combinators — no extra comment needed } else { comments.push("nested filter"); } } if (comments.length > 0) { ann.fieldComments!.set(field.name, comments); } } } function buildFilterExamples(typeName: string, info: TypeInfo, schema: GraphQLSchema): string[] { const examples: string[] = []; const _sObject = typeName.replace(/_Filter$/, ""); const simpleField = info.inputFields.find( (f) => f.typeName.endsWith("Operators") && !["and", "or", "not"].includes(f.name), ); if (simpleField) { const namedType = schema.getType(stripWrapping(simpleField.typeName)); if (namedType && isInputObjectType(namedType)) { const ops = Object.keys(namedType.getFields()); const op = ops.includes("gt") ? "gt" : ops.includes("eq") ? "eq" : ops[0]; examples.push(`{ "${simpleField.name}": { "${op}": } }`); } } const twoFilterable = info.inputFields .filter((f) => f.typeName.endsWith("Operators") && !["and", "or", "not"].includes(f.name)) .slice(0, 2); if (twoFilterable.length === 2) { examples.push( `{ "and": [{ "${twoFilterable[0].name}": { "eq": } }, { "${twoFilterable[1].name}": { "gt": } }] }`, ); } return examples; } // ── OrderBy annotation ──────────────────────────────────────────────────────── function annotateOrderByFields(schema: GraphQLSchema, info: TypeInfo, ann: TypeAnnotations): void { let shownClause = false; for (const field of info.inputFields) { const comments: string[] = []; const namedType = schema.getType(stripWrapping(field.typeName)); if (namedType && isInputObjectType(namedType)) { const _fieldNames = Object.keys(namedType.getFields()); if (!shownClause && field.typeName === "OrderByClause") { comments.push(`{ order: ASC | DESC, nulls: FIRST | LAST }`); shownClause = true; } else if (field.typeName !== "OrderByClause") { comments.push(`nested: ${namedType.name}`); } } if (comments.length > 0) { ann.fieldComments!.set(field.name, comments); } } } // ── Mutation input annotation ───────────────────────────────────────────────── async function annotateMutationInput( schema: GraphQLSchema, orgAlias: string, typeName: string, info: TypeInfo, ann: TypeAnnotations, sObjectName: string, _operation: string, ): Promise { const objInfo = await fetchObjectInfoQuietly(orgAlias, sObjectName); if (objInfo) { const fieldMap = new Map(objInfo.fields.map((f) => [f.apiName, f])); for (const field of info.inputFields) { const comments: string[] = []; const meta = fieldMap.get(field.name); if (meta) { addMutationFieldTags(meta, comments); addPicklistComment(objInfo, meta.apiName, comments); if (meta.reference && meta.referenceToInfos.length > 0) { const targets = meta.referenceToInfos.map((r) => r.apiName).join(", "); comments.push(`-> ${targets}`); } } if (comments.length > 0) { ann.fieldComments!.set(field.name, comments); } } } const nestedInput = info.inputFields.find((f) => { const t = schema.getType(stripWrapping(f.typeName)); return t && isInputObjectType(t); }); if (nestedInput) { const nestedType = schema.getType(stripWrapping(nestedInput.typeName)); if (nestedType && isInputObjectType(nestedType)) { const nestedInfo = inspectType(schema, nestedType.name); ann.inlinedInputs = ann.inlinedInputs ?? []; const nestedAnn: TypeAnnotations = { fieldComments: new Map() }; if (objInfo) { const fieldMap = new Map(objInfo.fields.map((f) => [f.apiName, f])); for (const field of nestedInfo.inputFields) { const comments: string[] = []; const meta = fieldMap.get(field.name); if (meta) { addMutationFieldTags(meta, comments); addPicklistComment(objInfo, meta.apiName, comments); if (meta.reference && meta.referenceToInfos.length > 0) { const targets = meta.referenceToInfos.map((r) => r.apiName).join(", "); comments.push(`-> ${targets}`); } } if (comments.length > 0) { nestedAnn.fieldComments!.set(field.name, comments); } } nestedAnn.sampleInput = buildSampleInput(sObjectName, objInfo, nestedInfo); } ann.inlinedInputs.push({ info: nestedInfo, annotations: nestedAnn }); } } } function addMutationFieldTags(meta: FieldMetadata, comments: string[]): void { const tags: string[] = []; if (meta.required && meta.createable && !meta.defaultedOnCreate) tags.push("required"); if (meta.defaultedOnCreate) tags.push("has default"); if (!meta.createable && !meta.updateable) tags.push("read-only"); if (tags.length > 0) comments.push(tags.join(" | ")); } function buildSampleInput( sObjectName: string, objInfo: ObjectInfoResult, nestedInfo: TypeInfo, ): string { const requiredFields = objInfo.fields.filter( (f) => f.required && f.createable && !f.defaultedOnCreate, ); const nestedFieldNames = new Set(nestedInfo.inputFields.map((f) => f.name)); const relevant = requiredFields.filter((f) => nestedFieldNames.has(f.apiName)); const entries: Record = {}; for (const f of relevant) { const picklist = objInfo.picklists.find((p) => p.apiName === f.apiName); if (picklist && picklist.values.length > 0) { entries[f.apiName] = JSON.stringify(picklist.values[0].value); } else if (f.dataType === "STRING" || f.dataType === "TEXTAREA") { entries[f.apiName] = `"<${f.label ?? f.apiName}>"`; } else if ( f.dataType === "CURRENCY" || f.dataType === "DOUBLE" || f.dataType === "INT" || f.dataType === "PERCENT" ) { entries[f.apiName] = "0"; } else if (f.dataType === "DATE") { entries[f.apiName] = `"${new Date().toISOString().split("T")[0]}"`; } else if (f.dataType === "DATETIME") { entries[f.apiName] = `"${new Date().toISOString()}"`; } else if (f.dataType === "BOOLEAN") { entries[f.apiName] = "false"; } else if (f.dataType === "REFERENCE") { entries[f.apiName] = '""'; } else { entries[f.apiName] = `"<${f.apiName}>"`; } } const inner = Object.entries(entries) .map(([k, v]) => ` ${JSON.stringify(k)}: ${v}`) .join(",\n"); return `{ "${sObjectName}": {\n${inner}\n } }`; } // ── Helpers ─────────────────────────────────────────────────────────────────── function addPicklistComment( objInfo: ObjectInfoResult, fieldName: string, comments: string[], ): void { const picklist = objInfo.picklists.find((p) => p.apiName === fieldName); if (picklist && picklist.values.length > 0) { const vals = picklist.values.map((v) => v.value).join(", "); comments.push(`picklist: [${vals}]`); } } function stripWrapping(typeName: string): string { return typeName.replace(/[[\]!]/g, ""); } function isFilterType(name: string): boolean { return /_Filter$/.test(name); } function isOrderByType(name: string): boolean { return /_OrderBy$/.test(name); } function detectSObjectFromObjectType(typeName: string): string | null { if (typeName.endsWith("Connection")) { return typeName.replace(/Connection$/, ""); } if (typeName.endsWith("Edge")) { return typeName.replace(/Edge$/, ""); } const schemaOnlyTypes = new Set([ "UIAPI", "RecordQuery", "RecordQueryAggregate", "PageInfo", "UIAPIMutations", "FieldValue", ]); if (schemaOnlyTypes.has(typeName)) return null; return typeName; } async function fetchObjectInfoQuietly( orgAlias: string, sObjectName: string, ): Promise { let info = getCachedObjectInfo(orgAlias, sObjectName); if (info) return info; try { const auth = await getOrgAuth(orgAlias); info = await getObjectInfo(auth, orgAlias, sObjectName); return info; } catch { return null; } }