@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.
- 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 +209 -53
- 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 +210 -54
- 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([
|
|
@@ -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(
|
|
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)),
|
|
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(
|
|
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)),
|
|
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'),
|
|
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(
|
|
433
|
+
t.variableDeclarator(fieldSchemaId, buildFieldSchemaObject(table)),
|
|
300
434
|
]));
|
|
301
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
statements.push(buildMutationHandler(table, '
|
|
379
|
-
|
|
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.
|
|
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": "
|
|
104
|
+
"gitHead": "f4176b73429bffca0aec8dc89abe9835bf40e5c6"
|
|
105
105
|
}
|