@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,46 +1,32 @@
1
- import { createProject, createSourceFile, getFormattedOutput, createFileHeader, createImport, createInterface, createConst, } from './ts-ast';
1
+ import * as t from '@babel/types';
2
+ import { generateCode, addJSDocComment, typedParam } from './babel-ast';
2
3
  import { buildCreateMutationAST, buildUpdateMutationAST, buildDeleteMutationAST, printGraphQL, } from './gql-ast';
3
- import { getTableNames, getCreateMutationHookName, getUpdateMutationHookName, getDeleteMutationHookName, getCreateMutationFileName, getUpdateMutationFileName, getDeleteMutationFileName, getCreateMutationName, getUpdateMutationName, getDeleteMutationName, getScalarFields, getPrimaryKeyInfo, fieldTypeToTs, ucFirst, lcFirst, } from './utils';
4
- /**
5
- * Check if a field is auto-generated and should be excluded from create inputs
6
- * Uses primary key from constraints and common timestamp patterns
7
- */
4
+ import { getTableNames, getCreateMutationHookName, getUpdateMutationHookName, getDeleteMutationHookName, getCreateMutationFileName, getUpdateMutationFileName, getDeleteMutationFileName, getCreateMutationName, getUpdateMutationName, getDeleteMutationName, getScalarFields, getPrimaryKeyInfo, fieldTypeToTs, ucFirst, lcFirst, getGeneratedFileHeader, } from './utils';
8
5
  function isAutoGeneratedField(fieldName, pkFieldNames) {
9
6
  const name = fieldName.toLowerCase();
10
- // Exclude primary key fields (from constraints)
11
7
  if (pkFieldNames.has(fieldName))
12
8
  return true;
13
- // Exclude common timestamp patterns (case-insensitive)
14
- // These are typically auto-set by database triggers or defaults
15
9
  const timestampPatterns = [
16
10
  'createdat', 'created_at', 'createddate', 'created_date',
17
11
  'updatedat', 'updated_at', 'updateddate', 'updated_date',
18
- 'deletedat', 'deleted_at', // soft delete timestamps
12
+ 'deletedat', 'deleted_at',
19
13
  ];
20
14
  return timestampPatterns.includes(name);
21
15
  }
22
- // ============================================================================
23
- // Create mutation hook generator
24
- // ============================================================================
25
- /**
26
- * Generate create mutation hook file content using AST
27
- * When reactQueryEnabled is false, returns null since mutations require React Query
28
- */
29
16
  export function generateCreateMutationHook(table, options = {}) {
30
- const { reactQueryEnabled = true, enumsFromSchemaTypes = [] } = options;
31
- // Mutations require React Query - skip generation when disabled
17
+ const { reactQueryEnabled = true, enumsFromSchemaTypes = [], useCentralizedKeys = true, hasRelationships = false, } = options;
32
18
  if (!reactQueryEnabled) {
33
19
  return null;
34
20
  }
35
21
  const enumSet = new Set(enumsFromSchemaTypes);
36
- const project = createProject();
37
22
  const { typeName, singularName } = getTableNames(table);
38
23
  const hookName = getCreateMutationHookName(table);
24
+ const keysName = `${lcFirst(typeName)}Keys`;
25
+ const mutationKeysName = `${lcFirst(typeName)}MutationKeys`;
26
+ const scopeTypeName = `${typeName}Scope`;
39
27
  const mutationName = getCreateMutationName(table);
40
28
  const scalarFields = getScalarFields(table);
41
- // Get primary key field names dynamically from table constraints
42
- const pkFieldNames = new Set(getPrimaryKeyInfo(table).map(pk => pk.name));
43
- // Collect which enums are used by this table's fields
29
+ const pkFieldNames = new Set(getPrimaryKeyInfo(table).map((pk) => pk.name));
44
30
  const usedEnums = new Set();
45
31
  for (const field of scalarFields) {
46
32
  const cleanType = field.type.gqlType.replace(/!/g, '');
@@ -48,161 +34,141 @@ export function generateCreateMutationHook(table, options = {}) {
48
34
  usedEnums.add(cleanType);
49
35
  }
50
36
  }
51
- // Generate GraphQL document via AST
52
37
  const mutationAST = buildCreateMutationAST({ table });
53
38
  const mutationDocument = printGraphQL(mutationAST);
54
- const sourceFile = createSourceFile(project, getCreateMutationFileName(table));
55
- // Add file header
56
- sourceFile.insertText(0, createFileHeader(`Create mutation hook for ${typeName}`) + '\n\n');
57
- // Build import declarations
58
- const imports = [
59
- createImport({
60
- moduleSpecifier: '@tanstack/react-query',
61
- namedImports: ['useMutation', 'useQueryClient'],
62
- typeOnlyNamedImports: ['UseMutationOptions'],
63
- }),
64
- createImport({
65
- moduleSpecifier: '../client',
66
- namedImports: ['execute'],
67
- }),
68
- createImport({
69
- moduleSpecifier: '../types',
70
- typeOnlyNamedImports: [typeName],
71
- }),
72
- ];
73
- // Add import for enum types from schema-types if any are used
39
+ const statements = [];
40
+ const reactQueryImport = t.importDeclaration([
41
+ t.importSpecifier(t.identifier('useMutation'), t.identifier('useMutation')),
42
+ t.importSpecifier(t.identifier('useQueryClient'), t.identifier('useQueryClient')),
43
+ ], t.stringLiteral('@tanstack/react-query'));
44
+ statements.push(reactQueryImport);
45
+ const reactQueryTypeImport = t.importDeclaration([t.importSpecifier(t.identifier('UseMutationOptions'), t.identifier('UseMutationOptions'))], t.stringLiteral('@tanstack/react-query'));
46
+ reactQueryTypeImport.importKind = 'type';
47
+ statements.push(reactQueryTypeImport);
48
+ const clientImport = t.importDeclaration([t.importSpecifier(t.identifier('execute'), t.identifier('execute'))], t.stringLiteral('../client'));
49
+ statements.push(clientImport);
50
+ const typesImport = t.importDeclaration([t.importSpecifier(t.identifier(typeName), t.identifier(typeName))], t.stringLiteral('../types'));
51
+ typesImport.importKind = 'type';
52
+ statements.push(typesImport);
74
53
  if (usedEnums.size > 0) {
75
- imports.push(createImport({
76
- moduleSpecifier: '../schema-types',
77
- typeOnlyNamedImports: Array.from(usedEnums).sort(),
78
- }));
54
+ const enumImport = t.importDeclaration(Array.from(usedEnums).sort().map((e) => t.importSpecifier(t.identifier(e), t.identifier(e))), t.stringLiteral('../schema-types'));
55
+ enumImport.importKind = 'type';
56
+ statements.push(enumImport);
57
+ }
58
+ if (useCentralizedKeys) {
59
+ const queryKeyImport = t.importDeclaration([t.importSpecifier(t.identifier(keysName), t.identifier(keysName))], t.stringLiteral('../query-keys'));
60
+ statements.push(queryKeyImport);
61
+ if (hasRelationships) {
62
+ const scopeTypeImport = t.importDeclaration([t.importSpecifier(t.identifier(scopeTypeName), t.identifier(scopeTypeName))], t.stringLiteral('../query-keys'));
63
+ scopeTypeImport.importKind = 'type';
64
+ statements.push(scopeTypeImport);
65
+ }
66
+ const mutationKeyImport = t.importDeclaration([t.importSpecifier(t.identifier(mutationKeysName), t.identifier(mutationKeysName))], t.stringLiteral('../mutation-keys'));
67
+ statements.push(mutationKeyImport);
79
68
  }
80
- // Add imports
81
- sourceFile.addImportDeclarations(imports);
82
- // Re-export entity type
83
- sourceFile.addStatements(`\n// Re-export entity type for convenience\nexport type { ${typeName} };\n`);
84
- // Add section comment
85
- sourceFile.addStatements('\n// ============================================================================');
86
- sourceFile.addStatements('// GraphQL Document');
87
- sourceFile.addStatements('// ============================================================================\n');
88
- // Add mutation document constant
89
- sourceFile.addVariableStatement(createConst(`${mutationName}MutationDocument`, '`\n' + mutationDocument + '`'));
90
- // Add section comment
91
- sourceFile.addStatements('\n// ============================================================================');
92
- sourceFile.addStatements('// Types');
93
- sourceFile.addStatements('// ============================================================================\n');
94
- // Generate CreateInput type - exclude auto-generated fields
95
- // Note: Not exported to avoid conflicts with schema-types
69
+ const reExportDecl = t.exportNamedDeclaration(null, [t.exportSpecifier(t.identifier(typeName), t.identifier(typeName))], t.stringLiteral('../types'));
70
+ reExportDecl.exportKind = 'type';
71
+ statements.push(reExportDecl);
72
+ const mutationDocConst = t.variableDeclaration('const', [
73
+ t.variableDeclarator(t.identifier(`${mutationName}MutationDocument`), t.templateLiteral([t.templateElement({ raw: '\n' + mutationDocument, cooked: '\n' + mutationDocument }, true)], [])),
74
+ ]);
75
+ statements.push(t.exportNamedDeclaration(mutationDocConst));
96
76
  const inputFields = scalarFields
97
77
  .filter((f) => !isAutoGeneratedField(f.name, pkFieldNames))
98
- .map((f) => ({
99
- name: f.name,
100
- type: `${fieldTypeToTs(f.type)} | null`,
101
- optional: true,
102
- }));
103
- sourceFile.addInterface(createInterface(`${typeName}CreateInput`, inputFields, {
104
- docs: [`Input type for creating a ${typeName}`],
105
- isExported: false,
106
- }));
107
- // Variables interface
108
- sourceFile.addInterface(createInterface(`${ucFirst(mutationName)}MutationVariables`, [
109
- {
110
- name: 'input',
111
- type: `{
112
- ${lcFirst(typeName)}: ${typeName}CreateInput;
113
- }`,
114
- },
115
- ]));
116
- // Result interface
117
- sourceFile.addInterface(createInterface(`${ucFirst(mutationName)}MutationResult`, [
118
- {
119
- name: mutationName,
120
- type: `{
121
- ${singularName}: ${typeName};
122
- }`,
123
- },
124
- ]));
125
- // Add section comment
126
- sourceFile.addStatements('\n// ============================================================================');
127
- sourceFile.addStatements('// Hook');
128
- sourceFile.addStatements('// ============================================================================\n');
129
- // Hook function
130
- sourceFile.addFunction({
131
- name: hookName,
132
- isExported: true,
133
- parameters: [
134
- {
135
- name: 'options',
136
- type: `Omit<UseMutationOptions<${ucFirst(mutationName)}MutationResult, Error, ${ucFirst(mutationName)}MutationVariables>, 'mutationFn'>`,
137
- hasQuestionToken: true,
138
- },
139
- ],
140
- statements: `const queryClient = useQueryClient();
141
-
142
- return useMutation({
143
- mutationFn: (variables: ${ucFirst(mutationName)}MutationVariables) =>
144
- execute<${ucFirst(mutationName)}MutationResult, ${ucFirst(mutationName)}MutationVariables>(
145
- ${mutationName}MutationDocument,
146
- variables
147
- ),
148
- onSuccess: () => {
149
- // Invalidate list queries to refetch
150
- queryClient.invalidateQueries({ queryKey: ['${typeName.toLowerCase()}', 'list'] });
151
- },
152
- ...options,
153
- });`,
154
- docs: [
155
- {
156
- description: `Mutation hook for creating a ${typeName}
157
-
158
- @example
159
- \`\`\`tsx
160
- const { mutate, isPending } = ${hookName}();
161
-
162
- mutate({
163
- input: {
164
- ${lcFirst(typeName)}: {
165
- // ... fields
166
- },
167
- },
168
- });
169
- \`\`\``,
170
- },
171
- ],
78
+ .map((f) => {
79
+ const prop = t.tsPropertySignature(t.identifier(f.name), t.tsTypeAnnotation(t.tsUnionType([
80
+ t.tsTypeReference(t.identifier(fieldTypeToTs(f.type))),
81
+ t.tsNullKeyword(),
82
+ ])));
83
+ prop.optional = true;
84
+ return prop;
172
85
  });
86
+ const createInputInterface = t.tsInterfaceDeclaration(t.identifier(`${typeName}CreateInput`), null, null, t.tsInterfaceBody(inputFields));
87
+ addJSDocComment(createInputInterface, [`Input type for creating a ${typeName}`]);
88
+ statements.push(createInputInterface);
89
+ const variablesInterfaceBody = t.tsInterfaceBody([
90
+ t.tsPropertySignature(t.identifier('input'), t.tsTypeAnnotation(t.tsTypeLiteral([
91
+ t.tsPropertySignature(t.identifier(lcFirst(typeName)), t.tsTypeAnnotation(t.tsTypeReference(t.identifier(`${typeName}CreateInput`)))),
92
+ ]))),
93
+ ]);
94
+ const variablesInterface = t.tsInterfaceDeclaration(t.identifier(`${ucFirst(mutationName)}MutationVariables`), null, null, variablesInterfaceBody);
95
+ statements.push(t.exportNamedDeclaration(variablesInterface));
96
+ const resultInterfaceBody = t.tsInterfaceBody([
97
+ t.tsPropertySignature(t.identifier(mutationName), t.tsTypeAnnotation(t.tsTypeLiteral([
98
+ t.tsPropertySignature(t.identifier(singularName), t.tsTypeAnnotation(t.tsTypeReference(t.identifier(typeName)))),
99
+ ]))),
100
+ ]);
101
+ const resultInterface = t.tsInterfaceDeclaration(t.identifier(`${ucFirst(mutationName)}MutationResult`), null, null, resultInterfaceBody);
102
+ statements.push(t.exportNamedDeclaration(resultInterface));
103
+ const hookBodyStatements = [];
104
+ hookBodyStatements.push(t.variableDeclaration('const', [
105
+ t.variableDeclarator(t.identifier('queryClient'), t.callExpression(t.identifier('useQueryClient'), [])),
106
+ ]));
107
+ const mutationOptions = [];
108
+ if (useCentralizedKeys) {
109
+ mutationOptions.push(t.objectProperty(t.identifier('mutationKey'), t.callExpression(t.memberExpression(t.identifier(mutationKeysName), t.identifier('create')), [])));
110
+ }
111
+ mutationOptions.push(t.objectProperty(t.identifier('mutationFn'), t.arrowFunctionExpression([typedParam('variables', t.tsTypeReference(t.identifier(`${ucFirst(mutationName)}MutationVariables`)))], t.callExpression(t.identifier('execute'), [
112
+ t.identifier(`${mutationName}MutationDocument`),
113
+ t.identifier('variables'),
114
+ ]))));
115
+ const invalidateQueryKey = useCentralizedKeys
116
+ ? t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('lists')), [])
117
+ : t.arrayExpression([t.stringLiteral(typeName.toLowerCase()), t.stringLiteral('list')]);
118
+ mutationOptions.push(t.objectProperty(t.identifier('onSuccess'), t.arrowFunctionExpression([], t.blockStatement([
119
+ t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('queryClient'), t.identifier('invalidateQueries')), [t.objectExpression([t.objectProperty(t.identifier('queryKey'), invalidateQueryKey)])])),
120
+ ]))));
121
+ mutationOptions.push(t.spreadElement(t.identifier('options')));
122
+ hookBodyStatements.push(t.returnStatement(t.callExpression(t.identifier('useMutation'), [t.objectExpression(mutationOptions)])));
123
+ const optionsTypeStr = `Omit<UseMutationOptions<${ucFirst(mutationName)}MutationResult, Error, ${ucFirst(mutationName)}MutationVariables>, 'mutationFn'>`;
124
+ const optionsParam = t.identifier('options');
125
+ optionsParam.optional = true;
126
+ optionsParam.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier(optionsTypeStr)));
127
+ const hookFunc = t.functionDeclaration(t.identifier(hookName), [optionsParam], t.blockStatement(hookBodyStatements));
128
+ const hookExport = t.exportNamedDeclaration(hookFunc);
129
+ addJSDocComment(hookExport, [
130
+ `Mutation hook for creating a ${typeName}`,
131
+ '',
132
+ '@example',
133
+ '```tsx',
134
+ `const { mutate, isPending } = ${hookName}();`,
135
+ '',
136
+ 'mutate({',
137
+ ' input: {',
138
+ ` ${lcFirst(typeName)}: {`,
139
+ ' // ... fields',
140
+ ' },',
141
+ ' },',
142
+ '});',
143
+ '```',
144
+ ]);
145
+ statements.push(hookExport);
146
+ const code = generateCode(statements);
147
+ const content = getGeneratedFileHeader(`Create mutation hook for ${typeName}`) + '\n\n' + code;
173
148
  return {
174
149
  fileName: getCreateMutationFileName(table),
175
- content: getFormattedOutput(sourceFile),
150
+ content,
176
151
  };
177
152
  }
178
- // ============================================================================
179
- // Update mutation hook generator
180
- // ============================================================================
181
- /**
182
- * Generate update mutation hook file content using AST
183
- * When reactQueryEnabled is false, returns null since mutations require React Query
184
- */
185
153
  export function generateUpdateMutationHook(table, options = {}) {
186
- const { reactQueryEnabled = true, enumsFromSchemaTypes = [] } = options;
187
- // Mutations require React Query - skip generation when disabled
154
+ const { reactQueryEnabled = true, enumsFromSchemaTypes = [], useCentralizedKeys = true, hasRelationships = false, } = options;
188
155
  if (!reactQueryEnabled) {
189
156
  return null;
190
157
  }
191
- // Check if update mutation exists
192
158
  if (table.query?.update === null) {
193
159
  return null;
194
160
  }
195
161
  const enumSet = new Set(enumsFromSchemaTypes);
196
- const project = createProject();
197
162
  const { typeName, singularName } = getTableNames(table);
198
163
  const hookName = getUpdateMutationHookName(table);
199
164
  const mutationName = getUpdateMutationName(table);
200
165
  const scalarFields = getScalarFields(table);
201
- // Get primary key info dynamically from table constraints
166
+ const keysName = `${lcFirst(typeName)}Keys`;
167
+ const mutationKeysName = `${lcFirst(typeName)}MutationKeys`;
168
+ const scopeTypeName = `${typeName}Scope`;
202
169
  const pkFields = getPrimaryKeyInfo(table);
203
- const pkField = pkFields[0]; // Use first PK field
204
- const pkFieldNames = new Set(pkFields.map(pk => pk.name));
205
- // Collect which enums are used by this table's fields
170
+ const pkField = pkFields[0];
171
+ const pkFieldNames = new Set(pkFields.map((pk) => pk.name));
206
172
  const usedEnums = new Set();
207
173
  for (const field of scalarFields) {
208
174
  const cleanType = field.type.gqlType.replace(/!/g, '');
@@ -210,266 +176,256 @@ export function generateUpdateMutationHook(table, options = {}) {
210
176
  usedEnums.add(cleanType);
211
177
  }
212
178
  }
213
- // Generate GraphQL document via AST
214
179
  const mutationAST = buildUpdateMutationAST({ table });
215
180
  const mutationDocument = printGraphQL(mutationAST);
216
- const sourceFile = createSourceFile(project, getUpdateMutationFileName(table));
217
- // Add file header
218
- sourceFile.insertText(0, createFileHeader(`Update mutation hook for ${typeName}`) + '\n\n');
219
- // Build import declarations
220
- const imports = [
221
- createImport({
222
- moduleSpecifier: '@tanstack/react-query',
223
- namedImports: ['useMutation', 'useQueryClient'],
224
- typeOnlyNamedImports: ['UseMutationOptions'],
225
- }),
226
- createImport({
227
- moduleSpecifier: '../client',
228
- namedImports: ['execute'],
229
- }),
230
- createImport({
231
- moduleSpecifier: '../types',
232
- typeOnlyNamedImports: [typeName],
233
- }),
234
- ];
235
- // Add import for enum types from schema-types if any are used
181
+ const statements = [];
182
+ const reactQueryImport = t.importDeclaration([
183
+ t.importSpecifier(t.identifier('useMutation'), t.identifier('useMutation')),
184
+ t.importSpecifier(t.identifier('useQueryClient'), t.identifier('useQueryClient')),
185
+ ], t.stringLiteral('@tanstack/react-query'));
186
+ statements.push(reactQueryImport);
187
+ const reactQueryTypeImport = t.importDeclaration([t.importSpecifier(t.identifier('UseMutationOptions'), t.identifier('UseMutationOptions'))], t.stringLiteral('@tanstack/react-query'));
188
+ reactQueryTypeImport.importKind = 'type';
189
+ statements.push(reactQueryTypeImport);
190
+ const clientImport = t.importDeclaration([t.importSpecifier(t.identifier('execute'), t.identifier('execute'))], t.stringLiteral('../client'));
191
+ statements.push(clientImport);
192
+ const typesImport = t.importDeclaration([t.importSpecifier(t.identifier(typeName), t.identifier(typeName))], t.stringLiteral('../types'));
193
+ typesImport.importKind = 'type';
194
+ statements.push(typesImport);
236
195
  if (usedEnums.size > 0) {
237
- imports.push(createImport({
238
- moduleSpecifier: '../schema-types',
239
- typeOnlyNamedImports: Array.from(usedEnums).sort(),
240
- }));
196
+ const enumImport = t.importDeclaration(Array.from(usedEnums).sort().map((e) => t.importSpecifier(t.identifier(e), t.identifier(e))), t.stringLiteral('../schema-types'));
197
+ enumImport.importKind = 'type';
198
+ statements.push(enumImport);
241
199
  }
242
- // Add imports
243
- sourceFile.addImportDeclarations(imports);
244
- // Re-export entity type
245
- sourceFile.addStatements(`\n// Re-export entity type for convenience\nexport type { ${typeName} };\n`);
246
- // Add section comment
247
- sourceFile.addStatements('\n// ============================================================================');
248
- sourceFile.addStatements('// GraphQL Document');
249
- sourceFile.addStatements('// ============================================================================\n');
250
- // Add mutation document constant
251
- sourceFile.addVariableStatement(createConst(`${mutationName}MutationDocument`, '`\n' + mutationDocument + '`'));
252
- // Add section comment
253
- sourceFile.addStatements('\n// ============================================================================');
254
- sourceFile.addStatements('// Types');
255
- sourceFile.addStatements('// ============================================================================\n');
256
- // Generate Patch type - all fields optional, exclude primary key
257
- // Note: Not exported to avoid conflicts with schema-types
200
+ if (useCentralizedKeys) {
201
+ const queryKeyImport = t.importDeclaration([t.importSpecifier(t.identifier(keysName), t.identifier(keysName))], t.stringLiteral('../query-keys'));
202
+ statements.push(queryKeyImport);
203
+ if (hasRelationships) {
204
+ const scopeTypeImport = t.importDeclaration([t.importSpecifier(t.identifier(scopeTypeName), t.identifier(scopeTypeName))], t.stringLiteral('../query-keys'));
205
+ scopeTypeImport.importKind = 'type';
206
+ statements.push(scopeTypeImport);
207
+ }
208
+ const mutationKeyImport = t.importDeclaration([t.importSpecifier(t.identifier(mutationKeysName), t.identifier(mutationKeysName))], t.stringLiteral('../mutation-keys'));
209
+ statements.push(mutationKeyImport);
210
+ }
211
+ const reExportDecl = t.exportNamedDeclaration(null, [t.exportSpecifier(t.identifier(typeName), t.identifier(typeName))], t.stringLiteral('../types'));
212
+ reExportDecl.exportKind = 'type';
213
+ statements.push(reExportDecl);
214
+ const mutationDocConst = t.variableDeclaration('const', [
215
+ t.variableDeclarator(t.identifier(`${mutationName}MutationDocument`), t.templateLiteral([t.templateElement({ raw: '\n' + mutationDocument, cooked: '\n' + mutationDocument }, true)], [])),
216
+ ]);
217
+ statements.push(t.exportNamedDeclaration(mutationDocConst));
258
218
  const patchFields = scalarFields
259
219
  .filter((f) => !pkFieldNames.has(f.name))
260
- .map((f) => ({
261
- name: f.name,
262
- type: `${fieldTypeToTs(f.type)} | null`,
263
- optional: true,
264
- }));
265
- sourceFile.addInterface(createInterface(`${typeName}Patch`, patchFields, {
266
- docs: [`Patch type for updating a ${typeName} - all fields optional`],
267
- isExported: false,
268
- }));
269
- // Variables interface - use dynamic PK field name and type
270
- sourceFile.addInterface(createInterface(`${ucFirst(mutationName)}MutationVariables`, [
271
- {
272
- name: 'input',
273
- type: `{
274
- ${pkField.name}: ${pkField.tsType};
275
- patch: ${typeName}Patch;
276
- }`,
277
- },
278
- ]));
279
- // Result interface
280
- sourceFile.addInterface(createInterface(`${ucFirst(mutationName)}MutationResult`, [
281
- {
282
- name: mutationName,
283
- type: `{
284
- ${singularName}: ${typeName};
285
- }`,
286
- },
287
- ]));
288
- // Add section comment
289
- sourceFile.addStatements('\n// ============================================================================');
290
- sourceFile.addStatements('// Hook');
291
- sourceFile.addStatements('// ============================================================================\n');
292
- // Hook function
293
- sourceFile.addFunction({
294
- name: hookName,
295
- isExported: true,
296
- parameters: [
297
- {
298
- name: 'options',
299
- type: `Omit<UseMutationOptions<${ucFirst(mutationName)}MutationResult, Error, ${ucFirst(mutationName)}MutationVariables>, 'mutationFn'>`,
300
- hasQuestionToken: true,
301
- },
302
- ],
303
- statements: `const queryClient = useQueryClient();
304
-
305
- return useMutation({
306
- mutationFn: (variables: ${ucFirst(mutationName)}MutationVariables) =>
307
- execute<${ucFirst(mutationName)}MutationResult, ${ucFirst(mutationName)}MutationVariables>(
308
- ${mutationName}MutationDocument,
309
- variables
310
- ),
311
- onSuccess: (_, variables) => {
312
- // Invalidate specific item and list queries
313
- queryClient.invalidateQueries({ queryKey: ['${typeName.toLowerCase()}', 'detail', variables.input.${pkField.name}] });
314
- queryClient.invalidateQueries({ queryKey: ['${typeName.toLowerCase()}', 'list'] });
315
- },
316
- ...options,
317
- });`,
318
- docs: [
319
- {
320
- description: `Mutation hook for updating a ${typeName}
321
-
322
- @example
323
- \`\`\`tsx
324
- const { mutate, isPending } = ${hookName}();
325
-
326
- mutate({
327
- input: {
328
- ${pkField.name}: ${pkField.tsType === 'string' ? "'value-here'" : '123'},
329
- patch: {
330
- // ... fields to update
331
- },
332
- },
333
- });
334
- \`\`\``,
335
- },
336
- ],
220
+ .map((f) => {
221
+ const prop = t.tsPropertySignature(t.identifier(f.name), t.tsTypeAnnotation(t.tsUnionType([
222
+ t.tsTypeReference(t.identifier(fieldTypeToTs(f.type))),
223
+ t.tsNullKeyword(),
224
+ ])));
225
+ prop.optional = true;
226
+ return prop;
337
227
  });
228
+ const patchInterface = t.tsInterfaceDeclaration(t.identifier(`${typeName}Patch`), null, null, t.tsInterfaceBody(patchFields));
229
+ addJSDocComment(patchInterface, [`Patch type for updating a ${typeName} - all fields optional`]);
230
+ statements.push(patchInterface);
231
+ const pkTypeAnnotation = pkField.tsType === 'string'
232
+ ? t.tsStringKeyword()
233
+ : pkField.tsType === 'number'
234
+ ? t.tsNumberKeyword()
235
+ : t.tsTypeReference(t.identifier(pkField.tsType));
236
+ const variablesInterfaceBody = t.tsInterfaceBody([
237
+ t.tsPropertySignature(t.identifier('input'), t.tsTypeAnnotation(t.tsTypeLiteral([
238
+ t.tsPropertySignature(t.identifier(pkField.name), t.tsTypeAnnotation(pkTypeAnnotation)),
239
+ t.tsPropertySignature(t.identifier('patch'), t.tsTypeAnnotation(t.tsTypeReference(t.identifier(`${typeName}Patch`)))),
240
+ ]))),
241
+ ]);
242
+ const variablesInterface = t.tsInterfaceDeclaration(t.identifier(`${ucFirst(mutationName)}MutationVariables`), null, null, variablesInterfaceBody);
243
+ statements.push(t.exportNamedDeclaration(variablesInterface));
244
+ const resultInterfaceBody = t.tsInterfaceBody([
245
+ t.tsPropertySignature(t.identifier(mutationName), t.tsTypeAnnotation(t.tsTypeLiteral([
246
+ t.tsPropertySignature(t.identifier(singularName), t.tsTypeAnnotation(t.tsTypeReference(t.identifier(typeName)))),
247
+ ]))),
248
+ ]);
249
+ const resultInterface = t.tsInterfaceDeclaration(t.identifier(`${ucFirst(mutationName)}MutationResult`), null, null, resultInterfaceBody);
250
+ statements.push(t.exportNamedDeclaration(resultInterface));
251
+ const hookBodyStatements = [];
252
+ hookBodyStatements.push(t.variableDeclaration('const', [
253
+ t.variableDeclarator(t.identifier('queryClient'), t.callExpression(t.identifier('useQueryClient'), [])),
254
+ ]));
255
+ const mutationOptions = [];
256
+ if (useCentralizedKeys) {
257
+ mutationOptions.push(t.objectProperty(t.identifier('mutationKey'), t.memberExpression(t.identifier(mutationKeysName), t.identifier('all'))));
258
+ }
259
+ mutationOptions.push(t.objectProperty(t.identifier('mutationFn'), t.arrowFunctionExpression([typedParam('variables', t.tsTypeReference(t.identifier(`${ucFirst(mutationName)}MutationVariables`)))], t.callExpression(t.identifier('execute'), [
260
+ t.identifier(`${mutationName}MutationDocument`),
261
+ t.identifier('variables'),
262
+ ]))));
263
+ const detailQueryKey = useCentralizedKeys
264
+ ? t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('detail')), [t.memberExpression(t.memberExpression(t.identifier('variables'), t.identifier('input')), t.identifier(pkField.name))])
265
+ : t.arrayExpression([
266
+ t.stringLiteral(typeName.toLowerCase()),
267
+ t.stringLiteral('detail'),
268
+ t.memberExpression(t.memberExpression(t.identifier('variables'), t.identifier('input')), t.identifier(pkField.name)),
269
+ ]);
270
+ const listQueryKey = useCentralizedKeys
271
+ ? t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('lists')), [])
272
+ : t.arrayExpression([t.stringLiteral(typeName.toLowerCase()), t.stringLiteral('list')]);
273
+ mutationOptions.push(t.objectProperty(t.identifier('onSuccess'), t.arrowFunctionExpression([t.identifier('_'), t.identifier('variables')], t.blockStatement([
274
+ t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('queryClient'), t.identifier('invalidateQueries')), [t.objectExpression([t.objectProperty(t.identifier('queryKey'), detailQueryKey)])])),
275
+ t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('queryClient'), t.identifier('invalidateQueries')), [t.objectExpression([t.objectProperty(t.identifier('queryKey'), listQueryKey)])])),
276
+ ]))));
277
+ mutationOptions.push(t.spreadElement(t.identifier('options')));
278
+ hookBodyStatements.push(t.returnStatement(t.callExpression(t.identifier('useMutation'), [t.objectExpression(mutationOptions)])));
279
+ const optionsTypeStr = `Omit<UseMutationOptions<${ucFirst(mutationName)}MutationResult, Error, ${ucFirst(mutationName)}MutationVariables>, 'mutationFn'>`;
280
+ const optionsParam = t.identifier('options');
281
+ optionsParam.optional = true;
282
+ optionsParam.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier(optionsTypeStr)));
283
+ const hookFunc = t.functionDeclaration(t.identifier(hookName), [optionsParam], t.blockStatement(hookBodyStatements));
284
+ const hookExport = t.exportNamedDeclaration(hookFunc);
285
+ addJSDocComment(hookExport, [
286
+ `Mutation hook for updating a ${typeName}`,
287
+ '',
288
+ '@example',
289
+ '```tsx',
290
+ `const { mutate, isPending } = ${hookName}();`,
291
+ '',
292
+ 'mutate({',
293
+ ' input: {',
294
+ ` ${pkField.name}: ${pkField.tsType === 'string' ? "'value-here'" : '123'},`,
295
+ ' patch: {',
296
+ ' // ... fields to update',
297
+ ' },',
298
+ ' },',
299
+ '});',
300
+ '```',
301
+ ]);
302
+ statements.push(hookExport);
303
+ const code = generateCode(statements);
304
+ const content = getGeneratedFileHeader(`Update mutation hook for ${typeName}`) + '\n\n' + code;
338
305
  return {
339
306
  fileName: getUpdateMutationFileName(table),
340
- content: getFormattedOutput(sourceFile),
307
+ content,
341
308
  };
342
309
  }
343
- // ============================================================================
344
- // Delete mutation hook generator
345
- // ============================================================================
346
- /**
347
- * Generate delete mutation hook file content using AST
348
- * When reactQueryEnabled is false, returns null since mutations require React Query
349
- */
350
310
  export function generateDeleteMutationHook(table, options = {}) {
351
- const { reactQueryEnabled = true } = options;
352
- // Mutations require React Query - skip generation when disabled
311
+ const { reactQueryEnabled = true, useCentralizedKeys = true, hasRelationships = false, } = options;
353
312
  if (!reactQueryEnabled) {
354
313
  return null;
355
314
  }
356
- // Check if delete mutation exists
357
315
  if (table.query?.delete === null) {
358
316
  return null;
359
317
  }
360
- const project = createProject();
361
318
  const { typeName } = getTableNames(table);
362
319
  const hookName = getDeleteMutationHookName(table);
363
320
  const mutationName = getDeleteMutationName(table);
364
- // Get primary key info dynamically from table constraints
321
+ const keysName = `${lcFirst(typeName)}Keys`;
322
+ const mutationKeysName = `${lcFirst(typeName)}MutationKeys`;
323
+ const scopeTypeName = `${typeName}Scope`;
365
324
  const pkFields = getPrimaryKeyInfo(table);
366
- const pkField = pkFields[0]; // Use first PK field
367
- // Generate GraphQL document via AST
325
+ const pkField = pkFields[0];
368
326
  const mutationAST = buildDeleteMutationAST({ table });
369
327
  const mutationDocument = printGraphQL(mutationAST);
370
- const sourceFile = createSourceFile(project, getDeleteMutationFileName(table));
371
- // Add file header
372
- sourceFile.insertText(0, createFileHeader(`Delete mutation hook for ${typeName}`) + '\n\n');
373
- // Add imports
374
- sourceFile.addImportDeclarations([
375
- createImport({
376
- moduleSpecifier: '@tanstack/react-query',
377
- namedImports: ['useMutation', 'useQueryClient'],
378
- typeOnlyNamedImports: ['UseMutationOptions'],
379
- }),
380
- createImport({
381
- moduleSpecifier: '../client',
382
- namedImports: ['execute'],
383
- }),
328
+ const statements = [];
329
+ const reactQueryImport = t.importDeclaration([
330
+ t.importSpecifier(t.identifier('useMutation'), t.identifier('useMutation')),
331
+ t.importSpecifier(t.identifier('useQueryClient'), t.identifier('useQueryClient')),
332
+ ], t.stringLiteral('@tanstack/react-query'));
333
+ statements.push(reactQueryImport);
334
+ const reactQueryTypeImport = t.importDeclaration([t.importSpecifier(t.identifier('UseMutationOptions'), t.identifier('UseMutationOptions'))], t.stringLiteral('@tanstack/react-query'));
335
+ reactQueryTypeImport.importKind = 'type';
336
+ statements.push(reactQueryTypeImport);
337
+ const clientImport = t.importDeclaration([t.importSpecifier(t.identifier('execute'), t.identifier('execute'))], t.stringLiteral('../client'));
338
+ statements.push(clientImport);
339
+ if (useCentralizedKeys) {
340
+ const queryKeyImport = t.importDeclaration([t.importSpecifier(t.identifier(keysName), t.identifier(keysName))], t.stringLiteral('../query-keys'));
341
+ statements.push(queryKeyImport);
342
+ if (hasRelationships) {
343
+ const scopeTypeImport = t.importDeclaration([t.importSpecifier(t.identifier(scopeTypeName), t.identifier(scopeTypeName))], t.stringLiteral('../query-keys'));
344
+ scopeTypeImport.importKind = 'type';
345
+ statements.push(scopeTypeImport);
346
+ }
347
+ const mutationKeyImport = t.importDeclaration([t.importSpecifier(t.identifier(mutationKeysName), t.identifier(mutationKeysName))], t.stringLiteral('../mutation-keys'));
348
+ statements.push(mutationKeyImport);
349
+ }
350
+ const mutationDocConst = t.variableDeclaration('const', [
351
+ t.variableDeclarator(t.identifier(`${mutationName}MutationDocument`), t.templateLiteral([t.templateElement({ raw: '\n' + mutationDocument, cooked: '\n' + mutationDocument }, true)], [])),
384
352
  ]);
385
- // Add section comment
386
- sourceFile.addStatements('\n// ============================================================================');
387
- sourceFile.addStatements('// GraphQL Document');
388
- sourceFile.addStatements('// ============================================================================\n');
389
- // Add mutation document constant
390
- sourceFile.addVariableStatement(createConst(`${mutationName}MutationDocument`, '`\n' + mutationDocument + '`'));
391
- // Add section comment
392
- sourceFile.addStatements('\n// ============================================================================');
393
- sourceFile.addStatements('// Types');
394
- sourceFile.addStatements('// ============================================================================\n');
395
- // Variables interface - use dynamic PK field name and type
396
- sourceFile.addInterface(createInterface(`${ucFirst(mutationName)}MutationVariables`, [
397
- {
398
- name: 'input',
399
- type: `{
400
- ${pkField.name}: ${pkField.tsType};
401
- }`,
402
- },
403
- ]));
404
- // Result interface
405
- sourceFile.addInterface(createInterface(`${ucFirst(mutationName)}MutationResult`, [
406
- {
407
- name: mutationName,
408
- type: `{
409
- clientMutationId: string | null;
410
- deleted${ucFirst(pkField.name)}: ${pkField.tsType} | null;
411
- }`,
412
- },
353
+ statements.push(t.exportNamedDeclaration(mutationDocConst));
354
+ const pkTypeAnnotation = pkField.tsType === 'string'
355
+ ? t.tsStringKeyword()
356
+ : pkField.tsType === 'number'
357
+ ? t.tsNumberKeyword()
358
+ : t.tsTypeReference(t.identifier(pkField.tsType));
359
+ const variablesInterfaceBody = t.tsInterfaceBody([
360
+ t.tsPropertySignature(t.identifier('input'), t.tsTypeAnnotation(t.tsTypeLiteral([
361
+ t.tsPropertySignature(t.identifier(pkField.name), t.tsTypeAnnotation(pkTypeAnnotation)),
362
+ ]))),
363
+ ]);
364
+ const variablesInterface = t.tsInterfaceDeclaration(t.identifier(`${ucFirst(mutationName)}MutationVariables`), null, null, variablesInterfaceBody);
365
+ statements.push(t.exportNamedDeclaration(variablesInterface));
366
+ const deletedPkProp = t.tsPropertySignature(t.identifier(`deleted${ucFirst(pkField.name)}`), t.tsTypeAnnotation(t.tsUnionType([pkTypeAnnotation, t.tsNullKeyword()])));
367
+ const clientMutationIdProp = t.tsPropertySignature(t.identifier('clientMutationId'), t.tsTypeAnnotation(t.tsUnionType([t.tsStringKeyword(), t.tsNullKeyword()])));
368
+ const resultInterfaceBody = t.tsInterfaceBody([
369
+ t.tsPropertySignature(t.identifier(mutationName), t.tsTypeAnnotation(t.tsTypeLiteral([clientMutationIdProp, deletedPkProp]))),
370
+ ]);
371
+ const resultInterface = t.tsInterfaceDeclaration(t.identifier(`${ucFirst(mutationName)}MutationResult`), null, null, resultInterfaceBody);
372
+ statements.push(t.exportNamedDeclaration(resultInterface));
373
+ const hookBodyStatements = [];
374
+ hookBodyStatements.push(t.variableDeclaration('const', [
375
+ t.variableDeclarator(t.identifier('queryClient'), t.callExpression(t.identifier('useQueryClient'), [])),
413
376
  ]));
414
- // Add section comment
415
- sourceFile.addStatements('\n// ============================================================================');
416
- sourceFile.addStatements('// Hook');
417
- sourceFile.addStatements('// ============================================================================\n');
418
- // Hook function
419
- sourceFile.addFunction({
420
- name: hookName,
421
- isExported: true,
422
- parameters: [
423
- {
424
- name: 'options',
425
- type: `Omit<UseMutationOptions<${ucFirst(mutationName)}MutationResult, Error, ${ucFirst(mutationName)}MutationVariables>, 'mutationFn'>`,
426
- hasQuestionToken: true,
427
- },
428
- ],
429
- statements: `const queryClient = useQueryClient();
430
-
431
- return useMutation({
432
- mutationFn: (variables: ${ucFirst(mutationName)}MutationVariables) =>
433
- execute<${ucFirst(mutationName)}MutationResult, ${ucFirst(mutationName)}MutationVariables>(
434
- ${mutationName}MutationDocument,
435
- variables
436
- ),
437
- onSuccess: (_, variables) => {
438
- // Remove from cache and invalidate list
439
- queryClient.removeQueries({ queryKey: ['${typeName.toLowerCase()}', 'detail', variables.input.${pkField.name}] });
440
- queryClient.invalidateQueries({ queryKey: ['${typeName.toLowerCase()}', 'list'] });
441
- },
442
- ...options,
443
- });`,
444
- docs: [
445
- {
446
- description: `Mutation hook for deleting a ${typeName}
447
-
448
- @example
449
- \`\`\`tsx
450
- const { mutate, isPending } = ${hookName}();
451
-
452
- mutate({
453
- input: {
454
- ${pkField.name}: ${pkField.tsType === 'string' ? "'value-to-delete'" : '123'},
455
- },
456
- });
457
- \`\`\``,
458
- },
459
- ],
460
- });
377
+ const mutationOptions = [];
378
+ if (useCentralizedKeys) {
379
+ mutationOptions.push(t.objectProperty(t.identifier('mutationKey'), t.memberExpression(t.identifier(mutationKeysName), t.identifier('all'))));
380
+ }
381
+ mutationOptions.push(t.objectProperty(t.identifier('mutationFn'), t.arrowFunctionExpression([typedParam('variables', t.tsTypeReference(t.identifier(`${ucFirst(mutationName)}MutationVariables`)))], t.callExpression(t.identifier('execute'), [
382
+ t.identifier(`${mutationName}MutationDocument`),
383
+ t.identifier('variables'),
384
+ ]))));
385
+ const detailQueryKey = useCentralizedKeys
386
+ ? t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('detail')), [t.memberExpression(t.memberExpression(t.identifier('variables'), t.identifier('input')), t.identifier(pkField.name))])
387
+ : t.arrayExpression([
388
+ t.stringLiteral(typeName.toLowerCase()),
389
+ t.stringLiteral('detail'),
390
+ t.memberExpression(t.memberExpression(t.identifier('variables'), t.identifier('input')), t.identifier(pkField.name)),
391
+ ]);
392
+ const listQueryKey = useCentralizedKeys
393
+ ? t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('lists')), [])
394
+ : t.arrayExpression([t.stringLiteral(typeName.toLowerCase()), t.stringLiteral('list')]);
395
+ mutationOptions.push(t.objectProperty(t.identifier('onSuccess'), t.arrowFunctionExpression([t.identifier('_'), t.identifier('variables')], t.blockStatement([
396
+ t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('queryClient'), t.identifier('removeQueries')), [t.objectExpression([t.objectProperty(t.identifier('queryKey'), detailQueryKey)])])),
397
+ t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('queryClient'), t.identifier('invalidateQueries')), [t.objectExpression([t.objectProperty(t.identifier('queryKey'), listQueryKey)])])),
398
+ ]))));
399
+ mutationOptions.push(t.spreadElement(t.identifier('options')));
400
+ hookBodyStatements.push(t.returnStatement(t.callExpression(t.identifier('useMutation'), [t.objectExpression(mutationOptions)])));
401
+ const optionsTypeStr = `Omit<UseMutationOptions<${ucFirst(mutationName)}MutationResult, Error, ${ucFirst(mutationName)}MutationVariables>, 'mutationFn'>`;
402
+ const optionsParam = t.identifier('options');
403
+ optionsParam.optional = true;
404
+ optionsParam.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier(optionsTypeStr)));
405
+ const hookFunc = t.functionDeclaration(t.identifier(hookName), [optionsParam], t.blockStatement(hookBodyStatements));
406
+ const hookExport = t.exportNamedDeclaration(hookFunc);
407
+ addJSDocComment(hookExport, [
408
+ `Mutation hook for deleting a ${typeName}`,
409
+ '',
410
+ '@example',
411
+ '```tsx',
412
+ `const { mutate, isPending } = ${hookName}();`,
413
+ '',
414
+ 'mutate({',
415
+ ' input: {',
416
+ ` ${pkField.name}: ${pkField.tsType === 'string' ? "'value-to-delete'" : '123'},`,
417
+ ' },',
418
+ '});',
419
+ '```',
420
+ ]);
421
+ statements.push(hookExport);
422
+ const code = generateCode(statements);
423
+ const content = getGeneratedFileHeader(`Delete mutation hook for ${typeName}`) + '\n\n' + code;
461
424
  return {
462
425
  fileName: getDeleteMutationFileName(table),
463
- content: getFormattedOutput(sourceFile),
426
+ content,
464
427
  };
465
428
  }
466
- // ============================================================================
467
- // Batch generator
468
- // ============================================================================
469
- /**
470
- * Generate all mutation hook files for all tables
471
- * When reactQueryEnabled is false, returns empty array since mutations require React Query
472
- */
473
429
  export function generateAllMutationHooks(tables, options = {}) {
474
430
  const files = [];
475
431
  for (const table of tables) {