@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
|
@@ -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 {
|
|
@@ -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;
|
|
@@ -79,9 +79,15 @@ export function gqlTypeToJsonSchemaType(gqlType) {
|
|
|
79
79
|
return 'string';
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
-
export function buildSkillFile(skill) {
|
|
82
|
+
export function buildSkillFile(skill, referenceNames) {
|
|
83
83
|
const lang = skill.language ?? 'bash';
|
|
84
84
|
const lines = [];
|
|
85
|
+
// YAML frontmatter (Agent Skills format)
|
|
86
|
+
lines.push('---');
|
|
87
|
+
lines.push(`name: ${skill.name}`);
|
|
88
|
+
lines.push(`description: ${skill.description}`);
|
|
89
|
+
lines.push('---');
|
|
90
|
+
lines.push('');
|
|
85
91
|
lines.push(`# ${skill.name}`);
|
|
86
92
|
lines.push('');
|
|
87
93
|
lines.push('<!-- @constructive-io/graphql-codegen - DO NOT EDIT -->');
|
|
@@ -108,5 +114,46 @@ export function buildSkillFile(skill) {
|
|
|
108
114
|
lines.push('```');
|
|
109
115
|
lines.push('');
|
|
110
116
|
}
|
|
117
|
+
if (referenceNames && referenceNames.length > 0) {
|
|
118
|
+
lines.push('## References');
|
|
119
|
+
lines.push('');
|
|
120
|
+
lines.push('See the `references/` directory for detailed per-entity API documentation:');
|
|
121
|
+
lines.push('');
|
|
122
|
+
for (const name of referenceNames) {
|
|
123
|
+
lines.push(`- [${name}](references/${name}.md)`);
|
|
124
|
+
}
|
|
125
|
+
lines.push('');
|
|
126
|
+
}
|
|
127
|
+
return lines.join('\n');
|
|
128
|
+
}
|
|
129
|
+
export function buildSkillReference(ref) {
|
|
130
|
+
const lang = ref.language ?? 'bash';
|
|
131
|
+
const lines = [];
|
|
132
|
+
lines.push(`# ${ref.title}`);
|
|
133
|
+
lines.push('');
|
|
134
|
+
lines.push('<!-- @constructive-io/graphql-codegen - DO NOT EDIT -->');
|
|
135
|
+
lines.push('');
|
|
136
|
+
lines.push(ref.description);
|
|
137
|
+
lines.push('');
|
|
138
|
+
lines.push('## Usage');
|
|
139
|
+
lines.push('');
|
|
140
|
+
lines.push(`\`\`\`${lang}`);
|
|
141
|
+
for (const u of ref.usage) {
|
|
142
|
+
lines.push(u);
|
|
143
|
+
}
|
|
144
|
+
lines.push('```');
|
|
145
|
+
lines.push('');
|
|
146
|
+
lines.push('## Examples');
|
|
147
|
+
lines.push('');
|
|
148
|
+
for (const ex of ref.examples) {
|
|
149
|
+
lines.push(`### ${ex.description}`);
|
|
150
|
+
lines.push('');
|
|
151
|
+
lines.push(`\`\`\`${lang}`);
|
|
152
|
+
for (const cmd of ex.code) {
|
|
153
|
+
lines.push(cmd);
|
|
154
|
+
}
|
|
155
|
+
lines.push('```');
|
|
156
|
+
lines.push('');
|
|
157
|
+
}
|
|
111
158
|
return lines.join('\n');
|
|
112
159
|
}
|
|
@@ -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[];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { toKebabCase } from 'komoji';
|
|
2
|
+
import { buildSkillFile, buildSkillReference, formatArgType, getReadmeHeader, getReadmeFooter, gqlTypeToJsonSchemaType, } from './docs-utils';
|
|
2
3
|
import { getTableNames, getScalarFields, getPrimaryKeyInfo, getListQueryHookName, getSingleQueryHookName, getCreateMutationHookName, getUpdateMutationHookName, getDeleteMutationHookName, hasValidPrimaryKey, ucFirst, lcFirst, fieldTypeToTs, } from './utils';
|
|
3
4
|
function getCustomHookName(op) {
|
|
4
5
|
if (op.kind === 'query') {
|
|
@@ -371,8 +372,11 @@ export function getHooksMcpTools(tables, customOperations) {
|
|
|
371
372
|
}
|
|
372
373
|
return tools;
|
|
373
374
|
}
|
|
374
|
-
export function generateHooksSkills(tables, customOperations) {
|
|
375
|
+
export function generateHooksSkills(tables, customOperations, targetName) {
|
|
375
376
|
const files = [];
|
|
377
|
+
const skillName = `hooks-${targetName}`;
|
|
378
|
+
const referenceNames = [];
|
|
379
|
+
// Generate reference files for each table
|
|
376
380
|
for (const table of tables) {
|
|
377
381
|
const { singularName, pluralName } = getTableNames(table);
|
|
378
382
|
const pk = getPrimaryKeyInfo(table)[0];
|
|
@@ -380,10 +384,12 @@ export function generateHooksSkills(tables, customOperations) {
|
|
|
380
384
|
const selectFields = scalarFields
|
|
381
385
|
.map((f) => `${f.name}: true`)
|
|
382
386
|
.join(', ');
|
|
387
|
+
const refName = toKebabCase(singularName);
|
|
388
|
+
referenceNames.push(refName);
|
|
383
389
|
files.push({
|
|
384
|
-
fileName:
|
|
385
|
-
content:
|
|
386
|
-
|
|
390
|
+
fileName: `${skillName}/references/${refName}.md`,
|
|
391
|
+
content: buildSkillReference({
|
|
392
|
+
title: singularName,
|
|
387
393
|
description: table.description || `React Query hooks for ${table.name} data operations`,
|
|
388
394
|
language: 'typescript',
|
|
389
395
|
usage: [
|
|
@@ -423,15 +429,18 @@ export function generateHooksSkills(tables, customOperations) {
|
|
|
423
429
|
}),
|
|
424
430
|
});
|
|
425
431
|
}
|
|
432
|
+
// Generate reference files for custom operations
|
|
426
433
|
for (const op of customOperations) {
|
|
427
434
|
const hookName = getCustomHookName(op);
|
|
428
435
|
const callArgs = op.args.length > 0
|
|
429
436
|
? `{ ${op.args.map((a) => `${a.name}: '<value>'`).join(', ')} }`
|
|
430
437
|
: '';
|
|
438
|
+
const refName = toKebabCase(op.name);
|
|
439
|
+
referenceNames.push(refName);
|
|
431
440
|
files.push({
|
|
432
|
-
fileName:
|
|
433
|
-
content:
|
|
434
|
-
|
|
441
|
+
fileName: `${skillName}/references/${refName}.md`,
|
|
442
|
+
content: buildSkillReference({
|
|
443
|
+
title: op.name,
|
|
435
444
|
description: op.description ||
|
|
436
445
|
`React Query ${op.kind} hook for ${op.name}`,
|
|
437
446
|
language: 'typescript',
|
|
@@ -458,5 +467,36 @@ export function generateHooksSkills(tables, customOperations) {
|
|
|
458
467
|
}),
|
|
459
468
|
});
|
|
460
469
|
}
|
|
470
|
+
// Generate the overview SKILL.md
|
|
471
|
+
const hookExamples = tables.slice(0, 3).map((t) => getListQueryHookName(t));
|
|
472
|
+
files.push({
|
|
473
|
+
fileName: `${skillName}/SKILL.md`,
|
|
474
|
+
content: buildSkillFile({
|
|
475
|
+
name: skillName,
|
|
476
|
+
description: `React Query hooks for the ${targetName} API — provides typed query and mutation hooks for ${tables.length} tables and ${customOperations.length} custom operations`,
|
|
477
|
+
language: 'typescript',
|
|
478
|
+
usage: [
|
|
479
|
+
`// Import hooks`,
|
|
480
|
+
`import { ${hookExamples[0] || 'useModelQuery'} } from './hooks';`,
|
|
481
|
+
'',
|
|
482
|
+
`// Query hooks: use<Model>Query, use<Model>sQuery`,
|
|
483
|
+
`// Mutation hooks: useCreate<Model>Mutation, useUpdate<Model>Mutation, useDelete<Model>Mutation`,
|
|
484
|
+
'',
|
|
485
|
+
`const { data, isLoading } = ${hookExamples[0] || 'useModelQuery'}({`,
|
|
486
|
+
` selection: { fields: { id: true } },`,
|
|
487
|
+
`});`,
|
|
488
|
+
],
|
|
489
|
+
examples: [
|
|
490
|
+
{
|
|
491
|
+
description: 'Query records',
|
|
492
|
+
code: [
|
|
493
|
+
`const { data, isLoading } = ${hookExamples[0] || 'useModelQuery'}({`,
|
|
494
|
+
' selection: { fields: { id: true } },',
|
|
495
|
+
'});',
|
|
496
|
+
],
|
|
497
|
+
},
|
|
498
|
+
],
|
|
499
|
+
}, referenceNames),
|
|
500
|
+
});
|
|
461
501
|
return files;
|
|
462
502
|
}
|
|
@@ -3,4 +3,4 @@ import type { GeneratedDocFile, McpTool } from '../docs-utils';
|
|
|
3
3
|
export declare function generateOrmReadme(tables: CleanTable[], customOperations: CleanOperation[]): GeneratedDocFile;
|
|
4
4
|
export declare function generateOrmAgentsDocs(tables: CleanTable[], customOperations: CleanOperation[]): GeneratedDocFile;
|
|
5
5
|
export declare function getOrmMcpTools(tables: CleanTable[], customOperations: CleanOperation[]): McpTool[];
|
|
6
|
-
export declare function generateOrmSkills(tables: CleanTable[], customOperations: CleanOperation[]): GeneratedDocFile[];
|
|
6
|
+
export declare function generateOrmSkills(tables: CleanTable[], customOperations: CleanOperation[], targetName: string): GeneratedDocFile[];
|