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

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 (54) hide show
  1. package/CHANGELOG.md +945 -0
  2. package/fesm2022/ethlete-query-devtools-contract.mjs +1 -0
  3. package/fesm2022/ethlete-query-ngrx-toolkit.mjs +411 -0
  4. package/fesm2022/ethlete-query-ngrx-toolkit.mjs.map +1 -0
  5. package/fesm2022/ethlete-query-testing.mjs +548 -169
  6. package/fesm2022/ethlete-query-testing.mjs.map +1 -1
  7. package/fesm2022/ethlete-query.mjs +14680 -7172
  8. package/fesm2022/ethlete-query.mjs.map +1 -1
  9. package/generators/deprecate-legacy-queries/migration.js +85 -0
  10. package/generators/deprecate-legacy-queries/schema.json +26 -0
  11. package/generators/generators.json +30 -0
  12. package/generators/migrate-from-ngrx-toolkit/args-assignability.js +140 -0
  13. package/generators/migrate-from-ngrx-toolkit/consumers.js +521 -0
  14. package/generators/migrate-from-ngrx-toolkit/creators.js +301 -0
  15. package/generators/migrate-from-ngrx-toolkit/facade.js +295 -0
  16. package/generators/migrate-from-ngrx-toolkit/feature.js +513 -0
  17. package/generators/migrate-from-ngrx-toolkit/migration.js +140 -0
  18. package/generators/migrate-from-ngrx-toolkit/report.js +154 -0
  19. package/generators/migrate-from-ngrx-toolkit/schema.json +42 -0
  20. package/generators/migrate-from-ngrx-toolkit/store-removal.js +633 -0
  21. package/generators/migrate-from-ngrx-toolkit/ts-edits.js +128 -0
  22. package/generators/migrate-query-client-features/migration.js +144 -0
  23. package/generators/migrate-query-client-features/report.js +136 -0
  24. package/generators/migrate-query-client-features/schema.json +26 -0
  25. package/generators/migrate-query-opt-in-features/migration.js +195 -0
  26. package/generators/migrate-query-opt-in-features/report.js +143 -0
  27. package/generators/migrate-query-opt-in-features/schema.json +31 -0
  28. package/generators/migrate-to-query-v3/auth-provider-scaffold.js +160 -0
  29. package/generators/migrate-to-query-v3/cleanup-migration.js +247 -125
  30. package/generators/migrate-to-query-v3/http-client-check.js +93 -0
  31. package/generators/migrate-to-query-v3/legacy-prepare-migration.js +129 -40
  32. package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +274 -152
  33. package/generators/migrate-to-query-v3/migration-scope.js +40 -0
  34. package/generators/migrate-to-query-v3/migration.js +19 -10
  35. package/generators/migrate-to-query-v3/module-graph.js +165 -0
  36. package/generators/migrate-to-query-v3/query-client-migration.js +342 -160
  37. package/generators/migrate-to-query-v3/rename-symbols.js +233 -0
  38. package/generators/migrate-to-query-v3/report.js +40 -7
  39. package/generators/migrate-to-query-v3/schema.json +12 -0
  40. package/generators/migrate-to-query-v3/shared.js +97 -3
  41. package/generators/move-devtools-contract-imports/migration.js +155 -0
  42. package/generators/move-devtools-contract-imports/moved-names.js +153 -0
  43. package/generators/move-devtools-contract-imports/schema.json +26 -0
  44. package/generators/prep-for-query-v3/migration.js +465 -65
  45. package/generators/report-legacy-query-apis/migration.js +256 -0
  46. package/generators/report-legacy-query-apis/schema.json +25 -0
  47. package/migrations/query-field-default-types.md +43 -0
  48. package/migrations/search-query-field-string.md +43 -0
  49. package/migrations.json +87 -0
  50. package/package.json +34 -14
  51. package/types/ethlete-query-devtools-contract.d.ts +2 -0
  52. package/types/ethlete-query-ngrx-toolkit.d.ts +131 -0
  53. package/types/ethlete-query-testing.d.ts +295 -160
  54. package/types/ethlete-query.d.ts +9755 -3682
@@ -0,0 +1,143 @@
1
+ export const QUERY_OPT_IN_FEATURES_REPORT_PATH = 'query-opt-in-features-migration-tasks.md';
2
+ export const MIGRATION_TASK_KIND = {
3
+ WARNING: 'warning',
4
+ MANUAL_REVIEW: 'manual-review',
5
+ FOLLOW_UP: 'follow-up',
6
+ };
7
+ const renderTaskLocations = (locations) => {
8
+ if (locations.length === 0) {
9
+ return '- None recorded';
10
+ }
11
+ return locations.map((location) => `- ${location.filePath}${location.line ? `:${location.line}` : ''}`).join('\n');
12
+ };
13
+ export class QueryOptInFeaturesMigrationReport {
14
+ constructor() {
15
+ this.tasks = [];
16
+ }
17
+ addTask(input) {
18
+ const dedupeKey = input.dedupeKey ?? `${input.kind}:${input.title}:${input.summary}:${input.action}`;
19
+ const existing = this.tasks.find((task) => task.dedupeKey === dedupeKey);
20
+ if (existing) {
21
+ existing.locations.push(...(input.locations ?? []));
22
+ return;
23
+ }
24
+ this.tasks.push({
25
+ id: `QOF-${String(this.tasks.length + 1).padStart(3, '0')}`,
26
+ kind: input.kind,
27
+ title: input.title,
28
+ summary: input.summary,
29
+ action: input.action,
30
+ locations: input.locations ?? [],
31
+ source: input.source,
32
+ dedupeKey,
33
+ });
34
+ }
35
+ addWarning(input) {
36
+ this.addTask({ ...input, kind: MIGRATION_TASK_KIND.WARNING });
37
+ }
38
+ addManualReview(input) {
39
+ this.addTask({ ...input, kind: MIGRATION_TASK_KIND.MANUAL_REVIEW });
40
+ }
41
+ addFollowUp(input) {
42
+ this.addTask({ ...input, kind: MIGRATION_TASK_KIND.FOLLOW_UP });
43
+ }
44
+ printSummary() {
45
+ if (this.tasks.length === 0) {
46
+ return;
47
+ }
48
+ console.warn(`\n⚠️ Generated ${this.tasks.length} follow-up task${this.tasks.length === 1 ? '' : 's'}:`);
49
+ this.tasks.forEach((task) => {
50
+ const locations = task.locations
51
+ .map((location) => `${location.filePath}${location.line ? `:${location.line}` : ''}`)
52
+ .join(', ');
53
+ console.warn(` - ${task.id} [${task.kind}] ${task.title}${locations ? ` (${locations})` : ''}`);
54
+ });
55
+ console.warn(`\n📄 See ${QUERY_OPT_IN_FEATURES_REPORT_PATH} for the AI-ready follow-up list.`);
56
+ }
57
+ writeToTree(tree) {
58
+ tree.write(QUERY_OPT_IN_FEATURES_REPORT_PATH, renderMigrationReportMarkdown(this.tasks));
59
+ }
60
+ }
61
+ export const renderMigrationReportMarkdown = (tasks) => {
62
+ const header = [
63
+ '# Query Opt-In Features Migration Follow-Up',
64
+ '',
65
+ 'This file is generated by the `migrate-query-opt-in-features` generator.',
66
+ '',
67
+ '## Context',
68
+ '',
69
+ 'Two things that used to be on by default are now opt-in features:',
70
+ '',
71
+ '- error-response parsing (HTML error pages, Symfony / class-validator violation lists) and the',
72
+ ' default retry policy, now `withEthleteApiErrors()` on `createQueryClient`,',
73
+ '- bearer-auth multi-tab sync and leader election, now `withBearerAuthMultiTabSync()` on',
74
+ ' `createBearerAuthProvider` (the `multiTabSync` config option is gone).',
75
+ '',
76
+ 'The codemod is **behavior preserving**: it adds the features every call site used to get for free.',
77
+ 'That is rarely what an app wants long term — an API that never answers with an HTML error page, or',
78
+ 'an app that is only ever open in one tab, ships that code for nothing. Review each added feature',
79
+ 'and delete the ones you do not need; `withEthleteApiErrors()` in particular splits into',
80
+ '`withHtmlErrorParsing()`, `withSymfonyErrors()` and `withDefaultRetry()`.',
81
+ '',
82
+ '## AI Instructions',
83
+ '',
84
+ '1. Re-read the referenced files before changing anything. The migration output is only a starting point.',
85
+ '2. Prefer minimal fixes that align with the current runtime APIs in `@ethlete/query`.',
86
+ '3. Update or remove a task only after the code and tests confirm that it is resolved.',
87
+ '4. If a task is still ambiguous after inspection, keep it open and append a short note instead of guessing.',
88
+ '',
89
+ '## Task Format',
90
+ '',
91
+ '- `Status`: always treat generated tasks as `open` until verified manually or by tests.',
92
+ '- `Kind`: `warning`, `manual-review`, or `follow-up`.',
93
+ '- `Source`: which migration phase raised the task.',
94
+ '',
95
+ ].join('\n');
96
+ if (tasks.length === 0) {
97
+ return `${header}## Tasks\n\nNo open follow-up tasks were recorded during this migration run.\n`;
98
+ }
99
+ const tasksSection = tasks
100
+ .map((task) => [
101
+ `### ${task.id} - ${task.title}`,
102
+ '',
103
+ '- Status: open',
104
+ `- Kind: ${task.kind}`,
105
+ `- Source: ${task.source}`,
106
+ `- Summary: ${task.summary}`,
107
+ `- Suggested action: ${task.action}`,
108
+ '- Locations:',
109
+ renderTaskLocations(task.locations),
110
+ '',
111
+ ].join('\n'))
112
+ .join('');
113
+ const machineSection = [
114
+ '## Machine Summary',
115
+ '',
116
+ '```yaml',
117
+ 'tasks:',
118
+ ...tasks.flatMap((task) => {
119
+ const nestedLocations = task.locations.flatMap((location) => {
120
+ const lines = [` - filePath: ${location.filePath}`];
121
+ if (location.line) {
122
+ lines.push(` line: ${location.line}`);
123
+ }
124
+ return lines;
125
+ });
126
+ return [
127
+ ` - id: ${task.id}`,
128
+ ` status: open`,
129
+ ` kind: ${task.kind}`,
130
+ ` source: ${task.source}`,
131
+ ` title: ${task.title}`,
132
+ ` summary: ${task.summary}`,
133
+ ` action: ${task.action}`,
134
+ ` locations:`,
135
+ ...(nestedLocations.length > 0 ? nestedLocations : [' []']),
136
+ ];
137
+ }),
138
+ '```',
139
+ '',
140
+ ].join('\n');
141
+ return `${header}## Tasks\n\n${tasksSection}${machineSection}`;
142
+ };
143
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1,31 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "$id": "migrate-query-opt-in-features",
4
+ "title": "Migrate query clients and auth providers to opt-in features",
5
+ "type": "object",
6
+ "properties": {
7
+ "projects": {
8
+ "type": "array",
9
+ "items": { "type": "string" },
10
+ "description": "Nx projects to migrate. Defaults to every project in the workspace.",
11
+ "alias": "project"
12
+ },
13
+ "include": {
14
+ "type": "array",
15
+ "items": { "type": "string" },
16
+ "description": "Path prefixes to migrate, relative to the workspace root (e.g. 'libs/queries'). Unioned with `projects`. Defaults to the whole workspace.",
17
+ "alias": "includePath"
18
+ },
19
+ "reportOnly": {
20
+ "type": "boolean",
21
+ "description": "Only list the affected call sites in the report file, without changing any code.",
22
+ "default": false
23
+ },
24
+ "skipFormat": {
25
+ "type": "boolean",
26
+ "description": "Skip formatting files after migration",
27
+ "default": false
28
+ }
29
+ },
30
+ "required": []
31
+ }
@@ -0,0 +1,160 @@
1
+ import * as ts from 'typescript';
2
+ import { createSourceFile } from './shared.js';
3
+ const REFRESH_CONFIG_KEYS = new Set([
4
+ 'queryCreator',
5
+ 'responseAdapter',
6
+ 'requestArgsAdapter',
7
+ 'cookieName',
8
+ 'cookieDomain',
9
+ 'cookieExpiresInDays',
10
+ 'expiresInPropertyName',
11
+ 'cookiePath',
12
+ 'cookieSameSite',
13
+ 'cookieEnabled',
14
+ 'refreshBuffer',
15
+ ]);
16
+ export const collectV2BearerAuthConfigs = (tree, scope) => {
17
+ const configs = [];
18
+ scope.visit(tree, (filePath) => {
19
+ if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
20
+ return;
21
+ }
22
+ const content = tree.read(filePath, 'utf-8');
23
+ if (!content || !content.includes('V2BearerAuthProvider')) {
24
+ return;
25
+ }
26
+ const sourceFile = createSourceFile(content, filePath);
27
+ const visit = (node) => {
28
+ if (ts.isNewExpression(node) &&
29
+ ts.isIdentifier(node.expression) &&
30
+ node.expression.text === 'V2BearerAuthProvider') {
31
+ const configArgument = node.arguments?.[0];
32
+ if (configArgument && ts.isObjectLiteralExpression(configArgument)) {
33
+ configs.push(readConfig(configArgument, sourceFile, filePath, node));
34
+ }
35
+ }
36
+ ts.forEachChild(node, visit);
37
+ };
38
+ visit(sourceFile);
39
+ });
40
+ return configs;
41
+ };
42
+ const readConfig = (configArgument, sourceFile, filePath, node) => {
43
+ const config = {
44
+ filePath,
45
+ line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
46
+ };
47
+ const refreshConfig = configArgument.properties.find((property) => ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === 'refreshConfig');
48
+ if (!refreshConfig ||
49
+ !ts.isPropertyAssignment(refreshConfig) ||
50
+ !ts.isObjectLiteralExpression(refreshConfig.initializer)) {
51
+ return config;
52
+ }
53
+ refreshConfig.initializer.properties.forEach((property) => {
54
+ const isShorthand = ts.isShorthandPropertyAssignment(property);
55
+ if ((!ts.isPropertyAssignment(property) && !isShorthand) || !ts.isIdentifier(property.name)) {
56
+ return;
57
+ }
58
+ const key = property.name.text;
59
+ if (!REFRESH_CONFIG_KEYS.has(key)) {
60
+ return;
61
+ }
62
+ const value = isShorthand ? property.name : property.initializer;
63
+ const text = value.getText(sourceFile);
64
+ if (key === 'queryCreator') {
65
+ if (ts.isIdentifier(value)) {
66
+ config.refreshCreatorName = value.text;
67
+ }
68
+ return;
69
+ }
70
+ config[key] = text;
71
+ });
72
+ return config;
73
+ };
74
+ const REFRESH_QUERY_KEY = 'tokenRefresh';
75
+ /**
76
+ * Renders the `queries` / `features` arguments of a v3 provider from a v2 config.
77
+ *
78
+ * The mapping is close to 1:1 - `queryCreator` → `withRefreshQuery`, `responseAdapter` →
79
+ * `extractTokens`, `cookieName` → `withPersistentAuth({ cookie })` - but the adapters are copied
80
+ * verbatim from v2 and their parameter shapes differ, so each one is emitted with a TODO rather
81
+ * than presented as finished.
82
+ */
83
+ export const renderAuthProviderBody = (config) => {
84
+ const warnings = [];
85
+ if (!config?.refreshCreatorName) {
86
+ return {
87
+ queries: ' queries: [],',
88
+ features: '',
89
+ importsNeeded: [],
90
+ isScaffolded: false,
91
+ warnings,
92
+ };
93
+ }
94
+ const refreshEntries = [` queryCreator: ${config.refreshCreatorName},`];
95
+ if (config.responseAdapter) {
96
+ refreshEntries.push(` // TODO(query-v3): v2 responseAdapter. It must return { accessToken, refreshToken } - v2 returned { token, refreshToken }.`, ` extractTokens: ${config.responseAdapter},`);
97
+ }
98
+ if (config.requestArgsAdapter) {
99
+ refreshEntries.push(` // TODO(query-v3): v2 requestArgsAdapter took { token, refreshToken }; buildArgs takes the refresh token alone.`, ` buildArgs: ${config.requestArgsAdapter},`);
100
+ }
101
+ if (config.expiresInPropertyName) {
102
+ refreshEntries.push(` expiresInPropertyName: ${config.expiresInPropertyName},`);
103
+ }
104
+ if (config.refreshBuffer) {
105
+ refreshEntries.push(` refreshStrategy: ${renderRefreshStrategy(config.refreshBuffer)},`);
106
+ }
107
+ if (config.cookieName && config.cookieEnabled && config.cookieEnabled !== 'true') {
108
+ warnings.push({
109
+ title: 'Replace the v2 cookieEnabled switch',
110
+ summary: `The v2 provider set \`cookieEnabled: ${config.cookieEnabled}\`. \`withPersistentAuth\` has no switch for it: it always keeps the refresh token in the cookie, and \`setRememberMe(false)\` only turns that cookie into a session cookie. v2's \`enableCookie()\` / \`disableCookie()\` have no v3 counterpart either.`,
111
+ action: 'If the cookie was only written after an opt-in (a "remember me" box), drive `setRememberMe()` from it. If it was never written, remove `withPersistentAuth` from the scaffold.',
112
+ });
113
+ }
114
+ const queries = [
115
+ ' queries: [',
116
+ ` // Derived from the v2 BearerAuthProvider in ${config.filePath}:${config.line}. Verify before shipping.`,
117
+ ` withRefreshQuery('${REFRESH_QUERY_KEY}', {`,
118
+ ...refreshEntries,
119
+ ' }),',
120
+ ` // TODO(query-v3): add withAuthenticationQuery('login', { queryCreator: … }) for the login query.`,
121
+ ' ],',
122
+ ].join('\n');
123
+ const importsNeeded = ['withRefreshQuery'];
124
+ if (!config.cookieName) {
125
+ return { queries, features: '', importsNeeded, isScaffolded: true, warnings };
126
+ }
127
+ const cookieEntries = [` name: ${config.cookieName},`];
128
+ if (config.cookieDomain)
129
+ cookieEntries.push(` domain: ${config.cookieDomain},`);
130
+ if (config.cookieExpiresInDays)
131
+ cookieEntries.push(` expiresInDays: ${config.cookieExpiresInDays},`);
132
+ if (config.cookiePath)
133
+ cookieEntries.push(` path: ${config.cookiePath},`);
134
+ if (config.cookieSameSite)
135
+ cookieEntries.push(` sameSite: ${config.cookieSameSite},`);
136
+ const features = [
137
+ ' features: [',
138
+ ' withPersistentAuth({',
139
+ ' cookie: {',
140
+ ...cookieEntries,
141
+ ' },',
142
+ ' autoLogin: {',
143
+ ` queryKey: '${REFRESH_QUERY_KEY}',`,
144
+ ` // TODO(query-v3): shape the auto-login request from the cookie value.`,
145
+ ' buildArgs: (token) => ({ body: { refreshToken: token } }),',
146
+ ' },',
147
+ ' }),',
148
+ ' ],',
149
+ ].join('\n');
150
+ importsNeeded.push('withPersistentAuth');
151
+ return { queries, features, importsNeeded, isScaffolded: true, warnings };
152
+ };
153
+ // A v3 `refreshStrategy` number from 0 to 1 is a fraction of the token lifetime, not milliseconds.
154
+ const renderRefreshStrategy = (refreshBuffer) => {
155
+ const value = Number(refreshBuffer.replace(/_/g, ''));
156
+ return Number.isFinite(value) && value > 1
157
+ ? refreshBuffer
158
+ : `{ minBufferMs: ${refreshBuffer}, maxBufferMs: ${refreshBuffer} }`;
159
+ };
160
+ //# sourceMappingURL=auth-provider-scaffold.js.map