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
Original file line number Diff line number Diff line change
Expand Up @@ -887,7 +887,7 @@ async function handleCreate(argv: Partial<Record<string, unknown>>, prompter: In
message: "year",
required: true
}, {
type: "text",
type: "boolean",
name: "isElectric",
message: "isElectric",
required: true
Expand Down Expand Up @@ -943,7 +943,7 @@ async function handleUpdate(argv: Partial<Record<string, unknown>>, prompter: In
message: "year",
required: false
}, {
type: "text",
type: "boolean",
name: "isElectric",
message: "isElectric",
required: false
Expand Down Expand Up @@ -4338,7 +4338,7 @@ async function handleCreate(argv: Partial<Record<string, unknown>>, prompter: In
message: "year",
required: true
}, {
type: "text",
type: "boolean",
name: "isElectric",
message: "isElectric",
required: true
Expand Down Expand Up @@ -4394,7 +4394,7 @@ async function handleUpdate(argv: Partial<Record<string, unknown>>, prompter: In
message: "year",
required: false
}, {
type: "text",
type: "boolean",
name: "isElectric",
message: "isElectric",
required: false
Expand Down
26 changes: 15 additions & 11 deletions graphql/codegen/src/core/codegen/cli/arg-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,23 @@ function resolveBaseType(typeRef: CleanTypeRef): CleanTypeRef {
return typeRef;
}

export function buildQuestionObject(arg: CleanArgument): t.ObjectExpression {
export function buildQuestionObject(arg: CleanArgument, namePrefix?: string): t.ObjectExpression {
const { inner, required } = unwrapNonNull(arg.type);
const base = resolveBaseType(arg.type);
const props: t.ObjectProperty[] = [];
const questionName = namePrefix ? `${namePrefix}.${arg.name}` : arg.name;

if (base.kind === 'ENUM' && base.enumValues && base.enumValues.length > 0) {
props.push(
t.objectProperty(t.identifier('type'), t.stringLiteral('autocomplete')),
);
props.push(
t.objectProperty(t.identifier('name'), t.stringLiteral(arg.name)),
t.objectProperty(t.identifier('name'), t.stringLiteral(questionName)),
);
props.push(
t.objectProperty(
t.identifier('message'),
t.stringLiteral(arg.description || arg.name),
t.stringLiteral(arg.description || questionName),
),
);
props.push(
Expand All @@ -45,12 +46,12 @@ export function buildQuestionObject(arg: CleanArgument): t.ObjectExpression {
t.objectProperty(t.identifier('type'), t.stringLiteral('confirm')),
);
props.push(
t.objectProperty(t.identifier('name'), t.stringLiteral(arg.name)),
t.objectProperty(t.identifier('name'), t.stringLiteral(questionName)),
);
props.push(
t.objectProperty(
t.identifier('message'),
t.stringLiteral(arg.description || arg.name),
t.stringLiteral(arg.description || questionName),
),
);
props.push(
Expand All @@ -64,27 +65,28 @@ export function buildQuestionObject(arg: CleanArgument): t.ObjectExpression {
t.objectProperty(t.identifier('type'), t.stringLiteral('text')),
);
props.push(
t.objectProperty(t.identifier('name'), t.stringLiteral(arg.name)),
t.objectProperty(t.identifier('name'), t.stringLiteral(questionName)),
);
props.push(
t.objectProperty(
t.identifier('message'),
t.stringLiteral(arg.description || `${arg.name} (number)`),
t.stringLiteral(arg.description || `${questionName} (number)`),
),
);
} else if (inner.kind === 'INPUT_OBJECT' && inner.inputFields) {
// INPUT_OBJECT fields are flattened in buildQuestionsArray with dot-notation
return buildInputObjectQuestion(arg.name, inner, required);
} else {
props.push(
t.objectProperty(t.identifier('type'), t.stringLiteral('text')),
);
props.push(
t.objectProperty(t.identifier('name'), t.stringLiteral(arg.name)),
t.objectProperty(t.identifier('name'), t.stringLiteral(questionName)),
);
props.push(
t.objectProperty(
t.identifier('message'),
t.stringLiteral(arg.description || arg.name),
t.stringLiteral(arg.description || questionName),
),
);
}
Expand Down Expand Up @@ -124,12 +126,14 @@ export function buildQuestionsArray(args: CleanArgument[]): t.ArrayExpression {
const { inner } = unwrapNonNull(arg.type);

if (inner.kind === 'INPUT_OBJECT' && inner.inputFields) {
// Flatten INPUT_OBJECT fields with dot-notation: e.g. input.email, input.password
for (const field of inner.inputFields) {
questions.push(buildQuestionObject(field));
questions.push(buildQuestionObject(field, arg.name));
}
} else if (base.kind === 'INPUT_OBJECT' && base.inputFields) {
// Same for NON_NULL-wrapped INPUT_OBJECT
for (const field of base.inputFields) {
questions.push(buildQuestionObject(field));
questions.push(buildQuestionObject(field, arg.name));
}
} else {
questions.push(buildQuestionObject(arg));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ export function generateCustomCommand(op: CleanOperation, options?: CustomComman
// Build the list of utils imports needed
const utilsImports: string[] = [];
if (hasInputObjectArg) {
utilsImports.push('parseMutationInput');
utilsImports.push('unflattenDotNotation');
}
if (isObjectReturn) {
utilsImports.push('buildSelectFromPaths');
Expand Down Expand Up @@ -341,13 +341,14 @@ export function generateCustomCommand(op: CleanOperation, options?: CustomComman
);

// For mutations with INPUT_OBJECT args (like `input: SignUpInput`),
// parse JSON strings from CLI into proper objects
// reconstruct nested objects from dot-notation CLI answers.
// e.g. { 'input.email': 'foo', 'input.password': 'bar' } → { input: { email: 'foo', password: 'bar' } }
if (hasInputObjectArg && op.args.length > 0) {
bodyStatements.push(
t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier('parsedAnswers'),
t.callExpression(t.identifier('parseMutationInput'), [
t.callExpression(t.identifier('unflattenDotNotation'), [
t.identifier('answers'),
]),
),
Expand Down
20 changes: 19 additions & 1 deletion graphql/codegen/src/core/codegen/cli/table-command-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,23 @@ function getTsTypeForField(field: { type: { gqlType: string; isArray: boolean }
}
}

/**
* Maps a GraphQL scalar type to the appropriate inquirerer question type.
* Used by table CRUD commands to generate semantic prompts.
*/
function getQuestionTypeForField(field: { type: { gqlType: string } }): string {
const gqlType = field.type.gqlType.replace(/!/g, '');
switch (gqlType) {
case 'Boolean':
return 'boolean';
case 'JSON':
case 'GeoJSON':
return 'json';
default:
return 'text';
}
}

function buildFieldSchemaObject(table: CleanTable): t.ObjectExpression {
const fields = getScalarFields(table);
return t.objectExpression(
Expand Down Expand Up @@ -537,8 +554,9 @@ function buildMutationHandler(
// For update: all fields are optional (user only updates what they want)
const isRequired = operation === 'create' && !fieldsWithDefaults.has(field.name);
const hasDefault = fieldsWithDefaults.has(field.name);
const questionType = getQuestionTypeForField(field);
const questionProps = [
t.objectProperty(t.identifier('type'), t.stringLiteral('text')),
t.objectProperty(t.identifier('type'), t.stringLiteral(questionType)),
t.objectProperty(
t.identifier('name'),
t.stringLiteral(field.name),
Expand Down
4 changes: 2 additions & 2 deletions graphql/codegen/src/core/codegen/templates/cli-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ if (process.argv.includes('--version') || process.argv.includes('-v')) {
process.exit(0);
}

// Check for --tty false to enable non-interactive mode (noTty)
// Check for --tty false or --no-tty to enable non-interactive mode (noTty)
const ttyIdx = process.argv.indexOf('--tty');
const noTty = ttyIdx !== -1 && process.argv[ttyIdx + 1] === 'false';
const noTty = (ttyIdx !== -1 && process.argv[ttyIdx + 1] === 'false') || process.argv.includes('--no-tty');

const options: Partial<CLIOptions> = {
noTty,
Expand Down
28 changes: 28 additions & 0 deletions graphql/codegen/src/core/codegen/templates/cli-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,34 @@ export function parseMutationInput(
return answers;
}

/**
* Reconstruct nested objects from dot-notation CLI answers.
* When INPUT_OBJECT args are flattened to dot-notation questions
* (e.g. `--input.email foo --input.password bar`), this function
* rebuilds the nested structure expected by the ORM:
*
* { 'input.email': 'foo', 'input.password': 'bar' }
* → { input: { email: 'foo', password: 'bar' } }
*
* Non-dotted keys are passed through unchanged.
* Uses `nested-obj` for safe nested property setting.
*/
export function unflattenDotNotation(
answers: Record<string, unknown>,
): Record<string, unknown> {
const result: Record<string, unknown> = {};

for (const [key, value] of Object.entries(answers)) {
if (key.includes('.')) {
objectPath.set(result, key, value);
} else {
result[key] = value;
}
}

return result;
}

/**
* Build a select object from a comma-separated list of dot-notation paths.
* Uses `nested-obj` to parse paths like 'clientMutationId,result.accessToken,result.userId'
Expand Down
Loading