@constructive-io/graphql-codegen 2.21.0 → 2.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +4 -0
- package/cli/codegen/custom-mutations.js +39 -13
- package/cli/codegen/custom-queries.d.ts +4 -0
- package/cli/codegen/custom-queries.js +36 -11
- package/cli/codegen/gql-ast.js +9 -5
- package/cli/codegen/index.js +35 -7
- package/cli/codegen/mutations.d.ts +2 -0
- package/cli/codegen/mutations.js +87 -23
- 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.js +36 -27
- 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 +4 -0
- package/esm/cli/codegen/custom-mutations.js +40 -14
- package/esm/cli/codegen/custom-queries.d.ts +4 -0
- package/esm/cli/codegen/custom-queries.js +37 -12
- package/esm/cli/codegen/gql-ast.js +10 -6
- package/esm/cli/codegen/index.js +35 -7
- package/esm/cli/codegen/mutations.d.ts +2 -0
- package/esm/cli/codegen/mutations.js +88 -24
- 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.js +37 -28
- 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/schema.d.ts +2 -0
- package/package.json +8 -7
- 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/__tests__/codegen/react-query-optional.test.d.ts +0 -1
- package/__tests__/codegen/react-query-optional.test.js +0 -292
- 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/__tests__/codegen/react-query-optional.test.d.ts +0 -1
- package/esm/__tests__/codegen/react-query-optional.test.js +0 -290
- 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
|
@@ -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
|
-
|
|
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:
|
|
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`, `(
|
|
320
|
-
['${typeName.toLowerCase()}', 'detail',
|
|
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:
|
|
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(
|
|
348
|
+
queryKey: ${queryName}QueryKey(${pkName}),
|
|
340
349
|
queryFn: () => execute<${ucFirst(singularName)}QueryResult, ${ucFirst(singularName)}QueryVariables>(
|
|
341
350
|
${queryName}QueryDocument,
|
|
342
|
-
{
|
|
351
|
+
{ ${pkName} }
|
|
343
352
|
),
|
|
344
|
-
enabled:
|
|
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
|
|
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}('
|
|
362
|
+
const { data, isLoading } = ${hookName}(${pkTsType === 'string' ? "'value-here'" : '123'});
|
|
354
363
|
|
|
355
364
|
if (data?.${queryName}) {
|
|
356
|
-
console.log(data.${queryName}
|
|
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:
|
|
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
|
-
{
|
|
392
|
+
{ ${pkName} },
|
|
384
393
|
options
|
|
385
394
|
);`,
|
|
386
395
|
docs: [
|
|
387
396
|
{
|
|
388
|
-
description: `Fetch a single ${typeName} by
|
|
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('
|
|
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:
|
|
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(
|
|
423
|
+
queryKey: ${queryName}QueryKey(${pkName}),
|
|
415
424
|
queryFn: () => execute<${ucFirst(singularName)}QueryResult, ${ucFirst(singularName)}QueryVariables>(
|
|
416
425
|
${queryName}QueryDocument,
|
|
417
|
-
{
|
|
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, '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|