@constructive-io/graphql-codegen 2.23.3 → 2.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +147 -2
  2. package/cli/codegen/babel-ast.d.ts +53 -0
  3. package/cli/codegen/babel-ast.js +160 -0
  4. package/cli/codegen/barrel.d.ts +7 -2
  5. package/cli/codegen/barrel.js +193 -102
  6. package/cli/codegen/client.js +61 -0
  7. package/cli/codegen/custom-mutations.d.ts +2 -12
  8. package/cli/codegen/custom-mutations.js +116 -124
  9. package/cli/codegen/custom-queries.d.ts +2 -10
  10. package/cli/codegen/custom-queries.js +236 -335
  11. package/cli/codegen/gql-ast.js +22 -1
  12. package/cli/codegen/index.d.ts +3 -0
  13. package/cli/codegen/index.js +73 -3
  14. package/cli/codegen/invalidation.d.ts +20 -0
  15. package/cli/codegen/invalidation.js +327 -0
  16. package/cli/codegen/mutation-keys.d.ts +24 -0
  17. package/cli/codegen/mutation-keys.js +247 -0
  18. package/cli/codegen/mutations.d.ts +5 -19
  19. package/cli/codegen/mutations.js +385 -383
  20. package/cli/codegen/orm/barrel.d.ts +1 -1
  21. package/cli/codegen/orm/barrel.js +42 -10
  22. package/cli/codegen/orm/client-generator.d.ts +1 -19
  23. package/cli/codegen/orm/client-generator.js +108 -77
  24. package/cli/codegen/orm/custom-ops-generator.d.ts +1 -12
  25. package/cli/codegen/orm/custom-ops-generator.js +192 -235
  26. package/cli/codegen/orm/input-types-generator.d.ts +13 -1
  27. package/cli/codegen/orm/input-types-generator.js +425 -147
  28. package/cli/codegen/orm/model-generator.d.ts +1 -19
  29. package/cli/codegen/orm/model-generator.js +229 -234
  30. package/cli/codegen/queries.d.ts +4 -12
  31. package/cli/codegen/queries.js +660 -390
  32. package/cli/codegen/query-keys.d.ts +15 -0
  33. package/cli/codegen/query-keys.js +477 -0
  34. package/cli/codegen/scalars.js +1 -0
  35. package/cli/codegen/schema-types-generator.d.ts +15 -10
  36. package/cli/codegen/schema-types-generator.js +87 -175
  37. package/cli/codegen/type-resolver.d.ts +1 -30
  38. package/cli/codegen/type-resolver.js +0 -53
  39. package/cli/codegen/types.d.ts +1 -1
  40. package/cli/codegen/types.js +76 -21
  41. package/cli/codegen/utils.d.ts +6 -0
  42. package/cli/codegen/utils.js +19 -0
  43. package/esm/cli/codegen/babel-ast.d.ts +53 -0
  44. package/esm/cli/codegen/babel-ast.js +111 -0
  45. package/esm/cli/codegen/barrel.d.ts +7 -2
  46. package/esm/cli/codegen/barrel.js +161 -103
  47. package/esm/cli/codegen/client.js +61 -0
  48. package/esm/cli/codegen/custom-mutations.d.ts +2 -12
  49. package/esm/cli/codegen/custom-mutations.js +83 -124
  50. package/esm/cli/codegen/custom-queries.d.ts +2 -10
  51. package/esm/cli/codegen/custom-queries.js +204 -336
  52. package/esm/cli/codegen/gql-ast.js +23 -2
  53. package/esm/cli/codegen/index.d.ts +3 -0
  54. package/esm/cli/codegen/index.js +69 -2
  55. package/esm/cli/codegen/invalidation.d.ts +20 -0
  56. package/esm/cli/codegen/invalidation.js +291 -0
  57. package/esm/cli/codegen/mutation-keys.d.ts +24 -0
  58. package/esm/cli/codegen/mutation-keys.js +211 -0
  59. package/esm/cli/codegen/mutations.d.ts +5 -19
  60. package/esm/cli/codegen/mutations.js +353 -384
  61. package/esm/cli/codegen/orm/barrel.d.ts +1 -1
  62. package/esm/cli/codegen/orm/barrel.js +10 -11
  63. package/esm/cli/codegen/orm/client-generator.d.ts +1 -19
  64. package/esm/cli/codegen/orm/client-generator.js +76 -78
  65. package/esm/cli/codegen/orm/custom-ops-generator.d.ts +1 -12
  66. package/esm/cli/codegen/orm/custom-ops-generator.js +160 -236
  67. package/esm/cli/codegen/orm/input-types-generator.d.ts +13 -1
  68. package/esm/cli/codegen/orm/input-types-generator.js +393 -148
  69. package/esm/cli/codegen/orm/model-generator.d.ts +1 -19
  70. package/esm/cli/codegen/orm/model-generator.js +197 -235
  71. package/esm/cli/codegen/queries.d.ts +4 -12
  72. package/esm/cli/codegen/queries.js +628 -391
  73. package/esm/cli/codegen/query-keys.d.ts +15 -0
  74. package/esm/cli/codegen/query-keys.js +441 -0
  75. package/esm/cli/codegen/scalars.js +1 -0
  76. package/esm/cli/codegen/schema-types-generator.d.ts +15 -10
  77. package/esm/cli/codegen/schema-types-generator.js +54 -175
  78. package/esm/cli/codegen/type-resolver.d.ts +1 -30
  79. package/esm/cli/codegen/type-resolver.js +0 -49
  80. package/esm/cli/codegen/types.d.ts +1 -1
  81. package/esm/cli/codegen/types.js +44 -22
  82. package/esm/cli/codegen/utils.d.ts +6 -0
  83. package/esm/cli/codegen/utils.js +18 -0
  84. package/esm/types/config.d.ts +75 -0
  85. package/esm/types/config.js +18 -0
  86. package/package.json +6 -4
  87. package/types/config.d.ts +75 -0
  88. package/types/config.js +19 -1
  89. package/cli/codegen/ts-ast.d.ts +0 -124
  90. package/cli/codegen/ts-ast.js +0 -280
  91. package/esm/cli/codegen/ts-ast.d.ts +0 -124
  92. package/esm/cli/codegen/ts-ast.js +0 -260
@@ -69,3 +69,6 @@ export { generateAllMutationHooks, generateCreateMutationHook, generateUpdateMut
69
69
  export { generateAllCustomQueryHooks, generateCustomQueryHook, } from './custom-queries';
70
70
  export { generateAllCustomMutationHooks, generateCustomMutationHook, } from './custom-mutations';
71
71
  export { generateQueriesBarrel, generateMutationsBarrel, generateMainBarrel, generateCustomQueriesBarrel, generateCustomMutationsBarrel, } from './barrel';
72
+ export { generateQueryKeysFile } from './query-keys';
73
+ export { generateMutationKeysFile } from './mutation-keys';
74
+ export { generateInvalidationFile } from './invalidation';
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_QUERY_KEY_CONFIG } from '../../types/config';
1
2
  import { generateClientFile } from './client';
2
3
  import { generateTypesFile } from './types';
3
4
  import { generateSchemaTypesFile } from './schema-types-generator';
@@ -5,6 +6,9 @@ import { generateAllQueryHooks } from './queries';
5
6
  import { generateAllMutationHooks } from './mutations';
6
7
  import { generateAllCustomQueryHooks } from './custom-queries';
7
8
  import { generateAllCustomMutationHooks } from './custom-mutations';
9
+ import { generateQueryKeysFile } from './query-keys';
10
+ import { generateMutationKeysFile } from './mutation-keys';
11
+ import { generateInvalidationFile } from './invalidation';
8
12
  import { generateQueriesBarrel, generateMutationsBarrel, generateMainBarrel, generateCustomQueriesBarrel, generateCustomMutationsBarrel, } from './barrel';
9
13
  import { getTableNames } from './utils';
10
14
  // ============================================================================
@@ -26,6 +30,10 @@ export function generate(options) {
26
30
  const maxDepth = config.codegen.maxFieldDepth;
27
31
  const skipQueryField = config.codegen.skipQueryField;
28
32
  const reactQueryEnabled = config.reactQuery.enabled;
33
+ // Query key configuration (use defaults if not provided)
34
+ const queryKeyConfig = config.queryKeys ?? DEFAULT_QUERY_KEY_CONFIG;
35
+ const useCentralizedKeys = queryKeyConfig.generateScopedKeys;
36
+ const hasRelationships = Object.keys(queryKeyConfig.relationships).length > 0;
29
37
  // 1. Generate client.ts
30
38
  files.push({
31
39
  path: 'client.ts',
@@ -59,8 +67,53 @@ export function generate(options) {
59
67
  enumsFromSchemaTypes: generatedEnumNames,
60
68
  }),
61
69
  });
70
+ // 3b. Generate centralized query keys (query-keys.ts)
71
+ let hasQueryKeys = false;
72
+ if (useCentralizedKeys) {
73
+ const queryKeysResult = generateQueryKeysFile({
74
+ tables,
75
+ customQueries: customOperations?.queries ?? [],
76
+ config: queryKeyConfig,
77
+ });
78
+ files.push({
79
+ path: queryKeysResult.fileName,
80
+ content: queryKeysResult.content,
81
+ });
82
+ hasQueryKeys = true;
83
+ }
84
+ // 3c. Generate centralized mutation keys (mutation-keys.ts)
85
+ let hasMutationKeys = false;
86
+ if (useCentralizedKeys && queryKeyConfig.generateMutationKeys) {
87
+ const mutationKeysResult = generateMutationKeysFile({
88
+ tables,
89
+ customMutations: customOperations?.mutations ?? [],
90
+ config: queryKeyConfig,
91
+ });
92
+ files.push({
93
+ path: mutationKeysResult.fileName,
94
+ content: mutationKeysResult.content,
95
+ });
96
+ hasMutationKeys = true;
97
+ }
98
+ // 3d. Generate cache invalidation helpers (invalidation.ts)
99
+ let hasInvalidation = false;
100
+ if (useCentralizedKeys && queryKeyConfig.generateCascadeHelpers) {
101
+ const invalidationResult = generateInvalidationFile({
102
+ tables,
103
+ config: queryKeyConfig,
104
+ });
105
+ files.push({
106
+ path: invalidationResult.fileName,
107
+ content: invalidationResult.content,
108
+ });
109
+ hasInvalidation = true;
110
+ }
62
111
  // 4. Generate table-based query hooks (queries/*.ts)
63
- const queryHooks = generateAllQueryHooks(tables, { reactQueryEnabled });
112
+ const queryHooks = generateAllQueryHooks(tables, {
113
+ reactQueryEnabled,
114
+ useCentralizedKeys,
115
+ hasRelationships,
116
+ });
64
117
  for (const hook of queryHooks) {
65
118
  files.push({
66
119
  path: `queries/${hook.fileName}`,
@@ -77,6 +130,7 @@ export function generate(options) {
77
130
  skipQueryField,
78
131
  reactQueryEnabled,
79
132
  tableTypeNames,
133
+ useCentralizedKeys,
80
134
  });
81
135
  for (const hook of customQueryHooks) {
82
136
  files.push({
@@ -96,6 +150,9 @@ export function generate(options) {
96
150
  const mutationHooks = generateAllMutationHooks(tables, {
97
151
  reactQueryEnabled,
98
152
  enumsFromSchemaTypes: generatedEnumNames,
153
+ useCentralizedKeys,
154
+ hasRelationships,
155
+ tableTypeNames,
99
156
  });
100
157
  for (const hook of mutationHooks) {
101
158
  files.push({
@@ -113,6 +170,7 @@ export function generate(options) {
113
170
  skipQueryField,
114
171
  reactQueryEnabled,
115
172
  tableTypeNames,
173
+ useCentralizedKeys,
116
174
  });
117
175
  for (const hook of customMutationHooks) {
118
176
  files.push({
@@ -135,7 +193,13 @@ export function generate(options) {
135
193
  // 9. Generate main index.ts barrel (with schema-types if present)
136
194
  files.push({
137
195
  path: 'index.ts',
138
- content: generateMainBarrel(tables, { hasSchemaTypes, hasMutations }),
196
+ content: generateMainBarrel(tables, {
197
+ hasSchemaTypes,
198
+ hasMutations,
199
+ hasQueryKeys,
200
+ hasMutationKeys,
201
+ hasInvalidation,
202
+ }),
139
203
  });
140
204
  return {
141
205
  files,
@@ -159,3 +223,6 @@ export { generateAllMutationHooks, generateCreateMutationHook, generateUpdateMut
159
223
  export { generateAllCustomQueryHooks, generateCustomQueryHook, } from './custom-queries';
160
224
  export { generateAllCustomMutationHooks, generateCustomMutationHook, } from './custom-mutations';
161
225
  export { generateQueriesBarrel, generateMutationsBarrel, generateMainBarrel, generateCustomQueriesBarrel, generateCustomMutationsBarrel, } from './barrel';
226
+ export { generateQueryKeysFile } from './query-keys';
227
+ export { generateMutationKeysFile } from './mutation-keys';
228
+ export { generateInvalidationFile } from './invalidation';
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Cache invalidation helpers generator
3
+ *
4
+ * Generates type-safe cache invalidation utilities with cascade support
5
+ * for parent-child entity relationships.
6
+ */
7
+ import type { CleanTable } from '../../types/schema';
8
+ import type { ResolvedQueryKeyConfig } from '../../types/config';
9
+ export interface InvalidationGeneratorOptions {
10
+ tables: CleanTable[];
11
+ config: ResolvedQueryKeyConfig;
12
+ }
13
+ export interface GeneratedInvalidationFile {
14
+ fileName: string;
15
+ content: string;
16
+ }
17
+ /**
18
+ * Generate the complete invalidation.ts file
19
+ */
20
+ export declare function generateInvalidationFile(options: InvalidationGeneratorOptions): GeneratedInvalidationFile;
@@ -0,0 +1,291 @@
1
+ import { getTableNames, getGeneratedFileHeader, ucFirst, lcFirst } from './utils';
2
+ import * as t from '@babel/types';
3
+ import { generateCode, addJSDocComment, asConst, typedParam, addLineComment, } from './babel-ast';
4
+ /**
5
+ * Build a map of parent -> children for cascade invalidation
6
+ */
7
+ function buildChildrenMap(relationships) {
8
+ const childrenMap = new Map();
9
+ for (const [child, rel] of Object.entries(relationships)) {
10
+ const parent = rel.parent.toLowerCase();
11
+ if (!childrenMap.has(parent)) {
12
+ childrenMap.set(parent, []);
13
+ }
14
+ childrenMap.get(parent).push(child);
15
+ }
16
+ return childrenMap;
17
+ }
18
+ /**
19
+ * Get all descendants (children, grandchildren, etc.) of an entity
20
+ */
21
+ function getAllDescendants(entity, childrenMap) {
22
+ const descendants = [];
23
+ const queue = [entity.toLowerCase()];
24
+ while (queue.length > 0) {
25
+ const current = queue.shift();
26
+ const children = childrenMap.get(current) ?? [];
27
+ for (const child of children) {
28
+ descendants.push(child);
29
+ queue.push(child);
30
+ }
31
+ }
32
+ return descendants;
33
+ }
34
+ /**
35
+ * Build the invalidate object property for a single entity
36
+ */
37
+ function buildEntityInvalidateProperty(table, relationships, childrenMap, allTables) {
38
+ const { typeName, singularName } = getTableNames(table);
39
+ const entityKey = typeName.toLowerCase();
40
+ const keysName = `${lcFirst(typeName)}Keys`;
41
+ const descendants = getAllDescendants(entityKey, childrenMap);
42
+ const hasDescendants = descendants.length > 0;
43
+ const relationship = relationships[entityKey];
44
+ const hasParent = !!relationship;
45
+ const innerProperties = [];
46
+ // Helper to create QueryClient type reference
47
+ const queryClientTypeRef = () => t.tsTypeReference(t.identifier('QueryClient'));
48
+ const stringOrNumberType = () => t.tsUnionType([t.tsStringKeyword(), t.tsNumberKeyword()]);
49
+ // Helper to create queryClient.invalidateQueries({ queryKey: ... })
50
+ const invalidateCall = (queryKeyExpr) => t.callExpression(t.memberExpression(t.identifier('queryClient'), t.identifier('invalidateQueries')), [t.objectExpression([t.objectProperty(t.identifier('queryKey'), queryKeyExpr)])]);
51
+ // all property
52
+ const allArrowFn = t.arrowFunctionExpression([typedParam('queryClient', queryClientTypeRef())], invalidateCall(t.memberExpression(t.identifier(keysName), t.identifier('all'))));
53
+ const allProp = t.objectProperty(t.identifier('all'), allArrowFn);
54
+ addJSDocComment(allProp, [`Invalidate all ${singularName} queries`]);
55
+ innerProperties.push(allProp);
56
+ // lists property
57
+ let listsProp;
58
+ if (hasParent) {
59
+ const scopeTypeName = `${typeName}Scope`;
60
+ const scopeParam = typedParam('scope', t.tsTypeReference(t.identifier(scopeTypeName)), true);
61
+ const listsArrowFn = t.arrowFunctionExpression([typedParam('queryClient', queryClientTypeRef()), scopeParam], invalidateCall(t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('lists')), [t.identifier('scope')])));
62
+ listsProp = t.objectProperty(t.identifier('lists'), listsArrowFn);
63
+ }
64
+ else {
65
+ const listsArrowFn = t.arrowFunctionExpression([typedParam('queryClient', queryClientTypeRef())], invalidateCall(t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('lists')), [])));
66
+ listsProp = t.objectProperty(t.identifier('lists'), listsArrowFn);
67
+ }
68
+ addJSDocComment(listsProp, [`Invalidate ${singularName} list queries`]);
69
+ innerProperties.push(listsProp);
70
+ // detail property
71
+ let detailProp;
72
+ if (hasParent) {
73
+ const scopeTypeName = `${typeName}Scope`;
74
+ const scopeParam = typedParam('scope', t.tsTypeReference(t.identifier(scopeTypeName)), true);
75
+ const detailArrowFn = t.arrowFunctionExpression([typedParam('queryClient', queryClientTypeRef()), typedParam('id', stringOrNumberType()), scopeParam], invalidateCall(t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('detail')), [t.identifier('id'), t.identifier('scope')])));
76
+ detailProp = t.objectProperty(t.identifier('detail'), detailArrowFn);
77
+ }
78
+ else {
79
+ const detailArrowFn = t.arrowFunctionExpression([typedParam('queryClient', queryClientTypeRef()), typedParam('id', stringOrNumberType())], invalidateCall(t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('detail')), [t.identifier('id')])));
80
+ detailProp = t.objectProperty(t.identifier('detail'), detailArrowFn);
81
+ }
82
+ addJSDocComment(detailProp, [`Invalidate a specific ${singularName}`]);
83
+ innerProperties.push(detailProp);
84
+ // withChildren property (cascade)
85
+ if (hasDescendants) {
86
+ const cascadeStatements = [];
87
+ // Comment: Invalidate this entity
88
+ const selfDetailStmt = t.expressionStatement(invalidateCall(t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('detail')), [t.identifier('id')])));
89
+ addLineComment(selfDetailStmt, `Invalidate this ${singularName}`);
90
+ cascadeStatements.push(selfDetailStmt);
91
+ cascadeStatements.push(t.expressionStatement(invalidateCall(t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('lists')), []))));
92
+ // Comment: Cascade to child entities
93
+ let firstCascade = true;
94
+ for (const descendant of descendants) {
95
+ const descendantTable = allTables.find((tbl) => getTableNames(tbl).typeName.toLowerCase() === descendant);
96
+ if (descendantTable) {
97
+ const { typeName: descTypeName } = getTableNames(descendantTable);
98
+ const descRel = relationships[descendant];
99
+ if (descRel) {
100
+ let fkField = null;
101
+ if (descRel.parent.toLowerCase() === entityKey) {
102
+ fkField = descRel.foreignKey;
103
+ }
104
+ else if (descRel.ancestors?.includes(typeName.toLowerCase())) {
105
+ fkField = `${lcFirst(typeName)}Id`;
106
+ }
107
+ const descKeysName = `${lcFirst(descTypeName)}Keys`;
108
+ let cascadeStmt;
109
+ if (fkField) {
110
+ cascadeStmt = t.expressionStatement(invalidateCall(t.callExpression(t.memberExpression(t.identifier(descKeysName), t.identifier(`by${ucFirst(typeName)}`)), [t.identifier('id')])));
111
+ }
112
+ else {
113
+ cascadeStmt = t.expressionStatement(invalidateCall(t.memberExpression(t.identifier(descKeysName), t.identifier('all'))));
114
+ }
115
+ if (firstCascade) {
116
+ addLineComment(cascadeStmt, 'Cascade to child entities');
117
+ firstCascade = false;
118
+ }
119
+ cascadeStatements.push(cascadeStmt);
120
+ }
121
+ }
122
+ }
123
+ const withChildrenArrowFn = t.arrowFunctionExpression([typedParam('queryClient', queryClientTypeRef()), typedParam('id', stringOrNumberType())], t.blockStatement(cascadeStatements));
124
+ const withChildrenProp = t.objectProperty(t.identifier('withChildren'), withChildrenArrowFn);
125
+ addJSDocComment(withChildrenProp, [
126
+ `Invalidate ${singularName} and all child entities`,
127
+ `Cascades to: ${descendants.join(', ')}`,
128
+ ]);
129
+ innerProperties.push(withChildrenProp);
130
+ }
131
+ const entityProp = t.objectProperty(t.identifier(singularName), t.objectExpression(innerProperties));
132
+ addJSDocComment(entityProp, [`Invalidate ${singularName} queries`]);
133
+ return entityProp;
134
+ }
135
+ /**
136
+ * Build the remove object property for a single entity
137
+ */
138
+ function buildEntityRemoveProperty(table, relationships) {
139
+ const { typeName, singularName } = getTableNames(table);
140
+ const keysName = `${lcFirst(typeName)}Keys`;
141
+ const relationship = relationships[typeName.toLowerCase()];
142
+ // Helper types
143
+ const queryClientTypeRef = () => t.tsTypeReference(t.identifier('QueryClient'));
144
+ const stringOrNumberType = () => t.tsUnionType([t.tsStringKeyword(), t.tsNumberKeyword()]);
145
+ // Helper to create queryClient.removeQueries({ queryKey: ... })
146
+ const removeCall = (queryKeyExpr) => t.callExpression(t.memberExpression(t.identifier('queryClient'), t.identifier('removeQueries')), [t.objectExpression([t.objectProperty(t.identifier('queryKey'), queryKeyExpr)])]);
147
+ let removeProp;
148
+ if (relationship) {
149
+ const scopeTypeName = `${typeName}Scope`;
150
+ const scopeParam = typedParam('scope', t.tsTypeReference(t.identifier(scopeTypeName)), true);
151
+ const removeArrowFn = t.arrowFunctionExpression([typedParam('queryClient', queryClientTypeRef()), typedParam('id', stringOrNumberType()), scopeParam], t.blockStatement([
152
+ t.expressionStatement(removeCall(t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('detail')), [t.identifier('id'), t.identifier('scope')])))
153
+ ]));
154
+ removeProp = t.objectProperty(t.identifier(singularName), removeArrowFn);
155
+ }
156
+ else {
157
+ const removeArrowFn = t.arrowFunctionExpression([typedParam('queryClient', queryClientTypeRef()), typedParam('id', stringOrNumberType())], t.blockStatement([
158
+ t.expressionStatement(removeCall(t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('detail')), [t.identifier('id')])))
159
+ ]));
160
+ removeProp = t.objectProperty(t.identifier(singularName), removeArrowFn);
161
+ }
162
+ addJSDocComment(removeProp, [`Remove ${singularName} from cache`]);
163
+ return removeProp;
164
+ }
165
+ /**
166
+ * Generate the complete invalidation.ts file
167
+ */
168
+ export function generateInvalidationFile(options) {
169
+ const { tables, config } = options;
170
+ const { relationships, generateCascadeHelpers } = config;
171
+ const childrenMap = buildChildrenMap(relationships);
172
+ const statements = [];
173
+ // Import QueryClient type
174
+ const queryClientImport = t.importDeclaration([t.importSpecifier(t.identifier('QueryClient'), t.identifier('QueryClient'))], t.stringLiteral('@tanstack/react-query'));
175
+ queryClientImport.importKind = 'type';
176
+ statements.push(queryClientImport);
177
+ // Import query keys
178
+ const keyImports = [];
179
+ for (const table of tables) {
180
+ const { typeName } = getTableNames(table);
181
+ keyImports.push(`${lcFirst(typeName)}Keys`);
182
+ }
183
+ statements.push(t.importDeclaration(keyImports.map(name => t.importSpecifier(t.identifier(name), t.identifier(name))), t.stringLiteral('./query-keys')));
184
+ // Import scope types if needed
185
+ const scopeTypes = [];
186
+ for (const table of tables) {
187
+ const { typeName } = getTableNames(table);
188
+ if (relationships[typeName.toLowerCase()]) {
189
+ scopeTypes.push(`${typeName}Scope`);
190
+ }
191
+ }
192
+ if (scopeTypes.length > 0) {
193
+ const scopeImport = t.importDeclaration(scopeTypes.map(name => t.importSpecifier(t.identifier(name), t.identifier(name))), t.stringLiteral('./query-keys'));
194
+ scopeImport.importKind = 'type';
195
+ statements.push(scopeImport);
196
+ }
197
+ // Generate invalidate object
198
+ const invalidateProperties = [];
199
+ for (const table of tables) {
200
+ invalidateProperties.push(buildEntityInvalidateProperty(table, relationships, childrenMap, tables));
201
+ }
202
+ const invalidateDecl = t.exportNamedDeclaration(t.variableDeclaration('const', [
203
+ t.variableDeclarator(t.identifier('invalidate'), asConst(t.objectExpression(invalidateProperties)))
204
+ ]));
205
+ // Build JSDoc for invalidate
206
+ const invalidateDocLines = [
207
+ 'Type-safe query invalidation helpers',
208
+ '',
209
+ '@example',
210
+ '```ts',
211
+ '// Invalidate all user queries',
212
+ 'invalidate.user.all(queryClient);',
213
+ '',
214
+ '// Invalidate user lists',
215
+ 'invalidate.user.lists(queryClient);',
216
+ '',
217
+ '// Invalidate specific user',
218
+ 'invalidate.user.detail(queryClient, userId);',
219
+ ];
220
+ if (generateCascadeHelpers && Object.keys(relationships).length > 0) {
221
+ invalidateDocLines.push('');
222
+ invalidateDocLines.push('// Cascade invalidate (entity + all children)');
223
+ invalidateDocLines.push('invalidate.database.withChildren(queryClient, databaseId);');
224
+ }
225
+ invalidateDocLines.push('```');
226
+ addJSDocComment(invalidateDecl, invalidateDocLines);
227
+ statements.push(invalidateDecl);
228
+ // Generate remove object
229
+ const removeProperties = [];
230
+ for (const table of tables) {
231
+ removeProperties.push(buildEntityRemoveProperty(table, relationships));
232
+ }
233
+ const removeDecl = t.exportNamedDeclaration(t.variableDeclaration('const', [
234
+ t.variableDeclarator(t.identifier('remove'), asConst(t.objectExpression(removeProperties)))
235
+ ]));
236
+ addJSDocComment(removeDecl, [
237
+ 'Remove queries from cache (for delete operations)',
238
+ '',
239
+ 'Use these when an entity is deleted to remove it from cache',
240
+ 'instead of just invalidating (which would trigger a refetch).',
241
+ ]);
242
+ statements.push(removeDecl);
243
+ // Generate code from AST
244
+ const code = generateCode(statements);
245
+ // Build final content with header and section comments
246
+ const header = getGeneratedFileHeader('Cache invalidation helpers');
247
+ const description = `// ============================================================================
248
+ // Type-safe cache invalidation utilities
249
+ //
250
+ // Features:
251
+ // - Simple invalidation helpers per entity
252
+ // - Cascade invalidation for parent-child relationships
253
+ // - Remove helpers for delete operations
254
+ // ============================================================================`;
255
+ let content = `${header}
256
+
257
+ ${description}
258
+
259
+ `;
260
+ // Insert section comments into the generated code
261
+ const codeLines = code.split('\n');
262
+ let addedInvalidationSection = false;
263
+ let addedRemoveSection = false;
264
+ for (let i = 0; i < codeLines.length; i++) {
265
+ const line = codeLines[i];
266
+ // Detect invalidation section (after imports)
267
+ if (!addedInvalidationSection && line.includes('* Type-safe query invalidation helpers')) {
268
+ content += `// ============================================================================
269
+ // Invalidation Helpers
270
+ // ============================================================================
271
+
272
+ `;
273
+ addedInvalidationSection = true;
274
+ }
275
+ // Detect remove section
276
+ if (!addedRemoveSection && line.includes('* Remove queries from cache')) {
277
+ content += `
278
+ // ============================================================================
279
+ // Remove Helpers (for delete operations)
280
+ // ============================================================================
281
+
282
+ `;
283
+ addedRemoveSection = true;
284
+ }
285
+ content += line + '\n';
286
+ }
287
+ return {
288
+ fileName: 'invalidation.ts',
289
+ content,
290
+ };
291
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Mutation key factory generator
3
+ *
4
+ * Generates centralized mutation keys for tracking in-flight mutations.
5
+ * Useful for:
6
+ * - Optimistic updates with rollback
7
+ * - Mutation deduplication
8
+ * - Tracking mutation state with useIsMutating
9
+ */
10
+ import type { CleanTable, CleanOperation } from '../../types/schema';
11
+ import type { ResolvedQueryKeyConfig } from '../../types/config';
12
+ export interface MutationKeyGeneratorOptions {
13
+ tables: CleanTable[];
14
+ customMutations: CleanOperation[];
15
+ config: ResolvedQueryKeyConfig;
16
+ }
17
+ export interface GeneratedMutationKeysFile {
18
+ fileName: string;
19
+ content: string;
20
+ }
21
+ /**
22
+ * Generate the complete mutation-keys.ts file
23
+ */
24
+ export declare function generateMutationKeysFile(options: MutationKeyGeneratorOptions): GeneratedMutationKeysFile;