@constructive-io/graphql-codegen 4.8.0 → 4.9.0
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.
- package/core/codegen/cli/command-map-generator.js +36 -11
- package/core/codegen/cli/custom-command-generator.js +59 -13
- package/core/codegen/cli/executor-generator.js +20 -6
- package/core/codegen/cli/infra-generator.js +17 -14
- package/core/codegen/cli/table-command-generator.js +218 -55
- package/esm/core/codegen/cli/command-map-generator.js +36 -11
- package/esm/core/codegen/cli/custom-command-generator.js +60 -14
- package/esm/core/codegen/cli/executor-generator.js +20 -6
- package/esm/core/codegen/cli/infra-generator.js +17 -14
- package/esm/core/codegen/cli/table-command-generator.js +219 -56
- package/package.json +2 -2
|
@@ -38,7 +38,6 @@ const t = __importStar(require("@babel/types"));
|
|
|
38
38
|
const komoji_1 = require("komoji");
|
|
39
39
|
const babel_ast_1 = require("../babel-ast");
|
|
40
40
|
const utils_1 = require("../utils");
|
|
41
|
-
const utils_2 = require("../utils");
|
|
42
41
|
function createImportDeclaration(moduleSpecifier, namedImports, typeOnly = false) {
|
|
43
42
|
const specifiers = namedImports.map((name) => t.importSpecifier(t.identifier(name), t.identifier(name)));
|
|
44
43
|
const decl = t.importDeclaration(specifiers, t.stringLiteral(moduleSpecifier));
|
|
@@ -50,6 +49,60 @@ function createImportDeclaration(moduleSpecifier, namedImports, typeOnly = false
|
|
|
50
49
|
* This is used at runtime for type coercion (string CLI args → proper types).
|
|
51
50
|
* e.g., { name: 'string', isActive: 'boolean', position: 'int', status: 'enum' }
|
|
52
51
|
*/
|
|
52
|
+
/**
|
|
53
|
+
* Returns a t.TSType node for the appropriate TypeScript type assertion
|
|
54
|
+
* based on a field's GraphQL type. Used to cast `cleanedData.fieldName`
|
|
55
|
+
* to the correct type expected by the ORM.
|
|
56
|
+
*/
|
|
57
|
+
/**
|
|
58
|
+
* Known GraphQL scalar types. Anything not in this set is an enum or custom type.
|
|
59
|
+
*/
|
|
60
|
+
const KNOWN_SCALARS = new Set([
|
|
61
|
+
'String', 'Boolean', 'Int', 'BigInt', 'Float', 'UUID',
|
|
62
|
+
'JSON', 'GeoJSON', 'Datetime', 'Date', 'Time', 'Cursor',
|
|
63
|
+
'BigFloat', 'Interval',
|
|
64
|
+
]);
|
|
65
|
+
/**
|
|
66
|
+
* Returns true if the GraphQL type is a known scalar.
|
|
67
|
+
* Non-scalar types (enums, custom input types) need different handling.
|
|
68
|
+
*/
|
|
69
|
+
function isKnownScalar(gqlType) {
|
|
70
|
+
return KNOWN_SCALARS.has(gqlType.replace(/!/g, ''));
|
|
71
|
+
}
|
|
72
|
+
function getTsTypeForField(field) {
|
|
73
|
+
const gqlType = field.type.gqlType.replace(/!/g, '');
|
|
74
|
+
// For non-scalar types (enums, custom types), return null to signal
|
|
75
|
+
// that no type assertion should be emitted — the value will be passed
|
|
76
|
+
// without casting, which avoids "string is not assignable to EnumType" errors.
|
|
77
|
+
if (!isKnownScalar(gqlType)) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
// Determine the base scalar type
|
|
81
|
+
// Note: ORM input types flatten array fields to their scalar base type
|
|
82
|
+
// (e.g., _uuid[] in PG -> string in the ORM input), so we do NOT wrap
|
|
83
|
+
// in tsArrayType here.
|
|
84
|
+
switch (gqlType) {
|
|
85
|
+
case 'Boolean':
|
|
86
|
+
return t.tsBooleanKeyword();
|
|
87
|
+
case 'Int':
|
|
88
|
+
case 'BigInt':
|
|
89
|
+
case 'Float':
|
|
90
|
+
case 'BigFloat':
|
|
91
|
+
return t.tsNumberKeyword();
|
|
92
|
+
case 'JSON':
|
|
93
|
+
case 'GeoJSON':
|
|
94
|
+
return t.tsTypeReference(t.identifier('Record'), t.tsTypeParameterInstantiation([
|
|
95
|
+
t.tsStringKeyword(),
|
|
96
|
+
t.tsUnknownKeyword(),
|
|
97
|
+
]));
|
|
98
|
+
case 'Interval':
|
|
99
|
+
// IntervalInput is a complex type, skip assertion
|
|
100
|
+
return null;
|
|
101
|
+
case 'UUID':
|
|
102
|
+
default:
|
|
103
|
+
return t.tsStringKeyword();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
53
106
|
function buildFieldSchemaObject(table) {
|
|
54
107
|
const fields = (0, utils_1.getScalarFields)(table);
|
|
55
108
|
return t.objectExpression(fields.map((f) => {
|
|
@@ -162,8 +215,11 @@ function buildGetHandler(table, targetName) {
|
|
|
162
215
|
t.objectProperty(t.identifier('message'), t.stringLiteral(pk.name)),
|
|
163
216
|
t.objectProperty(t.identifier('required'), t.booleanLiteral(true)),
|
|
164
217
|
]);
|
|
218
|
+
const pkTsType = pk.gqlType === 'Int' || pk.gqlType === 'BigInt'
|
|
219
|
+
? t.tsNumberKeyword()
|
|
220
|
+
: t.tsStringKeyword();
|
|
165
221
|
const ormArgs = t.objectExpression([
|
|
166
|
-
t.objectProperty(t.identifier(pk.name), t.memberExpression(t.identifier('answers'), t.identifier(pk.name))),
|
|
222
|
+
t.objectProperty(t.identifier(pk.name), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(pk.name)), pkTsType)),
|
|
167
223
|
t.objectProperty(t.identifier('select'), selectObj),
|
|
168
224
|
]);
|
|
169
225
|
const tryBody = [
|
|
@@ -189,19 +245,16 @@ function buildGetHandler(table, targetName) {
|
|
|
189
245
|
* Looks up the CreateXInput -> inner input type (e.g. DatabaseInput) in the
|
|
190
246
|
* TypeRegistry and checks each field's defaultValue from introspection.
|
|
191
247
|
*/
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
if (!
|
|
200
|
-
return
|
|
201
|
-
|
|
202
|
-
// Find the inner input type that contains the actual field definitions
|
|
203
|
-
for (const inputField of createInputType.inputFields) {
|
|
204
|
-
// The inner field's type name is the actual input type (e.g. DatabaseInput)
|
|
248
|
+
/**
|
|
249
|
+
* Resolve the inner input type from a CreateXInput or UpdateXInput type.
|
|
250
|
+
* The CreateXInput has an inner field (e.g. "database" of type DatabaseInput)
|
|
251
|
+
* that contains the actual field definitions.
|
|
252
|
+
*/
|
|
253
|
+
function resolveInnerInputType(inputTypeName, typeRegistry) {
|
|
254
|
+
const inputType = typeRegistry.get(inputTypeName);
|
|
255
|
+
if (!inputType?.inputFields)
|
|
256
|
+
return null;
|
|
257
|
+
for (const inputField of inputType.inputFields) {
|
|
205
258
|
const innerTypeName = inputField.type.name
|
|
206
259
|
|| inputField.type.ofType?.name
|
|
207
260
|
|| inputField.type.ofType?.ofType?.name;
|
|
@@ -210,29 +263,70 @@ function getFieldsWithDefaults(table, typeRegistry) {
|
|
|
210
263
|
const innerType = typeRegistry.get(innerTypeName);
|
|
211
264
|
if (!innerType?.inputFields)
|
|
212
265
|
continue;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
266
|
+
const fields = new Set(innerType.inputFields.map((f) => f.name));
|
|
267
|
+
return { name: innerTypeName, fields };
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
function getFieldsWithDefaults(table, typeRegistry) {
|
|
272
|
+
const fieldsWithDefaults = new Set();
|
|
273
|
+
if (!typeRegistry)
|
|
274
|
+
return fieldsWithDefaults;
|
|
275
|
+
const createInputTypeName = (0, utils_1.getCreateInputTypeName)(table);
|
|
276
|
+
const resolved = resolveInnerInputType(createInputTypeName, typeRegistry);
|
|
277
|
+
if (!resolved)
|
|
278
|
+
return fieldsWithDefaults;
|
|
279
|
+
const innerType = typeRegistry.get(resolved.name);
|
|
280
|
+
if (!innerType?.inputFields)
|
|
281
|
+
return fieldsWithDefaults;
|
|
282
|
+
for (const field of innerType.inputFields) {
|
|
283
|
+
if (field.defaultValue !== undefined) {
|
|
284
|
+
fieldsWithDefaults.add(field.name);
|
|
285
|
+
}
|
|
286
|
+
if (field.type.kind !== 'NON_NULL') {
|
|
287
|
+
fieldsWithDefaults.add(field.name);
|
|
222
288
|
}
|
|
223
289
|
}
|
|
224
290
|
return fieldsWithDefaults;
|
|
225
291
|
}
|
|
226
|
-
|
|
292
|
+
/**
|
|
293
|
+
* Get the set of field names that actually exist in the create/update input type.
|
|
294
|
+
* Fields not in this set (e.g. computed fields like searchTsvRank, hashUuid)
|
|
295
|
+
* should be excluded from the data object in create/update handlers.
|
|
296
|
+
*/
|
|
297
|
+
function getWritableFieldNames(table, typeRegistry) {
|
|
298
|
+
if (!typeRegistry)
|
|
299
|
+
return null;
|
|
300
|
+
const createInputTypeName = (0, utils_1.getCreateInputTypeName)(table);
|
|
301
|
+
const resolved = resolveInnerInputType(createInputTypeName, typeRegistry);
|
|
302
|
+
return resolved?.fields ?? null;
|
|
303
|
+
}
|
|
304
|
+
function buildMutationHandler(table, operation, targetName, typeRegistry, ormTypes) {
|
|
227
305
|
const { singularName } = (0, utils_1.getTableNames)(table);
|
|
228
306
|
const pkFields = (0, utils_1.getPrimaryKeyInfo)(table);
|
|
229
307
|
const pk = pkFields[0];
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
308
|
+
// Get the set of writable field names from the type registry
|
|
309
|
+
// This filters out computed fields (e.g. searchTsvRank, hashUuid) that exist
|
|
310
|
+
// on the entity type but not on the create/update input type.
|
|
311
|
+
const writableFields = getWritableFieldNames(table, typeRegistry);
|
|
234
312
|
// Get fields that have defaults from introspection (for create operations)
|
|
235
313
|
const fieldsWithDefaults = getFieldsWithDefaults(table, typeRegistry);
|
|
314
|
+
// For create: include fields that are in the create input type.
|
|
315
|
+
// For update/delete: always exclude the PK (it goes in `where`, not `data`).
|
|
316
|
+
// The ORM input-types generator always excludes these fields from create inputs
|
|
317
|
+
// (see EXCLUDED_MUTATION_FIELDS in input-types-generator.ts). We must match this
|
|
318
|
+
// to avoid generating data properties that don't exist on the ORM create type.
|
|
319
|
+
// For non-'id' PKs (e.g. NodeTypeRegistry.name), we allow them in create data
|
|
320
|
+
// since they are user-provided natural keys that DO appear in the create input.
|
|
321
|
+
const ORM_EXCLUDED_FIELDS = ['id', 'createdAt', 'updatedAt', 'nodeId'];
|
|
322
|
+
const editableFields = (0, utils_1.getScalarFields)(table).filter((f) =>
|
|
323
|
+
// For update/delete: always exclude PK (it goes in `where`, not `data`)
|
|
324
|
+
// For create: exclude PK only if it's in the ORM exclusion list (e.g. 'id')
|
|
325
|
+
(f.name !== pk.name || (operation === 'create' && !ORM_EXCLUDED_FIELDS.includes(pk.name))) &&
|
|
326
|
+
// Always exclude ORM-excluded fields (except PK which is handled above)
|
|
327
|
+
(f.name === pk.name || !ORM_EXCLUDED_FIELDS.includes(f.name)) &&
|
|
328
|
+
// If we have type registry info, only include fields that exist in the input type
|
|
329
|
+
(writableFields === null || writableFields.has(f.name)));
|
|
236
330
|
const questions = [];
|
|
237
331
|
if (operation === 'update' || operation === 'delete') {
|
|
238
332
|
questions.push(t.objectExpression([
|
|
@@ -247,12 +341,19 @@ function buildMutationHandler(table, operation, targetName, typeRegistry) {
|
|
|
247
341
|
// For create: field is required only if it has no default value
|
|
248
342
|
// For update: all fields are optional (user only updates what they want)
|
|
249
343
|
const isRequired = operation === 'create' && !fieldsWithDefaults.has(field.name);
|
|
250
|
-
|
|
344
|
+
const hasDefault = fieldsWithDefaults.has(field.name);
|
|
345
|
+
const questionProps = [
|
|
251
346
|
t.objectProperty(t.identifier('type'), t.stringLiteral('text')),
|
|
252
347
|
t.objectProperty(t.identifier('name'), t.stringLiteral(field.name)),
|
|
253
348
|
t.objectProperty(t.identifier('message'), t.stringLiteral(field.name)),
|
|
254
349
|
t.objectProperty(t.identifier('required'), t.booleanLiteral(isRequired)),
|
|
255
|
-
]
|
|
350
|
+
];
|
|
351
|
+
// Skip prompting for fields with backend-managed defaults.
|
|
352
|
+
// The field still appears in man pages and can be overridden via CLI flags.
|
|
353
|
+
if (hasDefault) {
|
|
354
|
+
questionProps.push(t.objectProperty(t.identifier('skipPrompt'), t.booleanLiteral(true)));
|
|
355
|
+
}
|
|
356
|
+
questions.push(t.objectExpression(questionProps));
|
|
256
357
|
}
|
|
257
358
|
}
|
|
258
359
|
const selectObj = operation === 'delete'
|
|
@@ -261,27 +362,36 @@ function buildMutationHandler(table, operation, targetName, typeRegistry) {
|
|
|
261
362
|
])
|
|
262
363
|
: buildSelectObject(table);
|
|
263
364
|
let ormArgs;
|
|
365
|
+
// Build data properties without individual type assertions.
|
|
366
|
+
// Instead, we build a plain object from cleanedData and cast the entire
|
|
367
|
+
// data value through `unknown` to bridge the type gap between
|
|
368
|
+
// Record<string, unknown> and the ORM's specific input type.
|
|
369
|
+
// This handles scalars, enums (string literal unions like ObjectCategory),
|
|
370
|
+
// and array fields uniformly without needing to import each type.
|
|
371
|
+
const buildDataProps = () => editableFields.map((f) => t.objectProperty(t.identifier(f.name), t.memberExpression(t.identifier('cleanedData'), t.identifier(f.name))));
|
|
264
372
|
if (operation === 'create') {
|
|
265
|
-
const dataProps = editableFields.map((f) => t.objectProperty(t.identifier(f.name), t.memberExpression(t.identifier('cleanedData'), t.identifier(f.name)), false, true));
|
|
266
373
|
ormArgs = t.objectExpression([
|
|
267
|
-
t.objectProperty(t.identifier('data'), t.objectExpression(
|
|
374
|
+
t.objectProperty(t.identifier('data'), t.objectExpression(buildDataProps())),
|
|
268
375
|
t.objectProperty(t.identifier('select'), selectObj),
|
|
269
376
|
]);
|
|
270
377
|
}
|
|
271
378
|
else if (operation === 'update') {
|
|
272
|
-
const dataProps = editableFields.map((f) => t.objectProperty(t.identifier(f.name), t.memberExpression(t.identifier('cleanedData'), t.identifier(f.name)), false, true));
|
|
273
379
|
ormArgs = t.objectExpression([
|
|
274
380
|
t.objectProperty(t.identifier('where'), t.objectExpression([
|
|
275
|
-
t.objectProperty(t.identifier(pk.name), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(pk.name)),
|
|
381
|
+
t.objectProperty(t.identifier(pk.name), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(pk.name)), pk.gqlType === 'Int' || pk.gqlType === 'BigInt'
|
|
382
|
+
? t.tsNumberKeyword()
|
|
383
|
+
: t.tsStringKeyword())),
|
|
276
384
|
])),
|
|
277
|
-
t.objectProperty(t.identifier('data'), t.objectExpression(
|
|
385
|
+
t.objectProperty(t.identifier('data'), t.objectExpression(buildDataProps())),
|
|
278
386
|
t.objectProperty(t.identifier('select'), selectObj),
|
|
279
387
|
]);
|
|
280
388
|
}
|
|
281
389
|
else {
|
|
282
390
|
ormArgs = t.objectExpression([
|
|
283
391
|
t.objectProperty(t.identifier('where'), t.objectExpression([
|
|
284
|
-
t.objectProperty(t.identifier(pk.name), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(pk.name)),
|
|
392
|
+
t.objectProperty(t.identifier(pk.name), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(pk.name)), pk.gqlType === 'Int' || pk.gqlType === 'BigInt'
|
|
393
|
+
? t.tsNumberKeyword()
|
|
394
|
+
: t.tsStringKeyword())),
|
|
285
395
|
])),
|
|
286
396
|
t.objectProperty(t.identifier('select'), selectObj),
|
|
287
397
|
]);
|
|
@@ -298,11 +408,25 @@ function buildMutationHandler(table, operation, targetName, typeRegistry) {
|
|
|
298
408
|
]),
|
|
299
409
|
];
|
|
300
410
|
if (operation !== 'delete') {
|
|
411
|
+
// Build stripUndefined call and cast to the proper ORM input type
|
|
412
|
+
// so that property accesses on cleanedData are correctly typed.
|
|
413
|
+
const stripUndefinedCall = t.callExpression(t.identifier('stripUndefined'), [
|
|
414
|
+
t.identifier('answers'),
|
|
415
|
+
t.identifier('fieldSchema'),
|
|
416
|
+
]);
|
|
417
|
+
let cleanedDataExpr = stripUndefinedCall;
|
|
418
|
+
if (ormTypes) {
|
|
419
|
+
if (operation === 'create') {
|
|
420
|
+
// cleanedData as CreateXxxInput['fieldName']
|
|
421
|
+
cleanedDataExpr = t.tsAsExpression(stripUndefinedCall, t.tsIndexedAccessType(t.tsTypeReference(t.identifier(ormTypes.createInputTypeName)), t.tsLiteralType(t.stringLiteral(ormTypes.innerFieldName))));
|
|
422
|
+
}
|
|
423
|
+
else if (operation === 'update') {
|
|
424
|
+
// cleanedData as XxxPatch
|
|
425
|
+
cleanedDataExpr = t.tsAsExpression(stripUndefinedCall, t.tsTypeReference(t.identifier(ormTypes.patchTypeName)));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
301
428
|
tryBody.push(t.variableDeclaration('const', [
|
|
302
|
-
t.variableDeclarator(t.identifier('cleanedData'),
|
|
303
|
-
t.identifier('answers'),
|
|
304
|
-
t.identifier('fieldSchema'),
|
|
305
|
-
])),
|
|
429
|
+
t.variableDeclarator(t.identifier('cleanedData'), cleanedDataExpr),
|
|
306
430
|
]));
|
|
307
431
|
}
|
|
308
432
|
tryBody.push(buildGetClientStatement(targetName), t.variableDeclaration('const', [
|
|
@@ -330,25 +454,60 @@ function generateTableCommand(table, options) {
|
|
|
330
454
|
statements.push(createImportDeclaration(executorPath, ['getClient']));
|
|
331
455
|
const utilsPath = options?.targetName ? '../../utils' : '../utils';
|
|
332
456
|
statements.push(createImportDeclaration(utilsPath, ['coerceAnswers', 'stripUndefined']));
|
|
457
|
+
statements.push(createImportDeclaration(utilsPath, ['FieldSchema'], true));
|
|
458
|
+
// Import ORM input types for proper type assertions in mutation handlers.
|
|
459
|
+
// These types ensure that cleanedData is cast to the correct ORM input type
|
|
460
|
+
// (e.g., CreateAppPermissionInput['appPermission'] for create, AppPermissionPatch for update)
|
|
461
|
+
// instead of remaining as Record<string, unknown>.
|
|
462
|
+
const createInputTypeName = (0, utils_1.getCreateInputTypeName)(table);
|
|
463
|
+
const patchTypeName = (0, utils_1.getPatchTypeName)(table);
|
|
464
|
+
const innerFieldName = (0, utils_1.lcFirst)(table.name);
|
|
465
|
+
// Commands are at cli/commands/xxx.ts (no target) or cli/commands/{target}/xxx.ts (with target).
|
|
466
|
+
// ORM input-types is at orm/input-types.ts — two or three levels up from commands.
|
|
467
|
+
const inputTypesPath = options?.targetName
|
|
468
|
+
? `../../../orm/input-types`
|
|
469
|
+
: `../../orm/input-types`;
|
|
470
|
+
statements.push(createImportDeclaration(inputTypesPath, [createInputTypeName, patchTypeName], true));
|
|
333
471
|
// Generate field schema for type coercion
|
|
472
|
+
// Use explicit FieldSchema type annotation so TS narrows string literals to FieldType
|
|
473
|
+
const fieldSchemaId = t.identifier('fieldSchema');
|
|
474
|
+
fieldSchemaId.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('FieldSchema')));
|
|
334
475
|
statements.push(t.variableDeclaration('const', [
|
|
335
|
-
t.variableDeclarator(
|
|
476
|
+
t.variableDeclarator(fieldSchemaId, buildFieldSchemaObject(table)),
|
|
336
477
|
]));
|
|
337
|
-
|
|
478
|
+
// Determine which operations the ORM model supports for this table.
|
|
479
|
+
// Most tables have `one: null` simply because there's no dedicated GraphQL
|
|
480
|
+
// findOne query, but the ORM still generates `findOne` using the PK.
|
|
481
|
+
// The only tables WITHOUT `findOne` are pure record types from SQL functions
|
|
482
|
+
// (e.g. GetAllRecord, OrgGetManagersRecord) which have no update/delete either.
|
|
483
|
+
// We detect these by checking: if one, update, AND delete are all null, it's a
|
|
484
|
+
// read-only record type with no `findOne`.
|
|
485
|
+
const hasUpdate = table.query?.update !== undefined && table.query?.update !== null;
|
|
486
|
+
const hasDelete = table.query?.delete !== undefined && table.query?.delete !== null;
|
|
487
|
+
const hasGet = table.query?.one !== null || hasUpdate || hasDelete;
|
|
488
|
+
const subcommands = ['list'];
|
|
489
|
+
if (hasGet)
|
|
490
|
+
subcommands.push('get');
|
|
491
|
+
subcommands.push('create');
|
|
492
|
+
if (hasUpdate)
|
|
493
|
+
subcommands.push('update');
|
|
494
|
+
if (hasDelete)
|
|
495
|
+
subcommands.push('delete');
|
|
338
496
|
const usageLines = [
|
|
339
497
|
'',
|
|
340
498
|
`${commandName} <command>`,
|
|
341
499
|
'',
|
|
342
500
|
'Commands:',
|
|
343
501
|
` list List all ${singularName} records`,
|
|
344
|
-
` get Get a ${singularName} by ID`,
|
|
345
|
-
` create Create a new ${singularName}`,
|
|
346
|
-
` update Update an existing ${singularName}`,
|
|
347
|
-
` delete Delete a ${singularName}`,
|
|
348
|
-
'',
|
|
349
|
-
' --help, -h Show this help message',
|
|
350
|
-
'',
|
|
351
502
|
];
|
|
503
|
+
if (hasGet)
|
|
504
|
+
usageLines.push(` get Get a ${singularName} by ID`);
|
|
505
|
+
usageLines.push(` create Create a new ${singularName}`);
|
|
506
|
+
if (hasUpdate)
|
|
507
|
+
usageLines.push(` update Update an existing ${singularName}`);
|
|
508
|
+
if (hasDelete)
|
|
509
|
+
usageLines.push(` delete Delete a ${singularName}`);
|
|
510
|
+
usageLines.push('', ' --help, -h Show this help message', '');
|
|
352
511
|
statements.push(t.variableDeclaration('const', [
|
|
353
512
|
t.variableDeclarator(t.identifier('usage'), t.stringLiteral(usageLines.join('\n'))),
|
|
354
513
|
]));
|
|
@@ -386,7 +545,7 @@ function generateTableCommand(table, options) {
|
|
|
386
545
|
]))),
|
|
387
546
|
]),
|
|
388
547
|
t.returnStatement(t.callExpression(t.identifier('handleTableSubcommand'), [
|
|
389
|
-
t.memberExpression(t.identifier('answer'), t.identifier('subcommand')),
|
|
548
|
+
t.tsAsExpression(t.memberExpression(t.identifier('answer'), t.identifier('subcommand')), t.tsStringKeyword()),
|
|
390
549
|
t.identifier('newArgv'),
|
|
391
550
|
t.identifier('prompter'),
|
|
392
551
|
])),
|
|
@@ -408,11 +567,15 @@ function generateTableCommand(table, options) {
|
|
|
408
567
|
buildSubcommandSwitch(subcommands, 'handle', 'usage'),
|
|
409
568
|
]), false, true));
|
|
410
569
|
const tn = options?.targetName;
|
|
570
|
+
const ormTypes = { createInputTypeName, patchTypeName, innerFieldName };
|
|
411
571
|
statements.push(buildListHandler(table, tn));
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
statements.push(buildMutationHandler(table, '
|
|
415
|
-
|
|
572
|
+
if (hasGet)
|
|
573
|
+
statements.push(buildGetHandler(table, tn));
|
|
574
|
+
statements.push(buildMutationHandler(table, 'create', tn, options?.typeRegistry, ormTypes));
|
|
575
|
+
if (hasUpdate)
|
|
576
|
+
statements.push(buildMutationHandler(table, 'update', tn, options?.typeRegistry, ormTypes));
|
|
577
|
+
if (hasDelete)
|
|
578
|
+
statements.push(buildMutationHandler(table, 'delete', tn, options?.typeRegistry, ormTypes));
|
|
416
579
|
const header = (0, utils_1.getGeneratedFileHeader)(`CLI commands for ${table.name}`);
|
|
417
580
|
const code = (0, babel_ast_1.generateCode)(statements);
|
|
418
581
|
return {
|
|
@@ -11,6 +11,25 @@ function createNamedImportDeclaration(moduleSpecifier, namedImports, typeOnly =
|
|
|
11
11
|
decl.importKind = typeOnly ? 'type' : 'value';
|
|
12
12
|
return decl;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Build the command handler function type:
|
|
16
|
+
* (argv: Partial<Record<string, unknown>>, prompter: Inquirerer, options: CLIOptions) => Promise<void>
|
|
17
|
+
* This matches the actual exported handler signatures from table/custom command files.
|
|
18
|
+
*/
|
|
19
|
+
function buildCommandHandlerType() {
|
|
20
|
+
const argvParam = t.identifier('argv');
|
|
21
|
+
argvParam.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('Partial'), t.tsTypeParameterInstantiation([
|
|
22
|
+
t.tsTypeReference(t.identifier('Record'), t.tsTypeParameterInstantiation([
|
|
23
|
+
t.tsStringKeyword(),
|
|
24
|
+
t.tsUnknownKeyword(),
|
|
25
|
+
])),
|
|
26
|
+
])));
|
|
27
|
+
const prompterParam = t.identifier('prompter');
|
|
28
|
+
prompterParam.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('Inquirerer')));
|
|
29
|
+
const optionsParam = t.identifier('options');
|
|
30
|
+
optionsParam.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('CLIOptions')));
|
|
31
|
+
return t.tsFunctionType(null, [argvParam, prompterParam, optionsParam], t.tsTypeAnnotation(t.tsTypeReference(t.identifier('Promise'), t.tsTypeParameterInstantiation([t.tsVoidKeyword()]))));
|
|
32
|
+
}
|
|
14
33
|
export function generateCommandMap(tables, customOperations, toolName) {
|
|
15
34
|
const statements = [];
|
|
16
35
|
statements.push(createNamedImportDeclaration('inquirerer', [
|
|
@@ -37,18 +56,17 @@ export function generateCommandMap(tables, customOperations, toolName) {
|
|
|
37
56
|
statements.push(createImportDeclaration(`./commands/${kebab}`, importName));
|
|
38
57
|
}
|
|
39
58
|
const mapProperties = commandEntries.map((entry) => t.objectProperty(t.stringLiteral(entry.kebab), t.identifier(entry.importName)));
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const createCommandMapAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('Record'), t.tsTypeParameterInstantiation([
|
|
44
|
-
t.tsStringKeyword(),
|
|
45
|
-
t.tsFunctionType(null, [], t.tsTypeAnnotation(t.tsAnyKeyword())),
|
|
46
|
-
])));
|
|
59
|
+
// Build command handler type matching actual handler signature:
|
|
60
|
+
// (argv: Partial<Record<string, unknown>>, prompter: Inquirerer, options: CLIOptions) => Promise<void>
|
|
61
|
+
const commandHandlerType = buildCommandHandlerType();
|
|
47
62
|
const createCommandMapId = t.identifier('createCommandMap');
|
|
48
63
|
createCommandMapId.typeAnnotation = t.tsTypeAnnotation(t.tsParenthesizedType(t.tsFunctionType(null, [], t.tsTypeAnnotation(t.tsTypeReference(t.identifier('Record'), t.tsTypeParameterInstantiation([
|
|
49
64
|
t.tsStringKeyword(),
|
|
50
|
-
|
|
65
|
+
commandHandlerType,
|
|
51
66
|
]))))));
|
|
67
|
+
const createCommandMapFunc = t.variableDeclaration('const', [
|
|
68
|
+
t.variableDeclarator(createCommandMapId, t.arrowFunctionExpression([], t.objectExpression(mapProperties))),
|
|
69
|
+
]);
|
|
52
70
|
statements.push(createCommandMapFunc);
|
|
53
71
|
const usageLines = [
|
|
54
72
|
'',
|
|
@@ -115,7 +133,7 @@ export function generateCommandMap(tables, customOperations, toolName) {
|
|
|
115
133
|
]),
|
|
116
134
|
]))),
|
|
117
135
|
]),
|
|
118
|
-
t.expressionStatement(t.assignmentExpression('=', t.identifier('command'), t.memberExpression(t.identifier('answer'), t.identifier('command')))),
|
|
136
|
+
t.expressionStatement(t.assignmentExpression('=', t.identifier('command'), t.tsAsExpression(t.memberExpression(t.identifier('answer'), t.identifier('command')), t.tsStringKeyword()))),
|
|
119
137
|
])),
|
|
120
138
|
t.variableDeclaration('const', [
|
|
121
139
|
t.variableDeclarator(t.identifier('commandFn'), t.memberExpression(t.identifier('commandMap'), t.identifier('command'), true)),
|
|
@@ -186,8 +204,15 @@ export function generateMultiTargetCommandMap(input) {
|
|
|
186
204
|
}
|
|
187
205
|
}
|
|
188
206
|
const mapProperties = commandEntries.map((entry) => t.objectProperty(t.stringLiteral(entry.kebab), t.identifier(entry.importName)));
|
|
207
|
+
// Build command handler type matching actual handler signature
|
|
208
|
+
const multiTargetCommandHandlerType = buildCommandHandlerType();
|
|
209
|
+
const multiTargetCreateCommandMapId = t.identifier('createCommandMap');
|
|
210
|
+
multiTargetCreateCommandMapId.typeAnnotation = t.tsTypeAnnotation(t.tsParenthesizedType(t.tsFunctionType(null, [], t.tsTypeAnnotation(t.tsTypeReference(t.identifier('Record'), t.tsTypeParameterInstantiation([
|
|
211
|
+
t.tsStringKeyword(),
|
|
212
|
+
multiTargetCommandHandlerType,
|
|
213
|
+
]))))));
|
|
189
214
|
const createCommandMapFunc = t.variableDeclaration('const', [
|
|
190
|
-
t.variableDeclarator(
|
|
215
|
+
t.variableDeclarator(multiTargetCreateCommandMapId, t.arrowFunctionExpression([], t.objectExpression(mapProperties))),
|
|
191
216
|
]);
|
|
192
217
|
statements.push(createCommandMapFunc);
|
|
193
218
|
const usageLines = [
|
|
@@ -261,7 +286,7 @@ export function generateMultiTargetCommandMap(input) {
|
|
|
261
286
|
]),
|
|
262
287
|
]))),
|
|
263
288
|
]),
|
|
264
|
-
t.expressionStatement(t.assignmentExpression('=', t.identifier('command'), t.memberExpression(t.identifier('answer'), t.identifier('command')))),
|
|
289
|
+
t.expressionStatement(t.assignmentExpression('=', t.identifier('command'), t.tsAsExpression(t.memberExpression(t.identifier('answer'), t.identifier('command')), t.tsStringKeyword()))),
|
|
265
290
|
])),
|
|
266
291
|
t.variableDeclaration('const', [
|
|
267
292
|
t.variableDeclarator(t.identifier('commandFn'), t.memberExpression(t.identifier('commandMap'), t.identifier('command'), true)),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as t from '@babel/types';
|
|
2
2
|
import { toKebabCase } from 'komoji';
|
|
3
3
|
import { generateCode } from '../babel-ast';
|
|
4
|
-
import { getGeneratedFileHeader } from '../utils';
|
|
4
|
+
import { getGeneratedFileHeader, ucFirst } from '../utils';
|
|
5
5
|
import { buildQuestionsArray } from './arg-mapper';
|
|
6
6
|
function createImportDeclaration(moduleSpecifier, namedImports, typeOnly = false) {
|
|
7
7
|
const specifiers = namedImports.map((name) => t.importSpecifier(t.identifier(name), t.identifier(name)));
|
|
@@ -65,22 +65,33 @@ function buildDefaultSelectString(returnType, isMutation) {
|
|
|
65
65
|
}
|
|
66
66
|
return '';
|
|
67
67
|
}
|
|
68
|
-
function buildOrmCustomCall(opKind, opName, argsExpr, selectExpr, hasArgs = true) {
|
|
68
|
+
function buildOrmCustomCall(opKind, opName, argsExpr, selectExpr, hasArgs = true, selectTypeName) {
|
|
69
69
|
const callArgs = [];
|
|
70
|
+
// Helper: wrap { select } and cast to `{ select: XxxSelect }` via `unknown`.
|
|
71
|
+
// The ORM method's second parameter is `{ select: S } & StrictSelect<S, XxxSelect>`.
|
|
72
|
+
// We import the concrete Select type (e.g. CheckPasswordPayloadSelect) and cast
|
|
73
|
+
// `{ select: selectFields } as unknown as { select: XxxSelect }` so TS infers
|
|
74
|
+
// `S = XxxSelect` and StrictSelect is satisfied.
|
|
75
|
+
const castSelectWrapper = (sel) => {
|
|
76
|
+
const selectObj = t.objectExpression([
|
|
77
|
+
t.objectProperty(t.identifier('select'), sel),
|
|
78
|
+
]);
|
|
79
|
+
if (!selectTypeName)
|
|
80
|
+
return selectObj;
|
|
81
|
+
return t.tsAsExpression(t.tsAsExpression(selectObj, t.tsUnknownKeyword()), t.tsTypeLiteral([
|
|
82
|
+
t.tsPropertySignature(t.identifier('select'), t.tsTypeAnnotation(t.tsTypeReference(t.identifier(selectTypeName)))),
|
|
83
|
+
]));
|
|
84
|
+
};
|
|
70
85
|
if (hasArgs) {
|
|
71
|
-
// Operation has arguments: pass args as first param, select as second
|
|
86
|
+
// Operation has arguments: pass args as first param, select as second.
|
|
72
87
|
callArgs.push(argsExpr);
|
|
73
88
|
if (selectExpr) {
|
|
74
|
-
callArgs.push(
|
|
75
|
-
t.objectProperty(t.identifier('select'), selectExpr),
|
|
76
|
-
]));
|
|
89
|
+
callArgs.push(castSelectWrapper(selectExpr));
|
|
77
90
|
}
|
|
78
91
|
}
|
|
79
92
|
else if (selectExpr) {
|
|
80
|
-
// No arguments: pass { select } as the only param (ORM signature)
|
|
81
|
-
callArgs.push(
|
|
82
|
-
t.objectProperty(t.identifier('select'), selectExpr),
|
|
83
|
-
]));
|
|
93
|
+
// No arguments: pass { select } as the only param (ORM signature).
|
|
94
|
+
callArgs.push(castSelectWrapper(selectExpr));
|
|
84
95
|
}
|
|
85
96
|
return t.callExpression(t.memberExpression(t.callExpression(t.memberExpression(t.memberExpression(t.identifier('client'), t.identifier(opKind)), t.identifier(opName)), callArgs), t.identifier('execute')), []);
|
|
86
97
|
}
|
|
@@ -116,6 +127,18 @@ export function generateCustomCommand(op, options) {
|
|
|
116
127
|
if (utilsImports.length > 0) {
|
|
117
128
|
statements.push(createImportDeclaration(utilsPath, utilsImports));
|
|
118
129
|
}
|
|
130
|
+
// Import the Variables type for this operation from the ORM query/mutation module.
|
|
131
|
+
// Custom operations define their own Variables types (e.g. CheckPasswordVariables)
|
|
132
|
+
// in the ORM layer. We import and cast CLI answers to this type for proper typing.
|
|
133
|
+
if (op.args.length > 0) {
|
|
134
|
+
const variablesTypeName = `${ucFirst(op.name)}Variables`;
|
|
135
|
+
// Commands are at cli/commands/xxx.ts (no target) or cli/commands/{target}/xxx.ts (with target).
|
|
136
|
+
// ORM query/mutation is at orm/{opKind}/ — two or three levels up from commands.
|
|
137
|
+
const ormOpPath = options?.targetName
|
|
138
|
+
? `../../../orm/${opKind}`
|
|
139
|
+
: `../../orm/${opKind}`;
|
|
140
|
+
statements.push(createImportDeclaration(ormOpPath, [variablesTypeName], true));
|
|
141
|
+
}
|
|
119
142
|
const questionsArray = op.args.length > 0
|
|
120
143
|
? buildQuestionsArray(op.args)
|
|
121
144
|
: t.arrayExpression([]);
|
|
@@ -153,10 +176,16 @@ export function generateCustomCommand(op, options) {
|
|
|
153
176
|
])),
|
|
154
177
|
]));
|
|
155
178
|
}
|
|
179
|
+
// Cast args to the specific Variables type for this operation.
|
|
180
|
+
// The ORM expects typed variables (e.g. CheckPasswordVariables), and CLI
|
|
181
|
+
// prompt answers are Record<string, unknown>. We cast through `unknown`
|
|
182
|
+
// first because Record<string, unknown> doesn't directly overlap with
|
|
183
|
+
// Variables types that have specific property types (like `input: SomeInput`).
|
|
184
|
+
const variablesTypeName = `${ucFirst(op.name)}Variables`;
|
|
156
185
|
const argsExpr = op.args.length > 0
|
|
157
|
-
? (hasInputObjectArg
|
|
186
|
+
? t.tsAsExpression(t.tsAsExpression(hasInputObjectArg
|
|
158
187
|
? t.identifier('parsedAnswers')
|
|
159
|
-
: t.identifier('answers'))
|
|
188
|
+
: t.identifier('answers'), t.tsUnknownKeyword()), t.tsTypeReference(t.identifier(variablesTypeName)))
|
|
160
189
|
: t.objectExpression([]);
|
|
161
190
|
// For OBJECT return types, generate runtime select from --select flag
|
|
162
191
|
// For scalar return types, no select is needed
|
|
@@ -166,14 +195,31 @@ export function generateCustomCommand(op, options) {
|
|
|
166
195
|
// Generate: const selectFields = buildSelectFromPaths(argv.select ?? 'defaultFields')
|
|
167
196
|
bodyStatements.push(t.variableDeclaration('const', [
|
|
168
197
|
t.variableDeclarator(t.identifier('selectFields'), t.callExpression(t.identifier('buildSelectFromPaths'), [
|
|
169
|
-
t.logicalExpression('??', t.memberExpression(t.identifier('argv'), t.identifier('select')), t.stringLiteral(defaultSelect)),
|
|
198
|
+
t.logicalExpression('??', t.tsAsExpression(t.memberExpression(t.identifier('argv'), t.identifier('select')), t.tsStringKeyword()), t.stringLiteral(defaultSelect)),
|
|
170
199
|
])),
|
|
171
200
|
]));
|
|
172
201
|
selectExpr = t.identifier('selectFields');
|
|
173
202
|
}
|
|
203
|
+
// Derive the Select type name from the operation's return type.
|
|
204
|
+
// e.g. CheckPasswordPayload → CheckPasswordPayloadSelect
|
|
205
|
+
// This is used to cast { select } to the proper type for StrictSelect.
|
|
206
|
+
let selectTypeName;
|
|
207
|
+
if (isObjectReturn) {
|
|
208
|
+
const baseReturnType = unwrapType(op.returnType);
|
|
209
|
+
if (baseReturnType.name) {
|
|
210
|
+
selectTypeName = `${baseReturnType.name}Select`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Import the Select type from orm/input-types if we have one
|
|
214
|
+
if (selectTypeName) {
|
|
215
|
+
const inputTypesPath = options?.targetName
|
|
216
|
+
? `../../../orm/input-types`
|
|
217
|
+
: `../../orm/input-types`;
|
|
218
|
+
statements.push(createImportDeclaration(inputTypesPath, [selectTypeName], true));
|
|
219
|
+
}
|
|
174
220
|
const hasArgs = op.args.length > 0;
|
|
175
221
|
bodyStatements.push(t.variableDeclaration('const', [
|
|
176
|
-
t.variableDeclarator(t.identifier('result'), t.awaitExpression(buildOrmCustomCall(opKind, op.name, argsExpr, selectExpr, hasArgs))),
|
|
222
|
+
t.variableDeclarator(t.identifier('result'), t.awaitExpression(buildOrmCustomCall(opKind, op.name, argsExpr, selectExpr, hasArgs, selectTypeName))),
|
|
177
223
|
]));
|
|
178
224
|
if (options?.saveToken) {
|
|
179
225
|
bodyStatements.push(t.ifStatement(t.logicalExpression('&&', t.memberExpression(t.identifier('argv'), t.identifier('saveToken')), t.identifier('result')), t.blockStatement([
|
|
@@ -49,9 +49,16 @@ export function generateExecutorFile(toolName, options) {
|
|
|
49
49
|
])),
|
|
50
50
|
])),
|
|
51
51
|
])),
|
|
52
|
-
|
|
53
|
-
t.
|
|
54
|
-
|
|
52
|
+
(() => {
|
|
53
|
+
const headersId = t.identifier('headers');
|
|
54
|
+
headersId.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('Record'), t.tsTypeParameterInstantiation([
|
|
55
|
+
t.tsStringKeyword(),
|
|
56
|
+
t.tsStringKeyword(),
|
|
57
|
+
])));
|
|
58
|
+
return t.variableDeclaration('const', [
|
|
59
|
+
t.variableDeclarator(headersId, t.objectExpression([])),
|
|
60
|
+
]);
|
|
61
|
+
})(),
|
|
55
62
|
t.ifStatement(t.callExpression(t.memberExpression(t.identifier('store'), t.identifier('hasValidCredentials')), [t.memberExpression(t.identifier('ctx'), t.identifier('name'))]), t.blockStatement([
|
|
56
63
|
t.variableDeclaration('const', [
|
|
57
64
|
t.variableDeclarator(t.identifier('creds'), t.callExpression(t.memberExpression(t.identifier('store'), t.identifier('getCredentials')), [t.memberExpression(t.identifier('ctx'), t.identifier('name'))])),
|
|
@@ -156,9 +163,16 @@ export function generateMultiTargetExecutorFile(toolName, targets, options) {
|
|
|
156
163
|
], [t.identifier('targetName')]),
|
|
157
164
|
])),
|
|
158
165
|
])),
|
|
159
|
-
|
|
160
|
-
t.
|
|
161
|
-
|
|
166
|
+
(() => {
|
|
167
|
+
const headersId = t.identifier('headers');
|
|
168
|
+
headersId.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('Record'), t.tsTypeParameterInstantiation([
|
|
169
|
+
t.tsStringKeyword(),
|
|
170
|
+
t.tsStringKeyword(),
|
|
171
|
+
])));
|
|
172
|
+
return t.variableDeclaration('const', [
|
|
173
|
+
t.variableDeclarator(headersId, t.objectExpression([])),
|
|
174
|
+
]);
|
|
175
|
+
})(),
|
|
162
176
|
t.variableDeclaration('let', [
|
|
163
177
|
t.variableDeclarator(t.identifier('endpoint'), t.stringLiteral('')),
|
|
164
178
|
]),
|