@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.
Files changed (31) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/fesm2022/ethlete-query-ngrx-toolkit.mjs +321 -0
  3. package/fesm2022/ethlete-query-ngrx-toolkit.mjs.map +1 -0
  4. package/fesm2022/ethlete-query.mjs +340 -136
  5. package/fesm2022/ethlete-query.mjs.map +1 -1
  6. package/generators/generators.json +5 -0
  7. package/generators/migrate-from-ngrx-toolkit/args-assignability.js +140 -0
  8. package/generators/migrate-from-ngrx-toolkit/consumers.js +453 -0
  9. package/generators/migrate-from-ngrx-toolkit/creators.js +301 -0
  10. package/generators/migrate-from-ngrx-toolkit/facade.js +295 -0
  11. package/generators/migrate-from-ngrx-toolkit/feature.js +513 -0
  12. package/generators/migrate-from-ngrx-toolkit/migration.js +139 -0
  13. package/generators/migrate-from-ngrx-toolkit/report.js +154 -0
  14. package/generators/migrate-from-ngrx-toolkit/schema.json +42 -0
  15. package/generators/migrate-from-ngrx-toolkit/store-removal.js +633 -0
  16. package/generators/migrate-from-ngrx-toolkit/ts-edits.js +128 -0
  17. package/generators/migrate-query-opt-in-features/migration.js +10 -9
  18. package/generators/migrate-to-query-v3/auth-provider-scaffold.js +29 -2
  19. package/generators/migrate-to-query-v3/cleanup-migration.js +22 -1
  20. package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +39 -2
  21. package/generators/migrate-to-query-v3/migration.js +2 -1
  22. package/generators/migrate-to-query-v3/module-graph.js +2 -0
  23. package/generators/migrate-to-query-v3/query-client-migration.js +71 -2
  24. package/generators/migrate-to-query-v3/shared.js +27 -0
  25. package/generators/prep-for-query-v3/migration.js +83 -1
  26. package/migrations/query-field-default-types.md +43 -0
  27. package/migrations/search-query-field-string.md +43 -0
  28. package/migrations.json +16 -0
  29. package/package.json +5 -1
  30. package/types/ethlete-query-ngrx-toolkit.d.ts +143 -0
  31. 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.51",
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 };