@ethlete/query 6.0.0-next.61 → 6.0.0-next.63

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 (28) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/fesm2022/ethlete-query-devtools-contract.mjs +1 -1
  3. package/fesm2022/ethlete-query-ngrx-toolkit.mjs +30 -7
  4. package/fesm2022/ethlete-query-ngrx-toolkit.mjs.map +1 -1
  5. package/fesm2022/ethlete-query.mjs +183 -43
  6. package/fesm2022/ethlete-query.mjs.map +1 -1
  7. package/generators/migrate-from-ngrx-toolkit/args-assignability.js +55 -8
  8. package/generators/migrate-from-ngrx-toolkit/consumers.js +133 -37
  9. package/generators/migrate-from-ngrx-toolkit/creators.js +8 -4
  10. package/generators/migrate-from-ngrx-toolkit/feature.js +31 -4
  11. package/generators/migrate-from-ngrx-toolkit/migration.js +3 -1
  12. package/generators/migrate-from-ngrx-toolkit/module-resolution.js +23 -0
  13. package/generators/migrate-from-ngrx-toolkit/store-removal.js +64 -11
  14. package/generators/migrate-to-query-v3/auth-consumers-migration.js +64 -0
  15. package/generators/migrate-to-query-v3/auth-provider-scaffold.js +93 -1
  16. package/generators/migrate-to-query-v3/cleanup-migration.js +135 -15
  17. package/generators/migrate-to-query-v3/legacy-prepare-migration.js +275 -27
  18. package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +171 -25
  19. package/generators/migrate-to-query-v3/migration.js +9 -3
  20. package/generators/migrate-to-query-v3/module-resolution.js +131 -0
  21. package/generators/migrate-to-query-v3/query-client-migration.js +82 -14
  22. package/generators/migrate-to-query-v3/rename-symbols.js +16 -11
  23. package/generators/move-devtools-contract-imports/moved-names.js +1 -0
  24. package/migrations.json +10 -0
  25. package/package.json +1 -1
  26. package/types/ethlete-query-devtools-contract.d.ts +2 -2
  27. package/types/ethlete-query-ngrx-toolkit.d.ts +13 -0
  28. package/types/ethlete-query.d.ts +40 -6
@@ -1,5 +1,8 @@
1
1
  import * as ts from 'typescript';
2
- import { createSourceFile, ensureAngularCoreImports, getIndentation, getLineNumber, getLineNumberFromPosition, } from './shared.js';
2
+ import { findUsedNames, pruneUnusedNamedImports } from './rename-symbols.js';
3
+ import { createSourceFile, ensureAngularCoreImports, ensureImportFromQuery, getIndentation, getLineNumber, getLineNumberFromPosition, } from './shared.js';
4
+ /** The creator methods that resolve an injector, and so throw `ET950` when called without one. */
5
+ const INJECTOR_METHODS = new Set(['prepare', 'createSubject', 'createSignal', 'behaviorSubject']);
3
6
  /** `default` can never be a class name, so it cannot collide with a named class of the same file. */
4
7
  const ANONYMOUS_DEFAULT_CLASS_KEY = 'default';
5
8
  const getClassKey = (node) => node.name?.text ?? ANONYMOUS_DEFAULT_CLASS_KEY;
@@ -22,6 +25,7 @@ export const migrateLegacyPrepareCalls = (tree, report, scope) => {
22
25
  return;
23
26
  }
24
27
  allUsages.push(...findLegacyCreatorUsages(content, filePath));
28
+ reportUnresolvedReceivers(content, filePath, report);
25
29
  });
26
30
  if (allUsages.length === 0) {
27
31
  console.log('\n✅ No legacy query creator usages found');
@@ -60,12 +64,13 @@ const findLegacyCreatorUsages = (content, filePath) => {
60
64
  const visit = (node) => {
61
65
  if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
62
66
  const propertyAccess = node.expression;
63
- if (ts.isIdentifier(propertyAccess.name) && propertyAccess.name.text === 'prepare') {
67
+ if (ts.isIdentifier(propertyAccess.name) && INJECTOR_METHODS.has(propertyAccess.name.text)) {
64
68
  const creatorName = ts.isIdentifier(propertyAccess.expression) ? propertyAccess.expression.text : undefined;
65
69
  if (creatorName && legacyCreatorNames.has(creatorName)) {
66
70
  usages.push({
67
71
  filePath,
68
72
  creatorName,
73
+ method: propertyAccess.name.text,
69
74
  line: getLineNumber(node, sourceFile),
70
75
  position: {
71
76
  start: node.getStart(sourceFile),
@@ -82,6 +87,35 @@ const findLegacyCreatorUsages = (content, filePath) => {
82
87
  visit(sourceFile);
83
88
  return usages;
84
89
  };
90
+ const LEGACY_PROPERTY_NAME = /^legacy[A-Z]/;
91
+ const reportUnresolvedReceivers = (content, filePath, report) => {
92
+ if (!/\blegacy[A-Z]\w*\s*\.(prepare|createSubject|createSignal|behaviorSubject)\(/.test(content)) {
93
+ return;
94
+ }
95
+ const sourceFile = createSourceFile(content, filePath);
96
+ const visit = (node) => {
97
+ if (ts.isCallExpression(node) &&
98
+ ts.isPropertyAccessExpression(node.expression) &&
99
+ ts.isIdentifier(node.expression.name) &&
100
+ INJECTOR_METHODS.has(node.expression.name.text) &&
101
+ ts.isPropertyAccessExpression(node.expression.expression) &&
102
+ LEGACY_PROPERTY_NAME.test(node.expression.expression.name.text) &&
103
+ !CONTEXTS_WITH_INJECTOR.includes(determineUsageContext(node))) {
104
+ const line = getLineNumber(node, sourceFile);
105
+ const receiver = node.expression.expression.getText(sourceFile);
106
+ report.addManualReview({
107
+ title: `Check the injector of ${receiver}.${node.expression.name.text}()`,
108
+ summary: `${receiver}.${node.expression.name.text}() is called on a creator the migration cannot resolve, because the receiver is not a plain legacy* identifier. The call may run outside an injection context and then throws ET950.`,
109
+ action: 'Pass `{ injector }` to the call, or add provideLegacyPrepareFallback() to the app providers. A call in a constructor, a field initializer or an inject() function needs nothing.',
110
+ locations: [{ filePath, line }],
111
+ source: 'legacy-prepare-migration',
112
+ dedupeKey: `unresolved-receiver:${filePath}:${line}`,
113
+ });
114
+ }
115
+ ts.forEachChild(node, visit);
116
+ };
117
+ visit(sourceFile);
118
+ };
85
119
  const findLegacyCreatorNames = (sourceFile) => {
86
120
  const names = new Set();
87
121
  const visit = (node) => {
@@ -330,6 +364,12 @@ const createInjectorMembers = (tree, classInjectors) => {
330
364
  console.log(`\n✅ Added injector members to ${Array.from(filesToUpdate.values()).flat().length} classes`);
331
365
  }
332
366
  };
367
+ const getMemberStartWithComments = (content, member) => {
368
+ const ownLineComment = ts
369
+ .getLeadingCommentRanges(content, member.getFullStart())
370
+ ?.find((range) => /\n[ \t]*$/.test(content.slice(0, range.pos)));
371
+ return ownLineComment?.pos ?? member.getStart();
372
+ };
333
373
  const addInjectorMemberToClass = (content, classInfo) => {
334
374
  const sourceFile = createSourceFile(content);
335
375
  let targetClass;
@@ -343,7 +383,9 @@ const addInjectorMemberToClass = (content, classInfo) => {
343
383
  if (!targetClass) {
344
384
  return content;
345
385
  }
346
- const classBodyStart = targetClass.members.length > 0 ? targetClass.members[0].getStart(sourceFile) : targetClass.getEnd() - 1;
386
+ const classBodyStart = targetClass.members.length > 0
387
+ ? getMemberStartWithComments(content, targetClass.members[0])
388
+ : targetClass.getEnd() - 1;
347
389
  const indentation = getIndentation(content, classBodyStart);
348
390
  const injectorMember = `private ${classInfo.injectorMemberName} = inject(Injector);\n\n${indentation}`;
349
391
  return content.slice(0, classBodyStart) + injectorMember + content.slice(classBodyStart);
@@ -362,7 +404,7 @@ const transformLegacyCreatorPrepareCalls = (tree, classInjectors, pollingInfo, s
362
404
  return;
363
405
  }
364
406
  const content = tree.read(filePath, 'utf-8');
365
- if (!content || !content.includes('.prepare(')) {
407
+ if (!content || !/\.(prepare|createSubject|createSignal|behaviorSubject)\(/.test(content)) {
366
408
  return;
367
409
  }
368
410
  const usages = findLegacyCreatorUsages(content, filePath);
@@ -371,12 +413,11 @@ const transformLegacyCreatorPrepareCalls = (tree, classInjectors, pollingInfo, s
371
413
  }
372
414
  });
373
415
  filesByPath.forEach((classes, filePath) => {
374
- let content = tree.read(filePath, 'utf-8');
416
+ const content = tree.read(filePath, 'utf-8');
375
417
  if (!content) {
376
418
  return;
377
419
  }
378
- content = ensureAngularCoreImports(content, ['inject', 'Injector']);
379
- const nextContent = transformPrepareCallsInFile(content, classes, pollingInfo);
420
+ const nextContent = pruneUnusedNamedImports(transformPrepareCallsInFile(ensureAngularCoreImports(content, ['inject', 'Injector']), classes, pollingInfo), new Set([...findUsedNames(content), 'inject', 'Injector']));
380
421
  if (nextContent !== content) {
381
422
  tree.write(filePath, nextContent);
382
423
  updatedFiles.push(filePath);
@@ -391,6 +432,7 @@ const transformPrepareCallsInFile = (content, classes, pollingInfo) => {
391
432
  const replacements = [];
392
433
  const legacyCreatorNames = findLegacyCreatorNames(sourceFile);
393
434
  const classNameToInjector = new Map();
435
+ const rewrittenComputeds = new Set();
394
436
  const functionsNeedingInjector = new Map();
395
437
  if (legacyCreatorNames.size === 0) {
396
438
  return content;
@@ -401,13 +443,31 @@ const transformPrepareCallsInFile = (content, classes, pollingInfo) => {
401
443
  const visit = (node) => {
402
444
  if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
403
445
  const propertyAccess = node.expression;
404
- if (ts.isIdentifier(propertyAccess.name) && propertyAccess.name.text === 'prepare') {
446
+ if (ts.isIdentifier(propertyAccess.name) && INJECTOR_METHODS.has(propertyAccess.name.text)) {
405
447
  const creatorName = ts.isIdentifier(propertyAccess.expression) ? propertyAccess.expression.text : undefined;
406
448
  if (!creatorName || !legacyCreatorNames.has(creatorName)) {
407
449
  ts.forEachChild(node, visit);
408
450
  return;
409
451
  }
410
452
  const context = determineUsageContext(node);
453
+ const computedHost = findReturningPlainComputed(node);
454
+ const rewriteComputed = (injectorExpression) => {
455
+ if (!computedHost || rewrittenComputeds.has(computedHost))
456
+ return;
457
+ rewrittenComputeds.add(computedHost);
458
+ replacements.push({
459
+ start: computedHost.expression.getStart(sourceFile),
460
+ end: computedHost.expression.getEnd(),
461
+ replacement: 'queryComputed',
462
+ });
463
+ if (computedHost.arguments.length === 1) {
464
+ replacements.push({
465
+ start: computedHost.arguments[0].getEnd(),
466
+ end: computedHost.arguments[0].getEnd(),
467
+ replacement: `, { injector: ${injectorExpression} }`,
468
+ });
469
+ }
470
+ };
411
471
  if (CONTEXTS_WITH_INJECTOR.includes(context)) {
412
472
  ts.forEachChild(node, visit);
413
473
  return;
@@ -420,7 +480,8 @@ const transformPrepareCallsInFile = (content, classes, pollingInfo) => {
420
480
  ts.forEachChild(node, visit);
421
481
  return;
422
482
  }
423
- const transformedCall = transformSinglePrepareCall(node, `this.${injectorMember}`, sourceFile, pollingInfo);
483
+ rewriteComputed(`this.${injectorMember}`);
484
+ const transformedCall = transformCall(node, `this.${injectorMember}`, sourceFile, pollingInfo);
424
485
  if (transformedCall) {
425
486
  replacements.push({
426
487
  start: node.getStart(sourceFile),
@@ -432,15 +493,22 @@ const transformPrepareCallsInFile = (content, classes, pollingInfo) => {
432
493
  else if (containingFunction) {
433
494
  const functionWithInject = findOutermostFunctionWithInject(containingFunction);
434
495
  if (!functionWithInject) {
496
+ if (propertyAccess.name.text === 'prepare' && isInsideAsyncValidator(node)) {
497
+ replacements.push({
498
+ start: node.getStart(sourceFile),
499
+ end: node.getEnd(),
500
+ replacement: transformSinglePrepareCall(node, undefined, sourceFile, pollingInfo),
501
+ });
502
+ }
435
503
  ts.forEachChild(node, visit);
436
504
  return;
437
505
  }
438
506
  const functionPosition = functionWithInject.getStart(sourceFile);
439
507
  if (!functionsNeedingInjector.has(functionPosition)) {
440
- const existingInjectorParameter = findInjectorParameter(functionWithInject);
441
- if (existingInjectorParameter) {
508
+ const existingInjector = findExistingInjector(functionWithInject);
509
+ if (existingInjector) {
442
510
  functionsNeedingInjector.set(functionPosition, {
443
- injectorName: existingInjectorParameter,
511
+ injectorName: existingInjector,
444
512
  bodyStart: -1,
445
513
  needsDeclaration: false,
446
514
  });
@@ -457,7 +525,8 @@ const transformPrepareCallsInFile = (content, classes, pollingInfo) => {
457
525
  }
458
526
  }
459
527
  const injectorName = functionsNeedingInjector.get(functionPosition)?.injectorName ?? 'injector';
460
- const transformedCall = transformSinglePrepareCall(node, injectorName, sourceFile, pollingInfo);
528
+ rewriteComputed(injectorName);
529
+ const transformedCall = transformCall(node, injectorName, sourceFile, pollingInfo);
461
530
  if (transformedCall) {
462
531
  replacements.push({
463
532
  start: node.getStart(sourceFile),
@@ -497,9 +566,9 @@ const transformPrepareCallsInFile = (content, classes, pollingInfo) => {
497
566
  });
498
567
  result = result.slice(0, start + adjustment) + replacement + result.slice(end + adjustment);
499
568
  });
500
- return result;
569
+ return rewrittenComputeds.size > 0 ? ensureImportFromQuery(result, ['queryComputed']) : result;
501
570
  };
502
- const findInjectorParameter = (func) => {
571
+ const findExistingInjector = (func) => {
503
572
  for (const parameter of func.parameters) {
504
573
  if (!ts.isIdentifier(parameter.name)) {
505
574
  continue;
@@ -511,6 +580,23 @@ const findInjectorParameter = (func) => {
511
580
  return parameter.name.text;
512
581
  }
513
582
  }
583
+ if (!func.body || !ts.isBlock(func.body))
584
+ return undefined;
585
+ for (const statement of func.body.statements) {
586
+ if (!ts.isVariableStatement(statement))
587
+ continue;
588
+ for (const declaration of statement.declarationList.declarations) {
589
+ const initializer = declaration.initializer;
590
+ if (ts.isIdentifier(declaration.name) &&
591
+ initializer &&
592
+ ts.isCallExpression(initializer) &&
593
+ ts.isIdentifier(initializer.expression) &&
594
+ initializer.expression.text === 'inject' &&
595
+ initializer.arguments[0]?.getText() === 'Injector') {
596
+ return declaration.name.text;
597
+ }
598
+ }
599
+ }
514
600
  return undefined;
515
601
  };
516
602
  const getFunctionBodyStart = (func, sourceFile) => {
@@ -560,6 +646,26 @@ const findContainingClass = (node) => {
560
646
  }
561
647
  return undefined;
562
648
  };
649
+ const transformCall = (callNode, injectorReference, sourceFile, pollingInfo) => callNode.expression.name.text === 'prepare'
650
+ ? transformSinglePrepareCall(callNode, injectorReference, sourceFile, pollingInfo)
651
+ : transformContainerCall(callNode, injectorReference, sourceFile);
652
+ const transformContainerCall = (callNode, injectorReference, sourceFile) => {
653
+ const propertyAccess = callNode.expression;
654
+ const initialValue = callNode.arguments[0]?.getText(sourceFile) ?? 'null';
655
+ const config = callNode.arguments[1];
656
+ let nextConfig = `{ injector: ${injectorReference} }`;
657
+ if (config && ts.isObjectLiteralExpression(config)) {
658
+ const hasInjector = config.properties.some((property) => !!property.name && ts.isIdentifier(property.name) && property.name.text === 'injector');
659
+ if (hasInjector) {
660
+ return undefined;
661
+ }
662
+ nextConfig = `{ ${[...config.properties.map((property) => property.getText(sourceFile)), `injector: ${injectorReference}`].join(', ')} }`;
663
+ }
664
+ else if (config) {
665
+ nextConfig = `{ ...${config.getText(sourceFile)}, injector: ${injectorReference} }`;
666
+ }
667
+ return `${propertyAccess.expression.getText(sourceFile)}.${propertyAccess.name.text}(${initialValue}, ${nextConfig})`;
668
+ };
563
669
  const transformSinglePrepareCall = (callNode, injectorReference, sourceFile, pollingInfo) => {
564
670
  const propertyAccess = callNode.expression;
565
671
  const creatorName = propertyAccess.expression.text;
@@ -594,9 +700,10 @@ const transformSinglePrepareCall = (callNode, injectorReference, sourceFile, pol
594
700
  spreadExpression = existingArgument.getText(sourceFile);
595
701
  }
596
702
  }
597
- nextArgumentProperties.push(`injector: ${injectorReference}`);
703
+ if (injectorReference)
704
+ nextArgumentProperties.push(`injector: ${injectorReference}`);
598
705
  const queryVariableName = findQueryVariableNameForPrepareCall(callNode, sourceFile);
599
- const addDestroyOnResponse = shouldAddDestroyOnResponse(queryVariableName, pollingInfo, isDiscardedPrepareCall(callNode));
706
+ const addDestroyOnResponse = shouldAddDestroyOnResponse(queryVariableName, pollingInfo, isDiscardedPrepareCall(callNode), isOneShotLocalVariable(callNode) || isInsideAsyncValidator(callNode));
600
707
  if (existingConfig && ts.isObjectLiteralExpression(existingConfig.initializer)) {
601
708
  const configProperties = [];
602
709
  existingConfig.initializer.properties.forEach((property) => {
@@ -685,9 +792,9 @@ const reportManualReviewLocations = (tree, allUsages, report) => {
685
792
  visit(sourceFile);
686
793
  if (foundFunctionWithoutInject) {
687
794
  report.addManualReview({
688
- title: `Review standalone prepare() usage for ${usage.creatorName}`,
689
- summary: `${usage.creatorName}.prepare() is used in a standalone function without an obvious injection context.`,
690
- action: 'Pass an Injector explicitly, move the call into queryComputed(), or wrap the logic in a function that already uses inject().',
795
+ title: `Review standalone ${usage.method}() usage for ${usage.creatorName}`,
796
+ summary: `${usage.creatorName}.${usage.method}() is used in a standalone function without an obvious injection context.`,
797
+ action: 'If the factory that returns this function runs in an injection context, add `const injector = inject(Injector);` at the top of the factory and pass `{ injector }` to the call. Otherwise pass an Injector from the caller, move the call into queryComputed(), or add provideLegacyPrepareFallback() to the app providers.',
691
798
  locations: [{ filePath: usage.filePath, line: usage.line }],
692
799
  source: 'legacy-prepare-migration',
693
800
  dedupeKey: `manual-review:${usage.filePath}:${usage.line}:${usage.creatorName}`,
@@ -697,7 +804,7 @@ const reportManualReviewLocations = (tree, allUsages, report) => {
697
804
  if (usage.context === 'unknown') {
698
805
  report.addManualReview({
699
806
  title: `Verify execution context for ${usage.creatorName}`,
700
- summary: `The migration could not classify the execution context of ${usage.creatorName}.prepare().`,
807
+ summary: `The migration could not classify the execution context of ${usage.creatorName}.${usage.method}().`,
701
808
  action: 'Inspect the surrounding code and confirm that the generated injector handling and destroyOnResponse behavior are correct.',
702
809
  locations: [{ filePath: usage.filePath, line: usage.line }],
703
810
  source: 'legacy-prepare-migration',
@@ -851,12 +958,153 @@ const isDiscardedPrepareCall = (callNode) => {
851
958
  }
852
959
  return false;
853
960
  };
854
- const shouldAddDestroyOnResponse = (queryVariableName, pollingInfo, isDiscarded) => {
855
- if (!queryVariableName) {
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;
961
+ const unwrapChainEnd = (callNode) => {
962
+ let current = callNode;
963
+ while (current.parent) {
964
+ const parent = current.parent;
965
+ if (ts.isParenthesizedExpression(parent) || ts.isNonNullExpression(parent)) {
966
+ current = parent;
967
+ continue;
968
+ }
969
+ const call = parent.parent;
970
+ if (ts.isPropertyAccessExpression(parent) &&
971
+ parent.expression === current &&
972
+ parent.name.text === 'execute' &&
973
+ call &&
974
+ ts.isCallExpression(call) &&
975
+ call.expression === parent) {
976
+ current = call;
977
+ continue;
978
+ }
979
+ break;
980
+ }
981
+ return current;
982
+ };
983
+ /**
984
+ * The Angular `computed(() => legacy.prepare(...).execute())` whose callback returns the prepared query.
985
+ * Such a computed builds one unmanaged query per input change, so it becomes `queryComputed`, whose
986
+ * container destroys the superseded one.
987
+ */
988
+ const findReturningPlainComputed = (callNode) => {
989
+ const chainEnd = unwrapChainEnd(callNode);
990
+ const callback = chainEnd.parent;
991
+ if (!callback || !ts.isArrowFunction(callback) || callback.body !== chainEnd) {
992
+ const returnStatement = chainEnd.parent;
993
+ if (!returnStatement || !ts.isReturnStatement(returnStatement))
994
+ return undefined;
995
+ let fn = returnStatement.parent;
996
+ while (fn && !isCallbackFunction(fn))
997
+ fn = fn.parent;
998
+ return fn && isCallbackFunction(fn) ? getPlainComputedHost(fn) : undefined;
999
+ }
1000
+ return getPlainComputedHost(callback);
1001
+ };
1002
+ const getPlainComputedHost = (node) => {
1003
+ const host = getCallbackHost(node);
1004
+ if (!host || !ts.isIdentifier(host.expression) || host.expression.text !== 'computed' || host.arguments[0] !== node) {
1005
+ return undefined;
1006
+ }
1007
+ return host;
1008
+ };
1009
+ const ASYNC_VALIDATOR_PROPERTIES = new Set(['asyncValidator', 'asyncValidators']);
1010
+ const ASYNC_VALIDATOR_SETTERS = new Set(['setAsyncValidators', 'addAsyncValidators']);
1011
+ /**
1012
+ * Whether a function around the call is an Angular async validator: a value of an `asyncValidator(s)` property or
1013
+ * setter argument, an `AsyncValidatorFn` or a factory of one, a function returning `Observable`/`Promise` of
1014
+ * `ValidationErrors`, or `validate` of an `AsyncValidator`.
1015
+ */
1016
+ const isInsideAsyncValidator = (callNode) => {
1017
+ for (let node = callNode.parent; node; node = node.parent) {
1018
+ if (ts.isMethodDeclaration(node))
1019
+ return isAsyncValidatorMethod(node);
1020
+ if (isCallbackFunction(node) && isAsyncValidatorFunction(node))
1021
+ return true;
1022
+ }
1023
+ return false;
1024
+ };
1025
+ const isAsyncValidatorMethod = (method) => {
1026
+ const owner = method.parent;
1027
+ return (ts.isIdentifier(method.name) &&
1028
+ method.name.text === 'validate' &&
1029
+ ts.isClassDeclaration(owner) &&
1030
+ !!owner.heritageClauses?.some((clause) => clause.types.some((type) => type.expression.getText() === 'AsyncValidator')));
1031
+ };
1032
+ const ASYNC_VALIDATOR_SIGNATURE = /\bAsyncValidatorFn\b|\b(?:Observable|Promise)<\s*ValidationErrors\b/;
1033
+ const isAsyncValidatorFunction = (fn) => {
1034
+ if (fn.type && ASYNC_VALIDATOR_SIGNATURE.test(fn.type.getText()))
1035
+ return true;
1036
+ let value = fn;
1037
+ if (ts.isArrayLiteralExpression(value.parent))
1038
+ value = value.parent;
1039
+ const parent = value.parent;
1040
+ if (ts.isPropertyAssignment(parent) && ts.isIdentifier(parent.name)) {
1041
+ return ASYNC_VALIDATOR_PROPERTIES.has(parent.name.text);
1042
+ }
1043
+ if (ts.isCallExpression(parent) && ts.isPropertyAccessExpression(parent.expression)) {
1044
+ return ASYNC_VALIDATOR_SETTERS.has(parent.expression.name.text);
1045
+ }
1046
+ if ((ts.isVariableDeclaration(parent) || ts.isPropertyDeclaration(parent)) && parent.type) {
1047
+ return parent.type.getText().includes('AsyncValidatorFn');
1048
+ }
1049
+ return false;
1050
+ };
1051
+ const ONE_SHOT_QUERY_MEMBERS = ['execute', 'onSuccess', 'onFailure'];
1052
+ const isOneShotLocalVariable = (callNode) => {
1053
+ let chainEnd = callNode;
1054
+ let executeCalls = 0;
1055
+ while (chainEnd.parent) {
1056
+ const parent = chainEnd.parent;
1057
+ if (ts.isPropertyAccessExpression(parent) && parent.expression === chainEnd) {
1058
+ if (!ONE_SHOT_QUERY_MEMBERS.includes(parent.name.text))
1059
+ return false;
1060
+ if (parent.name.text === 'execute')
1061
+ executeCalls++;
1062
+ chainEnd = parent;
1063
+ continue;
1064
+ }
1065
+ if (ts.isCallExpression(parent) && parent.expression === chainEnd) {
1066
+ chainEnd = parent;
1067
+ continue;
1068
+ }
1069
+ break;
1070
+ }
1071
+ const declaration = chainEnd.parent;
1072
+ if (!declaration ||
1073
+ !ts.isVariableDeclaration(declaration) ||
1074
+ declaration.initializer !== chainEnd ||
1075
+ !ts.isIdentifier(declaration.name)) {
1076
+ return false;
859
1077
  }
860
- return !pollingInfo.get(queryVariableName)?.hasPolling;
1078
+ const scope = declaration.parent.parent.parent;
1079
+ if (!ts.isVariableStatement(declaration.parent.parent) || !ts.isBlock(scope))
1080
+ return false;
1081
+ const variableName = declaration.name.text;
1082
+ let isOneShot = executeCalls <= 1;
1083
+ const visit = (node) => {
1084
+ if (!isOneShot)
1085
+ return;
1086
+ if (ts.isIdentifier(node) && node.text === variableName && node !== declaration.name) {
1087
+ const parent = node.parent;
1088
+ if (ts.isPropertyAccessExpression(parent) &&
1089
+ parent.expression === node &&
1090
+ ONE_SHOT_QUERY_MEMBERS.includes(parent.name.text)) {
1091
+ if (parent.name.text === 'execute')
1092
+ executeCalls++;
1093
+ if (executeCalls > 1)
1094
+ isOneShot = false;
1095
+ }
1096
+ else if (!(ts.isPropertyAccessExpression(parent) && parent.name === node)) {
1097
+ isOneShot = false;
1098
+ }
1099
+ }
1100
+ ts.forEachChild(node, visit);
1101
+ };
1102
+ visit(scope);
1103
+ return isOneShot;
1104
+ };
1105
+ const shouldAddDestroyOnResponse = (queryVariableName, pollingInfo, isDiscarded, isOneShotLocal) => {
1106
+ if (queryVariableName && pollingInfo.get(queryVariableName)?.hasPolling)
1107
+ return false;
1108
+ return isDiscarded || isOneShotLocal;
861
1109
  };
862
1110
  //# sourceMappingURL=legacy-prepare-migration.js.map