@constructive-io/graphql-codegen 4.8.0 → 4.8.1

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.
@@ -96,7 +96,7 @@ Create Options:
96
96
  ]))),
97
97
  ]),
98
98
  t.returnStatement(t.callExpression(t.identifier('handleSubcommand'), [
99
- t.memberExpression(t.identifier('answer'), t.identifier('subcommand')),
99
+ t.tsAsExpression(t.memberExpression(t.identifier('answer'), t.identifier('subcommand')), t.tsStringKeyword()),
100
100
  t.identifier('newArgv'),
101
101
  t.identifier('prompter'),
102
102
  t.identifier('store'),
@@ -185,7 +185,7 @@ function buildCreateHandler() {
185
185
  ])),
186
186
  ]),
187
187
  t.variableDeclaration('const', [
188
- t.variableDeclarator(t.identifier('answers'), t.awaitExpression(t.callExpression(t.memberExpression(t.identifier('prompter'), t.identifier('prompt')), [
188
+ t.variableDeclarator(t.identifier('answers'), t.tsAsExpression(t.tsAsExpression(t.awaitExpression(t.callExpression(t.memberExpression(t.identifier('prompter'), t.identifier('prompt')), [
189
189
  t.objectExpression([
190
190
  t.objectProperty(t.identifier('name'), t.identifier('name'), false, true),
191
191
  t.spreadElement(t.identifier('restArgv')),
@@ -204,7 +204,10 @@ function buildCreateHandler() {
204
204
  t.objectProperty(t.identifier('required'), t.booleanLiteral(true)),
205
205
  ]),
206
206
  ]),
207
- ]))),
207
+ ])), t.tsUnknownKeyword()), t.tsTypeReference(t.identifier('Record'), t.tsTypeParameterInstantiation([
208
+ t.tsStringKeyword(),
209
+ t.tsStringKeyword(),
210
+ ])))),
208
211
  ]),
209
212
  t.variableDeclaration('const', [
210
213
  t.variableDeclarator(t.identifier('contextName'), t.memberExpression(t.identifier('answers'), t.identifier('name'))),
@@ -341,7 +344,7 @@ function buildUseHandler() {
341
344
  ]),
342
345
  ]))),
343
346
  ]),
344
- t.expressionStatement(t.assignmentExpression('=', t.identifier('contextName'), t.memberExpression(t.identifier('answer'), t.identifier('name')))),
347
+ t.expressionStatement(t.assignmentExpression('=', t.identifier('contextName'), t.tsAsExpression(t.memberExpression(t.identifier('answer'), t.identifier('name')), t.tsStringKeyword()))),
345
348
  ])),
346
349
  t.ifStatement(t.callExpression(t.memberExpression(t.identifier('store'), t.identifier('setCurrentContext')), [t.identifier('contextName')]), t.blockStatement([
347
350
  t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('console'), t.identifier('log')), [
@@ -457,7 +460,7 @@ function buildDeleteHandler() {
457
460
  ]),
458
461
  ]))),
459
462
  ]),
460
- t.expressionStatement(t.assignmentExpression('=', t.identifier('contextName'), t.memberExpression(t.identifier('answer'), t.identifier('name')))),
463
+ t.expressionStatement(t.assignmentExpression('=', t.identifier('contextName'), t.tsAsExpression(t.memberExpression(t.identifier('answer'), t.identifier('name')), t.tsStringKeyword()))),
461
464
  ])),
462
465
  t.ifStatement(t.callExpression(t.memberExpression(t.identifier('store'), t.identifier('deleteContext')), [t.identifier('contextName')]), t.blockStatement([
463
466
  t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('console'), t.identifier('log')), [
@@ -549,7 +552,7 @@ Options:
549
552
  ]))),
550
553
  ]),
551
554
  t.returnStatement(t.callExpression(t.identifier('handleAuthSubcommand'), [
552
- t.memberExpression(t.identifier('answer'), t.identifier('subcommand')),
555
+ t.tsAsExpression(t.memberExpression(t.identifier('answer'), t.identifier('subcommand')), t.tsStringKeyword()),
553
556
  t.identifier('newArgv'),
554
557
  t.identifier('prompter'),
555
558
  t.identifier('store'),
@@ -651,7 +654,7 @@ function buildSetTokenHandler() {
651
654
  ]),
652
655
  ]))),
653
656
  ]),
654
- t.expressionStatement(t.assignmentExpression('=', t.identifier('tokenValue'), t.memberExpression(t.identifier('answer'), t.identifier('token')))),
657
+ t.expressionStatement(t.assignmentExpression('=', t.identifier('tokenValue'), t.tsAsExpression(t.memberExpression(t.identifier('answer'), t.identifier('token')), t.tsStringKeyword()))),
655
658
  ])),
656
659
  t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('store'), t.identifier('setCredentials')), [
657
660
  t.memberExpression(t.identifier('current'), t.identifier('name')),
@@ -770,7 +773,7 @@ function buildLogoutHandler() {
770
773
  ]),
771
774
  ]))),
772
775
  ]),
773
- t.ifStatement(t.unaryExpression('!', t.memberExpression(t.identifier('confirm'), t.identifier('confirm'))), t.blockStatement([t.returnStatement()])),
776
+ t.ifStatement(t.unaryExpression('!', t.tsAsExpression(t.memberExpression(t.identifier('confirm'), t.identifier('confirm')), t.tsBooleanKeyword())), t.blockStatement([t.returnStatement()])),
774
777
  t.ifStatement(t.callExpression(t.memberExpression(t.identifier('store'), t.identifier('removeCredentials')), [
775
778
  t.memberExpression(t.identifier('current'), t.identifier('name')),
776
779
  ]), t.blockStatement([
@@ -870,7 +873,7 @@ ${targets.map((tgt) => ` --${tgt.name}-endpoint <url> ${tgt.name} endpoint (de
870
873
  ]))),
871
874
  ]),
872
875
  t.returnStatement(t.callExpression(t.identifier('handleSubcommand'), [
873
- t.memberExpression(t.identifier('answer'), t.identifier('subcommand')),
876
+ t.tsAsExpression(t.memberExpression(t.identifier('answer'), t.identifier('subcommand')), t.tsStringKeyword()),
874
877
  t.identifier('newArgv'),
875
878
  t.identifier('prompter'),
876
879
  t.identifier('store'),
@@ -969,7 +972,7 @@ function buildMultiTargetCreateHandler(targets) {
969
972
  const targetsObjProps = targets.map((target) => {
970
973
  const fieldName = `${target.name}Endpoint`;
971
974
  return t.objectProperty(t.stringLiteral(target.name), t.objectExpression([
972
- t.objectProperty(t.identifier('endpoint'), t.memberExpression(t.identifier('answers'), t.identifier(fieldName))),
975
+ t.objectProperty(t.identifier('endpoint'), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(fieldName)), t.tsStringKeyword())),
973
976
  ]));
974
977
  });
975
978
  const body = [
@@ -991,7 +994,7 @@ function buildMultiTargetCreateHandler(targets) {
991
994
  ]))),
992
995
  ]),
993
996
  t.variableDeclaration('const', [
994
- t.variableDeclarator(t.identifier('contextName'), t.memberExpression(t.identifier('answers'), t.identifier('name'))),
997
+ t.variableDeclarator(t.identifier('contextName'), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier('name')), t.tsStringKeyword())),
995
998
  ]),
996
999
  t.variableDeclaration('const', [
997
1000
  t.variableDeclarator(t.identifier('targets'), t.objectExpression(targetsObjProps)),
@@ -999,7 +1002,7 @@ function buildMultiTargetCreateHandler(targets) {
999
1002
  t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('store'), t.identifier('createContext')), [
1000
1003
  t.identifier('contextName'),
1001
1004
  t.objectExpression([
1002
- t.objectProperty(t.identifier('endpoint'), t.memberExpression(t.identifier('answers'), t.identifier(`${targets[0].name}Endpoint`))),
1005
+ t.objectProperty(t.identifier('endpoint'), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(`${targets[0].name}Endpoint`)), t.tsStringKeyword())),
1003
1006
  t.objectProperty(t.identifier('targets'), t.identifier('targets')),
1004
1007
  ]),
1005
1008
  ])),
@@ -1022,7 +1025,7 @@ function buildMultiTargetCreateHandler(targets) {
1022
1025
  t.templateLiteral([
1023
1026
  t.templateElement({ raw: ` ${target.name}: `, cooked: ` ${target.name}: ` }),
1024
1027
  t.templateElement({ raw: '', cooked: '' }, true),
1025
- ], [t.memberExpression(t.identifier('answers'), t.identifier(fieldName))]),
1028
+ ], [t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(fieldName)), t.tsStringKeyword())]),
1026
1029
  ])));
1027
1030
  }
1028
1031
  const func = t.functionDeclaration(t.identifier('handleCreate'), [argvParam, prompterParam, storeParam], t.blockStatement(body), false, true);
@@ -1098,7 +1101,7 @@ Options:
1098
1101
  ]))),
1099
1102
  ]),
1100
1103
  t.returnStatement(t.callExpression(t.identifier('handleAuthSubcommand'), [
1101
- t.memberExpression(t.identifier('answer'), t.identifier('subcommand')),
1104
+ t.tsAsExpression(t.memberExpression(t.identifier('answer'), t.identifier('subcommand')), t.tsStringKeyword()),
1102
1105
  t.identifier('newArgv'),
1103
1106
  t.identifier('prompter'),
1104
1107
  t.identifier('store'),
@@ -1,8 +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, getPrimaryKeyInfo, getScalarFields, getTableNames, ucFirst, } from '../utils';
5
- import { getCreateInputTypeName } from '../utils';
4
+ import { getGeneratedFileHeader, getPrimaryKeyInfo, getScalarFields, getTableNames, ucFirst, lcFirst, getCreateInputTypeName, getPatchTypeName, } from '../utils';
6
5
  function createImportDeclaration(moduleSpecifier, namedImports, typeOnly = false) {
7
6
  const specifiers = namedImports.map((name) => t.importSpecifier(t.identifier(name), t.identifier(name)));
8
7
  const decl = t.importDeclaration(specifiers, t.stringLiteral(moduleSpecifier));
@@ -14,6 +13,60 @@ function createImportDeclaration(moduleSpecifier, namedImports, typeOnly = false
14
13
  * This is used at runtime for type coercion (string CLI args → proper types).
15
14
  * e.g., { name: 'string', isActive: 'boolean', position: 'int', status: 'enum' }
16
15
  */
16
+ /**
17
+ * Returns a t.TSType node for the appropriate TypeScript type assertion
18
+ * based on a field's GraphQL type. Used to cast `cleanedData.fieldName`
19
+ * to the correct type expected by the ORM.
20
+ */
21
+ /**
22
+ * Known GraphQL scalar types. Anything not in this set is an enum or custom type.
23
+ */
24
+ const KNOWN_SCALARS = new Set([
25
+ 'String', 'Boolean', 'Int', 'BigInt', 'Float', 'UUID',
26
+ 'JSON', 'GeoJSON', 'Datetime', 'Date', 'Time', 'Cursor',
27
+ 'BigFloat', 'Interval',
28
+ ]);
29
+ /**
30
+ * Returns true if the GraphQL type is a known scalar.
31
+ * Non-scalar types (enums, custom input types) need different handling.
32
+ */
33
+ function isKnownScalar(gqlType) {
34
+ return KNOWN_SCALARS.has(gqlType.replace(/!/g, ''));
35
+ }
36
+ function getTsTypeForField(field) {
37
+ const gqlType = field.type.gqlType.replace(/!/g, '');
38
+ // For non-scalar types (enums, custom types), return null to signal
39
+ // that no type assertion should be emitted — the value will be passed
40
+ // without casting, which avoids "string is not assignable to EnumType" errors.
41
+ if (!isKnownScalar(gqlType)) {
42
+ return null;
43
+ }
44
+ // Determine the base scalar type
45
+ // Note: ORM input types flatten array fields to their scalar base type
46
+ // (e.g., _uuid[] in PG -> string in the ORM input), so we do NOT wrap
47
+ // in tsArrayType here.
48
+ switch (gqlType) {
49
+ case 'Boolean':
50
+ return t.tsBooleanKeyword();
51
+ case 'Int':
52
+ case 'BigInt':
53
+ case 'Float':
54
+ case 'BigFloat':
55
+ return t.tsNumberKeyword();
56
+ case 'JSON':
57
+ case 'GeoJSON':
58
+ return t.tsTypeReference(t.identifier('Record'), t.tsTypeParameterInstantiation([
59
+ t.tsStringKeyword(),
60
+ t.tsUnknownKeyword(),
61
+ ]));
62
+ case 'Interval':
63
+ // IntervalInput is a complex type, skip assertion
64
+ return null;
65
+ case 'UUID':
66
+ default:
67
+ return t.tsStringKeyword();
68
+ }
69
+ }
17
70
  function buildFieldSchemaObject(table) {
18
71
  const fields = getScalarFields(table);
19
72
  return t.objectExpression(fields.map((f) => {
@@ -126,8 +179,11 @@ function buildGetHandler(table, targetName) {
126
179
  t.objectProperty(t.identifier('message'), t.stringLiteral(pk.name)),
127
180
  t.objectProperty(t.identifier('required'), t.booleanLiteral(true)),
128
181
  ]);
182
+ const pkTsType = pk.gqlType === 'Int' || pk.gqlType === 'BigInt'
183
+ ? t.tsNumberKeyword()
184
+ : t.tsStringKeyword();
129
185
  const ormArgs = t.objectExpression([
130
- t.objectProperty(t.identifier(pk.name), t.memberExpression(t.identifier('answers'), t.identifier(pk.name))),
186
+ t.objectProperty(t.identifier(pk.name), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(pk.name)), pkTsType)),
131
187
  t.objectProperty(t.identifier('select'), selectObj),
132
188
  ]);
133
189
  const tryBody = [
@@ -153,19 +209,16 @@ function buildGetHandler(table, targetName) {
153
209
  * Looks up the CreateXInput -> inner input type (e.g. DatabaseInput) in the
154
210
  * TypeRegistry and checks each field's defaultValue from introspection.
155
211
  */
156
- function getFieldsWithDefaults(table, typeRegistry) {
157
- const fieldsWithDefaults = new Set();
158
- if (!typeRegistry)
159
- return fieldsWithDefaults;
160
- // Look up the CreateXInput type (e.g. CreateDatabaseInput)
161
- const createInputTypeName = getCreateInputTypeName(table);
162
- const createInputType = typeRegistry.get(createInputTypeName);
163
- if (!createInputType?.inputFields)
164
- return fieldsWithDefaults;
165
- // The CreateXInput has an inner field (e.g. "database" of type DatabaseInput)
166
- // Find the inner input type that contains the actual field definitions
167
- for (const inputField of createInputType.inputFields) {
168
- // The inner field's type name is the actual input type (e.g. DatabaseInput)
212
+ /**
213
+ * Resolve the inner input type from a CreateXInput or UpdateXInput type.
214
+ * The CreateXInput has an inner field (e.g. "database" of type DatabaseInput)
215
+ * that contains the actual field definitions.
216
+ */
217
+ function resolveInnerInputType(inputTypeName, typeRegistry) {
218
+ const inputType = typeRegistry.get(inputTypeName);
219
+ if (!inputType?.inputFields)
220
+ return null;
221
+ for (const inputField of inputType.inputFields) {
169
222
  const innerTypeName = inputField.type.name
170
223
  || inputField.type.ofType?.name
171
224
  || inputField.type.ofType?.ofType?.name;
@@ -174,29 +227,70 @@ function getFieldsWithDefaults(table, typeRegistry) {
174
227
  const innerType = typeRegistry.get(innerTypeName);
175
228
  if (!innerType?.inputFields)
176
229
  continue;
177
- // Check each field in the inner input type for defaultValue
178
- for (const field of innerType.inputFields) {
179
- if (field.defaultValue !== undefined) {
180
- fieldsWithDefaults.add(field.name);
181
- }
182
- // Also check if the field is NOT wrapped in NON_NULL (nullable = has default or is optional)
183
- if (field.type.kind !== 'NON_NULL') {
184
- fieldsWithDefaults.add(field.name);
185
- }
230
+ const fields = new Set(innerType.inputFields.map((f) => f.name));
231
+ return { name: innerTypeName, fields };
232
+ }
233
+ return null;
234
+ }
235
+ function getFieldsWithDefaults(table, typeRegistry) {
236
+ const fieldsWithDefaults = new Set();
237
+ if (!typeRegistry)
238
+ return fieldsWithDefaults;
239
+ const createInputTypeName = getCreateInputTypeName(table);
240
+ const resolved = resolveInnerInputType(createInputTypeName, typeRegistry);
241
+ if (!resolved)
242
+ return fieldsWithDefaults;
243
+ const innerType = typeRegistry.get(resolved.name);
244
+ if (!innerType?.inputFields)
245
+ return fieldsWithDefaults;
246
+ for (const field of innerType.inputFields) {
247
+ if (field.defaultValue !== undefined) {
248
+ fieldsWithDefaults.add(field.name);
249
+ }
250
+ if (field.type.kind !== 'NON_NULL') {
251
+ fieldsWithDefaults.add(field.name);
186
252
  }
187
253
  }
188
254
  return fieldsWithDefaults;
189
255
  }
190
- function buildMutationHandler(table, operation, targetName, typeRegistry) {
256
+ /**
257
+ * Get the set of field names that actually exist in the create/update input type.
258
+ * Fields not in this set (e.g. computed fields like searchTsvRank, hashUuid)
259
+ * should be excluded from the data object in create/update handlers.
260
+ */
261
+ function getWritableFieldNames(table, typeRegistry) {
262
+ if (!typeRegistry)
263
+ return null;
264
+ const createInputTypeName = getCreateInputTypeName(table);
265
+ const resolved = resolveInnerInputType(createInputTypeName, typeRegistry);
266
+ return resolved?.fields ?? null;
267
+ }
268
+ function buildMutationHandler(table, operation, targetName, typeRegistry, ormTypes) {
191
269
  const { singularName } = getTableNames(table);
192
270
  const pkFields = getPrimaryKeyInfo(table);
193
271
  const pk = pkFields[0];
194
- const editableFields = getScalarFields(table).filter((f) => f.name !== pk.name &&
195
- f.name !== 'nodeId' &&
196
- f.name !== 'createdAt' &&
197
- f.name !== 'updatedAt');
272
+ // Get the set of writable field names from the type registry
273
+ // This filters out computed fields (e.g. searchTsvRank, hashUuid) that exist
274
+ // on the entity type but not on the create/update input type.
275
+ const writableFields = getWritableFieldNames(table, typeRegistry);
198
276
  // Get fields that have defaults from introspection (for create operations)
199
277
  const fieldsWithDefaults = getFieldsWithDefaults(table, typeRegistry);
278
+ // For create: include fields that are in the create input type.
279
+ // For update/delete: always exclude the PK (it goes in `where`, not `data`).
280
+ // The ORM input-types generator always excludes these fields from create inputs
281
+ // (see EXCLUDED_MUTATION_FIELDS in input-types-generator.ts). We must match this
282
+ // to avoid generating data properties that don't exist on the ORM create type.
283
+ // For non-'id' PKs (e.g. NodeTypeRegistry.name), we allow them in create data
284
+ // since they are user-provided natural keys that DO appear in the create input.
285
+ const ORM_EXCLUDED_FIELDS = ['id', 'createdAt', 'updatedAt', 'nodeId'];
286
+ const editableFields = getScalarFields(table).filter((f) =>
287
+ // For update/delete: always exclude PK (it goes in `where`, not `data`)
288
+ // For create: exclude PK only if it's in the ORM exclusion list (e.g. 'id')
289
+ (f.name !== pk.name || (operation === 'create' && !ORM_EXCLUDED_FIELDS.includes(pk.name))) &&
290
+ // Always exclude ORM-excluded fields (except PK which is handled above)
291
+ (f.name === pk.name || !ORM_EXCLUDED_FIELDS.includes(f.name)) &&
292
+ // If we have type registry info, only include fields that exist in the input type
293
+ (writableFields === null || writableFields.has(f.name)));
200
294
  const questions = [];
201
295
  if (operation === 'update' || operation === 'delete') {
202
296
  questions.push(t.objectExpression([
@@ -225,27 +319,36 @@ function buildMutationHandler(table, operation, targetName, typeRegistry) {
225
319
  ])
226
320
  : buildSelectObject(table);
227
321
  let ormArgs;
322
+ // Build data properties without individual type assertions.
323
+ // Instead, we build a plain object from cleanedData and cast the entire
324
+ // data value through `unknown` to bridge the type gap between
325
+ // Record<string, unknown> and the ORM's specific input type.
326
+ // This handles scalars, enums (string literal unions like ObjectCategory),
327
+ // and array fields uniformly without needing to import each type.
328
+ const buildDataProps = () => editableFields.map((f) => t.objectProperty(t.identifier(f.name), t.memberExpression(t.identifier('cleanedData'), t.identifier(f.name))));
228
329
  if (operation === 'create') {
229
- const dataProps = editableFields.map((f) => t.objectProperty(t.identifier(f.name), t.memberExpression(t.identifier('cleanedData'), t.identifier(f.name)), false, true));
230
330
  ormArgs = t.objectExpression([
231
- t.objectProperty(t.identifier('data'), t.objectExpression(dataProps)),
331
+ t.objectProperty(t.identifier('data'), t.objectExpression(buildDataProps())),
232
332
  t.objectProperty(t.identifier('select'), selectObj),
233
333
  ]);
234
334
  }
235
335
  else if (operation === 'update') {
236
- const dataProps = editableFields.map((f) => t.objectProperty(t.identifier(f.name), t.memberExpression(t.identifier('cleanedData'), t.identifier(f.name)), false, true));
237
336
  ormArgs = t.objectExpression([
238
337
  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())),
338
+ t.objectProperty(t.identifier(pk.name), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(pk.name)), pk.gqlType === 'Int' || pk.gqlType === 'BigInt'
339
+ ? t.tsNumberKeyword()
340
+ : t.tsStringKeyword())),
240
341
  ])),
241
- t.objectProperty(t.identifier('data'), t.objectExpression(dataProps)),
342
+ t.objectProperty(t.identifier('data'), t.objectExpression(buildDataProps())),
242
343
  t.objectProperty(t.identifier('select'), selectObj),
243
344
  ]);
244
345
  }
245
346
  else {
246
347
  ormArgs = t.objectExpression([
247
348
  t.objectProperty(t.identifier('where'), t.objectExpression([
248
- t.objectProperty(t.identifier(pk.name), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(pk.name)), t.tsStringKeyword())),
349
+ t.objectProperty(t.identifier(pk.name), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(pk.name)), pk.gqlType === 'Int' || pk.gqlType === 'BigInt'
350
+ ? t.tsNumberKeyword()
351
+ : t.tsStringKeyword())),
249
352
  ])),
250
353
  t.objectProperty(t.identifier('select'), selectObj),
251
354
  ]);
@@ -262,11 +365,25 @@ function buildMutationHandler(table, operation, targetName, typeRegistry) {
262
365
  ]),
263
366
  ];
264
367
  if (operation !== 'delete') {
368
+ // Build stripUndefined call and cast to the proper ORM input type
369
+ // so that property accesses on cleanedData are correctly typed.
370
+ const stripUndefinedCall = t.callExpression(t.identifier('stripUndefined'), [
371
+ t.identifier('answers'),
372
+ t.identifier('fieldSchema'),
373
+ ]);
374
+ let cleanedDataExpr = stripUndefinedCall;
375
+ if (ormTypes) {
376
+ if (operation === 'create') {
377
+ // cleanedData as CreateXxxInput['fieldName']
378
+ cleanedDataExpr = t.tsAsExpression(stripUndefinedCall, t.tsIndexedAccessType(t.tsTypeReference(t.identifier(ormTypes.createInputTypeName)), t.tsLiteralType(t.stringLiteral(ormTypes.innerFieldName))));
379
+ }
380
+ else if (operation === 'update') {
381
+ // cleanedData as XxxPatch
382
+ cleanedDataExpr = t.tsAsExpression(stripUndefinedCall, t.tsTypeReference(t.identifier(ormTypes.patchTypeName)));
383
+ }
384
+ }
265
385
  tryBody.push(t.variableDeclaration('const', [
266
- t.variableDeclarator(t.identifier('cleanedData'), t.callExpression(t.identifier('stripUndefined'), [
267
- t.identifier('answers'),
268
- t.identifier('fieldSchema'),
269
- ])),
386
+ t.variableDeclarator(t.identifier('cleanedData'), cleanedDataExpr),
270
387
  ]));
271
388
  }
272
389
  tryBody.push(buildGetClientStatement(targetName), t.variableDeclaration('const', [
@@ -294,25 +411,60 @@ export function generateTableCommand(table, options) {
294
411
  statements.push(createImportDeclaration(executorPath, ['getClient']));
295
412
  const utilsPath = options?.targetName ? '../../utils' : '../utils';
296
413
  statements.push(createImportDeclaration(utilsPath, ['coerceAnswers', 'stripUndefined']));
414
+ statements.push(createImportDeclaration(utilsPath, ['FieldSchema'], true));
415
+ // Import ORM input types for proper type assertions in mutation handlers.
416
+ // These types ensure that cleanedData is cast to the correct ORM input type
417
+ // (e.g., CreateAppPermissionInput['appPermission'] for create, AppPermissionPatch for update)
418
+ // instead of remaining as Record<string, unknown>.
419
+ const createInputTypeName = getCreateInputTypeName(table);
420
+ const patchTypeName = getPatchTypeName(table);
421
+ const innerFieldName = lcFirst(table.name);
422
+ // Commands are at cli/commands/xxx.ts (no target) or cli/commands/{target}/xxx.ts (with target).
423
+ // ORM input-types is at orm/input-types.ts — two or three levels up from commands.
424
+ const inputTypesPath = options?.targetName
425
+ ? `../../../orm/input-types`
426
+ : `../../orm/input-types`;
427
+ statements.push(createImportDeclaration(inputTypesPath, [createInputTypeName, patchTypeName], true));
297
428
  // Generate field schema for type coercion
429
+ // Use explicit FieldSchema type annotation so TS narrows string literals to FieldType
430
+ const fieldSchemaId = t.identifier('fieldSchema');
431
+ fieldSchemaId.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('FieldSchema')));
298
432
  statements.push(t.variableDeclaration('const', [
299
- t.variableDeclarator(t.identifier('fieldSchema'), buildFieldSchemaObject(table)),
433
+ t.variableDeclarator(fieldSchemaId, buildFieldSchemaObject(table)),
300
434
  ]));
301
- const subcommands = ['list', 'get', 'create', 'update', 'delete'];
435
+ // Determine which operations the ORM model supports for this table.
436
+ // Most tables have `one: null` simply because there's no dedicated GraphQL
437
+ // findOne query, but the ORM still generates `findOne` using the PK.
438
+ // The only tables WITHOUT `findOne` are pure record types from SQL functions
439
+ // (e.g. GetAllRecord, OrgGetManagersRecord) which have no update/delete either.
440
+ // We detect these by checking: if one, update, AND delete are all null, it's a
441
+ // read-only record type with no `findOne`.
442
+ const hasUpdate = table.query?.update !== undefined && table.query?.update !== null;
443
+ const hasDelete = table.query?.delete !== undefined && table.query?.delete !== null;
444
+ const hasGet = table.query?.one !== null || hasUpdate || hasDelete;
445
+ const subcommands = ['list'];
446
+ if (hasGet)
447
+ subcommands.push('get');
448
+ subcommands.push('create');
449
+ if (hasUpdate)
450
+ subcommands.push('update');
451
+ if (hasDelete)
452
+ subcommands.push('delete');
302
453
  const usageLines = [
303
454
  '',
304
455
  `${commandName} <command>`,
305
456
  '',
306
457
  'Commands:',
307
458
  ` list List all ${singularName} records`,
308
- ` get Get a ${singularName} by ID`,
309
- ` create Create a new ${singularName}`,
310
- ` update Update an existing ${singularName}`,
311
- ` delete Delete a ${singularName}`,
312
- '',
313
- ' --help, -h Show this help message',
314
- '',
315
459
  ];
460
+ if (hasGet)
461
+ usageLines.push(` get Get a ${singularName} by ID`);
462
+ usageLines.push(` create Create a new ${singularName}`);
463
+ if (hasUpdate)
464
+ usageLines.push(` update Update an existing ${singularName}`);
465
+ if (hasDelete)
466
+ usageLines.push(` delete Delete a ${singularName}`);
467
+ usageLines.push('', ' --help, -h Show this help message', '');
316
468
  statements.push(t.variableDeclaration('const', [
317
469
  t.variableDeclarator(t.identifier('usage'), t.stringLiteral(usageLines.join('\n'))),
318
470
  ]));
@@ -350,7 +502,7 @@ export function generateTableCommand(table, options) {
350
502
  ]))),
351
503
  ]),
352
504
  t.returnStatement(t.callExpression(t.identifier('handleTableSubcommand'), [
353
- t.memberExpression(t.identifier('answer'), t.identifier('subcommand')),
505
+ t.tsAsExpression(t.memberExpression(t.identifier('answer'), t.identifier('subcommand')), t.tsStringKeyword()),
354
506
  t.identifier('newArgv'),
355
507
  t.identifier('prompter'),
356
508
  ])),
@@ -372,11 +524,15 @@ export function generateTableCommand(table, options) {
372
524
  buildSubcommandSwitch(subcommands, 'handle', 'usage'),
373
525
  ]), false, true));
374
526
  const tn = options?.targetName;
527
+ const ormTypes = { createInputTypeName, patchTypeName, innerFieldName };
375
528
  statements.push(buildListHandler(table, tn));
376
- statements.push(buildGetHandler(table, tn));
377
- statements.push(buildMutationHandler(table, 'create', tn, options?.typeRegistry));
378
- statements.push(buildMutationHandler(table, 'update', tn, options?.typeRegistry));
379
- statements.push(buildMutationHandler(table, 'delete', tn, options?.typeRegistry));
529
+ if (hasGet)
530
+ statements.push(buildGetHandler(table, tn));
531
+ statements.push(buildMutationHandler(table, 'create', tn, options?.typeRegistry, ormTypes));
532
+ if (hasUpdate)
533
+ statements.push(buildMutationHandler(table, 'update', tn, options?.typeRegistry, ormTypes));
534
+ if (hasDelete)
535
+ statements.push(buildMutationHandler(table, 'delete', tn, options?.typeRegistry, ormTypes));
380
536
  const header = getGeneratedFileHeader(`CLI commands for ${table.name}`);
381
537
  const code = generateCode(statements);
382
538
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constructive-io/graphql-codegen",
3
- "version": "4.8.0",
3
+ "version": "4.8.1",
4
4
  "description": "GraphQL SDK generator for Constructive databases with React Query hooks",
5
5
  "keywords": [
6
6
  "graphql",
@@ -101,5 +101,5 @@
101
101
  "tsx": "^4.21.0",
102
102
  "typescript": "^5.9.3"
103
103
  },
104
- "gitHead": "d0d7d3916b70c8d960bc13e40ac85d73ea869224"
104
+ "gitHead": "f4176b73429bffca0aec8dc89abe9835bf40e5c6"
105
105
  }