@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.
- package/README.md +15 -3
- package/cli/codegen/barrel.d.ts +4 -1
- package/cli/codegen/barrel.js +18 -12
- package/cli/codegen/client.js +33 -0
- package/cli/codegen/custom-mutations.d.ts +11 -1
- package/cli/codegen/custom-mutations.js +49 -15
- package/cli/codegen/custom-queries.d.ts +8 -0
- package/cli/codegen/custom-queries.js +82 -47
- package/cli/codegen/gql-ast.js +9 -5
- package/cli/codegen/index.js +39 -8
- package/cli/codegen/mutations.d.ts +14 -4
- package/cli/codegen/mutations.js +114 -28
- package/cli/codegen/orm/barrel.js +4 -2
- package/cli/codegen/orm/index.js +17 -0
- package/cli/codegen/orm/input-types-generator.js +83 -29
- package/cli/codegen/orm/model-generator.js +6 -4
- package/cli/codegen/queries.d.ts +7 -3
- package/cli/codegen/queries.js +185 -158
- package/cli/codegen/scalars.d.ts +6 -4
- package/cli/codegen/scalars.js +17 -9
- package/cli/codegen/schema-types-generator.d.ts +26 -0
- package/cli/codegen/schema-types-generator.js +365 -0
- package/cli/codegen/ts-ast.d.ts +3 -1
- package/cli/codegen/ts-ast.js +2 -2
- package/cli/codegen/type-resolver.d.ts +52 -6
- package/cli/codegen/type-resolver.js +97 -19
- package/cli/codegen/types.d.ts +7 -4
- package/cli/codegen/types.js +94 -41
- package/cli/codegen/utils.d.ts +20 -2
- package/cli/codegen/utils.js +32 -7
- package/cli/commands/generate-orm.js +5 -5
- package/cli/commands/generate.d.ts +4 -1
- package/cli/commands/generate.js +27 -8
- package/cli/introspect/transform-schema.d.ts +33 -21
- package/cli/introspect/transform-schema.js +31 -21
- package/esm/cli/codegen/barrel.d.ts +4 -1
- package/esm/cli/codegen/barrel.js +18 -12
- package/esm/cli/codegen/client.js +33 -0
- package/esm/cli/codegen/custom-mutations.d.ts +11 -1
- package/esm/cli/codegen/custom-mutations.js +50 -16
- package/esm/cli/codegen/custom-queries.d.ts +8 -0
- package/esm/cli/codegen/custom-queries.js +83 -48
- package/esm/cli/codegen/gql-ast.js +10 -6
- package/esm/cli/codegen/index.js +39 -8
- package/esm/cli/codegen/mutations.d.ts +14 -4
- package/esm/cli/codegen/mutations.js +115 -29
- package/esm/cli/codegen/orm/barrel.js +4 -2
- package/esm/cli/codegen/orm/index.js +17 -0
- package/esm/cli/codegen/orm/input-types-generator.js +83 -29
- package/esm/cli/codegen/orm/model-generator.js +7 -5
- package/esm/cli/codegen/queries.d.ts +7 -3
- package/esm/cli/codegen/queries.js +186 -159
- package/esm/cli/codegen/scalars.d.ts +6 -4
- package/esm/cli/codegen/scalars.js +16 -8
- package/esm/cli/codegen/schema-types-generator.d.ts +26 -0
- package/esm/cli/codegen/schema-types-generator.js +362 -0
- package/esm/cli/codegen/ts-ast.d.ts +3 -1
- package/esm/cli/codegen/ts-ast.js +2 -2
- package/esm/cli/codegen/type-resolver.d.ts +52 -6
- package/esm/cli/codegen/type-resolver.js +97 -20
- package/esm/cli/codegen/types.d.ts +7 -4
- package/esm/cli/codegen/types.js +95 -41
- package/esm/cli/codegen/utils.d.ts +20 -2
- package/esm/cli/codegen/utils.js +31 -7
- package/esm/cli/commands/generate-orm.js +5 -5
- package/esm/cli/commands/generate.d.ts +4 -1
- package/esm/cli/commands/generate.js +27 -8
- package/esm/cli/introspect/transform-schema.d.ts +33 -21
- package/esm/cli/introspect/transform-schema.js +31 -21
- package/esm/types/config.d.ts +16 -1
- package/esm/types/config.js +6 -0
- package/esm/types/schema.d.ts +2 -0
- package/package.json +8 -6
- package/types/config.d.ts +16 -1
- package/types/config.js +6 -0
- package/types/schema.d.ts +2 -0
- package/__tests__/codegen/input-types-generator.test.d.ts +0 -1
- package/__tests__/codegen/input-types-generator.test.js +0 -635
- package/cli/codegen/filters.d.ts +0 -27
- package/cli/codegen/filters.js +0 -357
- package/cli/codegen/orm/input-types-generator.test.d.ts +0 -1
- package/cli/codegen/orm/input-types-generator.test.js +0 -75
- package/cli/codegen/orm/select-types.test.d.ts +0 -11
- package/cli/codegen/orm/select-types.test.js +0 -22
- package/cli/introspect/transform-schema.test.d.ts +0 -1
- package/cli/introspect/transform-schema.test.js +0 -67
- package/esm/__tests__/codegen/input-types-generator.test.d.ts +0 -1
- package/esm/__tests__/codegen/input-types-generator.test.js +0 -633
- package/esm/cli/codegen/filters.d.ts +0 -27
- package/esm/cli/codegen/filters.js +0 -351
- package/esm/cli/codegen/orm/input-types-generator.test.d.ts +0 -1
- package/esm/cli/codegen/orm/input-types-generator.test.js +0 -73
- package/esm/cli/codegen/orm/select-types.test.d.ts +0 -11
- package/esm/cli/codegen/orm/select-types.test.js +0 -21
- package/esm/cli/introspect/transform-schema.test.d.ts +0 -1
- package/esm/cli/introspect/transform-schema.test.js +0 -65
package/esm/cli/codegen/types.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
*
|
|
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
|
/**
|
package/esm/cli/codegen/utils.js
CHANGED
|
@@ -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
|
|
250
|
+
* Get primary key field information from table constraints
|
|
251
|
+
* Returns array to support composite primary keys
|
|
249
252
|
*/
|
|
250
|
-
export function
|
|
253
|
+
export function getPrimaryKeyInfo(table) {
|
|
251
254
|
const pk = table.constraints?.primaryKey?.[0];
|
|
252
|
-
if (!pk)
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
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(`
|
|
121
|
-
log(`
|
|
122
|
-
log(`
|
|
123
|
-
log(`
|
|
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
|
|
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(`
|
|
122
|
-
log(`
|
|
123
|
-
log(`
|
|
124
|
-
log(`
|
|
125
|
-
log(`
|
|
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 (
|
|
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
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
-
|
|
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
|
|
211
|
-
*
|
|
212
|
-
*
|
|
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
|
|
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
|
-
|
|
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));
|
package/esm/types/config.d.ts
CHANGED
|
@@ -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
|
/**
|
package/esm/types/config.js
CHANGED
|
@@ -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,
|
package/esm/types/schema.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constructive-io/graphql-codegen",
|
|
3
|
-
"version": "2.
|
|
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:
|
|
45
|
-
"example:
|
|
46
|
-
"example:
|
|
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.
|
|
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": "
|
|
83
|
+
"gitHead": "fe5e59d3a4497d7ac91a051b74e181851393f56c"
|
|
82
84
|
}
|