@constructive-io/graphql-codegen 4.1.1 → 4.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -69,8 +69,40 @@ function buildErrorCatch(errorMessage) {
69
69
  t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('process'), t.identifier('exit')), [t.numericLiteral(1)])),
70
70
  ]));
71
71
  }
72
- function buildOrmCustomCall(opKind, opName, argsExpr) {
73
- return t.callExpression(t.memberExpression(t.callExpression(t.memberExpression(t.memberExpression(t.identifier('client'), t.identifier(opKind)), t.identifier(opName)), [argsExpr]), t.identifier('execute')), []);
72
+ /**
73
+ * Unwrap NON_NULL / LIST wrappers to get the underlying named type.
74
+ */
75
+ function unwrapType(ref) {
76
+ if ((ref.kind === 'NON_NULL' || ref.kind === 'LIST') && ref.ofType) {
77
+ return unwrapType(ref.ofType);
78
+ }
79
+ return ref;
80
+ }
81
+ /**
82
+ * Build a select object expression from return-type fields.
83
+ * If the return type has known fields, generates { field1: true, field2: true, ... }.
84
+ * Falls back to { clientMutationId: true } for mutations without known fields.
85
+ */
86
+ function buildSelectObject(returnType, isMutation) {
87
+ const base = unwrapType(returnType);
88
+ if (base.fields && base.fields.length > 0) {
89
+ return t.objectExpression(base.fields.map((f) => t.objectProperty(t.identifier(f.name), t.booleanLiteral(true))));
90
+ }
91
+ // Fallback: all PostGraphile mutation payloads have clientMutationId
92
+ if (isMutation) {
93
+ return t.objectExpression([
94
+ t.objectProperty(t.identifier('clientMutationId'), t.booleanLiteral(true)),
95
+ ]);
96
+ }
97
+ return t.objectExpression([]);
98
+ }
99
+ function buildOrmCustomCall(opKind, opName, argsExpr, selectExpr) {
100
+ return t.callExpression(t.memberExpression(t.callExpression(t.memberExpression(t.memberExpression(t.identifier('client'), t.identifier(opKind)), t.identifier(opName)), [
101
+ argsExpr,
102
+ t.objectExpression([
103
+ t.objectProperty(t.identifier('select'), selectExpr),
104
+ ]),
105
+ ]), t.identifier('execute')), []);
74
106
  }
75
107
  function generateCustomCommand(op, options) {
76
108
  const commandName = (0, komoji_1.toKebabCase)(op.name);
@@ -81,8 +113,19 @@ function generateCustomCommand(op, options) {
81
113
  if (options?.saveToken) {
82
114
  imports.push('getStore');
83
115
  }
116
+ // Check if any argument is an INPUT_OBJECT (i.e. takes JSON input like { input: SomeInput })
117
+ const hasInputObjectArg = op.args.some((arg) => {
118
+ const base = unwrapType(arg.type);
119
+ return base.kind === 'INPUT_OBJECT';
120
+ });
121
+ const utilsPath = options?.executorImportPath
122
+ ? options.executorImportPath.replace(/\/executor$/, '/utils')
123
+ : '../utils';
84
124
  statements.push(createImportDeclaration('inquirerer', ['CLIOptions', 'Inquirerer']));
85
125
  statements.push(createImportDeclaration(executorPath, imports));
126
+ if (hasInputObjectArg) {
127
+ statements.push(createImportDeclaration(utilsPath, ['parseMutationInput']));
128
+ }
86
129
  const questionsArray = op.args.length > 0
87
130
  ? (0, arg_mapper_1.buildQuestionsArray)(op.args)
88
131
  : t.arrayExpression([]);
@@ -111,11 +154,23 @@ function generateCustomCommand(op, options) {
111
154
  bodyStatements.push(t.variableDeclaration('const', [
112
155
  t.variableDeclarator(t.identifier('client'), t.callExpression(t.identifier('getClient'), getClientArgs)),
113
156
  ]));
157
+ // For mutations with INPUT_OBJECT args (like `input: SignUpInput`),
158
+ // parse JSON strings from CLI into proper objects
159
+ if (hasInputObjectArg && op.args.length > 0) {
160
+ bodyStatements.push(t.variableDeclaration('const', [
161
+ t.variableDeclarator(t.identifier('parsedAnswers'), t.callExpression(t.identifier('parseMutationInput'), [
162
+ t.identifier('answers'),
163
+ ])),
164
+ ]));
165
+ }
114
166
  const argsExpr = op.args.length > 0
115
- ? t.identifier('answers')
167
+ ? (hasInputObjectArg
168
+ ? t.identifier('parsedAnswers')
169
+ : t.identifier('answers'))
116
170
  : t.objectExpression([]);
171
+ const selectExpr = buildSelectObject(op.returnType, op.kind === 'mutation');
117
172
  bodyStatements.push(t.variableDeclaration('const', [
118
- t.variableDeclarator(t.identifier('result'), t.awaitExpression(buildOrmCustomCall(opKind, op.name, argsExpr))),
173
+ t.variableDeclarator(t.identifier('result'), t.awaitExpression(buildOrmCustomCall(opKind, op.name, argsExpr, selectExpr))),
119
174
  ]));
120
175
  if (options?.saveToken) {
121
176
  bodyStatements.push(t.ifStatement(t.logicalExpression('&&', t.memberExpression(t.identifier('argv'), t.identifier('saveToken')), t.identifier('result')), t.blockStatement([
@@ -50,4 +50,5 @@ export { generateReadme, generateAgentsDocs, getCliMcpTools, generateSkills, gen
50
50
  export type { MultiTargetDocsInput } from './docs-generator';
51
51
  export { resolveDocsConfig } from '../docs-utils';
52
52
  export type { GeneratedDocFile, McpTool } from '../docs-utils';
53
+ export { generateUtilsFile } from './utils-generator';
53
54
  export type { GeneratedFile, MultiTargetExecutorInput } from './executor-generator';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resolveDocsConfig = exports.generateMultiTargetSkills = exports.getMultiTargetCliMcpTools = exports.generateMultiTargetAgentsDocs = exports.generateMultiTargetReadme = exports.generateSkills = exports.getCliMcpTools = exports.generateAgentsDocs = exports.generateReadme = exports.generateAuthCommandWithName = exports.generateMultiTargetContextCommand = exports.generateAuthCommand = exports.generateContextCommand = exports.generateMultiTargetCommandMap = exports.generateCommandMap = exports.generateCustomCommand = exports.generateTableCommand = exports.generateMultiTargetExecutorFile = exports.generateExecutorFile = void 0;
3
+ exports.generateUtilsFile = exports.resolveDocsConfig = exports.generateMultiTargetSkills = exports.getMultiTargetCliMcpTools = exports.generateMultiTargetAgentsDocs = exports.generateMultiTargetReadme = exports.generateSkills = exports.getCliMcpTools = exports.generateAgentsDocs = exports.generateReadme = exports.generateAuthCommandWithName = exports.generateMultiTargetContextCommand = exports.generateAuthCommand = exports.generateContextCommand = exports.generateMultiTargetCommandMap = exports.generateCommandMap = exports.generateCustomCommand = exports.generateTableCommand = exports.generateMultiTargetExecutorFile = exports.generateExecutorFile = void 0;
4
4
  exports.generateCli = generateCli;
5
5
  exports.resolveBuiltinNames = resolveBuiltinNames;
6
6
  exports.generateMultiTargetCli = generateMultiTargetCli;
@@ -9,6 +9,7 @@ const custom_command_generator_1 = require("./custom-command-generator");
9
9
  const executor_generator_1 = require("./executor-generator");
10
10
  const infra_generator_1 = require("./infra-generator");
11
11
  const table_command_generator_1 = require("./table-command-generator");
12
+ const utils_generator_1 = require("./utils-generator");
12
13
  function generateCli(options) {
13
14
  const { tables, customOperations, config } = options;
14
15
  const files = [];
@@ -18,6 +19,8 @@ function generateCli(options) {
18
19
  : 'app';
19
20
  const executorFile = (0, executor_generator_1.generateExecutorFile)(toolName);
20
21
  files.push(executorFile);
22
+ const utilsFile = (0, utils_generator_1.generateUtilsFile)();
23
+ files.push(utilsFile);
21
24
  const contextFile = (0, infra_generator_1.generateContextCommand)(toolName);
22
25
  files.push(contextFile);
23
26
  const authFile = (0, infra_generator_1.generateAuthCommand)(toolName);
@@ -42,7 +45,7 @@ function generateCli(options) {
42
45
  tables: tables.length,
43
46
  customQueries: customOperations?.queries.length ?? 0,
44
47
  customMutations: customOperations?.mutations.length ?? 0,
45
- infraFiles: 3,
48
+ infraFiles: 4,
46
49
  totalFiles: files.length,
47
50
  },
48
51
  };
@@ -70,6 +73,8 @@ function generateMultiTargetCli(options) {
70
73
  }));
71
74
  const executorFile = (0, executor_generator_1.generateMultiTargetExecutorFile)(toolName, executorInputs);
72
75
  files.push(executorFile);
76
+ const utilsFile = (0, utils_generator_1.generateUtilsFile)();
77
+ files.push(utilsFile);
73
78
  const contextFile = (0, infra_generator_1.generateMultiTargetContextCommand)(toolName, builtinNames.context, targets.map((t) => ({ name: t.name, endpoint: t.endpoint })));
74
79
  files.push(contextFile);
75
80
  const authFile = (0, infra_generator_1.generateAuthCommandWithName)(toolName, builtinNames.auth);
@@ -120,7 +125,7 @@ function generateMultiTargetCli(options) {
120
125
  tables: totalTables,
121
126
  customQueries: totalQueries,
122
127
  customMutations: totalMutations,
123
- infraFiles: 3,
128
+ infraFiles: 4,
124
129
  totalFiles: files.length,
125
130
  },
126
131
  };
@@ -151,3 +156,5 @@ Object.defineProperty(exports, "getMultiTargetCliMcpTools", { enumerable: true,
151
156
  Object.defineProperty(exports, "generateMultiTargetSkills", { enumerable: true, get: function () { return docs_generator_1.generateMultiTargetSkills; } });
152
157
  var docs_utils_1 = require("../docs-utils");
153
158
  Object.defineProperty(exports, "resolveDocsConfig", { enumerable: true, get: function () { return docs_utils_1.resolveDocsConfig; } });
159
+ var utils_generator_2 = require("./utils-generator");
160
+ Object.defineProperty(exports, "generateUtilsFile", { enumerable: true, get: function () { return utils_generator_2.generateUtilsFile; } });
@@ -695,7 +695,7 @@ function buildSetTokenHandler() {
695
695
  t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('store'), t.identifier('setCredentials')), [
696
696
  t.memberExpression(t.identifier('current'), t.identifier('name')),
697
697
  t.objectExpression([
698
- t.objectProperty(t.identifier('token'), t.callExpression(t.memberExpression(t.callExpression(t.memberExpression(t.identifier('String'), t.identifier('call')), [
698
+ t.objectProperty(t.identifier('token'), t.callExpression(t.memberExpression(t.callExpression(t.identifier('String'), [
699
699
  t.logicalExpression('||', t.identifier('tokenValue'), t.stringLiteral('')),
700
700
  ]), t.identifier('trim')), [])),
701
701
  ]),
@@ -44,6 +44,40 @@ function createImportDeclaration(moduleSpecifier, namedImports, typeOnly = false
44
44
  decl.importKind = typeOnly ? 'type' : 'value';
45
45
  return decl;
46
46
  }
47
+ /**
48
+ * Build a field schema object that maps field names to their GraphQL types.
49
+ * This is used at runtime for type coercion (string CLI args → proper types).
50
+ * e.g., { name: 'string', isActive: 'boolean', position: 'int', status: 'enum' }
51
+ */
52
+ function buildFieldSchemaObject(table) {
53
+ const fields = (0, utils_1.getScalarFields)(table);
54
+ return t.objectExpression(fields.map((f) => {
55
+ const gqlType = f.type.gqlType.replace(/!/g, '');
56
+ let schemaType;
57
+ switch (gqlType) {
58
+ case 'Boolean':
59
+ schemaType = 'boolean';
60
+ break;
61
+ case 'Int':
62
+ case 'BigInt':
63
+ schemaType = 'int';
64
+ break;
65
+ case 'Float':
66
+ schemaType = 'float';
67
+ break;
68
+ case 'JSON':
69
+ case 'GeoJSON':
70
+ schemaType = 'json';
71
+ break;
72
+ case 'UUID':
73
+ schemaType = 'uuid';
74
+ break;
75
+ default:
76
+ schemaType = 'string';
77
+ }
78
+ return t.objectProperty(t.identifier(f.name), t.stringLiteral(schemaType));
79
+ }));
80
+ }
47
81
  function buildSelectObject(table) {
48
82
  const fields = (0, utils_1.getScalarFields)(table);
49
83
  return t.objectExpression(fields.map((f) => t.objectProperty(t.identifier(f.name), t.booleanLiteral(true))));
@@ -183,36 +217,52 @@ function buildMutationHandler(table, operation, targetName) {
183
217
  : buildSelectObject(table);
184
218
  let ormArgs;
185
219
  if (operation === 'create') {
186
- const dataProps = editableFields.map((f) => t.objectProperty(t.identifier(f.name), t.memberExpression(t.identifier('answers'), t.identifier(f.name)), false, true));
220
+ const dataProps = editableFields.map((f) => t.objectProperty(t.identifier(f.name), t.memberExpression(t.identifier('cleanedData'), t.identifier(f.name)), false, true));
187
221
  ormArgs = t.objectExpression([
188
222
  t.objectProperty(t.identifier('data'), t.objectExpression(dataProps)),
189
223
  t.objectProperty(t.identifier('select'), selectObj),
190
224
  ]);
191
225
  }
192
226
  else if (operation === 'update') {
193
- const dataProps = editableFields.map((f) => t.objectProperty(t.identifier(f.name), t.memberExpression(t.identifier('answers'), t.identifier(f.name)), false, true));
227
+ const dataProps = editableFields.map((f) => t.objectProperty(t.identifier(f.name), t.memberExpression(t.identifier('cleanedData'), t.identifier(f.name)), false, true));
194
228
  ormArgs = t.objectExpression([
195
- t.objectProperty(t.identifier(pk.name), t.memberExpression(t.identifier('answers'), t.identifier(pk.name))),
229
+ t.objectProperty(t.identifier('where'), t.objectExpression([
230
+ t.objectProperty(t.identifier(pk.name), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(pk.name)), t.tsStringKeyword())),
231
+ ])),
196
232
  t.objectProperty(t.identifier('data'), t.objectExpression(dataProps)),
197
233
  t.objectProperty(t.identifier('select'), selectObj),
198
234
  ]);
199
235
  }
200
236
  else {
201
237
  ormArgs = t.objectExpression([
202
- t.objectProperty(t.identifier(pk.name), t.memberExpression(t.identifier('answers'), t.identifier(pk.name))),
238
+ t.objectProperty(t.identifier('where'), t.objectExpression([
239
+ t.objectProperty(t.identifier(pk.name), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(pk.name)), t.tsStringKeyword())),
240
+ ])),
203
241
  t.objectProperty(t.identifier('select'), selectObj),
204
242
  ]);
205
243
  }
206
244
  const tryBody = [
207
245
  t.variableDeclaration('const', [
208
- t.variableDeclarator(t.identifier('answers'), t.awaitExpression(t.callExpression(t.memberExpression(t.identifier('prompter'), t.identifier('prompt')), [t.identifier('argv'), t.arrayExpression(questions)]))),
246
+ t.variableDeclarator(t.identifier('rawAnswers'), t.awaitExpression(t.callExpression(t.memberExpression(t.identifier('prompter'), t.identifier('prompt')), [t.identifier('argv'), t.arrayExpression(questions)]))),
209
247
  ]),
210
- buildGetClientStatement(targetName),
211
248
  t.variableDeclaration('const', [
212
- t.variableDeclarator(t.identifier('result'), t.awaitExpression(buildOrmCall(singularName, operation, ormArgs))),
249
+ t.variableDeclarator(t.identifier('answers'), t.callExpression(t.identifier('coerceAnswers'), [
250
+ t.identifier('rawAnswers'),
251
+ t.identifier('fieldSchema'),
252
+ ])),
213
253
  ]),
214
- buildJsonLog(t.identifier('result')),
215
254
  ];
255
+ if (operation !== 'delete') {
256
+ tryBody.push(t.variableDeclaration('const', [
257
+ t.variableDeclarator(t.identifier('cleanedData'), t.callExpression(t.identifier('stripUndefined'), [
258
+ t.identifier('answers'),
259
+ t.identifier('fieldSchema'),
260
+ ])),
261
+ ]));
262
+ }
263
+ tryBody.push(buildGetClientStatement(targetName), t.variableDeclaration('const', [
264
+ t.variableDeclarator(t.identifier('result'), t.awaitExpression(buildOrmCall(singularName, operation, ormArgs))),
265
+ ]), buildJsonLog(t.identifier('result')));
216
266
  const argvParam = t.identifier('argv');
217
267
  argvParam.typeAnnotation = buildArgvType();
218
268
  const prompterParam = t.identifier('prompter');
@@ -233,6 +283,12 @@ function generateTableCommand(table, options) {
233
283
  'extractFirst',
234
284
  ]));
235
285
  statements.push(createImportDeclaration(executorPath, ['getClient']));
286
+ const utilsPath = options?.targetName ? '../../utils' : '../utils';
287
+ statements.push(createImportDeclaration(utilsPath, ['coerceAnswers', 'stripUndefined']));
288
+ // Generate field schema for type coercion
289
+ statements.push(t.variableDeclaration('const', [
290
+ t.variableDeclarator(t.identifier('fieldSchema'), buildFieldSchemaObject(table)),
291
+ ]));
236
292
  const subcommands = ['list', 'get', 'create', 'update', 'delete'];
237
293
  const usageLines = [
238
294
  '',
@@ -0,0 +1,10 @@
1
+ import type { GeneratedFile } from './executor-generator';
2
+ /**
3
+ * Generate a utils.ts file with runtime helpers for CLI commands.
4
+ * Reads from the templates directory (cli-utils.ts) for proper type checking.
5
+ *
6
+ * Includes type coercion (string CLI args -> proper GraphQL types),
7
+ * field filtering (strip extra minimist fields like _ and tty),
8
+ * and mutation input parsing.
9
+ */
10
+ export declare function generateUtilsFile(): GeneratedFile;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.generateUtilsFile = generateUtilsFile;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const utils_1 = require("../utils");
40
+ /**
41
+ * Find the cli-utils template file path.
42
+ * Templates are at ../templates/ relative to this file in both src/ and dist/.
43
+ */
44
+ function findTemplateFile(templateName) {
45
+ const templatePath = path.join(__dirname, '../templates', templateName);
46
+ if (fs.existsSync(templatePath)) {
47
+ return templatePath;
48
+ }
49
+ throw new Error(`Could not find template file: ${templateName}. ` +
50
+ `Searched in: ${templatePath}`);
51
+ }
52
+ /**
53
+ * Read a template file and replace the header with generated file header.
54
+ * Follows the same pattern as ORM client-generator.ts readTemplateFile().
55
+ */
56
+ function readTemplateFile(templateName, description) {
57
+ const templatePath = findTemplateFile(templateName);
58
+ let content = fs.readFileSync(templatePath, 'utf-8');
59
+ // Replace the source file header comment with the generated file header
60
+ // Match the header pattern used in template files
61
+ const headerPattern = /\/\*\*[\s\S]*?\* NOTE: This file is read at codegen time and written to output\.[\s\S]*?\*\/\n*/;
62
+ content = content.replace(headerPattern, (0, utils_1.getGeneratedFileHeader)(description) + '\n');
63
+ return content;
64
+ }
65
+ /**
66
+ * Generate a utils.ts file with runtime helpers for CLI commands.
67
+ * Reads from the templates directory (cli-utils.ts) for proper type checking.
68
+ *
69
+ * Includes type coercion (string CLI args -> proper GraphQL types),
70
+ * field filtering (strip extra minimist fields like _ and tty),
71
+ * and mutation input parsing.
72
+ */
73
+ function generateUtilsFile() {
74
+ return {
75
+ fileName: 'utils.ts',
76
+ content: readTemplateFile('cli-utils.ts', 'CLI utility functions for type coercion and input handling'),
77
+ };
78
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * CLI utility functions for type coercion and input handling
3
+ *
4
+ * This is the RUNTIME code that gets copied to generated output.
5
+ * Provides helpers for CLI commands: type coercion (string CLI args -> proper
6
+ * GraphQL types), field filtering (strip extra minimist fields), and
7
+ * mutation input parsing.
8
+ *
9
+ * NOTE: This file is read at codegen time and written to output.
10
+ * Any changes here will affect all generated CLI utils.
11
+ */
12
+
13
+ export type FieldType =
14
+ | 'string'
15
+ | 'boolean'
16
+ | 'int'
17
+ | 'float'
18
+ | 'json'
19
+ | 'uuid'
20
+ | 'enum';
21
+
22
+ export interface FieldSchema {
23
+ [fieldName: string]: FieldType;
24
+ }
25
+
26
+ /**
27
+ * Coerce CLI string arguments to their proper GraphQL types based on a field schema.
28
+ * CLI args always arrive as strings from minimist, but GraphQL expects
29
+ * Boolean, Int, Float, JSON, etc.
30
+ */
31
+ export function coerceAnswers(
32
+ answers: Record<string, unknown>,
33
+ schema: FieldSchema,
34
+ ): Record<string, unknown> {
35
+ const result: Record<string, unknown> = { ...answers };
36
+
37
+ for (const [key, value] of Object.entries(result)) {
38
+ const fieldType = schema[key];
39
+ if (!fieldType || value === undefined || value === null) continue;
40
+
41
+ const strValue = String(value);
42
+
43
+ // Empty strings become undefined for non-string types
44
+ if (strValue === '' && fieldType !== 'string') {
45
+ result[key] = undefined;
46
+ continue;
47
+ }
48
+
49
+ switch (fieldType) {
50
+ case 'boolean':
51
+ if (typeof value === 'boolean') break;
52
+ result[key] =
53
+ strValue === 'true' || strValue === '1' || strValue === 'yes';
54
+ break;
55
+ case 'int':
56
+ if (typeof value === 'number') break;
57
+ {
58
+ const parsed = parseInt(strValue, 10);
59
+ result[key] = isNaN(parsed) ? undefined : parsed;
60
+ }
61
+ break;
62
+ case 'float':
63
+ if (typeof value === 'number') break;
64
+ {
65
+ const parsed = parseFloat(strValue);
66
+ result[key] = isNaN(parsed) ? undefined : parsed;
67
+ }
68
+ break;
69
+ case 'json':
70
+ if (typeof value === 'object') break;
71
+ if (strValue === '') {
72
+ result[key] = undefined;
73
+ } else {
74
+ try {
75
+ result[key] = JSON.parse(strValue);
76
+ } catch {
77
+ result[key] = undefined;
78
+ }
79
+ }
80
+ break;
81
+ case 'uuid':
82
+ // Empty UUIDs become undefined
83
+ if (strValue === '') {
84
+ result[key] = undefined;
85
+ }
86
+ break;
87
+ case 'enum':
88
+ // Enums stay as strings but empty ones become undefined
89
+ if (strValue === '') {
90
+ result[key] = undefined;
91
+ }
92
+ break;
93
+ default:
94
+ // String type: empty strings also become undefined to avoid
95
+ // sending empty strings for optional fields
96
+ if (strValue === '') {
97
+ result[key] = undefined;
98
+ }
99
+ break;
100
+ }
101
+ }
102
+
103
+ return result;
104
+ }
105
+
106
+ /**
107
+ * Strip undefined values and filter to only schema-defined keys.
108
+ * This removes extra fields injected by minimist (like _, tty, etc.)
109
+ * and any fields that were coerced to undefined.
110
+ */
111
+ export function stripUndefined(
112
+ obj: Record<string, unknown>,
113
+ schema?: FieldSchema,
114
+ ): Record<string, unknown> {
115
+ const result: Record<string, unknown> = {};
116
+ const allowedKeys = schema ? new Set(Object.keys(schema)) : null;
117
+
118
+ for (const [key, value] of Object.entries(obj)) {
119
+ if (value === undefined) continue;
120
+ if (allowedKeys && !allowedKeys.has(key)) continue;
121
+ result[key] = value;
122
+ }
123
+
124
+ return result;
125
+ }
126
+
127
+ /**
128
+ * Parse mutation input from CLI.
129
+ * Custom mutation commands receive an `input` field as a JSON string
130
+ * from the CLI prompt. This parses it into a proper object.
131
+ */
132
+ export function parseMutationInput(
133
+ answers: Record<string, unknown>,
134
+ ): Record<string, unknown> {
135
+ if (typeof answers.input === 'string') {
136
+ try {
137
+ const parsed = JSON.parse(answers.input);
138
+ return { ...answers, input: parsed };
139
+ } catch {
140
+ return answers;
141
+ }
142
+ }
143
+ return answers;
144
+ }
@@ -33,8 +33,40 @@ function buildErrorCatch(errorMessage) {
33
33
  t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('process'), t.identifier('exit')), [t.numericLiteral(1)])),
34
34
  ]));
35
35
  }
36
- function buildOrmCustomCall(opKind, opName, argsExpr) {
37
- return t.callExpression(t.memberExpression(t.callExpression(t.memberExpression(t.memberExpression(t.identifier('client'), t.identifier(opKind)), t.identifier(opName)), [argsExpr]), t.identifier('execute')), []);
36
+ /**
37
+ * Unwrap NON_NULL / LIST wrappers to get the underlying named type.
38
+ */
39
+ function unwrapType(ref) {
40
+ if ((ref.kind === 'NON_NULL' || ref.kind === 'LIST') && ref.ofType) {
41
+ return unwrapType(ref.ofType);
42
+ }
43
+ return ref;
44
+ }
45
+ /**
46
+ * Build a select object expression from return-type fields.
47
+ * If the return type has known fields, generates { field1: true, field2: true, ... }.
48
+ * Falls back to { clientMutationId: true } for mutations without known fields.
49
+ */
50
+ function buildSelectObject(returnType, isMutation) {
51
+ const base = unwrapType(returnType);
52
+ if (base.fields && base.fields.length > 0) {
53
+ return t.objectExpression(base.fields.map((f) => t.objectProperty(t.identifier(f.name), t.booleanLiteral(true))));
54
+ }
55
+ // Fallback: all PostGraphile mutation payloads have clientMutationId
56
+ if (isMutation) {
57
+ return t.objectExpression([
58
+ t.objectProperty(t.identifier('clientMutationId'), t.booleanLiteral(true)),
59
+ ]);
60
+ }
61
+ return t.objectExpression([]);
62
+ }
63
+ function buildOrmCustomCall(opKind, opName, argsExpr, selectExpr) {
64
+ return t.callExpression(t.memberExpression(t.callExpression(t.memberExpression(t.memberExpression(t.identifier('client'), t.identifier(opKind)), t.identifier(opName)), [
65
+ argsExpr,
66
+ t.objectExpression([
67
+ t.objectProperty(t.identifier('select'), selectExpr),
68
+ ]),
69
+ ]), t.identifier('execute')), []);
38
70
  }
39
71
  export function generateCustomCommand(op, options) {
40
72
  const commandName = toKebabCase(op.name);
@@ -45,8 +77,19 @@ export function generateCustomCommand(op, options) {
45
77
  if (options?.saveToken) {
46
78
  imports.push('getStore');
47
79
  }
80
+ // Check if any argument is an INPUT_OBJECT (i.e. takes JSON input like { input: SomeInput })
81
+ const hasInputObjectArg = op.args.some((arg) => {
82
+ const base = unwrapType(arg.type);
83
+ return base.kind === 'INPUT_OBJECT';
84
+ });
85
+ const utilsPath = options?.executorImportPath
86
+ ? options.executorImportPath.replace(/\/executor$/, '/utils')
87
+ : '../utils';
48
88
  statements.push(createImportDeclaration('inquirerer', ['CLIOptions', 'Inquirerer']));
49
89
  statements.push(createImportDeclaration(executorPath, imports));
90
+ if (hasInputObjectArg) {
91
+ statements.push(createImportDeclaration(utilsPath, ['parseMutationInput']));
92
+ }
50
93
  const questionsArray = op.args.length > 0
51
94
  ? buildQuestionsArray(op.args)
52
95
  : t.arrayExpression([]);
@@ -75,11 +118,23 @@ export function generateCustomCommand(op, options) {
75
118
  bodyStatements.push(t.variableDeclaration('const', [
76
119
  t.variableDeclarator(t.identifier('client'), t.callExpression(t.identifier('getClient'), getClientArgs)),
77
120
  ]));
121
+ // For mutations with INPUT_OBJECT args (like `input: SignUpInput`),
122
+ // parse JSON strings from CLI into proper objects
123
+ if (hasInputObjectArg && op.args.length > 0) {
124
+ bodyStatements.push(t.variableDeclaration('const', [
125
+ t.variableDeclarator(t.identifier('parsedAnswers'), t.callExpression(t.identifier('parseMutationInput'), [
126
+ t.identifier('answers'),
127
+ ])),
128
+ ]));
129
+ }
78
130
  const argsExpr = op.args.length > 0
79
- ? t.identifier('answers')
131
+ ? (hasInputObjectArg
132
+ ? t.identifier('parsedAnswers')
133
+ : t.identifier('answers'))
80
134
  : t.objectExpression([]);
135
+ const selectExpr = buildSelectObject(op.returnType, op.kind === 'mutation');
81
136
  bodyStatements.push(t.variableDeclaration('const', [
82
- t.variableDeclarator(t.identifier('result'), t.awaitExpression(buildOrmCustomCall(opKind, op.name, argsExpr))),
137
+ t.variableDeclarator(t.identifier('result'), t.awaitExpression(buildOrmCustomCall(opKind, op.name, argsExpr, selectExpr))),
83
138
  ]));
84
139
  if (options?.saveToken) {
85
140
  bodyStatements.push(t.ifStatement(t.logicalExpression('&&', t.memberExpression(t.identifier('argv'), t.identifier('saveToken')), t.identifier('result')), t.blockStatement([
@@ -50,4 +50,5 @@ export { generateReadme, generateAgentsDocs, getCliMcpTools, generateSkills, gen
50
50
  export type { MultiTargetDocsInput } from './docs-generator';
51
51
  export { resolveDocsConfig } from '../docs-utils';
52
52
  export type { GeneratedDocFile, McpTool } from '../docs-utils';
53
+ export { generateUtilsFile } from './utils-generator';
53
54
  export type { GeneratedFile, MultiTargetExecutorInput } from './executor-generator';
@@ -3,6 +3,7 @@ import { generateCustomCommand } from './custom-command-generator';
3
3
  import { generateExecutorFile, generateMultiTargetExecutorFile } from './executor-generator';
4
4
  import { generateAuthCommand, generateAuthCommandWithName, generateContextCommand, generateMultiTargetContextCommand, } from './infra-generator';
5
5
  import { generateTableCommand } from './table-command-generator';
6
+ import { generateUtilsFile } from './utils-generator';
6
7
  export function generateCli(options) {
7
8
  const { tables, customOperations, config } = options;
8
9
  const files = [];
@@ -12,6 +13,8 @@ export function generateCli(options) {
12
13
  : 'app';
13
14
  const executorFile = generateExecutorFile(toolName);
14
15
  files.push(executorFile);
16
+ const utilsFile = generateUtilsFile();
17
+ files.push(utilsFile);
15
18
  const contextFile = generateContextCommand(toolName);
16
19
  files.push(contextFile);
17
20
  const authFile = generateAuthCommand(toolName);
@@ -36,7 +39,7 @@ export function generateCli(options) {
36
39
  tables: tables.length,
37
40
  customQueries: customOperations?.queries.length ?? 0,
38
41
  customMutations: customOperations?.mutations.length ?? 0,
39
- infraFiles: 3,
42
+ infraFiles: 4,
40
43
  totalFiles: files.length,
41
44
  },
42
45
  };
@@ -64,6 +67,8 @@ export function generateMultiTargetCli(options) {
64
67
  }));
65
68
  const executorFile = generateMultiTargetExecutorFile(toolName, executorInputs);
66
69
  files.push(executorFile);
70
+ const utilsFile = generateUtilsFile();
71
+ files.push(utilsFile);
67
72
  const contextFile = generateMultiTargetContextCommand(toolName, builtinNames.context, targets.map((t) => ({ name: t.name, endpoint: t.endpoint })));
68
73
  files.push(contextFile);
69
74
  const authFile = generateAuthCommandWithName(toolName, builtinNames.auth);
@@ -114,7 +119,7 @@ export function generateMultiTargetCli(options) {
114
119
  tables: totalTables,
115
120
  customQueries: totalQueries,
116
121
  customMutations: totalMutations,
117
- infraFiles: 3,
122
+ infraFiles: 4,
118
123
  totalFiles: files.length,
119
124
  },
120
125
  };
@@ -126,3 +131,4 @@ export { generateCommandMap, generateMultiTargetCommandMap } from './command-map
126
131
  export { generateContextCommand, generateAuthCommand, generateMultiTargetContextCommand, generateAuthCommandWithName, } from './infra-generator';
127
132
  export { generateReadme, generateAgentsDocs, getCliMcpTools, generateSkills, generateMultiTargetReadme, generateMultiTargetAgentsDocs, getMultiTargetCliMcpTools, generateMultiTargetSkills, } from './docs-generator';
128
133
  export { resolveDocsConfig } from '../docs-utils';
134
+ export { generateUtilsFile } from './utils-generator';
@@ -656,7 +656,7 @@ function buildSetTokenHandler() {
656
656
  t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('store'), t.identifier('setCredentials')), [
657
657
  t.memberExpression(t.identifier('current'), t.identifier('name')),
658
658
  t.objectExpression([
659
- t.objectProperty(t.identifier('token'), t.callExpression(t.memberExpression(t.callExpression(t.memberExpression(t.identifier('String'), t.identifier('call')), [
659
+ t.objectProperty(t.identifier('token'), t.callExpression(t.memberExpression(t.callExpression(t.identifier('String'), [
660
660
  t.logicalExpression('||', t.identifier('tokenValue'), t.stringLiteral('')),
661
661
  ]), t.identifier('trim')), [])),
662
662
  ]),
@@ -8,6 +8,40 @@ function createImportDeclaration(moduleSpecifier, namedImports, typeOnly = false
8
8
  decl.importKind = typeOnly ? 'type' : 'value';
9
9
  return decl;
10
10
  }
11
+ /**
12
+ * Build a field schema object that maps field names to their GraphQL types.
13
+ * This is used at runtime for type coercion (string CLI args → proper types).
14
+ * e.g., { name: 'string', isActive: 'boolean', position: 'int', status: 'enum' }
15
+ */
16
+ function buildFieldSchemaObject(table) {
17
+ const fields = getScalarFields(table);
18
+ return t.objectExpression(fields.map((f) => {
19
+ const gqlType = f.type.gqlType.replace(/!/g, '');
20
+ let schemaType;
21
+ switch (gqlType) {
22
+ case 'Boolean':
23
+ schemaType = 'boolean';
24
+ break;
25
+ case 'Int':
26
+ case 'BigInt':
27
+ schemaType = 'int';
28
+ break;
29
+ case 'Float':
30
+ schemaType = 'float';
31
+ break;
32
+ case 'JSON':
33
+ case 'GeoJSON':
34
+ schemaType = 'json';
35
+ break;
36
+ case 'UUID':
37
+ schemaType = 'uuid';
38
+ break;
39
+ default:
40
+ schemaType = 'string';
41
+ }
42
+ return t.objectProperty(t.identifier(f.name), t.stringLiteral(schemaType));
43
+ }));
44
+ }
11
45
  function buildSelectObject(table) {
12
46
  const fields = getScalarFields(table);
13
47
  return t.objectExpression(fields.map((f) => t.objectProperty(t.identifier(f.name), t.booleanLiteral(true))));
@@ -147,36 +181,52 @@ function buildMutationHandler(table, operation, targetName) {
147
181
  : buildSelectObject(table);
148
182
  let ormArgs;
149
183
  if (operation === 'create') {
150
- const dataProps = editableFields.map((f) => t.objectProperty(t.identifier(f.name), t.memberExpression(t.identifier('answers'), t.identifier(f.name)), false, true));
184
+ const dataProps = editableFields.map((f) => t.objectProperty(t.identifier(f.name), t.memberExpression(t.identifier('cleanedData'), t.identifier(f.name)), false, true));
151
185
  ormArgs = t.objectExpression([
152
186
  t.objectProperty(t.identifier('data'), t.objectExpression(dataProps)),
153
187
  t.objectProperty(t.identifier('select'), selectObj),
154
188
  ]);
155
189
  }
156
190
  else if (operation === 'update') {
157
- const dataProps = editableFields.map((f) => t.objectProperty(t.identifier(f.name), t.memberExpression(t.identifier('answers'), t.identifier(f.name)), false, true));
191
+ const dataProps = editableFields.map((f) => t.objectProperty(t.identifier(f.name), t.memberExpression(t.identifier('cleanedData'), t.identifier(f.name)), false, true));
158
192
  ormArgs = t.objectExpression([
159
- t.objectProperty(t.identifier(pk.name), t.memberExpression(t.identifier('answers'), t.identifier(pk.name))),
193
+ t.objectProperty(t.identifier('where'), t.objectExpression([
194
+ t.objectProperty(t.identifier(pk.name), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(pk.name)), t.tsStringKeyword())),
195
+ ])),
160
196
  t.objectProperty(t.identifier('data'), t.objectExpression(dataProps)),
161
197
  t.objectProperty(t.identifier('select'), selectObj),
162
198
  ]);
163
199
  }
164
200
  else {
165
201
  ormArgs = t.objectExpression([
166
- t.objectProperty(t.identifier(pk.name), t.memberExpression(t.identifier('answers'), t.identifier(pk.name))),
202
+ t.objectProperty(t.identifier('where'), t.objectExpression([
203
+ t.objectProperty(t.identifier(pk.name), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(pk.name)), t.tsStringKeyword())),
204
+ ])),
167
205
  t.objectProperty(t.identifier('select'), selectObj),
168
206
  ]);
169
207
  }
170
208
  const tryBody = [
171
209
  t.variableDeclaration('const', [
172
- t.variableDeclarator(t.identifier('answers'), t.awaitExpression(t.callExpression(t.memberExpression(t.identifier('prompter'), t.identifier('prompt')), [t.identifier('argv'), t.arrayExpression(questions)]))),
210
+ t.variableDeclarator(t.identifier('rawAnswers'), t.awaitExpression(t.callExpression(t.memberExpression(t.identifier('prompter'), t.identifier('prompt')), [t.identifier('argv'), t.arrayExpression(questions)]))),
173
211
  ]),
174
- buildGetClientStatement(targetName),
175
212
  t.variableDeclaration('const', [
176
- t.variableDeclarator(t.identifier('result'), t.awaitExpression(buildOrmCall(singularName, operation, ormArgs))),
213
+ t.variableDeclarator(t.identifier('answers'), t.callExpression(t.identifier('coerceAnswers'), [
214
+ t.identifier('rawAnswers'),
215
+ t.identifier('fieldSchema'),
216
+ ])),
177
217
  ]),
178
- buildJsonLog(t.identifier('result')),
179
218
  ];
219
+ if (operation !== 'delete') {
220
+ tryBody.push(t.variableDeclaration('const', [
221
+ t.variableDeclarator(t.identifier('cleanedData'), t.callExpression(t.identifier('stripUndefined'), [
222
+ t.identifier('answers'),
223
+ t.identifier('fieldSchema'),
224
+ ])),
225
+ ]));
226
+ }
227
+ tryBody.push(buildGetClientStatement(targetName), t.variableDeclaration('const', [
228
+ t.variableDeclarator(t.identifier('result'), t.awaitExpression(buildOrmCall(singularName, operation, ormArgs))),
229
+ ]), buildJsonLog(t.identifier('result')));
180
230
  const argvParam = t.identifier('argv');
181
231
  argvParam.typeAnnotation = buildArgvType();
182
232
  const prompterParam = t.identifier('prompter');
@@ -197,6 +247,12 @@ export function generateTableCommand(table, options) {
197
247
  'extractFirst',
198
248
  ]));
199
249
  statements.push(createImportDeclaration(executorPath, ['getClient']));
250
+ const utilsPath = options?.targetName ? '../../utils' : '../utils';
251
+ statements.push(createImportDeclaration(utilsPath, ['coerceAnswers', 'stripUndefined']));
252
+ // Generate field schema for type coercion
253
+ statements.push(t.variableDeclaration('const', [
254
+ t.variableDeclarator(t.identifier('fieldSchema'), buildFieldSchemaObject(table)),
255
+ ]));
200
256
  const subcommands = ['list', 'get', 'create', 'update', 'delete'];
201
257
  const usageLines = [
202
258
  '',
@@ -0,0 +1,10 @@
1
+ import type { GeneratedFile } from './executor-generator';
2
+ /**
3
+ * Generate a utils.ts file with runtime helpers for CLI commands.
4
+ * Reads from the templates directory (cli-utils.ts) for proper type checking.
5
+ *
6
+ * Includes type coercion (string CLI args -> proper GraphQL types),
7
+ * field filtering (strip extra minimist fields like _ and tty),
8
+ * and mutation input parsing.
9
+ */
10
+ export declare function generateUtilsFile(): GeneratedFile;
@@ -0,0 +1,42 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { getGeneratedFileHeader } from '../utils';
4
+ /**
5
+ * Find the cli-utils template file path.
6
+ * Templates are at ../templates/ relative to this file in both src/ and dist/.
7
+ */
8
+ function findTemplateFile(templateName) {
9
+ const templatePath = path.join(__dirname, '../templates', templateName);
10
+ if (fs.existsSync(templatePath)) {
11
+ return templatePath;
12
+ }
13
+ throw new Error(`Could not find template file: ${templateName}. ` +
14
+ `Searched in: ${templatePath}`);
15
+ }
16
+ /**
17
+ * Read a template file and replace the header with generated file header.
18
+ * Follows the same pattern as ORM client-generator.ts readTemplateFile().
19
+ */
20
+ function readTemplateFile(templateName, description) {
21
+ const templatePath = findTemplateFile(templateName);
22
+ let content = fs.readFileSync(templatePath, 'utf-8');
23
+ // Replace the source file header comment with the generated file header
24
+ // Match the header pattern used in template files
25
+ const headerPattern = /\/\*\*[\s\S]*?\* NOTE: This file is read at codegen time and written to output\.[\s\S]*?\*\/\n*/;
26
+ content = content.replace(headerPattern, getGeneratedFileHeader(description) + '\n');
27
+ return content;
28
+ }
29
+ /**
30
+ * Generate a utils.ts file with runtime helpers for CLI commands.
31
+ * Reads from the templates directory (cli-utils.ts) for proper type checking.
32
+ *
33
+ * Includes type coercion (string CLI args -> proper GraphQL types),
34
+ * field filtering (strip extra minimist fields like _ and tty),
35
+ * and mutation input parsing.
36
+ */
37
+ export function generateUtilsFile() {
38
+ return {
39
+ fileName: 'utils.ts',
40
+ content: readTemplateFile('cli-utils.ts', 'CLI utility functions for type coercion and input handling'),
41
+ };
42
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constructive-io/graphql-codegen",
3
- "version": "4.1.1",
3
+ "version": "4.1.3",
4
4
  "description": "GraphQL SDK generator for Constructive databases with React Query hooks",
5
5
  "keywords": [
6
6
  "graphql",
@@ -100,5 +100,5 @@
100
100
  "tsx": "^4.21.0",
101
101
  "typescript": "^5.9.3"
102
102
  },
103
- "gitHead": "921594a77964d3467261c70fed1d529eddabb549"
103
+ "gitHead": "f80e2e5e112cab91eac53e61c5b257f4ac88efb2"
104
104
  }