@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.
- package/README.md +147 -2
- package/cli/codegen/babel-ast.d.ts +46 -0
- package/cli/codegen/babel-ast.js +145 -0
- package/cli/codegen/barrel.d.ts +7 -2
- package/cli/codegen/barrel.js +159 -97
- package/cli/codegen/client.js +61 -0
- package/cli/codegen/custom-mutations.d.ts +2 -12
- package/cli/codegen/custom-mutations.js +116 -124
- package/cli/codegen/custom-queries.d.ts +2 -10
- package/cli/codegen/custom-queries.js +246 -335
- package/cli/codegen/index.d.ts +3 -0
- package/cli/codegen/index.js +72 -3
- package/cli/codegen/invalidation.d.ts +20 -0
- package/cli/codegen/invalidation.js +327 -0
- package/cli/codegen/mutation-keys.d.ts +24 -0
- package/cli/codegen/mutation-keys.js +247 -0
- package/cli/codegen/mutations.d.ts +3 -19
- package/cli/codegen/mutations.js +372 -383
- package/cli/codegen/orm/barrel.d.ts +1 -1
- package/cli/codegen/orm/barrel.js +42 -10
- package/cli/codegen/orm/client-generator.d.ts +1 -19
- package/cli/codegen/orm/client-generator.js +108 -77
- package/cli/codegen/orm/custom-ops-generator.d.ts +1 -12
- package/cli/codegen/orm/custom-ops-generator.js +192 -235
- package/cli/codegen/orm/input-types-generator.d.ts +13 -1
- package/cli/codegen/orm/input-types-generator.js +403 -147
- package/cli/codegen/orm/model-generator.d.ts +1 -19
- package/cli/codegen/orm/model-generator.js +229 -234
- package/cli/codegen/queries.d.ts +3 -11
- package/cli/codegen/queries.js +582 -389
- package/cli/codegen/query-keys.d.ts +15 -0
- package/cli/codegen/query-keys.js +477 -0
- package/cli/codegen/scalars.js +1 -0
- package/cli/codegen/schema-types-generator.d.ts +15 -10
- package/cli/codegen/schema-types-generator.js +87 -175
- package/cli/codegen/type-resolver.d.ts +1 -30
- package/cli/codegen/type-resolver.js +0 -53
- package/cli/codegen/types.d.ts +1 -1
- package/cli/codegen/types.js +76 -21
- package/cli/commands/generate.js +1 -0
- package/cli/index.js +1 -0
- package/esm/cli/codegen/babel-ast.d.ts +46 -0
- package/esm/cli/codegen/babel-ast.js +97 -0
- package/esm/cli/codegen/barrel.d.ts +7 -2
- package/esm/cli/codegen/barrel.js +126 -97
- package/esm/cli/codegen/client.js +61 -0
- package/esm/cli/codegen/custom-mutations.d.ts +2 -12
- package/esm/cli/codegen/custom-mutations.js +83 -124
- package/esm/cli/codegen/custom-queries.d.ts +2 -10
- package/esm/cli/codegen/custom-queries.js +214 -336
- package/esm/cli/codegen/index.d.ts +3 -0
- package/esm/cli/codegen/index.js +68 -2
- package/esm/cli/codegen/invalidation.d.ts +20 -0
- package/esm/cli/codegen/invalidation.js +291 -0
- package/esm/cli/codegen/mutation-keys.d.ts +24 -0
- package/esm/cli/codegen/mutation-keys.js +211 -0
- package/esm/cli/codegen/mutations.d.ts +3 -19
- package/esm/cli/codegen/mutations.js +340 -384
- package/esm/cli/codegen/orm/barrel.d.ts +1 -1
- package/esm/cli/codegen/orm/barrel.js +10 -11
- package/esm/cli/codegen/orm/client-generator.d.ts +1 -19
- package/esm/cli/codegen/orm/client-generator.js +76 -78
- package/esm/cli/codegen/orm/custom-ops-generator.d.ts +1 -12
- package/esm/cli/codegen/orm/custom-ops-generator.js +160 -236
- package/esm/cli/codegen/orm/input-types-generator.d.ts +13 -1
- package/esm/cli/codegen/orm/input-types-generator.js +371 -148
- package/esm/cli/codegen/orm/model-generator.d.ts +1 -19
- package/esm/cli/codegen/orm/model-generator.js +197 -235
- package/esm/cli/codegen/queries.d.ts +3 -11
- package/esm/cli/codegen/queries.js +550 -390
- package/esm/cli/codegen/query-keys.d.ts +15 -0
- package/esm/cli/codegen/query-keys.js +441 -0
- package/esm/cli/codegen/scalars.js +1 -0
- package/esm/cli/codegen/schema-types-generator.d.ts +15 -10
- package/esm/cli/codegen/schema-types-generator.js +54 -175
- package/esm/cli/codegen/type-resolver.d.ts +1 -30
- package/esm/cli/codegen/type-resolver.js +0 -49
- package/esm/cli/codegen/types.d.ts +1 -1
- package/esm/cli/codegen/types.js +44 -22
- package/esm/cli/commands/generate.js +1 -0
- package/esm/cli/index.js +1 -0
- package/esm/types/config.d.ts +75 -0
- package/esm/types/config.js +19 -1
- package/package.json +6 -4
- package/types/config.d.ts +75 -0
- package/types/config.js +20 -2
- package/cli/codegen/ts-ast.d.ts +0 -124
- package/cli/codegen/ts-ast.js +0 -280
- package/esm/cli/codegen/ts-ast.d.ts +0 -124
- package/esm/cli/codegen/ts-ast.js +0 -260
package/esm/cli/codegen/index.js
CHANGED
|
@@ -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, {
|
|
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,8 @@ export function generate(options) {
|
|
|
96
150
|
const mutationHooks = generateAllMutationHooks(tables, {
|
|
97
151
|
reactQueryEnabled,
|
|
98
152
|
enumsFromSchemaTypes: generatedEnumNames,
|
|
153
|
+
useCentralizedKeys,
|
|
154
|
+
hasRelationships,
|
|
99
155
|
});
|
|
100
156
|
for (const hook of mutationHooks) {
|
|
101
157
|
files.push({
|
|
@@ -113,6 +169,7 @@ export function generate(options) {
|
|
|
113
169
|
skipQueryField,
|
|
114
170
|
reactQueryEnabled,
|
|
115
171
|
tableTypeNames,
|
|
172
|
+
useCentralizedKeys,
|
|
116
173
|
});
|
|
117
174
|
for (const hook of customMutationHooks) {
|
|
118
175
|
files.push({
|
|
@@ -135,7 +192,13 @@ export function generate(options) {
|
|
|
135
192
|
// 9. Generate main index.ts barrel (with schema-types if present)
|
|
136
193
|
files.push({
|
|
137
194
|
path: 'index.ts',
|
|
138
|
-
content: generateMainBarrel(tables, {
|
|
195
|
+
content: generateMainBarrel(tables, {
|
|
196
|
+
hasSchemaTypes,
|
|
197
|
+
hasMutations,
|
|
198
|
+
hasQueryKeys,
|
|
199
|
+
hasMutationKeys,
|
|
200
|
+
hasInvalidation,
|
|
201
|
+
}),
|
|
139
202
|
});
|
|
140
203
|
return {
|
|
141
204
|
files,
|
|
@@ -159,3 +222,6 @@ export { generateAllMutationHooks, generateCreateMutationHook, generateUpdateMut
|
|
|
159
222
|
export { generateAllCustomQueryHooks, generateCustomQueryHook, } from './custom-queries';
|
|
160
223
|
export { generateAllCustomMutationHooks, generateCustomMutationHook, } from './custom-mutations';
|
|
161
224
|
export { generateQueriesBarrel, generateMutationsBarrel, generateMainBarrel, generateCustomQueriesBarrel, generateCustomMutationsBarrel, } from './barrel';
|
|
225
|
+
export { generateQueryKeysFile } from './query-keys';
|
|
226
|
+
export { generateMutationKeysFile } from './mutation-keys';
|
|
227
|
+
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;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { getTableNames, getGeneratedFileHeader, lcFirst } from './utils';
|
|
2
|
+
import * as t from '@babel/types';
|
|
3
|
+
import { generateCode, addJSDocComment, asConst, constArray, typedParam, } from './babel-ast';
|
|
4
|
+
/**
|
|
5
|
+
* Generate mutation keys declaration for a single table entity
|
|
6
|
+
*/
|
|
7
|
+
function generateEntityMutationKeysDeclaration(table, relationships) {
|
|
8
|
+
const { typeName, singularName } = getTableNames(table);
|
|
9
|
+
const entityKey = typeName.toLowerCase();
|
|
10
|
+
const keysName = `${lcFirst(typeName)}MutationKeys`;
|
|
11
|
+
const relationship = relationships[entityKey];
|
|
12
|
+
const properties = [];
|
|
13
|
+
// all property
|
|
14
|
+
const allProp = t.objectProperty(t.identifier('all'), constArray([t.stringLiteral('mutation'), t.stringLiteral(entityKey)]));
|
|
15
|
+
addJSDocComment(allProp, [`All ${singularName} mutation keys`]);
|
|
16
|
+
properties.push(allProp);
|
|
17
|
+
// create property
|
|
18
|
+
let createProp;
|
|
19
|
+
if (relationship) {
|
|
20
|
+
const fkParam = t.identifier(relationship.foreignKey);
|
|
21
|
+
fkParam.optional = true;
|
|
22
|
+
fkParam.typeAnnotation = t.tsTypeAnnotation(t.tsStringKeyword());
|
|
23
|
+
const arrowFn = t.arrowFunctionExpression([fkParam], t.conditionalExpression(t.identifier(relationship.foreignKey), constArray([
|
|
24
|
+
t.stringLiteral('mutation'),
|
|
25
|
+
t.stringLiteral(entityKey),
|
|
26
|
+
t.stringLiteral('create'),
|
|
27
|
+
t.objectExpression([
|
|
28
|
+
t.objectProperty(t.identifier(relationship.foreignKey), t.identifier(relationship.foreignKey), false, true)
|
|
29
|
+
]),
|
|
30
|
+
]), constArray([
|
|
31
|
+
t.stringLiteral('mutation'),
|
|
32
|
+
t.stringLiteral(entityKey),
|
|
33
|
+
t.stringLiteral('create'),
|
|
34
|
+
])));
|
|
35
|
+
createProp = t.objectProperty(t.identifier('create'), arrowFn);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const arrowFn = t.arrowFunctionExpression([], constArray([
|
|
39
|
+
t.stringLiteral('mutation'),
|
|
40
|
+
t.stringLiteral(entityKey),
|
|
41
|
+
t.stringLiteral('create'),
|
|
42
|
+
]));
|
|
43
|
+
createProp = t.objectProperty(t.identifier('create'), arrowFn);
|
|
44
|
+
}
|
|
45
|
+
addJSDocComment(createProp, [`Create ${singularName} mutation key`]);
|
|
46
|
+
properties.push(createProp);
|
|
47
|
+
// update property
|
|
48
|
+
const updateArrowFn = t.arrowFunctionExpression([typedParam('id', t.tsUnionType([t.tsStringKeyword(), t.tsNumberKeyword()]))], constArray([
|
|
49
|
+
t.stringLiteral('mutation'),
|
|
50
|
+
t.stringLiteral(entityKey),
|
|
51
|
+
t.stringLiteral('update'),
|
|
52
|
+
t.identifier('id'),
|
|
53
|
+
]));
|
|
54
|
+
const updateProp = t.objectProperty(t.identifier('update'), updateArrowFn);
|
|
55
|
+
addJSDocComment(updateProp, [`Update ${singularName} mutation key`]);
|
|
56
|
+
properties.push(updateProp);
|
|
57
|
+
// delete property
|
|
58
|
+
const deleteArrowFn = t.arrowFunctionExpression([typedParam('id', t.tsUnionType([t.tsStringKeyword(), t.tsNumberKeyword()]))], constArray([
|
|
59
|
+
t.stringLiteral('mutation'),
|
|
60
|
+
t.stringLiteral(entityKey),
|
|
61
|
+
t.stringLiteral('delete'),
|
|
62
|
+
t.identifier('id'),
|
|
63
|
+
]));
|
|
64
|
+
const deleteProp = t.objectProperty(t.identifier('delete'), deleteArrowFn);
|
|
65
|
+
addJSDocComment(deleteProp, [`Delete ${singularName} mutation key`]);
|
|
66
|
+
properties.push(deleteProp);
|
|
67
|
+
return t.exportNamedDeclaration(t.variableDeclaration('const', [
|
|
68
|
+
t.variableDeclarator(t.identifier(keysName), asConst(t.objectExpression(properties)))
|
|
69
|
+
]));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Generate custom mutation keys declaration
|
|
73
|
+
*/
|
|
74
|
+
function generateCustomMutationKeysDeclaration(operations) {
|
|
75
|
+
if (operations.length === 0)
|
|
76
|
+
return null;
|
|
77
|
+
const properties = [];
|
|
78
|
+
for (const op of operations) {
|
|
79
|
+
const hasArgs = op.args.length > 0;
|
|
80
|
+
let prop;
|
|
81
|
+
if (hasArgs) {
|
|
82
|
+
const identifierParam = t.identifier('identifier');
|
|
83
|
+
identifierParam.optional = true;
|
|
84
|
+
identifierParam.typeAnnotation = t.tsTypeAnnotation(t.tsStringKeyword());
|
|
85
|
+
const arrowFn = t.arrowFunctionExpression([identifierParam], t.conditionalExpression(t.identifier('identifier'), constArray([
|
|
86
|
+
t.stringLiteral('mutation'),
|
|
87
|
+
t.stringLiteral(op.name),
|
|
88
|
+
t.identifier('identifier'),
|
|
89
|
+
]), constArray([t.stringLiteral('mutation'), t.stringLiteral(op.name)])));
|
|
90
|
+
prop = t.objectProperty(t.identifier(op.name), arrowFn);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const arrowFn = t.arrowFunctionExpression([], constArray([t.stringLiteral('mutation'), t.stringLiteral(op.name)]));
|
|
94
|
+
prop = t.objectProperty(t.identifier(op.name), arrowFn);
|
|
95
|
+
}
|
|
96
|
+
addJSDocComment(prop, [`Mutation key for ${op.name}`]);
|
|
97
|
+
properties.push(prop);
|
|
98
|
+
}
|
|
99
|
+
return t.exportNamedDeclaration(t.variableDeclaration('const', [
|
|
100
|
+
t.variableDeclarator(t.identifier('customMutationKeys'), asConst(t.objectExpression(properties)))
|
|
101
|
+
]));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Generate the unified mutation keys store declaration
|
|
105
|
+
*/
|
|
106
|
+
function generateUnifiedMutationStoreDeclaration(tables, hasCustomMutations) {
|
|
107
|
+
const properties = [];
|
|
108
|
+
for (const table of tables) {
|
|
109
|
+
const { typeName } = getTableNames(table);
|
|
110
|
+
const keysName = `${lcFirst(typeName)}MutationKeys`;
|
|
111
|
+
properties.push(t.objectProperty(t.identifier(lcFirst(typeName)), t.identifier(keysName)));
|
|
112
|
+
}
|
|
113
|
+
if (hasCustomMutations) {
|
|
114
|
+
properties.push(t.objectProperty(t.identifier('custom'), t.identifier('customMutationKeys')));
|
|
115
|
+
}
|
|
116
|
+
const decl = t.exportNamedDeclaration(t.variableDeclaration('const', [
|
|
117
|
+
t.variableDeclarator(t.identifier('mutationKeys'), asConst(t.objectExpression(properties)))
|
|
118
|
+
]));
|
|
119
|
+
addJSDocComment(decl, [
|
|
120
|
+
'Unified mutation key store',
|
|
121
|
+
'',
|
|
122
|
+
'Use this for tracking in-flight mutations with useIsMutating.',
|
|
123
|
+
'',
|
|
124
|
+
'@example',
|
|
125
|
+
'```ts',
|
|
126
|
+
"import { useIsMutating } from '@tanstack/react-query';",
|
|
127
|
+
"import { mutationKeys } from './generated';",
|
|
128
|
+
'',
|
|
129
|
+
'// Check if any user mutations are in progress',
|
|
130
|
+
'const isMutatingUser = useIsMutating({ mutationKey: mutationKeys.user.all });',
|
|
131
|
+
'',
|
|
132
|
+
'// Check if a specific user is being updated',
|
|
133
|
+
'const isUpdating = useIsMutating({ mutationKey: mutationKeys.user.update(userId) });',
|
|
134
|
+
'```',
|
|
135
|
+
]);
|
|
136
|
+
return decl;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Generate the complete mutation-keys.ts file
|
|
140
|
+
*/
|
|
141
|
+
export function generateMutationKeysFile(options) {
|
|
142
|
+
const { tables, customMutations, config } = options;
|
|
143
|
+
const { relationships } = config;
|
|
144
|
+
const statements = [];
|
|
145
|
+
// Generate entity mutation keys
|
|
146
|
+
for (const table of tables) {
|
|
147
|
+
statements.push(generateEntityMutationKeysDeclaration(table, relationships));
|
|
148
|
+
}
|
|
149
|
+
// Generate custom mutation keys
|
|
150
|
+
const mutationOperations = customMutations.filter((op) => op.kind === 'mutation');
|
|
151
|
+
const customKeysDecl = generateCustomMutationKeysDeclaration(mutationOperations);
|
|
152
|
+
if (customKeysDecl) {
|
|
153
|
+
statements.push(customKeysDecl);
|
|
154
|
+
}
|
|
155
|
+
// Generate unified store
|
|
156
|
+
statements.push(generateUnifiedMutationStoreDeclaration(tables, mutationOperations.length > 0));
|
|
157
|
+
// Generate code from AST
|
|
158
|
+
const code = generateCode(statements);
|
|
159
|
+
// Build final content with header and section comments
|
|
160
|
+
const header = getGeneratedFileHeader('Centralized mutation key factory');
|
|
161
|
+
const description = `// ============================================================================
|
|
162
|
+
// Mutation keys for tracking in-flight mutations
|
|
163
|
+
//
|
|
164
|
+
// Benefits:
|
|
165
|
+
// - Track mutation state with useIsMutating
|
|
166
|
+
// - Implement optimistic updates with proper rollback
|
|
167
|
+
// - Deduplicate identical mutations
|
|
168
|
+
// - Coordinate related mutations
|
|
169
|
+
// ============================================================================`;
|
|
170
|
+
let content = `${header}
|
|
171
|
+
|
|
172
|
+
${description}
|
|
173
|
+
|
|
174
|
+
// ============================================================================
|
|
175
|
+
// Entity Mutation Keys
|
|
176
|
+
// ============================================================================
|
|
177
|
+
|
|
178
|
+
`;
|
|
179
|
+
// Insert section comments into the generated code
|
|
180
|
+
const codeLines = code.split('\n');
|
|
181
|
+
let addedCustomSection = false;
|
|
182
|
+
let addedUnifiedSection = false;
|
|
183
|
+
for (let i = 0; i < codeLines.length; i++) {
|
|
184
|
+
const line = codeLines[i];
|
|
185
|
+
// Detect custom mutation keys section
|
|
186
|
+
if (!addedCustomSection && line.startsWith('export const customMutationKeys')) {
|
|
187
|
+
content += `
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// Custom Mutation Keys
|
|
190
|
+
// ============================================================================
|
|
191
|
+
|
|
192
|
+
`;
|
|
193
|
+
addedCustomSection = true;
|
|
194
|
+
}
|
|
195
|
+
// Detect unified store section
|
|
196
|
+
if (!addedUnifiedSection && line.includes('* Unified mutation key store')) {
|
|
197
|
+
content += `
|
|
198
|
+
// ============================================================================
|
|
199
|
+
// Unified Mutation Key Store
|
|
200
|
+
// ============================================================================
|
|
201
|
+
|
|
202
|
+
`;
|
|
203
|
+
addedUnifiedSection = true;
|
|
204
|
+
}
|
|
205
|
+
content += line + '\n';
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
fileName: 'mutation-keys.ts',
|
|
209
|
+
content,
|
|
210
|
+
};
|
|
211
|
+
}
|