@ethlete/query 6.0.0-next.51 → 6.0.0-next.52
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 +35 -0
- package/fesm2022/ethlete-query-ngrx-toolkit.mjs +321 -0
- package/fesm2022/ethlete-query-ngrx-toolkit.mjs.map +1 -0
- package/fesm2022/ethlete-query.mjs +340 -136
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/generators/generators.json +5 -0
- package/generators/migrate-from-ngrx-toolkit/args-assignability.js +140 -0
- package/generators/migrate-from-ngrx-toolkit/consumers.js +453 -0
- package/generators/migrate-from-ngrx-toolkit/creators.js +301 -0
- package/generators/migrate-from-ngrx-toolkit/facade.js +295 -0
- package/generators/migrate-from-ngrx-toolkit/feature.js +513 -0
- package/generators/migrate-from-ngrx-toolkit/migration.js +139 -0
- package/generators/migrate-from-ngrx-toolkit/report.js +154 -0
- package/generators/migrate-from-ngrx-toolkit/schema.json +42 -0
- package/generators/migrate-from-ngrx-toolkit/store-removal.js +633 -0
- package/generators/migrate-from-ngrx-toolkit/ts-edits.js +128 -0
- package/generators/migrate-query-opt-in-features/migration.js +10 -9
- package/generators/migrate-to-query-v3/auth-provider-scaffold.js +29 -2
- package/generators/migrate-to-query-v3/cleanup-migration.js +22 -1
- package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +39 -2
- package/generators/migrate-to-query-v3/migration.js +2 -1
- package/generators/migrate-to-query-v3/module-graph.js +2 -0
- package/generators/migrate-to-query-v3/query-client-migration.js +71 -2
- package/generators/migrate-to-query-v3/shared.js +27 -0
- package/generators/prep-for-query-v3/migration.js +83 -1
- package/migrations/query-field-default-types.md +43 -0
- package/migrations/search-query-field-string.md +43 -0
- package/migrations.json +16 -0
- package/package.json +5 -1
- package/types/ethlete-query-ngrx-toolkit.d.ts +143 -0
- package/types/ethlete-query.d.ts +129 -34
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# `queryField<T>()` is typed by its default and reads the URL into that type
|
|
2
|
+
|
|
3
|
+
`queryField<T>()` used to be typed `T | null` whatever its default, and guessed the type of a URL
|
|
4
|
+
value: `?id=123` became the number `123` even in a `queryField<string>()`. Now:
|
|
5
|
+
|
|
6
|
+
- A field with a non-null `defaultValue` is typed `T`, and a reset or an unreadable URL value falls
|
|
7
|
+
back to the default.
|
|
8
|
+
- A URL value is read into the type of the default (number, boolean, string, string list). Without a
|
|
9
|
+
default it stays a string.
|
|
10
|
+
- Any other `T` needs a `queryParamToValue`: `queryField<number>()` without a default, a `Date` or an
|
|
11
|
+
object default.
|
|
12
|
+
- `searchQueryField()` and `sortQueryField()` no longer count in `activeFilterCount`, whatever their key.
|
|
13
|
+
|
|
14
|
+
The type check finds every call site that has to change.
|
|
15
|
+
|
|
16
|
+
## Find the call sites
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
grep -rnE '\bqueryField<' apps libs --include='*.ts'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## What to change
|
|
23
|
+
|
|
24
|
+
1. `queryField<number>()` or `queryField<boolean>()` without a default: give it the `defaultValue` it
|
|
25
|
+
effectively had, or add `queryParamToValue: transformToNumber` (`transformToBoolean`, …) and keep
|
|
26
|
+
the `null` start. Both transforms are exported from `@ethlete/query`.
|
|
27
|
+
2. A `Date` or object default without a `queryParamToValue`: add one (`transformToDate` for a date).
|
|
28
|
+
3. A field that must hold `null` beside a non-null default - an explicit "none" - is declared
|
|
29
|
+
`queryField<T | null>({ defaultValue: … })`.
|
|
30
|
+
4. Delete the `?? DEFAULT` on reads of a field that now has a non-null type, and any `!` or cast that
|
|
31
|
+
only existed to strip `null`.
|
|
32
|
+
5. A field bound to a native `<input type="number">` is declared `number | null`: the input writes
|
|
33
|
+
`null` when cleared.
|
|
34
|
+
6. A field that relied on the old guess - a `queryField<string>()` read as a number somewhere - gets
|
|
35
|
+
the type it was really used as.
|
|
36
|
+
7. A UI that counted a search or sort field as a filter: pass `skipInFilterCount: false` to keep it.
|
|
37
|
+
|
|
38
|
+
## When you are done
|
|
39
|
+
|
|
40
|
+
Run the type check, the lint task and the tests of every project you changed, and check in the
|
|
41
|
+
browser that a reload restores each list.
|
|
42
|
+
|
|
43
|
+
The query forms guide is at <https://ethlete-sdk-docs.web.app/query/query-forms>.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# `searchQueryField()` is a `string` that starts at `''`
|
|
2
|
+
|
|
3
|
+
`searchQueryField()` used to be typed `string | null` and start at `null`. It is typed `string` now and
|
|
4
|
+
starts at `''`, so `[formField]` binds it to `<et-input>` under `strictTemplates`. An empty search still
|
|
5
|
+
writes no URL param, so URLs do not change. The type check finds most call sites; a comparison with
|
|
6
|
+
`null` compiles and silently stops matching.
|
|
7
|
+
|
|
8
|
+
## Find the call sites
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
grep -rnE 'searchQueryField\(' apps libs --include='*.ts' -l
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
In each file found, and in each file that reads the form's value, look for the search field's key
|
|
15
|
+
(`search`, `query`, `q`, …) next to `null`:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
grep -rnE '(search|query|q)\b[^;]*(null|\?\?)' apps libs --include='*.ts'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## What to change
|
|
22
|
+
|
|
23
|
+
1. A write of `null` - `setValue({ search: null, … })`, `patchValue({ search: null })`, a seed object,
|
|
24
|
+
a test fixture - writes `''` instead.
|
|
25
|
+
2. A check for an empty search - `search === null`, `search !== null`, `search == null`,
|
|
26
|
+
`search ?? ''` - becomes `search === ''`, `search !== ''`, or just `search`. `?? ''` is dead code
|
|
27
|
+
now; delete it.
|
|
28
|
+
3. A search that goes into a request body or GraphQL variables passes `search || null` where the API
|
|
29
|
+
expects `null` for "no search". Query params need nothing: the client drops an empty string.
|
|
30
|
+
4. A type that restates the value, such as `search: string | null` in an interface built from the
|
|
31
|
+
form, becomes `string`. Prefer deriving it from `ReturnType<typeof qf.value>`.
|
|
32
|
+
5. A test that expects `null` for an empty search expects `''`.
|
|
33
|
+
|
|
34
|
+
## Leave these alone
|
|
35
|
+
|
|
36
|
+
- `queryField<string>()` fields. They still start at `null`.
|
|
37
|
+
- The legacy `SearchQueryField` class. It is unchanged.
|
|
38
|
+
|
|
39
|
+
## When you are done
|
|
40
|
+
|
|
41
|
+
Run the type check, the lint task and the tests of every project you changed.
|
|
42
|
+
|
|
43
|
+
The query forms guide is at <https://ethlete-sdk-docs.web.app/query/query-forms>.
|
package/migrations.json
CHANGED
|
@@ -39,6 +39,22 @@
|
|
|
39
39
|
"description": "Mark the remaining createLegacyQueryCreator wrappers as deprecated, so their call sites surface as still on v2.",
|
|
40
40
|
"generator": "@ethlete/query:deprecate-legacy-queries",
|
|
41
41
|
"docs": "/query/legacy"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"name": "search-query-field-string",
|
|
45
|
+
"version": "6.0.0-next.52",
|
|
46
|
+
"kind": "assisted",
|
|
47
|
+
"description": "searchQueryField() is typed string and starts at '' instead of null; writes and checks of null move to ''.",
|
|
48
|
+
"instructions": "./migrations/search-query-field-string.md",
|
|
49
|
+
"docs": "/query/query-forms"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "query-field-default-types",
|
|
53
|
+
"version": "6.0.0-next.52",
|
|
54
|
+
"kind": "assisted",
|
|
55
|
+
"description": "queryField() is typed by its default and reads the URL into that type; fields without a default or with a non-primitive one need a queryParamToValue.",
|
|
56
|
+
"instructions": "./migrations/query-field-default-types.md",
|
|
57
|
+
"docs": "/query/query-forms"
|
|
42
58
|
}
|
|
43
59
|
]
|
|
44
60
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ethlete/query",
|
|
3
|
-
"version": "6.0.0-next.
|
|
3
|
+
"version": "6.0.0-next.52",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"peerDependencies": {
|
|
@@ -44,6 +44,10 @@
|
|
|
44
44
|
"types": "./types/ethlete-query.d.ts",
|
|
45
45
|
"default": "./fesm2022/ethlete-query.mjs"
|
|
46
46
|
},
|
|
47
|
+
"./ngrx-toolkit": {
|
|
48
|
+
"types": "./types/ethlete-query-ngrx-toolkit.d.ts",
|
|
49
|
+
"default": "./fesm2022/ethlete-query-ngrx-toolkit.mjs"
|
|
50
|
+
},
|
|
47
51
|
"./testing": {
|
|
48
52
|
"types": "./types/ethlete-query-testing.d.ts",
|
|
49
53
|
"default": "./fesm2022/ethlete-query-testing.mjs"
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { PipeTransform, Injector } from '@angular/core';
|
|
3
|
+
import { Observable, Subject } from 'rxjs';
|
|
4
|
+
import { AnyQueryCreator, QueryArgsOf, QueryHeaders, ResponseType } from '@ethlete/query';
|
|
5
|
+
|
|
6
|
+
/** The call state of a toolkit handle, as `@tomtomb/ngrx-toolkit` names it. */
|
|
7
|
+
declare const CallState: {
|
|
8
|
+
readonly INIT: "INIT";
|
|
9
|
+
readonly LOADING: "LOADING";
|
|
10
|
+
readonly SUCCESS: "SUCCESS";
|
|
11
|
+
readonly ERROR: "ERROR";
|
|
12
|
+
};
|
|
13
|
+
type CallState = (typeof CallState)[keyof typeof CallState];
|
|
14
|
+
/** The error shape of `@tomtomb/ngrx-toolkit`: the HTTP status, the error message and the response body. */
|
|
15
|
+
type ToolkitError<T = unknown> = {
|
|
16
|
+
status: number | string;
|
|
17
|
+
message: string;
|
|
18
|
+
data: T | null;
|
|
19
|
+
};
|
|
20
|
+
type ToolkitActionOptions = {
|
|
21
|
+
headers?: QueryHeaders;
|
|
22
|
+
extras?: {
|
|
23
|
+
skipCache?: boolean;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The toolkit-shaped args of a query creator: `queryParams` holds the path params, `params` the query string and
|
|
28
|
+
* `body` the body, the way `@tomtomb/ngrx-toolkit` action groups named them.
|
|
29
|
+
*/
|
|
30
|
+
type ActionCallArgs<TCreator extends AnyQueryCreator> = {
|
|
31
|
+
[K in keyof QueryArgsOf<TCreator> as K extends 'pathParams' ? 'queryParams' : K extends 'queryParams' ? 'params' : K extends 'body' ? 'body' : never]: QueryArgsOf<TCreator>[K];
|
|
32
|
+
} & {
|
|
33
|
+
actionOptions?: ToolkitActionOptions;
|
|
34
|
+
};
|
|
35
|
+
type ToolkitPollingOptions = {
|
|
36
|
+
intervalDuration: number;
|
|
37
|
+
killSwitch: Subject<boolean>;
|
|
38
|
+
};
|
|
39
|
+
/** The handle `toolkitCall` returns, shaped like the `MappedEntityState` of `@tomtomb/ngrx-toolkit`. */
|
|
40
|
+
type MappedEntityState<TCreator extends AnyQueryCreator> = {
|
|
41
|
+
response$: Observable<ResponseType<QueryArgsOf<TCreator>> | null>;
|
|
42
|
+
cachedResponse$: Observable<ResponseType<QueryArgsOf<TCreator>>>;
|
|
43
|
+
error$: Observable<ToolkitError | null>;
|
|
44
|
+
args$: Observable<ActionCallArgs<TCreator> | null>;
|
|
45
|
+
isInit$: Observable<boolean | null>;
|
|
46
|
+
isLoading$: Observable<boolean | null>;
|
|
47
|
+
isSuccess$: Observable<boolean | null>;
|
|
48
|
+
isError$: Observable<boolean | null>;
|
|
49
|
+
timestamp$: Observable<number | null>;
|
|
50
|
+
callState$: Observable<CallState | null>;
|
|
51
|
+
refresh: () => void;
|
|
52
|
+
remove: () => void;
|
|
53
|
+
startPolling: (options: ToolkitPollingOptions) => void;
|
|
54
|
+
stopPolling: () => void;
|
|
55
|
+
};
|
|
56
|
+
type AnyMappedEntityState = MappedEntityState<any>;
|
|
57
|
+
|
|
58
|
+
type ExtractFrom<T> = T extends Observable<infer U> ? U : never;
|
|
59
|
+
/** What `suspense` and `suspenseMulti` render for a handle: every stream's current value, plus the handle's methods. */
|
|
60
|
+
type ToolkitSuspenseState<T extends AnyMappedEntityState | null | undefined> = T extends AnyMappedEntityState ? {
|
|
61
|
+
args: ExtractFrom<T['args$']>;
|
|
62
|
+
cachedResponse: ExtractFrom<T['cachedResponse$']> | null;
|
|
63
|
+
callState: ExtractFrom<T['callState$']>;
|
|
64
|
+
error: ExtractFrom<T['error$']>;
|
|
65
|
+
isError: ExtractFrom<T['isError$']>;
|
|
66
|
+
isInit: ExtractFrom<T['isInit$']>;
|
|
67
|
+
isLoading: ExtractFrom<T['isLoading$']>;
|
|
68
|
+
isSuccess: ExtractFrom<T['isSuccess$']>;
|
|
69
|
+
response: ExtractFrom<T['response$']>;
|
|
70
|
+
timestamp: ExtractFrom<T['timestamp$']>;
|
|
71
|
+
refresh: () => void;
|
|
72
|
+
remove: () => void;
|
|
73
|
+
startPolling: (options: ToolkitPollingOptions) => void;
|
|
74
|
+
stopPolling: () => void;
|
|
75
|
+
} : T;
|
|
76
|
+
/** Subscribes to a toolkit handle and renders its current state, like the `suspense` pipe of `@tomtomb/ngrx-toolkit`. */
|
|
77
|
+
declare class SuspensePipe implements PipeTransform {
|
|
78
|
+
transform: <T extends AnyMappedEntityState>(value: T | null | undefined) => ToolkitSuspenseState<T>;
|
|
79
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<SuspensePipe, never>;
|
|
80
|
+
static ɵpipe: i0.ɵɵPipeDeclaration<SuspensePipe, "suspense", true>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
type AnyHandleRecord = Record<string, AnyMappedEntityState | null | undefined>;
|
|
84
|
+
type ToolkitSuspenseMultiState<T extends AnyHandleRecord> = {
|
|
85
|
+
[K in keyof T]: ToolkitSuspenseState<T[K]>;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Subscribes to a record of toolkit handles and renders each one's current state, like the `suspenseMulti` pipe of
|
|
89
|
+
* `@tomtomb/ngrx-toolkit`. Nothing renders until every non-null handle has emitted.
|
|
90
|
+
*/
|
|
91
|
+
declare class SuspenseMultiPipe implements PipeTransform {
|
|
92
|
+
transform: <T extends AnyHandleRecord>(value: T | null | undefined) => ToolkitSuspenseMultiState<T>;
|
|
93
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<SuspenseMultiPipe, never>;
|
|
94
|
+
static ɵpipe: i0.ɵɵPipeDeclaration<SuspenseMultiPipe, "suspenseMulti", true>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Exports the `suspense` and `suspenseMulti` pipes, like the `NgRxToolkitModule` of `@tomtomb/ngrx-toolkit`. */
|
|
98
|
+
declare class NgRxToolkitModule {
|
|
99
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<NgRxToolkitModule, never>;
|
|
100
|
+
static ɵmod: i0.ɵɵNgModuleDeclaration<NgRxToolkitModule, never, [typeof SuspensePipe, typeof SuspenseMultiPipe], [typeof SuspensePipe, typeof SuspenseMultiPipe]>;
|
|
101
|
+
static ɵinj: i0.ɵɵInjectorDeclaration<NgRxToolkitModule>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type ToolkitCallOptions = {
|
|
105
|
+
/**
|
|
106
|
+
* The injector the handle's query lives in. Required outside an injection context, which is where a
|
|
107
|
+
* `providedIn: 'root'` facade calls from; pass the facade's `inject(Injector)`.
|
|
108
|
+
*/
|
|
109
|
+
injector?: Injector;
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Returns the handle for a creator and args without executing it - the `FacadeBase.select(group, actionId)` of
|
|
113
|
+
* `@tomtomb/ngrx-toolkit`. Args that build the same request (in any key order, other keys ignored) give the same
|
|
114
|
+
* handle `toolkitCall` returns, so `toolkitSelect(getTeam, args, { injector }).refresh()` re-runs a request another
|
|
115
|
+
* component started.
|
|
116
|
+
*/
|
|
117
|
+
declare const toolkitSelect: <TCreator extends AnyQueryCreator>(creator: TCreator, args: ActionCallArgs<TCreator>, options?: ToolkitCallOptions) => MappedEntityState<TCreator>;
|
|
118
|
+
/**
|
|
119
|
+
* Executes a query creator with toolkit-shaped args and returns its handle - the `FacadeBase.call(group, args)` of
|
|
120
|
+
* `@tomtomb/ngrx-toolkit`. Every call sends the request again; equal args return the same handle, which lives as
|
|
121
|
+
* long as the injector.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* @Injectable({ providedIn: 'root' })
|
|
126
|
+
* export class TeamFacade {
|
|
127
|
+
* private injector = inject(Injector);
|
|
128
|
+
*
|
|
129
|
+
* getTeam(args: ActionCallArgs<typeof getTeam>) {
|
|
130
|
+
* return toolkitCall(getTeam, args, { injector: this.injector });
|
|
131
|
+
* }
|
|
132
|
+
* }
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
declare const toolkitCall: <TCreator extends AnyQueryCreator>(creator: TCreator, args: ActionCallArgs<TCreator>, options?: ToolkitCallOptions) => MappedEntityState<TCreator>;
|
|
136
|
+
|
|
137
|
+
/** Emits `true` while any of the given loading streams is `true`. Nullish entries are skipped. */
|
|
138
|
+
declare const joinLoading: (observables: (Observable<boolean | null> | null | undefined)[]) => Observable<boolean>;
|
|
139
|
+
/** Emits every non-null error of the given error streams, or `null` when there is none. Nullish entries are skipped. */
|
|
140
|
+
declare const joinErrors: (observables: (Observable<ToolkitError | null> | null | undefined)[]) => Observable<ToolkitError[] | null>;
|
|
141
|
+
|
|
142
|
+
export { CallState, NgRxToolkitModule, SuspenseMultiPipe, SuspensePipe, joinErrors, joinLoading, toolkitCall, toolkitSelect };
|
|
143
|
+
export type { ActionCallArgs, AnyMappedEntityState, MappedEntityState, ToolkitActionOptions, ToolkitCallOptions, ToolkitError, ToolkitPollingOptions, ToolkitSuspenseMultiState, ToolkitSuspenseState };
|