@constructive-io/graphql-codegen 2.21.0 → 2.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/cli/codegen/barrel.d.ts +4 -1
  2. package/cli/codegen/barrel.js +18 -12
  3. package/cli/codegen/client.js +33 -0
  4. package/cli/codegen/custom-mutations.d.ts +4 -0
  5. package/cli/codegen/custom-mutations.js +39 -13
  6. package/cli/codegen/custom-queries.d.ts +4 -0
  7. package/cli/codegen/custom-queries.js +36 -11
  8. package/cli/codegen/gql-ast.js +9 -5
  9. package/cli/codegen/index.js +35 -7
  10. package/cli/codegen/mutations.d.ts +2 -0
  11. package/cli/codegen/mutations.js +87 -23
  12. package/cli/codegen/orm/barrel.js +4 -2
  13. package/cli/codegen/orm/index.js +17 -0
  14. package/cli/codegen/orm/input-types-generator.js +83 -29
  15. package/cli/codegen/orm/model-generator.js +6 -4
  16. package/cli/codegen/queries.js +36 -27
  17. package/cli/codegen/scalars.d.ts +6 -4
  18. package/cli/codegen/scalars.js +17 -9
  19. package/cli/codegen/schema-types-generator.d.ts +26 -0
  20. package/cli/codegen/schema-types-generator.js +365 -0
  21. package/cli/codegen/ts-ast.d.ts +3 -1
  22. package/cli/codegen/ts-ast.js +2 -2
  23. package/cli/codegen/type-resolver.d.ts +52 -6
  24. package/cli/codegen/type-resolver.js +97 -19
  25. package/cli/codegen/types.d.ts +7 -4
  26. package/cli/codegen/types.js +94 -41
  27. package/cli/codegen/utils.d.ts +20 -2
  28. package/cli/codegen/utils.js +32 -7
  29. package/cli/commands/generate-orm.js +5 -5
  30. package/cli/commands/generate.d.ts +4 -1
  31. package/cli/commands/generate.js +27 -8
  32. package/cli/introspect/transform-schema.d.ts +33 -21
  33. package/cli/introspect/transform-schema.js +31 -21
  34. package/esm/cli/codegen/barrel.d.ts +4 -1
  35. package/esm/cli/codegen/barrel.js +18 -12
  36. package/esm/cli/codegen/client.js +33 -0
  37. package/esm/cli/codegen/custom-mutations.d.ts +4 -0
  38. package/esm/cli/codegen/custom-mutations.js +40 -14
  39. package/esm/cli/codegen/custom-queries.d.ts +4 -0
  40. package/esm/cli/codegen/custom-queries.js +37 -12
  41. package/esm/cli/codegen/gql-ast.js +10 -6
  42. package/esm/cli/codegen/index.js +35 -7
  43. package/esm/cli/codegen/mutations.d.ts +2 -0
  44. package/esm/cli/codegen/mutations.js +88 -24
  45. package/esm/cli/codegen/orm/barrel.js +4 -2
  46. package/esm/cli/codegen/orm/index.js +17 -0
  47. package/esm/cli/codegen/orm/input-types-generator.js +83 -29
  48. package/esm/cli/codegen/orm/model-generator.js +7 -5
  49. package/esm/cli/codegen/queries.js +37 -28
  50. package/esm/cli/codegen/scalars.d.ts +6 -4
  51. package/esm/cli/codegen/scalars.js +16 -8
  52. package/esm/cli/codegen/schema-types-generator.d.ts +26 -0
  53. package/esm/cli/codegen/schema-types-generator.js +362 -0
  54. package/esm/cli/codegen/ts-ast.d.ts +3 -1
  55. package/esm/cli/codegen/ts-ast.js +2 -2
  56. package/esm/cli/codegen/type-resolver.d.ts +52 -6
  57. package/esm/cli/codegen/type-resolver.js +97 -20
  58. package/esm/cli/codegen/types.d.ts +7 -4
  59. package/esm/cli/codegen/types.js +95 -41
  60. package/esm/cli/codegen/utils.d.ts +20 -2
  61. package/esm/cli/codegen/utils.js +31 -7
  62. package/esm/cli/commands/generate-orm.js +5 -5
  63. package/esm/cli/commands/generate.d.ts +4 -1
  64. package/esm/cli/commands/generate.js +27 -8
  65. package/esm/cli/introspect/transform-schema.d.ts +33 -21
  66. package/esm/cli/introspect/transform-schema.js +31 -21
  67. package/esm/types/schema.d.ts +2 -0
  68. package/package.json +8 -6
  69. package/types/schema.d.ts +2 -0
  70. package/__tests__/codegen/input-types-generator.test.d.ts +0 -1
  71. package/__tests__/codegen/input-types-generator.test.js +0 -635
  72. package/__tests__/codegen/react-query-optional.test.d.ts +0 -1
  73. package/__tests__/codegen/react-query-optional.test.js +0 -292
  74. package/cli/codegen/filters.d.ts +0 -27
  75. package/cli/codegen/filters.js +0 -357
  76. package/cli/codegen/orm/input-types-generator.test.d.ts +0 -1
  77. package/cli/codegen/orm/input-types-generator.test.js +0 -75
  78. package/cli/codegen/orm/select-types.test.d.ts +0 -11
  79. package/cli/codegen/orm/select-types.test.js +0 -22
  80. package/cli/introspect/transform-schema.test.d.ts +0 -1
  81. package/cli/introspect/transform-schema.test.js +0 -67
  82. package/esm/__tests__/codegen/input-types-generator.test.d.ts +0 -1
  83. package/esm/__tests__/codegen/input-types-generator.test.js +0 -633
  84. package/esm/__tests__/codegen/react-query-optional.test.d.ts +0 -1
  85. package/esm/__tests__/codegen/react-query-optional.test.js +0 -290
  86. package/esm/cli/codegen/filters.d.ts +0 -27
  87. package/esm/cli/codegen/filters.js +0 -351
  88. package/esm/cli/codegen/orm/input-types-generator.test.d.ts +0 -1
  89. package/esm/cli/codegen/orm/input-types-generator.test.js +0 -73
  90. package/esm/cli/codegen/orm/select-types.test.d.ts +0 -11
  91. package/esm/cli/codegen/orm/select-types.test.js +0 -21
  92. package/esm/cli/introspect/transform-schema.test.d.ts +0 -1
  93. package/esm/cli/introspect/transform-schema.test.js +0 -65
@@ -1,6 +1,6 @@
1
1
  import { createProject, createSourceFile, getFormattedOutput, createFileHeader, createImport, createInterface, createConst, createTypeAlias, createUnionType, createFilterInterface, } from './ts-ast';
2
2
  import { buildListQueryAST, buildSingleQueryAST, printGraphQL, } from './gql-ast';
3
- import { getTableNames, getListQueryHookName, getSingleQueryHookName, getListQueryFileName, getSingleQueryFileName, getAllRowsQueryName, getSingleRowQueryName, getFilterTypeName, getOrderByTypeName, getScalarFields, getScalarFilterType, toScreamingSnake, ucFirst, } from './utils';
3
+ import { getTableNames, getListQueryHookName, getSingleQueryHookName, getListQueryFileName, getSingleQueryFileName, getAllRowsQueryName, getSingleRowQueryName, getFilterTypeName, getOrderByTypeName, getScalarFields, getScalarFilterType, getPrimaryKeyInfo, toScreamingSnake, ucFirst, } from './utils';
4
4
  // ============================================================================
5
5
  // List query hook generator
6
6
  // ============================================================================
@@ -28,7 +28,7 @@ export function generateListQueryHook(table, options = {}) {
28
28
  // Collect all filter types used by this table's fields
29
29
  const filterTypesUsed = new Set();
30
30
  for (const field of scalarFields) {
31
- const filterType = getScalarFilterType(field.type.gqlType);
31
+ const filterType = getScalarFilterType(field.type.gqlType, field.type.isArray);
32
32
  if (filterType) {
33
33
  filterTypesUsed.add(filterType);
34
34
  }
@@ -66,12 +66,14 @@ export function generateListQueryHook(table, options = {}) {
66
66
  // Generate filter interface
67
67
  const fieldFilters = scalarFields
68
68
  .map((field) => {
69
- const filterType = getScalarFilterType(field.type.gqlType);
69
+ const filterType = getScalarFilterType(field.type.gqlType, field.type.isArray);
70
70
  return filterType ? { fieldName: field.name, filterType } : null;
71
71
  })
72
72
  .filter((f) => f !== null);
73
- sourceFile.addInterface(createFilterInterface(filterTypeName, fieldFilters));
73
+ // Note: Not exported to avoid conflicts with schema-types
74
+ sourceFile.addInterface(createFilterInterface(filterTypeName, fieldFilters, { isExported: false }));
74
75
  // Generate OrderBy type
76
+ // Note: Not exported to avoid conflicts with schema-types
75
77
  const orderByValues = [
76
78
  ...scalarFields.flatMap((f) => [
77
79
  `${toScreamingSnake(f.name)}_ASC`,
@@ -81,7 +83,7 @@ export function generateListQueryHook(table, options = {}) {
81
83
  'PRIMARY_KEY_ASC',
82
84
  'PRIMARY_KEY_DESC',
83
85
  ];
84
- sourceFile.addTypeAlias(createTypeAlias(orderByTypeName, createUnionType(orderByValues)));
86
+ sourceFile.addTypeAlias(createTypeAlias(orderByTypeName, createUnionType(orderByValues), { isExported: false }));
85
87
  // Variables interface
86
88
  const variablesProps = [
87
89
  { name: 'first', type: 'number', optional: true },
@@ -264,6 +266,13 @@ export function generateSingleQueryHook(table, options = {}) {
264
266
  const { typeName, singularName } = getTableNames(table);
265
267
  const hookName = getSingleQueryHookName(table);
266
268
  const queryName = getSingleRowQueryName(table);
269
+ // Get primary key info dynamically from table constraints
270
+ const pkFields = getPrimaryKeyInfo(table);
271
+ // For simplicity, use first PK field (most common case)
272
+ // Composite PKs would need more complex handling
273
+ const pkField = pkFields[0];
274
+ const pkName = pkField.name;
275
+ const pkTsType = pkField.tsType;
267
276
  // Generate GraphQL document via AST
268
277
  const queryAST = buildSingleQueryAST({ table });
269
278
  const queryDocument = printGraphQL(queryAST);
@@ -303,9 +312,9 @@ export function generateSingleQueryHook(table, options = {}) {
303
312
  sourceFile.addStatements('\n// ============================================================================');
304
313
  sourceFile.addStatements('// Types');
305
314
  sourceFile.addStatements('// ============================================================================\n');
306
- // Variables interface
315
+ // Variables interface - use dynamic PK field name and type
307
316
  sourceFile.addInterface(createInterface(`${ucFirst(singularName)}QueryVariables`, [
308
- { name: 'id', type: 'string' },
317
+ { name: pkName, type: pkTsType },
309
318
  ]));
310
319
  // Result interface
311
320
  sourceFile.addInterface(createInterface(`${ucFirst(singularName)}QueryResult`, [
@@ -315,20 +324,20 @@ export function generateSingleQueryHook(table, options = {}) {
315
324
  sourceFile.addStatements('\n// ============================================================================');
316
325
  sourceFile.addStatements('// Query Key');
317
326
  sourceFile.addStatements('// ============================================================================\n');
318
- // Query key factory
319
- sourceFile.addVariableStatement(createConst(`${queryName}QueryKey`, `(id: string) =>
320
- ['${typeName.toLowerCase()}', 'detail', id] as const`));
327
+ // Query key factory - use dynamic PK field name and type
328
+ sourceFile.addVariableStatement(createConst(`${queryName}QueryKey`, `(${pkName}: ${pkTsType}) =>
329
+ ['${typeName.toLowerCase()}', 'detail', ${pkName}] as const`));
321
330
  // Add React Query hook section (only if enabled)
322
331
  if (reactQueryEnabled) {
323
332
  sourceFile.addStatements('\n// ============================================================================');
324
333
  sourceFile.addStatements('// Hook');
325
334
  sourceFile.addStatements('// ============================================================================\n');
326
- // Hook function
335
+ // Hook function - use dynamic PK field name and type
327
336
  sourceFile.addFunction({
328
337
  name: hookName,
329
338
  isExported: true,
330
339
  parameters: [
331
- { name: 'id', type: 'string' },
340
+ { name: pkName, type: pkTsType },
332
341
  {
333
342
  name: 'options',
334
343
  type: `Omit<UseQueryOptions<${ucFirst(singularName)}QueryResult, Error>, 'queryKey' | 'queryFn'>`,
@@ -336,24 +345,24 @@ export function generateSingleQueryHook(table, options = {}) {
336
345
  },
337
346
  ],
338
347
  statements: `return useQuery({
339
- queryKey: ${queryName}QueryKey(id),
348
+ queryKey: ${queryName}QueryKey(${pkName}),
340
349
  queryFn: () => execute<${ucFirst(singularName)}QueryResult, ${ucFirst(singularName)}QueryVariables>(
341
350
  ${queryName}QueryDocument,
342
- { id }
351
+ { ${pkName} }
343
352
  ),
344
- enabled: !!id && (options?.enabled !== false),
353
+ enabled: !!${pkName} && (options?.enabled !== false),
345
354
  ...options,
346
355
  });`,
347
356
  docs: [
348
357
  {
349
- description: `Query hook for fetching a single ${typeName} by ID
358
+ description: `Query hook for fetching a single ${typeName} by primary key
350
359
 
351
360
  @example
352
361
  \`\`\`tsx
353
- const { data, isLoading } = ${hookName}('uuid-here');
362
+ const { data, isLoading } = ${hookName}(${pkTsType === 'string' ? "'value-here'" : '123'});
354
363
 
355
364
  if (data?.${queryName}) {
356
- console.log(data.${queryName}.id);
365
+ console.log(data.${queryName}.${pkName});
357
366
  }
358
367
  \`\`\``,
359
368
  },
@@ -364,13 +373,13 @@ if (data?.${queryName}) {
364
373
  sourceFile.addStatements('\n// ============================================================================');
365
374
  sourceFile.addStatements('// Standalone Functions (non-React)');
366
375
  sourceFile.addStatements('// ============================================================================\n');
367
- // Fetch function (standalone, no React)
376
+ // Fetch function (standalone, no React) - use dynamic PK
368
377
  sourceFile.addFunction({
369
378
  name: `fetch${ucFirst(singularName)}Query`,
370
379
  isExported: true,
371
380
  isAsync: true,
372
381
  parameters: [
373
- { name: 'id', type: 'string' },
382
+ { name: pkName, type: pkTsType },
374
383
  {
375
384
  name: 'options',
376
385
  type: 'ExecuteOptions',
@@ -380,21 +389,21 @@ if (data?.${queryName}) {
380
389
  returnType: `Promise<${ucFirst(singularName)}QueryResult>`,
381
390
  statements: `return execute<${ucFirst(singularName)}QueryResult, ${ucFirst(singularName)}QueryVariables>(
382
391
  ${queryName}QueryDocument,
383
- { id },
392
+ { ${pkName} },
384
393
  options
385
394
  );`,
386
395
  docs: [
387
396
  {
388
- description: `Fetch a single ${typeName} by ID without React hooks
397
+ description: `Fetch a single ${typeName} by primary key without React hooks
389
398
 
390
399
  @example
391
400
  \`\`\`ts
392
- const data = await fetch${ucFirst(singularName)}Query('uuid-here');
401
+ const data = await fetch${ucFirst(singularName)}Query(${pkTsType === 'string' ? "'value-here'" : '123'});
393
402
  \`\`\``,
394
403
  },
395
404
  ],
396
405
  });
397
- // Prefetch function (for SSR/QueryClient) - only if React Query is enabled
406
+ // Prefetch function (for SSR/QueryClient) - only if React Query is enabled, use dynamic PK
398
407
  if (reactQueryEnabled) {
399
408
  sourceFile.addFunction({
400
409
  name: `prefetch${ucFirst(singularName)}Query`,
@@ -402,7 +411,7 @@ const data = await fetch${ucFirst(singularName)}Query('uuid-here');
402
411
  isAsync: true,
403
412
  parameters: [
404
413
  { name: 'queryClient', type: 'QueryClient' },
405
- { name: 'id', type: 'string' },
414
+ { name: pkName, type: pkTsType },
406
415
  {
407
416
  name: 'options',
408
417
  type: 'ExecuteOptions',
@@ -411,10 +420,10 @@ const data = await fetch${ucFirst(singularName)}Query('uuid-here');
411
420
  ],
412
421
  returnType: 'Promise<void>',
413
422
  statements: `await queryClient.prefetchQuery({
414
- queryKey: ${queryName}QueryKey(id),
423
+ queryKey: ${queryName}QueryKey(${pkName}),
415
424
  queryFn: () => execute<${ucFirst(singularName)}QueryResult, ${ucFirst(singularName)}QueryVariables>(
416
425
  ${queryName}QueryDocument,
417
- { id },
426
+ { ${pkName} },
418
427
  options
419
428
  ),
420
429
  });`,
@@ -424,7 +433,7 @@ const data = await fetch${ucFirst(singularName)}Query('uuid-here');
424
433
 
425
434
  @example
426
435
  \`\`\`ts
427
- await prefetch${ucFirst(singularName)}Query(queryClient, 'uuid-here');
436
+ await prefetch${ucFirst(singularName)}Query(queryClient, ${pkTsType === 'string' ? "'value-here'" : '123'});
428
437
  \`\`\``,
429
438
  },
430
439
  ],
@@ -4,9 +4,11 @@
4
4
  export declare const SCALAR_TS_MAP: Record<string, string>;
5
5
  export declare const SCALAR_FILTER_MAP: Record<string, string>;
6
6
  export declare const SCALAR_NAMES: Set<string>;
7
- export interface ScalarToTsOptions {
7
+ /** All base filter type names - skip these in schema-types.ts to avoid duplicates */
8
+ export declare const BASE_FILTER_TYPE_NAMES: Set<string>;
9
+ export declare function scalarToTsType(scalarName: string, options?: {
8
10
  unknownScalar?: 'unknown' | 'name';
9
11
  overrides?: Record<string, string>;
10
- }
11
- export declare function scalarToTsType(scalarName: string, options?: ScalarToTsOptions): string;
12
- export declare function scalarToFilterType(scalarName: string): string | null;
12
+ }): string;
13
+ /** Get the filter type for a scalar (handles both scalar and array types) */
14
+ export declare function scalarToFilterType(scalarName: string, isArray?: boolean): string | null;
@@ -32,6 +32,8 @@ export const SCALAR_TS_MAP = {
32
32
  MacAddr: 'string',
33
33
  TsVector: 'string',
34
34
  TsQuery: 'string',
35
+ // File upload
36
+ Upload: 'File',
35
37
  };
36
38
  export const SCALAR_FILTER_MAP = {
37
39
  String: 'StringFilter',
@@ -52,15 +54,21 @@ export const SCALAR_FILTER_MAP = {
52
54
  Interval: 'StringFilter',
53
55
  };
54
56
  export const SCALAR_NAMES = new Set(Object.keys(SCALAR_TS_MAP));
57
+ /** Scalars that have list filter variants (e.g., StringListFilter) */
58
+ const LIST_FILTER_SCALARS = new Set(['String', 'Int', 'UUID']);
59
+ /** All base filter type names - skip these in schema-types.ts to avoid duplicates */
60
+ export const BASE_FILTER_TYPE_NAMES = new Set([
61
+ ...new Set(Object.values(SCALAR_FILTER_MAP)),
62
+ ...Array.from(LIST_FILTER_SCALARS).map((s) => `${s}ListFilter`),
63
+ ]);
55
64
  export function scalarToTsType(scalarName, options = {}) {
56
- const override = options.overrides?.[scalarName];
57
- if (override)
58
- return override;
59
- const mapped = SCALAR_TS_MAP[scalarName];
60
- if (mapped)
61
- return mapped;
62
- return options.unknownScalar === 'unknown' ? 'unknown' : scalarName;
65
+ return options.overrides?.[scalarName] ?? SCALAR_TS_MAP[scalarName] ?? (options.unknownScalar === 'unknown' ? 'unknown' : scalarName);
63
66
  }
64
- export function scalarToFilterType(scalarName) {
67
+ /** Get the filter type for a scalar (handles both scalar and array types) */
68
+ export function scalarToFilterType(scalarName, isArray = false) {
69
+ const baseName = scalarName === 'ID' ? 'UUID' : scalarName;
70
+ if (isArray) {
71
+ return LIST_FILTER_SCALARS.has(baseName) ? `${baseName}ListFilter` : null;
72
+ }
65
73
  return SCALAR_FILTER_MAP[scalarName] ?? null;
66
74
  }
@@ -0,0 +1,26 @@
1
+ import type { TypeRegistry } from '../../types/schema';
2
+ export interface GeneratedSchemaTypesFile {
3
+ fileName: string;
4
+ content: string;
5
+ /** List of enum type names that were generated */
6
+ generatedEnums: string[];
7
+ /** List of table entity types that are referenced */
8
+ referencedTableTypes: string[];
9
+ }
10
+ export interface GenerateSchemaTypesOptions {
11
+ /** The TypeRegistry containing all GraphQL types */
12
+ typeRegistry: TypeRegistry;
13
+ /** Type names that already exist in types.ts (table entity types) */
14
+ tableTypeNames: Set<string>;
15
+ }
16
+ export interface PayloadTypesResult {
17
+ generatedTypes: Set<string>;
18
+ referencedTableTypes: Set<string>;
19
+ }
20
+ /**
21
+ * Generate comprehensive schema-types.ts file using ts-morph AST
22
+ *
23
+ * This generates all Input/Payload/Enum types from the TypeRegistry
24
+ * that are needed by custom mutation/query hooks.
25
+ */
26
+ export declare function generateSchemaTypesFile(options: GenerateSchemaTypesOptions): GeneratedSchemaTypesFile;
@@ -0,0 +1,362 @@
1
+ import { createProject, createSourceFile, getMinimalFormattedOutput, createFileHeader, createInterface, createTypeAlias, addSectionComment, } from './ts-ast';
2
+ import { getTypeBaseName } from './type-resolver';
3
+ import { scalarToTsType, SCALAR_NAMES, BASE_FILTER_TYPE_NAMES } from './scalars';
4
+ // ============================================================================
5
+ // Constants
6
+ // ============================================================================
7
+ /**
8
+ * Types that should not be generated (scalars, built-ins, types generated elsewhere)
9
+ */
10
+ const SKIP_TYPES = new Set([
11
+ ...SCALAR_NAMES,
12
+ // GraphQL built-ins
13
+ 'Query',
14
+ 'Mutation',
15
+ 'Subscription',
16
+ '__Schema',
17
+ '__Type',
18
+ '__Field',
19
+ '__InputValue',
20
+ '__EnumValue',
21
+ '__Directive',
22
+ // Note: PageInfo and Cursor are NOT skipped - they're needed by Connection types
23
+ // Base filter types (generated in types.ts via filters.ts)
24
+ ...BASE_FILTER_TYPE_NAMES,
25
+ ]);
26
+ /**
27
+ * Type name patterns to skip (regex patterns)
28
+ *
29
+ * Note: We intentionally DO NOT skip Connection, Edge, Filter, or Patch types
30
+ * because they may be referenced by custom operations or payload types.
31
+ * Only skip Condition and OrderBy which are typically not needed.
32
+ */
33
+ const SKIP_TYPE_PATTERNS = [
34
+ /Condition$/, // e.g., UserCondition (filter conditions are separate)
35
+ /OrderBy$/, // e.g., UsersOrderBy (ordering is handled separately)
36
+ ];
37
+ // ============================================================================
38
+ // Type Conversion Utilities
39
+ // ============================================================================
40
+ /**
41
+ * Convert a CleanTypeRef to TypeScript type string
42
+ */
43
+ function typeRefToTs(typeRef) {
44
+ if (typeRef.kind === 'NON_NULL') {
45
+ if (typeRef.ofType) {
46
+ return typeRefToTs(typeRef.ofType);
47
+ }
48
+ return typeRef.name ?? 'unknown';
49
+ }
50
+ if (typeRef.kind === 'LIST') {
51
+ if (typeRef.ofType) {
52
+ return `${typeRefToTs(typeRef.ofType)}[]`;
53
+ }
54
+ return 'unknown[]';
55
+ }
56
+ // Scalar or named type
57
+ const name = typeRef.name ?? 'unknown';
58
+ return scalarToTsType(name, { unknownScalar: 'name' });
59
+ }
60
+ /**
61
+ * Check if a type is required (NON_NULL)
62
+ */
63
+ function isRequired(typeRef) {
64
+ return typeRef.kind === 'NON_NULL';
65
+ }
66
+ /**
67
+ * Check if a type should be skipped
68
+ */
69
+ function shouldSkipType(typeName, tableTypeNames) {
70
+ // Skip scalars and built-ins
71
+ if (SKIP_TYPES.has(typeName))
72
+ return true;
73
+ // Skip table entity types (already in types.ts)
74
+ if (tableTypeNames.has(typeName))
75
+ return true;
76
+ // Skip types matching patterns
77
+ for (const pattern of SKIP_TYPE_PATTERNS) {
78
+ if (pattern.test(typeName))
79
+ return true;
80
+ }
81
+ // Skip table-specific types that would conflict with inline types in table-based hooks.
82
+ // Note: Patch and CreateInput are now NOT exported from hooks (isExported: false),
83
+ // so we only skip Filter types here.
84
+ // The Filter and OrderBy types are generated inline (non-exported) by table query hooks,
85
+ // but schema-types should still generate them for custom operations that need them.
86
+ // Actually, we don't skip any table-based types now since hooks don't export them.
87
+ return false;
88
+ }
89
+ // ============================================================================
90
+ // ENUM Types Generator
91
+ // ============================================================================
92
+ /**
93
+ * Add ENUM types to source file
94
+ */
95
+ function addEnumTypes(sourceFile, typeRegistry, tableTypeNames) {
96
+ const generatedTypes = new Set();
97
+ addSectionComment(sourceFile, 'Enum Types');
98
+ for (const [typeName, typeInfo] of typeRegistry) {
99
+ if (typeInfo.kind !== 'ENUM')
100
+ continue;
101
+ if (shouldSkipType(typeName, tableTypeNames))
102
+ continue;
103
+ if (!typeInfo.enumValues || typeInfo.enumValues.length === 0)
104
+ continue;
105
+ const values = typeInfo.enumValues.map((v) => `'${v}'`).join(' | ');
106
+ sourceFile.addTypeAlias(createTypeAlias(typeName, values));
107
+ generatedTypes.add(typeName);
108
+ }
109
+ return generatedTypes;
110
+ }
111
+ // ============================================================================
112
+ // INPUT_OBJECT Types Generator
113
+ // ============================================================================
114
+ /**
115
+ * Add INPUT_OBJECT types to source file
116
+ * Uses iteration to handle nested input types
117
+ */
118
+ function addInputObjectTypes(sourceFile, typeRegistry, tableTypeNames, alreadyGenerated) {
119
+ const generatedTypes = new Set(alreadyGenerated);
120
+ const typesToGenerate = new Set();
121
+ // Collect all INPUT_OBJECT types
122
+ for (const [typeName, typeInfo] of typeRegistry) {
123
+ if (typeInfo.kind !== 'INPUT_OBJECT')
124
+ continue;
125
+ if (shouldSkipType(typeName, tableTypeNames))
126
+ continue;
127
+ if (generatedTypes.has(typeName))
128
+ continue;
129
+ typesToGenerate.add(typeName);
130
+ }
131
+ if (typesToGenerate.size === 0)
132
+ return generatedTypes;
133
+ addSectionComment(sourceFile, 'Input Object Types');
134
+ // Process all types - no artificial limit
135
+ while (typesToGenerate.size > 0) {
136
+ const typeNameResult = typesToGenerate.values().next();
137
+ if (typeNameResult.done)
138
+ break;
139
+ const typeName = typeNameResult.value;
140
+ typesToGenerate.delete(typeName);
141
+ if (generatedTypes.has(typeName))
142
+ continue;
143
+ const typeInfo = typeRegistry.get(typeName);
144
+ if (!typeInfo || typeInfo.kind !== 'INPUT_OBJECT')
145
+ continue;
146
+ generatedTypes.add(typeName);
147
+ if (typeInfo.inputFields && typeInfo.inputFields.length > 0) {
148
+ const properties = [];
149
+ for (const field of typeInfo.inputFields) {
150
+ const optional = !isRequired(field.type);
151
+ const tsType = typeRefToTs(field.type);
152
+ properties.push({
153
+ name: field.name,
154
+ type: tsType,
155
+ optional,
156
+ docs: field.description ? [field.description] : undefined,
157
+ });
158
+ // Follow nested Input types
159
+ const baseType = getTypeBaseName(field.type);
160
+ if (baseType &&
161
+ !generatedTypes.has(baseType) &&
162
+ !shouldSkipType(baseType, tableTypeNames)) {
163
+ const nestedType = typeRegistry.get(baseType);
164
+ if (nestedType?.kind === 'INPUT_OBJECT') {
165
+ typesToGenerate.add(baseType);
166
+ }
167
+ }
168
+ }
169
+ sourceFile.addInterface(createInterface(typeName, properties));
170
+ }
171
+ else {
172
+ // Empty input object
173
+ sourceFile.addInterface(createInterface(typeName, []));
174
+ }
175
+ }
176
+ return generatedTypes;
177
+ }
178
+ // ============================================================================
179
+ // UNION Types Generator
180
+ // ============================================================================
181
+ /**
182
+ * Add UNION types to source file
183
+ */
184
+ function addUnionTypes(sourceFile, typeRegistry, tableTypeNames, alreadyGenerated) {
185
+ const generatedTypes = new Set(alreadyGenerated);
186
+ const unionTypesToGenerate = new Set();
187
+ // Collect all UNION types
188
+ for (const [typeName, typeInfo] of typeRegistry) {
189
+ if (typeInfo.kind !== 'UNION')
190
+ continue;
191
+ if (shouldSkipType(typeName, tableTypeNames))
192
+ continue;
193
+ if (generatedTypes.has(typeName))
194
+ continue;
195
+ unionTypesToGenerate.add(typeName);
196
+ }
197
+ if (unionTypesToGenerate.size === 0)
198
+ return generatedTypes;
199
+ addSectionComment(sourceFile, 'Union Types');
200
+ for (const typeName of unionTypesToGenerate) {
201
+ const typeInfo = typeRegistry.get(typeName);
202
+ if (!typeInfo || typeInfo.kind !== 'UNION')
203
+ continue;
204
+ if (!typeInfo.possibleTypes || typeInfo.possibleTypes.length === 0)
205
+ continue;
206
+ // Generate union type as TypeScript union
207
+ const unionMembers = typeInfo.possibleTypes.join(' | ');
208
+ sourceFile.addTypeAlias(createTypeAlias(typeName, unionMembers));
209
+ generatedTypes.add(typeName);
210
+ }
211
+ return generatedTypes;
212
+ }
213
+ /**
214
+ * Collect return types from Query and Mutation root types
215
+ * This dynamically discovers what OBJECT types need to be generated
216
+ * based on actual schema structure, not pattern matching
217
+ */
218
+ function collectReturnTypesFromRootTypes(typeRegistry, tableTypeNames) {
219
+ const returnTypes = new Set();
220
+ // Get Query and Mutation root types
221
+ const queryType = typeRegistry.get('Query');
222
+ const mutationType = typeRegistry.get('Mutation');
223
+ const processFields = (fields) => {
224
+ if (!fields)
225
+ return;
226
+ for (const field of fields) {
227
+ const baseType = getTypeBaseName(field.type);
228
+ if (baseType && !shouldSkipType(baseType, tableTypeNames)) {
229
+ const typeInfo = typeRegistry.get(baseType);
230
+ if (typeInfo?.kind === 'OBJECT') {
231
+ returnTypes.add(baseType);
232
+ }
233
+ }
234
+ }
235
+ };
236
+ if (queryType?.fields)
237
+ processFields(queryType.fields);
238
+ if (mutationType?.fields)
239
+ processFields(mutationType.fields);
240
+ return returnTypes;
241
+ }
242
+ /**
243
+ * Add Payload OBJECT types to source file
244
+ * These are return types from mutations (e.g., LoginPayload, BootstrapUserPayload)
245
+ *
246
+ * Also tracks which table entity types are referenced so they can be imported.
247
+ *
248
+ * Uses dynamic type discovery from Query/Mutation return types instead of pattern matching.
249
+ */
250
+ function addPayloadObjectTypes(sourceFile, typeRegistry, tableTypeNames, alreadyGenerated) {
251
+ const generatedTypes = new Set(alreadyGenerated);
252
+ const referencedTableTypes = new Set();
253
+ // Dynamically collect return types from Query and Mutation
254
+ const typesToGenerate = collectReturnTypesFromRootTypes(typeRegistry, tableTypeNames);
255
+ // Filter out already generated types
256
+ for (const typeName of Array.from(typesToGenerate)) {
257
+ if (generatedTypes.has(typeName)) {
258
+ typesToGenerate.delete(typeName);
259
+ }
260
+ }
261
+ if (typesToGenerate.size === 0) {
262
+ return { generatedTypes, referencedTableTypes };
263
+ }
264
+ addSectionComment(sourceFile, 'Payload/Return Object Types');
265
+ // Process all types - no artificial limit
266
+ while (typesToGenerate.size > 0) {
267
+ const typeNameResult = typesToGenerate.values().next();
268
+ if (typeNameResult.done)
269
+ break;
270
+ const typeName = typeNameResult.value;
271
+ typesToGenerate.delete(typeName);
272
+ if (generatedTypes.has(typeName))
273
+ continue;
274
+ const typeInfo = typeRegistry.get(typeName);
275
+ if (!typeInfo || typeInfo.kind !== 'OBJECT')
276
+ continue;
277
+ generatedTypes.add(typeName);
278
+ if (typeInfo.fields && typeInfo.fields.length > 0) {
279
+ const properties = [];
280
+ for (const field of typeInfo.fields) {
281
+ const baseType = getTypeBaseName(field.type);
282
+ // Skip Query and Mutation fields
283
+ if (baseType === 'Query' || baseType === 'Mutation')
284
+ continue;
285
+ const tsType = typeRefToTs(field.type);
286
+ const isNullable = !isRequired(field.type);
287
+ properties.push({
288
+ name: field.name,
289
+ type: isNullable ? `${tsType} | null` : tsType,
290
+ optional: isNullable,
291
+ docs: field.description ? [field.description] : undefined,
292
+ });
293
+ // Track table entity types that are referenced
294
+ if (baseType && tableTypeNames.has(baseType)) {
295
+ referencedTableTypes.add(baseType);
296
+ }
297
+ // Follow nested OBJECT types that aren't table types
298
+ if (baseType &&
299
+ !generatedTypes.has(baseType) &&
300
+ !shouldSkipType(baseType, tableTypeNames)) {
301
+ const nestedType = typeRegistry.get(baseType);
302
+ if (nestedType?.kind === 'OBJECT') {
303
+ typesToGenerate.add(baseType);
304
+ }
305
+ }
306
+ }
307
+ sourceFile.addInterface(createInterface(typeName, properties));
308
+ }
309
+ else {
310
+ // Empty payload object
311
+ sourceFile.addInterface(createInterface(typeName, []));
312
+ }
313
+ }
314
+ return { generatedTypes, referencedTableTypes };
315
+ }
316
+ // ============================================================================
317
+ // Main Generator
318
+ // ============================================================================
319
+ /**
320
+ * Generate comprehensive schema-types.ts file using ts-morph AST
321
+ *
322
+ * This generates all Input/Payload/Enum types from the TypeRegistry
323
+ * that are needed by custom mutation/query hooks.
324
+ */
325
+ export function generateSchemaTypesFile(options) {
326
+ const { typeRegistry, tableTypeNames } = options;
327
+ const project = createProject();
328
+ const sourceFile = createSourceFile(project, 'schema-types.ts');
329
+ // Add file header
330
+ sourceFile.insertText(0, createFileHeader('GraphQL schema types for custom operations') + '\n');
331
+ // Track all generated types
332
+ let generatedTypes = new Set();
333
+ // 1. Generate ENUM types
334
+ const enumTypes = addEnumTypes(sourceFile, typeRegistry, tableTypeNames);
335
+ generatedTypes = new Set([...generatedTypes, ...enumTypes]);
336
+ // 2. Generate UNION types
337
+ const unionTypes = addUnionTypes(sourceFile, typeRegistry, tableTypeNames, generatedTypes);
338
+ generatedTypes = new Set([...generatedTypes, ...unionTypes]);
339
+ // 3. Generate INPUT_OBJECT types
340
+ const inputTypes = addInputObjectTypes(sourceFile, typeRegistry, tableTypeNames, generatedTypes);
341
+ generatedTypes = new Set([...generatedTypes, ...inputTypes]);
342
+ // 4. Generate Payload OBJECT types
343
+ const payloadResult = addPayloadObjectTypes(sourceFile, typeRegistry, tableTypeNames, generatedTypes);
344
+ // 5. Add imports from types.ts (table entity types + base filter types)
345
+ const referencedTableTypes = Array.from(payloadResult.referencedTableTypes).sort();
346
+ // Always import base filter types since generated Filter interfaces reference them
347
+ const baseFilterImports = Array.from(BASE_FILTER_TYPE_NAMES).sort();
348
+ const allTypesImports = [...referencedTableTypes, ...baseFilterImports];
349
+ if (allTypesImports.length > 0) {
350
+ // Insert import after the file header comment
351
+ const importStatement = `import type { ${allTypesImports.join(', ')} } from './types';\n\n`;
352
+ // Find position after header (after first */ + newlines)
353
+ const headerEndIndex = sourceFile.getFullText().indexOf('*/') + 3;
354
+ sourceFile.insertText(headerEndIndex, '\n' + importStatement);
355
+ }
356
+ return {
357
+ fileName: 'schema-types.ts',
358
+ content: getMinimalFormattedOutput(sourceFile),
359
+ generatedEnums: Array.from(enumTypes).sort(),
360
+ referencedTableTypes,
361
+ };
362
+ }
@@ -66,7 +66,9 @@ export declare function createInterface(name: string, properties: InterfacePrope
66
66
  export declare function createFilterInterface(name: string, fieldFilters: Array<{
67
67
  fieldName: string;
68
68
  filterType: string;
69
- }>): InterfaceDeclarationStructure;
69
+ }>, options?: {
70
+ isExported?: boolean;
71
+ }): InterfaceDeclarationStructure;
70
72
  /**
71
73
  * Create type alias declaration structure
72
74
  */
@@ -143,7 +143,7 @@ export function createInterface(name, properties, options) {
143
143
  /**
144
144
  * Create filter interface with standard PostGraphile operators
145
145
  */
146
- export function createFilterInterface(name, fieldFilters) {
146
+ export function createFilterInterface(name, fieldFilters, options) {
147
147
  const properties = [
148
148
  ...fieldFilters.map((f) => ({
149
149
  name: f.fieldName,
@@ -154,7 +154,7 @@ export function createFilterInterface(name, fieldFilters) {
154
154
  { name: 'or', type: `${name}[]`, optional: true, docs: ['Logical OR'] },
155
155
  { name: 'not', type: name, optional: true, docs: ['Logical NOT'] },
156
156
  ];
157
- return createInterface(name, properties);
157
+ return createInterface(name, properties, { isExported: options?.isExported ?? true });
158
158
  }
159
159
  // ============================================================================
160
160
  // Type alias builders