@ethlete/query 5.43.1 → 6.0.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,30 +1,37 @@
1
1
  {
2
2
  "name": "@ethlete/query",
3
- "version": "5.43.1",
3
+ "version": "6.0.0-next.0",
4
4
  "license": "MIT",
5
5
  "sideEffects": false,
6
6
  "peerDependencies": {
7
- "@angular/common": "20.3.6",
8
- "@angular/core": "20.3.6",
9
- "@angular/forms": "20.3.6",
10
- "@angular/router": "20.3.6",
11
- "@ethlete/core": "^4.0.3",
7
+ "@analogjs/vite-plugin-angular": "2.3.1",
8
+ "@angular/common": "21.2.1",
9
+ "@angular/core": "21.2.1",
10
+ "@angular/forms": "21.2.1",
11
+ "@angular/router": "21.2.1",
12
+ "@ethlete/core": "^5.0.0-next.0",
12
13
  "@ethlete/types": "^1.6.2",
13
- "@nx/devkit": "21.6.5",
14
+ "@nx/devkit": "22.5.4",
15
+ "@nx/vite": "22.5.4",
14
16
  "rxjs": "7.8.2",
15
- "socket.io-client": "4.8.1",
16
- "typescript": "5.9.3"
17
+ "socket.io-client": "4.8.3",
18
+ "typescript": "5.9.3",
19
+ "vite": "^7.0.0"
17
20
  },
18
21
  "generators": "./generators/generators.json",
19
22
  "module": "fesm2022/ethlete-query.mjs",
20
- "typings": "index.d.ts",
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": "./index.d.ts",
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
  },
30
37
  "dependencies": {
@@ -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 };