@constructive-io/graphql-codegen 2.20.1 → 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 (96) hide show
  1. package/README.md +15 -3
  2. package/cli/codegen/barrel.d.ts +4 -1
  3. package/cli/codegen/barrel.js +18 -12
  4. package/cli/codegen/client.js +33 -0
  5. package/cli/codegen/custom-mutations.d.ts +11 -1
  6. package/cli/codegen/custom-mutations.js +49 -15
  7. package/cli/codegen/custom-queries.d.ts +8 -0
  8. package/cli/codegen/custom-queries.js +82 -47
  9. package/cli/codegen/gql-ast.js +9 -5
  10. package/cli/codegen/index.js +39 -8
  11. package/cli/codegen/mutations.d.ts +14 -4
  12. package/cli/codegen/mutations.js +114 -28
  13. package/cli/codegen/orm/barrel.js +4 -2
  14. package/cli/codegen/orm/index.js +17 -0
  15. package/cli/codegen/orm/input-types-generator.js +83 -29
  16. package/cli/codegen/orm/model-generator.js +6 -4
  17. package/cli/codegen/queries.d.ts +7 -3
  18. package/cli/codegen/queries.js +185 -158
  19. package/cli/codegen/scalars.d.ts +6 -4
  20. package/cli/codegen/scalars.js +17 -9
  21. package/cli/codegen/schema-types-generator.d.ts +26 -0
  22. package/cli/codegen/schema-types-generator.js +365 -0
  23. package/cli/codegen/ts-ast.d.ts +3 -1
  24. package/cli/codegen/ts-ast.js +2 -2
  25. package/cli/codegen/type-resolver.d.ts +52 -6
  26. package/cli/codegen/type-resolver.js +97 -19
  27. package/cli/codegen/types.d.ts +7 -4
  28. package/cli/codegen/types.js +94 -41
  29. package/cli/codegen/utils.d.ts +20 -2
  30. package/cli/codegen/utils.js +32 -7
  31. package/cli/commands/generate-orm.js +5 -5
  32. package/cli/commands/generate.d.ts +4 -1
  33. package/cli/commands/generate.js +27 -8
  34. package/cli/introspect/transform-schema.d.ts +33 -21
  35. package/cli/introspect/transform-schema.js +31 -21
  36. package/esm/cli/codegen/barrel.d.ts +4 -1
  37. package/esm/cli/codegen/barrel.js +18 -12
  38. package/esm/cli/codegen/client.js +33 -0
  39. package/esm/cli/codegen/custom-mutations.d.ts +11 -1
  40. package/esm/cli/codegen/custom-mutations.js +50 -16
  41. package/esm/cli/codegen/custom-queries.d.ts +8 -0
  42. package/esm/cli/codegen/custom-queries.js +83 -48
  43. package/esm/cli/codegen/gql-ast.js +10 -6
  44. package/esm/cli/codegen/index.js +39 -8
  45. package/esm/cli/codegen/mutations.d.ts +14 -4
  46. package/esm/cli/codegen/mutations.js +115 -29
  47. package/esm/cli/codegen/orm/barrel.js +4 -2
  48. package/esm/cli/codegen/orm/index.js +17 -0
  49. package/esm/cli/codegen/orm/input-types-generator.js +83 -29
  50. package/esm/cli/codegen/orm/model-generator.js +7 -5
  51. package/esm/cli/codegen/queries.d.ts +7 -3
  52. package/esm/cli/codegen/queries.js +186 -159
  53. package/esm/cli/codegen/scalars.d.ts +6 -4
  54. package/esm/cli/codegen/scalars.js +16 -8
  55. package/esm/cli/codegen/schema-types-generator.d.ts +26 -0
  56. package/esm/cli/codegen/schema-types-generator.js +362 -0
  57. package/esm/cli/codegen/ts-ast.d.ts +3 -1
  58. package/esm/cli/codegen/ts-ast.js +2 -2
  59. package/esm/cli/codegen/type-resolver.d.ts +52 -6
  60. package/esm/cli/codegen/type-resolver.js +97 -20
  61. package/esm/cli/codegen/types.d.ts +7 -4
  62. package/esm/cli/codegen/types.js +95 -41
  63. package/esm/cli/codegen/utils.d.ts +20 -2
  64. package/esm/cli/codegen/utils.js +31 -7
  65. package/esm/cli/commands/generate-orm.js +5 -5
  66. package/esm/cli/commands/generate.d.ts +4 -1
  67. package/esm/cli/commands/generate.js +27 -8
  68. package/esm/cli/introspect/transform-schema.d.ts +33 -21
  69. package/esm/cli/introspect/transform-schema.js +31 -21
  70. package/esm/types/config.d.ts +16 -1
  71. package/esm/types/config.js +6 -0
  72. package/esm/types/schema.d.ts +2 -0
  73. package/package.json +8 -6
  74. package/types/config.d.ts +16 -1
  75. package/types/config.js +6 -0
  76. package/types/schema.d.ts +2 -0
  77. package/__tests__/codegen/input-types-generator.test.d.ts +0 -1
  78. package/__tests__/codegen/input-types-generator.test.js +0 -635
  79. package/cli/codegen/filters.d.ts +0 -27
  80. package/cli/codegen/filters.js +0 -357
  81. package/cli/codegen/orm/input-types-generator.test.d.ts +0 -1
  82. package/cli/codegen/orm/input-types-generator.test.js +0 -75
  83. package/cli/codegen/orm/select-types.test.d.ts +0 -11
  84. package/cli/codegen/orm/select-types.test.js +0 -22
  85. package/cli/introspect/transform-schema.test.d.ts +0 -1
  86. package/cli/introspect/transform-schema.test.js +0 -67
  87. package/esm/__tests__/codegen/input-types-generator.test.d.ts +0 -1
  88. package/esm/__tests__/codegen/input-types-generator.test.js +0 -633
  89. package/esm/cli/codegen/filters.d.ts +0 -27
  90. package/esm/cli/codegen/filters.js +0 -351
  91. package/esm/cli/codegen/orm/input-types-generator.test.d.ts +0 -1
  92. package/esm/cli/codegen/orm/input-types-generator.test.js +0 -73
  93. package/esm/cli/codegen/orm/select-types.test.d.ts +0 -11
  94. package/esm/cli/codegen/orm/select-types.test.js +0 -21
  95. package/esm/cli/introspect/transform-schema.test.d.ts +0 -1
  96. package/esm/cli/introspect/transform-schema.test.js +0 -65
@@ -1,15 +1,100 @@
1
- import { createProject, createSourceFile, getFormattedOutput, createFileHeader, createInterface, } from './ts-ast';
1
+ import { createProject, createSourceFile, getFormattedOutput, createFileHeader, createImport, createInterface, } from './ts-ast';
2
2
  import { getScalarFields, fieldTypeToTs } from './utils';
3
- import { generateBaseFilterTypes } from './filters';
3
+ /** All filter type configurations - scalar and list filters */
4
+ const FILTER_CONFIGS = [
5
+ // Scalar filters
6
+ { name: 'StringFilter', tsType: 'string', operators: ['equality', 'distinct', 'inArray', 'comparison', 'string'] },
7
+ { name: 'IntFilter', tsType: 'number', operators: ['equality', 'distinct', 'inArray', 'comparison'] },
8
+ { name: 'FloatFilter', tsType: 'number', operators: ['equality', 'distinct', 'inArray', 'comparison'] },
9
+ { name: 'BooleanFilter', tsType: 'boolean', operators: ['equality'] },
10
+ { name: 'UUIDFilter', tsType: 'string', operators: ['equality', 'distinct', 'inArray'] },
11
+ { name: 'DatetimeFilter', tsType: 'string', operators: ['equality', 'distinct', 'inArray', 'comparison'] },
12
+ { name: 'DateFilter', tsType: 'string', operators: ['equality', 'distinct', 'inArray', 'comparison'] },
13
+ { name: 'JSONFilter', tsType: 'Record<string, unknown>', operators: ['equality', 'distinct', 'json'] },
14
+ { name: 'BigIntFilter', tsType: 'string', operators: ['equality', 'distinct', 'inArray', 'comparison'] },
15
+ { name: 'BigFloatFilter', tsType: 'string', operators: ['equality', 'distinct', 'inArray', 'comparison'] },
16
+ { name: 'BitStringFilter', tsType: 'string', operators: ['equality'] },
17
+ { name: 'InternetAddressFilter', tsType: 'string', operators: ['equality', 'distinct', 'inArray', 'comparison', 'inet'] },
18
+ { name: 'FullTextFilter', tsType: 'string', operators: ['fulltext'] },
19
+ // List filters
20
+ { name: 'StringListFilter', tsType: 'string[]', operators: ['equality', 'distinct', 'comparison', 'listArray'] },
21
+ { name: 'IntListFilter', tsType: 'number[]', operators: ['equality', 'distinct', 'comparison', 'listArray'] },
22
+ { name: 'UUIDListFilter', tsType: 'string[]', operators: ['equality', 'distinct', 'comparison', 'listArray'] },
23
+ ];
24
+ /** Build filter properties based on operator sets */
25
+ function buildFilterProperties(tsType, operators) {
26
+ const props = [];
27
+ // Equality operators (isNull, equalTo, notEqualTo)
28
+ if (operators.includes('equality')) {
29
+ props.push({ name: 'isNull', type: 'boolean', optional: true }, { name: 'equalTo', type: tsType, optional: true }, { name: 'notEqualTo', type: tsType, optional: true });
30
+ }
31
+ // Distinct operators
32
+ if (operators.includes('distinct')) {
33
+ props.push({ name: 'distinctFrom', type: tsType, optional: true }, { name: 'notDistinctFrom', type: tsType, optional: true });
34
+ }
35
+ // In array operators
36
+ if (operators.includes('inArray')) {
37
+ props.push({ name: 'in', type: `${tsType}[]`, optional: true }, { name: 'notIn', type: `${tsType}[]`, optional: true });
38
+ }
39
+ // Comparison operators (lt, lte, gt, gte)
40
+ if (operators.includes('comparison')) {
41
+ props.push({ name: 'lessThan', type: tsType, optional: true }, { name: 'lessThanOrEqualTo', type: tsType, optional: true }, { name: 'greaterThan', type: tsType, optional: true }, { name: 'greaterThanOrEqualTo', type: tsType, optional: true });
42
+ }
43
+ // String-specific operators
44
+ if (operators.includes('string')) {
45
+ props.push({ name: 'includes', type: 'string', optional: true }, { name: 'notIncludes', type: 'string', optional: true }, { name: 'includesInsensitive', type: 'string', optional: true }, { name: 'notIncludesInsensitive', type: 'string', optional: true }, { name: 'startsWith', type: 'string', optional: true }, { name: 'notStartsWith', type: 'string', optional: true }, { name: 'startsWithInsensitive', type: 'string', optional: true }, { name: 'notStartsWithInsensitive', type: 'string', optional: true }, { name: 'endsWith', type: 'string', optional: true }, { name: 'notEndsWith', type: 'string', optional: true }, { name: 'endsWithInsensitive', type: 'string', optional: true }, { name: 'notEndsWithInsensitive', type: 'string', optional: true }, { name: 'like', type: 'string', optional: true }, { name: 'notLike', type: 'string', optional: true }, { name: 'likeInsensitive', type: 'string', optional: true }, { name: 'notLikeInsensitive', type: 'string', optional: true });
46
+ }
47
+ // JSON-specific operators
48
+ if (operators.includes('json')) {
49
+ props.push({ name: 'contains', type: 'unknown', optional: true }, { name: 'containedBy', type: 'unknown', optional: true }, { name: 'containsKey', type: 'string', optional: true }, { name: 'containsAllKeys', type: 'string[]', optional: true }, { name: 'containsAnyKeys', type: 'string[]', optional: true });
50
+ }
51
+ // Internet address operators
52
+ if (operators.includes('inet')) {
53
+ props.push({ name: 'contains', type: 'string', optional: true }, { name: 'containedBy', type: 'string', optional: true }, { name: 'containsOrContainedBy', type: 'string', optional: true });
54
+ }
55
+ // Full-text search operator
56
+ if (operators.includes('fulltext')) {
57
+ props.push({ name: 'matches', type: 'string', optional: true });
58
+ }
59
+ // List/Array operators (for StringListFilter, IntListFilter, etc.)
60
+ if (operators.includes('listArray')) {
61
+ // Extract base type from array type (e.g., 'string[]' -> 'string')
62
+ const baseType = tsType.replace('[]', '');
63
+ props.push({ name: 'contains', type: tsType, optional: true }, { name: 'containedBy', type: tsType, optional: true }, { name: 'overlaps', type: tsType, optional: true }, { name: 'anyEqualTo', type: baseType, optional: true }, { name: 'anyNotEqualTo', type: baseType, optional: true }, { name: 'anyLessThan', type: baseType, optional: true }, { name: 'anyLessThanOrEqualTo', type: baseType, optional: true }, { name: 'anyGreaterThan', type: baseType, optional: true }, { name: 'anyGreaterThanOrEqualTo', type: baseType, optional: true });
64
+ }
65
+ return props;
66
+ }
4
67
  /**
5
68
  * Generate types.ts content with all entity interfaces and base filter types
6
69
  */
7
- export function generateTypesFile(tables) {
70
+ export function generateTypesFile(tables, options = {}) {
71
+ const { enumsFromSchemaTypes = [] } = options;
72
+ const enumSet = new Set(enumsFromSchemaTypes);
8
73
  const project = createProject();
9
74
  const sourceFile = createSourceFile(project, 'types.ts');
10
75
  // Add file header
11
76
  sourceFile.insertText(0, createFileHeader('Entity types and filter types') + '\n\n');
12
- // Add section comment
77
+ // Collect which enums are actually used by entity fields
78
+ const usedEnums = new Set();
79
+ for (const table of tables) {
80
+ const scalarFields = getScalarFields(table);
81
+ for (const field of scalarFields) {
82
+ // Check if the field's gqlType matches any known enum
83
+ const cleanType = field.type.gqlType.replace(/!/g, '');
84
+ if (enumSet.has(cleanType)) {
85
+ usedEnums.add(cleanType);
86
+ }
87
+ }
88
+ }
89
+ // Add import for enum types from schema-types if any are used
90
+ if (usedEnums.size > 0) {
91
+ sourceFile.addImportDeclaration(createImport({
92
+ moduleSpecifier: './schema-types',
93
+ typeOnlyNamedImports: Array.from(usedEnums).sort(),
94
+ }));
95
+ sourceFile.addStatements('');
96
+ }
97
+ // Add section comment for entity types
13
98
  sourceFile.addStatements('// ============================================================================');
14
99
  sourceFile.addStatements('// Entity types');
15
100
  sourceFile.addStatements('// ============================================================================\n');
@@ -22,44 +107,13 @@ export function generateTypesFile(tables) {
22
107
  }));
23
108
  sourceFile.addInterface(createInterface(table.name, properties));
24
109
  }
25
- // Add section comment for filters
110
+ // Add section comment for filter types
26
111
  sourceFile.addStatements('\n// ============================================================================');
27
- sourceFile.addStatements('// Filter types (shared)');
112
+ sourceFile.addStatements('// Filter types (shared PostGraphile filter interfaces)');
28
113
  sourceFile.addStatements('// ============================================================================\n');
29
- // Add base filter types (using string concat for complex types - acceptable for static definitions)
30
- const filterTypesContent = generateBaseFilterTypes();
31
- // Extract just the interfaces part (skip the header)
32
- const filterInterfaces = filterTypesContent
33
- .split('\n')
34
- .slice(6) // Skip header lines
35
- .join('\n');
36
- sourceFile.addStatements(filterInterfaces);
37
- return getFormattedOutput(sourceFile);
38
- }
39
- /**
40
- * Generate a minimal entity type (just id and display fields)
41
- */
42
- export function generateMinimalEntityType(table) {
43
- const project = createProject();
44
- const sourceFile = createSourceFile(project, 'minimal.ts');
45
- const scalarFields = getScalarFields(table);
46
- // Find id and likely display fields
47
- const displayFields = scalarFields.filter((f) => {
48
- const name = f.name.toLowerCase();
49
- return (name === 'id' ||
50
- name === 'name' ||
51
- name === 'title' ||
52
- name === 'label' ||
53
- name === 'email' ||
54
- name.endsWith('name') ||
55
- name.endsWith('title'));
56
- });
57
- // If no display fields found, take first 5 scalar fields
58
- const fieldsToUse = displayFields.length > 0 ? displayFields : scalarFields.slice(0, 5);
59
- const properties = fieldsToUse.map((field) => ({
60
- name: field.name,
61
- type: `${fieldTypeToTs(field.type)} | null`,
62
- }));
63
- sourceFile.addInterface(createInterface(`${table.name}Minimal`, properties));
114
+ // Generate all filter types
115
+ for (const { name, tsType, operators } of FILTER_CONFIGS) {
116
+ sourceFile.addInterface(createInterface(name, buildFilterProperties(tsType, operators)));
117
+ }
64
118
  return getFormattedOutput(sourceFile);
65
119
  }
@@ -134,8 +134,10 @@ export declare function gqlTypeToTs(gqlType: string, isArray?: boolean): string;
134
134
  export declare function fieldTypeToTs(fieldType: CleanFieldType): string;
135
135
  /**
136
136
  * Get the PostGraphile filter type for a GraphQL scalar
137
+ * @param gqlType - The GraphQL type string (e.g., "String", "UUID")
138
+ * @param isArray - Whether this is an array type
137
139
  */
138
- export declare function getScalarFilterType(gqlType: string): string | null;
140
+ export declare function getScalarFilterType(gqlType: string, isArray?: boolean): string | null;
139
141
  /**
140
142
  * Check if a field is a relation field (not a scalar)
141
143
  */
@@ -145,7 +147,23 @@ export declare function isRelationField(fieldName: string, table: CleanTable): b
145
147
  */
146
148
  export declare function getScalarFields(table: CleanTable): CleanField[];
147
149
  /**
148
- * Get primary key field names
150
+ * Primary key field information
151
+ */
152
+ export interface PrimaryKeyField {
153
+ /** Field name */
154
+ name: string;
155
+ /** GraphQL type (e.g., "UUID", "Int", "String") */
156
+ gqlType: string;
157
+ /** TypeScript type (e.g., "string", "number") */
158
+ tsType: string;
159
+ }
160
+ /**
161
+ * Get primary key field information from table constraints
162
+ * Returns array to support composite primary keys
163
+ */
164
+ export declare function getPrimaryKeyInfo(table: CleanTable): PrimaryKeyField[];
165
+ /**
166
+ * Get primary key field names (convenience wrapper)
149
167
  */
150
168
  export declare function getPrimaryKeyFields(table: CleanTable): string[];
151
169
  /**
@@ -220,10 +220,12 @@ export function fieldTypeToTs(fieldType) {
220
220
  // ============================================================================
221
221
  /**
222
222
  * Get the PostGraphile filter type for a GraphQL scalar
223
+ * @param gqlType - The GraphQL type string (e.g., "String", "UUID")
224
+ * @param isArray - Whether this is an array type
223
225
  */
224
- export function getScalarFilterType(gqlType) {
226
+ export function getScalarFilterType(gqlType, isArray = false) {
225
227
  const cleanType = gqlType.replace(/!/g, '');
226
- return scalarToFilterType(cleanType);
228
+ return scalarToFilterType(cleanType, isArray);
227
229
  }
228
230
  // ============================================================================
229
231
  // Field filtering utilities
@@ -245,13 +247,35 @@ export function getScalarFields(table) {
245
247
  return table.fields.filter((f) => !isRelationField(f.name, table));
246
248
  }
247
249
  /**
248
- * Get primary key field names
250
+ * Get primary key field information from table constraints
251
+ * Returns array to support composite primary keys
249
252
  */
250
- export function getPrimaryKeyFields(table) {
253
+ export function getPrimaryKeyInfo(table) {
251
254
  const pk = table.constraints?.primaryKey?.[0];
252
- if (!pk)
253
- return ['id']; // Default assumption
254
- return pk.fields.map((f) => f.name);
255
+ if (!pk || pk.fields.length === 0) {
256
+ // Fallback: try to find 'id' field in table fields
257
+ const idField = table.fields.find(f => f.name.toLowerCase() === 'id');
258
+ if (idField) {
259
+ return [{
260
+ name: idField.name,
261
+ gqlType: idField.type.gqlType,
262
+ tsType: fieldTypeToTs(idField.type),
263
+ }];
264
+ }
265
+ // Last resort: assume 'id' of type string (UUID)
266
+ return [{ name: 'id', gqlType: 'UUID', tsType: 'string' }];
267
+ }
268
+ return pk.fields.map((f) => ({
269
+ name: f.name,
270
+ gqlType: f.type.gqlType,
271
+ tsType: fieldTypeToTs(f.type),
272
+ }));
273
+ }
274
+ /**
275
+ * Get primary key field names (convenience wrapper)
276
+ */
277
+ export function getPrimaryKeyFields(table) {
278
+ return getPrimaryKeyInfo(table).map((pk) => pk.name);
255
279
  }
256
280
  // ============================================================================
257
281
  // Query key generation
@@ -111,16 +111,16 @@ export async function generateOrmCommand(options = {}) {
111
111
  }
112
112
  }
113
113
  // 7. Generate ORM code
114
- log('Generating ORM client...');
114
+ console.log('Generating code...');
115
115
  const { files: generatedFiles, stats } = generateOrm({
116
116
  tables,
117
117
  customOperations: customOperationsData,
118
118
  config,
119
119
  });
120
- log(` Generated ${stats.tables} table models`);
121
- log(` Generated ${stats.customQueries} custom query operations`);
122
- log(` Generated ${stats.customMutations} custom mutation operations`);
123
- log(` Total files: ${stats.totalFiles}`);
120
+ console.log(`Generated ${stats.totalFiles} files`);
121
+ log(` ${stats.tables} table models`);
122
+ log(` ${stats.customQueries} custom query operations`);
123
+ log(` ${stats.customMutations} custom mutation operations`);
124
124
  if (options.dryRun) {
125
125
  return {
126
126
  success: true,
@@ -36,4 +36,7 @@ export interface WriteResult {
36
36
  filesWritten?: string[];
37
37
  errors?: string[];
38
38
  }
39
- export declare function writeGeneratedFiles(files: GeneratedFile[], outputDir: string, subdirs: string[]): Promise<WriteResult>;
39
+ export interface WriteOptions {
40
+ showProgress?: boolean;
41
+ }
42
+ export declare function writeGeneratedFiles(files: GeneratedFile[], outputDir: string, subdirs: string[], options?: WriteOptions): Promise<WriteResult>;
@@ -112,17 +112,17 @@ export async function generateCommand(options = {}) {
112
112
  }
113
113
  }
114
114
  // 7. Generate code
115
- log('Generating code...');
115
+ console.log('Generating code...');
116
116
  const { files: generatedFiles, stats } = generate({
117
117
  tables,
118
118
  customOperations: customOperationsData,
119
119
  config,
120
120
  });
121
- log(` Generated ${stats.queryHooks} table query hooks`);
122
- log(` Generated ${stats.mutationHooks} table mutation hooks`);
123
- log(` Generated ${stats.customQueryHooks} custom query hooks`);
124
- log(` Generated ${stats.customMutationHooks} custom mutation hooks`);
125
- log(` Total files: ${stats.totalFiles}`);
121
+ console.log(`Generated ${stats.totalFiles} files`);
122
+ log(` ${stats.queryHooks} table query hooks`);
123
+ log(` ${stats.mutationHooks} table mutation hooks`);
124
+ log(` ${stats.customQueryHooks} custom query hooks`);
125
+ log(` ${stats.customMutationHooks} custom mutation hooks`);
126
126
  if (options.dryRun) {
127
127
  return {
128
128
  success: true,
@@ -191,9 +191,12 @@ async function loadConfig(options) {
191
191
  const config = resolveConfig(mergedConfig);
192
192
  return { success: true, config };
193
193
  }
194
- export async function writeGeneratedFiles(files, outputDir, subdirs) {
194
+ export async function writeGeneratedFiles(files, outputDir, subdirs, options = {}) {
195
+ const { showProgress = true } = options;
195
196
  const errors = [];
196
197
  const written = [];
198
+ const total = files.length;
199
+ const isTTY = process.stdout.isTTY;
197
200
  // Ensure output directory exists
198
201
  try {
199
202
  fs.mkdirSync(outputDir, { recursive: true });
@@ -219,8 +222,20 @@ export async function writeGeneratedFiles(files, outputDir, subdirs) {
219
222
  if (errors.length > 0) {
220
223
  return { success: false, errors };
221
224
  }
222
- for (const file of files) {
225
+ for (let i = 0; i < files.length; i++) {
226
+ const file = files[i];
223
227
  const filePath = path.join(outputDir, file.path);
228
+ // Show progress
229
+ if (showProgress) {
230
+ const progress = Math.round(((i + 1) / total) * 100);
231
+ if (isTTY) {
232
+ process.stdout.write(`\rWriting files: ${i + 1}/${total} (${progress}%)`);
233
+ }
234
+ else if (i % 100 === 0 || i === total - 1) {
235
+ // Non-TTY: periodic updates for CI/CD
236
+ console.log(`Writing files: ${i + 1}/${total}`);
237
+ }
238
+ }
224
239
  // Ensure parent directory exists
225
240
  const parentDir = path.dirname(filePath);
226
241
  try {
@@ -240,6 +255,10 @@ export async function writeGeneratedFiles(files, outputDir, subdirs) {
240
255
  errors.push(`Failed to write ${filePath}: ${message}`);
241
256
  }
242
257
  }
258
+ // Clear progress line
259
+ if (showProgress && isTTY) {
260
+ process.stdout.write('\r' + ' '.repeat(40) + '\r');
261
+ }
243
262
  return {
244
263
  success: errors.length === 0,
245
264
  filesWritten: written,
@@ -30,13 +30,29 @@ export declare function transformSchemaToOperations(response: IntrospectionQuery
30
30
  * Uses glob-like patterns (supports * wildcard)
31
31
  */
32
32
  export declare function filterOperations(operations: CleanOperation[], include?: string[], exclude?: string[]): CleanOperation[];
33
+ /**
34
+ * Result type for table operation names lookup
35
+ */
36
+ export interface TableOperationNames {
37
+ queries: Set<string>;
38
+ mutations: Set<string>;
39
+ }
33
40
  /**
34
41
  * Get the set of table-related operation names from tables
35
42
  * Used to identify which operations are already covered by table generators
36
43
  *
37
- * This includes:
38
- * - Basic CRUD: all, one, create, update, delete
39
- * - PostGraphile alternate mutations: updateXByY, deleteXByY (for unique constraints)
44
+ * IMPORTANT: This uses EXACT matches only from _meta.query fields.
45
+ * Any operation not explicitly listed in _meta will flow through as a
46
+ * custom operation, ensuring 100% coverage of the schema.
47
+ *
48
+ * Table operations (generated by table generators):
49
+ * - Queries: all (list), one (single by PK)
50
+ * - Mutations: create, update (by PK), delete (by PK)
51
+ *
52
+ * Custom operations (generated by custom operation generators):
53
+ * - Unique constraint lookups: *ByUsername, *ByEmail, etc.
54
+ * - Unique constraint mutations: update*By*, delete*By*
55
+ * - True custom operations: login, register, bootstrapUser, etc.
40
56
  */
41
57
  export declare function getTableOperationNames(tables: Array<{
42
58
  name: string;
@@ -47,28 +63,24 @@ export declare function getTableOperationNames(tables: Array<{
47
63
  update: string | null;
48
64
  delete: string | null;
49
65
  };
50
- inflection?: {
51
- tableType: string;
52
- };
53
- }>): {
54
- queries: Set<string>;
55
- mutations: Set<string>;
56
- tableTypePatterns: RegExp[];
57
- };
66
+ }>): TableOperationNames;
58
67
  /**
59
68
  * Check if an operation is a table operation (already handled by table generators)
69
+ *
70
+ * Uses EXACT match only - no pattern matching. This ensures:
71
+ * 1. Only operations explicitly in _meta.query are treated as table operations
72
+ * 2. All other operations (including update*By*, delete*By*) become custom operations
73
+ * 3. 100% schema coverage is guaranteed
60
74
  */
61
- export declare function isTableOperation(operation: CleanOperation, tableOperationNames: {
62
- queries: Set<string>;
63
- mutations: Set<string>;
64
- tableTypePatterns: RegExp[];
65
- }): boolean;
75
+ export declare function isTableOperation(operation: CleanOperation, tableOperationNames: TableOperationNames): boolean;
66
76
  /**
67
77
  * Get only custom operations (not covered by table generators)
78
+ *
79
+ * This returns ALL operations that are not exact matches for table CRUD operations.
80
+ * Includes:
81
+ * - Unique constraint queries (*ByUsername, *ByEmail, etc.)
82
+ * - Unique constraint mutations (update*By*, delete*By*)
83
+ * - True custom operations (login, register, bootstrapUser, etc.)
68
84
  */
69
- export declare function getCustomOperations(operations: CleanOperation[], tableOperationNames: {
70
- queries: Set<string>;
71
- mutations: Set<string>;
72
- tableTypePatterns: RegExp[];
73
- }): CleanOperation[];
85
+ export declare function getCustomOperations(operations: CleanOperation[], tableOperationNames: TableOperationNames): CleanOperation[];
74
86
  export { unwrapType, getBaseTypeName, isNonNull };
@@ -26,6 +26,10 @@ export function buildTypeRegistry(types) {
26
26
  if (type.kind === 'ENUM' && type.enumValues) {
27
27
  resolvedType.enumValues = type.enumValues.map((ev) => ev.name);
28
28
  }
29
+ // Resolve possible types for UNION types (no circular refs for names)
30
+ if (type.kind === 'UNION' && type.possibleTypes) {
31
+ resolvedType.possibleTypes = type.possibleTypes.map((pt) => pt.name);
32
+ }
29
33
  registry.set(type.name, resolvedType);
30
34
  }
31
35
  // Second pass: Resolve fields (now that all types exist in registry)
@@ -200,57 +204,63 @@ function matchesPatterns(name, patterns) {
200
204
  return name === pattern;
201
205
  });
202
206
  }
203
- // ============================================================================
204
- // Utility Functions
205
- // ============================================================================
206
207
  /**
207
208
  * Get the set of table-related operation names from tables
208
209
  * Used to identify which operations are already covered by table generators
209
210
  *
210
- * This includes:
211
- * - Basic CRUD: all, one, create, update, delete
212
- * - PostGraphile alternate mutations: updateXByY, deleteXByY (for unique constraints)
211
+ * IMPORTANT: This uses EXACT matches only from _meta.query fields.
212
+ * Any operation not explicitly listed in _meta will flow through as a
213
+ * custom operation, ensuring 100% coverage of the schema.
214
+ *
215
+ * Table operations (generated by table generators):
216
+ * - Queries: all (list), one (single by PK)
217
+ * - Mutations: create, update (by PK), delete (by PK)
218
+ *
219
+ * Custom operations (generated by custom operation generators):
220
+ * - Unique constraint lookups: *ByUsername, *ByEmail, etc.
221
+ * - Unique constraint mutations: update*By*, delete*By*
222
+ * - True custom operations: login, register, bootstrapUser, etc.
213
223
  */
214
224
  export function getTableOperationNames(tables) {
215
225
  const queries = new Set();
216
226
  const mutations = new Set();
217
- const tableTypePatterns = [];
218
227
  for (const table of tables) {
219
228
  if (table.query) {
229
+ // Add exact query names from _meta
220
230
  queries.add(table.query.all);
221
231
  queries.add(table.query.one);
232
+ // Add exact mutation names from _meta
222
233
  mutations.add(table.query.create);
223
234
  if (table.query.update)
224
235
  mutations.add(table.query.update);
225
236
  if (table.query.delete)
226
237
  mutations.add(table.query.delete);
227
238
  }
228
- // Create patterns to match alternate CRUD mutations (updateXByY, deleteXByY)
229
- if (table.inflection?.tableType) {
230
- const typeName = table.inflection.tableType;
231
- // Match: update{TypeName}By*, delete{TypeName}By*
232
- tableTypePatterns.push(new RegExp(`^update${typeName}By`, 'i'));
233
- tableTypePatterns.push(new RegExp(`^delete${typeName}By`, 'i'));
234
- }
235
239
  }
236
- return { queries, mutations, tableTypePatterns };
240
+ return { queries, mutations };
237
241
  }
238
242
  /**
239
243
  * Check if an operation is a table operation (already handled by table generators)
244
+ *
245
+ * Uses EXACT match only - no pattern matching. This ensures:
246
+ * 1. Only operations explicitly in _meta.query are treated as table operations
247
+ * 2. All other operations (including update*By*, delete*By*) become custom operations
248
+ * 3. 100% schema coverage is guaranteed
240
249
  */
241
250
  export function isTableOperation(operation, tableOperationNames) {
242
251
  if (operation.kind === 'query') {
243
252
  return tableOperationNames.queries.has(operation.name);
244
253
  }
245
- // Check exact match first
246
- if (tableOperationNames.mutations.has(operation.name)) {
247
- return true;
248
- }
249
- // Check pattern match for alternate CRUD mutations (updateXByY, deleteXByY)
250
- return tableOperationNames.tableTypePatterns.some((pattern) => pattern.test(operation.name));
254
+ return tableOperationNames.mutations.has(operation.name);
251
255
  }
252
256
  /**
253
257
  * Get only custom operations (not covered by table generators)
258
+ *
259
+ * This returns ALL operations that are not exact matches for table CRUD operations.
260
+ * Includes:
261
+ * - Unique constraint queries (*ByUsername, *ByEmail, etc.)
262
+ * - Unique constraint mutations (update*By*, delete*By*)
263
+ * - True custom operations (login, register, bootstrapUser, etc.)
254
264
  */
255
265
  export function getCustomOperations(operations, tableOperationNames) {
256
266
  return operations.filter((op) => !isTableOperation(op, tableOperationNames));
@@ -95,6 +95,18 @@ export interface GraphQLSDKConfig {
95
95
  */
96
96
  useSharedTypes?: boolean;
97
97
  };
98
+ /**
99
+ * React Query integration options
100
+ * Controls whether React Query hooks are generated
101
+ */
102
+ reactQuery?: {
103
+ /**
104
+ * Whether to generate React Query hooks (useQuery, useMutation)
105
+ * When false, only standalone fetch functions are generated (no React dependency)
106
+ * @default false
107
+ */
108
+ enabled?: boolean;
109
+ };
98
110
  /**
99
111
  * Watch mode configuration (dev-only feature)
100
112
  * When enabled via CLI --watch flag, the CLI will poll the endpoint for schema changes
@@ -142,7 +154,7 @@ export interface ResolvedWatchConfig {
142
154
  /**
143
155
  * Resolved configuration with defaults applied
144
156
  */
145
- export interface ResolvedConfig extends Required<Omit<GraphQLSDKConfig, 'headers' | 'tables' | 'queries' | 'mutations' | 'hooks' | 'postgraphile' | 'codegen' | 'orm' | 'watch'>> {
157
+ export interface ResolvedConfig extends Required<Omit<GraphQLSDKConfig, 'headers' | 'tables' | 'queries' | 'mutations' | 'hooks' | 'postgraphile' | 'codegen' | 'orm' | 'reactQuery' | 'watch'>> {
146
158
  headers: Record<string, string>;
147
159
  tables: {
148
160
  include: string[];
@@ -172,6 +184,9 @@ export interface ResolvedConfig extends Required<Omit<GraphQLSDKConfig, 'headers
172
184
  output: string;
173
185
  useSharedTypes: boolean;
174
186
  } | null;
187
+ reactQuery: {
188
+ enabled: boolean;
189
+ };
175
190
  watch: ResolvedWatchConfig;
176
191
  }
177
192
  /**
@@ -42,6 +42,9 @@ export const DEFAULT_CONFIG = {
42
42
  skipQueryField: true,
43
43
  },
44
44
  orm: null, // ORM generation disabled by default
45
+ reactQuery: {
46
+ enabled: false, // React Query hooks disabled by default
47
+ },
45
48
  watch: DEFAULT_WATCH_CONFIG,
46
49
  };
47
50
  /**
@@ -96,6 +99,9 @@ export function resolveConfig(config) {
96
99
  useSharedTypes: config.orm.useSharedTypes ?? DEFAULT_ORM_CONFIG.useSharedTypes,
97
100
  }
98
101
  : null,
102
+ reactQuery: {
103
+ enabled: config.reactQuery?.enabled ?? DEFAULT_CONFIG.reactQuery.enabled,
104
+ },
99
105
  watch: {
100
106
  pollInterval: config.watch?.pollInterval ?? DEFAULT_WATCH_CONFIG.pollInterval,
101
107
  debounce: config.watch?.debounce ?? DEFAULT_WATCH_CONFIG.debounce,
@@ -250,4 +250,6 @@ export interface ResolvedType {
250
250
  inputFields?: CleanArgument[];
251
251
  /** Values for ENUM types */
252
252
  enumValues?: string[];
253
+ /** Possible types for UNION types */
254
+ possibleTypes?: string[];
253
255
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constructive-io/graphql-codegen",
3
- "version": "2.20.1",
3
+ "version": "2.22.0",
4
4
  "description": "CLI-based GraphQL SDK generator for PostGraphile endpoints with React Query hooks",
5
5
  "keywords": [
6
6
  "graphql",
@@ -41,15 +41,16 @@
41
41
  "lint": "eslint . --fix",
42
42
  "test": "jest --passWithNoTests",
43
43
  "test:watch": "jest --watch",
44
- "example:orm": "tsx examples/test-orm.ts",
45
- "example:rq": "tsx examples/test-rq.ts",
46
- "example:schema": "tsx examples/download-schema.ts"
44
+ "example:codegen:sdk": "node dist/cli/index.js generate --endpoint http://api.localhost:3000/graphql --output examples/output/generated-sdk",
45
+ "example:codegen:orm": "node dist/cli/index.js generate-orm --endpoint http://api.localhost:3000/graphql --output examples/output/generated-orm",
46
+ "example:sdk": "tsx examples/react-query-sdk.ts",
47
+ "example:orm": "tsx examples/orm-sdk.ts"
47
48
  },
48
49
  "dependencies": {
49
50
  "ajv": "^8.17.1",
50
51
  "commander": "^12.1.0",
51
52
  "gql-ast": "^2.4.6",
52
- "graphql": "15.8.0",
53
+ "graphql": "15.10.1",
53
54
  "inflection": "^3.0.2",
54
55
  "jiti": "^2.6.1",
55
56
  "prettier": "^3.7.4",
@@ -76,7 +77,8 @@
76
77
  "jest": "^29.7.0",
77
78
  "react": "^19.2.3",
78
79
  "ts-jest": "^29.2.5",
80
+ "tsx": "^4.21.0",
79
81
  "typescript": "^5.9.3"
80
82
  },
81
- "gitHead": "fc89c642ea6c3e1cd69e47a4151cec1d802520b8"
83
+ "gitHead": "fe5e59d3a4497d7ac91a051b74e181851393f56c"
82
84
  }