@constructive-io/graphql-codegen 2.23.3 → 2.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +147 -2
  2. package/cli/codegen/babel-ast.d.ts +53 -0
  3. package/cli/codegen/babel-ast.js +160 -0
  4. package/cli/codegen/barrel.d.ts +7 -2
  5. package/cli/codegen/barrel.js +193 -102
  6. package/cli/codegen/client.js +61 -0
  7. package/cli/codegen/custom-mutations.d.ts +2 -12
  8. package/cli/codegen/custom-mutations.js +116 -124
  9. package/cli/codegen/custom-queries.d.ts +2 -10
  10. package/cli/codegen/custom-queries.js +236 -335
  11. package/cli/codegen/gql-ast.js +22 -1
  12. package/cli/codegen/index.d.ts +3 -0
  13. package/cli/codegen/index.js +73 -3
  14. package/cli/codegen/invalidation.d.ts +20 -0
  15. package/cli/codegen/invalidation.js +327 -0
  16. package/cli/codegen/mutation-keys.d.ts +24 -0
  17. package/cli/codegen/mutation-keys.js +247 -0
  18. package/cli/codegen/mutations.d.ts +5 -19
  19. package/cli/codegen/mutations.js +385 -383
  20. package/cli/codegen/orm/barrel.d.ts +1 -1
  21. package/cli/codegen/orm/barrel.js +42 -10
  22. package/cli/codegen/orm/client-generator.d.ts +1 -19
  23. package/cli/codegen/orm/client-generator.js +108 -77
  24. package/cli/codegen/orm/custom-ops-generator.d.ts +1 -12
  25. package/cli/codegen/orm/custom-ops-generator.js +192 -235
  26. package/cli/codegen/orm/input-types-generator.d.ts +13 -1
  27. package/cli/codegen/orm/input-types-generator.js +425 -147
  28. package/cli/codegen/orm/model-generator.d.ts +1 -19
  29. package/cli/codegen/orm/model-generator.js +229 -234
  30. package/cli/codegen/queries.d.ts +4 -12
  31. package/cli/codegen/queries.js +660 -390
  32. package/cli/codegen/query-keys.d.ts +15 -0
  33. package/cli/codegen/query-keys.js +477 -0
  34. package/cli/codegen/scalars.js +1 -0
  35. package/cli/codegen/schema-types-generator.d.ts +15 -10
  36. package/cli/codegen/schema-types-generator.js +87 -175
  37. package/cli/codegen/type-resolver.d.ts +1 -30
  38. package/cli/codegen/type-resolver.js +0 -53
  39. package/cli/codegen/types.d.ts +1 -1
  40. package/cli/codegen/types.js +76 -21
  41. package/cli/codegen/utils.d.ts +6 -0
  42. package/cli/codegen/utils.js +19 -0
  43. package/esm/cli/codegen/babel-ast.d.ts +53 -0
  44. package/esm/cli/codegen/babel-ast.js +111 -0
  45. package/esm/cli/codegen/barrel.d.ts +7 -2
  46. package/esm/cli/codegen/barrel.js +161 -103
  47. package/esm/cli/codegen/client.js +61 -0
  48. package/esm/cli/codegen/custom-mutations.d.ts +2 -12
  49. package/esm/cli/codegen/custom-mutations.js +83 -124
  50. package/esm/cli/codegen/custom-queries.d.ts +2 -10
  51. package/esm/cli/codegen/custom-queries.js +204 -336
  52. package/esm/cli/codegen/gql-ast.js +23 -2
  53. package/esm/cli/codegen/index.d.ts +3 -0
  54. package/esm/cli/codegen/index.js +69 -2
  55. package/esm/cli/codegen/invalidation.d.ts +20 -0
  56. package/esm/cli/codegen/invalidation.js +291 -0
  57. package/esm/cli/codegen/mutation-keys.d.ts +24 -0
  58. package/esm/cli/codegen/mutation-keys.js +211 -0
  59. package/esm/cli/codegen/mutations.d.ts +5 -19
  60. package/esm/cli/codegen/mutations.js +353 -384
  61. package/esm/cli/codegen/orm/barrel.d.ts +1 -1
  62. package/esm/cli/codegen/orm/barrel.js +10 -11
  63. package/esm/cli/codegen/orm/client-generator.d.ts +1 -19
  64. package/esm/cli/codegen/orm/client-generator.js +76 -78
  65. package/esm/cli/codegen/orm/custom-ops-generator.d.ts +1 -12
  66. package/esm/cli/codegen/orm/custom-ops-generator.js +160 -236
  67. package/esm/cli/codegen/orm/input-types-generator.d.ts +13 -1
  68. package/esm/cli/codegen/orm/input-types-generator.js +393 -148
  69. package/esm/cli/codegen/orm/model-generator.d.ts +1 -19
  70. package/esm/cli/codegen/orm/model-generator.js +197 -235
  71. package/esm/cli/codegen/queries.d.ts +4 -12
  72. package/esm/cli/codegen/queries.js +628 -391
  73. package/esm/cli/codegen/query-keys.d.ts +15 -0
  74. package/esm/cli/codegen/query-keys.js +441 -0
  75. package/esm/cli/codegen/scalars.js +1 -0
  76. package/esm/cli/codegen/schema-types-generator.d.ts +15 -10
  77. package/esm/cli/codegen/schema-types-generator.js +54 -175
  78. package/esm/cli/codegen/type-resolver.d.ts +1 -30
  79. package/esm/cli/codegen/type-resolver.js +0 -49
  80. package/esm/cli/codegen/types.d.ts +1 -1
  81. package/esm/cli/codegen/types.js +44 -22
  82. package/esm/cli/codegen/utils.d.ts +6 -0
  83. package/esm/cli/codegen/utils.js +18 -0
  84. package/esm/types/config.d.ts +75 -0
  85. package/esm/types/config.js +18 -0
  86. package/package.json +6 -4
  87. package/types/config.d.ts +75 -0
  88. package/types/config.js +19 -1
  89. package/cli/codegen/ts-ast.d.ts +0 -124
  90. package/cli/codegen/ts-ast.js +0 -280
  91. package/esm/cli/codegen/ts-ast.d.ts +0 -124
  92. package/esm/cli/codegen/ts-ast.js +0 -260
@@ -1,31 +1,47 @@
1
- import { createProject, createSourceFile, getFormattedOutput, createFileHeader, createImport, createInterface, createConst, createTypeAlias, createUnionType, createFilterInterface, } from './ts-ast';
1
+ import * as t from '@babel/types';
2
+ import { generateCode, addJSDocComment, typedParam, createTypedCallExpression } from './babel-ast';
2
3
  import { buildListQueryAST, buildSingleQueryAST, printGraphQL, } from './gql-ast';
3
- import { getTableNames, getListQueryHookName, getSingleQueryHookName, getListQueryFileName, getSingleQueryFileName, getAllRowsQueryName, getSingleRowQueryName, getFilterTypeName, getOrderByTypeName, getScalarFields, getScalarFilterType, getPrimaryKeyInfo, toScreamingSnake, ucFirst, } from './utils';
4
- // ============================================================================
5
- // List query hook generator
6
- // ============================================================================
7
- /**
8
- * Generate list query hook file content using AST
9
- */
4
+ import { getTableNames, getListQueryHookName, getSingleQueryHookName, getListQueryFileName, getSingleQueryFileName, getAllRowsQueryName, getSingleRowQueryName, getFilterTypeName, getConditionTypeName, getOrderByTypeName, getScalarFields, getScalarFilterType, getPrimaryKeyInfo, hasValidPrimaryKey, fieldTypeToTs, toScreamingSnake, ucFirst, lcFirst, getGeneratedFileHeader, } from './utils';
5
+ function createUnionType(values) {
6
+ return t.tsUnionType(values.map((v) => t.tsLiteralType(t.stringLiteral(v))));
7
+ }
8
+ function createFilterInterfaceDeclaration(name, fieldFilters, isExported = true) {
9
+ const properties = [];
10
+ for (const filter of fieldFilters) {
11
+ const prop = t.tsPropertySignature(t.identifier(filter.fieldName), t.tsTypeAnnotation(t.tsTypeReference(t.identifier(filter.filterType))));
12
+ prop.optional = true;
13
+ properties.push(prop);
14
+ }
15
+ const andProp = t.tsPropertySignature(t.identifier('and'), t.tsTypeAnnotation(t.tsArrayType(t.tsTypeReference(t.identifier(name)))));
16
+ andProp.optional = true;
17
+ properties.push(andProp);
18
+ const orProp = t.tsPropertySignature(t.identifier('or'), t.tsTypeAnnotation(t.tsArrayType(t.tsTypeReference(t.identifier(name)))));
19
+ orProp.optional = true;
20
+ properties.push(orProp);
21
+ const notProp = t.tsPropertySignature(t.identifier('not'), t.tsTypeAnnotation(t.tsTypeReference(t.identifier(name))));
22
+ notProp.optional = true;
23
+ properties.push(notProp);
24
+ const body = t.tsInterfaceBody(properties);
25
+ const interfaceDecl = t.tsInterfaceDeclaration(t.identifier(name), null, null, body);
26
+ if (isExported) {
27
+ return t.exportNamedDeclaration(interfaceDecl);
28
+ }
29
+ return interfaceDecl;
30
+ }
10
31
  export function generateListQueryHook(table, options = {}) {
11
- const { reactQueryEnabled = true } = options;
12
- const project = createProject();
32
+ const { reactQueryEnabled = true, useCentralizedKeys = true, hasRelationships = false, } = options;
13
33
  const { typeName, pluralName } = getTableNames(table);
14
34
  const hookName = getListQueryHookName(table);
15
35
  const queryName = getAllRowsQueryName(table);
16
36
  const filterTypeName = getFilterTypeName(table);
37
+ const conditionTypeName = getConditionTypeName(table);
17
38
  const orderByTypeName = getOrderByTypeName(table);
18
39
  const scalarFields = getScalarFields(table);
19
- // Generate GraphQL document via AST
40
+ const keysName = `${lcFirst(typeName)}Keys`;
41
+ const scopeTypeName = `${typeName}Scope`;
20
42
  const queryAST = buildListQueryAST({ table });
21
43
  const queryDocument = printGraphQL(queryAST);
22
- const sourceFile = createSourceFile(project, getListQueryFileName(table));
23
- // Add file header as leading comment
24
- const headerText = reactQueryEnabled
25
- ? `List query hook for ${typeName}`
26
- : `List query functions for ${typeName}`;
27
- sourceFile.insertText(0, createFileHeader(headerText) + '\n\n');
28
- // Collect all filter types used by this table's fields
44
+ const statements = [];
29
45
  const filterTypesUsed = new Set();
30
46
  for (const field of scalarFields) {
31
47
  const filterType = getScalarFilterType(field.type.gqlType, field.type.isArray);
@@ -33,47 +49,109 @@ export function generateListQueryHook(table, options = {}) {
33
49
  filterTypesUsed.add(filterType);
34
50
  }
35
51
  }
36
- // Add imports - conditionally include React Query imports
37
- const imports = [];
38
52
  if (reactQueryEnabled) {
39
- imports.push(createImport({
40
- moduleSpecifier: '@tanstack/react-query',
41
- namedImports: ['useQuery'],
42
- typeOnlyNamedImports: ['UseQueryOptions', 'QueryClient'],
43
- }));
53
+ const reactQueryImport = t.importDeclaration([t.importSpecifier(t.identifier('useQuery'), t.identifier('useQuery'))], t.stringLiteral('@tanstack/react-query'));
54
+ statements.push(reactQueryImport);
55
+ const reactQueryTypeImport = t.importDeclaration([
56
+ t.importSpecifier(t.identifier('UseQueryOptions'), t.identifier('UseQueryOptions')),
57
+ t.importSpecifier(t.identifier('QueryClient'), t.identifier('QueryClient')),
58
+ ], t.stringLiteral('@tanstack/react-query'));
59
+ reactQueryTypeImport.importKind = 'type';
60
+ statements.push(reactQueryTypeImport);
44
61
  }
45
- imports.push(createImport({
46
- moduleSpecifier: '../client',
47
- namedImports: ['execute'],
48
- typeOnlyNamedImports: ['ExecuteOptions'],
49
- }), createImport({
50
- moduleSpecifier: '../types',
51
- typeOnlyNamedImports: [typeName, ...Array.from(filterTypesUsed)],
52
- }));
53
- sourceFile.addImportDeclarations(imports);
54
- // Re-export entity type
55
- sourceFile.addStatements(`\n// Re-export entity type for convenience\nexport type { ${typeName} };\n`);
56
- // Add section comment
57
- sourceFile.addStatements('\n// ============================================================================');
58
- sourceFile.addStatements('// GraphQL Document');
59
- sourceFile.addStatements('// ============================================================================\n');
60
- // Add query document constant
61
- sourceFile.addVariableStatement(createConst(`${queryName}QueryDocument`, '`\n' + queryDocument + '`'));
62
- // Add section comment
63
- sourceFile.addStatements('\n// ============================================================================');
64
- sourceFile.addStatements('// Types');
65
- sourceFile.addStatements('// ============================================================================\n');
66
- // Generate filter interface
62
+ const clientImport = t.importDeclaration([t.importSpecifier(t.identifier('execute'), t.identifier('execute'))], t.stringLiteral('../client'));
63
+ statements.push(clientImport);
64
+ const clientTypeImport = t.importDeclaration([
65
+ t.importSpecifier(t.identifier('ExecuteOptions'), t.identifier('ExecuteOptions')),
66
+ ], t.stringLiteral('../client'));
67
+ clientTypeImport.importKind = 'type';
68
+ statements.push(clientTypeImport);
69
+ const typesImport = t.importDeclaration([
70
+ t.importSpecifier(t.identifier(typeName), t.identifier(typeName)),
71
+ ...Array.from(filterTypesUsed).map((ft) => t.importSpecifier(t.identifier(ft), t.identifier(ft))),
72
+ ], t.stringLiteral('../types'));
73
+ typesImport.importKind = 'type';
74
+ statements.push(typesImport);
75
+ if (useCentralizedKeys) {
76
+ const queryKeyImport = t.importDeclaration([t.importSpecifier(t.identifier(keysName), t.identifier(keysName))], t.stringLiteral('../query-keys'));
77
+ statements.push(queryKeyImport);
78
+ if (hasRelationships) {
79
+ const scopeTypeImport = t.importDeclaration([
80
+ t.importSpecifier(t.identifier(scopeTypeName), t.identifier(scopeTypeName)),
81
+ ], t.stringLiteral('../query-keys'));
82
+ scopeTypeImport.importKind = 'type';
83
+ statements.push(scopeTypeImport);
84
+ }
85
+ }
86
+ const reExportDecl = t.exportNamedDeclaration(null, [t.exportSpecifier(t.identifier(typeName), t.identifier(typeName))], t.stringLiteral('../types'));
87
+ reExportDecl.exportKind = 'type';
88
+ statements.push(reExportDecl);
89
+ const queryDocConst = t.variableDeclaration('const', [
90
+ t.variableDeclarator(t.identifier(`${queryName}QueryDocument`), t.templateLiteral([
91
+ t.templateElement({ raw: '\n' + queryDocument, cooked: '\n' + queryDocument }, true),
92
+ ], [])),
93
+ ]);
94
+ statements.push(t.exportNamedDeclaration(queryDocConst));
67
95
  const fieldFilters = scalarFields
68
96
  .map((field) => {
69
97
  const filterType = getScalarFilterType(field.type.gqlType, field.type.isArray);
70
98
  return filterType ? { fieldName: field.name, filterType } : null;
71
99
  })
72
100
  .filter((f) => f !== null);
73
- // Note: Not exported to avoid conflicts with schema-types
74
- sourceFile.addInterface(createFilterInterface(filterTypeName, fieldFilters, { isExported: false }));
75
- // Generate OrderBy type
76
- // Note: Not exported to avoid conflicts with schema-types
101
+ statements.push(createFilterInterfaceDeclaration(filterTypeName, fieldFilters, false));
102
+ // Generate Condition interface (simple equality filter with scalar types)
103
+ // Track non-primitive types (enums) that need to be imported
104
+ const enumTypesUsed = new Set();
105
+ const conditionProperties = scalarFields.map((field) => {
106
+ const tsType = fieldTypeToTs(field.type);
107
+ const isPrimitive = tsType === 'string' ||
108
+ tsType === 'number' ||
109
+ tsType === 'boolean' ||
110
+ tsType === 'unknown' ||
111
+ tsType.endsWith('[]');
112
+ let typeAnnotation;
113
+ if (field.type.isArray) {
114
+ const baseType = tsType.replace('[]', '');
115
+ const isBasePrimitive = baseType === 'string' ||
116
+ baseType === 'number' ||
117
+ baseType === 'boolean' ||
118
+ baseType === 'unknown';
119
+ if (!isBasePrimitive) {
120
+ enumTypesUsed.add(baseType);
121
+ }
122
+ typeAnnotation = t.tsArrayType(baseType === 'string'
123
+ ? t.tsStringKeyword()
124
+ : baseType === 'number'
125
+ ? t.tsNumberKeyword()
126
+ : baseType === 'boolean'
127
+ ? t.tsBooleanKeyword()
128
+ : t.tsTypeReference(t.identifier(baseType)));
129
+ }
130
+ else {
131
+ if (!isPrimitive) {
132
+ enumTypesUsed.add(tsType);
133
+ }
134
+ typeAnnotation =
135
+ tsType === 'string'
136
+ ? t.tsStringKeyword()
137
+ : tsType === 'number'
138
+ ? t.tsNumberKeyword()
139
+ : tsType === 'boolean'
140
+ ? t.tsBooleanKeyword()
141
+ : t.tsTypeReference(t.identifier(tsType));
142
+ }
143
+ const prop = t.tsPropertySignature(t.identifier(field.name), t.tsTypeAnnotation(typeAnnotation));
144
+ prop.optional = true;
145
+ return prop;
146
+ });
147
+ // Add import for enum types if any are used
148
+ if (enumTypesUsed.size > 0) {
149
+ const schemaTypesImport = t.importDeclaration(Array.from(enumTypesUsed).map((et) => t.importSpecifier(t.identifier(et), t.identifier(et))), t.stringLiteral('../schema-types'));
150
+ schemaTypesImport.importKind = 'type';
151
+ statements.push(schemaTypesImport);
152
+ }
153
+ const conditionInterface = t.tsInterfaceDeclaration(t.identifier(conditionTypeName), null, null, t.tsInterfaceBody(conditionProperties));
154
+ statements.push(conditionInterface);
77
155
  const orderByValues = [
78
156
  ...scalarFields.flatMap((f) => [
79
157
  `${toScreamingSnake(f.name)}_ASC`,
@@ -83,378 +161,537 @@ export function generateListQueryHook(table, options = {}) {
83
161
  'PRIMARY_KEY_ASC',
84
162
  'PRIMARY_KEY_DESC',
85
163
  ];
86
- sourceFile.addTypeAlias(createTypeAlias(orderByTypeName, createUnionType(orderByValues), { isExported: false }));
87
- // Variables interface
88
- const variablesProps = [
89
- { name: 'first', type: 'number', optional: true },
90
- { name: 'offset', type: 'number', optional: true },
91
- { name: 'filter', type: filterTypeName, optional: true },
92
- { name: 'orderBy', type: `${orderByTypeName}[]`, optional: true },
93
- ];
94
- sourceFile.addInterface(createInterface(`${ucFirst(pluralName)}QueryVariables`, variablesProps));
95
- // Result interface
96
- const resultProps = [
97
- {
98
- name: queryName,
99
- type: `{
100
- totalCount: number;
101
- nodes: ${typeName}[];
102
- pageInfo: {
103
- hasNextPage: boolean;
104
- hasPreviousPage: boolean;
105
- startCursor: string | null;
106
- endCursor: string | null;
107
- };
108
- }`,
109
- },
110
- ];
111
- sourceFile.addInterface(createInterface(`${ucFirst(pluralName)}QueryResult`, resultProps));
112
- // Add section comment
113
- sourceFile.addStatements('\n// ============================================================================');
114
- sourceFile.addStatements('// Query Key');
115
- sourceFile.addStatements('// ============================================================================\n');
116
- // Query key factory
117
- sourceFile.addVariableStatement(createConst(`${queryName}QueryKey`, `(variables?: ${ucFirst(pluralName)}QueryVariables) =>
118
- ['${typeName.toLowerCase()}', 'list', variables] as const`));
119
- // Add React Query hook section (only if enabled)
164
+ const orderByTypeAlias = t.tsTypeAliasDeclaration(t.identifier(orderByTypeName), null, createUnionType(orderByValues));
165
+ statements.push(orderByTypeAlias);
166
+ const variablesInterfaceBody = t.tsInterfaceBody([
167
+ (() => {
168
+ const p = t.tsPropertySignature(t.identifier('first'), t.tsTypeAnnotation(t.tsNumberKeyword()));
169
+ p.optional = true;
170
+ return p;
171
+ })(),
172
+ (() => {
173
+ const p = t.tsPropertySignature(t.identifier('last'), t.tsTypeAnnotation(t.tsNumberKeyword()));
174
+ p.optional = true;
175
+ return p;
176
+ })(),
177
+ (() => {
178
+ const p = t.tsPropertySignature(t.identifier('offset'), t.tsTypeAnnotation(t.tsNumberKeyword()));
179
+ p.optional = true;
180
+ return p;
181
+ })(),
182
+ (() => {
183
+ const p = t.tsPropertySignature(t.identifier('before'), t.tsTypeAnnotation(t.tsStringKeyword()));
184
+ p.optional = true;
185
+ return p;
186
+ })(),
187
+ (() => {
188
+ const p = t.tsPropertySignature(t.identifier('after'), t.tsTypeAnnotation(t.tsStringKeyword()));
189
+ p.optional = true;
190
+ return p;
191
+ })(),
192
+ (() => {
193
+ const p = t.tsPropertySignature(t.identifier('filter'), t.tsTypeAnnotation(t.tsTypeReference(t.identifier(filterTypeName))));
194
+ p.optional = true;
195
+ return p;
196
+ })(),
197
+ (() => {
198
+ const p = t.tsPropertySignature(t.identifier('condition'), t.tsTypeAnnotation(t.tsTypeReference(t.identifier(conditionTypeName))));
199
+ p.optional = true;
200
+ return p;
201
+ })(),
202
+ (() => {
203
+ const p = t.tsPropertySignature(t.identifier('orderBy'), t.tsTypeAnnotation(t.tsArrayType(t.tsTypeReference(t.identifier(orderByTypeName)))));
204
+ p.optional = true;
205
+ return p;
206
+ })(),
207
+ ]);
208
+ const variablesInterface = t.tsInterfaceDeclaration(t.identifier(`${ucFirst(pluralName)}QueryVariables`), null, null, variablesInterfaceBody);
209
+ statements.push(t.exportNamedDeclaration(variablesInterface));
210
+ const pageInfoType = t.tsTypeLiteral([
211
+ t.tsPropertySignature(t.identifier('hasNextPage'), t.tsTypeAnnotation(t.tsBooleanKeyword())),
212
+ t.tsPropertySignature(t.identifier('hasPreviousPage'), t.tsTypeAnnotation(t.tsBooleanKeyword())),
213
+ t.tsPropertySignature(t.identifier('startCursor'), t.tsTypeAnnotation(t.tsUnionType([t.tsStringKeyword(), t.tsNullKeyword()]))),
214
+ t.tsPropertySignature(t.identifier('endCursor'), t.tsTypeAnnotation(t.tsUnionType([t.tsStringKeyword(), t.tsNullKeyword()]))),
215
+ ]);
216
+ const resultType = t.tsTypeLiteral([
217
+ t.tsPropertySignature(t.identifier('totalCount'), t.tsTypeAnnotation(t.tsNumberKeyword())),
218
+ t.tsPropertySignature(t.identifier('nodes'), t.tsTypeAnnotation(t.tsArrayType(t.tsTypeReference(t.identifier(typeName))))),
219
+ t.tsPropertySignature(t.identifier('pageInfo'), t.tsTypeAnnotation(pageInfoType)),
220
+ ]);
221
+ const resultInterfaceBody = t.tsInterfaceBody([
222
+ t.tsPropertySignature(t.identifier(queryName), t.tsTypeAnnotation(resultType)),
223
+ ]);
224
+ const resultInterface = t.tsInterfaceDeclaration(t.identifier(`${ucFirst(pluralName)}QueryResult`), null, null, resultInterfaceBody);
225
+ statements.push(t.exportNamedDeclaration(resultInterface));
226
+ if (useCentralizedKeys) {
227
+ const queryKeyConst = t.variableDeclaration('const', [
228
+ t.variableDeclarator(t.identifier(`${queryName}QueryKey`), t.memberExpression(t.identifier(keysName), t.identifier('list'))),
229
+ ]);
230
+ const queryKeyExport = t.exportNamedDeclaration(queryKeyConst);
231
+ addJSDocComment(queryKeyExport, [
232
+ 'Query key factory - re-exported from query-keys.ts',
233
+ ]);
234
+ statements.push(queryKeyExport);
235
+ }
236
+ else {
237
+ const queryKeyArrow = t.arrowFunctionExpression([
238
+ typedParam('variables', t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)), true),
239
+ ], t.tsAsExpression(t.arrayExpression([
240
+ t.stringLiteral(typeName.toLowerCase()),
241
+ t.stringLiteral('list'),
242
+ t.identifier('variables'),
243
+ ]), t.tsTypeReference(t.identifier('const'))));
244
+ const queryKeyConst = t.variableDeclaration('const', [
245
+ t.variableDeclarator(t.identifier(`${queryName}QueryKey`), queryKeyArrow),
246
+ ]);
247
+ statements.push(t.exportNamedDeclaration(queryKeyConst));
248
+ }
120
249
  if (reactQueryEnabled) {
121
- sourceFile.addStatements('\n// ============================================================================');
122
- sourceFile.addStatements('// Hook');
123
- sourceFile.addStatements('// ============================================================================\n');
124
- // Hook function
125
- sourceFile.addFunction({
126
- name: hookName,
127
- isExported: true,
128
- parameters: [
129
- {
130
- name: 'variables',
131
- type: `${ucFirst(pluralName)}QueryVariables`,
132
- hasQuestionToken: true,
133
- },
134
- {
135
- name: 'options',
136
- type: `Omit<UseQueryOptions<${ucFirst(pluralName)}QueryResult, Error>, 'queryKey' | 'queryFn'>`,
137
- hasQuestionToken: true,
138
- },
139
- ],
140
- statements: `return useQuery({
141
- queryKey: ${queryName}QueryKey(variables),
142
- queryFn: () => execute<${ucFirst(pluralName)}QueryResult, ${ucFirst(pluralName)}QueryVariables>(
143
- ${queryName}QueryDocument,
144
- variables
145
- ),
146
- ...options,
147
- });`,
148
- docs: [
149
- {
150
- description: `Query hook for fetching ${typeName} list
151
-
152
- @example
153
- \`\`\`tsx
154
- const { data, isLoading } = ${hookName}({
155
- first: 10,
156
- filter: { name: { equalTo: "example" } },
157
- orderBy: ['CREATED_AT_DESC'],
158
- });
159
- \`\`\``,
160
- },
161
- ],
162
- });
250
+ const hookBodyStatements = [];
251
+ if (hasRelationships && useCentralizedKeys) {
252
+ hookBodyStatements.push(t.variableDeclaration('const', [
253
+ t.variableDeclarator(t.objectPattern([
254
+ t.objectProperty(t.identifier('scope'), t.identifier('scope'), false, true),
255
+ t.restElement(t.identifier('queryOptions')),
256
+ ]), t.logicalExpression('??', t.identifier('options'), t.objectExpression([]))),
257
+ ]));
258
+ hookBodyStatements.push(t.returnStatement(t.callExpression(t.identifier('useQuery'), [
259
+ t.objectExpression([
260
+ t.objectProperty(t.identifier('queryKey'), t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('list')), [t.identifier('variables'), t.identifier('scope')])),
261
+ t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables')], [
262
+ t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryResult`)),
263
+ t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)),
264
+ ]))),
265
+ t.spreadElement(t.identifier('queryOptions')),
266
+ ]),
267
+ ])));
268
+ }
269
+ else if (useCentralizedKeys) {
270
+ hookBodyStatements.push(t.returnStatement(t.callExpression(t.identifier('useQuery'), [
271
+ t.objectExpression([
272
+ t.objectProperty(t.identifier('queryKey'), t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('list')), [t.identifier('variables')])),
273
+ t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables')], [
274
+ t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryResult`)),
275
+ t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)),
276
+ ]))),
277
+ t.spreadElement(t.identifier('options')),
278
+ ]),
279
+ ])));
280
+ }
281
+ else {
282
+ hookBodyStatements.push(t.returnStatement(t.callExpression(t.identifier('useQuery'), [
283
+ t.objectExpression([
284
+ t.objectProperty(t.identifier('queryKey'), t.callExpression(t.identifier(`${queryName}QueryKey`), [
285
+ t.identifier('variables'),
286
+ ])),
287
+ t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables')], [
288
+ t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryResult`)),
289
+ t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)),
290
+ ]))),
291
+ t.spreadElement(t.identifier('options')),
292
+ ]),
293
+ ])));
294
+ }
295
+ const hookParams = [
296
+ typedParam('variables', t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)), true),
297
+ ];
298
+ let optionsTypeStr;
299
+ if (hasRelationships && useCentralizedKeys) {
300
+ optionsTypeStr = `Omit<UseQueryOptions<${ucFirst(pluralName)}QueryResult, Error>, 'queryKey' | 'queryFn'> & { scope?: ${scopeTypeName} }`;
301
+ }
302
+ else {
303
+ optionsTypeStr = `Omit<UseQueryOptions<${ucFirst(pluralName)}QueryResult, Error>, 'queryKey' | 'queryFn'>`;
304
+ }
305
+ const optionsParam = t.identifier('options');
306
+ optionsParam.optional = true;
307
+ optionsParam.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier(optionsTypeStr)));
308
+ hookParams.push(optionsParam);
309
+ const hookFunc = t.functionDeclaration(t.identifier(hookName), hookParams, t.blockStatement(hookBodyStatements));
310
+ const hookExport = t.exportNamedDeclaration(hookFunc);
311
+ const docLines = [
312
+ `Query hook for fetching ${typeName} list`,
313
+ '',
314
+ '@example',
315
+ '```tsx',
316
+ `const { data, isLoading } = ${hookName}({`,
317
+ ' first: 10,',
318
+ ' filter: { name: { equalTo: "example" } },',
319
+ " orderBy: ['CREATED_AT_DESC'],",
320
+ '});',
321
+ '```',
322
+ ];
323
+ if (hasRelationships && useCentralizedKeys) {
324
+ docLines.push('');
325
+ docLines.push('@example With scope for hierarchical cache invalidation');
326
+ docLines.push('```tsx');
327
+ docLines.push(`const { data } = ${hookName}(`);
328
+ docLines.push(' { first: 10 },');
329
+ docLines.push(" { scope: { parentId: 'parent-id' } }");
330
+ docLines.push(');');
331
+ docLines.push('```');
332
+ }
333
+ addJSDocComment(hookExport, docLines);
334
+ statements.push(hookExport);
163
335
  }
164
- // Add section comment for standalone functions
165
- sourceFile.addStatements('\n// ============================================================================');
166
- sourceFile.addStatements('// Standalone Functions (non-React)');
167
- sourceFile.addStatements('// ============================================================================\n');
168
- // Fetch function (standalone, no React)
169
- sourceFile.addFunction({
170
- name: `fetch${ucFirst(pluralName)}Query`,
171
- isExported: true,
172
- isAsync: true,
173
- parameters: [
174
- {
175
- name: 'variables',
176
- type: `${ucFirst(pluralName)}QueryVariables`,
177
- hasQuestionToken: true,
178
- },
179
- {
180
- name: 'options',
181
- type: 'ExecuteOptions',
182
- hasQuestionToken: true,
183
- },
184
- ],
185
- returnType: `Promise<${ucFirst(pluralName)}QueryResult>`,
186
- statements: `return execute<${ucFirst(pluralName)}QueryResult, ${ucFirst(pluralName)}QueryVariables>(
187
- ${queryName}QueryDocument,
188
- variables,
189
- options
190
- );`,
191
- docs: [
192
- {
193
- description: `Fetch ${typeName} list without React hooks
194
-
195
- @example
196
- \`\`\`ts
197
- // Direct fetch
198
- const data = await fetch${ucFirst(pluralName)}Query({ first: 10 });
199
-
200
- // With QueryClient
201
- const data = await queryClient.fetchQuery({
202
- queryKey: ${queryName}QueryKey(variables),
203
- queryFn: () => fetch${ucFirst(pluralName)}Query(variables),
204
- });
205
- \`\`\``,
206
- },
207
- ],
208
- });
209
- // Prefetch function (for SSR/QueryClient) - only if React Query is enabled
336
+ const fetchFuncBody = t.blockStatement([
337
+ t.returnStatement(createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables'), t.identifier('options')], [
338
+ t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryResult`)),
339
+ t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)),
340
+ ])),
341
+ ]);
342
+ const fetchFunc = t.functionDeclaration(t.identifier(`fetch${ucFirst(pluralName)}Query`), [
343
+ typedParam('variables', t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)), true),
344
+ typedParam('options', t.tsTypeReference(t.identifier('ExecuteOptions')), true),
345
+ ], fetchFuncBody);
346
+ fetchFunc.async = true;
347
+ fetchFunc.returnType = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('Promise'), t.tsTypeParameterInstantiation([
348
+ t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryResult`)),
349
+ ])));
350
+ const fetchExport = t.exportNamedDeclaration(fetchFunc);
351
+ addJSDocComment(fetchExport, [
352
+ `Fetch ${typeName} list without React hooks`,
353
+ '',
354
+ '@example',
355
+ '```ts',
356
+ '// Direct fetch',
357
+ `const data = await fetch${ucFirst(pluralName)}Query({ first: 10 });`,
358
+ '',
359
+ '// With QueryClient',
360
+ 'const data = await queryClient.fetchQuery({',
361
+ ` queryKey: ${queryName}QueryKey(variables),`,
362
+ ` queryFn: () => fetch${ucFirst(pluralName)}Query(variables),`,
363
+ '});',
364
+ '```',
365
+ ]);
366
+ statements.push(fetchExport);
210
367
  if (reactQueryEnabled) {
211
- sourceFile.addFunction({
212
- name: `prefetch${ucFirst(pluralName)}Query`,
213
- isExported: true,
214
- isAsync: true,
215
- parameters: [
216
- {
217
- name: 'queryClient',
218
- type: 'QueryClient',
219
- },
220
- {
221
- name: 'variables',
222
- type: `${ucFirst(pluralName)}QueryVariables`,
223
- hasQuestionToken: true,
224
- },
225
- {
226
- name: 'options',
227
- type: 'ExecuteOptions',
228
- hasQuestionToken: true,
229
- },
230
- ],
231
- returnType: 'Promise<void>',
232
- statements: `await queryClient.prefetchQuery({
233
- queryKey: ${queryName}QueryKey(variables),
234
- queryFn: () => execute<${ucFirst(pluralName)}QueryResult, ${ucFirst(pluralName)}QueryVariables>(
235
- ${queryName}QueryDocument,
236
- variables,
237
- options
238
- ),
239
- });`,
240
- docs: [
241
- {
242
- description: `Prefetch ${typeName} list for SSR or cache warming
243
-
244
- @example
245
- \`\`\`ts
246
- await prefetch${ucFirst(pluralName)}Query(queryClient, { first: 10 });
247
- \`\`\``,
248
- },
249
- ],
250
- });
368
+ const prefetchParams = [
369
+ typedParam('queryClient', t.tsTypeReference(t.identifier('QueryClient'))),
370
+ typedParam('variables', t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)), true),
371
+ ];
372
+ if (hasRelationships && useCentralizedKeys) {
373
+ prefetchParams.push(typedParam('scope', t.tsTypeReference(t.identifier(scopeTypeName)), true));
374
+ }
375
+ prefetchParams.push(typedParam('options', t.tsTypeReference(t.identifier('ExecuteOptions')), true));
376
+ let prefetchQueryKeyExpr;
377
+ if (hasRelationships && useCentralizedKeys) {
378
+ prefetchQueryKeyExpr = t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('list')), [t.identifier('variables'), t.identifier('scope')]);
379
+ }
380
+ else if (useCentralizedKeys) {
381
+ prefetchQueryKeyExpr = t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('list')), [t.identifier('variables')]);
382
+ }
383
+ else {
384
+ prefetchQueryKeyExpr = t.callExpression(t.identifier(`${queryName}QueryKey`), [t.identifier('variables')]);
385
+ }
386
+ const prefetchFuncBody = t.blockStatement([
387
+ t.expressionStatement(t.awaitExpression(t.callExpression(t.memberExpression(t.identifier('queryClient'), t.identifier('prefetchQuery')), [
388
+ t.objectExpression([
389
+ t.objectProperty(t.identifier('queryKey'), prefetchQueryKeyExpr),
390
+ t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables'), t.identifier('options')], [
391
+ t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryResult`)),
392
+ t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)),
393
+ ]))),
394
+ ]),
395
+ ]))),
396
+ ]);
397
+ const prefetchFunc = t.functionDeclaration(t.identifier(`prefetch${ucFirst(pluralName)}Query`), prefetchParams, prefetchFuncBody);
398
+ prefetchFunc.async = true;
399
+ prefetchFunc.returnType = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('Promise'), t.tsTypeParameterInstantiation([t.tsVoidKeyword()])));
400
+ const prefetchExport = t.exportNamedDeclaration(prefetchFunc);
401
+ addJSDocComment(prefetchExport, [
402
+ `Prefetch ${typeName} list for SSR or cache warming`,
403
+ '',
404
+ '@example',
405
+ '```ts',
406
+ `await prefetch${ucFirst(pluralName)}Query(queryClient, { first: 10 });`,
407
+ '```',
408
+ ]);
409
+ statements.push(prefetchExport);
251
410
  }
411
+ const code = generateCode(statements);
412
+ const headerText = reactQueryEnabled
413
+ ? `List query hook for ${typeName}`
414
+ : `List query functions for ${typeName}`;
415
+ const content = getGeneratedFileHeader(headerText) + '\n\n' + code;
252
416
  return {
253
417
  fileName: getListQueryFileName(table),
254
- content: getFormattedOutput(sourceFile),
418
+ content,
255
419
  };
256
420
  }
257
- // ============================================================================
258
- // Single item query hook generator
259
- // ============================================================================
260
- /**
261
- * Generate single item query hook file content using AST
262
- */
263
421
  export function generateSingleQueryHook(table, options = {}) {
264
- const { reactQueryEnabled = true } = options;
265
- const project = createProject();
422
+ // Skip tables with composite keys - they are handled as custom queries
423
+ if (!hasValidPrimaryKey(table)) {
424
+ return null;
425
+ }
426
+ const { reactQueryEnabled = true, useCentralizedKeys = true, hasRelationships = false, } = options;
266
427
  const { typeName, singularName } = getTableNames(table);
267
428
  const hookName = getSingleQueryHookName(table);
268
429
  const queryName = getSingleRowQueryName(table);
269
- // Get primary key info dynamically from table constraints
430
+ const keysName = `${lcFirst(typeName)}Keys`;
431
+ const scopeTypeName = `${typeName}Scope`;
270
432
  const pkFields = getPrimaryKeyInfo(table);
271
- // For simplicity, use first PK field (most common case)
272
- // Composite PKs would need more complex handling
273
433
  const pkField = pkFields[0];
274
434
  const pkName = pkField.name;
275
435
  const pkTsType = pkField.tsType;
276
- // Generate GraphQL document via AST
277
436
  const queryAST = buildSingleQueryAST({ table });
278
437
  const queryDocument = printGraphQL(queryAST);
279
- const sourceFile = createSourceFile(project, getSingleQueryFileName(table));
280
- // Add file header
281
- const headerText = reactQueryEnabled
282
- ? `Single item query hook for ${typeName}`
283
- : `Single item query functions for ${typeName}`;
284
- sourceFile.insertText(0, createFileHeader(headerText) + '\n\n');
285
- // Add imports - conditionally include React Query imports
286
- const imports = [];
438
+ const statements = [];
287
439
  if (reactQueryEnabled) {
288
- imports.push(createImport({
289
- moduleSpecifier: '@tanstack/react-query',
290
- namedImports: ['useQuery'],
291
- typeOnlyNamedImports: ['UseQueryOptions', 'QueryClient'],
292
- }));
440
+ const reactQueryImport = t.importDeclaration([t.importSpecifier(t.identifier('useQuery'), t.identifier('useQuery'))], t.stringLiteral('@tanstack/react-query'));
441
+ statements.push(reactQueryImport);
442
+ const reactQueryTypeImport = t.importDeclaration([
443
+ t.importSpecifier(t.identifier('UseQueryOptions'), t.identifier('UseQueryOptions')),
444
+ t.importSpecifier(t.identifier('QueryClient'), t.identifier('QueryClient')),
445
+ ], t.stringLiteral('@tanstack/react-query'));
446
+ reactQueryTypeImport.importKind = 'type';
447
+ statements.push(reactQueryTypeImport);
448
+ }
449
+ const clientImport = t.importDeclaration([t.importSpecifier(t.identifier('execute'), t.identifier('execute'))], t.stringLiteral('../client'));
450
+ statements.push(clientImport);
451
+ const clientTypeImport = t.importDeclaration([
452
+ t.importSpecifier(t.identifier('ExecuteOptions'), t.identifier('ExecuteOptions')),
453
+ ], t.stringLiteral('../client'));
454
+ clientTypeImport.importKind = 'type';
455
+ statements.push(clientTypeImport);
456
+ const typesImport = t.importDeclaration([t.importSpecifier(t.identifier(typeName), t.identifier(typeName))], t.stringLiteral('../types'));
457
+ typesImport.importKind = 'type';
458
+ statements.push(typesImport);
459
+ if (useCentralizedKeys) {
460
+ const queryKeyImport = t.importDeclaration([t.importSpecifier(t.identifier(keysName), t.identifier(keysName))], t.stringLiteral('../query-keys'));
461
+ statements.push(queryKeyImport);
462
+ if (hasRelationships) {
463
+ const scopeTypeImport = t.importDeclaration([
464
+ t.importSpecifier(t.identifier(scopeTypeName), t.identifier(scopeTypeName)),
465
+ ], t.stringLiteral('../query-keys'));
466
+ scopeTypeImport.importKind = 'type';
467
+ statements.push(scopeTypeImport);
468
+ }
469
+ }
470
+ const reExportDecl = t.exportNamedDeclaration(null, [t.exportSpecifier(t.identifier(typeName), t.identifier(typeName))], t.stringLiteral('../types'));
471
+ reExportDecl.exportKind = 'type';
472
+ statements.push(reExportDecl);
473
+ const queryDocConst = t.variableDeclaration('const', [
474
+ t.variableDeclarator(t.identifier(`${queryName}QueryDocument`), t.templateLiteral([
475
+ t.templateElement({ raw: '\n' + queryDocument, cooked: '\n' + queryDocument }, true),
476
+ ], [])),
477
+ ]);
478
+ statements.push(t.exportNamedDeclaration(queryDocConst));
479
+ const pkTypeAnnotation = pkTsType === 'string'
480
+ ? t.tsStringKeyword()
481
+ : pkTsType === 'number'
482
+ ? t.tsNumberKeyword()
483
+ : t.tsTypeReference(t.identifier(pkTsType));
484
+ const variablesInterfaceBody = t.tsInterfaceBody([
485
+ t.tsPropertySignature(t.identifier(pkName), t.tsTypeAnnotation(pkTypeAnnotation)),
486
+ ]);
487
+ const variablesInterface = t.tsInterfaceDeclaration(t.identifier(`${ucFirst(singularName)}QueryVariables`), null, null, variablesInterfaceBody);
488
+ statements.push(t.exportNamedDeclaration(variablesInterface));
489
+ const resultInterfaceBody = t.tsInterfaceBody([
490
+ t.tsPropertySignature(t.identifier(queryName), t.tsTypeAnnotation(t.tsUnionType([
491
+ t.tsTypeReference(t.identifier(typeName)),
492
+ t.tsNullKeyword(),
493
+ ]))),
494
+ ]);
495
+ const resultInterface = t.tsInterfaceDeclaration(t.identifier(`${ucFirst(singularName)}QueryResult`), null, null, resultInterfaceBody);
496
+ statements.push(t.exportNamedDeclaration(resultInterface));
497
+ if (useCentralizedKeys) {
498
+ const queryKeyConst = t.variableDeclaration('const', [
499
+ t.variableDeclarator(t.identifier(`${queryName}QueryKey`), t.memberExpression(t.identifier(keysName), t.identifier('detail'))),
500
+ ]);
501
+ const queryKeyExport = t.exportNamedDeclaration(queryKeyConst);
502
+ addJSDocComment(queryKeyExport, [
503
+ 'Query key factory - re-exported from query-keys.ts',
504
+ ]);
505
+ statements.push(queryKeyExport);
506
+ }
507
+ else {
508
+ const queryKeyArrow = t.arrowFunctionExpression([typedParam(pkName, pkTypeAnnotation)], t.tsAsExpression(t.arrayExpression([
509
+ t.stringLiteral(typeName.toLowerCase()),
510
+ t.stringLiteral('detail'),
511
+ t.identifier(pkName),
512
+ ]), t.tsTypeReference(t.identifier('const'))));
513
+ const queryKeyConst = t.variableDeclaration('const', [
514
+ t.variableDeclarator(t.identifier(`${queryName}QueryKey`), queryKeyArrow),
515
+ ]);
516
+ statements.push(t.exportNamedDeclaration(queryKeyConst));
293
517
  }
294
- imports.push(createImport({
295
- moduleSpecifier: '../client',
296
- namedImports: ['execute'],
297
- typeOnlyNamedImports: ['ExecuteOptions'],
298
- }), createImport({
299
- moduleSpecifier: '../types',
300
- typeOnlyNamedImports: [typeName],
301
- }));
302
- sourceFile.addImportDeclarations(imports);
303
- // Re-export entity type
304
- sourceFile.addStatements(`\n// Re-export entity type for convenience\nexport type { ${typeName} };\n`);
305
- // Add section comment
306
- sourceFile.addStatements('\n// ============================================================================');
307
- sourceFile.addStatements('// GraphQL Document');
308
- sourceFile.addStatements('// ============================================================================\n');
309
- // Add query document constant
310
- sourceFile.addVariableStatement(createConst(`${queryName}QueryDocument`, '`\n' + queryDocument + '`'));
311
- // Add section comment
312
- sourceFile.addStatements('\n// ============================================================================');
313
- sourceFile.addStatements('// Types');
314
- sourceFile.addStatements('// ============================================================================\n');
315
- // Variables interface - use dynamic PK field name and type
316
- sourceFile.addInterface(createInterface(`${ucFirst(singularName)}QueryVariables`, [
317
- { name: pkName, type: pkTsType },
318
- ]));
319
- // Result interface
320
- sourceFile.addInterface(createInterface(`${ucFirst(singularName)}QueryResult`, [
321
- { name: queryName, type: `${typeName} | null` },
322
- ]));
323
- // Add section comment
324
- sourceFile.addStatements('\n// ============================================================================');
325
- sourceFile.addStatements('// Query Key');
326
- sourceFile.addStatements('// ============================================================================\n');
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`));
330
- // Add React Query hook section (only if enabled)
331
518
  if (reactQueryEnabled) {
332
- sourceFile.addStatements('\n// ============================================================================');
333
- sourceFile.addStatements('// Hook');
334
- sourceFile.addStatements('// ============================================================================\n');
335
- // Hook function - use dynamic PK field name and type
336
- sourceFile.addFunction({
337
- name: hookName,
338
- isExported: true,
339
- parameters: [
340
- { name: pkName, type: pkTsType },
341
- {
342
- name: 'options',
343
- type: `Omit<UseQueryOptions<${ucFirst(singularName)}QueryResult, Error>, 'queryKey' | 'queryFn'>`,
344
- hasQuestionToken: true,
345
- },
346
- ],
347
- statements: `return useQuery({
348
- queryKey: ${queryName}QueryKey(${pkName}),
349
- queryFn: () => execute<${ucFirst(singularName)}QueryResult, ${ucFirst(singularName)}QueryVariables>(
350
- ${queryName}QueryDocument,
351
- { ${pkName} }
352
- ),
353
- enabled: !!${pkName} && (options?.enabled !== false),
354
- ...options,
355
- });`,
356
- docs: [
357
- {
358
- description: `Query hook for fetching a single ${typeName} by primary key
359
-
360
- @example
361
- \`\`\`tsx
362
- const { data, isLoading } = ${hookName}(${pkTsType === 'string' ? "'value-here'" : '123'});
363
-
364
- if (data?.${queryName}) {
365
- console.log(data.${queryName}.${pkName});
366
- }
367
- \`\`\``,
368
- },
369
- ],
370
- });
519
+ const hookBodyStatements = [];
520
+ if (hasRelationships && useCentralizedKeys) {
521
+ hookBodyStatements.push(t.variableDeclaration('const', [
522
+ t.variableDeclarator(t.objectPattern([
523
+ t.objectProperty(t.identifier('scope'), t.identifier('scope'), false, true),
524
+ t.restElement(t.identifier('queryOptions')),
525
+ ]), t.logicalExpression('??', t.identifier('options'), t.objectExpression([]))),
526
+ ]));
527
+ hookBodyStatements.push(t.returnStatement(t.callExpression(t.identifier('useQuery'), [
528
+ t.objectExpression([
529
+ t.objectProperty(t.identifier('queryKey'), t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('detail')), [
530
+ t.memberExpression(t.identifier('variables'), t.identifier(pkName)),
531
+ t.identifier('scope'),
532
+ ])),
533
+ t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables')], [
534
+ t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryResult`)),
535
+ t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`)),
536
+ ]))),
537
+ t.spreadElement(t.identifier('queryOptions')),
538
+ ]),
539
+ ])));
540
+ }
541
+ else if (useCentralizedKeys) {
542
+ hookBodyStatements.push(t.returnStatement(t.callExpression(t.identifier('useQuery'), [
543
+ t.objectExpression([
544
+ t.objectProperty(t.identifier('queryKey'), t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('detail')), [
545
+ t.memberExpression(t.identifier('variables'), t.identifier(pkName)),
546
+ ])),
547
+ t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables')], [
548
+ t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryResult`)),
549
+ t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`)),
550
+ ]))),
551
+ t.spreadElement(t.identifier('options')),
552
+ ]),
553
+ ])));
554
+ }
555
+ else {
556
+ hookBodyStatements.push(t.returnStatement(t.callExpression(t.identifier('useQuery'), [
557
+ t.objectExpression([
558
+ t.objectProperty(t.identifier('queryKey'), t.callExpression(t.identifier(`${queryName}QueryKey`), [
559
+ t.memberExpression(t.identifier('variables'), t.identifier(pkName)),
560
+ ])),
561
+ t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables')], [
562
+ t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryResult`)),
563
+ t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`)),
564
+ ]))),
565
+ t.spreadElement(t.identifier('options')),
566
+ ]),
567
+ ])));
568
+ }
569
+ const hookParams = [
570
+ typedParam('variables', t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`))),
571
+ ];
572
+ let optionsTypeStr;
573
+ if (hasRelationships && useCentralizedKeys) {
574
+ optionsTypeStr = `Omit<UseQueryOptions<${ucFirst(singularName)}QueryResult, Error>, 'queryKey' | 'queryFn'> & { scope?: ${scopeTypeName} }`;
575
+ }
576
+ else {
577
+ optionsTypeStr = `Omit<UseQueryOptions<${ucFirst(singularName)}QueryResult, Error>, 'queryKey' | 'queryFn'>`;
578
+ }
579
+ const optionsParam = t.identifier('options');
580
+ optionsParam.optional = true;
581
+ optionsParam.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier(optionsTypeStr)));
582
+ hookParams.push(optionsParam);
583
+ const hookFunc = t.functionDeclaration(t.identifier(hookName), hookParams, t.blockStatement(hookBodyStatements));
584
+ const hookExport = t.exportNamedDeclaration(hookFunc);
585
+ const docLines = [
586
+ `Query hook for fetching a single ${typeName}`,
587
+ '',
588
+ '@example',
589
+ '```tsx',
590
+ `const { data, isLoading } = ${hookName}({ ${pkName}: 'some-id' });`,
591
+ '```',
592
+ ];
593
+ if (hasRelationships && useCentralizedKeys) {
594
+ docLines.push('');
595
+ docLines.push('@example With scope for hierarchical cache invalidation');
596
+ docLines.push('```tsx');
597
+ docLines.push(`const { data } = ${hookName}(`);
598
+ docLines.push(` { ${pkName}: 'some-id' },`);
599
+ docLines.push(" { scope: { parentId: 'parent-id' } }");
600
+ docLines.push(');');
601
+ docLines.push('```');
602
+ }
603
+ addJSDocComment(hookExport, docLines);
604
+ statements.push(hookExport);
371
605
  }
372
- // Add section comment for standalone functions
373
- sourceFile.addStatements('\n// ============================================================================');
374
- sourceFile.addStatements('// Standalone Functions (non-React)');
375
- sourceFile.addStatements('// ============================================================================\n');
376
- // Fetch function (standalone, no React) - use dynamic PK
377
- sourceFile.addFunction({
378
- name: `fetch${ucFirst(singularName)}Query`,
379
- isExported: true,
380
- isAsync: true,
381
- parameters: [
382
- { name: pkName, type: pkTsType },
383
- {
384
- name: 'options',
385
- type: 'ExecuteOptions',
386
- hasQuestionToken: true,
387
- },
388
- ],
389
- returnType: `Promise<${ucFirst(singularName)}QueryResult>`,
390
- statements: `return execute<${ucFirst(singularName)}QueryResult, ${ucFirst(singularName)}QueryVariables>(
391
- ${queryName}QueryDocument,
392
- { ${pkName} },
393
- options
394
- );`,
395
- docs: [
396
- {
397
- description: `Fetch a single ${typeName} by primary key without React hooks
398
-
399
- @example
400
- \`\`\`ts
401
- const data = await fetch${ucFirst(singularName)}Query(${pkTsType === 'string' ? "'value-here'" : '123'});
402
- \`\`\``,
403
- },
404
- ],
405
- });
406
- // Prefetch function (for SSR/QueryClient) - only if React Query is enabled, use dynamic PK
606
+ const fetchFuncBody = t.blockStatement([
607
+ t.returnStatement(createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables'), t.identifier('options')], [
608
+ t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryResult`)),
609
+ t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`)),
610
+ ])),
611
+ ]);
612
+ const fetchFunc = t.functionDeclaration(t.identifier(`fetch${ucFirst(singularName)}Query`), [
613
+ typedParam('variables', t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`))),
614
+ typedParam('options', t.tsTypeReference(t.identifier('ExecuteOptions')), true),
615
+ ], fetchFuncBody);
616
+ fetchFunc.async = true;
617
+ fetchFunc.returnType = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('Promise'), t.tsTypeParameterInstantiation([
618
+ t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryResult`)),
619
+ ])));
620
+ const fetchExport = t.exportNamedDeclaration(fetchFunc);
621
+ addJSDocComment(fetchExport, [
622
+ `Fetch a single ${typeName} without React hooks`,
623
+ '',
624
+ '@example',
625
+ '```ts',
626
+ `const data = await fetch${ucFirst(singularName)}Query({ ${pkName}: 'some-id' });`,
627
+ '```',
628
+ ]);
629
+ statements.push(fetchExport);
407
630
  if (reactQueryEnabled) {
408
- sourceFile.addFunction({
409
- name: `prefetch${ucFirst(singularName)}Query`,
410
- isExported: true,
411
- isAsync: true,
412
- parameters: [
413
- { name: 'queryClient', type: 'QueryClient' },
414
- { name: pkName, type: pkTsType },
415
- {
416
- name: 'options',
417
- type: 'ExecuteOptions',
418
- hasQuestionToken: true,
419
- },
420
- ],
421
- returnType: 'Promise<void>',
422
- statements: `await queryClient.prefetchQuery({
423
- queryKey: ${queryName}QueryKey(${pkName}),
424
- queryFn: () => execute<${ucFirst(singularName)}QueryResult, ${ucFirst(singularName)}QueryVariables>(
425
- ${queryName}QueryDocument,
426
- { ${pkName} },
427
- options
428
- ),
429
- });`,
430
- docs: [
431
- {
432
- description: `Prefetch a single ${typeName} for SSR or cache warming
433
-
434
- @example
435
- \`\`\`ts
436
- await prefetch${ucFirst(singularName)}Query(queryClient, ${pkTsType === 'string' ? "'value-here'" : '123'});
437
- \`\`\``,
438
- },
439
- ],
440
- });
631
+ const prefetchParams = [
632
+ typedParam('queryClient', t.tsTypeReference(t.identifier('QueryClient'))),
633
+ typedParam('variables', t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`))),
634
+ ];
635
+ if (hasRelationships && useCentralizedKeys) {
636
+ prefetchParams.push(typedParam('scope', t.tsTypeReference(t.identifier(scopeTypeName)), true));
637
+ }
638
+ prefetchParams.push(typedParam('options', t.tsTypeReference(t.identifier('ExecuteOptions')), true));
639
+ let prefetchQueryKeyExpr;
640
+ if (hasRelationships && useCentralizedKeys) {
641
+ prefetchQueryKeyExpr = t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('detail')), [
642
+ t.memberExpression(t.identifier('variables'), t.identifier(pkName)),
643
+ t.identifier('scope'),
644
+ ]);
645
+ }
646
+ else if (useCentralizedKeys) {
647
+ prefetchQueryKeyExpr = t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('detail')), [t.memberExpression(t.identifier('variables'), t.identifier(pkName))]);
648
+ }
649
+ else {
650
+ prefetchQueryKeyExpr = t.callExpression(t.identifier(`${queryName}QueryKey`), [t.memberExpression(t.identifier('variables'), t.identifier(pkName))]);
651
+ }
652
+ const prefetchFuncBody = t.blockStatement([
653
+ t.expressionStatement(t.awaitExpression(t.callExpression(t.memberExpression(t.identifier('queryClient'), t.identifier('prefetchQuery')), [
654
+ t.objectExpression([
655
+ t.objectProperty(t.identifier('queryKey'), prefetchQueryKeyExpr),
656
+ t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables'), t.identifier('options')], [
657
+ t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryResult`)),
658
+ t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`)),
659
+ ]))),
660
+ ]),
661
+ ]))),
662
+ ]);
663
+ const prefetchFunc = t.functionDeclaration(t.identifier(`prefetch${ucFirst(singularName)}Query`), prefetchParams, prefetchFuncBody);
664
+ prefetchFunc.async = true;
665
+ prefetchFunc.returnType = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('Promise'), t.tsTypeParameterInstantiation([t.tsVoidKeyword()])));
666
+ const prefetchExport = t.exportNamedDeclaration(prefetchFunc);
667
+ addJSDocComment(prefetchExport, [
668
+ `Prefetch a single ${typeName} for SSR or cache warming`,
669
+ '',
670
+ '@example',
671
+ '```ts',
672
+ `await prefetch${ucFirst(singularName)}Query(queryClient, { ${pkName}: 'some-id' });`,
673
+ '```',
674
+ ]);
675
+ statements.push(prefetchExport);
441
676
  }
677
+ const code = generateCode(statements);
678
+ const headerText = reactQueryEnabled
679
+ ? `Single item query hook for ${typeName}`
680
+ : `Single item query functions for ${typeName}`;
681
+ const content = getGeneratedFileHeader(headerText) + '\n\n' + code;
442
682
  return {
443
683
  fileName: getSingleQueryFileName(table),
444
- content: getFormattedOutput(sourceFile),
684
+ content,
445
685
  };
446
686
  }
447
- // ============================================================================
448
- // Batch generator
449
- // ============================================================================
450
- /**
451
- * Generate all query hook files for all tables
452
- */
453
687
  export function generateAllQueryHooks(tables, options = {}) {
454
688
  const files = [];
455
689
  for (const table of tables) {
456
690
  files.push(generateListQueryHook(table, options));
457
- files.push(generateSingleQueryHook(table, options));
691
+ const singleHook = generateSingleQueryHook(table, options);
692
+ if (singleHook) {
693
+ files.push(singleHook);
694
+ }
458
695
  }
459
696
  return files;
460
697
  }