@ethlete/query 6.0.0-next.5 → 6.0.0-next.50
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/CHANGELOG.md +766 -0
- package/fesm2022/ethlete-query-testing.mjs +441 -38
- package/fesm2022/ethlete-query-testing.mjs.map +1 -1
- package/fesm2022/ethlete-query.mjs +15946 -5331
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/generators/deprecate-legacy-queries/migration.js +77 -0
- package/generators/deprecate-legacy-queries/schema.json +26 -0
- package/generators/generators.json +15 -0
- package/generators/migrate-query-client-features/migration.js +142 -0
- package/generators/migrate-query-client-features/report.js +136 -0
- package/generators/migrate-query-client-features/schema.json +26 -0
- package/generators/migrate-query-opt-in-features/migration.js +194 -0
- package/generators/migrate-query-opt-in-features/report.js +143 -0
- package/generators/migrate-query-opt-in-features/schema.json +31 -0
- package/generators/migrate-to-query-v3/auth-provider-scaffold.js +133 -0
- package/generators/migrate-to-query-v3/cleanup-migration.js +111 -99
- package/generators/migrate-to-query-v3/http-client-check.js +93 -0
- package/generators/migrate-to-query-v3/legacy-prepare-migration.js +129 -40
- package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +237 -152
- package/generators/migrate-to-query-v3/migration-scope.js +38 -0
- package/generators/migrate-to-query-v3/migration.js +17 -9
- package/generators/migrate-to-query-v3/module-graph.js +153 -0
- package/generators/migrate-to-query-v3/query-client-migration.js +152 -53
- package/generators/migrate-to-query-v3/rename-symbols.js +206 -0
- package/generators/migrate-to-query-v3/schema.json +12 -0
- package/generators/migrate-to-query-v3/shared.js +51 -2
- package/generators/prep-for-query-v3/migration.js +71 -14
- package/migrations.json +44 -0
- package/package.json +29 -13
- package/types/ethlete-query-testing.d.ts +179 -12
- package/types/ethlete-query.d.ts +8935 -3203
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { getLineNumberFromPosition } from './shared.js';
|
|
2
|
+
const APP_CONFIG_MARKERS = ['bootstrapApplication(', 'ApplicationConfig', 'provideServerRendering('];
|
|
3
|
+
/**
|
|
4
|
+
* Flags the v2 default-header APIs, which the client migration drops on the floor.
|
|
5
|
+
*
|
|
6
|
+
* `setDefaultHeaders` is not cosmetic - an API token or a preview credential lives there, so losing
|
|
7
|
+
* it means every request goes out unauthenticated. v3 has a home for it (`headers` on
|
|
8
|
+
* `createQueryClient`), but the call sites are imperative and scattered, so this is a pointer rather
|
|
9
|
+
* than a rewrite.
|
|
10
|
+
*/
|
|
11
|
+
export const reportDefaultHeaderUsages = (tree, report, scope) => {
|
|
12
|
+
const locations = [];
|
|
13
|
+
scope.visit(tree, (filePath) => {
|
|
14
|
+
if (!filePath.endsWith('.ts')) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const content = tree.read(filePath, 'utf-8');
|
|
18
|
+
if (!content) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
for (const marker of ['setDefaultHeaders', 'globalHeaders']) {
|
|
22
|
+
const index = content.indexOf(marker);
|
|
23
|
+
if (index >= 0) {
|
|
24
|
+
locations.push({ filePath, line: getLineNumberFromPosition(content, index) });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
if (locations.length === 0) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
report.addWarning({
|
|
32
|
+
title: 'Move default headers onto the query client',
|
|
33
|
+
summary: 'This workspace sets client-wide headers through the v2 `setDefaultHeaders` / `globalHeaders` API, which `createQueryClient` does not have. Left as is, those headers are simply not sent.',
|
|
34
|
+
action: 'Pass `headers` to `createQueryClient` - a function form re-reads on every request, so a signal can drive it. `refreshQueriesInUse: true` becomes `injectMyClient().refreshQueriesInUse()` after the value changes.',
|
|
35
|
+
locations,
|
|
36
|
+
source: 'http-client-check',
|
|
37
|
+
dedupeKey: 'default-headers',
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Flags applications that will build cleanly and then fail on their first request.
|
|
42
|
+
*
|
|
43
|
+
* The v2 client shipped its own transport; v3 does `inject(HttpClient)`. An app that never needed
|
|
44
|
+
* `provideHttpClient()` therefore keeps compiling after the migration and dies at runtime instead -
|
|
45
|
+
* the one failure mode a codemod should never leave behind silently.
|
|
46
|
+
*/
|
|
47
|
+
export const reportMissingHttpClientProviders = (tree, report, scope) => {
|
|
48
|
+
const appConfigFiles = [];
|
|
49
|
+
const filesWithProvider = [];
|
|
50
|
+
const filesWithUploadProgress = [];
|
|
51
|
+
scope.visit(tree, (filePath) => {
|
|
52
|
+
if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const content = tree.read(filePath, 'utf-8');
|
|
56
|
+
if (!content) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (content.includes('provideHttpClient(')) {
|
|
60
|
+
filesWithProvider.push(filePath);
|
|
61
|
+
}
|
|
62
|
+
const markerIndex = APP_CONFIG_MARKERS.map((marker) => content.indexOf(marker)).find((index) => index >= 0);
|
|
63
|
+
if (markerIndex !== undefined) {
|
|
64
|
+
appConfigFiles.push({ filePath, line: getLineNumberFromPosition(content, markerIndex) });
|
|
65
|
+
}
|
|
66
|
+
const progressIndex = content.indexOf('reportProgress');
|
|
67
|
+
if (progressIndex >= 0) {
|
|
68
|
+
filesWithUploadProgress.push({ filePath, line: getLineNumberFromPosition(content, progressIndex) });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
if (appConfigFiles.length > 0 && filesWithProvider.length === 0) {
|
|
72
|
+
report.addWarning({
|
|
73
|
+
title: 'Add provideHttpClient() to the application providers',
|
|
74
|
+
summary: 'v3 queries run on Angular’s `HttpClient`, but no `provideHttpClient()` was found. The app compiles and then throws on the first request - `@ethlete/query` never provides it, that is the application’s job.',
|
|
75
|
+
action: 'Add `provideHttpClient()` to every app config that uses a query client. If anything relies on upload progress, use `provideHttpClient(withXhr())` - on Angular ≥ 22 the default is fetch, which emits no upload progress events.',
|
|
76
|
+
locations: appConfigFiles,
|
|
77
|
+
source: 'http-client-check',
|
|
78
|
+
dedupeKey: 'missing-provide-http-client',
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (filesWithUploadProgress.length > 0) {
|
|
83
|
+
report.addManualReview({
|
|
84
|
+
title: 'Confirm the HttpClient backend supports upload progress',
|
|
85
|
+
summary: 'Queries in this workspace set `reportProgress`. On Angular ≥ 22 `provideHttpClient()` defaults to the fetch backend, which never emits upload progress events, so those queries would report download progress only.',
|
|
86
|
+
action: 'Switch the affected app to `provideHttpClient(withXhr())`, or drop `reportProgress` where it is unused.',
|
|
87
|
+
locations: filesWithUploadProgress,
|
|
88
|
+
source: 'http-client-check',
|
|
89
|
+
dedupeKey: 'upload-progress-backend',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
//# sourceMappingURL=http-client-check.js.map
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import { visitNotIgnoredFiles } from '@nx/devkit';
|
|
2
1
|
import * as ts from 'typescript';
|
|
3
2
|
import { createSourceFile, ensureAngularCoreImports, getIndentation, getLineNumber, getLineNumberFromPosition, } from './shared.js';
|
|
4
|
-
|
|
3
|
+
/** `default` can never be a class name, so it cannot collide with a named class of the same file. */
|
|
4
|
+
const ANONYMOUS_DEFAULT_CLASS_KEY = 'default';
|
|
5
|
+
const getClassKey = (node) => node.name?.text ?? ANONYMOUS_DEFAULT_CLASS_KEY;
|
|
6
|
+
/** Contexts that already have an injector of their own, so the call site needs no rewrite. */
|
|
7
|
+
const CONTEXTS_WITH_INJECTOR = [
|
|
8
|
+
'queryComputed',
|
|
9
|
+
'injection-context',
|
|
10
|
+
'class-field',
|
|
11
|
+
'constructor',
|
|
12
|
+
];
|
|
13
|
+
export const migrateLegacyPrepareCalls = (tree, report, scope) => {
|
|
5
14
|
console.log('\n🔍 Checking for legacy query creator usages...');
|
|
6
15
|
const allUsages = [];
|
|
7
|
-
|
|
16
|
+
scope.visit(tree, (filePath) => {
|
|
8
17
|
if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
|
|
9
18
|
return;
|
|
10
19
|
}
|
|
@@ -27,7 +36,7 @@ export const migrateLegacyPrepareCalls = (tree, report) => {
|
|
|
27
36
|
console.log(` - ${context}: ${count} usages`);
|
|
28
37
|
});
|
|
29
38
|
console.log('\n🔍 Detecting polling usage...');
|
|
30
|
-
const pollingInfo = detectPollingUsage(tree);
|
|
39
|
+
const pollingInfo = detectPollingUsage(tree, scope);
|
|
31
40
|
if (pollingInfo.size > 0) {
|
|
32
41
|
console.log(`\n⚠️ Found ${pollingInfo.size} queries with polling:`);
|
|
33
42
|
pollingInfo.forEach((info) => {
|
|
@@ -38,7 +47,7 @@ export const migrateLegacyPrepareCalls = (tree, report) => {
|
|
|
38
47
|
}
|
|
39
48
|
const classInjectors = findOrCreateInjectorMembers(tree, allUsages);
|
|
40
49
|
createInjectorMembers(tree, classInjectors);
|
|
41
|
-
transformLegacyCreatorPrepareCalls(tree, classInjectors, pollingInfo);
|
|
50
|
+
transformLegacyCreatorPrepareCalls(tree, classInjectors, pollingInfo, scope);
|
|
42
51
|
reportManualReviewLocations(tree, allUsages, report);
|
|
43
52
|
};
|
|
44
53
|
const findLegacyCreatorUsages = (content, filePath) => {
|
|
@@ -99,32 +108,88 @@ const findLegacyCreatorNames = (sourceFile) => {
|
|
|
99
108
|
visit(sourceFile);
|
|
100
109
|
return names;
|
|
101
110
|
};
|
|
111
|
+
/** Helpers that run their callback inside an injection context, so a `prepare()` in one needs nothing. */
|
|
112
|
+
const CONTEXT_PROVIDING_CALLEES = new Set([
|
|
113
|
+
'runInInjectionContext',
|
|
114
|
+
'queryComputed',
|
|
115
|
+
'queryComputedTillTruthy',
|
|
116
|
+
'queryArrayComputed',
|
|
117
|
+
]);
|
|
118
|
+
/**
|
|
119
|
+
* Array methods, which call back synchronously in the caller's own context. Matched only on a property
|
|
120
|
+
* access (`items.map(…)`) - the bare-identifier form is an RxJS operator, whose callback runs later.
|
|
121
|
+
*/
|
|
122
|
+
const TRANSPARENT_ARRAY_METHODS = new Set([
|
|
123
|
+
'map',
|
|
124
|
+
'flatMap',
|
|
125
|
+
'filter',
|
|
126
|
+
'forEach',
|
|
127
|
+
'find',
|
|
128
|
+
'findLast',
|
|
129
|
+
'findIndex',
|
|
130
|
+
'some',
|
|
131
|
+
'every',
|
|
132
|
+
'reduce',
|
|
133
|
+
'reduceRight',
|
|
134
|
+
'sort',
|
|
135
|
+
'flat',
|
|
136
|
+
]);
|
|
137
|
+
const isCallbackFunction = (node) => ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isFunctionDeclaration(node);
|
|
138
|
+
/** The call a function literal is being passed to, if it is an argument rather than a value. */
|
|
139
|
+
const getCallbackHost = (node) => {
|
|
140
|
+
const parent = node.parent;
|
|
141
|
+
if (!ts.isCallExpression(parent) || !parent.arguments.some((argument) => argument === node)) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
return parent;
|
|
145
|
+
};
|
|
146
|
+
const isContextProvidingCallback = (node) => {
|
|
147
|
+
const host = getCallbackHost(node);
|
|
148
|
+
if (!host) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
if (ts.isIdentifier(host.expression)) {
|
|
152
|
+
return CONTEXT_PROVIDING_CALLEES.has(host.expression.text);
|
|
153
|
+
}
|
|
154
|
+
return ts.isPropertyAccessExpression(host.expression) && host.expression.name.text === 'runInContext';
|
|
155
|
+
};
|
|
156
|
+
const isTransparentCallback = (node) => {
|
|
157
|
+
const host = getCallbackHost(node);
|
|
158
|
+
return (!!host && ts.isPropertyAccessExpression(host.expression) && TRANSPARENT_ARRAY_METHODS.has(host.expression.name.text));
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Classifies the **innermost** function boundary the call sits behind, not the first structural ancestor.
|
|
162
|
+
* The difference is the whole point: `effect(() => prepare())` in a constructor and
|
|
163
|
+
* `computed(() => prepare())` at a class field both run long after the context that created them, so they
|
|
164
|
+
* need an injector just like a plain method does - reading the constructor or the field as the answer
|
|
165
|
+
* skipped exactly the call sites that go on to throw ET950.
|
|
166
|
+
*/
|
|
102
167
|
const determineUsageContext = (node) => {
|
|
103
168
|
let current = node;
|
|
104
169
|
while (current) {
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
ts.isIdentifier(
|
|
109
|
-
|
|
110
|
-
|
|
170
|
+
if (isCallbackFunction(current)) {
|
|
171
|
+
if (isContextProvidingCallback(current)) {
|
|
172
|
+
const host = getCallbackHost(current);
|
|
173
|
+
const callee = host && ts.isIdentifier(host.expression) ? host.expression.text : undefined;
|
|
174
|
+
return callee?.startsWith('query') ? 'queryComputed' : 'injection-context';
|
|
175
|
+
}
|
|
176
|
+
if (!isTransparentCallback(current)) {
|
|
177
|
+
return isInsideClass(current) ? 'method' : 'function';
|
|
111
178
|
}
|
|
112
179
|
}
|
|
180
|
+
// Reached without crossing a callback: the call really does run while the class is being built, which
|
|
181
|
+
// is an injection context.
|
|
113
182
|
if (ts.isConstructorDeclaration(current)) {
|
|
114
183
|
return 'constructor';
|
|
115
184
|
}
|
|
116
|
-
if (ts.isMethodDeclaration(current)
|
|
185
|
+
if (ts.isMethodDeclaration(current) ||
|
|
186
|
+
ts.isGetAccessorDeclaration(current) ||
|
|
187
|
+
ts.isSetAccessorDeclaration(current)) {
|
|
117
188
|
return 'method';
|
|
118
189
|
}
|
|
119
190
|
if (ts.isPropertyDeclaration(current)) {
|
|
120
191
|
return 'class-field';
|
|
121
192
|
}
|
|
122
|
-
if (ts.isFunctionDeclaration(current)) {
|
|
123
|
-
return 'function';
|
|
124
|
-
}
|
|
125
|
-
if ((ts.isArrowFunction(current) || ts.isFunctionExpression(current)) && !isInsideClass(current)) {
|
|
126
|
-
return 'function';
|
|
127
|
-
}
|
|
128
193
|
current = current.parent;
|
|
129
194
|
}
|
|
130
195
|
return 'unknown';
|
|
@@ -178,7 +243,7 @@ const findOrCreateInjectorMembers = (tree, usages) => {
|
|
|
178
243
|
const classInjectors = new Map();
|
|
179
244
|
const usagesByFile = new Map();
|
|
180
245
|
usages.forEach((usage) => {
|
|
181
|
-
if (
|
|
246
|
+
if (CONTEXTS_WITH_INJECTOR.includes(usage.context)) {
|
|
182
247
|
return;
|
|
183
248
|
}
|
|
184
249
|
if (!usagesByFile.has(usage.filePath)) {
|
|
@@ -203,7 +268,7 @@ const findClassesNeedingInjector = (sourceFile, usages) => {
|
|
|
203
268
|
const filePath = usages[0]?.filePath ?? sourceFile.fileName;
|
|
204
269
|
const visit = (node) => {
|
|
205
270
|
if (ts.isClassDeclaration(node)) {
|
|
206
|
-
const className = node
|
|
271
|
+
const className = getClassKey(node);
|
|
207
272
|
const usagesInClass = usages.filter((usage) => {
|
|
208
273
|
return usage.position.start >= node.getStart(sourceFile) && usage.position.end <= node.getEnd();
|
|
209
274
|
});
|
|
@@ -269,7 +334,7 @@ const addInjectorMemberToClass = (content, classInfo) => {
|
|
|
269
334
|
const sourceFile = createSourceFile(content);
|
|
270
335
|
let targetClass;
|
|
271
336
|
const visit = (node) => {
|
|
272
|
-
if (ts.isClassDeclaration(node) && node
|
|
337
|
+
if (ts.isClassDeclaration(node) && getClassKey(node) === classInfo.className) {
|
|
273
338
|
targetClass = node;
|
|
274
339
|
}
|
|
275
340
|
ts.forEachChild(node, visit);
|
|
@@ -280,10 +345,10 @@ const addInjectorMemberToClass = (content, classInfo) => {
|
|
|
280
345
|
}
|
|
281
346
|
const classBodyStart = targetClass.members.length > 0 ? targetClass.members[0].getStart(sourceFile) : targetClass.getEnd() - 1;
|
|
282
347
|
const indentation = getIndentation(content, classBodyStart);
|
|
283
|
-
const injectorMember =
|
|
348
|
+
const injectorMember = `private ${classInfo.injectorMemberName} = inject(Injector);\n\n${indentation}`;
|
|
284
349
|
return content.slice(0, classBodyStart) + injectorMember + content.slice(classBodyStart);
|
|
285
350
|
};
|
|
286
|
-
const transformLegacyCreatorPrepareCalls = (tree, classInjectors, pollingInfo) => {
|
|
351
|
+
const transformLegacyCreatorPrepareCalls = (tree, classInjectors, pollingInfo, scope) => {
|
|
287
352
|
const updatedFiles = [];
|
|
288
353
|
const filesByPath = new Map();
|
|
289
354
|
classInjectors.forEach((classInfo) => {
|
|
@@ -292,7 +357,7 @@ const transformLegacyCreatorPrepareCalls = (tree, classInjectors, pollingInfo) =
|
|
|
292
357
|
}
|
|
293
358
|
filesByPath.get(classInfo.filePath).push(classInfo);
|
|
294
359
|
});
|
|
295
|
-
|
|
360
|
+
scope.visit(tree, (filePath) => {
|
|
296
361
|
if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts') || filesByPath.has(filePath)) {
|
|
297
362
|
return;
|
|
298
363
|
}
|
|
@@ -343,19 +408,14 @@ const transformPrepareCallsInFile = (content, classes, pollingInfo) => {
|
|
|
343
408
|
return;
|
|
344
409
|
}
|
|
345
410
|
const context = determineUsageContext(node);
|
|
346
|
-
if (
|
|
411
|
+
if (CONTEXTS_WITH_INJECTOR.includes(context)) {
|
|
347
412
|
ts.forEachChild(node, visit);
|
|
348
413
|
return;
|
|
349
414
|
}
|
|
350
415
|
const containingClass = findContainingClass(node);
|
|
351
416
|
const containingFunction = findContainingStandaloneFunction(node);
|
|
352
417
|
if (containingClass) {
|
|
353
|
-
const
|
|
354
|
-
if (!className) {
|
|
355
|
-
ts.forEachChild(node, visit);
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
const injectorMember = classNameToInjector.get(className);
|
|
418
|
+
const injectorMember = classNameToInjector.get(getClassKey(containingClass));
|
|
359
419
|
if (!injectorMember) {
|
|
360
420
|
ts.forEachChild(node, visit);
|
|
361
421
|
return;
|
|
@@ -419,7 +479,7 @@ const transformPrepareCallsInFile = (content, classes, pollingInfo) => {
|
|
|
419
479
|
}
|
|
420
480
|
injectorDeclarations.push({
|
|
421
481
|
position: bodyStart,
|
|
422
|
-
text:
|
|
482
|
+
text: `const ${injectorName} = inject(Injector);\n${getIndentation(content, bodyStart)}`,
|
|
423
483
|
});
|
|
424
484
|
});
|
|
425
485
|
injectorDeclarations.sort((left, right) => right.position - left.position);
|
|
@@ -536,7 +596,7 @@ const transformSinglePrepareCall = (callNode, injectorReference, sourceFile, pol
|
|
|
536
596
|
}
|
|
537
597
|
nextArgumentProperties.push(`injector: ${injectorReference}`);
|
|
538
598
|
const queryVariableName = findQueryVariableNameForPrepareCall(callNode, sourceFile);
|
|
539
|
-
const addDestroyOnResponse = shouldAddDestroyOnResponse(queryVariableName, pollingInfo);
|
|
599
|
+
const addDestroyOnResponse = shouldAddDestroyOnResponse(queryVariableName, pollingInfo, isDiscardedPrepareCall(callNode));
|
|
540
600
|
if (existingConfig && ts.isObjectLiteralExpression(existingConfig.initializer)) {
|
|
541
601
|
const configProperties = [];
|
|
542
602
|
existingConfig.initializer.properties.forEach((property) => {
|
|
@@ -569,7 +629,7 @@ const transformSinglePrepareCall = (callNode, injectorReference, sourceFile, pol
|
|
|
569
629
|
: `{\n ${nextArgumentProperties.join(',\n ')}\n}`;
|
|
570
630
|
return `${creatorName}.prepare(${nextArgumentString})`;
|
|
571
631
|
};
|
|
572
|
-
const findQueryVariableNameForPrepareCall = (prepareCallNode,
|
|
632
|
+
const findQueryVariableNameForPrepareCall = (prepareCallNode, _sourceFile) => {
|
|
573
633
|
let parent = prepareCallNode.parent;
|
|
574
634
|
while (parent) {
|
|
575
635
|
if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
|
|
@@ -663,9 +723,9 @@ const containsInjectCall = (node) => {
|
|
|
663
723
|
visit(node);
|
|
664
724
|
return hasInject;
|
|
665
725
|
};
|
|
666
|
-
const detectPollingUsage = (tree) => {
|
|
726
|
+
const detectPollingUsage = (tree, scope) => {
|
|
667
727
|
const pollingInfo = new Map();
|
|
668
|
-
|
|
728
|
+
scope.visit(tree, (filePath) => {
|
|
669
729
|
if (filePath.endsWith('.spec.ts')) {
|
|
670
730
|
return;
|
|
671
731
|
}
|
|
@@ -717,8 +777,7 @@ const getQueryVariableName = (expression) => {
|
|
|
717
777
|
return currentExpression.text;
|
|
718
778
|
}
|
|
719
779
|
if (ts.isPropertyAccessExpression(currentExpression)) {
|
|
720
|
-
|
|
721
|
-
continue;
|
|
780
|
+
return currentExpression.name.text;
|
|
722
781
|
}
|
|
723
782
|
if (ts.isCallExpression(currentExpression)) {
|
|
724
783
|
currentExpression = currentExpression.expression;
|
|
@@ -764,9 +823,39 @@ const detectPollingInTemplate = (content, filePath, pollingInfo) => {
|
|
|
764
823
|
}
|
|
765
824
|
}
|
|
766
825
|
};
|
|
767
|
-
|
|
826
|
+
/**
|
|
827
|
+
* Whether the prepared query is thrown away - `legacyGetUsers.prepare().execute()` as a statement, with
|
|
828
|
+
* nothing holding on to it. Chained `.poll()` disqualifies it: that query is meant to keep running.
|
|
829
|
+
*/
|
|
830
|
+
const isDiscardedPrepareCall = (callNode) => {
|
|
831
|
+
let current = callNode;
|
|
832
|
+
while (current.parent) {
|
|
833
|
+
const parent = current.parent;
|
|
834
|
+
if (ts.isParenthesizedExpression(parent) || ts.isAwaitExpression(parent) || ts.isNonNullExpression(parent)) {
|
|
835
|
+
current = parent;
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
if (ts.isPropertyAccessExpression(parent) && parent.expression === current) {
|
|
839
|
+
if (parent.name.text === 'poll') {
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
current = parent;
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
// Only a chained call keeps it discarded; being an *argument* means something else receives the query.
|
|
846
|
+
if (ts.isCallExpression(parent) && parent.expression === current) {
|
|
847
|
+
current = parent;
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
return ts.isExpressionStatement(parent);
|
|
851
|
+
}
|
|
852
|
+
return false;
|
|
853
|
+
};
|
|
854
|
+
const shouldAddDestroyOnResponse = (queryVariableName, pollingInfo, isDiscarded) => {
|
|
768
855
|
if (!queryVariableName) {
|
|
769
|
-
|
|
856
|
+
// A discarded query is the one case where nothing else can clean it up: no variable for a container
|
|
857
|
+
// to hold, no caller to destroy it. That is exactly what `destroyOnResponse` is for.
|
|
858
|
+
return isDiscarded;
|
|
770
859
|
}
|
|
771
860
|
return !pollingInfo.get(queryVariableName)?.hasPolling;
|
|
772
861
|
};
|