Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

116 changes: 89 additions & 27 deletions graphql/codegen/src/core/codegen/cli/command-map-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,47 @@ function createNamedImportDeclaration(
return decl;
}

/**
* Build the command handler function type:
* (argv: Partial<Record<string, unknown>>, prompter: Inquirerer, options: CLIOptions) => Promise<void>
* This matches the actual exported handler signatures from table/custom command files.
*/
function buildCommandHandlerType(): t.TSFunctionType {
const argvParam = t.identifier('argv');
argvParam.typeAnnotation = t.tsTypeAnnotation(
t.tsTypeReference(
t.identifier('Partial'),
t.tsTypeParameterInstantiation([
t.tsTypeReference(
t.identifier('Record'),
t.tsTypeParameterInstantiation([
t.tsStringKeyword(),
t.tsUnknownKeyword(),
]),
),
]),
),
);
const prompterParam = t.identifier('prompter');
prompterParam.typeAnnotation = t.tsTypeAnnotation(
t.tsTypeReference(t.identifier('Inquirerer')),
);
const optionsParam = t.identifier('options');
optionsParam.typeAnnotation = t.tsTypeAnnotation(
t.tsTypeReference(t.identifier('CLIOptions')),
);
return t.tsFunctionType(
null,
[argvParam, prompterParam, optionsParam],
t.tsTypeAnnotation(
t.tsTypeReference(
t.identifier('Promise'),
t.tsTypeParameterInstantiation([t.tsVoidKeyword()]),
),
),
);
}

export function generateCommandMap(
tables: CleanTable[],
customOperations: CleanOperation[],
Expand Down Expand Up @@ -83,25 +124,9 @@ export function generateCommandMap(
),
);

const createCommandMapFunc = t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier('createCommandMap'),
t.arrowFunctionExpression(
[],
t.objectExpression(mapProperties),
),
),
]);

const createCommandMapAnnotation = t.tsTypeAnnotation(
t.tsTypeReference(
t.identifier('Record'),
t.tsTypeParameterInstantiation([
t.tsStringKeyword(),
t.tsFunctionType(null, [], t.tsTypeAnnotation(t.tsAnyKeyword())),
]),
),
);
// Build command handler type matching actual handler signature:
// (argv: Partial<Record<string, unknown>>, prompter: Inquirerer, options: CLIOptions) => Promise<void>
const commandHandlerType = buildCommandHandlerType();

const createCommandMapId = t.identifier('createCommandMap');
createCommandMapId.typeAnnotation = t.tsTypeAnnotation(
Expand All @@ -114,14 +139,23 @@ export function generateCommandMap(
t.identifier('Record'),
t.tsTypeParameterInstantiation([
t.tsStringKeyword(),
t.tsFunctionType(null, [], t.tsTypeAnnotation(t.tsUnknownKeyword())),
commandHandlerType,
]),
),
),
),
),
);

const createCommandMapFunc = t.variableDeclaration('const', [
t.variableDeclarator(
createCommandMapId,
t.arrowFunctionExpression(
[],
t.objectExpression(mapProperties),
),
),
]);
statements.push(createCommandMapFunc);

const usageLines = [
Expand Down Expand Up @@ -285,9 +319,12 @@ export function generateCommandMap(
t.assignmentExpression(
'=',
t.identifier('command'),
t.memberExpression(
t.identifier('answer'),
t.identifier('command'),
t.tsAsExpression(
t.memberExpression(
t.identifier('answer'),
t.identifier('command'),
),
t.tsStringKeyword(),
),
),
),
Expand Down Expand Up @@ -449,9 +486,31 @@ export function generateMultiTargetCommandMap(
),
);

// Build command handler type matching actual handler signature
const multiTargetCommandHandlerType = buildCommandHandlerType();

const multiTargetCreateCommandMapId = t.identifier('createCommandMap');
multiTargetCreateCommandMapId.typeAnnotation = t.tsTypeAnnotation(
t.tsParenthesizedType(
t.tsFunctionType(
null,
[],
t.tsTypeAnnotation(
t.tsTypeReference(
t.identifier('Record'),
t.tsTypeParameterInstantiation([
t.tsStringKeyword(),
multiTargetCommandHandlerType,
]),
),
),
),
),
);

const createCommandMapFunc = t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier('createCommandMap'),
multiTargetCreateCommandMapId,
t.arrowFunctionExpression(
[],
t.objectExpression(mapProperties),
Expand Down Expand Up @@ -626,9 +685,12 @@ export function generateMultiTargetCommandMap(
t.assignmentExpression(
'=',
t.identifier('command'),
t.memberExpression(
t.identifier('answer'),
t.identifier('command'),
t.tsAsExpression(
t.memberExpression(
t.identifier('answer'),
t.identifier('command'),
),
t.tsStringKeyword(),
),
),
),
Expand Down
102 changes: 84 additions & 18 deletions graphql/codegen/src/core/codegen/cli/custom-command-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as t from '@babel/types';
import { toKebabCase } from 'komoji';

import { generateCode } from '../babel-ast';
import { getGeneratedFileHeader } from '../utils';
import { getGeneratedFileHeader, ucFirst } from '../utils';
import type { CleanOperation, CleanTypeRef } from '../../../types/schema';
import type { GeneratedFile } from './executor-generator';
import { buildQuestionsArray } from './arg-mapper';
Expand Down Expand Up @@ -144,25 +144,40 @@ function buildOrmCustomCall(
argsExpr: t.Expression,
selectExpr?: t.Expression,
hasArgs: boolean = true,
selectTypeName?: string,
): t.Expression {
const callArgs: t.Expression[] = [];
// Helper: wrap { select } and cast to `{ select: XxxSelect }` via `unknown`.
// The ORM method's second parameter is `{ select: S } & StrictSelect<S, XxxSelect>`.
// We import the concrete Select type (e.g. CheckPasswordPayloadSelect) and cast
// `{ select: selectFields } as unknown as { select: XxxSelect }` so TS infers
// `S = XxxSelect` and StrictSelect is satisfied.
const castSelectWrapper = (sel: t.Expression) => {
const selectObj = t.objectExpression([
t.objectProperty(t.identifier('select'), sel),
]);
if (!selectTypeName) return selectObj;
return t.tsAsExpression(
t.tsAsExpression(selectObj, t.tsUnknownKeyword()),
t.tsTypeLiteral([
t.tsPropertySignature(
t.identifier('select'),
t.tsTypeAnnotation(
t.tsTypeReference(t.identifier(selectTypeName)),
),
),
]),
);
};
if (hasArgs) {
// Operation has arguments: pass args as first param, select as second
// Operation has arguments: pass args as first param, select as second.
callArgs.push(argsExpr);
if (selectExpr) {
callArgs.push(
t.objectExpression([
t.objectProperty(t.identifier('select'), selectExpr),
]),
);
callArgs.push(castSelectWrapper(selectExpr));
}
} else if (selectExpr) {
// No arguments: pass { select } as the only param (ORM signature)
callArgs.push(
t.objectExpression([
t.objectProperty(t.identifier('select'), selectExpr),
]),
);
// No arguments: pass { select } as the only param (ORM signature).
callArgs.push(castSelectWrapper(selectExpr));
}
return t.callExpression(
t.memberExpression(
Expand Down Expand Up @@ -232,6 +247,21 @@ export function generateCustomCommand(op: CleanOperation, options?: CustomComman
);
}

// Import the Variables type for this operation from the ORM query/mutation module.
// Custom operations define their own Variables types (e.g. CheckPasswordVariables)
// in the ORM layer. We import and cast CLI answers to this type for proper typing.
if (op.args.length > 0) {
const variablesTypeName = `${ucFirst(op.name)}Variables`;
// Commands are at cli/commands/xxx.ts (no target) or cli/commands/{target}/xxx.ts (with target).
// ORM query/mutation is at orm/{opKind}/ — two or three levels up from commands.
const ormOpPath = options?.targetName
? `../../../orm/${opKind}`
: `../../orm/${opKind}`;
statements.push(
createImportDeclaration(ormOpPath, [variablesTypeName], true),
);
}

const questionsArray =
op.args.length > 0
? buildQuestionsArray(op.args)
Expand Down Expand Up @@ -325,11 +355,23 @@ export function generateCustomCommand(op: CleanOperation, options?: CustomComman
);
}

// Cast args to the specific Variables type for this operation.
// The ORM expects typed variables (e.g. CheckPasswordVariables), and CLI
// prompt answers are Record<string, unknown>. We cast through `unknown`
// first because Record<string, unknown> doesn't directly overlap with
// Variables types that have specific property types (like `input: SomeInput`).
const variablesTypeName = `${ucFirst(op.name)}Variables`;
const argsExpr =
op.args.length > 0
? (hasInputObjectArg
? t.identifier('parsedAnswers')
: t.identifier('answers'))
? t.tsAsExpression(
t.tsAsExpression(
hasInputObjectArg
? t.identifier('parsedAnswers')
: t.identifier('answers'),
t.tsUnknownKeyword(),
),
t.tsTypeReference(t.identifier(variablesTypeName)),
)
: t.objectExpression([]);

// For OBJECT return types, generate runtime select from --select flag
Expand All @@ -345,7 +387,10 @@ export function generateCustomCommand(op: CleanOperation, options?: CustomComman
t.callExpression(t.identifier('buildSelectFromPaths'), [
t.logicalExpression(
'??',
t.memberExpression(t.identifier('argv'), t.identifier('select')),
t.tsAsExpression(
t.memberExpression(t.identifier('argv'), t.identifier('select')),
t.tsStringKeyword(),
),
t.stringLiteral(defaultSelect),
),
]),
Expand All @@ -355,13 +400,34 @@ export function generateCustomCommand(op: CleanOperation, options?: CustomComman
selectExpr = t.identifier('selectFields');
}

// Derive the Select type name from the operation's return type.
// e.g. CheckPasswordPayload → CheckPasswordPayloadSelect
// This is used to cast { select } to the proper type for StrictSelect.
let selectTypeName: string | undefined;
if (isObjectReturn) {
const baseReturnType = unwrapType(op.returnType);
if (baseReturnType.name) {
selectTypeName = `${baseReturnType.name}Select`;
}
}

// Import the Select type from orm/input-types if we have one
if (selectTypeName) {
const inputTypesPath = options?.targetName
? `../../../orm/input-types`
: `../../orm/input-types`;
statements.push(
createImportDeclaration(inputTypesPath, [selectTypeName], true),
);
}

const hasArgs = op.args.length > 0;
bodyStatements.push(
t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier('result'),
t.awaitExpression(
buildOrmCustomCall(opKind, op.name, argsExpr, selectExpr, hasArgs),
buildOrmCustomCall(opKind, op.name, argsExpr, selectExpr, hasArgs, selectTypeName),
),
),
]),
Expand Down
48 changes: 36 additions & 12 deletions graphql/codegen/src/core/codegen/cli/executor-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,24 @@ export function generateExecutorFile(toolName: string, options?: ExecutorOptions
]),
),

t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier('headers'),
t.objectExpression([]),
),
]),
(() => {
const headersId = t.identifier('headers');
headersId.typeAnnotation = t.tsTypeAnnotation(
t.tsTypeReference(
t.identifier('Record'),
t.tsTypeParameterInstantiation([
t.tsStringKeyword(),
t.tsStringKeyword(),
]),
),
);
return t.variableDeclaration('const', [
t.variableDeclarator(
headersId,
t.objectExpression([]),
),
]);
})(),

t.ifStatement(
t.callExpression(
Expand Down Expand Up @@ -399,12 +411,24 @@ export function generateMultiTargetExecutorFile(
),
]),
),
t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier('headers'),
t.objectExpression([]),
),
]),
(() => {
const headersId = t.identifier('headers');
headersId.typeAnnotation = t.tsTypeAnnotation(
t.tsTypeReference(
t.identifier('Record'),
t.tsTypeParameterInstantiation([
t.tsStringKeyword(),
t.tsStringKeyword(),
]),
),
);
return t.variableDeclaration('const', [
t.variableDeclarator(
headersId,
t.objectExpression([]),
),
]);
})(),
t.variableDeclaration('let', [
t.variableDeclarator(t.identifier('endpoint'), t.stringLiteral('')),
]),
Expand Down
Loading