@constructive-io/graphql-codegen 4.7.3 → 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/docs-generator.d.ts +1 -1
- package/core/codegen/cli/docs-generator.js +137 -54
- 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/core/codegen/docs-utils.d.ts +12 -1
- package/core/codegen/docs-utils.js +49 -1
- package/core/codegen/hooks-docs-generator.d.ts +1 -1
- package/core/codegen/hooks-docs-generator.js +47 -7
- package/core/codegen/orm/docs-generator.d.ts +1 -1
- package/core/codegen/orm/docs-generator.js +57 -20
- package/core/generate.d.ts +5 -0
- package/core/generate.js +48 -12
- package/core/workspace.d.ts +13 -0
- package/core/workspace.js +92 -0
- 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/docs-generator.d.ts +1 -1
- package/esm/core/codegen/cli/docs-generator.js +138 -55
- 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/esm/core/codegen/docs-utils.d.ts +12 -1
- package/esm/core/codegen/docs-utils.js +48 -1
- package/esm/core/codegen/hooks-docs-generator.d.ts +1 -1
- package/esm/core/codegen/hooks-docs-generator.js +48 -8
- package/esm/core/codegen/orm/docs-generator.d.ts +1 -1
- package/esm/core/codegen/orm/docs-generator.js +58 -21
- package/esm/core/generate.d.ts +5 -0
- package/esm/core/generate.js +48 -12
- package/esm/core/workspace.d.ts +13 -0
- package/esm/core/workspace.js +89 -0
- package/esm/types/config.d.ts +12 -4
- package/package.json +22 -22
- package/types/config.d.ts +12 -4
|
@@ -135,7 +135,7 @@ Create Options:
|
|
|
135
135
|
]))),
|
|
136
136
|
]),
|
|
137
137
|
t.returnStatement(t.callExpression(t.identifier('handleSubcommand'), [
|
|
138
|
-
t.memberExpression(t.identifier('answer'), t.identifier('subcommand')),
|
|
138
|
+
t.tsAsExpression(t.memberExpression(t.identifier('answer'), t.identifier('subcommand')), t.tsStringKeyword()),
|
|
139
139
|
t.identifier('newArgv'),
|
|
140
140
|
t.identifier('prompter'),
|
|
141
141
|
t.identifier('store'),
|
|
@@ -224,7 +224,7 @@ function buildCreateHandler() {
|
|
|
224
224
|
])),
|
|
225
225
|
]),
|
|
226
226
|
t.variableDeclaration('const', [
|
|
227
|
-
t.variableDeclarator(t.identifier('answers'), t.awaitExpression(t.callExpression(t.memberExpression(t.identifier('prompter'), t.identifier('prompt')), [
|
|
227
|
+
t.variableDeclarator(t.identifier('answers'), t.tsAsExpression(t.tsAsExpression(t.awaitExpression(t.callExpression(t.memberExpression(t.identifier('prompter'), t.identifier('prompt')), [
|
|
228
228
|
t.objectExpression([
|
|
229
229
|
t.objectProperty(t.identifier('name'), t.identifier('name'), false, true),
|
|
230
230
|
t.spreadElement(t.identifier('restArgv')),
|
|
@@ -243,7 +243,10 @@ function buildCreateHandler() {
|
|
|
243
243
|
t.objectProperty(t.identifier('required'), t.booleanLiteral(true)),
|
|
244
244
|
]),
|
|
245
245
|
]),
|
|
246
|
-
]))),
|
|
246
|
+
])), t.tsUnknownKeyword()), t.tsTypeReference(t.identifier('Record'), t.tsTypeParameterInstantiation([
|
|
247
|
+
t.tsStringKeyword(),
|
|
248
|
+
t.tsStringKeyword(),
|
|
249
|
+
])))),
|
|
247
250
|
]),
|
|
248
251
|
t.variableDeclaration('const', [
|
|
249
252
|
t.variableDeclarator(t.identifier('contextName'), t.memberExpression(t.identifier('answers'), t.identifier('name'))),
|
|
@@ -380,7 +383,7 @@ function buildUseHandler() {
|
|
|
380
383
|
]),
|
|
381
384
|
]))),
|
|
382
385
|
]),
|
|
383
|
-
t.expressionStatement(t.assignmentExpression('=', t.identifier('contextName'), t.memberExpression(t.identifier('answer'), t.identifier('name')))),
|
|
386
|
+
t.expressionStatement(t.assignmentExpression('=', t.identifier('contextName'), t.tsAsExpression(t.memberExpression(t.identifier('answer'), t.identifier('name')), t.tsStringKeyword()))),
|
|
384
387
|
])),
|
|
385
388
|
t.ifStatement(t.callExpression(t.memberExpression(t.identifier('store'), t.identifier('setCurrentContext')), [t.identifier('contextName')]), t.blockStatement([
|
|
386
389
|
t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('console'), t.identifier('log')), [
|
|
@@ -496,7 +499,7 @@ function buildDeleteHandler() {
|
|
|
496
499
|
]),
|
|
497
500
|
]))),
|
|
498
501
|
]),
|
|
499
|
-
t.expressionStatement(t.assignmentExpression('=', t.identifier('contextName'), t.memberExpression(t.identifier('answer'), t.identifier('name')))),
|
|
502
|
+
t.expressionStatement(t.assignmentExpression('=', t.identifier('contextName'), t.tsAsExpression(t.memberExpression(t.identifier('answer'), t.identifier('name')), t.tsStringKeyword()))),
|
|
500
503
|
])),
|
|
501
504
|
t.ifStatement(t.callExpression(t.memberExpression(t.identifier('store'), t.identifier('deleteContext')), [t.identifier('contextName')]), t.blockStatement([
|
|
502
505
|
t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('console'), t.identifier('log')), [
|
|
@@ -588,7 +591,7 @@ Options:
|
|
|
588
591
|
]))),
|
|
589
592
|
]),
|
|
590
593
|
t.returnStatement(t.callExpression(t.identifier('handleAuthSubcommand'), [
|
|
591
|
-
t.memberExpression(t.identifier('answer'), t.identifier('subcommand')),
|
|
594
|
+
t.tsAsExpression(t.memberExpression(t.identifier('answer'), t.identifier('subcommand')), t.tsStringKeyword()),
|
|
592
595
|
t.identifier('newArgv'),
|
|
593
596
|
t.identifier('prompter'),
|
|
594
597
|
t.identifier('store'),
|
|
@@ -690,7 +693,7 @@ function buildSetTokenHandler() {
|
|
|
690
693
|
]),
|
|
691
694
|
]))),
|
|
692
695
|
]),
|
|
693
|
-
t.expressionStatement(t.assignmentExpression('=', t.identifier('tokenValue'), t.memberExpression(t.identifier('answer'), t.identifier('token')))),
|
|
696
|
+
t.expressionStatement(t.assignmentExpression('=', t.identifier('tokenValue'), t.tsAsExpression(t.memberExpression(t.identifier('answer'), t.identifier('token')), t.tsStringKeyword()))),
|
|
694
697
|
])),
|
|
695
698
|
t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('store'), t.identifier('setCredentials')), [
|
|
696
699
|
t.memberExpression(t.identifier('current'), t.identifier('name')),
|
|
@@ -809,7 +812,7 @@ function buildLogoutHandler() {
|
|
|
809
812
|
]),
|
|
810
813
|
]))),
|
|
811
814
|
]),
|
|
812
|
-
t.ifStatement(t.unaryExpression('!', t.memberExpression(t.identifier('confirm'), t.identifier('confirm'))), t.blockStatement([t.returnStatement()])),
|
|
815
|
+
t.ifStatement(t.unaryExpression('!', t.tsAsExpression(t.memberExpression(t.identifier('confirm'), t.identifier('confirm')), t.tsBooleanKeyword())), t.blockStatement([t.returnStatement()])),
|
|
813
816
|
t.ifStatement(t.callExpression(t.memberExpression(t.identifier('store'), t.identifier('removeCredentials')), [
|
|
814
817
|
t.memberExpression(t.identifier('current'), t.identifier('name')),
|
|
815
818
|
]), t.blockStatement([
|
|
@@ -909,7 +912,7 @@ ${targets.map((tgt) => ` --${tgt.name}-endpoint <url> ${tgt.name} endpoint (de
|
|
|
909
912
|
]))),
|
|
910
913
|
]),
|
|
911
914
|
t.returnStatement(t.callExpression(t.identifier('handleSubcommand'), [
|
|
912
|
-
t.memberExpression(t.identifier('answer'), t.identifier('subcommand')),
|
|
915
|
+
t.tsAsExpression(t.memberExpression(t.identifier('answer'), t.identifier('subcommand')), t.tsStringKeyword()),
|
|
913
916
|
t.identifier('newArgv'),
|
|
914
917
|
t.identifier('prompter'),
|
|
915
918
|
t.identifier('store'),
|
|
@@ -1008,7 +1011,7 @@ function buildMultiTargetCreateHandler(targets) {
|
|
|
1008
1011
|
const targetsObjProps = targets.map((target) => {
|
|
1009
1012
|
const fieldName = `${target.name}Endpoint`;
|
|
1010
1013
|
return t.objectProperty(t.stringLiteral(target.name), t.objectExpression([
|
|
1011
|
-
t.objectProperty(t.identifier('endpoint'), t.memberExpression(t.identifier('answers'), t.identifier(fieldName))),
|
|
1014
|
+
t.objectProperty(t.identifier('endpoint'), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(fieldName)), t.tsStringKeyword())),
|
|
1012
1015
|
]));
|
|
1013
1016
|
});
|
|
1014
1017
|
const body = [
|
|
@@ -1030,7 +1033,7 @@ function buildMultiTargetCreateHandler(targets) {
|
|
|
1030
1033
|
]))),
|
|
1031
1034
|
]),
|
|
1032
1035
|
t.variableDeclaration('const', [
|
|
1033
|
-
t.variableDeclarator(t.identifier('contextName'), t.memberExpression(t.identifier('answers'), t.identifier('name'))),
|
|
1036
|
+
t.variableDeclarator(t.identifier('contextName'), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier('name')), t.tsStringKeyword())),
|
|
1034
1037
|
]),
|
|
1035
1038
|
t.variableDeclaration('const', [
|
|
1036
1039
|
t.variableDeclarator(t.identifier('targets'), t.objectExpression(targetsObjProps)),
|
|
@@ -1038,7 +1041,7 @@ function buildMultiTargetCreateHandler(targets) {
|
|
|
1038
1041
|
t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('store'), t.identifier('createContext')), [
|
|
1039
1042
|
t.identifier('contextName'),
|
|
1040
1043
|
t.objectExpression([
|
|
1041
|
-
t.objectProperty(t.identifier('endpoint'), t.memberExpression(t.identifier('answers'), t.identifier(`${targets[0].name}Endpoint`))),
|
|
1044
|
+
t.objectProperty(t.identifier('endpoint'), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(`${targets[0].name}Endpoint`)), t.tsStringKeyword())),
|
|
1042
1045
|
t.objectProperty(t.identifier('targets'), t.identifier('targets')),
|
|
1043
1046
|
]),
|
|
1044
1047
|
])),
|
|
@@ -1061,7 +1064,7 @@ function buildMultiTargetCreateHandler(targets) {
|
|
|
1061
1064
|
t.templateLiteral([
|
|
1062
1065
|
t.templateElement({ raw: ` ${target.name}: `, cooked: ` ${target.name}: ` }),
|
|
1063
1066
|
t.templateElement({ raw: '', cooked: '' }, true),
|
|
1064
|
-
], [t.memberExpression(t.identifier('answers'), t.identifier(fieldName))]),
|
|
1067
|
+
], [t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(fieldName)), t.tsStringKeyword())]),
|
|
1065
1068
|
])));
|
|
1066
1069
|
}
|
|
1067
1070
|
const func = t.functionDeclaration(t.identifier('handleCreate'), [argvParam, prompterParam, storeParam], t.blockStatement(body), false, true);
|
|
@@ -1137,7 +1140,7 @@ Options:
|
|
|
1137
1140
|
]))),
|
|
1138
1141
|
]),
|
|
1139
1142
|
t.returnStatement(t.callExpression(t.identifier('handleAuthSubcommand'), [
|
|
1140
|
-
t.memberExpression(t.identifier('answer'), t.identifier('subcommand')),
|
|
1143
|
+
t.tsAsExpression(t.memberExpression(t.identifier('answer'), t.identifier('subcommand')), t.tsStringKeyword()),
|
|
1141
1144
|
t.identifier('newArgv'),
|
|
1142
1145
|
t.identifier('prompter'),
|
|
1143
1146
|
t.identifier('store'),
|
|
@@ -38,7 +38,6 @@ const t = __importStar(require("@babel/types"));
|
|
|
38
38
|
const komoji_1 = require("komoji");
|
|
39
39
|
const babel_ast_1 = require("../babel-ast");
|
|
40
40
|
const utils_1 = require("../utils");
|
|
41
|
-
const utils_2 = require("../utils");
|
|
42
41
|
function createImportDeclaration(moduleSpecifier, namedImports, typeOnly = false) {
|
|
43
42
|
const specifiers = namedImports.map((name) => t.importSpecifier(t.identifier(name), t.identifier(name)));
|
|
44
43
|
const decl = t.importDeclaration(specifiers, t.stringLiteral(moduleSpecifier));
|
|
@@ -50,6 +49,60 @@ function createImportDeclaration(moduleSpecifier, namedImports, typeOnly = false
|
|
|
50
49
|
* This is used at runtime for type coercion (string CLI args → proper types).
|
|
51
50
|
* e.g., { name: 'string', isActive: 'boolean', position: 'int', status: 'enum' }
|
|
52
51
|
*/
|
|
52
|
+
/**
|
|
53
|
+
* Returns a t.TSType node for the appropriate TypeScript type assertion
|
|
54
|
+
* based on a field's GraphQL type. Used to cast `cleanedData.fieldName`
|
|
55
|
+
* to the correct type expected by the ORM.
|
|
56
|
+
*/
|
|
57
|
+
/**
|
|
58
|
+
* Known GraphQL scalar types. Anything not in this set is an enum or custom type.
|
|
59
|
+
*/
|
|
60
|
+
const KNOWN_SCALARS = new Set([
|
|
61
|
+
'String', 'Boolean', 'Int', 'BigInt', 'Float', 'UUID',
|
|
62
|
+
'JSON', 'GeoJSON', 'Datetime', 'Date', 'Time', 'Cursor',
|
|
63
|
+
'BigFloat', 'Interval',
|
|
64
|
+
]);
|
|
65
|
+
/**
|
|
66
|
+
* Returns true if the GraphQL type is a known scalar.
|
|
67
|
+
* Non-scalar types (enums, custom input types) need different handling.
|
|
68
|
+
*/
|
|
69
|
+
function isKnownScalar(gqlType) {
|
|
70
|
+
return KNOWN_SCALARS.has(gqlType.replace(/!/g, ''));
|
|
71
|
+
}
|
|
72
|
+
function getTsTypeForField(field) {
|
|
73
|
+
const gqlType = field.type.gqlType.replace(/!/g, '');
|
|
74
|
+
// For non-scalar types (enums, custom types), return null to signal
|
|
75
|
+
// that no type assertion should be emitted — the value will be passed
|
|
76
|
+
// without casting, which avoids "string is not assignable to EnumType" errors.
|
|
77
|
+
if (!isKnownScalar(gqlType)) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
// Determine the base scalar type
|
|
81
|
+
// Note: ORM input types flatten array fields to their scalar base type
|
|
82
|
+
// (e.g., _uuid[] in PG -> string in the ORM input), so we do NOT wrap
|
|
83
|
+
// in tsArrayType here.
|
|
84
|
+
switch (gqlType) {
|
|
85
|
+
case 'Boolean':
|
|
86
|
+
return t.tsBooleanKeyword();
|
|
87
|
+
case 'Int':
|
|
88
|
+
case 'BigInt':
|
|
89
|
+
case 'Float':
|
|
90
|
+
case 'BigFloat':
|
|
91
|
+
return t.tsNumberKeyword();
|
|
92
|
+
case 'JSON':
|
|
93
|
+
case 'GeoJSON':
|
|
94
|
+
return t.tsTypeReference(t.identifier('Record'), t.tsTypeParameterInstantiation([
|
|
95
|
+
t.tsStringKeyword(),
|
|
96
|
+
t.tsUnknownKeyword(),
|
|
97
|
+
]));
|
|
98
|
+
case 'Interval':
|
|
99
|
+
// IntervalInput is a complex type, skip assertion
|
|
100
|
+
return null;
|
|
101
|
+
case 'UUID':
|
|
102
|
+
default:
|
|
103
|
+
return t.tsStringKeyword();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
53
106
|
function buildFieldSchemaObject(table) {
|
|
54
107
|
const fields = (0, utils_1.getScalarFields)(table);
|
|
55
108
|
return t.objectExpression(fields.map((f) => {
|
|
@@ -162,8 +215,11 @@ function buildGetHandler(table, targetName) {
|
|
|
162
215
|
t.objectProperty(t.identifier('message'), t.stringLiteral(pk.name)),
|
|
163
216
|
t.objectProperty(t.identifier('required'), t.booleanLiteral(true)),
|
|
164
217
|
]);
|
|
218
|
+
const pkTsType = pk.gqlType === 'Int' || pk.gqlType === 'BigInt'
|
|
219
|
+
? t.tsNumberKeyword()
|
|
220
|
+
: t.tsStringKeyword();
|
|
165
221
|
const ormArgs = t.objectExpression([
|
|
166
|
-
t.objectProperty(t.identifier(pk.name), t.memberExpression(t.identifier('answers'), t.identifier(pk.name))),
|
|
222
|
+
t.objectProperty(t.identifier(pk.name), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(pk.name)), pkTsType)),
|
|
167
223
|
t.objectProperty(t.identifier('select'), selectObj),
|
|
168
224
|
]);
|
|
169
225
|
const tryBody = [
|
|
@@ -189,19 +245,16 @@ function buildGetHandler(table, targetName) {
|
|
|
189
245
|
* Looks up the CreateXInput -> inner input type (e.g. DatabaseInput) in the
|
|
190
246
|
* TypeRegistry and checks each field's defaultValue from introspection.
|
|
191
247
|
*/
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
if (!
|
|
200
|
-
return
|
|
201
|
-
|
|
202
|
-
// Find the inner input type that contains the actual field definitions
|
|
203
|
-
for (const inputField of createInputType.inputFields) {
|
|
204
|
-
// The inner field's type name is the actual input type (e.g. DatabaseInput)
|
|
248
|
+
/**
|
|
249
|
+
* Resolve the inner input type from a CreateXInput or UpdateXInput type.
|
|
250
|
+
* The CreateXInput has an inner field (e.g. "database" of type DatabaseInput)
|
|
251
|
+
* that contains the actual field definitions.
|
|
252
|
+
*/
|
|
253
|
+
function resolveInnerInputType(inputTypeName, typeRegistry) {
|
|
254
|
+
const inputType = typeRegistry.get(inputTypeName);
|
|
255
|
+
if (!inputType?.inputFields)
|
|
256
|
+
return null;
|
|
257
|
+
for (const inputField of inputType.inputFields) {
|
|
205
258
|
const innerTypeName = inputField.type.name
|
|
206
259
|
|| inputField.type.ofType?.name
|
|
207
260
|
|| inputField.type.ofType?.ofType?.name;
|
|
@@ -210,29 +263,70 @@ function getFieldsWithDefaults(table, typeRegistry) {
|
|
|
210
263
|
const innerType = typeRegistry.get(innerTypeName);
|
|
211
264
|
if (!innerType?.inputFields)
|
|
212
265
|
continue;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
266
|
+
const fields = new Set(innerType.inputFields.map((f) => f.name));
|
|
267
|
+
return { name: innerTypeName, fields };
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
function getFieldsWithDefaults(table, typeRegistry) {
|
|
272
|
+
const fieldsWithDefaults = new Set();
|
|
273
|
+
if (!typeRegistry)
|
|
274
|
+
return fieldsWithDefaults;
|
|
275
|
+
const createInputTypeName = (0, utils_1.getCreateInputTypeName)(table);
|
|
276
|
+
const resolved = resolveInnerInputType(createInputTypeName, typeRegistry);
|
|
277
|
+
if (!resolved)
|
|
278
|
+
return fieldsWithDefaults;
|
|
279
|
+
const innerType = typeRegistry.get(resolved.name);
|
|
280
|
+
if (!innerType?.inputFields)
|
|
281
|
+
return fieldsWithDefaults;
|
|
282
|
+
for (const field of innerType.inputFields) {
|
|
283
|
+
if (field.defaultValue !== undefined) {
|
|
284
|
+
fieldsWithDefaults.add(field.name);
|
|
285
|
+
}
|
|
286
|
+
if (field.type.kind !== 'NON_NULL') {
|
|
287
|
+
fieldsWithDefaults.add(field.name);
|
|
222
288
|
}
|
|
223
289
|
}
|
|
224
290
|
return fieldsWithDefaults;
|
|
225
291
|
}
|
|
226
|
-
|
|
292
|
+
/**
|
|
293
|
+
* Get the set of field names that actually exist in the create/update input type.
|
|
294
|
+
* Fields not in this set (e.g. computed fields like searchTsvRank, hashUuid)
|
|
295
|
+
* should be excluded from the data object in create/update handlers.
|
|
296
|
+
*/
|
|
297
|
+
function getWritableFieldNames(table, typeRegistry) {
|
|
298
|
+
if (!typeRegistry)
|
|
299
|
+
return null;
|
|
300
|
+
const createInputTypeName = (0, utils_1.getCreateInputTypeName)(table);
|
|
301
|
+
const resolved = resolveInnerInputType(createInputTypeName, typeRegistry);
|
|
302
|
+
return resolved?.fields ?? null;
|
|
303
|
+
}
|
|
304
|
+
function buildMutationHandler(table, operation, targetName, typeRegistry, ormTypes) {
|
|
227
305
|
const { singularName } = (0, utils_1.getTableNames)(table);
|
|
228
306
|
const pkFields = (0, utils_1.getPrimaryKeyInfo)(table);
|
|
229
307
|
const pk = pkFields[0];
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
308
|
+
// Get the set of writable field names from the type registry
|
|
309
|
+
// This filters out computed fields (e.g. searchTsvRank, hashUuid) that exist
|
|
310
|
+
// on the entity type but not on the create/update input type.
|
|
311
|
+
const writableFields = getWritableFieldNames(table, typeRegistry);
|
|
234
312
|
// Get fields that have defaults from introspection (for create operations)
|
|
235
313
|
const fieldsWithDefaults = getFieldsWithDefaults(table, typeRegistry);
|
|
314
|
+
// For create: include fields that are in the create input type.
|
|
315
|
+
// For update/delete: always exclude the PK (it goes in `where`, not `data`).
|
|
316
|
+
// The ORM input-types generator always excludes these fields from create inputs
|
|
317
|
+
// (see EXCLUDED_MUTATION_FIELDS in input-types-generator.ts). We must match this
|
|
318
|
+
// to avoid generating data properties that don't exist on the ORM create type.
|
|
319
|
+
// For non-'id' PKs (e.g. NodeTypeRegistry.name), we allow them in create data
|
|
320
|
+
// since they are user-provided natural keys that DO appear in the create input.
|
|
321
|
+
const ORM_EXCLUDED_FIELDS = ['id', 'createdAt', 'updatedAt', 'nodeId'];
|
|
322
|
+
const editableFields = (0, utils_1.getScalarFields)(table).filter((f) =>
|
|
323
|
+
// For update/delete: always exclude PK (it goes in `where`, not `data`)
|
|
324
|
+
// For create: exclude PK only if it's in the ORM exclusion list (e.g. 'id')
|
|
325
|
+
(f.name !== pk.name || (operation === 'create' && !ORM_EXCLUDED_FIELDS.includes(pk.name))) &&
|
|
326
|
+
// Always exclude ORM-excluded fields (except PK which is handled above)
|
|
327
|
+
(f.name === pk.name || !ORM_EXCLUDED_FIELDS.includes(f.name)) &&
|
|
328
|
+
// If we have type registry info, only include fields that exist in the input type
|
|
329
|
+
(writableFields === null || writableFields.has(f.name)));
|
|
236
330
|
const questions = [];
|
|
237
331
|
if (operation === 'update' || operation === 'delete') {
|
|
238
332
|
questions.push(t.objectExpression([
|
|
@@ -261,27 +355,36 @@ function buildMutationHandler(table, operation, targetName, typeRegistry) {
|
|
|
261
355
|
])
|
|
262
356
|
: buildSelectObject(table);
|
|
263
357
|
let ormArgs;
|
|
358
|
+
// Build data properties without individual type assertions.
|
|
359
|
+
// Instead, we build a plain object from cleanedData and cast the entire
|
|
360
|
+
// data value through `unknown` to bridge the type gap between
|
|
361
|
+
// Record<string, unknown> and the ORM's specific input type.
|
|
362
|
+
// This handles scalars, enums (string literal unions like ObjectCategory),
|
|
363
|
+
// and array fields uniformly without needing to import each type.
|
|
364
|
+
const buildDataProps = () => editableFields.map((f) => t.objectProperty(t.identifier(f.name), t.memberExpression(t.identifier('cleanedData'), t.identifier(f.name))));
|
|
264
365
|
if (operation === 'create') {
|
|
265
|
-
const dataProps = editableFields.map((f) => t.objectProperty(t.identifier(f.name), t.memberExpression(t.identifier('cleanedData'), t.identifier(f.name)), false, true));
|
|
266
366
|
ormArgs = t.objectExpression([
|
|
267
|
-
t.objectProperty(t.identifier('data'), t.objectExpression(
|
|
367
|
+
t.objectProperty(t.identifier('data'), t.objectExpression(buildDataProps())),
|
|
268
368
|
t.objectProperty(t.identifier('select'), selectObj),
|
|
269
369
|
]);
|
|
270
370
|
}
|
|
271
371
|
else if (operation === 'update') {
|
|
272
|
-
const dataProps = editableFields.map((f) => t.objectProperty(t.identifier(f.name), t.memberExpression(t.identifier('cleanedData'), t.identifier(f.name)), false, true));
|
|
273
372
|
ormArgs = t.objectExpression([
|
|
274
373
|
t.objectProperty(t.identifier('where'), t.objectExpression([
|
|
275
|
-
t.objectProperty(t.identifier(pk.name), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(pk.name)),
|
|
374
|
+
t.objectProperty(t.identifier(pk.name), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(pk.name)), pk.gqlType === 'Int' || pk.gqlType === 'BigInt'
|
|
375
|
+
? t.tsNumberKeyword()
|
|
376
|
+
: t.tsStringKeyword())),
|
|
276
377
|
])),
|
|
277
|
-
t.objectProperty(t.identifier('data'), t.objectExpression(
|
|
378
|
+
t.objectProperty(t.identifier('data'), t.objectExpression(buildDataProps())),
|
|
278
379
|
t.objectProperty(t.identifier('select'), selectObj),
|
|
279
380
|
]);
|
|
280
381
|
}
|
|
281
382
|
else {
|
|
282
383
|
ormArgs = t.objectExpression([
|
|
283
384
|
t.objectProperty(t.identifier('where'), t.objectExpression([
|
|
284
|
-
t.objectProperty(t.identifier(pk.name), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(pk.name)),
|
|
385
|
+
t.objectProperty(t.identifier(pk.name), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(pk.name)), pk.gqlType === 'Int' || pk.gqlType === 'BigInt'
|
|
386
|
+
? t.tsNumberKeyword()
|
|
387
|
+
: t.tsStringKeyword())),
|
|
285
388
|
])),
|
|
286
389
|
t.objectProperty(t.identifier('select'), selectObj),
|
|
287
390
|
]);
|
|
@@ -298,11 +401,25 @@ function buildMutationHandler(table, operation, targetName, typeRegistry) {
|
|
|
298
401
|
]),
|
|
299
402
|
];
|
|
300
403
|
if (operation !== 'delete') {
|
|
404
|
+
// Build stripUndefined call and cast to the proper ORM input type
|
|
405
|
+
// so that property accesses on cleanedData are correctly typed.
|
|
406
|
+
const stripUndefinedCall = t.callExpression(t.identifier('stripUndefined'), [
|
|
407
|
+
t.identifier('answers'),
|
|
408
|
+
t.identifier('fieldSchema'),
|
|
409
|
+
]);
|
|
410
|
+
let cleanedDataExpr = stripUndefinedCall;
|
|
411
|
+
if (ormTypes) {
|
|
412
|
+
if (operation === 'create') {
|
|
413
|
+
// cleanedData as CreateXxxInput['fieldName']
|
|
414
|
+
cleanedDataExpr = t.tsAsExpression(stripUndefinedCall, t.tsIndexedAccessType(t.tsTypeReference(t.identifier(ormTypes.createInputTypeName)), t.tsLiteralType(t.stringLiteral(ormTypes.innerFieldName))));
|
|
415
|
+
}
|
|
416
|
+
else if (operation === 'update') {
|
|
417
|
+
// cleanedData as XxxPatch
|
|
418
|
+
cleanedDataExpr = t.tsAsExpression(stripUndefinedCall, t.tsTypeReference(t.identifier(ormTypes.patchTypeName)));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
301
421
|
tryBody.push(t.variableDeclaration('const', [
|
|
302
|
-
t.variableDeclarator(t.identifier('cleanedData'),
|
|
303
|
-
t.identifier('answers'),
|
|
304
|
-
t.identifier('fieldSchema'),
|
|
305
|
-
])),
|
|
422
|
+
t.variableDeclarator(t.identifier('cleanedData'), cleanedDataExpr),
|
|
306
423
|
]));
|
|
307
424
|
}
|
|
308
425
|
tryBody.push(buildGetClientStatement(targetName), t.variableDeclaration('const', [
|
|
@@ -330,25 +447,60 @@ function generateTableCommand(table, options) {
|
|
|
330
447
|
statements.push(createImportDeclaration(executorPath, ['getClient']));
|
|
331
448
|
const utilsPath = options?.targetName ? '../../utils' : '../utils';
|
|
332
449
|
statements.push(createImportDeclaration(utilsPath, ['coerceAnswers', 'stripUndefined']));
|
|
450
|
+
statements.push(createImportDeclaration(utilsPath, ['FieldSchema'], true));
|
|
451
|
+
// Import ORM input types for proper type assertions in mutation handlers.
|
|
452
|
+
// These types ensure that cleanedData is cast to the correct ORM input type
|
|
453
|
+
// (e.g., CreateAppPermissionInput['appPermission'] for create, AppPermissionPatch for update)
|
|
454
|
+
// instead of remaining as Record<string, unknown>.
|
|
455
|
+
const createInputTypeName = (0, utils_1.getCreateInputTypeName)(table);
|
|
456
|
+
const patchTypeName = (0, utils_1.getPatchTypeName)(table);
|
|
457
|
+
const innerFieldName = (0, utils_1.lcFirst)(table.name);
|
|
458
|
+
// Commands are at cli/commands/xxx.ts (no target) or cli/commands/{target}/xxx.ts (with target).
|
|
459
|
+
// ORM input-types is at orm/input-types.ts — two or three levels up from commands.
|
|
460
|
+
const inputTypesPath = options?.targetName
|
|
461
|
+
? `../../../orm/input-types`
|
|
462
|
+
: `../../orm/input-types`;
|
|
463
|
+
statements.push(createImportDeclaration(inputTypesPath, [createInputTypeName, patchTypeName], true));
|
|
333
464
|
// Generate field schema for type coercion
|
|
465
|
+
// Use explicit FieldSchema type annotation so TS narrows string literals to FieldType
|
|
466
|
+
const fieldSchemaId = t.identifier('fieldSchema');
|
|
467
|
+
fieldSchemaId.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('FieldSchema')));
|
|
334
468
|
statements.push(t.variableDeclaration('const', [
|
|
335
|
-
t.variableDeclarator(
|
|
469
|
+
t.variableDeclarator(fieldSchemaId, buildFieldSchemaObject(table)),
|
|
336
470
|
]));
|
|
337
|
-
|
|
471
|
+
// Determine which operations the ORM model supports for this table.
|
|
472
|
+
// Most tables have `one: null` simply because there's no dedicated GraphQL
|
|
473
|
+
// findOne query, but the ORM still generates `findOne` using the PK.
|
|
474
|
+
// The only tables WITHOUT `findOne` are pure record types from SQL functions
|
|
475
|
+
// (e.g. GetAllRecord, OrgGetManagersRecord) which have no update/delete either.
|
|
476
|
+
// We detect these by checking: if one, update, AND delete are all null, it's a
|
|
477
|
+
// read-only record type with no `findOne`.
|
|
478
|
+
const hasUpdate = table.query?.update !== undefined && table.query?.update !== null;
|
|
479
|
+
const hasDelete = table.query?.delete !== undefined && table.query?.delete !== null;
|
|
480
|
+
const hasGet = table.query?.one !== null || hasUpdate || hasDelete;
|
|
481
|
+
const subcommands = ['list'];
|
|
482
|
+
if (hasGet)
|
|
483
|
+
subcommands.push('get');
|
|
484
|
+
subcommands.push('create');
|
|
485
|
+
if (hasUpdate)
|
|
486
|
+
subcommands.push('update');
|
|
487
|
+
if (hasDelete)
|
|
488
|
+
subcommands.push('delete');
|
|
338
489
|
const usageLines = [
|
|
339
490
|
'',
|
|
340
491
|
`${commandName} <command>`,
|
|
341
492
|
'',
|
|
342
493
|
'Commands:',
|
|
343
494
|
` list List all ${singularName} records`,
|
|
344
|
-
` get Get a ${singularName} by ID`,
|
|
345
|
-
` create Create a new ${singularName}`,
|
|
346
|
-
` update Update an existing ${singularName}`,
|
|
347
|
-
` delete Delete a ${singularName}`,
|
|
348
|
-
'',
|
|
349
|
-
' --help, -h Show this help message',
|
|
350
|
-
'',
|
|
351
495
|
];
|
|
496
|
+
if (hasGet)
|
|
497
|
+
usageLines.push(` get Get a ${singularName} by ID`);
|
|
498
|
+
usageLines.push(` create Create a new ${singularName}`);
|
|
499
|
+
if (hasUpdate)
|
|
500
|
+
usageLines.push(` update Update an existing ${singularName}`);
|
|
501
|
+
if (hasDelete)
|
|
502
|
+
usageLines.push(` delete Delete a ${singularName}`);
|
|
503
|
+
usageLines.push('', ' --help, -h Show this help message', '');
|
|
352
504
|
statements.push(t.variableDeclaration('const', [
|
|
353
505
|
t.variableDeclarator(t.identifier('usage'), t.stringLiteral(usageLines.join('\n'))),
|
|
354
506
|
]));
|
|
@@ -386,7 +538,7 @@ function generateTableCommand(table, options) {
|
|
|
386
538
|
]))),
|
|
387
539
|
]),
|
|
388
540
|
t.returnStatement(t.callExpression(t.identifier('handleTableSubcommand'), [
|
|
389
|
-
t.memberExpression(t.identifier('answer'), t.identifier('subcommand')),
|
|
541
|
+
t.tsAsExpression(t.memberExpression(t.identifier('answer'), t.identifier('subcommand')), t.tsStringKeyword()),
|
|
390
542
|
t.identifier('newArgv'),
|
|
391
543
|
t.identifier('prompter'),
|
|
392
544
|
])),
|
|
@@ -408,11 +560,15 @@ function generateTableCommand(table, options) {
|
|
|
408
560
|
buildSubcommandSwitch(subcommands, 'handle', 'usage'),
|
|
409
561
|
]), false, true));
|
|
410
562
|
const tn = options?.targetName;
|
|
563
|
+
const ormTypes = { createInputTypeName, patchTypeName, innerFieldName };
|
|
411
564
|
statements.push(buildListHandler(table, tn));
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
statements.push(buildMutationHandler(table, '
|
|
415
|
-
|
|
565
|
+
if (hasGet)
|
|
566
|
+
statements.push(buildGetHandler(table, tn));
|
|
567
|
+
statements.push(buildMutationHandler(table, 'create', tn, options?.typeRegistry, ormTypes));
|
|
568
|
+
if (hasUpdate)
|
|
569
|
+
statements.push(buildMutationHandler(table, 'update', tn, options?.typeRegistry, ormTypes));
|
|
570
|
+
if (hasDelete)
|
|
571
|
+
statements.push(buildMutationHandler(table, 'delete', tn, options?.typeRegistry, ormTypes));
|
|
416
572
|
const header = (0, utils_1.getGeneratedFileHeader)(`CLI commands for ${table.name}`);
|
|
417
573
|
const code = (0, babel_ast_1.generateCode)(statements);
|
|
418
574
|
return {
|
|
@@ -20,6 +20,16 @@ export interface SkillDefinition {
|
|
|
20
20
|
}[];
|
|
21
21
|
language?: string;
|
|
22
22
|
}
|
|
23
|
+
export interface SkillReferenceDefinition {
|
|
24
|
+
title: string;
|
|
25
|
+
description: string;
|
|
26
|
+
usage: string[];
|
|
27
|
+
examples: {
|
|
28
|
+
description: string;
|
|
29
|
+
code: string[];
|
|
30
|
+
}[];
|
|
31
|
+
language?: string;
|
|
32
|
+
}
|
|
23
33
|
export declare function getReadmeHeader(title: string): string[];
|
|
24
34
|
export declare function getReadmeFooter(): string[];
|
|
25
35
|
export declare function resolveDocsConfig(docs: DocsConfig | boolean | undefined): DocsConfig;
|
|
@@ -27,4 +37,5 @@ export declare function formatArgType(arg: CleanOperation['args'][number]): stri
|
|
|
27
37
|
export declare function formatTypeRef(t: CleanOperation['args'][number]['type']): string;
|
|
28
38
|
export declare function getEditableFields(table: CleanTable): CleanField[];
|
|
29
39
|
export declare function gqlTypeToJsonSchemaType(gqlType: string): string;
|
|
30
|
-
export declare function buildSkillFile(skill: SkillDefinition): string;
|
|
40
|
+
export declare function buildSkillFile(skill: SkillDefinition, referenceNames?: string[]): string;
|
|
41
|
+
export declare function buildSkillReference(ref: SkillReferenceDefinition): string;
|
|
@@ -8,6 +8,7 @@ exports.formatTypeRef = formatTypeRef;
|
|
|
8
8
|
exports.getEditableFields = getEditableFields;
|
|
9
9
|
exports.gqlTypeToJsonSchemaType = gqlTypeToJsonSchemaType;
|
|
10
10
|
exports.buildSkillFile = buildSkillFile;
|
|
11
|
+
exports.buildSkillReference = buildSkillReference;
|
|
11
12
|
const utils_1 = require("./utils");
|
|
12
13
|
const CONSTRUCTIVE_LOGO_URL = 'https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg';
|
|
13
14
|
const CONSTRUCTIVE_REPO = 'https://github.com/constructive-io/constructive';
|
|
@@ -89,9 +90,15 @@ function gqlTypeToJsonSchemaType(gqlType) {
|
|
|
89
90
|
return 'string';
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
|
-
function buildSkillFile(skill) {
|
|
93
|
+
function buildSkillFile(skill, referenceNames) {
|
|
93
94
|
const lang = skill.language ?? 'bash';
|
|
94
95
|
const lines = [];
|
|
96
|
+
// YAML frontmatter (Agent Skills format)
|
|
97
|
+
lines.push('---');
|
|
98
|
+
lines.push(`name: ${skill.name}`);
|
|
99
|
+
lines.push(`description: ${skill.description}`);
|
|
100
|
+
lines.push('---');
|
|
101
|
+
lines.push('');
|
|
95
102
|
lines.push(`# ${skill.name}`);
|
|
96
103
|
lines.push('');
|
|
97
104
|
lines.push('<!-- @constructive-io/graphql-codegen - DO NOT EDIT -->');
|
|
@@ -118,5 +125,46 @@ function buildSkillFile(skill) {
|
|
|
118
125
|
lines.push('```');
|
|
119
126
|
lines.push('');
|
|
120
127
|
}
|
|
128
|
+
if (referenceNames && referenceNames.length > 0) {
|
|
129
|
+
lines.push('## References');
|
|
130
|
+
lines.push('');
|
|
131
|
+
lines.push('See the `references/` directory for detailed per-entity API documentation:');
|
|
132
|
+
lines.push('');
|
|
133
|
+
for (const name of referenceNames) {
|
|
134
|
+
lines.push(`- [${name}](references/${name}.md)`);
|
|
135
|
+
}
|
|
136
|
+
lines.push('');
|
|
137
|
+
}
|
|
138
|
+
return lines.join('\n');
|
|
139
|
+
}
|
|
140
|
+
function buildSkillReference(ref) {
|
|
141
|
+
const lang = ref.language ?? 'bash';
|
|
142
|
+
const lines = [];
|
|
143
|
+
lines.push(`# ${ref.title}`);
|
|
144
|
+
lines.push('');
|
|
145
|
+
lines.push('<!-- @constructive-io/graphql-codegen - DO NOT EDIT -->');
|
|
146
|
+
lines.push('');
|
|
147
|
+
lines.push(ref.description);
|
|
148
|
+
lines.push('');
|
|
149
|
+
lines.push('## Usage');
|
|
150
|
+
lines.push('');
|
|
151
|
+
lines.push(`\`\`\`${lang}`);
|
|
152
|
+
for (const u of ref.usage) {
|
|
153
|
+
lines.push(u);
|
|
154
|
+
}
|
|
155
|
+
lines.push('```');
|
|
156
|
+
lines.push('');
|
|
157
|
+
lines.push('## Examples');
|
|
158
|
+
lines.push('');
|
|
159
|
+
for (const ex of ref.examples) {
|
|
160
|
+
lines.push(`### ${ex.description}`);
|
|
161
|
+
lines.push('');
|
|
162
|
+
lines.push(`\`\`\`${lang}`);
|
|
163
|
+
for (const cmd of ex.code) {
|
|
164
|
+
lines.push(cmd);
|
|
165
|
+
}
|
|
166
|
+
lines.push('```');
|
|
167
|
+
lines.push('');
|
|
168
|
+
}
|
|
121
169
|
return lines.join('\n');
|
|
122
170
|
}
|
|
@@ -3,4 +3,4 @@ import type { GeneratedDocFile, McpTool } from './docs-utils';
|
|
|
3
3
|
export declare function generateHooksReadme(tables: CleanTable[], customOperations: CleanOperation[]): GeneratedDocFile;
|
|
4
4
|
export declare function generateHooksAgentsDocs(tables: CleanTable[], customOperations: CleanOperation[]): GeneratedDocFile;
|
|
5
5
|
export declare function getHooksMcpTools(tables: CleanTable[], customOperations: CleanOperation[]): McpTool[];
|
|
6
|
-
export declare function generateHooksSkills(tables: CleanTable[], customOperations: CleanOperation[]): GeneratedDocFile[];
|
|
6
|
+
export declare function generateHooksSkills(tables: CleanTable[], customOperations: CleanOperation[], targetName: string): GeneratedDocFile[];
|