@ethlete/query 5.43.0 ā 6.0.0-beta.8
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 +107 -0
- package/fesm2022/ethlete-query-testing.mjs +178 -0
- package/fesm2022/ethlete-query-testing.mjs.map +1 -0
- package/fesm2022/ethlete-query.mjs +4235 -3347
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/generators/generators.json +5 -0
- package/generators/migrate-to-query-v3/cleanup-migration.js +242 -0
- package/generators/migrate-to-query-v3/legacy-prepare-migration.js +773 -0
- package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +589 -0
- package/generators/migrate-to-query-v3/migration.js +18 -2937
- package/generators/migrate-to-query-v3/query-client-migration.js +477 -0
- package/generators/migrate-to-query-v3/report.js +136 -0
- package/generators/migrate-to-query-v3/shared.js +97 -0
- package/generators/prep-for-query-v3/migration.js +329 -0
- package/generators/prep-for-query-v3/schema.json +14 -0
- package/package.json +20 -12
- package/types/ethlete-query-testing.d.ts +167 -0
- package/{index.d.ts ā types/ethlete-query.d.ts} +3273 -2987
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
const TEMP_SOURCE_FILE = 'temp.ts';
|
|
3
|
+
export const createSourceFile = (content, filePath = TEMP_SOURCE_FILE) => ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
|
4
|
+
export const capitalizeFirstLetter = (value) => {
|
|
5
|
+
if (value.length === 0) {
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
return `${value[0].toUpperCase()}${value.slice(1)}`;
|
|
9
|
+
};
|
|
10
|
+
export const ensureConfigSuffix = (name) => {
|
|
11
|
+
if (/Config$/i.test(name)) {
|
|
12
|
+
return name;
|
|
13
|
+
}
|
|
14
|
+
return `${name}Config`;
|
|
15
|
+
};
|
|
16
|
+
export const ensureNamedImports = ({ content, importsNeeded, moduleSpecifier }) => {
|
|
17
|
+
const sourceFile = createSourceFile(content);
|
|
18
|
+
let importNode;
|
|
19
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
20
|
+
if (ts.isImportDeclaration(node) &&
|
|
21
|
+
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
22
|
+
node.moduleSpecifier.text === moduleSpecifier) {
|
|
23
|
+
importNode = node;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
const missingImports = new Set(importsNeeded);
|
|
27
|
+
if (importNode?.importClause?.namedBindings && ts.isNamedImports(importNode.importClause.namedBindings)) {
|
|
28
|
+
const namedBindings = importNode.importClause.namedBindings;
|
|
29
|
+
namedBindings.elements.forEach((element) => {
|
|
30
|
+
missingImports.delete(element.name.text);
|
|
31
|
+
});
|
|
32
|
+
if (missingImports.size === 0) {
|
|
33
|
+
return content;
|
|
34
|
+
}
|
|
35
|
+
const existingImports = namedBindings.elements.map((element) => element.getText(sourceFile));
|
|
36
|
+
const nextImports = [...existingImports, ...Array.from(missingImports).sort()].sort();
|
|
37
|
+
const nextImportStatement = `import { ${nextImports.join(', ')} } from '${moduleSpecifier}';`;
|
|
38
|
+
return content.slice(0, importNode.getStart(sourceFile)) + nextImportStatement + content.slice(importNode.getEnd());
|
|
39
|
+
}
|
|
40
|
+
let lastImportEnd = 0;
|
|
41
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
42
|
+
if (!ts.isImportDeclaration(node)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const end = node.getEnd();
|
|
46
|
+
if (end > lastImportEnd) {
|
|
47
|
+
lastImportEnd = end;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
const nextImportStatement = `\nimport { ${Array.from(missingImports).sort().join(', ')} } from '${moduleSpecifier}';`;
|
|
51
|
+
if (lastImportEnd > 0) {
|
|
52
|
+
return content.slice(0, lastImportEnd) + nextImportStatement + content.slice(lastImportEnd);
|
|
53
|
+
}
|
|
54
|
+
return `import { ${Array.from(missingImports).sort().join(', ')} } from '${moduleSpecifier}';\n\n${content}`;
|
|
55
|
+
};
|
|
56
|
+
export const ensureImportFromQuery = (content, importsNeeded) => ensureNamedImports({ content, importsNeeded, moduleSpecifier: '@ethlete/query' });
|
|
57
|
+
export const ensureAngularCoreImports = (content, importsNeeded) => ensureNamedImports({ content, importsNeeded, moduleSpecifier: '@angular/core' });
|
|
58
|
+
export const getVariableStatementEnd = (sourceFile, variableName, initializerName) => {
|
|
59
|
+
let variableStatementEnd;
|
|
60
|
+
const visit = (node) => {
|
|
61
|
+
if (ts.isVariableDeclaration(node) &&
|
|
62
|
+
ts.isIdentifier(node.name) &&
|
|
63
|
+
node.name.text === variableName &&
|
|
64
|
+
node.initializer &&
|
|
65
|
+
ts.isCallExpression(node.initializer) &&
|
|
66
|
+
ts.isIdentifier(node.initializer.expression) &&
|
|
67
|
+
node.initializer.expression.text === initializerName) {
|
|
68
|
+
let parent = node.parent;
|
|
69
|
+
while (parent && !ts.isVariableStatement(parent)) {
|
|
70
|
+
parent = parent.parent;
|
|
71
|
+
}
|
|
72
|
+
if (parent && ts.isVariableStatement(parent)) {
|
|
73
|
+
variableStatementEnd = parent.getEnd();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
ts.forEachChild(node, visit);
|
|
77
|
+
};
|
|
78
|
+
visit(sourceFile);
|
|
79
|
+
return variableStatementEnd;
|
|
80
|
+
};
|
|
81
|
+
export const getIndentation = (content, position) => {
|
|
82
|
+
let lineStart = position;
|
|
83
|
+
while (lineStart > 0 && content[lineStart - 1] !== '\n') {
|
|
84
|
+
lineStart -= 1;
|
|
85
|
+
}
|
|
86
|
+
let indentation = '';
|
|
87
|
+
for (let index = lineStart; index < position && /\s/.test(content[index]); index += 1) {
|
|
88
|
+
indentation += content[index];
|
|
89
|
+
}
|
|
90
|
+
return indentation || ' ';
|
|
91
|
+
};
|
|
92
|
+
export const getLineNumber = (node, sourceFile) => {
|
|
93
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
94
|
+
return line + 1;
|
|
95
|
+
};
|
|
96
|
+
export const getLineNumberFromPosition = (content, position) => content.slice(0, position).split('\n').length;
|
|
97
|
+
//# sourceMappingURL=shared.js.map
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { formatFiles, visitNotIgnoredFiles } from '@nx/devkit';
|
|
2
|
+
import * as ts from 'typescript';
|
|
3
|
+
export default async function migrate(tree, schema) {
|
|
4
|
+
console.log('\nš Starting Query v3 symbol conflict migration...');
|
|
5
|
+
renameConflictingSymbols(tree);
|
|
6
|
+
replaceExperimentalQueryNamespace(tree);
|
|
7
|
+
if (!schema.skipFormat) {
|
|
8
|
+
await formatFiles(tree);
|
|
9
|
+
}
|
|
10
|
+
console.log('\nā
Migration completed successfully!');
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region Symbol renaming
|
|
14
|
+
// Map of old symbol names to new symbol names
|
|
15
|
+
const TYPE_RENAMES = new Map([
|
|
16
|
+
['BearerAuthProvider', 'V2BearerAuthProvider'],
|
|
17
|
+
['AnyQueryCreator', 'AnyV2QueryCreator'],
|
|
18
|
+
['CacheAdapterFn', 'V2CacheAdapterFn'],
|
|
19
|
+
['Query', 'V2Query'],
|
|
20
|
+
['QueryArgsOf', 'V2QueryArgsOf'],
|
|
21
|
+
['QueryClient', 'V2QueryClient'],
|
|
22
|
+
['QueryClientConfig', 'V2QueryClientConfig'],
|
|
23
|
+
['QueryConfig', 'V2QueryConfig'],
|
|
24
|
+
['QueryCreator', 'V2QueryCreator'],
|
|
25
|
+
['QueryState', 'V2QueryState'],
|
|
26
|
+
['RouteType', 'V2RouteType'],
|
|
27
|
+
['RouteString', 'V2RouteString'],
|
|
28
|
+
['AnyQuery', 'AnyV2Query'],
|
|
29
|
+
]);
|
|
30
|
+
const FUNCTION_RENAMES = new Map([
|
|
31
|
+
['buildQueryCacheKey', 'v2BuildQueryCacheKey'],
|
|
32
|
+
['extractExpiresInSeconds', 'v2ExtractExpiresInSeconds'],
|
|
33
|
+
['shouldCacheQuery', 'v2ShouldCacheQuery'],
|
|
34
|
+
['shouldRetryRequest', 'v2ShouldRetryRequest'],
|
|
35
|
+
]);
|
|
36
|
+
function renameConflictingSymbols(tree) {
|
|
37
|
+
const updatedFiles = [];
|
|
38
|
+
const symbolUsageCounts = new Map();
|
|
39
|
+
// Initialize counts
|
|
40
|
+
[...TYPE_RENAMES.keys(), ...FUNCTION_RENAMES.keys()].forEach((symbol) => {
|
|
41
|
+
symbolUsageCounts.set(symbol, 0);
|
|
42
|
+
});
|
|
43
|
+
visitNotIgnoredFiles(tree, '', (filePath) => {
|
|
44
|
+
if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts'))
|
|
45
|
+
return;
|
|
46
|
+
const content = tree.read(filePath, 'utf-8');
|
|
47
|
+
if (!content)
|
|
48
|
+
return;
|
|
49
|
+
// Check if file imports from @ethlete/query
|
|
50
|
+
if (!content.includes('@ethlete/query'))
|
|
51
|
+
return;
|
|
52
|
+
const newContent = renameSymbolsInFile(content, filePath, symbolUsageCounts);
|
|
53
|
+
if (newContent !== content) {
|
|
54
|
+
tree.write(filePath, newContent);
|
|
55
|
+
updatedFiles.push(filePath);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
// Log statistics
|
|
59
|
+
if (updatedFiles.length > 0) {
|
|
60
|
+
console.log(`\nš Renamed symbols in ${updatedFiles.length} files:`);
|
|
61
|
+
const hasRenames = Array.from(symbolUsageCounts.entries()).filter(([_, count]) => count > 0);
|
|
62
|
+
if (hasRenames.length > 0) {
|
|
63
|
+
console.log('\n Symbol usage counts:');
|
|
64
|
+
hasRenames.forEach(([symbol, count]) => {
|
|
65
|
+
const newName = TYPE_RENAMES.get(symbol) || FUNCTION_RENAMES.get(symbol);
|
|
66
|
+
console.log(` - ${symbol} ā ${newName}: ${count} occurrences`);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
console.log('\nā
No conflicting symbols found in workspace');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function renameSymbolsInFile(content, filePath, symbolUsageCounts) {
|
|
75
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
|
76
|
+
const replacements = [];
|
|
77
|
+
// Track which symbols are imported from @ethlete/query
|
|
78
|
+
const importedSymbols = new Set();
|
|
79
|
+
function visit(node) {
|
|
80
|
+
// Track imports from @ethlete/query
|
|
81
|
+
if (ts.isImportDeclaration(node)) {
|
|
82
|
+
const moduleSpecifier = node.moduleSpecifier;
|
|
83
|
+
if (ts.isStringLiteral(moduleSpecifier) && moduleSpecifier.text === '@ethlete/query') {
|
|
84
|
+
const namedBindings = node.importClause?.namedBindings;
|
|
85
|
+
if (namedBindings && ts.isNamedImports(namedBindings)) {
|
|
86
|
+
namedBindings.elements.forEach((element) => {
|
|
87
|
+
const importedName = element.propertyName?.text || element.name.text;
|
|
88
|
+
if (TYPE_RENAMES.has(importedName) || FUNCTION_RENAMES.has(importedName)) {
|
|
89
|
+
importedSymbols.add(importedName);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Rename identifiers that match our rename maps
|
|
96
|
+
if (ts.isIdentifier(node)) {
|
|
97
|
+
const name = node.text;
|
|
98
|
+
const parent = node.parent;
|
|
99
|
+
// Only rename if this symbol was imported from @ethlete/query
|
|
100
|
+
if (!importedSymbols.has(name)) {
|
|
101
|
+
ts.forEachChild(node, visit);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const newName = TYPE_RENAMES.get(name) || FUNCTION_RENAMES.get(name);
|
|
105
|
+
if (!newName) {
|
|
106
|
+
ts.forEachChild(node, visit);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// Skip if it's the imported name in an import specifier (we'll handle that separately)
|
|
110
|
+
if (ts.isImportSpecifier(parent) && (parent.propertyName === node || parent.name === node)) {
|
|
111
|
+
ts.forEachChild(node, visit);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// Rename the identifier
|
|
115
|
+
replacements.push({
|
|
116
|
+
start: node.getStart(sourceFile),
|
|
117
|
+
end: node.getEnd(),
|
|
118
|
+
replacement: newName,
|
|
119
|
+
oldName: name,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
ts.forEachChild(node, visit);
|
|
123
|
+
}
|
|
124
|
+
visit(sourceFile);
|
|
125
|
+
// Update imports in @ethlete/query import declarations
|
|
126
|
+
const importReplacements = updateImports(sourceFile, content);
|
|
127
|
+
replacements.push(...importReplacements);
|
|
128
|
+
// Count usages
|
|
129
|
+
replacements.forEach((r) => {
|
|
130
|
+
const count = symbolUsageCounts.get(r.oldName) || 0;
|
|
131
|
+
symbolUsageCounts.set(r.oldName, count + 1);
|
|
132
|
+
});
|
|
133
|
+
// Apply replacements in reverse order
|
|
134
|
+
let result = content;
|
|
135
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
136
|
+
for (const { start, end, replacement } of replacements) {
|
|
137
|
+
result = result.slice(0, start) + replacement + result.slice(end);
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
function updateImports(sourceFile, content) {
|
|
142
|
+
const replacements = [];
|
|
143
|
+
function visit(node) {
|
|
144
|
+
if (ts.isImportDeclaration(node)) {
|
|
145
|
+
const moduleSpecifier = node.moduleSpecifier;
|
|
146
|
+
if (ts.isStringLiteral(moduleSpecifier) && moduleSpecifier.text === '@ethlete/query') {
|
|
147
|
+
const namedBindings = node.importClause?.namedBindings;
|
|
148
|
+
if (namedBindings && ts.isNamedImports(namedBindings)) {
|
|
149
|
+
namedBindings.elements.forEach((element) => {
|
|
150
|
+
const importedName = element.propertyName?.text || element.name.text;
|
|
151
|
+
const newName = TYPE_RENAMES.get(importedName) || FUNCTION_RENAMES.get(importedName);
|
|
152
|
+
if (newName) {
|
|
153
|
+
if (element.propertyName) {
|
|
154
|
+
// Handle: import { Query as MyQuery }
|
|
155
|
+
// Rename to: import { V2Query as MyQuery }
|
|
156
|
+
replacements.push({
|
|
157
|
+
start: element.propertyName.getStart(sourceFile),
|
|
158
|
+
end: element.propertyName.getEnd(),
|
|
159
|
+
replacement: newName,
|
|
160
|
+
oldName: importedName,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
// Handle: import { Query }
|
|
165
|
+
// Rename to: import { V2Query }
|
|
166
|
+
replacements.push({
|
|
167
|
+
start: element.name.getStart(sourceFile),
|
|
168
|
+
end: element.name.getEnd(),
|
|
169
|
+
replacement: newName,
|
|
170
|
+
oldName: importedName,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
ts.forEachChild(node, visit);
|
|
179
|
+
}
|
|
180
|
+
visit(sourceFile);
|
|
181
|
+
return replacements;
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region ExperimentalQuery namespace replacement
|
|
185
|
+
function replaceExperimentalQueryNamespace(tree) {
|
|
186
|
+
const updatedFiles = [];
|
|
187
|
+
let totalReplacements = 0;
|
|
188
|
+
visitNotIgnoredFiles(tree, '', (filePath) => {
|
|
189
|
+
if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts'))
|
|
190
|
+
return;
|
|
191
|
+
const content = tree.read(filePath, 'utf-8');
|
|
192
|
+
if (!content)
|
|
193
|
+
return;
|
|
194
|
+
// Check if file imports ExperimentalQuery
|
|
195
|
+
if (!content.includes('ExperimentalQuery'))
|
|
196
|
+
return;
|
|
197
|
+
const result = replaceNamespaceInFile(content, filePath);
|
|
198
|
+
if (result.modified) {
|
|
199
|
+
tree.write(filePath, result.content);
|
|
200
|
+
updatedFiles.push(filePath);
|
|
201
|
+
totalReplacements += result.replacementCount;
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
if (updatedFiles.length > 0) {
|
|
205
|
+
console.log(`\nš¦ Replaced ExperimentalQuery namespace in ${updatedFiles.length} files (${totalReplacements} usages)`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function replaceNamespaceInFile(content, filePath) {
|
|
209
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
|
210
|
+
const replacements = [];
|
|
211
|
+
const usedSymbols = new Set();
|
|
212
|
+
let hasExperimentalQueryImport = false;
|
|
213
|
+
let experimentalQueryAlias;
|
|
214
|
+
// First pass: find ExperimentalQuery import and get the alias
|
|
215
|
+
function collectImport(node) {
|
|
216
|
+
if (ts.isImportDeclaration(node)) {
|
|
217
|
+
const moduleSpecifier = node.moduleSpecifier;
|
|
218
|
+
if (ts.isStringLiteral(moduleSpecifier) && moduleSpecifier.text === '@ethlete/query') {
|
|
219
|
+
const namedBindings = node.importClause?.namedBindings;
|
|
220
|
+
if (namedBindings && ts.isNamedImports(namedBindings)) {
|
|
221
|
+
namedBindings.elements.forEach((element) => {
|
|
222
|
+
const importedName = element.propertyName?.text || element.name.text;
|
|
223
|
+
if (importedName === 'ExperimentalQuery') {
|
|
224
|
+
hasExperimentalQueryImport = true;
|
|
225
|
+
// The local name (alias) is what we use in the code
|
|
226
|
+
experimentalQueryAlias = element.name.text;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
ts.forEachChild(node, collectImport);
|
|
233
|
+
}
|
|
234
|
+
collectImport(sourceFile);
|
|
235
|
+
// If no ExperimentalQuery import found, return unchanged
|
|
236
|
+
if (!hasExperimentalQueryImport || !experimentalQueryAlias) {
|
|
237
|
+
return { content, modified: false, replacementCount: 0 };
|
|
238
|
+
}
|
|
239
|
+
// Second pass: collect used symbols
|
|
240
|
+
function findUsages(node) {
|
|
241
|
+
// Find usages: ExperimentalQuery.someSymbol or E.someSymbol (if aliased)
|
|
242
|
+
if (ts.isPropertyAccessExpression(node) &&
|
|
243
|
+
ts.isIdentifier(node.expression) &&
|
|
244
|
+
node.expression.text === experimentalQueryAlias) {
|
|
245
|
+
const symbolName = node.name.text;
|
|
246
|
+
usedSymbols.add(symbolName);
|
|
247
|
+
}
|
|
248
|
+
ts.forEachChild(node, findUsages);
|
|
249
|
+
}
|
|
250
|
+
findUsages(sourceFile);
|
|
251
|
+
// Build the new import statement with used symbols
|
|
252
|
+
const sortedSymbols = Array.from(usedSymbols).sort();
|
|
253
|
+
// Third pass: create replacements
|
|
254
|
+
function createReplacements(node) {
|
|
255
|
+
if (ts.isImportDeclaration(node)) {
|
|
256
|
+
const moduleSpecifier = node.moduleSpecifier;
|
|
257
|
+
if (ts.isStringLiteral(moduleSpecifier) && moduleSpecifier.text === '@ethlete/query') {
|
|
258
|
+
const namedBindings = node.importClause?.namedBindings;
|
|
259
|
+
if (namedBindings && ts.isNamedImports(namedBindings)) {
|
|
260
|
+
// We need to update the entire import declaration
|
|
261
|
+
const otherImports = [];
|
|
262
|
+
let hasExperimentalQuery = false;
|
|
263
|
+
namedBindings.elements.forEach((element) => {
|
|
264
|
+
const importedName = element.propertyName?.text || element.name.text;
|
|
265
|
+
if (importedName === 'ExperimentalQuery') {
|
|
266
|
+
hasExperimentalQuery = true;
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
// Keep other imports as-is
|
|
270
|
+
if (element.propertyName) {
|
|
271
|
+
otherImports.push(`${element.propertyName.text} as ${element.name.text}`);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
otherImports.push(element.name.text);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
if (hasExperimentalQuery) {
|
|
279
|
+
// Combine other imports with the expanded ExperimentalQuery symbols
|
|
280
|
+
const allImports = [...otherImports, ...sortedSymbols].sort();
|
|
281
|
+
let newImport;
|
|
282
|
+
if (allImports.length === 0) {
|
|
283
|
+
// No imports needed, remove the entire import
|
|
284
|
+
newImport = '';
|
|
285
|
+
}
|
|
286
|
+
else if (allImports.length <= 3) {
|
|
287
|
+
// Single line
|
|
288
|
+
newImport = `import { ${allImports.join(', ')} } from '@ethlete/query';`;
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
// Multi-line
|
|
292
|
+
newImport = `import {\n ${allImports.join(',\n ')}\n} from '@ethlete/query';`;
|
|
293
|
+
}
|
|
294
|
+
replacements.push({
|
|
295
|
+
start: node.getStart(sourceFile),
|
|
296
|
+
end: node.getEnd(),
|
|
297
|
+
replacement: newImport,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Replace usages: ExperimentalQuery.someSymbol ā someSymbol
|
|
304
|
+
if (ts.isPropertyAccessExpression(node) &&
|
|
305
|
+
ts.isIdentifier(node.expression) &&
|
|
306
|
+
node.expression.text === experimentalQueryAlias) {
|
|
307
|
+
replacements.push({
|
|
308
|
+
start: node.getStart(sourceFile),
|
|
309
|
+
end: node.getEnd(),
|
|
310
|
+
replacement: node.name.text,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
ts.forEachChild(node, createReplacements);
|
|
314
|
+
}
|
|
315
|
+
createReplacements(sourceFile);
|
|
316
|
+
// Apply replacements in reverse order
|
|
317
|
+
let result = content;
|
|
318
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
319
|
+
for (const { start, end, replacement } of replacements) {
|
|
320
|
+
result = result.slice(0, start) + replacement + result.slice(end);
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
content: result,
|
|
324
|
+
modified: replacements.length > 0,
|
|
325
|
+
replacementCount: replacements.length - 1, // Subtract 1 for the import replacement
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
//#endregion
|
|
329
|
+
//# sourceMappingURL=migration.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"$id": "prep-for-query-v3",
|
|
4
|
+
"title": "Prepare to migrate to Query v3",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"skipFormat": {
|
|
8
|
+
"type": "boolean",
|
|
9
|
+
"description": "Skip formatting files after migration",
|
|
10
|
+
"default": false
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"required": []
|
|
14
|
+
}
|
package/package.json
CHANGED
|
@@ -1,32 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ethlete/query",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0-beta.8",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"peerDependencies": {
|
|
7
|
-
"@angular
|
|
8
|
-
"@angular/
|
|
9
|
-
"@angular/
|
|
10
|
-
"@angular/
|
|
11
|
-
"@
|
|
12
|
-
"@ethlete/
|
|
13
|
-
"@
|
|
7
|
+
"@analogjs/vite-plugin-angular": "2.5.1",
|
|
8
|
+
"@angular/common": "21.2.13",
|
|
9
|
+
"@angular/core": "21.2.13",
|
|
10
|
+
"@angular/forms": "21.2.13",
|
|
11
|
+
"@angular/router": "21.2.13",
|
|
12
|
+
"@ethlete/core": "^5.0.0-beta.11",
|
|
13
|
+
"@ethlete/types": "^2.0.0-beta.4",
|
|
14
|
+
"@nx/devkit": "22.7.2",
|
|
15
|
+
"@nx/vite": "22.7.2",
|
|
14
16
|
"rxjs": "7.8.2",
|
|
15
|
-
"socket.io-client": "4.8.
|
|
16
|
-
"typescript": "5.9.3"
|
|
17
|
+
"socket.io-client": "4.8.3",
|
|
18
|
+
"typescript": "5.9.3",
|
|
19
|
+
"vite": "8.0.13"
|
|
17
20
|
},
|
|
18
21
|
"generators": "./generators/generators.json",
|
|
19
22
|
"module": "fesm2022/ethlete-query.mjs",
|
|
20
|
-
"typings": "
|
|
23
|
+
"typings": "types/ethlete-query.d.ts",
|
|
21
24
|
"exports": {
|
|
22
25
|
"./package.json": {
|
|
23
26
|
"default": "./package.json"
|
|
24
27
|
},
|
|
25
28
|
".": {
|
|
26
|
-
"types": "./
|
|
29
|
+
"types": "./types/ethlete-query.d.ts",
|
|
27
30
|
"default": "./fesm2022/ethlete-query.mjs"
|
|
31
|
+
},
|
|
32
|
+
"./testing": {
|
|
33
|
+
"types": "./types/ethlete-query-testing.d.ts",
|
|
34
|
+
"default": "./fesm2022/ethlete-query-testing.mjs"
|
|
28
35
|
}
|
|
29
36
|
},
|
|
37
|
+
"type": "module",
|
|
30
38
|
"dependencies": {
|
|
31
39
|
"tslib": "^2.3.0"
|
|
32
40
|
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { AnyCreateQueryClientResult, QueryClientRef, createGetQuery, createPostQuery, createPutQuery, createPatchQuery, createDeleteQuery, BearerAuthProviderFeatureContext, AnyQueryBuilder, BearerAuthProvider, AuthQueryBuilder, TokenRefreshQueryBuilder, TokenRefreshQueryConfig } from '@ethlete/query';
|
|
2
|
+
import { HttpClient } from '@angular/common/http';
|
|
3
|
+
import { HttpTestingController } from '@angular/common/http/testing';
|
|
4
|
+
import { Injector } from '@angular/core';
|
|
5
|
+
|
|
6
|
+
type QueryTestSetup = {
|
|
7
|
+
queryClient: NonNullable<ReturnType<AnyCreateQueryClientResult[1]>>;
|
|
8
|
+
queryClientRef: QueryClientRef;
|
|
9
|
+
httpClient: HttpClient;
|
|
10
|
+
httpTesting: HttpTestingController;
|
|
11
|
+
injector: Injector;
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
createGet: ReturnType<typeof createGetQuery>;
|
|
14
|
+
createPost: ReturnType<typeof createPostQuery>;
|
|
15
|
+
createPut: ReturnType<typeof createPutQuery>;
|
|
16
|
+
createPatch: ReturnType<typeof createPatchQuery>;
|
|
17
|
+
createDelete: ReturnType<typeof createDeleteQuery>;
|
|
18
|
+
};
|
|
19
|
+
type QueryTestSetupConfig = {
|
|
20
|
+
baseUrl?: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
mockErrorHandler?: boolean;
|
|
23
|
+
};
|
|
24
|
+
declare const setupQueryTest: (config?: QueryTestSetupConfig) => QueryTestSetup;
|
|
25
|
+
|
|
26
|
+
type AnyFeatureBuilder = (context: BearerAuthProviderFeatureContext<any, readonly AnyQueryBuilder[]>) => {
|
|
27
|
+
type: string;
|
|
28
|
+
instance: unknown;
|
|
29
|
+
};
|
|
30
|
+
type AuthTestSetupConfig<TLoginArgs extends {
|
|
31
|
+
body: Record<string, unknown>;
|
|
32
|
+
response: {
|
|
33
|
+
accessToken: string;
|
|
34
|
+
refreshToken: string;
|
|
35
|
+
};
|
|
36
|
+
} = {
|
|
37
|
+
body: Record<string, unknown>;
|
|
38
|
+
response: {
|
|
39
|
+
accessToken: string;
|
|
40
|
+
refreshToken: string;
|
|
41
|
+
};
|
|
42
|
+
}, TRefreshArgs extends {
|
|
43
|
+
body: Record<string, unknown>;
|
|
44
|
+
response: {
|
|
45
|
+
accessToken: string;
|
|
46
|
+
refreshToken: string;
|
|
47
|
+
};
|
|
48
|
+
} = {
|
|
49
|
+
body: Record<string, unknown>;
|
|
50
|
+
response: {
|
|
51
|
+
accessToken: string;
|
|
52
|
+
refreshToken: string;
|
|
53
|
+
};
|
|
54
|
+
}, TFeatures extends readonly AnyFeatureBuilder[] = [], TBearerData = unknown> = {
|
|
55
|
+
/** The query test setup instance */
|
|
56
|
+
querySetup: QueryTestSetup;
|
|
57
|
+
/** Login endpoint path. Default: '/auth/login' */
|
|
58
|
+
loginPath?: string;
|
|
59
|
+
/** Refresh endpoint path. Default: '/auth/refresh' */
|
|
60
|
+
refreshPath?: string;
|
|
61
|
+
/** Whether to enable auto-retry on 401. Default: false */
|
|
62
|
+
autoRetryOn401?: boolean;
|
|
63
|
+
/** Function to extract tokens from login response */
|
|
64
|
+
extractLoginTokens?: (response: TLoginArgs['response']) => {
|
|
65
|
+
accessToken: string;
|
|
66
|
+
refreshToken: string;
|
|
67
|
+
};
|
|
68
|
+
/** Function to extract tokens from refresh response */
|
|
69
|
+
extractRefreshTokens?: (response: TRefreshArgs['response']) => {
|
|
70
|
+
accessToken: string;
|
|
71
|
+
refreshToken: string;
|
|
72
|
+
};
|
|
73
|
+
/** Feature builders for additional auth functionality */
|
|
74
|
+
features?: [...TFeatures];
|
|
75
|
+
/** Custom bearer decrypt function for testing */
|
|
76
|
+
bearerDecryptFn?: (token: string) => TBearerData;
|
|
77
|
+
/** Whether to disable multi-tab sync. Default: false */
|
|
78
|
+
multiTabSync?: false | {
|
|
79
|
+
enabled?: boolean;
|
|
80
|
+
channelName?: string;
|
|
81
|
+
syncTokens?: boolean;
|
|
82
|
+
syncLogout?: boolean;
|
|
83
|
+
leaderElection?: boolean;
|
|
84
|
+
};
|
|
85
|
+
/** Refresh strategy configuration. See TokenRefreshQueryConfig for details */
|
|
86
|
+
refreshStrategy?: TokenRefreshQueryConfig<TRefreshArgs>['refreshStrategy'];
|
|
87
|
+
/** Minimum refresh interval in milliseconds */
|
|
88
|
+
minRefreshInterval?: number;
|
|
89
|
+
/** Whether to refresh if token is already expired */
|
|
90
|
+
refreshIfExpired?: boolean;
|
|
91
|
+
/** Property name for expiration in JWT */
|
|
92
|
+
expiresInPropertyName?: string;
|
|
93
|
+
};
|
|
94
|
+
type AuthTestSetup<TLoginArgs extends {
|
|
95
|
+
body: Record<string, unknown>;
|
|
96
|
+
response: {
|
|
97
|
+
accessToken: string;
|
|
98
|
+
refreshToken: string;
|
|
99
|
+
};
|
|
100
|
+
} = {
|
|
101
|
+
body: Record<string, unknown>;
|
|
102
|
+
response: {
|
|
103
|
+
accessToken: string;
|
|
104
|
+
refreshToken: string;
|
|
105
|
+
};
|
|
106
|
+
}, TRefreshArgs extends {
|
|
107
|
+
body: Record<string, unknown>;
|
|
108
|
+
response: {
|
|
109
|
+
accessToken: string;
|
|
110
|
+
refreshToken: string;
|
|
111
|
+
};
|
|
112
|
+
} = {
|
|
113
|
+
body: Record<string, unknown>;
|
|
114
|
+
response: {
|
|
115
|
+
accessToken: string;
|
|
116
|
+
refreshToken: string;
|
|
117
|
+
};
|
|
118
|
+
}, TFeatures extends readonly AnyFeatureBuilder[] = [], TBearerData = unknown> = {
|
|
119
|
+
/** The bearer auth provider instance */
|
|
120
|
+
auth: BearerAuthProvider<[
|
|
121
|
+
AuthQueryBuilder<'login', TLoginArgs>,
|
|
122
|
+
TokenRefreshQueryBuilder<'refresh', TRefreshArgs>
|
|
123
|
+
], TFeatures, TBearerData>;
|
|
124
|
+
/** Helper to login a user and flush the HTTP request */
|
|
125
|
+
login: (credentials: TLoginArgs['body'], response: TLoginArgs['response']) => void;
|
|
126
|
+
/** Helper to trigger a refresh and flush the HTTP request */
|
|
127
|
+
refresh: (token: string, response: TRefreshArgs['response']) => void;
|
|
128
|
+
/** Helper to make a secure request that requires authentication */
|
|
129
|
+
makeSecureRequest: (route: string) => void;
|
|
130
|
+
};
|
|
131
|
+
declare const setupAuthTest: <TLoginArgs extends {
|
|
132
|
+
body: Record<string, unknown>;
|
|
133
|
+
response: {
|
|
134
|
+
accessToken: string;
|
|
135
|
+
refreshToken: string;
|
|
136
|
+
};
|
|
137
|
+
} = {
|
|
138
|
+
body: Record<string, unknown>;
|
|
139
|
+
response: {
|
|
140
|
+
accessToken: string;
|
|
141
|
+
refreshToken: string;
|
|
142
|
+
};
|
|
143
|
+
}, TRefreshArgs extends {
|
|
144
|
+
body: Record<string, unknown>;
|
|
145
|
+
response: {
|
|
146
|
+
accessToken: string;
|
|
147
|
+
refreshToken: string;
|
|
148
|
+
};
|
|
149
|
+
} = {
|
|
150
|
+
body: Record<string, unknown>;
|
|
151
|
+
response: {
|
|
152
|
+
accessToken: string;
|
|
153
|
+
refreshToken: string;
|
|
154
|
+
};
|
|
155
|
+
}, TFeatures extends readonly AnyFeatureBuilder[] = readonly AnyFeatureBuilder[], TBearerData = unknown>(config: AuthTestSetupConfig<TLoginArgs, TRefreshArgs, TFeatures, TBearerData>) => AuthTestSetup<TLoginArgs, TRefreshArgs, TFeatures, TBearerData>;
|
|
156
|
+
|
|
157
|
+
declare const expectAndFlush: <T = unknown>(httpTesting: HttpTestingController, url: string, response: T, statusOpts?: {
|
|
158
|
+
status: number;
|
|
159
|
+
statusText: string;
|
|
160
|
+
}) => void;
|
|
161
|
+
declare const expectFlushAndWait: <T = unknown>(httpTesting: HttpTestingController, url: string, response: T, statusOpts?: {
|
|
162
|
+
status: number;
|
|
163
|
+
statusText: string;
|
|
164
|
+
}) => void;
|
|
165
|
+
|
|
166
|
+
export { expectAndFlush, expectFlushAndWait, setupAuthTest, setupQueryTest };
|
|
167
|
+
export type { AnyFeatureBuilder, AuthTestSetup, AuthTestSetupConfig, QueryTestSetup, QueryTestSetupConfig };
|