@ethlete/query 6.0.0-next.19 → 6.0.0-next.20
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 +56 -0
- package/fesm2022/ethlete-query.mjs +815 -78
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/generators/migrate-to-query-v3/auth-provider-scaffold.js +131 -0
- package/generators/migrate-to-query-v3/cleanup-migration.js +128 -77
- package/generators/migrate-to-query-v3/http-client-check.js +93 -0
- package/generators/migrate-to-query-v3/legacy-prepare-migration.js +8 -9
- package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +155 -137
- package/generators/migrate-to-query-v3/migration-scope.js +41 -0
- package/generators/migrate-to-query-v3/migration.js +17 -9
- package/generators/migrate-to-query-v3/module-graph.js +153 -0
- package/generators/migrate-to-query-v3/query-client-migration.js +53 -5
- package/generators/migrate-to-query-v3/rename-symbols.js +193 -0
- package/generators/migrate-to-query-v3/schema.json +12 -0
- package/package.json +1 -1
- package/types/ethlete-query.d.ts +540 -17
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { signal, isDevMode, makeEnvironmentProviders, DestroyRef, InjectionToken, inject, Injector, EnvironmentInjector, ElementRef, ErrorHandler, createEnvironmentInjector, effect, untracked, linkedSignal, computed, runInInjectionContext, assertInInjectionContext, ChangeDetectorRef, ViewContainerRef, TemplateRef, Input, Directive, input, Pipe, ViewEncapsulation, Component, resource, DOCUMENT, NgZone } from '@angular/core';
|
|
2
|
+
import { signal, isDevMode, makeEnvironmentProviders, DestroyRef, InjectionToken, inject, Injector, EnvironmentInjector, ElementRef, ErrorHandler, createEnvironmentInjector, effect, untracked, linkedSignal, computed, runInInjectionContext, assertInInjectionContext, ChangeDetectorRef, ViewContainerRef, TemplateRef, Input, Directive, input, Pipe, ViewEncapsulation, Component, resource, PLATFORM_ID, DOCUMENT, NgZone } from '@angular/core';
|
|
3
3
|
import { createStaticProvider, RuntimeError, clamp, createDestroy, computedTillTruthy, getCookie, setCookie, deleteCookie, createProvider, nextFrame, randomId, createRootProvider, isObject, injectRoute, getDomain, injectQueryParamChanges, injectQueryParams, clone, equal, ET_PROPERTY_REMOVED, previousSignalValue } from '@ethlete/core';
|
|
4
4
|
import { filter, Subject, Subscription, Observable, tap, retry, throwError, timer, catchError, takeUntil, startWith, pairwise, switchMap, of, BehaviorSubject, fromEvent, take, ReplaySubject, map, shareReplay, interval, skip, takeWhile, combineLatest, EMPTY, withLatestFrom, debounceTime, firstValueFrom, merge, throttleTime } from 'rxjs';
|
|
5
5
|
import { toObservable, takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
|
6
6
|
import { HttpClient, HttpEventType, HttpErrorResponse, HttpHeaders, HttpStatusCode } from '@angular/common/http';
|
|
7
7
|
import { getActiveConsumer, setActiveConsumer } from '@angular/core/primitives/signals';
|
|
8
|
-
import { NgClass, AsyncPipe, JsonPipe } from '@angular/common';
|
|
8
|
+
import { NgClass, AsyncPipe, JsonPipe, isPlatformBrowser } from '@angular/common';
|
|
9
9
|
import * as i1 from '@angular/forms';
|
|
10
10
|
import { FormControl, ReactiveFormsModule, FormGroup } from '@angular/forms';
|
|
11
|
-
import { validateAsync } from '@angular/forms/signals';
|
|
11
|
+
import { validateAsync, form } from '@angular/forms/signals';
|
|
12
12
|
import { Router, ActivatedRoute } from '@angular/router';
|
|
13
13
|
import { io } from 'socket.io-client';
|
|
14
14
|
|
|
@@ -318,6 +318,8 @@ const QueryRuntimeErrorCode = {
|
|
|
318
318
|
CIRCULAR_QUERY_DEPENDENCY: 800,
|
|
319
319
|
// Query Sequence
|
|
320
320
|
QUERY_SEQUENCE_ALREADY_RUNNING: 900,
|
|
321
|
+
// Legacy interop
|
|
322
|
+
LEGACY_PREPARE_WITHOUT_INJECTION_CONTEXT: 950,
|
|
321
323
|
};
|
|
322
324
|
const queryFeatureUsedMultipleTimes = (type) => {
|
|
323
325
|
return new RuntimeError(QueryRuntimeErrorCode.QUERY_FEATURE_USED_MULTIPLE_TIMES, `The query feature "${type}()" was used multiple times.`);
|
|
@@ -386,10 +388,53 @@ const invalidStateInsideSecureExecuteFactory = () => {
|
|
|
386
388
|
const circularQueryDependency = () => {
|
|
387
389
|
return new RuntimeError(QueryRuntimeErrorCode.CIRCULAR_QUERY_DEPENDENCY, `Query was executed more than 5 times in less than 100ms. This is usually a sign of a circular dependency.`);
|
|
388
390
|
};
|
|
391
|
+
const legacyPrepareWithoutInjectionContext = (creatorName) => {
|
|
392
|
+
const label = creatorName ? `"${creatorName}"` : 'A legacy query creator';
|
|
393
|
+
return new RuntimeError(QueryRuntimeErrorCode.LEGACY_PREPARE_WITHOUT_INJECTION_CONTEXT, `${label}.prepare() was called outside of an injection context. Unlike the v2 client, the legacy interop layer needs an injector to build the underlying query.\n\n` +
|
|
394
|
+
`Capture one where a context does exist (field initializer, constructor) and pass it along:\n\n` +
|
|
395
|
+
` private injector = inject(Injector);\n` +
|
|
396
|
+
` search = (term: string) => myQuery.prepare({ queryParams: { term }, injector: this.injector });\n\n` +
|
|
397
|
+
`Prefer an injector that outlives the call site. A component injector passed from a callback that can still fire after the component is gone leaves prepare() with a destroyed injector, which returns an inert query instead of executing.`);
|
|
398
|
+
};
|
|
389
399
|
const querySequenceAlreadyRunning = () => {
|
|
390
400
|
return new RuntimeError(QueryRuntimeErrorCode.QUERY_SEQUENCE_ALREADY_RUNNING, `run() was called on a query sequence that is already running. Wait for the current run to settle before starting another.`);
|
|
391
401
|
};
|
|
392
402
|
|
|
403
|
+
const isClassValidatorError = (error) => {
|
|
404
|
+
return typeof error === 'object' && error !== null && 'statusCode' in error && 'message' in error && 'error' in error;
|
|
405
|
+
};
|
|
406
|
+
const isSymfonyError = (error) => {
|
|
407
|
+
if (!error || typeof error !== 'object') {
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
const symfonyError = error;
|
|
411
|
+
return (symfonyError.class !== undefined &&
|
|
412
|
+
symfonyError.detail !== undefined &&
|
|
413
|
+
symfonyError.status !== undefined &&
|
|
414
|
+
symfonyError.title !== undefined &&
|
|
415
|
+
symfonyError.trace !== undefined &&
|
|
416
|
+
symfonyError.type !== undefined);
|
|
417
|
+
};
|
|
418
|
+
const isSymfonyPagerfantaOutOfRangeError = (error) => {
|
|
419
|
+
if (!isSymfonyError(error)) {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
return error.class.startsWith('Pagerfanta') && error.class.endsWith('OutOfRangeCurrentPageException');
|
|
423
|
+
};
|
|
424
|
+
const isSymfonyFormViolationListError = (error) => {
|
|
425
|
+
return typeof error === 'object' && error !== null && 'violations' in error;
|
|
426
|
+
};
|
|
427
|
+
const isSymfonyListError = (error) => {
|
|
428
|
+
return (!!error &&
|
|
429
|
+
typeof error === 'object' &&
|
|
430
|
+
Array.isArray(error) &&
|
|
431
|
+
!!error.length &&
|
|
432
|
+
typeof error[0] === 'object' &&
|
|
433
|
+
'message' in error[0] &&
|
|
434
|
+
'propertyPath' in error[0] &&
|
|
435
|
+
'invalidValue' in error[0]);
|
|
436
|
+
};
|
|
437
|
+
|
|
393
438
|
/**
|
|
394
439
|
* Returning this inside a withArgs feature will reset the query args to null.
|
|
395
440
|
* This will also pause polling and auto refresh until new args are set.
|
|
@@ -413,6 +458,7 @@ const QueryFeatureType = {
|
|
|
413
458
|
WITH_POLLING: 'WITH_POLLING',
|
|
414
459
|
WITH_AUTO_REFRESH: 'WITH_AUTO_REFRESH',
|
|
415
460
|
WITH_RESPONSE_UPDATE: 'WITH_RESPONSE_UPDATE',
|
|
461
|
+
WITH_PAGE_RESET_ON_ERROR: 'WITH_PAGE_RESET_ON_ERROR',
|
|
416
462
|
};
|
|
417
463
|
const createQueryFeature = (config) => {
|
|
418
464
|
return config;
|
|
@@ -578,6 +624,59 @@ const withResponseUpdate = (options) => {
|
|
|
578
624
|
},
|
|
579
625
|
});
|
|
580
626
|
};
|
|
627
|
+
/**
|
|
628
|
+
* Whether an error means the requested page is out of range: HTTP `416`, or a
|
|
629
|
+
* `500` carrying a Symfony/Pagerfanta out-of-range detail (the dev-mode shape).
|
|
630
|
+
* Exported so it can also be used as the predicate of a `withErrorHandling` handler.
|
|
631
|
+
*/
|
|
632
|
+
const isPageOutOfRangeError = (error) => {
|
|
633
|
+
if (error.code === 416)
|
|
634
|
+
return true;
|
|
635
|
+
return error.code === 500 && isSymfonyPagerfantaOutOfRangeError(error.raw.error);
|
|
636
|
+
};
|
|
637
|
+
/**
|
|
638
|
+
* A query feature that resets the page when the current page becomes out of range
|
|
639
|
+
* — e.g. after a filter shrinks the result set below the current page. It reacts
|
|
640
|
+
* to the query's error events and resets the page source; the normal reactive
|
|
641
|
+
* re-execution then runs with the corrected page.
|
|
642
|
+
*
|
|
643
|
+
* Because query args are reactively sourced, this fixes the *source* (a signal or
|
|
644
|
+
* query form field) rather than patching args directly — so the change sticks.
|
|
645
|
+
*
|
|
646
|
+
* @example
|
|
647
|
+
* // signal-driven page
|
|
648
|
+
* const users = getUsers(
|
|
649
|
+
* withArgs(() => ({ queryParams: { page: page() } })),
|
|
650
|
+
* withPageResetOnError({ page }),
|
|
651
|
+
* );
|
|
652
|
+
*
|
|
653
|
+
* @example
|
|
654
|
+
* // query-form-driven page
|
|
655
|
+
* const users = getUsers(
|
|
656
|
+
* withArgs(() => ({ queryParams: qf.value() })),
|
|
657
|
+
* withPageResetOnError({ reset: () => qf.resetFieldToDefault('page') }),
|
|
658
|
+
* );
|
|
659
|
+
*/
|
|
660
|
+
const withPageResetOnError = (options) => {
|
|
661
|
+
const matches = options.when ?? isPageOutOfRangeError;
|
|
662
|
+
return createQueryFeature({
|
|
663
|
+
type: QueryFeatureType.WITH_PAGE_RESET_ON_ERROR,
|
|
664
|
+
fn: (context) => {
|
|
665
|
+
context.state.events$
|
|
666
|
+
.pipe(filter((event) => event.type === 'error'), takeUntilDestroyed(context.deps.destroyRef))
|
|
667
|
+
.subscribe((event) => {
|
|
668
|
+
if (!matches(event.error))
|
|
669
|
+
return;
|
|
670
|
+
if (options.reset) {
|
|
671
|
+
options.reset();
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
options.page.set(options.resetTo ?? 1);
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
},
|
|
678
|
+
});
|
|
679
|
+
};
|
|
581
680
|
|
|
582
681
|
const setupQueryState = (options) => {
|
|
583
682
|
const request = signal(null, /* @ts-ignore */
|
|
@@ -963,6 +1062,19 @@ const createExecuteFn = (executeOptions) => {
|
|
|
963
1062
|
};
|
|
964
1063
|
|
|
965
1064
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1065
|
+
/**
|
|
1066
|
+
* The empty request-args object, typed to satisfy `RequestArgs<TArgs>` for a query that takes no
|
|
1067
|
+
* arguments — its `QueryArgs` is just `{ response: T }`, so `RequestArgs` resolves to `{}`.
|
|
1068
|
+
*
|
|
1069
|
+
* Only needed in generic code, where `TArgs` is still a type parameter and TS therefore cannot see
|
|
1070
|
+
* that `{}` is the whole of it. Against a concrete query, omit the argument instead: both
|
|
1071
|
+
* `query.execute()` and the auth registry's `execute()` already take none.
|
|
1072
|
+
*
|
|
1073
|
+
* @example
|
|
1074
|
+
* const runAnything = <TArgs extends QueryArgs>(query: Query<TArgs>) =>
|
|
1075
|
+
* query.execute({ args: emptyQueryArgs<TArgs>() });
|
|
1076
|
+
*/
|
|
1077
|
+
const emptyQueryArgs = () => ({});
|
|
966
1078
|
const createQuery = (options) => createBaseQuery({
|
|
967
1079
|
creator: options.creator,
|
|
968
1080
|
creatorInternals: options.creatorInternals,
|
|
@@ -1012,41 +1124,6 @@ const createBaseQueryCreator = (config) => {
|
|
|
1012
1124
|
return queryCreator;
|
|
1013
1125
|
};
|
|
1014
1126
|
|
|
1015
|
-
const isClassValidatorError = (error) => {
|
|
1016
|
-
return typeof error === 'object' && error !== null && 'statusCode' in error && 'message' in error && 'error' in error;
|
|
1017
|
-
};
|
|
1018
|
-
const isSymfonyError = (error) => {
|
|
1019
|
-
if (!error || typeof error !== 'object') {
|
|
1020
|
-
return false;
|
|
1021
|
-
}
|
|
1022
|
-
const symfonyError = error;
|
|
1023
|
-
return (symfonyError.class !== undefined &&
|
|
1024
|
-
symfonyError.detail !== undefined &&
|
|
1025
|
-
symfonyError.status !== undefined &&
|
|
1026
|
-
symfonyError.title !== undefined &&
|
|
1027
|
-
symfonyError.trace !== undefined &&
|
|
1028
|
-
symfonyError.type !== undefined);
|
|
1029
|
-
};
|
|
1030
|
-
const isSymfonyPagerfantaOutOfRangeError = (error) => {
|
|
1031
|
-
if (!isSymfonyError(error)) {
|
|
1032
|
-
return false;
|
|
1033
|
-
}
|
|
1034
|
-
return error.class.startsWith('Pagerfanta') && error.class.endsWith('OutOfRangeCurrentPageException');
|
|
1035
|
-
};
|
|
1036
|
-
const isSymfonyFormViolationListError = (error) => {
|
|
1037
|
-
return typeof error === 'object' && error !== null && 'violations' in error;
|
|
1038
|
-
};
|
|
1039
|
-
const isSymfonyListError = (error) => {
|
|
1040
|
-
return (!!error &&
|
|
1041
|
-
typeof error === 'object' &&
|
|
1042
|
-
Array.isArray(error) &&
|
|
1043
|
-
!!error.length &&
|
|
1044
|
-
typeof error[0] === 'object' &&
|
|
1045
|
-
'message' in error[0] &&
|
|
1046
|
-
'propertyPath' in error[0] &&
|
|
1047
|
-
'invalidValue' in error[0]);
|
|
1048
|
-
};
|
|
1049
|
-
|
|
1050
1127
|
const buildErrorMessage = (code, message) => `[@ethlete\\query:${code}] ${message}`;
|
|
1051
1128
|
class QueryError extends Error {
|
|
1052
1129
|
constructor(code, message, data) {
|
|
@@ -1647,6 +1724,31 @@ const shouldRetryRequest = (options) => {
|
|
|
1647
1724
|
const isQueryErrorResponse = (error) => {
|
|
1648
1725
|
return typeof error === 'object' && error !== null && 'raw' in error && 'retryState' in error && 'isList' in error;
|
|
1649
1726
|
};
|
|
1727
|
+
/**
|
|
1728
|
+
* Every message carried by an error, flattened into a plain list — one entry for a single error,
|
|
1729
|
+
* one per violation for a list (a form response with several field violations), empty for `null`.
|
|
1730
|
+
*
|
|
1731
|
+
* The single/list split exists because that is how APIs answer; UI almost never wants to branch on
|
|
1732
|
+
* it. Reach for this when rendering, and keep {@link QueryErrorResponse} for anything that needs
|
|
1733
|
+
* the status code or the raw response.
|
|
1734
|
+
*
|
|
1735
|
+
* @example
|
|
1736
|
+
* ```html
|
|
1737
|
+
* @for (message of queryErrorMessages(myQuery.error()); track message) {
|
|
1738
|
+
* <p class="error">{{ message }}</p>
|
|
1739
|
+
* }
|
|
1740
|
+
* ```
|
|
1741
|
+
*/
|
|
1742
|
+
const queryErrorMessages = (error) => {
|
|
1743
|
+
if (!error)
|
|
1744
|
+
return [];
|
|
1745
|
+
return error.isList ? error.errors.map((item) => item.message) : [error.error.message];
|
|
1746
|
+
};
|
|
1747
|
+
/**
|
|
1748
|
+
* The first message of an error, or `null` when there is none — the single-line counterpart to
|
|
1749
|
+
* {@link queryErrorMessages}, for a toast or a form-field hint.
|
|
1750
|
+
*/
|
|
1751
|
+
const queryErrorMessage = (error) => queryErrorMessages(error)[0] ?? null;
|
|
1650
1752
|
const createQueryErrorResponse = (error) => {
|
|
1651
1753
|
let err = error instanceof HttpErrorResponse ? error : null;
|
|
1652
1754
|
if (!err) {
|
|
@@ -1739,8 +1841,21 @@ const createHttpRequest = (options) => {
|
|
|
1739
1841
|
const expiresInTs = expiresIn();
|
|
1740
1842
|
return expiresInTs === null || expiresInTs < Date.now();
|
|
1741
1843
|
};
|
|
1844
|
+
// Resolved per execution rather than once at creation, so a client whose headers are a function
|
|
1845
|
+
// reading a signal (a preview token, a tenant id) sees the current value on every re-run.
|
|
1846
|
+
const resolveHeaders = () => {
|
|
1847
|
+
const clientHeaders = typeof options.clientHeaders === 'function' ? options.clientHeaders() : options.clientHeaders;
|
|
1848
|
+
const argHeaders = typeof args?.headers === 'function' ? args.headers() : args?.headers;
|
|
1849
|
+
if (!clientHeaders)
|
|
1850
|
+
return argHeaders;
|
|
1851
|
+
if (!argHeaders)
|
|
1852
|
+
return clientHeaders;
|
|
1853
|
+
// `set` replaces every value of that name, so a per-request header fully overrides the
|
|
1854
|
+
// client-level one instead of being appended next to it.
|
|
1855
|
+
return argHeaders.keys().reduce((merged, key) => merged.set(key, argHeaders.getAll(key) ?? []), clientHeaders);
|
|
1856
|
+
};
|
|
1742
1857
|
const createStream = () => {
|
|
1743
|
-
const headers =
|
|
1858
|
+
const headers = resolveHeaders();
|
|
1744
1859
|
return dependencies.httpClient
|
|
1745
1860
|
.request(options.method, options.fullPath, {
|
|
1746
1861
|
observe: 'events',
|
|
@@ -1766,7 +1881,7 @@ const createHttpRequest = (options) => {
|
|
|
1766
1881
|
}));
|
|
1767
1882
|
};
|
|
1768
1883
|
const execute = (options) => {
|
|
1769
|
-
if (loading() || (!isStale() && options?.allowCache)) {
|
|
1884
|
+
if (!options?.force && (loading() || (!isStale() && options?.allowCache))) {
|
|
1770
1885
|
// Do not execute if there is already a request in progress or caching is allowed
|
|
1771
1886
|
return false;
|
|
1772
1887
|
}
|
|
@@ -2879,6 +2994,12 @@ const createSecureExecuteFactory = (options) => {
|
|
|
2879
2994
|
executeOptions: { deps: options.deps, state: options.state },
|
|
2880
2995
|
});
|
|
2881
2996
|
};
|
|
2997
|
+
// A logout tears down the secure entries in the repository, but the query object holding one keeps
|
|
2998
|
+
// its own `response`/`error` signals — so without this a component still mounted after logout goes
|
|
2999
|
+
// on rendering the previous user's data until something calls `reset()` by hand.
|
|
3000
|
+
options.deps.client.repository.events$
|
|
3001
|
+
.pipe(filter((event) => event.type === 'unbind-all-secure'), takeUntilDestroyed(options.deps.destroyRef))
|
|
3002
|
+
.subscribe(() => reset());
|
|
2882
3003
|
const error = (query) => {
|
|
2883
3004
|
const state = options.state;
|
|
2884
3005
|
state.error.set(query.error());
|
|
@@ -3196,10 +3317,18 @@ class LegacyQuery {
|
|
|
3196
3317
|
get _isInMockMode() {
|
|
3197
3318
|
return false;
|
|
3198
3319
|
}
|
|
3199
|
-
constructor(newQuery, _arguments, entity
|
|
3320
|
+
constructor(newQuery, _arguments, entity,
|
|
3321
|
+
/**
|
|
3322
|
+
* Wraps an inert query — see `createInertQuery`. Skips everything that needs a live injector,
|
|
3323
|
+
* so the wrapper is safe to build during teardown.
|
|
3324
|
+
*
|
|
3325
|
+
* @internal
|
|
3326
|
+
*/
|
|
3327
|
+
_isInert = false) {
|
|
3200
3328
|
this.newQuery = newQuery;
|
|
3201
3329
|
this._arguments = _arguments;
|
|
3202
3330
|
this.entity = entity;
|
|
3331
|
+
this._isInert = _isInert;
|
|
3203
3332
|
/**
|
|
3204
3333
|
* @internal
|
|
3205
3334
|
*/
|
|
@@ -3208,9 +3337,31 @@ class LegacyQuery {
|
|
|
3208
3337
|
* @internal
|
|
3209
3338
|
*/
|
|
3210
3339
|
this._currentPollConfig = null;
|
|
3340
|
+
/**
|
|
3341
|
+
* `null` for an inert query — there is no live injector to own an effect.
|
|
3342
|
+
*
|
|
3343
|
+
* @internal
|
|
3344
|
+
*/
|
|
3345
|
+
this.storeSyncEffect = null;
|
|
3346
|
+
/**
|
|
3347
|
+
* @internal
|
|
3348
|
+
*/
|
|
3349
|
+
this._dependents = {};
|
|
3350
|
+
/**
|
|
3351
|
+
* @internal
|
|
3352
|
+
*/
|
|
3353
|
+
this._dependentsChanged$ = new ReplaySubject();
|
|
3211
3354
|
/**
|
|
3212
3355
|
* @internal
|
|
3213
3356
|
*/
|
|
3357
|
+
this._isPollingPaused = false;
|
|
3358
|
+
this._isDestroyed = false;
|
|
3359
|
+
if (this._isInert) {
|
|
3360
|
+
this.state$ = of(transformExecStateToQueryState(null));
|
|
3361
|
+
this._isDestroyed = true;
|
|
3362
|
+
return;
|
|
3363
|
+
}
|
|
3364
|
+
this.state$ = toObservable(this.newQuery.executionState, { injector: this.newQuery.subtle.injector }).pipe(map((execState) => transformExecStateToQueryState(execState)), switchMap((s) => this._transformState(s)), shareReplay({ bufferSize: 1, refCount: true }));
|
|
3214
3365
|
this.storeSyncEffect = effect(() => {
|
|
3215
3366
|
const res = this.newQuery.response();
|
|
3216
3367
|
untracked(() => {
|
|
@@ -3226,27 +3377,28 @@ class LegacyQuery {
|
|
|
3226
3377
|
});
|
|
3227
3378
|
}, /* @ts-ignore */
|
|
3228
3379
|
...(ngDevMode ? [{ debugName: "storeSyncEffect" }] : /* istanbul ignore next */ []));
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
this.
|
|
3233
|
-
/**
|
|
3234
|
-
* @internal
|
|
3235
|
-
*/
|
|
3236
|
-
this._dependentsChanged$ = new ReplaySubject();
|
|
3237
|
-
/**
|
|
3238
|
-
* @internal
|
|
3239
|
-
*/
|
|
3240
|
-
this._isPollingPaused = false;
|
|
3241
|
-
this.state$ = toObservable(this.newQuery.executionState, { injector: this.newQuery.subtle.injector }).pipe(map((execState) => transformExecStateToQueryState(execState)), switchMap((s) => this._transformState(s)), shareReplay({ bufferSize: 1, refCount: true }));
|
|
3242
|
-
this.newQuery.subtle.destroyRef.onDestroy(() => this.destroy());
|
|
3380
|
+
// Tear down only what this wrapper owns. The underlying query is already being destroyed when this fires, so
|
|
3381
|
+
// destroying it again would call `R3Injector.destroy()` on an injector that is mid-teardown, which throws
|
|
3382
|
+
// NG0205 ("Injector has already been destroyed") from inside the view's cleanup phase.
|
|
3383
|
+
this.newQuery.subtle.destroyRef.onDestroy(() => this._teardown());
|
|
3243
3384
|
}
|
|
3244
|
-
|
|
3385
|
+
_teardown() {
|
|
3386
|
+
if (this._isDestroyed) {
|
|
3387
|
+
return;
|
|
3388
|
+
}
|
|
3389
|
+
this._isDestroyed = true;
|
|
3245
3390
|
this._pollingSubscription?.unsubscribe();
|
|
3246
3391
|
this._pollingSubscription = null;
|
|
3247
3392
|
this._dependentsChanged$.complete();
|
|
3393
|
+
this.storeSyncEffect?.destroy();
|
|
3394
|
+
}
|
|
3395
|
+
destroy() {
|
|
3396
|
+
if (this._isDestroyed) {
|
|
3397
|
+
return;
|
|
3398
|
+
}
|
|
3399
|
+
this._teardown();
|
|
3400
|
+
// Re-enters `_teardown` through the query's `destroyRef`, which the `_isDestroyed` guard absorbs.
|
|
3248
3401
|
this.newQuery.subtle.destroy();
|
|
3249
|
-
this.storeSyncEffect.destroy();
|
|
3250
3402
|
}
|
|
3251
3403
|
execute(options = {}) {
|
|
3252
3404
|
untracked(() => this.newQuery.execute({ args: this._arguments, options: { allowCache: options.skipCache !== true } }));
|
|
@@ -3260,7 +3412,8 @@ class LegacyQuery {
|
|
|
3260
3412
|
return this;
|
|
3261
3413
|
}
|
|
3262
3414
|
poll(config) {
|
|
3263
|
-
|
|
3415
|
+
// An inert or already destroyed query has nothing to poll, and the interval would outlive it.
|
|
3416
|
+
if (this._pollingSubscription || this._isDestroyed) {
|
|
3264
3417
|
return this;
|
|
3265
3418
|
}
|
|
3266
3419
|
this._currentPollConfig = config;
|
|
@@ -3345,11 +3498,89 @@ class LegacyQuery {
|
|
|
3345
3498
|
}
|
|
3346
3499
|
}
|
|
3347
3500
|
|
|
3501
|
+
/**
|
|
3502
|
+
* A query that never executes and never leaves its initial state.
|
|
3503
|
+
*
|
|
3504
|
+
* `LegacyQueryCreator.prepare()` hands one back when the injector it was given is already destroyed
|
|
3505
|
+
* — the shape v2 call sites still fire into during teardown (a debounced search resolving after the
|
|
3506
|
+
* user navigated away, an RxJS callback on a stream that has not been unsubscribed yet). v2 had no
|
|
3507
|
+
* injector at all there, so the same code used to be harmless; building a real query would now
|
|
3508
|
+
* throw NG0205 from inside the view's cleanup phase, with no hint about which query caused it.
|
|
3509
|
+
*
|
|
3510
|
+
* Everything reads as "prepared but never executed": `execute()` and friends are no-ops, signals
|
|
3511
|
+
* stay `null`, observables complete immediately. Nothing here touches Angular's DI, which is the
|
|
3512
|
+
* point — there is no live injector left to bind to.
|
|
3513
|
+
*/
|
|
3514
|
+
const createInertQuery = () => {
|
|
3515
|
+
const inertSignal = (value) => Object.assign(signal(value), { asObservable: () => of(value) });
|
|
3516
|
+
const noop = () => {
|
|
3517
|
+
/* the query is inert — there is nothing to execute, reset or destroy */
|
|
3518
|
+
};
|
|
3519
|
+
const base = {
|
|
3520
|
+
args: inertSignal(null),
|
|
3521
|
+
response: inertSignal(null),
|
|
3522
|
+
latestHttpEvent: inertSignal(null),
|
|
3523
|
+
loading: inertSignal(null),
|
|
3524
|
+
error: inertSignal(null),
|
|
3525
|
+
lastTimeExecutedAt: inertSignal(null),
|
|
3526
|
+
triggeredBy: inertSignal(null),
|
|
3527
|
+
id: inertSignal(null),
|
|
3528
|
+
executionState: inertSignal(null),
|
|
3529
|
+
};
|
|
3530
|
+
const createSnapshot = () => ({ ...base, isAlive: inertSignal(false) });
|
|
3531
|
+
// A DestroyRef that is already destroyed: callbacks registered on it are dropped rather than
|
|
3532
|
+
// stored, so nothing keeps this object (or its registrant) alive.
|
|
3533
|
+
const destroyRef = {
|
|
3534
|
+
destroyed: true,
|
|
3535
|
+
onDestroy: () => noop,
|
|
3536
|
+
};
|
|
3537
|
+
return {
|
|
3538
|
+
...base,
|
|
3539
|
+
execute: noop,
|
|
3540
|
+
createSnapshot,
|
|
3541
|
+
reset: noop,
|
|
3542
|
+
asReadonly: () => ({ ...base, createSnapshot }),
|
|
3543
|
+
subtle: {
|
|
3544
|
+
destroy: noop,
|
|
3545
|
+
setResponse: noop,
|
|
3546
|
+
setLoading: noop,
|
|
3547
|
+
setError: noop,
|
|
3548
|
+
request: inertSignal(null),
|
|
3549
|
+
destroyRef,
|
|
3550
|
+
injector: Injector.NULL,
|
|
3551
|
+
},
|
|
3552
|
+
};
|
|
3553
|
+
};
|
|
3554
|
+
|
|
3555
|
+
/**
|
|
3556
|
+
* Whether an injector can still be used. A destroyed `R3Injector` throws on `get()` rather than
|
|
3557
|
+
* answering, so the throw itself is the answer; a node injector hands back a `DestroyRef` that
|
|
3558
|
+
* reports its own state.
|
|
3559
|
+
*/
|
|
3560
|
+
const isInjectorUsable = (injector) => {
|
|
3561
|
+
try {
|
|
3562
|
+
return injector.get(DestroyRef, null, { optional: true })?.destroyed !== true;
|
|
3563
|
+
}
|
|
3564
|
+
catch {
|
|
3565
|
+
return false;
|
|
3566
|
+
}
|
|
3567
|
+
};
|
|
3348
3568
|
class LegacyQueryCreator {
|
|
3349
3569
|
constructor(options) {
|
|
3350
3570
|
this.options = options;
|
|
3351
3571
|
this.prepare = (args) => {
|
|
3352
|
-
|
|
3572
|
+
// v2's `prepare()` needed no injection context, so plenty of call sites are plain callbacks.
|
|
3573
|
+
// Angular's raw NG0203 names neither the query nor the call site, which makes those a bisection
|
|
3574
|
+
// exercise — replace it with an error that does.
|
|
3575
|
+
const injector = args?.injector ??
|
|
3576
|
+
(() => {
|
|
3577
|
+
try {
|
|
3578
|
+
return inject(Injector);
|
|
3579
|
+
}
|
|
3580
|
+
catch {
|
|
3581
|
+
throw legacyPrepareWithoutInjectionContext(this.options.name);
|
|
3582
|
+
}
|
|
3583
|
+
})();
|
|
3353
3584
|
let headers = new HttpHeaders();
|
|
3354
3585
|
if (args?.headers) {
|
|
3355
3586
|
Object.entries(args.headers).forEach(([key, value]) => {
|
|
@@ -3364,6 +3595,17 @@ class LegacyQueryCreator {
|
|
|
3364
3595
|
...(args?.queryParams ? { queryParams: args.queryParams } : {}),
|
|
3365
3596
|
headers,
|
|
3366
3597
|
};
|
|
3598
|
+
// A captured component injector routinely outlives its component here: a debounced search
|
|
3599
|
+
// resolving after navigation, an RxJS callback on a stream torn down a tick later. That is not
|
|
3600
|
+
// a programming error the way it would be in new code — v2 had no injector to destroy — so it
|
|
3601
|
+
// yields a query that does nothing instead of an NG0205 raised from the view's cleanup phase.
|
|
3602
|
+
if (!isInjectorUsable(injector)) {
|
|
3603
|
+
if (isDevMode()) {
|
|
3604
|
+
console.warn(`${this.options.name ? `"${this.options.name}"` : 'A legacy query'}.prepare() was called with a destroyed injector, so an inert query was returned. ` +
|
|
3605
|
+
`This usually means a callback outlived the component whose injector it captured — guard the call site with DestroyRef.onDestroy, or pass an injector that outlives it.`);
|
|
3606
|
+
}
|
|
3607
|
+
return new LegacyQuery(createInertQuery(), queryArgs, this.options.entity, true);
|
|
3608
|
+
}
|
|
3367
3609
|
return runInInjectionContext(injector, () => {
|
|
3368
3610
|
return untracked(() => {
|
|
3369
3611
|
const newQuery = this.options.creator({
|
|
@@ -5453,6 +5695,14 @@ const validateWithV2Query = (path, config) => {
|
|
|
5453
5695
|
};
|
|
5454
5696
|
|
|
5455
5697
|
const generateUuid = () => randomId();
|
|
5698
|
+
/** @see CreateQueryClientConfigOptions.keepUnusedFor */
|
|
5699
|
+
const DEFAULT_KEEP_UNUSED_FOR = 300_000;
|
|
5700
|
+
/**
|
|
5701
|
+
* Hard cap on entries kept without consumers, per client. Retention is opportunistic, so a runaway
|
|
5702
|
+
* count (a search-as-you-type query produces a new cache key per keystroke, and each one goes through
|
|
5703
|
+
* `unbind`) must never grow unbounded — the least recently orphaned entries are dropped first.
|
|
5704
|
+
*/
|
|
5705
|
+
const MAX_UNUSED_ENTRIES = 50;
|
|
5456
5706
|
const createQueryRepository = (config) => {
|
|
5457
5707
|
const cache = new Map();
|
|
5458
5708
|
const eventsSubject = new Subject();
|
|
@@ -5461,6 +5711,11 @@ const createQueryRepository = (config) => {
|
|
|
5461
5711
|
const cacheVersion = signal(0, /* @ts-ignore */
|
|
5462
5712
|
...(ngDevMode ? [{ debugName: "cacheVersion" }] : /* istanbul ignore next */ []));
|
|
5463
5713
|
const bumpCacheVersion = () => cacheVersion.update((v) => v + 1);
|
|
5714
|
+
const resolveKeepUnusedFor = (creatorOptions, shouldCache) => {
|
|
5715
|
+
if (config.retentionEnabled === false || !shouldCache)
|
|
5716
|
+
return 0;
|
|
5717
|
+
return Math.max(0, creatorOptions?.keepUnusedFor ?? config.keepUnusedFor ?? DEFAULT_KEEP_UNUSED_FOR);
|
|
5718
|
+
};
|
|
5464
5719
|
const request = (options) => {
|
|
5465
5720
|
const { args, creatorOptions, runQueryOptions } = options;
|
|
5466
5721
|
const shouldCache = creatorOptions?.subtle?.useQueryRepositoryCache === false
|
|
@@ -5486,6 +5741,7 @@ const createQueryRepository = (config) => {
|
|
|
5486
5741
|
})
|
|
5487
5742
|
: false;
|
|
5488
5743
|
const trackingKey = cacheKey || generateUuid();
|
|
5744
|
+
const keepUnusedFor = resolveKeepUnusedFor(creatorOptions, shouldCache);
|
|
5489
5745
|
const previousKey = options.previousKey;
|
|
5490
5746
|
if (cacheKey !== previousKey && previousKey) {
|
|
5491
5747
|
unbind(previousKey, options.consumerDestroyRef);
|
|
@@ -5493,7 +5749,10 @@ const createQueryRepository = (config) => {
|
|
|
5493
5749
|
if (shouldCache && cacheKey) {
|
|
5494
5750
|
const cacheEntry = cache.get(cacheKey);
|
|
5495
5751
|
if (cacheEntry) {
|
|
5496
|
-
|
|
5752
|
+
// The entry may have been sitting out its `keepUnusedFor` window — a consumer binding again
|
|
5753
|
+
// makes it live, so the pending eviction must go.
|
|
5754
|
+
cancelEviction(cacheEntry);
|
|
5755
|
+
bind(cacheKey, options.consumerDestroyRef, cacheEntry.request, options.isSecure ?? false, true, keepUnusedFor);
|
|
5497
5756
|
if (!runQueryOptions?.allowCache || cacheEntry.request.isStale()) {
|
|
5498
5757
|
cacheEntry.request.execute({ allowCache: runQueryOptions?.allowCache });
|
|
5499
5758
|
}
|
|
@@ -5506,13 +5765,43 @@ const createQueryRepository = (config) => {
|
|
|
5506
5765
|
method: options.method,
|
|
5507
5766
|
dependencies: config.dependencies,
|
|
5508
5767
|
clientOptions: creatorOptions,
|
|
5768
|
+
clientHeaders: config.headers,
|
|
5509
5769
|
cacheAdapter: config.cacheAdapter,
|
|
5510
5770
|
retryFn: options.retryFn ?? config.retryFn,
|
|
5511
5771
|
});
|
|
5512
5772
|
request.execute();
|
|
5513
|
-
bind(trackingKey, options.consumerDestroyRef, request, options.isSecure ?? false, shouldCache);
|
|
5773
|
+
bind(trackingKey, options.consumerDestroyRef, request, options.isSecure ?? false, shouldCache, keepUnusedFor);
|
|
5514
5774
|
return { key: trackingKey, request };
|
|
5515
5775
|
};
|
|
5776
|
+
/** Tears an entry down for good, whether it currently has consumers or not. */
|
|
5777
|
+
const destroyEntry = (key, cacheEntry) => {
|
|
5778
|
+
clearTimeout(cacheEntry.evictTimer);
|
|
5779
|
+
cacheEntry.request.destroy();
|
|
5780
|
+
cacheEntry.eventSubscription?.unsubscribe();
|
|
5781
|
+
cache.delete(key);
|
|
5782
|
+
};
|
|
5783
|
+
const cancelEviction = (cacheEntry) => {
|
|
5784
|
+
if (cacheEntry.evictTimer === undefined)
|
|
5785
|
+
return;
|
|
5786
|
+
clearTimeout(cacheEntry.evictTimer);
|
|
5787
|
+
cacheEntry.evictTimer = undefined;
|
|
5788
|
+
cacheEntry.unusedSince = undefined;
|
|
5789
|
+
};
|
|
5790
|
+
/** Drops the least recently orphaned unused entries until the cap is respected again. */
|
|
5791
|
+
const enforceUnusedEntryLimit = () => {
|
|
5792
|
+
const unused = Array.from(cache.entries()).filter(([, entry]) => entry.consumers.size === 0);
|
|
5793
|
+
if (unused.length <= MAX_UNUSED_ENTRIES)
|
|
5794
|
+
return;
|
|
5795
|
+
unused.sort(([, a], [, b]) => (a.unusedSince ?? 0) - (b.unusedSince ?? 0));
|
|
5796
|
+
for (const [key, entry] of unused.slice(0, unused.length - MAX_UNUSED_ENTRIES)) {
|
|
5797
|
+
destroyEntry(key, entry);
|
|
5798
|
+
}
|
|
5799
|
+
};
|
|
5800
|
+
const retain = (key, cacheEntry) => {
|
|
5801
|
+
cacheEntry.unusedSince = Date.now();
|
|
5802
|
+
cacheEntry.evictTimer = setTimeout(() => evict(key), cacheEntry.keepUnusedFor);
|
|
5803
|
+
enforceUnusedEntryLimit();
|
|
5804
|
+
};
|
|
5516
5805
|
const unbind = (key, consumerDestroyRef) => {
|
|
5517
5806
|
if (key === null)
|
|
5518
5807
|
return false;
|
|
@@ -5521,26 +5810,41 @@ const createQueryRepository = (config) => {
|
|
|
5521
5810
|
return false;
|
|
5522
5811
|
cacheEntry.consumers.delete(consumerDestroyRef);
|
|
5523
5812
|
if (cacheEntry.consumers.size === 0) {
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5813
|
+
// Only data is worth keeping around: an entry that never produced a response has nothing to
|
|
5814
|
+
// hand back to a returning consumer, so it is aborted immediately as it always was.
|
|
5815
|
+
if (cacheEntry.keepUnusedFor > 0 && cacheEntry.request.response() !== null) {
|
|
5816
|
+
retain(key, cacheEntry);
|
|
5817
|
+
}
|
|
5818
|
+
else {
|
|
5819
|
+
destroyEntry(key, cacheEntry);
|
|
5820
|
+
}
|
|
5527
5821
|
}
|
|
5528
5822
|
bumpCacheVersion();
|
|
5529
5823
|
return true;
|
|
5530
5824
|
};
|
|
5531
5825
|
const unbindAllSecure = () => {
|
|
5532
5826
|
for (const [key, cacheEntry] of cache.entries()) {
|
|
5533
|
-
if (cacheEntry.isSecure)
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
}
|
|
5827
|
+
if (!cacheEntry.isSecure)
|
|
5828
|
+
continue;
|
|
5829
|
+
// Force the teardown instead of routing through `unbind`: a logged out session must not leave a
|
|
5830
|
+
// retained response body behind waiting out its `keepUnusedFor` window.
|
|
5831
|
+
cacheEntry.consumers.clear();
|
|
5832
|
+
destroyEntry(key, cacheEntry);
|
|
5540
5833
|
}
|
|
5541
5834
|
bumpCacheVersion();
|
|
5835
|
+
eventsSubject.next({ type: 'unbind-all-secure' });
|
|
5836
|
+
};
|
|
5837
|
+
const refreshInUse = () => {
|
|
5838
|
+
for (const cacheEntry of cache.values()) {
|
|
5839
|
+
if (cacheEntry.consumers.size === 0)
|
|
5840
|
+
continue;
|
|
5841
|
+
// Re-firing a mutation would be a side effect nobody asked for, so only reads are refreshed.
|
|
5842
|
+
if (!shouldCacheQuery(cacheEntry.request.method))
|
|
5843
|
+
continue;
|
|
5844
|
+
cacheEntry.request.execute({ force: true });
|
|
5845
|
+
}
|
|
5542
5846
|
};
|
|
5543
|
-
const bind = (key, consumerDestroyRef, request, isSecure, allowReuse) => {
|
|
5847
|
+
const bind = (key, consumerDestroyRef, request, isSecure, allowReuse, keepUnusedFor) => {
|
|
5544
5848
|
const destroyListener = consumerDestroyRef.onDestroy(() => unbind(key, consumerDestroyRef));
|
|
5545
5849
|
const cacheEntry = cache.get(key);
|
|
5546
5850
|
if (cacheEntry && allowReuse) {
|
|
@@ -5567,6 +5871,7 @@ const createQueryRepository = (config) => {
|
|
|
5567
5871
|
request,
|
|
5568
5872
|
isSecure,
|
|
5569
5873
|
eventSubscription,
|
|
5874
|
+
keepUnusedFor,
|
|
5570
5875
|
});
|
|
5571
5876
|
}
|
|
5572
5877
|
bumpCacheVersion();
|
|
@@ -5575,21 +5880,21 @@ const createQueryRepository = (config) => {
|
|
|
5575
5880
|
key,
|
|
5576
5881
|
isSecure: entry.isSecure,
|
|
5577
5882
|
consumerCount: entry.consumers.size,
|
|
5883
|
+
isUnused: entry.consumers.size === 0,
|
|
5578
5884
|
request: entry.request,
|
|
5579
5885
|
}));
|
|
5580
5886
|
const evict = (key) => {
|
|
5581
5887
|
const entry = cache.get(key);
|
|
5582
5888
|
if (!entry)
|
|
5583
5889
|
return;
|
|
5584
|
-
entry
|
|
5585
|
-
entry.eventSubscription?.unsubscribe();
|
|
5586
|
-
cache.delete(key);
|
|
5890
|
+
destroyEntry(key, entry);
|
|
5587
5891
|
bumpCacheVersion();
|
|
5588
5892
|
};
|
|
5589
5893
|
const repository = {
|
|
5590
5894
|
request,
|
|
5591
5895
|
unbind,
|
|
5592
5896
|
unbindAllSecure,
|
|
5897
|
+
refreshInUse,
|
|
5593
5898
|
events$: eventsSubject.asObservable(),
|
|
5594
5899
|
subtle: {
|
|
5595
5900
|
cacheEntries,
|
|
@@ -5606,11 +5911,15 @@ const createQueryClient = (options) => createRootProvider(() => {
|
|
|
5606
5911
|
const injector = inject(Injector);
|
|
5607
5912
|
const repository = createQueryRepository({
|
|
5608
5913
|
...options,
|
|
5914
|
+
// Retaining unused entries on the server would pin response bodies (and a pending timer) inside
|
|
5915
|
+
// a per-request injector for the whole window, so retention is browser only.
|
|
5916
|
+
retentionEnabled: isPlatformBrowser(inject(PLATFORM_ID)),
|
|
5609
5917
|
dependencies: { httpClient, ngErrorHandler, injector },
|
|
5610
5918
|
});
|
|
5611
5919
|
const client = {
|
|
5612
5920
|
repository,
|
|
5613
5921
|
baseUrl: options.baseUrl,
|
|
5922
|
+
refreshQueriesInUse: () => repository.refreshInUse(),
|
|
5614
5923
|
};
|
|
5615
5924
|
return client;
|
|
5616
5925
|
}, {
|
|
@@ -6453,6 +6762,7 @@ const createBearerAuthProviderImpl = (config) => {
|
|
|
6453
6762
|
latestExecutedQuery: latestExecutedQuery.asReadonly(),
|
|
6454
6763
|
latestNonInternalQuery: latestNonInternalQuery.asReadonly(),
|
|
6455
6764
|
executionState: executionState.asReadonly(),
|
|
6765
|
+
setTokens,
|
|
6456
6766
|
logout,
|
|
6457
6767
|
afterTokenRefresh$,
|
|
6458
6768
|
};
|
|
@@ -7579,7 +7889,7 @@ class DateArrayQueryField {
|
|
|
7579
7889
|
};
|
|
7580
7890
|
}
|
|
7581
7891
|
}
|
|
7582
|
-
const IGNORED_FILTER_COUNT_FIELDS = ['page', 'skip', 'take', 'limit', 'sort', 'sortBy', 'sortOrder', 'query', 'search'];
|
|
7892
|
+
const IGNORED_FILTER_COUNT_FIELDS$1 = ['page', 'skip', 'take', 'limit', 'sort', 'sortBy', 'sortOrder', 'query', 'search'];
|
|
7583
7893
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7584
7894
|
class QueryForm {
|
|
7585
7895
|
get _formValue() {
|
|
@@ -7654,7 +7964,7 @@ class QueryForm {
|
|
|
7654
7964
|
*/
|
|
7655
7965
|
this.activeFilterCount$ = this.changes$.pipe(map(({ currentValue }) => Object.entries(currentValue)
|
|
7656
7966
|
.map(([key, value]) => {
|
|
7657
|
-
if (IGNORED_FILTER_COUNT_FIELDS.includes(key) || this._fields[key]?.data.skipInFilterCount) {
|
|
7967
|
+
if (IGNORED_FILTER_COUNT_FIELDS$1.includes(key) || this._fields[key]?.data.skipInFilterCount) {
|
|
7658
7968
|
return true;
|
|
7659
7969
|
}
|
|
7660
7970
|
return this._isDefaultValue(key, value);
|
|
@@ -8020,6 +8330,433 @@ class QueryForm {
|
|
|
8020
8330
|
}
|
|
8021
8331
|
}
|
|
8022
8332
|
|
|
8333
|
+
/** URL sentinel for an explicit `null` value (a bare empty param would be ambiguous). */
|
|
8334
|
+
const ET_NULL_VALUE = 'ET_NULL__';
|
|
8335
|
+
/**
|
|
8336
|
+
* Fields excluded from `activeFilterCount` by default — pagination, sorting and
|
|
8337
|
+
* search are navigation state, not filters.
|
|
8338
|
+
*/
|
|
8339
|
+
const IGNORED_FILTER_COUNT_FIELDS = [
|
|
8340
|
+
'page',
|
|
8341
|
+
'skip',
|
|
8342
|
+
'take',
|
|
8343
|
+
'limit',
|
|
8344
|
+
'sort',
|
|
8345
|
+
'sortBy',
|
|
8346
|
+
'sortOrder',
|
|
8347
|
+
'query',
|
|
8348
|
+
'search',
|
|
8349
|
+
];
|
|
8350
|
+
const resolveDefault = (def) => {
|
|
8351
|
+
const { defaultValue } = def;
|
|
8352
|
+
return typeof defaultValue === 'function' ? defaultValue() : (defaultValue ?? null);
|
|
8353
|
+
};
|
|
8354
|
+
const buildDefaults = (fields) => {
|
|
8355
|
+
const out = {};
|
|
8356
|
+
for (const [key, def] of Object.entries(fields)) {
|
|
8357
|
+
out[key] = resolveDefault(def);
|
|
8358
|
+
}
|
|
8359
|
+
return out;
|
|
8360
|
+
};
|
|
8361
|
+
const changedKeysBetween = (previous, current) => Object.keys(current).filter((key) => !equal(previous?.[key], current[key]));
|
|
8362
|
+
/** Best-effort URL string → value coercion, mirroring the auto-transform of the legacy QueryForm. */
|
|
8363
|
+
const autoCoerce = (raw, defaultValue) => {
|
|
8364
|
+
if (raw === ET_NULL_VALUE)
|
|
8365
|
+
return null;
|
|
8366
|
+
if (typeof raw !== 'string')
|
|
8367
|
+
return raw;
|
|
8368
|
+
const defaultIsNumber = typeof defaultValue === 'number';
|
|
8369
|
+
const looksNumeric = raw.trim() === raw && !raw.startsWith('0') && !raw.endsWith('.') && !isNaN(Number(raw));
|
|
8370
|
+
if (defaultIsNumber || (looksNumeric && raw !== '')) {
|
|
8371
|
+
return transformToNumber(raw);
|
|
8372
|
+
}
|
|
8373
|
+
if (raw === 'true' || raw === 'false') {
|
|
8374
|
+
return transformToBoolean(raw);
|
|
8375
|
+
}
|
|
8376
|
+
return raw;
|
|
8377
|
+
};
|
|
8378
|
+
const computeFilterCount = (fields, value, defaults) => {
|
|
8379
|
+
let count = 0;
|
|
8380
|
+
for (const [key, def] of Object.entries(fields)) {
|
|
8381
|
+
if (IGNORED_FILTER_COUNT_FIELDS.includes(key) || def.skipInFilterCount)
|
|
8382
|
+
continue;
|
|
8383
|
+
if (!equal(value[key], defaults[key]))
|
|
8384
|
+
count++;
|
|
8385
|
+
}
|
|
8386
|
+
return count;
|
|
8387
|
+
};
|
|
8388
|
+
const createBranch = (fields, initial, injector) => {
|
|
8389
|
+
const defaults = buildDefaults(fields);
|
|
8390
|
+
const model = signal(clone(initial), /* @ts-ignore */
|
|
8391
|
+
...(ngDevMode ? [{ debugName: "model" }] : /* istanbul ignore next */ []));
|
|
8392
|
+
const tree = form(model, { injector });
|
|
8393
|
+
return {
|
|
8394
|
+
fields: tree,
|
|
8395
|
+
value: model.asReadonly(),
|
|
8396
|
+
activeFilterCount: computed(() => computeFilterCount(fields, model(), defaults)),
|
|
8397
|
+
setValue: (value) => model.set(clone(value)),
|
|
8398
|
+
patchValue: (value) => model.update((cur) => ({ ...cur, ...value })),
|
|
8399
|
+
resetFieldToDefault: (key) => model.update((cur) => ({ ...cur, [key]: defaults[key] })),
|
|
8400
|
+
resetAllFieldsToDefault: () => model.set(clone(defaults)),
|
|
8401
|
+
};
|
|
8402
|
+
};
|
|
8403
|
+
/**
|
|
8404
|
+
* A signals-first query form: binds on-screen controls to typed fields, keeps a
|
|
8405
|
+
* debounced committed value, syncs to the URL query params, and resets dependent
|
|
8406
|
+
* fields via the `isResetBy` graph — all as signals.
|
|
8407
|
+
*
|
|
8408
|
+
* @example
|
|
8409
|
+
* const qf = createQueryForm({
|
|
8410
|
+
* fields: {
|
|
8411
|
+
* search: searchQueryField(),
|
|
8412
|
+
* sort: sortQueryField(),
|
|
8413
|
+
* page: queryField<number>({ defaultValue: 1, isResetBy: ['search', 'sort'] }),
|
|
8414
|
+
* },
|
|
8415
|
+
* }).observe();
|
|
8416
|
+
* // template: <input etInput [formField]="qf.fields.search" />
|
|
8417
|
+
*/
|
|
8418
|
+
class QueryFormSignals {
|
|
8419
|
+
constructor(config) {
|
|
8420
|
+
this._router = inject(Router);
|
|
8421
|
+
this._route = inject(ActivatedRoute);
|
|
8422
|
+
this._injector = inject(Injector);
|
|
8423
|
+
this._previous = signal(null, /* @ts-ignore */
|
|
8424
|
+
...(ngDevMode ? [{ debugName: "_previous" }] : /* istanbul ignore next */ []));
|
|
8425
|
+
this._observing = signal(false, /* @ts-ignore */
|
|
8426
|
+
...(ngDevMode ? [{ debugName: "_observing" }] : /* istanbul ignore next */ []));
|
|
8427
|
+
this._queryParamChanges = injectQueryParamChanges();
|
|
8428
|
+
this._pendingTimer = null;
|
|
8429
|
+
this._skipNextResets = false;
|
|
8430
|
+
assertInInjectionContext(QueryFormSignals);
|
|
8431
|
+
this._fields = config.fields;
|
|
8432
|
+
this._prefix = config.queryParamPrefix;
|
|
8433
|
+
this._defaults = buildDefaults(config.fields);
|
|
8434
|
+
this.defaultValue = clone(this._defaults);
|
|
8435
|
+
const initial = clone(this._defaults);
|
|
8436
|
+
this._model = signal(initial, /* @ts-ignore */
|
|
8437
|
+
...(ngDevMode ? [{ debugName: "_model" }] : /* istanbul ignore next */ []));
|
|
8438
|
+
this._committed = signal(clone(initial), /* @ts-ignore */
|
|
8439
|
+
...(ngDevMode ? [{ debugName: "_committed" }] : /* istanbul ignore next */ []));
|
|
8440
|
+
this.fields = form(this._model);
|
|
8441
|
+
this.value = this._committed.asReadonly();
|
|
8442
|
+
this.previousValue = this._previous.asReadonly();
|
|
8443
|
+
this.changes = computed(() => ({ previousValue: this._previous(), currentValue: this._committed() }), /* @ts-ignore */
|
|
8444
|
+
...(ngDevMode ? [{ debugName: "changes" }] : /* istanbul ignore next */ []));
|
|
8445
|
+
this.activeFilterCount = computed(() => computeFilterCount(this._fields, this._committed(), this._defaults), /* @ts-ignore */
|
|
8446
|
+
...(ngDevMode ? [{ debugName: "activeFilterCount" }] : /* istanbul ignore next */ []));
|
|
8447
|
+
// React to live control edits: schedule a debounced commit.
|
|
8448
|
+
effect(() => {
|
|
8449
|
+
const live = this._model();
|
|
8450
|
+
untracked(() => this._onLiveChange(live));
|
|
8451
|
+
});
|
|
8452
|
+
// React to navigation (back/forward, external links): apply URL → form immediately.
|
|
8453
|
+
effect(() => {
|
|
8454
|
+
const changes = this._queryParamChanges();
|
|
8455
|
+
untracked(() => {
|
|
8456
|
+
if (!this._observing() || this._observeOptions?.syncOnNavigation === false)
|
|
8457
|
+
return;
|
|
8458
|
+
this._applyFromUrl(changes);
|
|
8459
|
+
});
|
|
8460
|
+
});
|
|
8461
|
+
inject(DestroyRef).onDestroy(() => this._cleanup());
|
|
8462
|
+
}
|
|
8463
|
+
observe(options) {
|
|
8464
|
+
if (this._observing()) {
|
|
8465
|
+
if (isDevMode()) {
|
|
8466
|
+
console.warn('QueryFormSignals.observe() was called more than once. Ignoring the extra call.');
|
|
8467
|
+
}
|
|
8468
|
+
return this;
|
|
8469
|
+
}
|
|
8470
|
+
this._observeOptions = options;
|
|
8471
|
+
this._observing.set(true);
|
|
8472
|
+
if (options?.syncOnNavigation !== false) {
|
|
8473
|
+
this._applyFromUrl(this._route.snapshot.queryParams);
|
|
8474
|
+
}
|
|
8475
|
+
// Commit whatever the model holds now (URL-restored or programmatic defaults).
|
|
8476
|
+
this._flush();
|
|
8477
|
+
return this;
|
|
8478
|
+
}
|
|
8479
|
+
unobserve() {
|
|
8480
|
+
this._cleanup();
|
|
8481
|
+
}
|
|
8482
|
+
setValue(value, options) {
|
|
8483
|
+
if (options?.skipResets)
|
|
8484
|
+
this._skipNextResets = true;
|
|
8485
|
+
this._model.set(clone(value));
|
|
8486
|
+
}
|
|
8487
|
+
patchValue(value, options) {
|
|
8488
|
+
if (options?.skipResets)
|
|
8489
|
+
this._skipNextResets = true;
|
|
8490
|
+
this._model.update((cur) => ({ ...cur, ...value }));
|
|
8491
|
+
}
|
|
8492
|
+
resetFieldToDefault(key, options) {
|
|
8493
|
+
if (options?.skipResets)
|
|
8494
|
+
this._skipNextResets = true;
|
|
8495
|
+
this._model.update((cur) => ({ ...cur, [key]: this._defaults[key] }));
|
|
8496
|
+
}
|
|
8497
|
+
resetFieldsToDefault(keys, options) {
|
|
8498
|
+
if (options?.skipResets)
|
|
8499
|
+
this._skipNextResets = true;
|
|
8500
|
+
this._model.update((cur) => {
|
|
8501
|
+
const next = { ...cur };
|
|
8502
|
+
for (const key of keys) {
|
|
8503
|
+
next[key] = this._defaults[key];
|
|
8504
|
+
}
|
|
8505
|
+
return next;
|
|
8506
|
+
});
|
|
8507
|
+
}
|
|
8508
|
+
resetAllFieldsToDefault(options) {
|
|
8509
|
+
const skip = new Set((options?.skipFields ?? []).map((key) => key));
|
|
8510
|
+
const keys = Object.keys(this._fields).filter((key) => !skip.has(key));
|
|
8511
|
+
this.resetFieldsToDefault(keys, options);
|
|
8512
|
+
}
|
|
8513
|
+
/** Create a detached editor over the same fields, seeded from the current committed value. */
|
|
8514
|
+
branch() {
|
|
8515
|
+
return createBranch(this._fields, clone(this._committed()), this._injector);
|
|
8516
|
+
}
|
|
8517
|
+
_onLiveChange(live) {
|
|
8518
|
+
if (!this._observing())
|
|
8519
|
+
return;
|
|
8520
|
+
if (equal(live, this._committed()))
|
|
8521
|
+
return;
|
|
8522
|
+
const changedKeys = changedKeysBetween(this._committed(), live);
|
|
8523
|
+
const debounceMs = this._resolveDebounce(changedKeys, live);
|
|
8524
|
+
this._clearTimer();
|
|
8525
|
+
if (debounceMs === null) {
|
|
8526
|
+
this._flush();
|
|
8527
|
+
}
|
|
8528
|
+
else {
|
|
8529
|
+
this._pendingTimer = setTimeout(() => {
|
|
8530
|
+
this._pendingTimer = null;
|
|
8531
|
+
this._flush();
|
|
8532
|
+
}, debounceMs);
|
|
8533
|
+
}
|
|
8534
|
+
}
|
|
8535
|
+
/** Shortest-debounce-wins; `null` means commit immediately. */
|
|
8536
|
+
_resolveDebounce(changedKeys, live) {
|
|
8537
|
+
if (!changedKeys.length)
|
|
8538
|
+
return null;
|
|
8539
|
+
const times = [];
|
|
8540
|
+
for (const key of changedKeys) {
|
|
8541
|
+
const def = this._fields[key];
|
|
8542
|
+
if (!def)
|
|
8543
|
+
return null;
|
|
8544
|
+
if (def.disableDebounceIfFalsy && !live[key])
|
|
8545
|
+
return null;
|
|
8546
|
+
if (def.debounce === undefined)
|
|
8547
|
+
return null;
|
|
8548
|
+
times.push(def.debounce);
|
|
8549
|
+
}
|
|
8550
|
+
return Math.min(...times);
|
|
8551
|
+
}
|
|
8552
|
+
_flush() {
|
|
8553
|
+
this._clearTimer();
|
|
8554
|
+
const live = this._model();
|
|
8555
|
+
const previous = this._committed();
|
|
8556
|
+
if (equal(live, previous)) {
|
|
8557
|
+
this._skipNextResets = false;
|
|
8558
|
+
return;
|
|
8559
|
+
}
|
|
8560
|
+
const changedKeys = changedKeysBetween(previous, live);
|
|
8561
|
+
const next = this._skipNextResets ? { ...live } : this._applyResets(live, changedKeys);
|
|
8562
|
+
this._skipNextResets = false;
|
|
8563
|
+
this._previous.set(clone(previous));
|
|
8564
|
+
this._committed.set(clone(next));
|
|
8565
|
+
// Reflect any reset overrides back into the bound controls. The effect re-runs
|
|
8566
|
+
// but no-ops because the model now equals the committed value.
|
|
8567
|
+
if (!equal(next, live)) {
|
|
8568
|
+
this._model.set(clone(next));
|
|
8569
|
+
}
|
|
8570
|
+
if (this._observing() && this._observeOptions?.writeToQueryParams !== false) {
|
|
8571
|
+
this._writeToUrl(next);
|
|
8572
|
+
}
|
|
8573
|
+
}
|
|
8574
|
+
_applyResets(live, changedKeys) {
|
|
8575
|
+
const next = { ...live };
|
|
8576
|
+
for (const [key, def] of Object.entries(this._fields)) {
|
|
8577
|
+
const resets = def.isResetBy;
|
|
8578
|
+
if (!resets?.length)
|
|
8579
|
+
continue;
|
|
8580
|
+
const triggered = resets.some((resetKey) => {
|
|
8581
|
+
if (!(resetKey in this._fields)) {
|
|
8582
|
+
if (isDevMode()) {
|
|
8583
|
+
console.warn(`QueryFormSignals: isResetBy references unknown field "${resetKey}". Is it a typo?`);
|
|
8584
|
+
}
|
|
8585
|
+
return false;
|
|
8586
|
+
}
|
|
8587
|
+
return changedKeys.includes(resetKey);
|
|
8588
|
+
});
|
|
8589
|
+
if (!triggered)
|
|
8590
|
+
continue;
|
|
8591
|
+
const defaultValue = this._defaults[key];
|
|
8592
|
+
if (!equal(next[key], defaultValue)) {
|
|
8593
|
+
next[key] = defaultValue;
|
|
8594
|
+
}
|
|
8595
|
+
}
|
|
8596
|
+
return next;
|
|
8597
|
+
}
|
|
8598
|
+
_applyFromUrl(params) {
|
|
8599
|
+
const current = { ...this._committed() };
|
|
8600
|
+
let changed = false;
|
|
8601
|
+
for (const [key, def] of Object.entries(this._fields)) {
|
|
8602
|
+
const raw = params[this._paramKey(key)];
|
|
8603
|
+
if (raw === undefined)
|
|
8604
|
+
continue;
|
|
8605
|
+
const value = raw === ET_PROPERTY_REMOVED ? this._defaults[key] : this._deserialize(def, raw);
|
|
8606
|
+
if (!equal(current[key], value)) {
|
|
8607
|
+
current[key] = value;
|
|
8608
|
+
changed = true;
|
|
8609
|
+
}
|
|
8610
|
+
}
|
|
8611
|
+
if (!changed)
|
|
8612
|
+
return;
|
|
8613
|
+
this._commitFromUrl(current);
|
|
8614
|
+
}
|
|
8615
|
+
_commitFromUrl(next) {
|
|
8616
|
+
this._clearTimer();
|
|
8617
|
+
this._previous.set(clone(this._committed()));
|
|
8618
|
+
this._committed.set(clone(next));
|
|
8619
|
+
// Feed the URL value into the live model & bound controls; the effect no-ops (model === committed).
|
|
8620
|
+
this._model.set(clone(next));
|
|
8621
|
+
}
|
|
8622
|
+
_deserialize(def, raw) {
|
|
8623
|
+
if (def.queryParamToValue)
|
|
8624
|
+
return def.queryParamToValue(raw);
|
|
8625
|
+
if (def.skipAutoTransform)
|
|
8626
|
+
return raw;
|
|
8627
|
+
return autoCoerce(raw, resolveDefault(def));
|
|
8628
|
+
}
|
|
8629
|
+
_writeToUrl(value) {
|
|
8630
|
+
const queryParams = {};
|
|
8631
|
+
for (const [key, def] of Object.entries(this._fields)) {
|
|
8632
|
+
const paramKey = this._paramKey(key);
|
|
8633
|
+
const fieldValue = value[key];
|
|
8634
|
+
const isDefault = equal(fieldValue, this._defaults[key]);
|
|
8635
|
+
const writeToUrl = def.appendToUrl !== false;
|
|
8636
|
+
const writeDefault = def.appendDefaultValueToUrl === true;
|
|
8637
|
+
if (!writeToUrl || (isDefault && !writeDefault)) {
|
|
8638
|
+
queryParams[paramKey] = undefined;
|
|
8639
|
+
}
|
|
8640
|
+
else if (def.valueToQueryParam) {
|
|
8641
|
+
queryParams[paramKey] = def.valueToQueryParam(fieldValue);
|
|
8642
|
+
}
|
|
8643
|
+
else {
|
|
8644
|
+
queryParams[paramKey] = fieldValue === null ? ET_NULL_VALUE : fieldValue;
|
|
8645
|
+
}
|
|
8646
|
+
}
|
|
8647
|
+
queueMicrotask(() => {
|
|
8648
|
+
this._router.navigate([], {
|
|
8649
|
+
queryParams,
|
|
8650
|
+
queryParamsHandling: 'merge',
|
|
8651
|
+
replaceUrl: this._observeOptions?.replaceUrl,
|
|
8652
|
+
});
|
|
8653
|
+
});
|
|
8654
|
+
}
|
|
8655
|
+
_paramKey(key) {
|
|
8656
|
+
const prefix = this._prefix;
|
|
8657
|
+
if (!prefix)
|
|
8658
|
+
return key;
|
|
8659
|
+
return `${typeof prefix === 'string' ? prefix : prefix()}-${key}`;
|
|
8660
|
+
}
|
|
8661
|
+
_clearTimer() {
|
|
8662
|
+
if (this._pendingTimer !== null) {
|
|
8663
|
+
clearTimeout(this._pendingTimer);
|
|
8664
|
+
this._pendingTimer = null;
|
|
8665
|
+
}
|
|
8666
|
+
}
|
|
8667
|
+
_cleanup() {
|
|
8668
|
+
if (!this._observing())
|
|
8669
|
+
return;
|
|
8670
|
+
this._observing.set(false);
|
|
8671
|
+
this._clearTimer();
|
|
8672
|
+
if (this._observeOptions?.writeToQueryParams === false)
|
|
8673
|
+
return;
|
|
8674
|
+
const queryParams = {};
|
|
8675
|
+
for (const key of Object.keys(this._fields)) {
|
|
8676
|
+
queryParams[this._paramKey(key)] = undefined;
|
|
8677
|
+
}
|
|
8678
|
+
queueMicrotask(() => {
|
|
8679
|
+
this._router.navigate([], { queryParams, queryParamsHandling: 'merge', replaceUrl: true });
|
|
8680
|
+
});
|
|
8681
|
+
}
|
|
8682
|
+
}
|
|
8683
|
+
/**
|
|
8684
|
+
* Create a {@link QueryFormSignals}. Call `.observe()` to start syncing with the
|
|
8685
|
+
* URL. Must be called in an injection context.
|
|
8686
|
+
*/
|
|
8687
|
+
const createQueryForm = (config) => new QueryFormSignals(config);
|
|
8688
|
+
|
|
8689
|
+
const normalizeConfig = (config) => {
|
|
8690
|
+
if (!config)
|
|
8691
|
+
return {};
|
|
8692
|
+
const { isResetBy, ...rest } = config;
|
|
8693
|
+
return {
|
|
8694
|
+
...rest,
|
|
8695
|
+
isResetBy: isResetBy === undefined ? undefined : Array.isArray(isResetBy) ? isResetBy : [isResetBy],
|
|
8696
|
+
};
|
|
8697
|
+
};
|
|
8698
|
+
/**
|
|
8699
|
+
* A generic query-form field. Defaults to `null` and, unless a transform is
|
|
8700
|
+
* given, relies on the auto-coercion when reading back from the URL.
|
|
8701
|
+
*
|
|
8702
|
+
* @example
|
|
8703
|
+
* queryField<number>({ defaultValue: 1, isResetBy: ['search'] })
|
|
8704
|
+
*/
|
|
8705
|
+
const queryField = (config) => ({
|
|
8706
|
+
defaultValue: null,
|
|
8707
|
+
...normalizeConfig(config),
|
|
8708
|
+
});
|
|
8709
|
+
/**
|
|
8710
|
+
* A debounced free-text search field. Debounces typing by 300ms but applies
|
|
8711
|
+
* clearing immediately (`disableDebounceIfFalsy`).
|
|
8712
|
+
*/
|
|
8713
|
+
const searchQueryField = (config) => ({
|
|
8714
|
+
defaultValue: null,
|
|
8715
|
+
debounce: 300,
|
|
8716
|
+
disableDebounceIfFalsy: true,
|
|
8717
|
+
...normalizeConfig(config),
|
|
8718
|
+
});
|
|
8719
|
+
/**
|
|
8720
|
+
* A sort field serialized as `"active:direction"` (e.g. `"name:asc"`). Matches
|
|
8721
|
+
* the wire format the table system's URL adapter uses, so the two interoperate.
|
|
8722
|
+
*/
|
|
8723
|
+
const sortQueryField = (config) => ({
|
|
8724
|
+
defaultValue: null,
|
|
8725
|
+
queryParamToValue: transformToSort,
|
|
8726
|
+
valueToQueryParam: transformToSortQueryParam,
|
|
8727
|
+
...normalizeConfig(config),
|
|
8728
|
+
});
|
|
8729
|
+
/** A field holding a list of strings. */
|
|
8730
|
+
const stringArrayQueryField = (config) => ({
|
|
8731
|
+
defaultValue: null,
|
|
8732
|
+
queryParamToValue: transformToStringArray,
|
|
8733
|
+
...normalizeConfig(config),
|
|
8734
|
+
});
|
|
8735
|
+
/** A field holding a list of numbers. */
|
|
8736
|
+
const numberArrayQueryField = (config) => ({
|
|
8737
|
+
defaultValue: null,
|
|
8738
|
+
queryParamToValue: transformToNumberArray,
|
|
8739
|
+
...normalizeConfig(config),
|
|
8740
|
+
});
|
|
8741
|
+
/** A field holding a list of booleans. */
|
|
8742
|
+
const booleanArrayQueryField = (config) => ({
|
|
8743
|
+
defaultValue: null,
|
|
8744
|
+
queryParamToValue: transformToBooleanArray,
|
|
8745
|
+
...normalizeConfig(config),
|
|
8746
|
+
});
|
|
8747
|
+
/** A single date field (expects an ISO/`Date`-parseable string in the URL). */
|
|
8748
|
+
const dateQueryField = (config) => ({
|
|
8749
|
+
defaultValue: null,
|
|
8750
|
+
queryParamToValue: transformToDate,
|
|
8751
|
+
...normalizeConfig(config),
|
|
8752
|
+
});
|
|
8753
|
+
/** A field holding a list of dates. */
|
|
8754
|
+
const dateArrayQueryField = (config) => ({
|
|
8755
|
+
defaultValue: null,
|
|
8756
|
+
queryParamToValue: transformToDateArray,
|
|
8757
|
+
...normalizeConfig(config),
|
|
8758
|
+
});
|
|
8759
|
+
|
|
8023
8760
|
// codes 1000-1999
|
|
8024
8761
|
const WebSocketRuntimeErrorCode = {
|
|
8025
8762
|
// Web Socket Client
|
|
@@ -8178,5 +8915,5 @@ const createWebSocketClient = (options) => {
|
|
|
8178
8915
|
* Generated bundle index. Do not edit.
|
|
8179
8916
|
*/
|
|
8180
8917
|
|
|
8181
|
-
export { BasicAuthProvider, BearerAuthFeatureType, BooleanArrayQueryField, CLEAR_QUERY_ARGS, CustomHeaderAuthProvider, DateArrayQueryField, DateQueryField, EntityStore, INFINITY_QUERY_TOKEN, InfinityQuery, InfinityQueryDirective, InfinityQueryTriggerDirective, LegacyQuery, LegacyQueryCreator, NumberArrayQueryField, ParseHttpErrorCodeToMessageDePipe, ParseHttpErrorCodeToMessageEnPipe, ParseHttpErrorCodeToTitleDePipe, ParseHttpErrorCodeToTitleEnPipe, QUERY_CLIENT_DEVTOOLS_TOKEN, QueryDevtoolsComponent, QueryDirective, QueryError, QueryFeatureType, QueryField, QueryForm, QueryRuntimeErrorCode, QueryShortNamePipe, QueryStore, SERVER_ERROR_KIND, SERVER_VIOLATION_ERROR_KIND, SPEED_BUFFER_TIME_IN_MS, SearchQueryField, SortQueryField, StringArrayQueryField, V2BearerAuthProvider, V2Query, V2QueryClient, V2QueryCreator, addQueryContainerHandling, applyQueryFeatures, authExtractTokensResponseMissingAccessToken, authExtractTokensResponseMissingRefreshToken, authExtractTokensResponseNotObject, authProviderFeatureUsedMultipleTimes, buildErrorMessage, buildQueryCacheKey, buildQueryString, buildRoute, buildTimestampFromSeconds, castQueryCreatorTypes, circularQueryDependency, circularQueryDependencyChecker, computeQueryAuthHeader, computeQueryBody, computeQueryHeaders, computeQueryMethod, computeQueryQueryParams, consumeSuppressQueryStackDevtools, contentfulGqlLikePaginationAdapter, createBaseQuery, createBaseQueryCreator, createBearerAuthProvider, createDeleteQuery, createExecuteFn, createGetQuery, createGqlExecuteFn, createGqlMutationViaGet, createGqlMutationViaPost, createGqlQuery, createGqlQueryCreator, createGqlQueryViaGet, createGqlQueryViaPost, createHttpRequest, createInfinityQueryConfig, createLegacyQueryCreator, createPagedQueryStack, createPatchQuery, createPersistentAuthFeature, createPostQuery, createPutQuery, createQuery, createQueryClient, createQueryCollection, createQueryCollectionSignal, createQueryCollectionSubject, createQueryContext, createQueryCreator, createQueryErrorResponse, createQueryFeature, createQueryObject, createQueryRepository, createQuerySnapshotFn, createQueryStack, createSecureDeleteQuery, createSecureExecuteFactory, createSecureExecuteFn, createSecureGetQuery, createSecureGqlExecuteFn, createSecureGqlMutationViaGet, createSecureGqlMutationViaPost, createSecureGqlQuery, createSecureGqlQueryCreator, createSecureGqlQueryViaGet, createSecureGqlQueryViaPost, createSecurePatchQuery, createSecurePostQuery, createSecurePutQuery, createSecureQuery, createSecureQueryCreator, createTrackingFeature, createWebSocketClient, decryptBearer, deepFreeze, def, detectContentTypeHeader, dynLikePaginationAdapter, effectComputed, ethletePaginationAdapter, executeUntilSettled, extractExpiresInSeconds, extractFormViolations, extractQuery, fakePaginationAdapter, filterFailure, filterNull, filterQueryStates, filterSuccess, forEachHeader, getDefaultHeaders, getQueryClientName, getQueryFeatureUsage, getResponseUrl, ggLikePaginationAdapter, gql, gqlDataPropertyMissingInResponse, hasHeader, ignoreAutoRefresh, injectInfinityQueryResponseDelay, injectQueryContext, insertFrom, invalidBaseRouteError, invalidBodyError, invalidRouteError, invalidStateInsideSecureExecuteFactory, isAutoRefresh, isBasicAuthProvider, isBearerAuthProvider, isClassValidatorError, isCreateGqlQueryOptions, isCustomHeaderAuthProvider, isEmptyString, isGqlQueryConfig, isLegacyQuery, isNaN$1 as isNaN, isQuery, isQueryCollection, isQueryDevtoolsEnabled, isQueryErrorResponse, isQueryStateCancelled, isQueryStateFailure, isQueryStateLoading, isQueryStatePrepared, isQueryStateSuccess, isRequestError, isSymfonyError, isSymfonyFormViolationListError, isSymfonyListError, isSymfonyPagerfantaOutOfRangeError, mapToPaginated, mapViolationsToFormErrors, maybeExecute, mergeHeaders, nestedEffect, pagedQueryStackNextPageCalledWithoutPreviousPage, pagedQueryStackPageBiggerThanTotalPages, pagedQueryStackPreviousPageCalledButAlreadyAtFirstPage, paginatedEntityValueUpdater, parseAllXhrResponseHeaders, parseHttpErrorCodeToMessageDe, parseHttpErrorCodeToMessageEn, parseHttpErrorCodeToTitleDe, parseHttpErrorCodeToTitleEn, pathParamsMissingInRouteFunctionError, provideInfinityQueryResponseDelay, provideQueryClientForDevtools, provideQueryDevtools, queryArrayComputed, queryComputed, queryComputedTillTruthy, queryDevtoolsEntries, queryExecute, queryFeatureUsedMultipleTimes, querySequence, querySequenceAlreadyRunning, queryStackTotalQueriesAndExpectedQueriesMismatch, queryStackWithArgsUsed, queryStackWithResponseUpdateUsed, queryStateAlreadyHasKeyError, queryStateCannotTransformError, queryStateDoesNotContainKeyError, queryStateErrorSignal, queryStateLoadingSignal, queryStateResponseSignal, queryStateSignal, registerQueryDevtoolsEntry, removeFrom, request, resetExecuteState, resetPageOnError, serializeBody, setupQueryDependencies, setupQueryExecuteState, setupQueryState, shouldAutoExecuteGqlQuery, shouldAutoExecuteQuery, shouldCacheQuery, shouldRetryRequest, silenceMissingWithArgsFeatureErrorUsedButWithArgsPresent, skipPaginationPageParamCalculator, splitQueryConfig, stringifyQueryRoute, suppressNextQueryStackDevtools, switchQueryCollectionState, switchQueryState, takeUntilResponse, toQuerySignal, toQuerySubject, tokensNotAvailableInsideAuthAndExec, transformArrayResponse, transformExecStateToQueryState, transformGql, transformMethod, transformPaginatedResponse, transformToBoolean, transformToBooleanArray, transformToDate, transformToDateArray, transformToNumber, transformToNumberArray, transformToSort, transformToSortQueryParam, transformToString, transformToStringArray, uncacheableRequestHasAllowCacheParam, uncacheableRequestHasCacheKeyParam, v2BuildQueryCacheKey, v2ExtractExpiresInSeconds, v2ShouldCacheQuery, v2ShouldRetryRequest, validateWithQuery, validateWithV2Query, withArgs, withArgsQueryFeatureMissingButRouteIsFunction, withAuthenticationQuery, withAutoRefresh, withAutoRefreshUsedInManualQuery, withAutoRefreshUsedOnUnsupportedHttpMethod, withErrorHandling, withInactivityLogout, withLogging, withPersistentAuth, withPolling, withPollingUsedOnUnsupportedHttpMethod, withRefreshQuery, withResponseUpdate, withSuccessHandling, withTokenExpirationWarning, withTokenRevocation, withTracking, wrapAsObservableSignal };
|
|
8918
|
+
export { BasicAuthProvider, BearerAuthFeatureType, BooleanArrayQueryField, CLEAR_QUERY_ARGS, CustomHeaderAuthProvider, DEFAULT_KEEP_UNUSED_FOR, DateArrayQueryField, DateQueryField, EntityStore, IGNORED_FILTER_COUNT_FIELDS, INFINITY_QUERY_TOKEN, InfinityQuery, InfinityQueryDirective, InfinityQueryTriggerDirective, LegacyQuery, LegacyQueryCreator, MAX_UNUSED_ENTRIES, NumberArrayQueryField, ParseHttpErrorCodeToMessageDePipe, ParseHttpErrorCodeToMessageEnPipe, ParseHttpErrorCodeToTitleDePipe, ParseHttpErrorCodeToTitleEnPipe, QUERY_CLIENT_DEVTOOLS_TOKEN, QueryDevtoolsComponent, QueryDirective, QueryError, QueryFeatureType, QueryField, QueryForm, QueryFormSignals, QueryRuntimeErrorCode, QueryShortNamePipe, QueryStore, SERVER_ERROR_KIND, SERVER_VIOLATION_ERROR_KIND, SPEED_BUFFER_TIME_IN_MS, SearchQueryField, SortQueryField, StringArrayQueryField, V2BearerAuthProvider, V2Query, V2QueryClient, V2QueryCreator, addQueryContainerHandling, applyQueryFeatures, authExtractTokensResponseMissingAccessToken, authExtractTokensResponseMissingRefreshToken, authExtractTokensResponseNotObject, authProviderFeatureUsedMultipleTimes, booleanArrayQueryField, buildErrorMessage, buildQueryCacheKey, buildQueryString, buildRoute, buildTimestampFromSeconds, castQueryCreatorTypes, circularQueryDependency, circularQueryDependencyChecker, computeQueryAuthHeader, computeQueryBody, computeQueryHeaders, computeQueryMethod, computeQueryQueryParams, consumeSuppressQueryStackDevtools, contentfulGqlLikePaginationAdapter, createBaseQuery, createBaseQueryCreator, createBearerAuthProvider, createDeleteQuery, createExecuteFn, createGetQuery, createGqlExecuteFn, createGqlMutationViaGet, createGqlMutationViaPost, createGqlQuery, createGqlQueryCreator, createGqlQueryViaGet, createGqlQueryViaPost, createHttpRequest, createInfinityQueryConfig, createLegacyQueryCreator, createPagedQueryStack, createPatchQuery, createPersistentAuthFeature, createPostQuery, createPutQuery, createQuery, createQueryClient, createQueryCollection, createQueryCollectionSignal, createQueryCollectionSubject, createQueryContext, createQueryCreator, createQueryErrorResponse, createQueryFeature, createQueryForm, createQueryObject, createQueryRepository, createQuerySnapshotFn, createQueryStack, createSecureDeleteQuery, createSecureExecuteFactory, createSecureExecuteFn, createSecureGetQuery, createSecureGqlExecuteFn, createSecureGqlMutationViaGet, createSecureGqlMutationViaPost, createSecureGqlQuery, createSecureGqlQueryCreator, createSecureGqlQueryViaGet, createSecureGqlQueryViaPost, createSecurePatchQuery, createSecurePostQuery, createSecurePutQuery, createSecureQuery, createSecureQueryCreator, createTrackingFeature, createWebSocketClient, dateArrayQueryField, dateQueryField, decryptBearer, deepFreeze, def, detectContentTypeHeader, dynLikePaginationAdapter, effectComputed, emptyQueryArgs, ethletePaginationAdapter, executeUntilSettled, extractExpiresInSeconds, extractFormViolations, extractQuery, fakePaginationAdapter, filterFailure, filterNull, filterQueryStates, filterSuccess, forEachHeader, getDefaultHeaders, getQueryClientName, getQueryFeatureUsage, getResponseUrl, ggLikePaginationAdapter, gql, gqlDataPropertyMissingInResponse, hasHeader, ignoreAutoRefresh, injectInfinityQueryResponseDelay, injectQueryContext, insertFrom, invalidBaseRouteError, invalidBodyError, invalidRouteError, invalidStateInsideSecureExecuteFactory, isAutoRefresh, isBasicAuthProvider, isBearerAuthProvider, isClassValidatorError, isCreateGqlQueryOptions, isCustomHeaderAuthProvider, isEmptyString, isGqlQueryConfig, isLegacyQuery, isNaN$1 as isNaN, isPageOutOfRangeError, isQuery, isQueryCollection, isQueryDevtoolsEnabled, isQueryErrorResponse, isQueryStateCancelled, isQueryStateFailure, isQueryStateLoading, isQueryStatePrepared, isQueryStateSuccess, isRequestError, isSymfonyError, isSymfonyFormViolationListError, isSymfonyListError, isSymfonyPagerfantaOutOfRangeError, legacyPrepareWithoutInjectionContext, mapToPaginated, mapViolationsToFormErrors, maybeExecute, mergeHeaders, nestedEffect, numberArrayQueryField, pagedQueryStackNextPageCalledWithoutPreviousPage, pagedQueryStackPageBiggerThanTotalPages, pagedQueryStackPreviousPageCalledButAlreadyAtFirstPage, paginatedEntityValueUpdater, parseAllXhrResponseHeaders, parseHttpErrorCodeToMessageDe, parseHttpErrorCodeToMessageEn, parseHttpErrorCodeToTitleDe, parseHttpErrorCodeToTitleEn, pathParamsMissingInRouteFunctionError, provideInfinityQueryResponseDelay, provideQueryClientForDevtools, provideQueryDevtools, queryArrayComputed, queryComputed, queryComputedTillTruthy, queryDevtoolsEntries, queryErrorMessage, queryErrorMessages, queryExecute, queryFeatureUsedMultipleTimes, queryField, querySequence, querySequenceAlreadyRunning, queryStackTotalQueriesAndExpectedQueriesMismatch, queryStackWithArgsUsed, queryStackWithResponseUpdateUsed, queryStateAlreadyHasKeyError, queryStateCannotTransformError, queryStateDoesNotContainKeyError, queryStateErrorSignal, queryStateLoadingSignal, queryStateResponseSignal, queryStateSignal, registerQueryDevtoolsEntry, removeFrom, request, resetExecuteState, resetPageOnError, searchQueryField, serializeBody, setupQueryDependencies, setupQueryExecuteState, setupQueryState, shouldAutoExecuteGqlQuery, shouldAutoExecuteQuery, shouldCacheQuery, shouldRetryRequest, silenceMissingWithArgsFeatureErrorUsedButWithArgsPresent, skipPaginationPageParamCalculator, sortQueryField, splitQueryConfig, stringArrayQueryField, stringifyQueryRoute, suppressNextQueryStackDevtools, switchQueryCollectionState, switchQueryState, takeUntilResponse, toQuerySignal, toQuerySubject, tokensNotAvailableInsideAuthAndExec, transformArrayResponse, transformExecStateToQueryState, transformGql, transformMethod, transformPaginatedResponse, transformToBoolean, transformToBooleanArray, transformToDate, transformToDateArray, transformToNumber, transformToNumberArray, transformToSort, transformToSortQueryParam, transformToString, transformToStringArray, uncacheableRequestHasAllowCacheParam, uncacheableRequestHasCacheKeyParam, v2BuildQueryCacheKey, v2ExtractExpiresInSeconds, v2ShouldCacheQuery, v2ShouldRetryRequest, validateWithQuery, validateWithV2Query, withArgs, withArgsQueryFeatureMissingButRouteIsFunction, withAuthenticationQuery, withAutoRefresh, withAutoRefreshUsedInManualQuery, withAutoRefreshUsedOnUnsupportedHttpMethod, withErrorHandling, withInactivityLogout, withLogging, withPageResetOnError, withPersistentAuth, withPolling, withPollingUsedOnUnsupportedHttpMethod, withRefreshQuery, withResponseUpdate, withSuccessHandling, withTokenExpirationWarning, withTokenRevocation, withTracking, wrapAsObservableSignal };
|
|
8182
8919
|
//# sourceMappingURL=ethlete-query.mjs.map
|