@haddowg/json-api-angular 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +106 -2
- package/dist/index.cjs +257 -0
- package/dist/index.d.cts +212 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +212 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +249 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +67 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gregory Haddow
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,107 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @haddowg/json-api-angular
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Part of the [jsonapi.rest](https://jsonapi.rest) suite** — a complete, spec-compliant
|
|
4
|
+
> JSON:API 1.1 stack: a framework-agnostic PHP [core](https://github.com/haddowg/json-api), a
|
|
5
|
+
> [Symfony bundle](https://github.com/haddowg/json-api-symfony), a
|
|
6
|
+
> [Laravel package](https://github.com/haddowg/json-api-laravel), and this **typed TypeScript
|
|
7
|
+
> client**, bound together by one conformance-tested OpenAPI 3.1 contract.
|
|
8
|
+
> Full documentation: **[haddowg.github.io/json-api-ts](https://haddowg.github.io/json-api-ts/)**.
|
|
9
|
+
|
|
10
|
+
Signal-based Angular bindings for a generated [`@haddowg/json-api-client`](../client). The client
|
|
11
|
+
goes into dependency injection and sends through Angular's `HttpClient`, so your interceptors run
|
|
12
|
+
on every request. Reads become signals through Angular's `resource()`, and writes get a small
|
|
13
|
+
signal-based mutation handle.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @haddowg/json-api-angular @haddowg/json-api-client
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`@angular/core` and `@angular/common` (20, 21 or 22) are peer dependencies.
|
|
22
|
+
|
|
23
|
+
## Provide the client
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { provideHttpClient, withInterceptors } from '@angular/common/http'
|
|
27
|
+
import { provideJsonApiClient } from '@haddowg/json-api-angular'
|
|
28
|
+
import { createClient } from './api/music.gen'
|
|
29
|
+
|
|
30
|
+
export const appConfig: ApplicationConfig = {
|
|
31
|
+
providers: [
|
|
32
|
+
provideHttpClient(withInterceptors([authInterceptor])),
|
|
33
|
+
provideJsonApiClient(createClient, { baseUrl: '/api/v5' }),
|
|
34
|
+
],
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Register the client's type once so it is typed everywhere without a token:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import type { JsonApiClientOf } from '@haddowg/json-api-angular'
|
|
42
|
+
|
|
43
|
+
declare module '@haddowg/json-api-angular' {
|
|
44
|
+
interface Register {
|
|
45
|
+
client: JsonApiClientOf<typeof createClient>
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
For a second client (another server), create a token with `createJsonApiClientToken()` and pass it
|
|
51
|
+
as the third argument to `provideJsonApiClient`, then to `injectJsonApiClient(token)` or a
|
|
52
|
+
resource's `client` option.
|
|
53
|
+
|
|
54
|
+
## Read with a resource
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
@Component({
|
|
58
|
+
template: `
|
|
59
|
+
@if (albums.hasValue()) {
|
|
60
|
+
@for (album of albums.value(); track album.id) {
|
|
61
|
+
<li>{{ album.title }} by {{ album.artist?.name }}</li>
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
@if (albums.jsonApiError(); as error) {
|
|
65
|
+
<p>{{ error.status }}</p>
|
|
66
|
+
}
|
|
67
|
+
`,
|
|
68
|
+
})
|
|
69
|
+
export class AlbumList {
|
|
70
|
+
readonly search = signal('')
|
|
71
|
+
|
|
72
|
+
readonly albums = jsonApiResource({
|
|
73
|
+
params: () => ({ filter: { title: this.search() } }),
|
|
74
|
+
loader: ({ client, params, abortSignal }) =>
|
|
75
|
+
client.albums.list({ ...params, include: ['artist'], signal: abortSignal }),
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Changing `search` starts a new read and cancels the one in flight. Return `undefined` from
|
|
81
|
+
`params` to hold the read until its inputs are ready. As with any Angular resource, `value()`
|
|
82
|
+
throws in the error state, so guard it with `hasValue()`.
|
|
83
|
+
|
|
84
|
+
`nextPage(list)` and `prevPage(list)` return the `page` params from a collection's links, ready to
|
|
85
|
+
set on the signal your params read. `loadAll(firstPage, { signal })` follows `next` links and
|
|
86
|
+
returns every member in one array.
|
|
87
|
+
|
|
88
|
+
## Write with a mutation
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
readonly rename = jsonApiMutation((client, title: string) =>
|
|
92
|
+
client.albums.id(this.id()).update({ title }),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
save(title: string) {
|
|
96
|
+
this.rename.mutate(title).then(() => this.albums.reload(), () => {})
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`isPending()`, `error()`, `jsonApiError()`, `value()` and `reset()` track the latest call. Nothing
|
|
101
|
+
is cached between resources, so reload the reads a write affects.
|
|
102
|
+
|
|
103
|
+
See the [Angular guide](https://haddowg.github.io/json-api-ts/angular/) for the details.
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _angular_core = require("@angular/core");
|
|
3
|
+
let _haddowg_json_api_client = require("@haddowg/json-api-client");
|
|
4
|
+
let _angular_common_http = require("@angular/common/http");
|
|
5
|
+
//#region src/transport.ts
|
|
6
|
+
/**
|
|
7
|
+
* A transport that sends every request through Angular's `HttpClient`, so the app's interceptors
|
|
8
|
+
* run. The body comes back as text for the client to parse, exactly as with `fetchTransport`.
|
|
9
|
+
*
|
|
10
|
+
* A non-2xx response resolves with its status, headers and body, because the client maps those to
|
|
11
|
+
* a `JsonApiError` itself. Only a request that never got a response (status 0: network failure,
|
|
12
|
+
* CORS, a blocked request) rejects, with the `HttpErrorResponse`. Aborting `req.signal`
|
|
13
|
+
* unsubscribes, which cancels the HTTP request, and rejects with the signal's reason.
|
|
14
|
+
*/
|
|
15
|
+
function httpClientTransport(http) {
|
|
16
|
+
return (req) => new Promise((resolve, reject) => {
|
|
17
|
+
const signal = req.signal;
|
|
18
|
+
if (signal?.aborted) {
|
|
19
|
+
reject(signal.reason);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
let unsubscribe;
|
|
23
|
+
const onAbort = () => {
|
|
24
|
+
unsubscribe?.();
|
|
25
|
+
reject(signal?.reason);
|
|
26
|
+
};
|
|
27
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
28
|
+
const settle = () => signal?.removeEventListener("abort", onAbort);
|
|
29
|
+
const subscription = http.request(req.method, req.url, {
|
|
30
|
+
body: req.body ?? null,
|
|
31
|
+
headers: req.headers,
|
|
32
|
+
observe: "response",
|
|
33
|
+
responseType: "text"
|
|
34
|
+
}).subscribe({
|
|
35
|
+
next: (res) => {
|
|
36
|
+
settle();
|
|
37
|
+
resolve({
|
|
38
|
+
status: res.status,
|
|
39
|
+
headers: toRecord(res.headers),
|
|
40
|
+
body: res.body ?? ""
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
error: (error) => {
|
|
44
|
+
settle();
|
|
45
|
+
if (error instanceof _angular_common_http.HttpErrorResponse && error.status !== 0) {
|
|
46
|
+
resolve({
|
|
47
|
+
status: error.status,
|
|
48
|
+
headers: toRecord(error.headers),
|
|
49
|
+
body: errorBody(error.error)
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
reject(error);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
unsubscribe = () => subscription.unsubscribe();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function toRecord(headers) {
|
|
60
|
+
const out = {};
|
|
61
|
+
for (const name of headers.keys()) {
|
|
62
|
+
const values = headers.getAll(name);
|
|
63
|
+
if (values !== null) out[name.toLowerCase()] = values.join(", ");
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
function errorBody(body) {
|
|
68
|
+
if (typeof body === "string") return body;
|
|
69
|
+
if (body === null || body === void 0) return "";
|
|
70
|
+
return JSON.stringify(body);
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/provide.ts
|
|
74
|
+
/** The token {@link provideJsonApiClient} uses when none is given. */
|
|
75
|
+
const JSON_API_CLIENT = new _angular_core.InjectionToken("JSON_API_CLIENT");
|
|
76
|
+
/**
|
|
77
|
+
* Create an injection token for one client, so an app can provide several (one per server).
|
|
78
|
+
* Type it with the client: `createJsonApiClientToken<JsonApiClientOf<typeof createAdminClient>>()`.
|
|
79
|
+
*/
|
|
80
|
+
function createJsonApiClientToken(description = "JsonApiClient") {
|
|
81
|
+
return new _angular_core.InjectionToken(description);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Provide a generated client to the environment injector. The client sends through `HttpClient`
|
|
85
|
+
* (so `provideHttpClient()` must also be present) unless `options.transport` overrides it.
|
|
86
|
+
*
|
|
87
|
+
* `options` may be a function, which runs in an injection context, to read values such as the
|
|
88
|
+
* base URL from other providers. `token` defaults to {@link JSON_API_CLIENT}; pass one from
|
|
89
|
+
* {@link createJsonApiClientToken} to provide a second client alongside it.
|
|
90
|
+
*/
|
|
91
|
+
function provideJsonApiClient(createClient, options, token = JSON_API_CLIENT) {
|
|
92
|
+
return (0, _angular_core.makeEnvironmentProviders)([{
|
|
93
|
+
provide: token,
|
|
94
|
+
useFactory: () => {
|
|
95
|
+
const resolved = typeof options === "function" ? options() : options;
|
|
96
|
+
return createClient({
|
|
97
|
+
...resolved,
|
|
98
|
+
transport: resolved.transport ?? httpClientTransport((0, _angular_core.inject)(_angular_common_http.HttpClient))
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}]);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Inject a provided client. Without a token this returns the default client, typed by
|
|
105
|
+
* {@link Register}. With a token it returns that token's client.
|
|
106
|
+
*/
|
|
107
|
+
function injectJsonApiClient(token, options) {
|
|
108
|
+
const resolved = token ?? JSON_API_CLIENT;
|
|
109
|
+
if (options?.injector !== void 0) return options.injector.get(resolved);
|
|
110
|
+
(0, _angular_core.assertInInjectionContext)(injectJsonApiClient);
|
|
111
|
+
return (0, _angular_core.inject)(resolved);
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/mutation.ts
|
|
115
|
+
/**
|
|
116
|
+
* Wrap a client write (create, update, delete, a relationship mutation, a custom action or an
|
|
117
|
+
* atomic batch) in signals. Nothing is cached or invalidated: `reload()` the resources the write
|
|
118
|
+
* affects.
|
|
119
|
+
*
|
|
120
|
+
* ```ts
|
|
121
|
+
* readonly rename = jsonApiMutation((client, title: string) =>
|
|
122
|
+
* client.albums.id(this.id()).update({ title }),
|
|
123
|
+
* )
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
function jsonApiMutation(mutation, options = {}) {
|
|
127
|
+
if (options.injector === void 0) (0, _angular_core.assertInInjectionContext)(jsonApiMutation);
|
|
128
|
+
const client = (options.injector ?? (0, _angular_core.inject)(_angular_core.Injector)).get(options.client ?? JSON_API_CLIENT);
|
|
129
|
+
const value = (0, _angular_core.signal)(void 0);
|
|
130
|
+
const error = (0, _angular_core.signal)(void 0);
|
|
131
|
+
const pending = (0, _angular_core.signal)(0);
|
|
132
|
+
let generation = 0;
|
|
133
|
+
const mutate = async (input) => {
|
|
134
|
+
const current = ++generation;
|
|
135
|
+
pending.update((n) => n + 1);
|
|
136
|
+
error.set(void 0);
|
|
137
|
+
try {
|
|
138
|
+
const result = await mutation(client, input);
|
|
139
|
+
if (current === generation) value.set(result);
|
|
140
|
+
return result;
|
|
141
|
+
} catch (thrown) {
|
|
142
|
+
if (current === generation) error.set(thrown instanceof Error ? thrown : new Error(String(thrown), { cause: thrown }));
|
|
143
|
+
throw thrown;
|
|
144
|
+
} finally {
|
|
145
|
+
pending.update((n) => Math.max(0, n - 1));
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
return {
|
|
149
|
+
mutate,
|
|
150
|
+
isPending: (0, _angular_core.computed)(() => pending() > 0),
|
|
151
|
+
error: error.asReadonly(),
|
|
152
|
+
jsonApiError: (0, _angular_core.computed)(() => {
|
|
153
|
+
const e = error();
|
|
154
|
+
return e instanceof _haddowg_json_api_client.JsonApiError ? e : void 0;
|
|
155
|
+
}),
|
|
156
|
+
value: value.asReadonly(),
|
|
157
|
+
reset: () => {
|
|
158
|
+
generation++;
|
|
159
|
+
value.set(void 0);
|
|
160
|
+
error.set(void 0);
|
|
161
|
+
pending.set(0);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region src/pagination.ts
|
|
167
|
+
/**
|
|
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 page, offset and cursor
|
|
170
|
+
* pagination, because the server's link already carries the right parameters. Feed the result
|
|
171
|
+
* back into the signal your resource's params read:
|
|
172
|
+
*
|
|
173
|
+
* ```ts
|
|
174
|
+
* const next = nextPage(this.albums.value())
|
|
175
|
+
* if (next) this.page.set(next)
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
function nextPage(list) {
|
|
179
|
+
return pageParams(list?.$page.links.next);
|
|
180
|
+
}
|
|
181
|
+
/** The `page` params for the page before `list`, from its `prev` link. See {@link nextPage}. */
|
|
182
|
+
function prevPage(list) {
|
|
183
|
+
return pageParams(list?.$page.links.prev);
|
|
184
|
+
}
|
|
185
|
+
function pageParams(link) {
|
|
186
|
+
if (link === void 0) return;
|
|
187
|
+
const query = link.includes("?") ? link.slice(link.indexOf("?") + 1).split("#")[0] : "";
|
|
188
|
+
const params = {};
|
|
189
|
+
for (const [key, value] of new URLSearchParams(query)) {
|
|
190
|
+
const match = /^page\[([^\]]+)\]$/.exec(key);
|
|
191
|
+
if (match?.[1] !== void 0) params[match[1]] = value;
|
|
192
|
+
}
|
|
193
|
+
return params;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Follow `next` links from the first page until the last, and return every member in one plain
|
|
197
|
+
* array. Use it inside a loader to read a whole (small) collection:
|
|
198
|
+
*
|
|
199
|
+
* ```ts
|
|
200
|
+
* loader: ({ client, abortSignal }) =>
|
|
201
|
+
* loadAll(client.genres.list({ signal: abortSignal }), { signal: abortSignal })
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
async function loadAll(first, options = {}) {
|
|
205
|
+
const { signal, maxPages = Number.POSITIVE_INFINITY } = options;
|
|
206
|
+
let page = await first;
|
|
207
|
+
const all = [];
|
|
208
|
+
let count = 0;
|
|
209
|
+
while (page !== void 0 && count < maxPages) {
|
|
210
|
+
all.push(...page);
|
|
211
|
+
count++;
|
|
212
|
+
if (count >= maxPages) break;
|
|
213
|
+
signal?.throwIfAborted();
|
|
214
|
+
page = await page.$next(signal !== void 0 ? { signal } : void 0);
|
|
215
|
+
}
|
|
216
|
+
return all;
|
|
217
|
+
}
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region src/resource.ts
|
|
220
|
+
function jsonApiResource(options) {
|
|
221
|
+
if (options.injector === void 0) (0, _angular_core.assertInInjectionContext)(jsonApiResource);
|
|
222
|
+
const injector = options.injector ?? (0, _angular_core.inject)(_angular_core.Injector);
|
|
223
|
+
const client = injector.get(options.client ?? JSON_API_CLIENT);
|
|
224
|
+
const ref = (0, _angular_core.resource)({
|
|
225
|
+
...options.params !== void 0 ? { params: options.params } : {},
|
|
226
|
+
loader: ({ params, abortSignal, previous }) => options.loader({
|
|
227
|
+
client,
|
|
228
|
+
params,
|
|
229
|
+
abortSignal,
|
|
230
|
+
previous
|
|
231
|
+
}),
|
|
232
|
+
...options.defaultValue !== void 0 ? { defaultValue: options.defaultValue } : {},
|
|
233
|
+
...options.equal !== void 0 ? { equal: options.equal } : {},
|
|
234
|
+
...options.debugName !== void 0 ? { debugName: options.debugName } : {},
|
|
235
|
+
injector
|
|
236
|
+
});
|
|
237
|
+
const jsonApiError = (0, _angular_core.computed)(() => {
|
|
238
|
+
const error = ref.error();
|
|
239
|
+
return error instanceof _haddowg_json_api_client.JsonApiError ? error : void 0;
|
|
240
|
+
});
|
|
241
|
+
Object.defineProperty(ref, "jsonApiError", {
|
|
242
|
+
value: jsonApiError,
|
|
243
|
+
enumerable: true
|
|
244
|
+
});
|
|
245
|
+
return ref;
|
|
246
|
+
}
|
|
247
|
+
//#endregion
|
|
248
|
+
exports.JSON_API_CLIENT = JSON_API_CLIENT;
|
|
249
|
+
exports.createJsonApiClientToken = createJsonApiClientToken;
|
|
250
|
+
exports.httpClientTransport = httpClientTransport;
|
|
251
|
+
exports.injectJsonApiClient = injectJsonApiClient;
|
|
252
|
+
exports.jsonApiMutation = jsonApiMutation;
|
|
253
|
+
exports.jsonApiResource = jsonApiResource;
|
|
254
|
+
exports.loadAll = loadAll;
|
|
255
|
+
exports.nextPage = nextPage;
|
|
256
|
+
exports.prevPage = prevPage;
|
|
257
|
+
exports.provideJsonApiClient = provideJsonApiClient;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
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";
|
|
3
|
+
import { HttpClient } from "@angular/common/http";
|
|
4
|
+
|
|
5
|
+
//#region src/provide.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Options for {@link provideJsonApiClient}: the client's options with the transport made
|
|
8
|
+
* optional. Leave `transport` out to send through `HttpClient`; set it only to opt out.
|
|
9
|
+
*/
|
|
10
|
+
type JsonApiClientOptions = Omit<ClientOptions, 'transport'> & {
|
|
11
|
+
transport?: JsonApiTransport;
|
|
12
|
+
};
|
|
13
|
+
/** A generated client factory: the bound `createClient` a codegen module exports. */
|
|
14
|
+
type JsonApiClientFactory<C> = (options: ClientOptions) => C;
|
|
15
|
+
/** The client type a generated `createClient` returns. */
|
|
16
|
+
type JsonApiClientOf<F> = F extends ((options: ClientOptions) => infer C) ? C : never;
|
|
17
|
+
/**
|
|
18
|
+
* Register the app's default client type once, so `injectJsonApiClient()` and `jsonApiResource()`
|
|
19
|
+
* are typed without a token:
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* declare module '@haddowg/json-api-angular' {
|
|
23
|
+
* interface Register {
|
|
24
|
+
* client: JsonApiClientOf<typeof createClient>
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
interface Register {}
|
|
30
|
+
/** The default client's type: whatever {@link Register} declares, else `unknown`. */
|
|
31
|
+
type RegisteredClient = Register extends {
|
|
32
|
+
client: infer C;
|
|
33
|
+
} ? C : unknown;
|
|
34
|
+
/** The token {@link provideJsonApiClient} uses when none is given. */
|
|
35
|
+
declare const JSON_API_CLIENT: InjectionToken<unknown>;
|
|
36
|
+
/**
|
|
37
|
+
* Create an injection token for one client, so an app can provide several (one per server).
|
|
38
|
+
* Type it with the client: `createJsonApiClientToken<JsonApiClientOf<typeof createAdminClient>>()`.
|
|
39
|
+
*/
|
|
40
|
+
declare function createJsonApiClientToken<C>(description?: string): InjectionToken<C>;
|
|
41
|
+
/**
|
|
42
|
+
* Provide a generated client to the environment injector. The client sends through `HttpClient`
|
|
43
|
+
* (so `provideHttpClient()` must also be present) unless `options.transport` overrides it.
|
|
44
|
+
*
|
|
45
|
+
* `options` may be a function, which runs in an injection context, to read values such as the
|
|
46
|
+
* base URL from other providers. `token` defaults to {@link JSON_API_CLIENT}; pass one from
|
|
47
|
+
* {@link createJsonApiClientToken} to provide a second client alongside it.
|
|
48
|
+
*/
|
|
49
|
+
declare function provideJsonApiClient<C>(createClient: JsonApiClientFactory<C>, options: JsonApiClientOptions | (() => JsonApiClientOptions), token?: InjectionToken<C>): EnvironmentProviders;
|
|
50
|
+
/**
|
|
51
|
+
* Inject a provided client. Without a token this returns the default client, typed by
|
|
52
|
+
* {@link Register}. With a token it returns that token's client.
|
|
53
|
+
*/
|
|
54
|
+
declare function injectJsonApiClient<C = RegisteredClient>(token?: InjectionToken<C>, options?: {
|
|
55
|
+
injector?: Injector;
|
|
56
|
+
}): C;
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/resource.d.ts
|
|
59
|
+
/**
|
|
60
|
+
* What a read can fail with: a `JsonApiError` for a non-2xx response, a `StructuralGuardError`
|
|
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`.
|
|
63
|
+
*/
|
|
64
|
+
type JsonApiResourceError = JsonApiError | StructuralGuardError | Error;
|
|
65
|
+
/** What the loader receives each time the params change or the resource reloads. */
|
|
66
|
+
interface JsonApiLoaderContext<C, P> {
|
|
67
|
+
client: C;
|
|
68
|
+
params: P;
|
|
69
|
+
/** Aborts when the params change again or the resource is destroyed. Pass it to the read. */
|
|
70
|
+
abortSignal: AbortSignal;
|
|
71
|
+
previous: {
|
|
72
|
+
status: ResourceStatus;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Options for {@link jsonApiResource}. */
|
|
76
|
+
interface JsonApiResourceOptions<T, P, C> {
|
|
77
|
+
/**
|
|
78
|
+
* A reactive function computing the read's params from signals. Return `undefined` while the
|
|
79
|
+
* read isn't ready (no id yet, say) and the resource stays idle. Leave it out to load once.
|
|
80
|
+
*/
|
|
81
|
+
params?: () => P | undefined;
|
|
82
|
+
/** Make the read. Return the client call's promise, passing `abortSignal` as its `signal`. */
|
|
83
|
+
loader: (context: JsonApiLoaderContext<C, P>) => PromiseLike<T>;
|
|
84
|
+
/** The client to read with. Defaults to the one `provideJsonApiClient` registered without a token. */
|
|
85
|
+
client?: InjectionToken<C>;
|
|
86
|
+
/** The value while nothing has loaded yet. */
|
|
87
|
+
defaultValue?: T;
|
|
88
|
+
equal?: ValueEqualityFn<T>;
|
|
89
|
+
/** Create the resource outside an injection context. */
|
|
90
|
+
injector?: Injector;
|
|
91
|
+
debugName?: string;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Angular's `ResourceRef` with `error()` typed to what a JSON:API read throws, plus
|
|
95
|
+
* `jsonApiError()`, the error when it is a `JsonApiError` (handy in templates, which can't
|
|
96
|
+
* use `instanceof`).
|
|
97
|
+
*/
|
|
98
|
+
interface JsonApiResourceRef<T> extends Omit<ResourceRef<T>, 'error' | 'hasValue'> {
|
|
99
|
+
readonly error: Signal<JsonApiResourceError | undefined>;
|
|
100
|
+
readonly jsonApiError: Signal<JsonApiError | undefined>;
|
|
101
|
+
/** 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(): boolean;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* A signal-based JSON:API read: a thin wrapper over Angular's `resource()` that injects the
|
|
107
|
+
* client and hands it to the loader. Changing a signal the params read starts a new load and
|
|
108
|
+
* aborts the one in flight.
|
|
109
|
+
*
|
|
110
|
+
* ```ts
|
|
111
|
+
* readonly albums = jsonApiResource({
|
|
112
|
+
* params: () => ({ filter: { title: this.search() } }),
|
|
113
|
+
* loader: ({ client, params, abortSignal }) =>
|
|
114
|
+
* client.albums.list({ ...params, include: ['artist'], signal: abortSignal }),
|
|
115
|
+
* })
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
declare function jsonApiResource<T, P = unknown, C = RegisteredClient>(options: JsonApiResourceOptions<T, P, C> & {
|
|
119
|
+
defaultValue: T;
|
|
120
|
+
}): JsonApiResourceRef<T>;
|
|
121
|
+
declare function jsonApiResource<T, P = unknown, C = RegisteredClient>(options: JsonApiResourceOptions<T, P, C>): JsonApiResourceRef<T | undefined>;
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/mutation.d.ts
|
|
124
|
+
/** Options for {@link jsonApiMutation}. */
|
|
125
|
+
interface JsonApiMutationOptions<C> {
|
|
126
|
+
/** The client to write with. Defaults to the one `provideJsonApiClient` registered without a token. */
|
|
127
|
+
client?: InjectionToken<C>;
|
|
128
|
+
/** Create the mutation outside an injection context. */
|
|
129
|
+
injector?: Injector;
|
|
130
|
+
}
|
|
131
|
+
/** The signal-based handle {@link jsonApiMutation} returns. */
|
|
132
|
+
interface JsonApiMutationRef<I, T> {
|
|
133
|
+
/**
|
|
134
|
+
* Run the write. Resolves with its result and rejects with its error; the signals track the
|
|
135
|
+
* latest call either way.
|
|
136
|
+
*/
|
|
137
|
+
mutate(input: I): Promise<T>;
|
|
138
|
+
readonly isPending: Signal<boolean>;
|
|
139
|
+
readonly error: Signal<JsonApiResourceError | undefined>;
|
|
140
|
+
/** The error when it is a `JsonApiError`; use `byPath()` on it for form errors. */
|
|
141
|
+
readonly jsonApiError: Signal<JsonApiError | undefined>;
|
|
142
|
+
/** The latest successful result. */
|
|
143
|
+
readonly value: Signal<T | undefined>;
|
|
144
|
+
/** Clear the value and error. A call still in flight no longer updates the signals. */
|
|
145
|
+
reset(): void;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Wrap a client write (create, update, delete, a relationship mutation, a custom action or an
|
|
149
|
+
* atomic batch) in signals. Nothing is cached or invalidated: `reload()` the resources the write
|
|
150
|
+
* affects.
|
|
151
|
+
*
|
|
152
|
+
* ```ts
|
|
153
|
+
* readonly rename = jsonApiMutation((client, title: string) =>
|
|
154
|
+
* client.albums.id(this.id()).update({ title }),
|
|
155
|
+
* )
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
declare function jsonApiMutation<I, T, C = RegisteredClient>(mutation: (client: C, input: I) => PromiseLike<T>, options?: JsonApiMutationOptions<C>): JsonApiMutationRef<I, T>;
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/pagination.d.ts
|
|
161
|
+
/** The `page` query a link selects: its `page[...]` parameters, keyed without the prefix. */
|
|
162
|
+
type PageParams = Record<string, string>;
|
|
163
|
+
/**
|
|
164
|
+
* 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 page, offset and cursor
|
|
166
|
+
* pagination, because the server's link already carries the right parameters. Feed the result
|
|
167
|
+
* back into the signal your resource's params read:
|
|
168
|
+
*
|
|
169
|
+
* ```ts
|
|
170
|
+
* const next = nextPage(this.albums.value())
|
|
171
|
+
* if (next) this.page.set(next)
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
declare function nextPage(list: {
|
|
175
|
+
readonly $page: Page;
|
|
176
|
+
} | undefined): PageParams | undefined;
|
|
177
|
+
/** The `page` params for the page before `list`, from its `prev` link. See {@link nextPage}. */
|
|
178
|
+
declare function prevPage(list: {
|
|
179
|
+
readonly $page: Page;
|
|
180
|
+
} | undefined): PageParams | undefined;
|
|
181
|
+
/** Options for {@link loadAll}. */
|
|
182
|
+
interface LoadAllOptions {
|
|
183
|
+
/** Stops the walk, rejecting with the signal's reason. Pass the loader's `abortSignal`. */
|
|
184
|
+
signal?: AbortSignal;
|
|
185
|
+
/** Stop after this many pages, as a guard against an unexpectedly large collection. */
|
|
186
|
+
maxPages?: number;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Follow `next` links from the first page until the last, and return every member in one plain
|
|
190
|
+
* array. Use it inside a loader to read a whole (small) collection:
|
|
191
|
+
*
|
|
192
|
+
* ```ts
|
|
193
|
+
* loader: ({ client, abortSignal }) =>
|
|
194
|
+
* loadAll(client.genres.list({ signal: abortSignal }), { signal: abortSignal })
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
declare function loadAll<T>(first: Collection<T> | PromiseLike<Collection<T>>, options?: LoadAllOptions): Promise<T[]>;
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/transport.d.ts
|
|
200
|
+
/**
|
|
201
|
+
* A transport that sends every request through Angular's `HttpClient`, so the app's interceptors
|
|
202
|
+
* run. The body comes back as text for the client to parse, exactly as with `fetchTransport`.
|
|
203
|
+
*
|
|
204
|
+
* A non-2xx response resolves with its status, headers and body, because the client maps those to
|
|
205
|
+
* a `JsonApiError` itself. Only a request that never got a response (status 0: network failure,
|
|
206
|
+
* CORS, a blocked request) rejects, with the `HttpErrorResponse`. Aborting `req.signal`
|
|
207
|
+
* unsubscribes, which cancels the HTTP request, and rejects with the signal's reason.
|
|
208
|
+
*/
|
|
209
|
+
declare function httpClientTransport(http: HttpClient): JsonApiTransport;
|
|
210
|
+
//#endregion
|
|
211
|
+
export { JSON_API_CLIENT, JsonApiClientFactory, JsonApiClientOf, JsonApiClientOptions, JsonApiLoaderContext, JsonApiMutationOptions, JsonApiMutationRef, JsonApiResourceError, JsonApiResourceOptions, JsonApiResourceRef, LoadAllOptions, PageParams, Register, RegisteredClient, createJsonApiClientToken, httpClientTransport, injectJsonApiClient, jsonApiMutation, jsonApiResource, loadAll, nextPage, prevPage, provideJsonApiClient };
|
|
212
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +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":"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"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
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";
|
|
3
|
+
import { HttpClient } from "@angular/common/http";
|
|
4
|
+
|
|
5
|
+
//#region src/provide.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Options for {@link provideJsonApiClient}: the client's options with the transport made
|
|
8
|
+
* optional. Leave `transport` out to send through `HttpClient`; set it only to opt out.
|
|
9
|
+
*/
|
|
10
|
+
type JsonApiClientOptions = Omit<ClientOptions, 'transport'> & {
|
|
11
|
+
transport?: JsonApiTransport;
|
|
12
|
+
};
|
|
13
|
+
/** A generated client factory: the bound `createClient` a codegen module exports. */
|
|
14
|
+
type JsonApiClientFactory<C> = (options: ClientOptions) => C;
|
|
15
|
+
/** The client type a generated `createClient` returns. */
|
|
16
|
+
type JsonApiClientOf<F> = F extends ((options: ClientOptions) => infer C) ? C : never;
|
|
17
|
+
/**
|
|
18
|
+
* Register the app's default client type once, so `injectJsonApiClient()` and `jsonApiResource()`
|
|
19
|
+
* are typed without a token:
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* declare module '@haddowg/json-api-angular' {
|
|
23
|
+
* interface Register {
|
|
24
|
+
* client: JsonApiClientOf<typeof createClient>
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
interface Register {}
|
|
30
|
+
/** The default client's type: whatever {@link Register} declares, else `unknown`. */
|
|
31
|
+
type RegisteredClient = Register extends {
|
|
32
|
+
client: infer C;
|
|
33
|
+
} ? C : unknown;
|
|
34
|
+
/** The token {@link provideJsonApiClient} uses when none is given. */
|
|
35
|
+
declare const JSON_API_CLIENT: InjectionToken<unknown>;
|
|
36
|
+
/**
|
|
37
|
+
* Create an injection token for one client, so an app can provide several (one per server).
|
|
38
|
+
* Type it with the client: `createJsonApiClientToken<JsonApiClientOf<typeof createAdminClient>>()`.
|
|
39
|
+
*/
|
|
40
|
+
declare function createJsonApiClientToken<C>(description?: string): InjectionToken<C>;
|
|
41
|
+
/**
|
|
42
|
+
* Provide a generated client to the environment injector. The client sends through `HttpClient`
|
|
43
|
+
* (so `provideHttpClient()` must also be present) unless `options.transport` overrides it.
|
|
44
|
+
*
|
|
45
|
+
* `options` may be a function, which runs in an injection context, to read values such as the
|
|
46
|
+
* base URL from other providers. `token` defaults to {@link JSON_API_CLIENT}; pass one from
|
|
47
|
+
* {@link createJsonApiClientToken} to provide a second client alongside it.
|
|
48
|
+
*/
|
|
49
|
+
declare function provideJsonApiClient<C>(createClient: JsonApiClientFactory<C>, options: JsonApiClientOptions | (() => JsonApiClientOptions), token?: InjectionToken<C>): EnvironmentProviders;
|
|
50
|
+
/**
|
|
51
|
+
* Inject a provided client. Without a token this returns the default client, typed by
|
|
52
|
+
* {@link Register}. With a token it returns that token's client.
|
|
53
|
+
*/
|
|
54
|
+
declare function injectJsonApiClient<C = RegisteredClient>(token?: InjectionToken<C>, options?: {
|
|
55
|
+
injector?: Injector;
|
|
56
|
+
}): C;
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/resource.d.ts
|
|
59
|
+
/**
|
|
60
|
+
* What a read can fail with: a `JsonApiError` for a non-2xx response, a `StructuralGuardError`
|
|
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`.
|
|
63
|
+
*/
|
|
64
|
+
type JsonApiResourceError = JsonApiError | StructuralGuardError | Error;
|
|
65
|
+
/** What the loader receives each time the params change or the resource reloads. */
|
|
66
|
+
interface JsonApiLoaderContext<C, P> {
|
|
67
|
+
client: C;
|
|
68
|
+
params: P;
|
|
69
|
+
/** Aborts when the params change again or the resource is destroyed. Pass it to the read. */
|
|
70
|
+
abortSignal: AbortSignal;
|
|
71
|
+
previous: {
|
|
72
|
+
status: ResourceStatus;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Options for {@link jsonApiResource}. */
|
|
76
|
+
interface JsonApiResourceOptions<T, P, C> {
|
|
77
|
+
/**
|
|
78
|
+
* A reactive function computing the read's params from signals. Return `undefined` while the
|
|
79
|
+
* read isn't ready (no id yet, say) and the resource stays idle. Leave it out to load once.
|
|
80
|
+
*/
|
|
81
|
+
params?: () => P | undefined;
|
|
82
|
+
/** Make the read. Return the client call's promise, passing `abortSignal` as its `signal`. */
|
|
83
|
+
loader: (context: JsonApiLoaderContext<C, P>) => PromiseLike<T>;
|
|
84
|
+
/** The client to read with. Defaults to the one `provideJsonApiClient` registered without a token. */
|
|
85
|
+
client?: InjectionToken<C>;
|
|
86
|
+
/** The value while nothing has loaded yet. */
|
|
87
|
+
defaultValue?: T;
|
|
88
|
+
equal?: ValueEqualityFn<T>;
|
|
89
|
+
/** Create the resource outside an injection context. */
|
|
90
|
+
injector?: Injector;
|
|
91
|
+
debugName?: string;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Angular's `ResourceRef` with `error()` typed to what a JSON:API read throws, plus
|
|
95
|
+
* `jsonApiError()`, the error when it is a `JsonApiError` (handy in templates, which can't
|
|
96
|
+
* use `instanceof`).
|
|
97
|
+
*/
|
|
98
|
+
interface JsonApiResourceRef<T> extends Omit<ResourceRef<T>, 'error' | 'hasValue'> {
|
|
99
|
+
readonly error: Signal<JsonApiResourceError | undefined>;
|
|
100
|
+
readonly jsonApiError: Signal<JsonApiError | undefined>;
|
|
101
|
+
/** 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(): boolean;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* A signal-based JSON:API read: a thin wrapper over Angular's `resource()` that injects the
|
|
107
|
+
* client and hands it to the loader. Changing a signal the params read starts a new load and
|
|
108
|
+
* aborts the one in flight.
|
|
109
|
+
*
|
|
110
|
+
* ```ts
|
|
111
|
+
* readonly albums = jsonApiResource({
|
|
112
|
+
* params: () => ({ filter: { title: this.search() } }),
|
|
113
|
+
* loader: ({ client, params, abortSignal }) =>
|
|
114
|
+
* client.albums.list({ ...params, include: ['artist'], signal: abortSignal }),
|
|
115
|
+
* })
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
declare function jsonApiResource<T, P = unknown, C = RegisteredClient>(options: JsonApiResourceOptions<T, P, C> & {
|
|
119
|
+
defaultValue: T;
|
|
120
|
+
}): JsonApiResourceRef<T>;
|
|
121
|
+
declare function jsonApiResource<T, P = unknown, C = RegisteredClient>(options: JsonApiResourceOptions<T, P, C>): JsonApiResourceRef<T | undefined>;
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/mutation.d.ts
|
|
124
|
+
/** Options for {@link jsonApiMutation}. */
|
|
125
|
+
interface JsonApiMutationOptions<C> {
|
|
126
|
+
/** The client to write with. Defaults to the one `provideJsonApiClient` registered without a token. */
|
|
127
|
+
client?: InjectionToken<C>;
|
|
128
|
+
/** Create the mutation outside an injection context. */
|
|
129
|
+
injector?: Injector;
|
|
130
|
+
}
|
|
131
|
+
/** The signal-based handle {@link jsonApiMutation} returns. */
|
|
132
|
+
interface JsonApiMutationRef<I, T> {
|
|
133
|
+
/**
|
|
134
|
+
* Run the write. Resolves with its result and rejects with its error; the signals track the
|
|
135
|
+
* latest call either way.
|
|
136
|
+
*/
|
|
137
|
+
mutate(input: I): Promise<T>;
|
|
138
|
+
readonly isPending: Signal<boolean>;
|
|
139
|
+
readonly error: Signal<JsonApiResourceError | undefined>;
|
|
140
|
+
/** The error when it is a `JsonApiError`; use `byPath()` on it for form errors. */
|
|
141
|
+
readonly jsonApiError: Signal<JsonApiError | undefined>;
|
|
142
|
+
/** The latest successful result. */
|
|
143
|
+
readonly value: Signal<T | undefined>;
|
|
144
|
+
/** Clear the value and error. A call still in flight no longer updates the signals. */
|
|
145
|
+
reset(): void;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Wrap a client write (create, update, delete, a relationship mutation, a custom action or an
|
|
149
|
+
* atomic batch) in signals. Nothing is cached or invalidated: `reload()` the resources the write
|
|
150
|
+
* affects.
|
|
151
|
+
*
|
|
152
|
+
* ```ts
|
|
153
|
+
* readonly rename = jsonApiMutation((client, title: string) =>
|
|
154
|
+
* client.albums.id(this.id()).update({ title }),
|
|
155
|
+
* )
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
declare function jsonApiMutation<I, T, C = RegisteredClient>(mutation: (client: C, input: I) => PromiseLike<T>, options?: JsonApiMutationOptions<C>): JsonApiMutationRef<I, T>;
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/pagination.d.ts
|
|
161
|
+
/** The `page` query a link selects: its `page[...]` parameters, keyed without the prefix. */
|
|
162
|
+
type PageParams = Record<string, string>;
|
|
163
|
+
/**
|
|
164
|
+
* 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 page, offset and cursor
|
|
166
|
+
* pagination, because the server's link already carries the right parameters. Feed the result
|
|
167
|
+
* back into the signal your resource's params read:
|
|
168
|
+
*
|
|
169
|
+
* ```ts
|
|
170
|
+
* const next = nextPage(this.albums.value())
|
|
171
|
+
* if (next) this.page.set(next)
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
declare function nextPage(list: {
|
|
175
|
+
readonly $page: Page;
|
|
176
|
+
} | undefined): PageParams | undefined;
|
|
177
|
+
/** The `page` params for the page before `list`, from its `prev` link. See {@link nextPage}. */
|
|
178
|
+
declare function prevPage(list: {
|
|
179
|
+
readonly $page: Page;
|
|
180
|
+
} | undefined): PageParams | undefined;
|
|
181
|
+
/** Options for {@link loadAll}. */
|
|
182
|
+
interface LoadAllOptions {
|
|
183
|
+
/** Stops the walk, rejecting with the signal's reason. Pass the loader's `abortSignal`. */
|
|
184
|
+
signal?: AbortSignal;
|
|
185
|
+
/** Stop after this many pages, as a guard against an unexpectedly large collection. */
|
|
186
|
+
maxPages?: number;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Follow `next` links from the first page until the last, and return every member in one plain
|
|
190
|
+
* array. Use it inside a loader to read a whole (small) collection:
|
|
191
|
+
*
|
|
192
|
+
* ```ts
|
|
193
|
+
* loader: ({ client, abortSignal }) =>
|
|
194
|
+
* loadAll(client.genres.list({ signal: abortSignal }), { signal: abortSignal })
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
declare function loadAll<T>(first: Collection<T> | PromiseLike<Collection<T>>, options?: LoadAllOptions): Promise<T[]>;
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/transport.d.ts
|
|
200
|
+
/**
|
|
201
|
+
* A transport that sends every request through Angular's `HttpClient`, so the app's interceptors
|
|
202
|
+
* run. The body comes back as text for the client to parse, exactly as with `fetchTransport`.
|
|
203
|
+
*
|
|
204
|
+
* A non-2xx response resolves with its status, headers and body, because the client maps those to
|
|
205
|
+
* a `JsonApiError` itself. Only a request that never got a response (status 0: network failure,
|
|
206
|
+
* CORS, a blocked request) rejects, with the `HttpErrorResponse`. Aborting `req.signal`
|
|
207
|
+
* unsubscribes, which cancels the HTTP request, and rejects with the signal's reason.
|
|
208
|
+
*/
|
|
209
|
+
declare function httpClientTransport(http: HttpClient): JsonApiTransport;
|
|
210
|
+
//#endregion
|
|
211
|
+
export { JSON_API_CLIENT, JsonApiClientFactory, JsonApiClientOf, JsonApiClientOptions, JsonApiLoaderContext, JsonApiMutationOptions, JsonApiMutationRef, JsonApiResourceError, JsonApiResourceOptions, JsonApiResourceRef, LoadAllOptions, PageParams, Register, RegisteredClient, createJsonApiClientToken, httpClientTransport, injectJsonApiClient, jsonApiMutation, jsonApiResource, loadAll, nextPage, prevPage, provideJsonApiClient };
|
|
212
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +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":"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"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { InjectionToken, Injector, assertInInjectionContext, computed, inject, makeEnvironmentProviders, resource, signal } from "@angular/core";
|
|
2
|
+
import { JsonApiError } from "@haddowg/json-api-client";
|
|
3
|
+
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
|
|
4
|
+
//#region src/transport.ts
|
|
5
|
+
/**
|
|
6
|
+
* A transport that sends every request through Angular's `HttpClient`, so the app's interceptors
|
|
7
|
+
* run. The body comes back as text for the client to parse, exactly as with `fetchTransport`.
|
|
8
|
+
*
|
|
9
|
+
* A non-2xx response resolves with its status, headers and body, because the client maps those to
|
|
10
|
+
* a `JsonApiError` itself. Only a request that never got a response (status 0: network failure,
|
|
11
|
+
* CORS, a blocked request) rejects, with the `HttpErrorResponse`. Aborting `req.signal`
|
|
12
|
+
* unsubscribes, which cancels the HTTP request, and rejects with the signal's reason.
|
|
13
|
+
*/
|
|
14
|
+
function httpClientTransport(http) {
|
|
15
|
+
return (req) => new Promise((resolve, reject) => {
|
|
16
|
+
const signal = req.signal;
|
|
17
|
+
if (signal?.aborted) {
|
|
18
|
+
reject(signal.reason);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
let unsubscribe;
|
|
22
|
+
const onAbort = () => {
|
|
23
|
+
unsubscribe?.();
|
|
24
|
+
reject(signal?.reason);
|
|
25
|
+
};
|
|
26
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
27
|
+
const settle = () => signal?.removeEventListener("abort", onAbort);
|
|
28
|
+
const subscription = http.request(req.method, req.url, {
|
|
29
|
+
body: req.body ?? null,
|
|
30
|
+
headers: req.headers,
|
|
31
|
+
observe: "response",
|
|
32
|
+
responseType: "text"
|
|
33
|
+
}).subscribe({
|
|
34
|
+
next: (res) => {
|
|
35
|
+
settle();
|
|
36
|
+
resolve({
|
|
37
|
+
status: res.status,
|
|
38
|
+
headers: toRecord(res.headers),
|
|
39
|
+
body: res.body ?? ""
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
error: (error) => {
|
|
43
|
+
settle();
|
|
44
|
+
if (error instanceof HttpErrorResponse && error.status !== 0) {
|
|
45
|
+
resolve({
|
|
46
|
+
status: error.status,
|
|
47
|
+
headers: toRecord(error.headers),
|
|
48
|
+
body: errorBody(error.error)
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
reject(error);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
unsubscribe = () => subscription.unsubscribe();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function toRecord(headers) {
|
|
59
|
+
const out = {};
|
|
60
|
+
for (const name of headers.keys()) {
|
|
61
|
+
const values = headers.getAll(name);
|
|
62
|
+
if (values !== null) out[name.toLowerCase()] = values.join(", ");
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
function errorBody(body) {
|
|
67
|
+
if (typeof body === "string") return body;
|
|
68
|
+
if (body === null || body === void 0) return "";
|
|
69
|
+
return JSON.stringify(body);
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/provide.ts
|
|
73
|
+
/** The token {@link provideJsonApiClient} uses when none is given. */
|
|
74
|
+
const JSON_API_CLIENT = new InjectionToken("JSON_API_CLIENT");
|
|
75
|
+
/**
|
|
76
|
+
* Create an injection token for one client, so an app can provide several (one per server).
|
|
77
|
+
* Type it with the client: `createJsonApiClientToken<JsonApiClientOf<typeof createAdminClient>>()`.
|
|
78
|
+
*/
|
|
79
|
+
function createJsonApiClientToken(description = "JsonApiClient") {
|
|
80
|
+
return new InjectionToken(description);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Provide a generated client to the environment injector. The client sends through `HttpClient`
|
|
84
|
+
* (so `provideHttpClient()` must also be present) unless `options.transport` overrides it.
|
|
85
|
+
*
|
|
86
|
+
* `options` may be a function, which runs in an injection context, to read values such as the
|
|
87
|
+
* base URL from other providers. `token` defaults to {@link JSON_API_CLIENT}; pass one from
|
|
88
|
+
* {@link createJsonApiClientToken} to provide a second client alongside it.
|
|
89
|
+
*/
|
|
90
|
+
function provideJsonApiClient(createClient, options, token = JSON_API_CLIENT) {
|
|
91
|
+
return makeEnvironmentProviders([{
|
|
92
|
+
provide: token,
|
|
93
|
+
useFactory: () => {
|
|
94
|
+
const resolved = typeof options === "function" ? options() : options;
|
|
95
|
+
return createClient({
|
|
96
|
+
...resolved,
|
|
97
|
+
transport: resolved.transport ?? httpClientTransport(inject(HttpClient))
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}]);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Inject a provided client. Without a token this returns the default client, typed by
|
|
104
|
+
* {@link Register}. With a token it returns that token's client.
|
|
105
|
+
*/
|
|
106
|
+
function injectJsonApiClient(token, options) {
|
|
107
|
+
const resolved = token ?? JSON_API_CLIENT;
|
|
108
|
+
if (options?.injector !== void 0) return options.injector.get(resolved);
|
|
109
|
+
assertInInjectionContext(injectJsonApiClient);
|
|
110
|
+
return inject(resolved);
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/mutation.ts
|
|
114
|
+
/**
|
|
115
|
+
* Wrap a client write (create, update, delete, a relationship mutation, a custom action or an
|
|
116
|
+
* atomic batch) in signals. Nothing is cached or invalidated: `reload()` the resources the write
|
|
117
|
+
* affects.
|
|
118
|
+
*
|
|
119
|
+
* ```ts
|
|
120
|
+
* readonly rename = jsonApiMutation((client, title: string) =>
|
|
121
|
+
* client.albums.id(this.id()).update({ title }),
|
|
122
|
+
* )
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
function jsonApiMutation(mutation, options = {}) {
|
|
126
|
+
if (options.injector === void 0) assertInInjectionContext(jsonApiMutation);
|
|
127
|
+
const client = (options.injector ?? inject(Injector)).get(options.client ?? JSON_API_CLIENT);
|
|
128
|
+
const value = signal(void 0);
|
|
129
|
+
const error = signal(void 0);
|
|
130
|
+
const pending = signal(0);
|
|
131
|
+
let generation = 0;
|
|
132
|
+
const mutate = async (input) => {
|
|
133
|
+
const current = ++generation;
|
|
134
|
+
pending.update((n) => n + 1);
|
|
135
|
+
error.set(void 0);
|
|
136
|
+
try {
|
|
137
|
+
const result = await mutation(client, input);
|
|
138
|
+
if (current === generation) value.set(result);
|
|
139
|
+
return result;
|
|
140
|
+
} catch (thrown) {
|
|
141
|
+
if (current === generation) error.set(thrown instanceof Error ? thrown : new Error(String(thrown), { cause: thrown }));
|
|
142
|
+
throw thrown;
|
|
143
|
+
} finally {
|
|
144
|
+
pending.update((n) => Math.max(0, n - 1));
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
mutate,
|
|
149
|
+
isPending: computed(() => pending() > 0),
|
|
150
|
+
error: error.asReadonly(),
|
|
151
|
+
jsonApiError: computed(() => {
|
|
152
|
+
const e = error();
|
|
153
|
+
return e instanceof JsonApiError ? e : void 0;
|
|
154
|
+
}),
|
|
155
|
+
value: value.asReadonly(),
|
|
156
|
+
reset: () => {
|
|
157
|
+
generation++;
|
|
158
|
+
value.set(void 0);
|
|
159
|
+
error.set(void 0);
|
|
160
|
+
pending.set(0);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/pagination.ts
|
|
166
|
+
/**
|
|
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 page, offset and cursor
|
|
169
|
+
* pagination, because the server's link already carries the right parameters. Feed the result
|
|
170
|
+
* back into the signal your resource's params read:
|
|
171
|
+
*
|
|
172
|
+
* ```ts
|
|
173
|
+
* const next = nextPage(this.albums.value())
|
|
174
|
+
* if (next) this.page.set(next)
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
function nextPage(list) {
|
|
178
|
+
return pageParams(list?.$page.links.next);
|
|
179
|
+
}
|
|
180
|
+
/** The `page` params for the page before `list`, from its `prev` link. See {@link nextPage}. */
|
|
181
|
+
function prevPage(list) {
|
|
182
|
+
return pageParams(list?.$page.links.prev);
|
|
183
|
+
}
|
|
184
|
+
function pageParams(link) {
|
|
185
|
+
if (link === void 0) return;
|
|
186
|
+
const query = link.includes("?") ? link.slice(link.indexOf("?") + 1).split("#")[0] : "";
|
|
187
|
+
const params = {};
|
|
188
|
+
for (const [key, value] of new URLSearchParams(query)) {
|
|
189
|
+
const match = /^page\[([^\]]+)\]$/.exec(key);
|
|
190
|
+
if (match?.[1] !== void 0) params[match[1]] = value;
|
|
191
|
+
}
|
|
192
|
+
return params;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Follow `next` links from the first page until the last, and return every member in one plain
|
|
196
|
+
* array. Use it inside a loader to read a whole (small) collection:
|
|
197
|
+
*
|
|
198
|
+
* ```ts
|
|
199
|
+
* loader: ({ client, abortSignal }) =>
|
|
200
|
+
* loadAll(client.genres.list({ signal: abortSignal }), { signal: abortSignal })
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
async function loadAll(first, options = {}) {
|
|
204
|
+
const { signal, maxPages = Number.POSITIVE_INFINITY } = options;
|
|
205
|
+
let page = await first;
|
|
206
|
+
const all = [];
|
|
207
|
+
let count = 0;
|
|
208
|
+
while (page !== void 0 && count < maxPages) {
|
|
209
|
+
all.push(...page);
|
|
210
|
+
count++;
|
|
211
|
+
if (count >= maxPages) break;
|
|
212
|
+
signal?.throwIfAborted();
|
|
213
|
+
page = await page.$next(signal !== void 0 ? { signal } : void 0);
|
|
214
|
+
}
|
|
215
|
+
return all;
|
|
216
|
+
}
|
|
217
|
+
//#endregion
|
|
218
|
+
//#region src/resource.ts
|
|
219
|
+
function jsonApiResource(options) {
|
|
220
|
+
if (options.injector === void 0) assertInInjectionContext(jsonApiResource);
|
|
221
|
+
const injector = options.injector ?? inject(Injector);
|
|
222
|
+
const client = injector.get(options.client ?? JSON_API_CLIENT);
|
|
223
|
+
const ref = resource({
|
|
224
|
+
...options.params !== void 0 ? { params: options.params } : {},
|
|
225
|
+
loader: ({ params, abortSignal, previous }) => options.loader({
|
|
226
|
+
client,
|
|
227
|
+
params,
|
|
228
|
+
abortSignal,
|
|
229
|
+
previous
|
|
230
|
+
}),
|
|
231
|
+
...options.defaultValue !== void 0 ? { defaultValue: options.defaultValue } : {},
|
|
232
|
+
...options.equal !== void 0 ? { equal: options.equal } : {},
|
|
233
|
+
...options.debugName !== void 0 ? { debugName: options.debugName } : {},
|
|
234
|
+
injector
|
|
235
|
+
});
|
|
236
|
+
const jsonApiError = computed(() => {
|
|
237
|
+
const error = ref.error();
|
|
238
|
+
return error instanceof JsonApiError ? error : void 0;
|
|
239
|
+
});
|
|
240
|
+
Object.defineProperty(ref, "jsonApiError", {
|
|
241
|
+
value: jsonApiError,
|
|
242
|
+
enumerable: true
|
|
243
|
+
});
|
|
244
|
+
return ref;
|
|
245
|
+
}
|
|
246
|
+
//#endregion
|
|
247
|
+
export { JSON_API_CLIENT, createJsonApiClientToken, httpClientTransport, injectJsonApiClient, jsonApiMutation, jsonApiResource, loadAll, nextPage, prevPage, provideJsonApiClient };
|
|
248
|
+
|
|
249
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,70 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haddowg/json-api-angular",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Signal-based Angular bindings for the JSON:API client: an HttpClient transport, DI providers, and resource/mutation helpers.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Gregory Haddow (https://github.com/haddowg)",
|
|
7
|
+
"homepage": "https://haddowg.github.io/json-api-ts/",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/haddowg/json-api-ts.git",
|
|
11
|
+
"directory": "packages/angular"
|
|
12
|
+
},
|
|
13
|
+
"bugs": "https://github.com/haddowg/json-api-ts/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"json-api",
|
|
16
|
+
"jsonapi",
|
|
17
|
+
"typescript",
|
|
18
|
+
"angular",
|
|
19
|
+
"signals",
|
|
20
|
+
"httpclient",
|
|
21
|
+
"openapi"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"import": {
|
|
28
|
+
"types": "./dist/index.d.mts",
|
|
29
|
+
"default": "./dist/index.mjs"
|
|
30
|
+
},
|
|
31
|
+
"require": {
|
|
32
|
+
"types": "./dist/index.d.cts",
|
|
33
|
+
"default": "./dist/index.cjs"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"main": "./dist/index.cjs",
|
|
38
|
+
"module": "./dist/index.mjs",
|
|
39
|
+
"types": "./dist/index.d.cts",
|
|
40
|
+
"files": [
|
|
41
|
+
"dist"
|
|
42
|
+
],
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=20"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public",
|
|
48
|
+
"provenance": true
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@haddowg/json-api-client": "^1.1.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@angular/common": "^20.0.0 || ^21.0.0 || ^22.0.0",
|
|
55
|
+
"@angular/core": "^20.0.0 || ^21.0.0 || ^22.0.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@angular/common": "^21.0.0",
|
|
59
|
+
"@angular/compiler": "^21.0.0",
|
|
60
|
+
"@angular/core": "^21.0.0",
|
|
61
|
+
"@angular/platform-browser": "^21.0.0",
|
|
62
|
+
"jsdom": "^25.0.0",
|
|
63
|
+
"rxjs": "^7.8.0"
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"build": "tsdown",
|
|
67
|
+
"typecheck": "tsc --noEmit",
|
|
68
|
+
"lint:publish": "publint --strict && attw --pack ."
|
|
69
|
+
}
|
|
6
70
|
}
|