@constructive-io/graphql-codegen 2.23.2 → 2.24.0

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