@haddowg/json-api-angular 1.0.0 → 1.1.1
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/dist/index.cjs +15 -10
- package/dist/index.d.cts +33 -26
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +33 -26
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +15 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -166,9 +166,9 @@ function jsonApiMutation(mutation, options = {}) {
|
|
|
166
166
|
//#region src/pagination.ts
|
|
167
167
|
/**
|
|
168
168
|
* The `page` params for the page after `list`, read from its `next` link, or `undefined` on the
|
|
169
|
-
* last page (or before anything has loaded). Works the same for
|
|
170
|
-
*
|
|
171
|
-
*
|
|
169
|
+
* last page (or before anything has loaded). Works the same for every strategy, because the
|
|
170
|
+
* server's link already carries the right parameters. Feed the result back into the signal your
|
|
171
|
+
* resource's params read:
|
|
172
172
|
*
|
|
173
173
|
* ```ts
|
|
174
174
|
* const next = nextPage(this.albums.value())
|
|
@@ -176,19 +176,24 @@ function jsonApiMutation(mutation, options = {}) {
|
|
|
176
176
|
* ```
|
|
177
177
|
*/
|
|
178
178
|
function nextPage(list) {
|
|
179
|
-
return pageParams(list?.$page.links.next);
|
|
179
|
+
return pageParams(list?.$page, list?.$page.links.next);
|
|
180
180
|
}
|
|
181
181
|
/** The `page` params for the page before `list`, from its `prev` link. See {@link nextPage}. */
|
|
182
182
|
function prevPage(list) {
|
|
183
|
-
return pageParams(list?.$page.links.prev);
|
|
183
|
+
return pageParams(list?.$page, list?.$page.links.prev);
|
|
184
184
|
}
|
|
185
|
-
|
|
186
|
-
|
|
185
|
+
const CURSOR_ROLES = ["after", "before"];
|
|
186
|
+
function pageParams(page, link) {
|
|
187
|
+
if (page === void 0 || link === void 0) return;
|
|
188
|
+
const keys = Object.entries(page.keys);
|
|
187
189
|
const query = link.includes("?") ? link.slice(link.indexOf("?") + 1).split("#")[0] : "";
|
|
188
190
|
const params = {};
|
|
189
|
-
for (const [
|
|
190
|
-
const
|
|
191
|
-
if (
|
|
191
|
+
for (const [name, value] of new URLSearchParams(query)) {
|
|
192
|
+
const member = /^page\[([^\]]+)\]$/.exec(name)?.[1];
|
|
193
|
+
if (member === void 0) continue;
|
|
194
|
+
const role = keys.find(([, key]) => key === member)?.[0];
|
|
195
|
+
const count = Number(value);
|
|
196
|
+
params[member] = role !== void 0 && !CURSOR_ROLES.includes(role) && value !== "" && Number.isFinite(count) ? count : value;
|
|
192
197
|
}
|
|
193
198
|
return params;
|
|
194
199
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EnvironmentProviders, InjectionToken, Injector, ResourceRef, ResourceStatus, Signal, ValueEqualityFn } from "@angular/core";
|
|
2
|
-
import { ClientOptions, Collection, JsonApiError, JsonApiTransport, Page, StructuralGuardError } from "@haddowg/json-api-client";
|
|
2
|
+
import { ClientOptions, Collection, ErrorCodeOf, JsonApiError, JsonApiTransport, Page, PageParamsOf, StructuralGuardError } from "@haddowg/json-api-client";
|
|
3
3
|
import { HttpClient } from "@angular/common/http";
|
|
4
4
|
|
|
5
5
|
//#region src/provide.d.ts
|
|
@@ -59,9 +59,9 @@ declare function injectJsonApiClient<C = RegisteredClient>(token?: InjectionToke
|
|
|
59
59
|
/**
|
|
60
60
|
* What a read can fail with: a `JsonApiError` for a non-2xx response, a `StructuralGuardError`
|
|
61
61
|
* for a response that isn't a JSON:API document, or any other error (a network failure, a
|
|
62
|
-
* validator's error). Narrow with `instanceof`.
|
|
62
|
+
* validator's error). Narrow with `instanceof`. `E` types the `JsonApiError`'s codes.
|
|
63
63
|
*/
|
|
64
|
-
type JsonApiResourceError = JsonApiError | StructuralGuardError | Error;
|
|
64
|
+
type JsonApiResourceError<E extends string = string> = JsonApiError<E> | StructuralGuardError | Error;
|
|
65
65
|
/** What the loader receives each time the params change or the resource reloads. */
|
|
66
66
|
interface JsonApiLoaderContext<C, P> {
|
|
67
67
|
client: C;
|
|
@@ -93,13 +93,14 @@ interface JsonApiResourceOptions<T, P, C> {
|
|
|
93
93
|
/**
|
|
94
94
|
* Angular's `ResourceRef` with `error()` typed to what a JSON:API read throws, plus
|
|
95
95
|
* `jsonApiError()`, the error when it is a `JsonApiError` (handy in templates, which can't
|
|
96
|
-
* use `instanceof`).
|
|
96
|
+
* use `instanceof`). `E` is the client's error-code union, so a generated client's
|
|
97
|
+
* `jsonApiError()` carries its server's `ErrorCode`.
|
|
97
98
|
*/
|
|
98
|
-
interface JsonApiResourceRef<T> extends Omit<ResourceRef<T>, 'error' | 'hasValue'> {
|
|
99
|
-
readonly error: Signal<JsonApiResourceError | undefined>;
|
|
100
|
-
readonly jsonApiError: Signal<JsonApiError | undefined>;
|
|
99
|
+
interface JsonApiResourceRef<T, E extends string = string> extends Omit<ResourceRef<T>, 'error' | 'hasValue'> {
|
|
100
|
+
readonly error: Signal<JsonApiResourceError<E> | undefined>;
|
|
101
|
+
readonly jsonApiError: Signal<JsonApiError<E> | undefined>;
|
|
101
102
|
/** Whether a value is loaded. Narrows `value()` to exclude `undefined`, in templates too. */
|
|
102
|
-
hasValue(this: T extends undefined ? this : never): this is JsonApiResourceRef<Exclude<T, undefined
|
|
103
|
+
hasValue(this: T extends undefined ? this : never): this is JsonApiResourceRef<Exclude<T, undefined>, E>;
|
|
103
104
|
hasValue(): boolean;
|
|
104
105
|
}
|
|
105
106
|
/**
|
|
@@ -117,8 +118,8 @@ interface JsonApiResourceRef<T> extends Omit<ResourceRef<T>, 'error' | 'hasValue
|
|
|
117
118
|
*/
|
|
118
119
|
declare function jsonApiResource<T, P = unknown, C = RegisteredClient>(options: JsonApiResourceOptions<T, P, C> & {
|
|
119
120
|
defaultValue: T;
|
|
120
|
-
}): JsonApiResourceRef<T
|
|
121
|
-
declare function jsonApiResource<T, P = unknown, C = RegisteredClient>(options: JsonApiResourceOptions<T, P, C>): JsonApiResourceRef<T | undefined
|
|
121
|
+
}): JsonApiResourceRef<T, ErrorCodeOf<C>>;
|
|
122
|
+
declare function jsonApiResource<T, P = unknown, C = RegisteredClient>(options: JsonApiResourceOptions<T, P, C>): JsonApiResourceRef<T | undefined, ErrorCodeOf<C>>;
|
|
122
123
|
//#endregion
|
|
123
124
|
//#region src/mutation.d.ts
|
|
124
125
|
/** Options for {@link jsonApiMutation}. */
|
|
@@ -128,17 +129,20 @@ interface JsonApiMutationOptions<C> {
|
|
|
128
129
|
/** Create the mutation outside an injection context. */
|
|
129
130
|
injector?: Injector;
|
|
130
131
|
}
|
|
131
|
-
/**
|
|
132
|
-
|
|
132
|
+
/**
|
|
133
|
+
* The signal-based handle {@link jsonApiMutation} returns. `E` is the client's error-code union,
|
|
134
|
+
* so a generated client's `jsonApiError()` carries its server's `ErrorCode`.
|
|
135
|
+
*/
|
|
136
|
+
interface JsonApiMutationRef<I, T, E extends string = string> {
|
|
133
137
|
/**
|
|
134
138
|
* Run the write. Resolves with its result and rejects with its error; the signals track the
|
|
135
139
|
* latest call either way.
|
|
136
140
|
*/
|
|
137
141
|
mutate(input: I): Promise<T>;
|
|
138
142
|
readonly isPending: Signal<boolean>;
|
|
139
|
-
readonly error: Signal<JsonApiResourceError | undefined>;
|
|
143
|
+
readonly error: Signal<JsonApiResourceError<E> | undefined>;
|
|
140
144
|
/** The error when it is a `JsonApiError`; use `byPath()` on it for form errors. */
|
|
141
|
-
readonly jsonApiError: Signal<JsonApiError | undefined>;
|
|
145
|
+
readonly jsonApiError: Signal<JsonApiError<E> | undefined>;
|
|
142
146
|
/** The latest successful result. */
|
|
143
147
|
readonly value: Signal<T | undefined>;
|
|
144
148
|
/** Clear the value and error. A call still in flight no longer updates the signals. */
|
|
@@ -155,29 +159,32 @@ interface JsonApiMutationRef<I, T> {
|
|
|
155
159
|
* )
|
|
156
160
|
* ```
|
|
157
161
|
*/
|
|
158
|
-
declare function jsonApiMutation<I, T, C = RegisteredClient>(mutation: (client: C, input: I) => PromiseLike<T>, options?: JsonApiMutationOptions<C>): JsonApiMutationRef<I, T
|
|
162
|
+
declare function jsonApiMutation<I, T, C = RegisteredClient>(mutation: (client: C, input: I) => PromiseLike<T>, options?: JsonApiMutationOptions<C>): JsonApiMutationRef<I, T, ErrorCodeOf<C>>;
|
|
159
163
|
//#endregion
|
|
160
164
|
//#region src/pagination.d.ts
|
|
161
|
-
/**
|
|
162
|
-
|
|
165
|
+
/**
|
|
166
|
+
* The `page` query a link selects, typed by the `$page` it came from: the strategy's members under
|
|
167
|
+
* their wire keys (counts as numbers, cursors as strings), plus the menu arm the read picked.
|
|
168
|
+
*/
|
|
169
|
+
type PageParams<P extends Page = Page> = PageParamsOf<P>;
|
|
163
170
|
/**
|
|
164
171
|
* The `page` params for the page after `list`, read from its `next` link, or `undefined` on the
|
|
165
|
-
* last page (or before anything has loaded). Works the same for
|
|
166
|
-
*
|
|
167
|
-
*
|
|
172
|
+
* last page (or before anything has loaded). Works the same for every strategy, because the
|
|
173
|
+
* server's link already carries the right parameters. Feed the result back into the signal your
|
|
174
|
+
* resource's params read:
|
|
168
175
|
*
|
|
169
176
|
* ```ts
|
|
170
177
|
* const next = nextPage(this.albums.value())
|
|
171
178
|
* if (next) this.page.set(next)
|
|
172
179
|
* ```
|
|
173
180
|
*/
|
|
174
|
-
declare function nextPage(list: {
|
|
175
|
-
readonly $page:
|
|
176
|
-
} | undefined): PageParams | undefined;
|
|
181
|
+
declare function nextPage<P extends Page>(list: {
|
|
182
|
+
readonly $page: P;
|
|
183
|
+
} | undefined): PageParams<P> | undefined;
|
|
177
184
|
/** The `page` params for the page before `list`, from its `prev` link. See {@link nextPage}. */
|
|
178
|
-
declare function prevPage(list: {
|
|
179
|
-
readonly $page:
|
|
180
|
-
} | undefined): PageParams | undefined;
|
|
185
|
+
declare function prevPage<P extends Page>(list: {
|
|
186
|
+
readonly $page: P;
|
|
187
|
+
} | undefined): PageParams<P> | undefined;
|
|
181
188
|
/** Options for {@link loadAll}. */
|
|
182
189
|
interface LoadAllOptions {
|
|
183
190
|
/** Stops the walk, rejecting with the signal's reason. Pass the loader's `abortSignal`. */
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/provide.ts","../src/resource.ts","../src/mutation.ts","../src/pagination.ts","../src/transport.ts"],"mappings":";;;;;;;;AAgBA;KAAY,oBAAA,GAAuB,IAAA,CAAK,aAAA;EACtC,SAAA,GAAY,gBAAA;AAAA;;KAIF,oBAAA,OAA2B,OAAA,EAAS,aAAA,KAAkB,CAAC;;KAGvD,eAAA,MAAqB,CAAA,WAAW,OAAA,EAAS,aAAa,gBAAe,CAAA;;;;;;AAPnD;AAI9B;;;;;;UAkBiB,QAAA;;KAGL,gBAAA,GAAmB,QAAQ;EAAW,MAAA;AAAA,IAAoB,CAAA;;cAGzD,eAAA,EAAe,cAAA;;;;;iBAMZ,wBAAA,IAA4B,WAAA,YAAgC,cAAc,CAAC,CAAA;;;AA3BT;AAelF;;;;AAAyB;iBAwBT,oBAAA,IACd,YAAA,EAAc,oBAAA,CAAqB,CAAA,GACnC,OAAA,EAAS,oBAAA,UAA8B,oBAAA,GACvC,KAAA,GAAO,cAAA,CAAe,CAAA,IACrB,oBAAA;;;;;iBAmBa,mBAAA,KAAwB,gBAAA,EACtC,KAAA,GAAQ,cAAA,CAAe,CAAA,GACvB,OAAA;EAAY,QAAA,GAAW,QAAA;AAAA,IACtB,CAAA;;;;;AAzEH;;;KCMY,oBAAA,
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/provide.ts","../src/resource.ts","../src/mutation.ts","../src/pagination.ts","../src/transport.ts"],"mappings":"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"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EnvironmentProviders, InjectionToken, Injector, ResourceRef, ResourceStatus, Signal, ValueEqualityFn } from "@angular/core";
|
|
2
|
-
import { ClientOptions, Collection, JsonApiError, JsonApiTransport, Page, StructuralGuardError } from "@haddowg/json-api-client";
|
|
2
|
+
import { ClientOptions, Collection, ErrorCodeOf, JsonApiError, JsonApiTransport, Page, PageParamsOf, StructuralGuardError } from "@haddowg/json-api-client";
|
|
3
3
|
import { HttpClient } from "@angular/common/http";
|
|
4
4
|
|
|
5
5
|
//#region src/provide.d.ts
|
|
@@ -59,9 +59,9 @@ declare function injectJsonApiClient<C = RegisteredClient>(token?: InjectionToke
|
|
|
59
59
|
/**
|
|
60
60
|
* What a read can fail with: a `JsonApiError` for a non-2xx response, a `StructuralGuardError`
|
|
61
61
|
* for a response that isn't a JSON:API document, or any other error (a network failure, a
|
|
62
|
-
* validator's error). Narrow with `instanceof`.
|
|
62
|
+
* validator's error). Narrow with `instanceof`. `E` types the `JsonApiError`'s codes.
|
|
63
63
|
*/
|
|
64
|
-
type JsonApiResourceError = JsonApiError | StructuralGuardError | Error;
|
|
64
|
+
type JsonApiResourceError<E extends string = string> = JsonApiError<E> | StructuralGuardError | Error;
|
|
65
65
|
/** What the loader receives each time the params change or the resource reloads. */
|
|
66
66
|
interface JsonApiLoaderContext<C, P> {
|
|
67
67
|
client: C;
|
|
@@ -93,13 +93,14 @@ interface JsonApiResourceOptions<T, P, C> {
|
|
|
93
93
|
/**
|
|
94
94
|
* Angular's `ResourceRef` with `error()` typed to what a JSON:API read throws, plus
|
|
95
95
|
* `jsonApiError()`, the error when it is a `JsonApiError` (handy in templates, which can't
|
|
96
|
-
* use `instanceof`).
|
|
96
|
+
* use `instanceof`). `E` is the client's error-code union, so a generated client's
|
|
97
|
+
* `jsonApiError()` carries its server's `ErrorCode`.
|
|
97
98
|
*/
|
|
98
|
-
interface JsonApiResourceRef<T> extends Omit<ResourceRef<T>, 'error' | 'hasValue'> {
|
|
99
|
-
readonly error: Signal<JsonApiResourceError | undefined>;
|
|
100
|
-
readonly jsonApiError: Signal<JsonApiError | undefined>;
|
|
99
|
+
interface JsonApiResourceRef<T, E extends string = string> extends Omit<ResourceRef<T>, 'error' | 'hasValue'> {
|
|
100
|
+
readonly error: Signal<JsonApiResourceError<E> | undefined>;
|
|
101
|
+
readonly jsonApiError: Signal<JsonApiError<E> | undefined>;
|
|
101
102
|
/** Whether a value is loaded. Narrows `value()` to exclude `undefined`, in templates too. */
|
|
102
|
-
hasValue(this: T extends undefined ? this : never): this is JsonApiResourceRef<Exclude<T, undefined
|
|
103
|
+
hasValue(this: T extends undefined ? this : never): this is JsonApiResourceRef<Exclude<T, undefined>, E>;
|
|
103
104
|
hasValue(): boolean;
|
|
104
105
|
}
|
|
105
106
|
/**
|
|
@@ -117,8 +118,8 @@ interface JsonApiResourceRef<T> extends Omit<ResourceRef<T>, 'error' | 'hasValue
|
|
|
117
118
|
*/
|
|
118
119
|
declare function jsonApiResource<T, P = unknown, C = RegisteredClient>(options: JsonApiResourceOptions<T, P, C> & {
|
|
119
120
|
defaultValue: T;
|
|
120
|
-
}): JsonApiResourceRef<T
|
|
121
|
-
declare function jsonApiResource<T, P = unknown, C = RegisteredClient>(options: JsonApiResourceOptions<T, P, C>): JsonApiResourceRef<T | undefined
|
|
121
|
+
}): JsonApiResourceRef<T, ErrorCodeOf<C>>;
|
|
122
|
+
declare function jsonApiResource<T, P = unknown, C = RegisteredClient>(options: JsonApiResourceOptions<T, P, C>): JsonApiResourceRef<T | undefined, ErrorCodeOf<C>>;
|
|
122
123
|
//#endregion
|
|
123
124
|
//#region src/mutation.d.ts
|
|
124
125
|
/** Options for {@link jsonApiMutation}. */
|
|
@@ -128,17 +129,20 @@ interface JsonApiMutationOptions<C> {
|
|
|
128
129
|
/** Create the mutation outside an injection context. */
|
|
129
130
|
injector?: Injector;
|
|
130
131
|
}
|
|
131
|
-
/**
|
|
132
|
-
|
|
132
|
+
/**
|
|
133
|
+
* The signal-based handle {@link jsonApiMutation} returns. `E` is the client's error-code union,
|
|
134
|
+
* so a generated client's `jsonApiError()` carries its server's `ErrorCode`.
|
|
135
|
+
*/
|
|
136
|
+
interface JsonApiMutationRef<I, T, E extends string = string> {
|
|
133
137
|
/**
|
|
134
138
|
* Run the write. Resolves with its result and rejects with its error; the signals track the
|
|
135
139
|
* latest call either way.
|
|
136
140
|
*/
|
|
137
141
|
mutate(input: I): Promise<T>;
|
|
138
142
|
readonly isPending: Signal<boolean>;
|
|
139
|
-
readonly error: Signal<JsonApiResourceError | undefined>;
|
|
143
|
+
readonly error: Signal<JsonApiResourceError<E> | undefined>;
|
|
140
144
|
/** The error when it is a `JsonApiError`; use `byPath()` on it for form errors. */
|
|
141
|
-
readonly jsonApiError: Signal<JsonApiError | undefined>;
|
|
145
|
+
readonly jsonApiError: Signal<JsonApiError<E> | undefined>;
|
|
142
146
|
/** The latest successful result. */
|
|
143
147
|
readonly value: Signal<T | undefined>;
|
|
144
148
|
/** Clear the value and error. A call still in flight no longer updates the signals. */
|
|
@@ -155,29 +159,32 @@ interface JsonApiMutationRef<I, T> {
|
|
|
155
159
|
* )
|
|
156
160
|
* ```
|
|
157
161
|
*/
|
|
158
|
-
declare function jsonApiMutation<I, T, C = RegisteredClient>(mutation: (client: C, input: I) => PromiseLike<T>, options?: JsonApiMutationOptions<C>): JsonApiMutationRef<I, T
|
|
162
|
+
declare function jsonApiMutation<I, T, C = RegisteredClient>(mutation: (client: C, input: I) => PromiseLike<T>, options?: JsonApiMutationOptions<C>): JsonApiMutationRef<I, T, ErrorCodeOf<C>>;
|
|
159
163
|
//#endregion
|
|
160
164
|
//#region src/pagination.d.ts
|
|
161
|
-
/**
|
|
162
|
-
|
|
165
|
+
/**
|
|
166
|
+
* The `page` query a link selects, typed by the `$page` it came from: the strategy's members under
|
|
167
|
+
* their wire keys (counts as numbers, cursors as strings), plus the menu arm the read picked.
|
|
168
|
+
*/
|
|
169
|
+
type PageParams<P extends Page = Page> = PageParamsOf<P>;
|
|
163
170
|
/**
|
|
164
171
|
* The `page` params for the page after `list`, read from its `next` link, or `undefined` on the
|
|
165
|
-
* last page (or before anything has loaded). Works the same for
|
|
166
|
-
*
|
|
167
|
-
*
|
|
172
|
+
* last page (or before anything has loaded). Works the same for every strategy, because the
|
|
173
|
+
* server's link already carries the right parameters. Feed the result back into the signal your
|
|
174
|
+
* resource's params read:
|
|
168
175
|
*
|
|
169
176
|
* ```ts
|
|
170
177
|
* const next = nextPage(this.albums.value())
|
|
171
178
|
* if (next) this.page.set(next)
|
|
172
179
|
* ```
|
|
173
180
|
*/
|
|
174
|
-
declare function nextPage(list: {
|
|
175
|
-
readonly $page:
|
|
176
|
-
} | undefined): PageParams | undefined;
|
|
181
|
+
declare function nextPage<P extends Page>(list: {
|
|
182
|
+
readonly $page: P;
|
|
183
|
+
} | undefined): PageParams<P> | undefined;
|
|
177
184
|
/** The `page` params for the page before `list`, from its `prev` link. See {@link nextPage}. */
|
|
178
|
-
declare function prevPage(list: {
|
|
179
|
-
readonly $page:
|
|
180
|
-
} | undefined): PageParams | undefined;
|
|
185
|
+
declare function prevPage<P extends Page>(list: {
|
|
186
|
+
readonly $page: P;
|
|
187
|
+
} | undefined): PageParams<P> | undefined;
|
|
181
188
|
/** Options for {@link loadAll}. */
|
|
182
189
|
interface LoadAllOptions {
|
|
183
190
|
/** Stops the walk, rejecting with the signal's reason. Pass the loader's `abortSignal`. */
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/provide.ts","../src/resource.ts","../src/mutation.ts","../src/pagination.ts","../src/transport.ts"],"mappings":";;;;;;;;AAgBA;KAAY,oBAAA,GAAuB,IAAA,CAAK,aAAA;EACtC,SAAA,GAAY,gBAAA;AAAA;;KAIF,oBAAA,OAA2B,OAAA,EAAS,aAAA,KAAkB,CAAC;;KAGvD,eAAA,MAAqB,CAAA,WAAW,OAAA,EAAS,aAAa,gBAAe,CAAA;;;;;;AAPnD;AAI9B;;;;;;UAkBiB,QAAA;;KAGL,gBAAA,GAAmB,QAAQ;EAAW,MAAA;AAAA,IAAoB,CAAA;;cAGzD,eAAA,EAAe,cAAA;;;;;iBAMZ,wBAAA,IAA4B,WAAA,YAAgC,cAAc,CAAC,CAAA;;;AA3BT;AAelF;;;;AAAyB;iBAwBT,oBAAA,IACd,YAAA,EAAc,oBAAA,CAAqB,CAAA,GACnC,OAAA,EAAS,oBAAA,UAA8B,oBAAA,GACvC,KAAA,GAAO,cAAA,CAAe,CAAA,IACrB,oBAAA;;;;;iBAmBa,mBAAA,KAAwB,gBAAA,EACtC,KAAA,GAAQ,cAAA,CAAe,CAAA,GACvB,OAAA;EAAY,QAAA,GAAW,QAAA;AAAA,IACtB,CAAA;;;;;AAzEH;;;KCMY,oBAAA,
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/provide.ts","../src/resource.ts","../src/mutation.ts","../src/pagination.ts","../src/transport.ts"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -165,9 +165,9 @@ function jsonApiMutation(mutation, options = {}) {
|
|
|
165
165
|
//#region src/pagination.ts
|
|
166
166
|
/**
|
|
167
167
|
* The `page` params for the page after `list`, read from its `next` link, or `undefined` on the
|
|
168
|
-
* last page (or before anything has loaded). Works the same for
|
|
169
|
-
*
|
|
170
|
-
*
|
|
168
|
+
* last page (or before anything has loaded). Works the same for every strategy, because the
|
|
169
|
+
* server's link already carries the right parameters. Feed the result back into the signal your
|
|
170
|
+
* resource's params read:
|
|
171
171
|
*
|
|
172
172
|
* ```ts
|
|
173
173
|
* const next = nextPage(this.albums.value())
|
|
@@ -175,19 +175,24 @@ function jsonApiMutation(mutation, options = {}) {
|
|
|
175
175
|
* ```
|
|
176
176
|
*/
|
|
177
177
|
function nextPage(list) {
|
|
178
|
-
return pageParams(list?.$page.links.next);
|
|
178
|
+
return pageParams(list?.$page, list?.$page.links.next);
|
|
179
179
|
}
|
|
180
180
|
/** The `page` params for the page before `list`, from its `prev` link. See {@link nextPage}. */
|
|
181
181
|
function prevPage(list) {
|
|
182
|
-
return pageParams(list?.$page.links.prev);
|
|
182
|
+
return pageParams(list?.$page, list?.$page.links.prev);
|
|
183
183
|
}
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
const CURSOR_ROLES = ["after", "before"];
|
|
185
|
+
function pageParams(page, link) {
|
|
186
|
+
if (page === void 0 || link === void 0) return;
|
|
187
|
+
const keys = Object.entries(page.keys);
|
|
186
188
|
const query = link.includes("?") ? link.slice(link.indexOf("?") + 1).split("#")[0] : "";
|
|
187
189
|
const params = {};
|
|
188
|
-
for (const [
|
|
189
|
-
const
|
|
190
|
-
if (
|
|
190
|
+
for (const [name, value] of new URLSearchParams(query)) {
|
|
191
|
+
const member = /^page\[([^\]]+)\]$/.exec(name)?.[1];
|
|
192
|
+
if (member === void 0) continue;
|
|
193
|
+
const role = keys.find(([, key]) => key === member)?.[0];
|
|
194
|
+
const count = Number(value);
|
|
195
|
+
params[member] = role !== void 0 && !CURSOR_ROLES.includes(role) && value !== "" && Number.isFinite(count) ? count : value;
|
|
191
196
|
}
|
|
192
197
|
return params;
|
|
193
198
|
}
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/transport.ts","../src/provide.ts","../src/mutation.ts","../src/pagination.ts","../src/resource.ts"],"sourcesContent":["import { HttpErrorResponse, type HttpClient, type HttpHeaders } from '@angular/common/http'\nimport type { JsonApiTransport, TransportResponse } from '@haddowg/json-api-client'\n\n/**\n * A transport that sends every request through Angular's `HttpClient`, so the app's interceptors\n * run. The body comes back as text for the client to parse, exactly as with `fetchTransport`.\n *\n * A non-2xx response resolves with its status, headers and body, because the client maps those to\n * a `JsonApiError` itself. Only a request that never got a response (status 0: network failure,\n * CORS, a blocked request) rejects, with the `HttpErrorResponse`. Aborting `req.signal`\n * unsubscribes, which cancels the HTTP request, and rejects with the signal's reason.\n */\nexport function httpClientTransport(http: HttpClient): JsonApiTransport {\n return (req) =>\n new Promise<TransportResponse>((resolve, reject) => {\n const signal = req.signal\n if (signal?.aborted) {\n reject(signal.reason)\n return\n }\n\n let unsubscribe: (() => void) | undefined\n const onAbort = (): void => {\n unsubscribe?.()\n reject(signal?.reason)\n }\n signal?.addEventListener('abort', onAbort, { once: true })\n const settle = (): void => signal?.removeEventListener('abort', onAbort)\n\n const subscription = http\n .request(req.method, req.url, {\n body: req.body ?? null,\n headers: req.headers,\n observe: 'response',\n responseType: 'text',\n })\n .subscribe({\n next: (res) => {\n settle()\n resolve({ status: res.status, headers: toRecord(res.headers), body: res.body ?? '' })\n },\n error: (error: unknown) => {\n settle()\n if (error instanceof HttpErrorResponse && error.status !== 0) {\n resolve({\n status: error.status,\n headers: toRecord(error.headers),\n body: errorBody(error.error),\n })\n return\n }\n reject(error)\n },\n })\n unsubscribe = () => subscription.unsubscribe()\n })\n}\n\nfunction toRecord(headers: HttpHeaders): Record<string, string> {\n const out: Record<string, string> = {}\n for (const name of headers.keys()) {\n const values = headers.getAll(name)\n if (values !== null) {\n out[name.toLowerCase()] = values.join(', ')\n }\n }\n return out\n}\n\n// With responseType 'text' the error body is already a string. Anything else (an interceptor that\n// replaced it) is serialised so the client can still try to parse a JSON:API error document.\nfunction errorBody(body: unknown): string {\n if (typeof body === 'string') {\n return body\n }\n if (body === null || body === undefined) {\n return ''\n }\n return JSON.stringify(body)\n}\n","import { HttpClient } from '@angular/common/http'\nimport {\n assertInInjectionContext,\n type EnvironmentProviders,\n inject,\n InjectionToken,\n type Injector,\n makeEnvironmentProviders,\n} from '@angular/core'\nimport type { ClientOptions, JsonApiTransport } from '@haddowg/json-api-client'\nimport { httpClientTransport } from './transport'\n\n/**\n * Options for {@link provideJsonApiClient}: the client's options with the transport made\n * optional. Leave `transport` out to send through `HttpClient`; set it only to opt out.\n */\nexport type JsonApiClientOptions = Omit<ClientOptions, 'transport'> & {\n transport?: JsonApiTransport\n}\n\n/** A generated client factory: the bound `createClient` a codegen module exports. */\nexport type JsonApiClientFactory<C> = (options: ClientOptions) => C\n\n/** The client type a generated `createClient` returns. */\nexport type JsonApiClientOf<F> = F extends (options: ClientOptions) => infer C ? C : never\n\n/**\n * Register the app's default client type once, so `injectJsonApiClient()` and `jsonApiResource()`\n * are typed without a token:\n *\n * ```ts\n * declare module '@haddowg/json-api-angular' {\n * interface Register {\n * client: JsonApiClientOf<typeof createClient>\n * }\n * }\n * ```\n */\n// oxlint-disable-next-line typescript/no-empty-object-type -- filled in by module augmentation\nexport interface Register {}\n\n/** The default client's type: whatever {@link Register} declares, else `unknown`. */\nexport type RegisteredClient = Register extends { client: infer C } ? C : unknown\n\n/** The token {@link provideJsonApiClient} uses when none is given. */\nexport const JSON_API_CLIENT = new InjectionToken<unknown>('JSON_API_CLIENT')\n\n/**\n * Create an injection token for one client, so an app can provide several (one per server).\n * Type it with the client: `createJsonApiClientToken<JsonApiClientOf<typeof createAdminClient>>()`.\n */\nexport function createJsonApiClientToken<C>(description = 'JsonApiClient'): InjectionToken<C> {\n return new InjectionToken<C>(description)\n}\n\n/**\n * Provide a generated client to the environment injector. The client sends through `HttpClient`\n * (so `provideHttpClient()` must also be present) unless `options.transport` overrides it.\n *\n * `options` may be a function, which runs in an injection context, to read values such as the\n * base URL from other providers. `token` defaults to {@link JSON_API_CLIENT}; pass one from\n * {@link createJsonApiClientToken} to provide a second client alongside it.\n */\nexport function provideJsonApiClient<C>(\n createClient: JsonApiClientFactory<C>,\n options: JsonApiClientOptions | (() => JsonApiClientOptions),\n token: InjectionToken<C> = JSON_API_CLIENT as InjectionToken<C>,\n): EnvironmentProviders {\n return makeEnvironmentProviders([\n {\n provide: token,\n useFactory: (): C => {\n const resolved = typeof options === 'function' ? options() : options\n return createClient({\n ...resolved,\n transport: resolved.transport ?? httpClientTransport(inject(HttpClient)),\n })\n },\n },\n ])\n}\n\n/**\n * Inject a provided client. Without a token this returns the default client, typed by\n * {@link Register}. With a token it returns that token's client.\n */\nexport function injectJsonApiClient<C = RegisteredClient>(\n token?: InjectionToken<C>,\n options?: { injector?: Injector },\n): C {\n const resolved = token ?? (JSON_API_CLIENT as InjectionToken<C>)\n if (options?.injector !== undefined) {\n return options.injector.get(resolved)\n }\n assertInInjectionContext(injectJsonApiClient)\n return inject(resolved)\n}\n","import {\n assertInInjectionContext,\n computed,\n inject,\n type InjectionToken,\n Injector,\n type Signal,\n signal,\n} from '@angular/core'\nimport { JsonApiError } from '@haddowg/json-api-client'\nimport { JSON_API_CLIENT, type RegisteredClient } from './provide'\nimport type { JsonApiResourceError } from './resource'\n\n/** Options for {@link jsonApiMutation}. */\nexport interface JsonApiMutationOptions<C> {\n /** The client to write with. Defaults to the one `provideJsonApiClient` registered without a token. */\n client?: InjectionToken<C>\n /** Create the mutation outside an injection context. */\n injector?: Injector\n}\n\n/** The signal-based handle {@link jsonApiMutation} returns. */\nexport interface JsonApiMutationRef<I, T> {\n /**\n * Run the write. Resolves with its result and rejects with its error; the signals track the\n * latest call either way.\n */\n mutate(input: I): Promise<T>\n readonly isPending: Signal<boolean>\n readonly error: Signal<JsonApiResourceError | undefined>\n /** The error when it is a `JsonApiError`; use `byPath()` on it for form errors. */\n readonly jsonApiError: Signal<JsonApiError | undefined>\n /** The latest successful result. */\n readonly value: Signal<T | undefined>\n /** Clear the value and error. A call still in flight no longer updates the signals. */\n reset(): void\n}\n\n/**\n * Wrap a client write (create, update, delete, a relationship mutation, a custom action or an\n * atomic batch) in signals. Nothing is cached or invalidated: `reload()` the resources the write\n * affects.\n *\n * ```ts\n * readonly rename = jsonApiMutation((client, title: string) =>\n * client.albums.id(this.id()).update({ title }),\n * )\n * ```\n */\nexport function jsonApiMutation<I, T, C = RegisteredClient>(\n mutation: (client: C, input: I) => PromiseLike<T>,\n options: JsonApiMutationOptions<C> = {},\n): JsonApiMutationRef<I, T> {\n if (options.injector === undefined) {\n assertInInjectionContext(jsonApiMutation)\n }\n const injector = options.injector ?? inject(Injector)\n const client = injector.get(options.client ?? (JSON_API_CLIENT as InjectionToken<C>))\n\n const value = signal<T | undefined>(undefined)\n const error = signal<JsonApiResourceError | undefined>(undefined)\n const pending = signal(0)\n // Each call and reset bumps this, so only the latest call writes to the signals.\n let generation = 0\n\n const mutate = async (input: I): Promise<T> => {\n const current = ++generation\n pending.update((n) => n + 1)\n error.set(undefined)\n try {\n const result = await mutation(client, input)\n if (current === generation) {\n value.set(result)\n }\n return result\n } catch (thrown) {\n if (current === generation) {\n error.set(thrown instanceof Error ? thrown : new Error(String(thrown), { cause: thrown }))\n }\n throw thrown\n } finally {\n pending.update((n) => Math.max(0, n - 1))\n }\n }\n\n return {\n mutate,\n isPending: computed(() => pending() > 0),\n error: error.asReadonly(),\n jsonApiError: computed(() => {\n const e = error()\n return e instanceof JsonApiError ? e : undefined\n }),\n value: value.asReadonly(),\n reset: () => {\n generation++\n value.set(undefined)\n error.set(undefined)\n pending.set(0)\n },\n }\n}\n","import type { Collection, Page } from '@haddowg/json-api-client'\n\n/** The `page` query a link selects: its `page[...]` parameters, keyed without the prefix. */\nexport type PageParams = Record<string, string>\n\n/**\n * The `page` params for the page after `list`, read from its `next` link, or `undefined` on the\n * last page (or before anything has loaded). Works the same for page, offset and cursor\n * pagination, because the server's link already carries the right parameters. Feed the result\n * back into the signal your resource's params read:\n *\n * ```ts\n * const next = nextPage(this.albums.value())\n * if (next) this.page.set(next)\n * ```\n */\nexport function nextPage(list: { readonly $page: Page } | undefined): PageParams | undefined {\n return pageParams(list?.$page.links.next)\n}\n\n/** The `page` params for the page before `list`, from its `prev` link. See {@link nextPage}. */\nexport function prevPage(list: { readonly $page: Page } | undefined): PageParams | undefined {\n return pageParams(list?.$page.links.prev)\n}\n\nfunction pageParams(link: string | undefined): PageParams | undefined {\n if (link === undefined) {\n return undefined\n }\n const query = link.includes('?') ? link.slice(link.indexOf('?') + 1).split('#')[0] : ''\n const params: PageParams = {}\n for (const [key, value] of new URLSearchParams(query)) {\n const match = /^page\\[([^\\]]+)\\]$/.exec(key)\n if (match?.[1] !== undefined) {\n params[match[1]] = value\n }\n }\n return params\n}\n\n/** Options for {@link loadAll}. */\nexport interface LoadAllOptions {\n /** Stops the walk, rejecting with the signal's reason. Pass the loader's `abortSignal`. */\n signal?: AbortSignal\n /** Stop after this many pages, as a guard against an unexpectedly large collection. */\n maxPages?: number\n}\n\n/**\n * Follow `next` links from the first page until the last, and return every member in one plain\n * array. Use it inside a loader to read a whole (small) collection:\n *\n * ```ts\n * loader: ({ client, abortSignal }) =>\n * loadAll(client.genres.list({ signal: abortSignal }), { signal: abortSignal })\n * ```\n */\nexport async function loadAll<T>(\n first: Collection<T> | PromiseLike<Collection<T>>,\n options: LoadAllOptions = {},\n): Promise<T[]> {\n const { signal, maxPages = Number.POSITIVE_INFINITY } = options\n let page: Collection<T> | undefined = await first\n const all: T[] = []\n let count = 0\n while (page !== undefined && count < maxPages) {\n all.push(...page)\n count++\n if (count >= maxPages) {\n break\n }\n signal?.throwIfAborted()\n page = await page.$next(signal !== undefined ? { signal } : undefined)\n }\n return all\n}\n","// The only module that touches Angular's `resource()`. It is experimental before Angular 22, so a\n// change to its options or ref shape is absorbed here.\nimport {\n assertInInjectionContext,\n computed,\n inject,\n type InjectionToken,\n Injector,\n resource,\n type ResourceRef,\n type ResourceStatus,\n type Signal,\n type ValueEqualityFn,\n} from '@angular/core'\nimport { JsonApiError, type StructuralGuardError } from '@haddowg/json-api-client'\nimport { JSON_API_CLIENT, type RegisteredClient } from './provide'\n\n/**\n * What a read can fail with: a `JsonApiError` for a non-2xx response, a `StructuralGuardError`\n * for a response that isn't a JSON:API document, or any other error (a network failure, a\n * validator's error). Narrow with `instanceof`.\n */\nexport type JsonApiResourceError = JsonApiError | StructuralGuardError | Error\n\n/** What the loader receives each time the params change or the resource reloads. */\nexport interface JsonApiLoaderContext<C, P> {\n client: C\n params: P\n /** Aborts when the params change again or the resource is destroyed. Pass it to the read. */\n abortSignal: AbortSignal\n previous: { status: ResourceStatus }\n}\n\n/** Options for {@link jsonApiResource}. */\nexport interface JsonApiResourceOptions<T, P, C> {\n /**\n * A reactive function computing the read's params from signals. Return `undefined` while the\n * read isn't ready (no id yet, say) and the resource stays idle. Leave it out to load once.\n */\n params?: () => P | undefined\n /** Make the read. Return the client call's promise, passing `abortSignal` as its `signal`. */\n loader: (context: JsonApiLoaderContext<C, P>) => PromiseLike<T>\n /** The client to read with. Defaults to the one `provideJsonApiClient` registered without a token. */\n client?: InjectionToken<C>\n /** The value while nothing has loaded yet. */\n defaultValue?: T\n equal?: ValueEqualityFn<T>\n /** Create the resource outside an injection context. */\n injector?: Injector\n debugName?: string\n}\n\n/**\n * Angular's `ResourceRef` with `error()` typed to what a JSON:API read throws, plus\n * `jsonApiError()`, the error when it is a `JsonApiError` (handy in templates, which can't\n * use `instanceof`).\n */\nexport interface JsonApiResourceRef<T> extends Omit<ResourceRef<T>, 'error' | 'hasValue'> {\n readonly error: Signal<JsonApiResourceError | undefined>\n readonly jsonApiError: Signal<JsonApiError | undefined>\n /** Whether a value is loaded. Narrows `value()` to exclude `undefined`, in templates too. */\n hasValue(\n this: T extends undefined ? this : never,\n ): this is JsonApiResourceRef<Exclude<T, undefined>>\n hasValue(): boolean\n}\n\n/**\n * A signal-based JSON:API read: a thin wrapper over Angular's `resource()` that injects the\n * client and hands it to the loader. Changing a signal the params read starts a new load and\n * aborts the one in flight.\n *\n * ```ts\n * readonly albums = jsonApiResource({\n * params: () => ({ filter: { title: this.search() } }),\n * loader: ({ client, params, abortSignal }) =>\n * client.albums.list({ ...params, include: ['artist'], signal: abortSignal }),\n * })\n * ```\n */\nexport function jsonApiResource<T, P = unknown, C = RegisteredClient>(\n options: JsonApiResourceOptions<T, P, C> & { defaultValue: T },\n): JsonApiResourceRef<T>\nexport function jsonApiResource<T, P = unknown, C = RegisteredClient>(\n options: JsonApiResourceOptions<T, P, C>,\n): JsonApiResourceRef<T | undefined>\nexport function jsonApiResource<T, P, C>(\n options: JsonApiResourceOptions<T, P, C>,\n): JsonApiResourceRef<T | undefined> {\n if (options.injector === undefined) {\n assertInInjectionContext(jsonApiResource)\n }\n const injector = options.injector ?? inject(Injector)\n const client = injector.get(options.client ?? (JSON_API_CLIENT as InjectionToken<C>))\n\n const ref = resource<T | undefined, P | undefined>({\n ...(options.params !== undefined ? { params: options.params } : {}),\n loader: ({ params, abortSignal, previous }) =>\n options.loader({ client, params: params as P, abortSignal, previous }),\n ...(options.defaultValue !== undefined ? { defaultValue: options.defaultValue } : {}),\n ...(options.equal !== undefined\n ? { equal: options.equal as ValueEqualityFn<T | undefined> }\n : {}),\n ...(options.debugName !== undefined ? { debugName: options.debugName } : {}),\n injector,\n })\n\n const jsonApiError = computed(() => {\n const error = ref.error()\n return error instanceof JsonApiError ? error : undefined\n })\n Object.defineProperty(ref, 'jsonApiError', { value: jsonApiError, enumerable: true })\n return ref as unknown as JsonApiResourceRef<T | undefined>\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/transport.ts","../src/provide.ts","../src/mutation.ts","../src/pagination.ts","../src/resource.ts"],"sourcesContent":["import { HttpErrorResponse, type HttpClient, type HttpHeaders } from '@angular/common/http'\nimport type { JsonApiTransport, TransportResponse } from '@haddowg/json-api-client'\n\n/**\n * A transport that sends every request through Angular's `HttpClient`, so the app's interceptors\n * run. The body comes back as text for the client to parse, exactly as with `fetchTransport`.\n *\n * A non-2xx response resolves with its status, headers and body, because the client maps those to\n * a `JsonApiError` itself. Only a request that never got a response (status 0: network failure,\n * CORS, a blocked request) rejects, with the `HttpErrorResponse`. Aborting `req.signal`\n * unsubscribes, which cancels the HTTP request, and rejects with the signal's reason.\n */\nexport function httpClientTransport(http: HttpClient): JsonApiTransport {\n return (req) =>\n new Promise<TransportResponse>((resolve, reject) => {\n const signal = req.signal\n if (signal?.aborted) {\n reject(signal.reason)\n return\n }\n\n let unsubscribe: (() => void) | undefined\n const onAbort = (): void => {\n unsubscribe?.()\n reject(signal?.reason)\n }\n signal?.addEventListener('abort', onAbort, { once: true })\n const settle = (): void => signal?.removeEventListener('abort', onAbort)\n\n const subscription = http\n .request(req.method, req.url, {\n body: req.body ?? null,\n headers: req.headers,\n observe: 'response',\n responseType: 'text',\n })\n .subscribe({\n next: (res) => {\n settle()\n resolve({ status: res.status, headers: toRecord(res.headers), body: res.body ?? '' })\n },\n error: (error: unknown) => {\n settle()\n if (error instanceof HttpErrorResponse && error.status !== 0) {\n resolve({\n status: error.status,\n headers: toRecord(error.headers),\n body: errorBody(error.error),\n })\n return\n }\n reject(error)\n },\n })\n unsubscribe = () => subscription.unsubscribe()\n })\n}\n\nfunction toRecord(headers: HttpHeaders): Record<string, string> {\n const out: Record<string, string> = {}\n for (const name of headers.keys()) {\n const values = headers.getAll(name)\n if (values !== null) {\n out[name.toLowerCase()] = values.join(', ')\n }\n }\n return out\n}\n\n// With responseType 'text' the error body is already a string. Anything else (an interceptor that\n// replaced it) is serialised so the client can still try to parse a JSON:API error document.\nfunction errorBody(body: unknown): string {\n if (typeof body === 'string') {\n return body\n }\n if (body === null || body === undefined) {\n return ''\n }\n return JSON.stringify(body)\n}\n","import { HttpClient } from '@angular/common/http'\nimport {\n assertInInjectionContext,\n type EnvironmentProviders,\n inject,\n InjectionToken,\n type Injector,\n makeEnvironmentProviders,\n} from '@angular/core'\nimport type { ClientOptions, JsonApiTransport } from '@haddowg/json-api-client'\nimport { httpClientTransport } from './transport'\n\n/**\n * Options for {@link provideJsonApiClient}: the client's options with the transport made\n * optional. Leave `transport` out to send through `HttpClient`; set it only to opt out.\n */\nexport type JsonApiClientOptions = Omit<ClientOptions, 'transport'> & {\n transport?: JsonApiTransport\n}\n\n/** A generated client factory: the bound `createClient` a codegen module exports. */\nexport type JsonApiClientFactory<C> = (options: ClientOptions) => C\n\n/** The client type a generated `createClient` returns. */\nexport type JsonApiClientOf<F> = F extends (options: ClientOptions) => infer C ? C : never\n\n/**\n * Register the app's default client type once, so `injectJsonApiClient()` and `jsonApiResource()`\n * are typed without a token:\n *\n * ```ts\n * declare module '@haddowg/json-api-angular' {\n * interface Register {\n * client: JsonApiClientOf<typeof createClient>\n * }\n * }\n * ```\n */\n// oxlint-disable-next-line typescript/no-empty-object-type -- filled in by module augmentation\nexport interface Register {}\n\n/** The default client's type: whatever {@link Register} declares, else `unknown`. */\nexport type RegisteredClient = Register extends { client: infer C } ? C : unknown\n\n/** The token {@link provideJsonApiClient} uses when none is given. */\nexport const JSON_API_CLIENT = new InjectionToken<unknown>('JSON_API_CLIENT')\n\n/**\n * Create an injection token for one client, so an app can provide several (one per server).\n * Type it with the client: `createJsonApiClientToken<JsonApiClientOf<typeof createAdminClient>>()`.\n */\nexport function createJsonApiClientToken<C>(description = 'JsonApiClient'): InjectionToken<C> {\n return new InjectionToken<C>(description)\n}\n\n/**\n * Provide a generated client to the environment injector. The client sends through `HttpClient`\n * (so `provideHttpClient()` must also be present) unless `options.transport` overrides it.\n *\n * `options` may be a function, which runs in an injection context, to read values such as the\n * base URL from other providers. `token` defaults to {@link JSON_API_CLIENT}; pass one from\n * {@link createJsonApiClientToken} to provide a second client alongside it.\n */\nexport function provideJsonApiClient<C>(\n createClient: JsonApiClientFactory<C>,\n options: JsonApiClientOptions | (() => JsonApiClientOptions),\n token: InjectionToken<C> = JSON_API_CLIENT as InjectionToken<C>,\n): EnvironmentProviders {\n return makeEnvironmentProviders([\n {\n provide: token,\n useFactory: (): C => {\n const resolved = typeof options === 'function' ? options() : options\n return createClient({\n ...resolved,\n transport: resolved.transport ?? httpClientTransport(inject(HttpClient)),\n })\n },\n },\n ])\n}\n\n/**\n * Inject a provided client. Without a token this returns the default client, typed by\n * {@link Register}. With a token it returns that token's client.\n */\nexport function injectJsonApiClient<C = RegisteredClient>(\n token?: InjectionToken<C>,\n options?: { injector?: Injector },\n): C {\n const resolved = token ?? (JSON_API_CLIENT as InjectionToken<C>)\n if (options?.injector !== undefined) {\n return options.injector.get(resolved)\n }\n assertInInjectionContext(injectJsonApiClient)\n return inject(resolved)\n}\n","import {\n assertInInjectionContext,\n computed,\n inject,\n type InjectionToken,\n Injector,\n type Signal,\n signal,\n} from '@angular/core'\nimport { type ErrorCodeOf, JsonApiError } from '@haddowg/json-api-client'\nimport { JSON_API_CLIENT, type RegisteredClient } from './provide'\nimport type { JsonApiResourceError } from './resource'\n\n/** Options for {@link jsonApiMutation}. */\nexport interface JsonApiMutationOptions<C> {\n /** The client to write with. Defaults to the one `provideJsonApiClient` registered without a token. */\n client?: InjectionToken<C>\n /** Create the mutation outside an injection context. */\n injector?: Injector\n}\n\n/**\n * The signal-based handle {@link jsonApiMutation} returns. `E` is the client's error-code union,\n * so a generated client's `jsonApiError()` carries its server's `ErrorCode`.\n */\nexport interface JsonApiMutationRef<I, T, E extends string = string> {\n /**\n * Run the write. Resolves with its result and rejects with its error; the signals track the\n * latest call either way.\n */\n mutate(input: I): Promise<T>\n readonly isPending: Signal<boolean>\n readonly error: Signal<JsonApiResourceError<E> | undefined>\n /** The error when it is a `JsonApiError`; use `byPath()` on it for form errors. */\n readonly jsonApiError: Signal<JsonApiError<E> | undefined>\n /** The latest successful result. */\n readonly value: Signal<T | undefined>\n /** Clear the value and error. A call still in flight no longer updates the signals. */\n reset(): void\n}\n\n/**\n * Wrap a client write (create, update, delete, a relationship mutation, a custom action or an\n * atomic batch) in signals. Nothing is cached or invalidated: `reload()` the resources the write\n * affects.\n *\n * ```ts\n * readonly rename = jsonApiMutation((client, title: string) =>\n * client.albums.id(this.id()).update({ title }),\n * )\n * ```\n */\nexport function jsonApiMutation<I, T, C = RegisteredClient>(\n mutation: (client: C, input: I) => PromiseLike<T>,\n options: JsonApiMutationOptions<C> = {},\n): JsonApiMutationRef<I, T, ErrorCodeOf<C>> {\n if (options.injector === undefined) {\n assertInInjectionContext(jsonApiMutation)\n }\n const injector = options.injector ?? inject(Injector)\n const client = injector.get(options.client ?? (JSON_API_CLIENT as InjectionToken<C>))\n\n const value = signal<T | undefined>(undefined)\n const error = signal<JsonApiResourceError | undefined>(undefined)\n const pending = signal(0)\n // Each call and reset bumps this, so only the latest call writes to the signals.\n let generation = 0\n\n const mutate = async (input: I): Promise<T> => {\n const current = ++generation\n pending.update((n) => n + 1)\n error.set(undefined)\n try {\n const result = await mutation(client, input)\n if (current === generation) {\n value.set(result)\n }\n return result\n } catch (thrown) {\n if (current === generation) {\n error.set(thrown instanceof Error ? thrown : new Error(String(thrown), { cause: thrown }))\n }\n throw thrown\n } finally {\n pending.update((n) => Math.max(0, n - 1))\n }\n }\n\n return {\n mutate,\n isPending: computed(() => pending() > 0),\n error: error.asReadonly(),\n jsonApiError: computed(() => {\n const e = error()\n return e instanceof JsonApiError ? e : undefined\n }),\n value: value.asReadonly(),\n reset: () => {\n generation++\n value.set(undefined)\n error.set(undefined)\n pending.set(0)\n },\n }\n}\n","import type { Collection, Page, PageParamsOf, PaginatorRole } from '@haddowg/json-api-client'\n\n/**\n * The `page` query a link selects, typed by the `$page` it came from: the strategy's members under\n * their wire keys (counts as numbers, cursors as strings), plus the menu arm the read picked.\n */\nexport type PageParams<P extends Page = Page> = PageParamsOf<P>\n\n/**\n * The `page` params for the page after `list`, read from its `next` link, or `undefined` on the\n * last page (or before anything has loaded). Works the same for every strategy, because the\n * server's link already carries the right parameters. Feed the result back into the signal your\n * resource's params read:\n *\n * ```ts\n * const next = nextPage(this.albums.value())\n * if (next) this.page.set(next)\n * ```\n */\nexport function nextPage<P extends Page>(\n list: { readonly $page: P } | undefined,\n): PageParams<P> | undefined {\n return pageParams(list?.$page, list?.$page.links.next)\n}\n\n/** The `page` params for the page before `list`, from its `prev` link. See {@link nextPage}. */\nexport function prevPage<P extends Page>(\n list: { readonly $page: P } | undefined,\n): PageParams<P> | undefined {\n return pageParams(list?.$page, list?.$page.links.prev)\n}\n\nconst CURSOR_ROLES: readonly string[] = ['after', 'before']\n\nfunction pageParams<P extends Page>(\n page: P | undefined,\n link: string | undefined,\n): PageParams<P> | undefined {\n if (page === undefined || link === undefined) {\n return undefined\n }\n const keys = Object.entries(page.keys) as [PaginatorRole, string][]\n const query = link.includes('?') ? link.slice(link.indexOf('?') + 1).split('#')[0] : ''\n const params: Record<string, string | number> = {}\n for (const [name, value] of new URLSearchParams(query)) {\n const member = /^page\\[([^\\]]+)\\]$/.exec(name)?.[1]\n if (member === undefined) {\n continue\n }\n const role = keys.find(([, key]) => key === member)?.[0]\n const count = Number(value)\n params[member] =\n role !== undefined && !CURSOR_ROLES.includes(role) && value !== '' && Number.isFinite(count)\n ? count\n : value\n }\n return params as PageParams<P>\n}\n\n/** Options for {@link loadAll}. */\nexport interface LoadAllOptions {\n /** Stops the walk, rejecting with the signal's reason. Pass the loader's `abortSignal`. */\n signal?: AbortSignal\n /** Stop after this many pages, as a guard against an unexpectedly large collection. */\n maxPages?: number\n}\n\n/**\n * Follow `next` links from the first page until the last, and return every member in one plain\n * array. Use it inside a loader to read a whole (small) collection:\n *\n * ```ts\n * loader: ({ client, abortSignal }) =>\n * loadAll(client.genres.list({ signal: abortSignal }), { signal: abortSignal })\n * ```\n */\nexport async function loadAll<T>(\n first: Collection<T> | PromiseLike<Collection<T>>,\n options: LoadAllOptions = {},\n): Promise<T[]> {\n const { signal, maxPages = Number.POSITIVE_INFINITY } = options\n let page: Collection<T> | undefined = await first\n const all: T[] = []\n let count = 0\n while (page !== undefined && count < maxPages) {\n all.push(...page)\n count++\n if (count >= maxPages) {\n break\n }\n signal?.throwIfAborted()\n page = await page.$next(signal !== undefined ? { signal } : undefined)\n }\n return all\n}\n","// The only module that touches Angular's `resource()`. It is experimental before Angular 22, so a\n// change to its options or ref shape is absorbed here.\nimport {\n assertInInjectionContext,\n computed,\n inject,\n type InjectionToken,\n Injector,\n resource,\n type ResourceRef,\n type ResourceStatus,\n type Signal,\n type ValueEqualityFn,\n} from '@angular/core'\nimport { type ErrorCodeOf, JsonApiError, type StructuralGuardError } from '@haddowg/json-api-client'\nimport { JSON_API_CLIENT, type RegisteredClient } from './provide'\n\n/**\n * What a read can fail with: a `JsonApiError` for a non-2xx response, a `StructuralGuardError`\n * for a response that isn't a JSON:API document, or any other error (a network failure, a\n * validator's error). Narrow with `instanceof`. `E` types the `JsonApiError`'s codes.\n */\nexport type JsonApiResourceError<E extends string = string> =\n | JsonApiError<E>\n | StructuralGuardError\n | Error\n\n/** What the loader receives each time the params change or the resource reloads. */\nexport interface JsonApiLoaderContext<C, P> {\n client: C\n params: P\n /** Aborts when the params change again or the resource is destroyed. Pass it to the read. */\n abortSignal: AbortSignal\n previous: { status: ResourceStatus }\n}\n\n/** Options for {@link jsonApiResource}. */\nexport interface JsonApiResourceOptions<T, P, C> {\n /**\n * A reactive function computing the read's params from signals. Return `undefined` while the\n * read isn't ready (no id yet, say) and the resource stays idle. Leave it out to load once.\n */\n params?: () => P | undefined\n /** Make the read. Return the client call's promise, passing `abortSignal` as its `signal`. */\n loader: (context: JsonApiLoaderContext<C, P>) => PromiseLike<T>\n /** The client to read with. Defaults to the one `provideJsonApiClient` registered without a token. */\n client?: InjectionToken<C>\n /** The value while nothing has loaded yet. */\n defaultValue?: T\n equal?: ValueEqualityFn<T>\n /** Create the resource outside an injection context. */\n injector?: Injector\n debugName?: string\n}\n\n/**\n * Angular's `ResourceRef` with `error()` typed to what a JSON:API read throws, plus\n * `jsonApiError()`, the error when it is a `JsonApiError` (handy in templates, which can't\n * use `instanceof`). `E` is the client's error-code union, so a generated client's\n * `jsonApiError()` carries its server's `ErrorCode`.\n */\nexport interface JsonApiResourceRef<T, E extends string = string> extends Omit<\n ResourceRef<T>,\n 'error' | 'hasValue'\n> {\n readonly error: Signal<JsonApiResourceError<E> | undefined>\n readonly jsonApiError: Signal<JsonApiError<E> | undefined>\n /** Whether a value is loaded. Narrows `value()` to exclude `undefined`, in templates too. */\n hasValue(\n this: T extends undefined ? this : never,\n ): this is JsonApiResourceRef<Exclude<T, undefined>, E>\n hasValue(): boolean\n}\n\n/**\n * A signal-based JSON:API read: a thin wrapper over Angular's `resource()` that injects the\n * client and hands it to the loader. Changing a signal the params read starts a new load and\n * aborts the one in flight.\n *\n * ```ts\n * readonly albums = jsonApiResource({\n * params: () => ({ filter: { title: this.search() } }),\n * loader: ({ client, params, abortSignal }) =>\n * client.albums.list({ ...params, include: ['artist'], signal: abortSignal }),\n * })\n * ```\n */\nexport function jsonApiResource<T, P = unknown, C = RegisteredClient>(\n options: JsonApiResourceOptions<T, P, C> & { defaultValue: T },\n): JsonApiResourceRef<T, ErrorCodeOf<C>>\nexport function jsonApiResource<T, P = unknown, C = RegisteredClient>(\n options: JsonApiResourceOptions<T, P, C>,\n): JsonApiResourceRef<T | undefined, ErrorCodeOf<C>>\nexport function jsonApiResource<T, P, C>(\n options: JsonApiResourceOptions<T, P, C>,\n): JsonApiResourceRef<T | undefined, ErrorCodeOf<C>> {\n if (options.injector === undefined) {\n assertInInjectionContext(jsonApiResource)\n }\n const injector = options.injector ?? inject(Injector)\n const client = injector.get(options.client ?? (JSON_API_CLIENT as InjectionToken<C>))\n\n const ref = resource<T | undefined, P | undefined>({\n ...(options.params !== undefined ? { params: options.params } : {}),\n loader: ({ params, abortSignal, previous }) =>\n options.loader({ client, params: params as P, abortSignal, previous }),\n ...(options.defaultValue !== undefined ? { defaultValue: options.defaultValue } : {}),\n ...(options.equal !== undefined\n ? { equal: options.equal as ValueEqualityFn<T | undefined> }\n : {}),\n ...(options.debugName !== undefined ? { debugName: options.debugName } : {}),\n injector,\n })\n\n const jsonApiError = computed(() => {\n const error = ref.error()\n return error instanceof JsonApiError ? error : undefined\n })\n Object.defineProperty(ref, 'jsonApiError', { value: jsonApiError, enumerable: true })\n return ref as unknown as JsonApiResourceRef<T | undefined, ErrorCodeOf<C>>\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haddowg/json-api-angular",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Signal-based Angular bindings for the JSON:API client: an HttpClient transport, DI providers, and resource/mutation helpers.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Gregory Haddow (https://github.com/haddowg)",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"provenance": true
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@haddowg/json-api-client": "^1.
|
|
51
|
+
"@haddowg/json-api-client": "^1.3.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"@angular/common": "^20.0.0 || ^21.0.0 || ^22.0.0",
|