@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.
Files changed (37) hide show
  1. package/core/codegen/cli/command-map-generator.js +36 -11
  2. package/core/codegen/cli/custom-command-generator.js +59 -13
  3. package/core/codegen/cli/docs-generator.d.ts +1 -1
  4. package/core/codegen/cli/docs-generator.js +137 -54
  5. package/core/codegen/cli/executor-generator.js +20 -6
  6. package/core/codegen/cli/infra-generator.js +17 -14
  7. package/core/codegen/cli/table-command-generator.js +209 -53
  8. package/core/codegen/docs-utils.d.ts +12 -1
  9. package/core/codegen/docs-utils.js +49 -1
  10. package/core/codegen/hooks-docs-generator.d.ts +1 -1
  11. package/core/codegen/hooks-docs-generator.js +47 -7
  12. package/core/codegen/orm/docs-generator.d.ts +1 -1
  13. package/core/codegen/orm/docs-generator.js +57 -20
  14. package/core/generate.d.ts +5 -0
  15. package/core/generate.js +48 -12
  16. package/core/workspace.d.ts +13 -0
  17. package/core/workspace.js +92 -0
  18. package/esm/core/codegen/cli/command-map-generator.js +36 -11
  19. package/esm/core/codegen/cli/custom-command-generator.js +60 -14
  20. package/esm/core/codegen/cli/docs-generator.d.ts +1 -1
  21. package/esm/core/codegen/cli/docs-generator.js +138 -55
  22. package/esm/core/codegen/cli/executor-generator.js +20 -6
  23. package/esm/core/codegen/cli/infra-generator.js +17 -14
  24. package/esm/core/codegen/cli/table-command-generator.js +210 -54
  25. package/esm/core/codegen/docs-utils.d.ts +12 -1
  26. package/esm/core/codegen/docs-utils.js +48 -1
  27. package/esm/core/codegen/hooks-docs-generator.d.ts +1 -1
  28. package/esm/core/codegen/hooks-docs-generator.js +48 -8
  29. package/esm/core/codegen/orm/docs-generator.d.ts +1 -1
  30. package/esm/core/codegen/orm/docs-generator.js +58 -21
  31. package/esm/core/generate.d.ts +5 -0
  32. package/esm/core/generate.js +48 -12
  33. package/esm/core/workspace.d.ts +13 -0
  34. package/esm/core/workspace.js +89 -0
  35. package/esm/types/config.d.ts +12 -4
  36. package/package.json +22 -22
  37. 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
- function getFieldsWithDefaults(table, typeRegistry) {
157
- const fieldsWithDefaults = new Set();
158
- if (!typeRegistry)
159
- return fieldsWithDefaults;
160
- // Look up the CreateXInput type (e.g. CreateDatabaseInput)
161
- const createInputTypeName = getCreateInputTypeName(table);
162
- const createInputType = typeRegistry.get(createInputTypeName);
163
- if (!createInputType?.inputFields)
164
- return fieldsWithDefaults;
165
- // The CreateXInput has an inner field (e.g. "database" of type DatabaseInput)
166
- // Find the inner input type that contains the actual field definitions
167
- for (const inputField of createInputType.inputFields) {
168
- // The inner field's type name is the actual input type (e.g. DatabaseInput)
212
+ /**
213
+ * Resolve the inner input type from a CreateXInput or UpdateXInput type.
214
+ * The CreateXInput has an inner field (e.g. "database" of type DatabaseInput)
215
+ * that contains the actual field definitions.
216
+ */
217
+ function resolveInnerInputType(inputTypeName, typeRegistry) {
218
+ const inputType = typeRegistry.get(inputTypeName);
219
+ if (!inputType?.inputFields)
220
+ return null;
221
+ for (const inputField of inputType.inputFields) {
169
222
  const innerTypeName = inputField.type.name
170
223
  || inputField.type.ofType?.name
171
224
  || inputField.type.ofType?.ofType?.name;
@@ -174,29 +227,70 @@ function getFieldsWithDefaults(table, typeRegistry) {
174
227
  const innerType = typeRegistry.get(innerTypeName);
175
228
  if (!innerType?.inputFields)
176
229
  continue;
177
- // Check each field in the inner input type for defaultValue
178
- for (const field of innerType.inputFields) {
179
- if (field.defaultValue !== undefined) {
180
- fieldsWithDefaults.add(field.name);
181
- }
182
- // Also check if the field is NOT wrapped in NON_NULL (nullable = has default or is optional)
183
- if (field.type.kind !== 'NON_NULL') {
184
- fieldsWithDefaults.add(field.name);
185
- }
230
+ const fields = new Set(innerType.inputFields.map((f) => f.name));
231
+ return { name: innerTypeName, fields };
232
+ }
233
+ return null;
234
+ }
235
+ function getFieldsWithDefaults(table, typeRegistry) {
236
+ const fieldsWithDefaults = new Set();
237
+ if (!typeRegistry)
238
+ return fieldsWithDefaults;
239
+ const createInputTypeName = getCreateInputTypeName(table);
240
+ const resolved = resolveInnerInputType(createInputTypeName, typeRegistry);
241
+ if (!resolved)
242
+ return fieldsWithDefaults;
243
+ const innerType = typeRegistry.get(resolved.name);
244
+ if (!innerType?.inputFields)
245
+ return fieldsWithDefaults;
246
+ for (const field of innerType.inputFields) {
247
+ if (field.defaultValue !== undefined) {
248
+ fieldsWithDefaults.add(field.name);
249
+ }
250
+ if (field.type.kind !== 'NON_NULL') {
251
+ fieldsWithDefaults.add(field.name);
186
252
  }
187
253
  }
188
254
  return fieldsWithDefaults;
189
255
  }
190
- function buildMutationHandler(table, operation, targetName, typeRegistry) {
256
+ /**
257
+ * Get the set of field names that actually exist in the create/update input type.
258
+ * Fields not in this set (e.g. computed fields like searchTsvRank, hashUuid)
259
+ * should be excluded from the data object in create/update handlers.
260
+ */
261
+ function getWritableFieldNames(table, typeRegistry) {
262
+ if (!typeRegistry)
263
+ return null;
264
+ const createInputTypeName = getCreateInputTypeName(table);
265
+ const resolved = resolveInnerInputType(createInputTypeName, typeRegistry);
266
+ return resolved?.fields ?? null;
267
+ }
268
+ function buildMutationHandler(table, operation, targetName, typeRegistry, ormTypes) {
191
269
  const { singularName } = getTableNames(table);
192
270
  const pkFields = getPrimaryKeyInfo(table);
193
271
  const pk = pkFields[0];
194
- const editableFields = getScalarFields(table).filter((f) => f.name !== pk.name &&
195
- f.name !== 'nodeId' &&
196
- f.name !== 'createdAt' &&
197
- f.name !== 'updatedAt');
272
+ // Get the set of writable field names from the type registry
273
+ // This filters out computed fields (e.g. searchTsvRank, hashUuid) that exist
274
+ // on the entity type but not on the create/update input type.
275
+ const writableFields = getWritableFieldNames(table, typeRegistry);
198
276
  // Get fields that have defaults from introspection (for create operations)
199
277
  const fieldsWithDefaults = getFieldsWithDefaults(table, typeRegistry);
278
+ // For create: include fields that are in the create input type.
279
+ // For update/delete: always exclude the PK (it goes in `where`, not `data`).
280
+ // The ORM input-types generator always excludes these fields from create inputs
281
+ // (see EXCLUDED_MUTATION_FIELDS in input-types-generator.ts). We must match this
282
+ // to avoid generating data properties that don't exist on the ORM create type.
283
+ // For non-'id' PKs (e.g. NodeTypeRegistry.name), we allow them in create data
284
+ // since they are user-provided natural keys that DO appear in the create input.
285
+ const ORM_EXCLUDED_FIELDS = ['id', 'createdAt', 'updatedAt', 'nodeId'];
286
+ const editableFields = getScalarFields(table).filter((f) =>
287
+ // For update/delete: always exclude PK (it goes in `where`, not `data`)
288
+ // For create: exclude PK only if it's in the ORM exclusion list (e.g. 'id')
289
+ (f.name !== pk.name || (operation === 'create' && !ORM_EXCLUDED_FIELDS.includes(pk.name))) &&
290
+ // Always exclude ORM-excluded fields (except PK which is handled above)
291
+ (f.name === pk.name || !ORM_EXCLUDED_FIELDS.includes(f.name)) &&
292
+ // If we have type registry info, only include fields that exist in the input type
293
+ (writableFields === null || writableFields.has(f.name)));
200
294
  const questions = [];
201
295
  if (operation === 'update' || operation === 'delete') {
202
296
  questions.push(t.objectExpression([
@@ -225,27 +319,36 @@ function buildMutationHandler(table, operation, targetName, typeRegistry) {
225
319
  ])
226
320
  : buildSelectObject(table);
227
321
  let ormArgs;
322
+ // Build data properties without individual type assertions.
323
+ // Instead, we build a plain object from cleanedData and cast the entire
324
+ // data value through `unknown` to bridge the type gap between
325
+ // Record<string, unknown> and the ORM's specific input type.
326
+ // This handles scalars, enums (string literal unions like ObjectCategory),
327
+ // and array fields uniformly without needing to import each type.
328
+ const buildDataProps = () => editableFields.map((f) => t.objectProperty(t.identifier(f.name), t.memberExpression(t.identifier('cleanedData'), t.identifier(f.name))));
228
329
  if (operation === 'create') {
229
- const dataProps = editableFields.map((f) => t.objectProperty(t.identifier(f.name), t.memberExpression(t.identifier('cleanedData'), t.identifier(f.name)), false, true));
230
330
  ormArgs = t.objectExpression([
231
- t.objectProperty(t.identifier('data'), t.objectExpression(dataProps)),
331
+ t.objectProperty(t.identifier('data'), t.objectExpression(buildDataProps())),
232
332
  t.objectProperty(t.identifier('select'), selectObj),
233
333
  ]);
234
334
  }
235
335
  else if (operation === 'update') {
236
- const dataProps = editableFields.map((f) => t.objectProperty(t.identifier(f.name), t.memberExpression(t.identifier('cleanedData'), t.identifier(f.name)), false, true));
237
336
  ormArgs = t.objectExpression([
238
337
  t.objectProperty(t.identifier('where'), t.objectExpression([
239
- t.objectProperty(t.identifier(pk.name), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(pk.name)), t.tsStringKeyword())),
338
+ t.objectProperty(t.identifier(pk.name), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(pk.name)), pk.gqlType === 'Int' || pk.gqlType === 'BigInt'
339
+ ? t.tsNumberKeyword()
340
+ : t.tsStringKeyword())),
240
341
  ])),
241
- t.objectProperty(t.identifier('data'), t.objectExpression(dataProps)),
342
+ t.objectProperty(t.identifier('data'), t.objectExpression(buildDataProps())),
242
343
  t.objectProperty(t.identifier('select'), selectObj),
243
344
  ]);
244
345
  }
245
346
  else {
246
347
  ormArgs = t.objectExpression([
247
348
  t.objectProperty(t.identifier('where'), t.objectExpression([
248
- t.objectProperty(t.identifier(pk.name), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(pk.name)), t.tsStringKeyword())),
349
+ t.objectProperty(t.identifier(pk.name), t.tsAsExpression(t.memberExpression(t.identifier('answers'), t.identifier(pk.name)), pk.gqlType === 'Int' || pk.gqlType === 'BigInt'
350
+ ? t.tsNumberKeyword()
351
+ : t.tsStringKeyword())),
249
352
  ])),
250
353
  t.objectProperty(t.identifier('select'), selectObj),
251
354
  ]);
@@ -262,11 +365,25 @@ function buildMutationHandler(table, operation, targetName, typeRegistry) {
262
365
  ]),
263
366
  ];
264
367
  if (operation !== 'delete') {
368
+ // Build stripUndefined call and cast to the proper ORM input type
369
+ // so that property accesses on cleanedData are correctly typed.
370
+ const stripUndefinedCall = t.callExpression(t.identifier('stripUndefined'), [
371
+ t.identifier('answers'),
372
+ t.identifier('fieldSchema'),
373
+ ]);
374
+ let cleanedDataExpr = stripUndefinedCall;
375
+ if (ormTypes) {
376
+ if (operation === 'create') {
377
+ // cleanedData as CreateXxxInput['fieldName']
378
+ cleanedDataExpr = t.tsAsExpression(stripUndefinedCall, t.tsIndexedAccessType(t.tsTypeReference(t.identifier(ormTypes.createInputTypeName)), t.tsLiteralType(t.stringLiteral(ormTypes.innerFieldName))));
379
+ }
380
+ else if (operation === 'update') {
381
+ // cleanedData as XxxPatch
382
+ cleanedDataExpr = t.tsAsExpression(stripUndefinedCall, t.tsTypeReference(t.identifier(ormTypes.patchTypeName)));
383
+ }
384
+ }
265
385
  tryBody.push(t.variableDeclaration('const', [
266
- t.variableDeclarator(t.identifier('cleanedData'), t.callExpression(t.identifier('stripUndefined'), [
267
- t.identifier('answers'),
268
- t.identifier('fieldSchema'),
269
- ])),
386
+ t.variableDeclarator(t.identifier('cleanedData'), cleanedDataExpr),
270
387
  ]));
271
388
  }
272
389
  tryBody.push(buildGetClientStatement(targetName), t.variableDeclaration('const', [
@@ -294,25 +411,60 @@ export function generateTableCommand(table, options) {
294
411
  statements.push(createImportDeclaration(executorPath, ['getClient']));
295
412
  const utilsPath = options?.targetName ? '../../utils' : '../utils';
296
413
  statements.push(createImportDeclaration(utilsPath, ['coerceAnswers', 'stripUndefined']));
414
+ statements.push(createImportDeclaration(utilsPath, ['FieldSchema'], true));
415
+ // Import ORM input types for proper type assertions in mutation handlers.
416
+ // These types ensure that cleanedData is cast to the correct ORM input type
417
+ // (e.g., CreateAppPermissionInput['appPermission'] for create, AppPermissionPatch for update)
418
+ // instead of remaining as Record<string, unknown>.
419
+ const createInputTypeName = getCreateInputTypeName(table);
420
+ const patchTypeName = getPatchTypeName(table);
421
+ const innerFieldName = lcFirst(table.name);
422
+ // Commands are at cli/commands/xxx.ts (no target) or cli/commands/{target}/xxx.ts (with target).
423
+ // ORM input-types is at orm/input-types.ts — two or three levels up from commands.
424
+ const inputTypesPath = options?.targetName
425
+ ? `../../../orm/input-types`
426
+ : `../../orm/input-types`;
427
+ statements.push(createImportDeclaration(inputTypesPath, [createInputTypeName, patchTypeName], true));
297
428
  // Generate field schema for type coercion
429
+ // Use explicit FieldSchema type annotation so TS narrows string literals to FieldType
430
+ const fieldSchemaId = t.identifier('fieldSchema');
431
+ fieldSchemaId.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('FieldSchema')));
298
432
  statements.push(t.variableDeclaration('const', [
299
- t.variableDeclarator(t.identifier('fieldSchema'), buildFieldSchemaObject(table)),
433
+ t.variableDeclarator(fieldSchemaId, buildFieldSchemaObject(table)),
300
434
  ]));
301
- const subcommands = ['list', 'get', 'create', 'update', 'delete'];
435
+ // Determine which operations the ORM model supports for this table.
436
+ // Most tables have `one: null` simply because there's no dedicated GraphQL
437
+ // findOne query, but the ORM still generates `findOne` using the PK.
438
+ // The only tables WITHOUT `findOne` are pure record types from SQL functions
439
+ // (e.g. GetAllRecord, OrgGetManagersRecord) which have no update/delete either.
440
+ // We detect these by checking: if one, update, AND delete are all null, it's a
441
+ // read-only record type with no `findOne`.
442
+ const hasUpdate = table.query?.update !== undefined && table.query?.update !== null;
443
+ const hasDelete = table.query?.delete !== undefined && table.query?.delete !== null;
444
+ const hasGet = table.query?.one !== null || hasUpdate || hasDelete;
445
+ const subcommands = ['list'];
446
+ if (hasGet)
447
+ subcommands.push('get');
448
+ subcommands.push('create');
449
+ if (hasUpdate)
450
+ subcommands.push('update');
451
+ if (hasDelete)
452
+ subcommands.push('delete');
302
453
  const usageLines = [
303
454
  '',
304
455
  `${commandName} <command>`,
305
456
  '',
306
457
  'Commands:',
307
458
  ` list List all ${singularName} records`,
308
- ` get Get a ${singularName} by ID`,
309
- ` create Create a new ${singularName}`,
310
- ` update Update an existing ${singularName}`,
311
- ` delete Delete a ${singularName}`,
312
- '',
313
- ' --help, -h Show this help message',
314
- '',
315
459
  ];
460
+ if (hasGet)
461
+ usageLines.push(` get Get a ${singularName} by ID`);
462
+ usageLines.push(` create Create a new ${singularName}`);
463
+ if (hasUpdate)
464
+ usageLines.push(` update Update an existing ${singularName}`);
465
+ if (hasDelete)
466
+ usageLines.push(` delete Delete a ${singularName}`);
467
+ usageLines.push('', ' --help, -h Show this help message', '');
316
468
  statements.push(t.variableDeclaration('const', [
317
469
  t.variableDeclarator(t.identifier('usage'), t.stringLiteral(usageLines.join('\n'))),
318
470
  ]));
@@ -350,7 +502,7 @@ export function generateTableCommand(table, options) {
350
502
  ]))),
351
503
  ]),
352
504
  t.returnStatement(t.callExpression(t.identifier('handleTableSubcommand'), [
353
- t.memberExpression(t.identifier('answer'), t.identifier('subcommand')),
505
+ t.tsAsExpression(t.memberExpression(t.identifier('answer'), t.identifier('subcommand')), t.tsStringKeyword()),
354
506
  t.identifier('newArgv'),
355
507
  t.identifier('prompter'),
356
508
  ])),
@@ -372,11 +524,15 @@ export function generateTableCommand(table, options) {
372
524
  buildSubcommandSwitch(subcommands, 'handle', 'usage'),
373
525
  ]), false, true));
374
526
  const tn = options?.targetName;
527
+ const ormTypes = { createInputTypeName, patchTypeName, innerFieldName };
375
528
  statements.push(buildListHandler(table, tn));
376
- statements.push(buildGetHandler(table, tn));
377
- statements.push(buildMutationHandler(table, 'create', tn, options?.typeRegistry));
378
- statements.push(buildMutationHandler(table, 'update', tn, options?.typeRegistry));
379
- statements.push(buildMutationHandler(table, 'delete', tn, options?.typeRegistry));
529
+ if (hasGet)
530
+ statements.push(buildGetHandler(table, tn));
531
+ statements.push(buildMutationHandler(table, 'create', tn, options?.typeRegistry, ormTypes));
532
+ if (hasUpdate)
533
+ statements.push(buildMutationHandler(table, 'update', tn, options?.typeRegistry, ormTypes));
534
+ if (hasDelete)
535
+ statements.push(buildMutationHandler(table, 'delete', tn, options?.typeRegistry, ormTypes));
380
536
  const header = getGeneratedFileHeader(`CLI commands for ${table.name}`);
381
537
  const code = generateCode(statements);
382
538
  return {
@@ -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 { buildSkillFile, formatArgType, getReadmeHeader, getReadmeFooter, gqlTypeToJsonSchemaType, } from './docs-utils';
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: `skills/${lcFirst(singularName)}.md`,
385
- content: buildSkillFile({
386
- name: `hooks-${lcFirst(singularName)}`,
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: `skills/${op.name}.md`,
433
- content: buildSkillFile({
434
- name: `hooks-${op.name}`,
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[];