@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
|
@@ -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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
if (!
|
|
164
|
-
return
|
|
165
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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([
|
|
@@ -211,12 +305,19 @@ function buildMutationHandler(table, operation, targetName, typeRegistry) {
|
|
|
211
305
|
// For create: field is required only if it has no default value
|
|
212
306
|
// For update: all fields are optional (user only updates what they want)
|
|
213
307
|
const isRequired = operation === 'create' && !fieldsWithDefaults.has(field.name);
|
|
214
|
-
|
|
308
|
+
const hasDefault = fieldsWithDefaults.has(field.name);
|
|
309
|
+
const questionProps = [
|
|
215
310
|
t.objectProperty(t.identifier('type'), t.stringLiteral('text')),
|
|
216
311
|
t.objectProperty(t.identifier('name'), t.stringLiteral(field.name)),
|
|
217
312
|
t.objectProperty(t.identifier('message'), t.stringLiteral(field.name)),
|
|
218
313
|
t.objectProperty(t.identifier('required'), t.booleanLiteral(isRequired)),
|
|
219
|
-
]
|
|
314
|
+
];
|
|
315
|
+
// Skip prompting for fields with backend-managed defaults.
|
|
316
|
+
// The field still appears in man pages and can be overridden via CLI flags.
|
|
317
|
+
if (hasDefault) {
|
|
318
|
+
questionProps.push(t.objectProperty(t.identifier('skipPrompt'), t.booleanLiteral(true)));
|
|
319
|
+
}
|
|
320
|
+
questions.push(t.objectExpression(questionProps));
|
|
220
321
|
}
|
|
221
322
|
}
|
|
222
323
|
const selectObj = operation === 'delete'
|
|
@@ -225,27 +326,36 @@ function buildMutationHandler(table, operation, targetName, typeRegistry) {
|
|
|
225
326
|
])
|
|
226
327
|
: buildSelectObject(table);
|
|
227
328
|
let ormArgs;
|
|
329
|
+
// Build data properties without individual type assertions.
|
|
330
|
+
// Instead, we build a plain object from cleanedData and cast the entire
|
|
331
|
+
// data value through `unknown` to bridge the type gap between
|
|
332
|
+
// Record<string, unknown> and the ORM's specific input type.
|
|
333
|
+
// This handles scalars, enums (string literal unions like ObjectCategory),
|
|
334
|
+
// and array fields uniformly without needing to import each type.
|
|
335
|
+
const buildDataProps = () => editableFields.map((f) => t.objectProperty(t.identifier(f.name), t.memberExpression(t.identifier('cleanedData'), t.identifier(f.name))));
|
|
228
336
|
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
337
|
ormArgs = t.objectExpression([
|
|
231
|
-
t.objectProperty(t.identifier('data'), t.objectExpression(
|
|
338
|
+
t.objectProperty(t.identifier('data'), t.objectExpression(buildDataProps())),
|
|
232
339
|
t.objectProperty(t.identifier('select'), selectObj),
|
|
233
340
|
]);
|
|
234
341
|
}
|
|
235
342
|
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
343
|
ormArgs = t.objectExpression([
|
|
238
344
|
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)),
|
|
345
|
+
t.objectProperty(t.identifier(pk.name), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(pk.name)), pk.gqlType === 'Int' || pk.gqlType === 'BigInt'
|
|
346
|
+
? t.tsNumberKeyword()
|
|
347
|
+
: t.tsStringKeyword())),
|
|
240
348
|
])),
|
|
241
|
-
t.objectProperty(t.identifier('data'), t.objectExpression(
|
|
349
|
+
t.objectProperty(t.identifier('data'), t.objectExpression(buildDataProps())),
|
|
242
350
|
t.objectProperty(t.identifier('select'), selectObj),
|
|
243
351
|
]);
|
|
244
352
|
}
|
|
245
353
|
else {
|
|
246
354
|
ormArgs = t.objectExpression([
|
|
247
355
|
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)),
|
|
356
|
+
t.objectProperty(t.identifier(pk.name), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(pk.name)), pk.gqlType === 'Int' || pk.gqlType === 'BigInt'
|
|
357
|
+
? t.tsNumberKeyword()
|
|
358
|
+
: t.tsStringKeyword())),
|
|
249
359
|
])),
|
|
250
360
|
t.objectProperty(t.identifier('select'), selectObj),
|
|
251
361
|
]);
|
|
@@ -262,11 +372,25 @@ function buildMutationHandler(table, operation, targetName, typeRegistry) {
|
|
|
262
372
|
]),
|
|
263
373
|
];
|
|
264
374
|
if (operation !== 'delete') {
|
|
375
|
+
// Build stripUndefined call and cast to the proper ORM input type
|
|
376
|
+
// so that property accesses on cleanedData are correctly typed.
|
|
377
|
+
const stripUndefinedCall = t.callExpression(t.identifier('stripUndefined'), [
|
|
378
|
+
t.identifier('answers'),
|
|
379
|
+
t.identifier('fieldSchema'),
|
|
380
|
+
]);
|
|
381
|
+
let cleanedDataExpr = stripUndefinedCall;
|
|
382
|
+
if (ormTypes) {
|
|
383
|
+
if (operation === 'create') {
|
|
384
|
+
// cleanedData as CreateXxxInput['fieldName']
|
|
385
|
+
cleanedDataExpr = t.tsAsExpression(stripUndefinedCall, t.tsIndexedAccessType(t.tsTypeReference(t.identifier(ormTypes.createInputTypeName)), t.tsLiteralType(t.stringLiteral(ormTypes.innerFieldName))));
|
|
386
|
+
}
|
|
387
|
+
else if (operation === 'update') {
|
|
388
|
+
// cleanedData as XxxPatch
|
|
389
|
+
cleanedDataExpr = t.tsAsExpression(stripUndefinedCall, t.tsTypeReference(t.identifier(ormTypes.patchTypeName)));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
265
392
|
tryBody.push(t.variableDeclaration('const', [
|
|
266
|
-
t.variableDeclarator(t.identifier('cleanedData'),
|
|
267
|
-
t.identifier('answers'),
|
|
268
|
-
t.identifier('fieldSchema'),
|
|
269
|
-
])),
|
|
393
|
+
t.variableDeclarator(t.identifier('cleanedData'), cleanedDataExpr),
|
|
270
394
|
]));
|
|
271
395
|
}
|
|
272
396
|
tryBody.push(buildGetClientStatement(targetName), t.variableDeclaration('const', [
|
|
@@ -294,25 +418,60 @@ export function generateTableCommand(table, options) {
|
|
|
294
418
|
statements.push(createImportDeclaration(executorPath, ['getClient']));
|
|
295
419
|
const utilsPath = options?.targetName ? '../../utils' : '../utils';
|
|
296
420
|
statements.push(createImportDeclaration(utilsPath, ['coerceAnswers', 'stripUndefined']));
|
|
421
|
+
statements.push(createImportDeclaration(utilsPath, ['FieldSchema'], true));
|
|
422
|
+
// Import ORM input types for proper type assertions in mutation handlers.
|
|
423
|
+
// These types ensure that cleanedData is cast to the correct ORM input type
|
|
424
|
+
// (e.g., CreateAppPermissionInput['appPermission'] for create, AppPermissionPatch for update)
|
|
425
|
+
// instead of remaining as Record<string, unknown>.
|
|
426
|
+
const createInputTypeName = getCreateInputTypeName(table);
|
|
427
|
+
const patchTypeName = getPatchTypeName(table);
|
|
428
|
+
const innerFieldName = lcFirst(table.name);
|
|
429
|
+
// Commands are at cli/commands/xxx.ts (no target) or cli/commands/{target}/xxx.ts (with target).
|
|
430
|
+
// ORM input-types is at orm/input-types.ts — two or three levels up from commands.
|
|
431
|
+
const inputTypesPath = options?.targetName
|
|
432
|
+
? `../../../orm/input-types`
|
|
433
|
+
: `../../orm/input-types`;
|
|
434
|
+
statements.push(createImportDeclaration(inputTypesPath, [createInputTypeName, patchTypeName], true));
|
|
297
435
|
// Generate field schema for type coercion
|
|
436
|
+
// Use explicit FieldSchema type annotation so TS narrows string literals to FieldType
|
|
437
|
+
const fieldSchemaId = t.identifier('fieldSchema');
|
|
438
|
+
fieldSchemaId.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('FieldSchema')));
|
|
298
439
|
statements.push(t.variableDeclaration('const', [
|
|
299
|
-
t.variableDeclarator(
|
|
440
|
+
t.variableDeclarator(fieldSchemaId, buildFieldSchemaObject(table)),
|
|
300
441
|
]));
|
|
301
|
-
|
|
442
|
+
// Determine which operations the ORM model supports for this table.
|
|
443
|
+
// Most tables have `one: null` simply because there's no dedicated GraphQL
|
|
444
|
+
// findOne query, but the ORM still generates `findOne` using the PK.
|
|
445
|
+
// The only tables WITHOUT `findOne` are pure record types from SQL functions
|
|
446
|
+
// (e.g. GetAllRecord, OrgGetManagersRecord) which have no update/delete either.
|
|
447
|
+
// We detect these by checking: if one, update, AND delete are all null, it's a
|
|
448
|
+
// read-only record type with no `findOne`.
|
|
449
|
+
const hasUpdate = table.query?.update !== undefined && table.query?.update !== null;
|
|
450
|
+
const hasDelete = table.query?.delete !== undefined && table.query?.delete !== null;
|
|
451
|
+
const hasGet = table.query?.one !== null || hasUpdate || hasDelete;
|
|
452
|
+
const subcommands = ['list'];
|
|
453
|
+
if (hasGet)
|
|
454
|
+
subcommands.push('get');
|
|
455
|
+
subcommands.push('create');
|
|
456
|
+
if (hasUpdate)
|
|
457
|
+
subcommands.push('update');
|
|
458
|
+
if (hasDelete)
|
|
459
|
+
subcommands.push('delete');
|
|
302
460
|
const usageLines = [
|
|
303
461
|
'',
|
|
304
462
|
`${commandName} <command>`,
|
|
305
463
|
'',
|
|
306
464
|
'Commands:',
|
|
307
465
|
` 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
466
|
];
|
|
467
|
+
if (hasGet)
|
|
468
|
+
usageLines.push(` get Get a ${singularName} by ID`);
|
|
469
|
+
usageLines.push(` create Create a new ${singularName}`);
|
|
470
|
+
if (hasUpdate)
|
|
471
|
+
usageLines.push(` update Update an existing ${singularName}`);
|
|
472
|
+
if (hasDelete)
|
|
473
|
+
usageLines.push(` delete Delete a ${singularName}`);
|
|
474
|
+
usageLines.push('', ' --help, -h Show this help message', '');
|
|
316
475
|
statements.push(t.variableDeclaration('const', [
|
|
317
476
|
t.variableDeclarator(t.identifier('usage'), t.stringLiteral(usageLines.join('\n'))),
|
|
318
477
|
]));
|
|
@@ -350,7 +509,7 @@ export function generateTableCommand(table, options) {
|
|
|
350
509
|
]))),
|
|
351
510
|
]),
|
|
352
511
|
t.returnStatement(t.callExpression(t.identifier('handleTableSubcommand'), [
|
|
353
|
-
t.memberExpression(t.identifier('answer'), t.identifier('subcommand')),
|
|
512
|
+
t.tsAsExpression(t.memberExpression(t.identifier('answer'), t.identifier('subcommand')), t.tsStringKeyword()),
|
|
354
513
|
t.identifier('newArgv'),
|
|
355
514
|
t.identifier('prompter'),
|
|
356
515
|
])),
|
|
@@ -372,11 +531,15 @@ export function generateTableCommand(table, options) {
|
|
|
372
531
|
buildSubcommandSwitch(subcommands, 'handle', 'usage'),
|
|
373
532
|
]), false, true));
|
|
374
533
|
const tn = options?.targetName;
|
|
534
|
+
const ormTypes = { createInputTypeName, patchTypeName, innerFieldName };
|
|
375
535
|
statements.push(buildListHandler(table, tn));
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
statements.push(buildMutationHandler(table, '
|
|
379
|
-
|
|
536
|
+
if (hasGet)
|
|
537
|
+
statements.push(buildGetHandler(table, tn));
|
|
538
|
+
statements.push(buildMutationHandler(table, 'create', tn, options?.typeRegistry, ormTypes));
|
|
539
|
+
if (hasUpdate)
|
|
540
|
+
statements.push(buildMutationHandler(table, 'update', tn, options?.typeRegistry, ormTypes));
|
|
541
|
+
if (hasDelete)
|
|
542
|
+
statements.push(buildMutationHandler(table, 'delete', tn, options?.typeRegistry, ormTypes));
|
|
380
543
|
const header = getGeneratedFileHeader(`CLI commands for ${table.name}`);
|
|
381
544
|
const code = generateCode(statements);
|
|
382
545
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constructive-io/graphql-codegen",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.9.0",
|
|
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": "
|
|
104
|
+
"gitHead": "f1f05509f0168497f17c3dbbddb131d6aa5310e1"
|
|
105
105
|
}
|