@haddowg/json-api-angular 1.1.1 → 1.2.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/dist/index.cjs CHANGED
@@ -5,7 +5,8 @@ let _angular_common_http = require("@angular/common/http");
5
5
  //#region src/transport.ts
6
6
  /**
7
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`.
8
+ * run. The body comes back as text for the client to parse, exactly as with `fetchTransport`, or
9
+ * as binary when the request sets `responseType` for a custom action's binary response.
9
10
  *
10
11
  * A non-2xx response resolves with its status, headers and body, because the client maps those to
11
12
  * a `JsonApiError` itself. Only a request that never got a response (status 0: network failure,
@@ -26,8 +27,21 @@ function httpClientTransport(http) {
26
27
  };
27
28
  signal?.addEventListener("abort", onAbort, { once: true });
28
29
  const settle = () => signal?.removeEventListener("abort", onAbort);
29
- const subscription = http.request(req.method, req.url, {
30
- body: req.body ?? null,
30
+ const body = req.rawBody ?? req.body ?? null;
31
+ const error = (failure) => {
32
+ settle();
33
+ if (failure instanceof _angular_common_http.HttpErrorResponse && failure.status !== 0) {
34
+ errorBody(failure.error).then((text) => resolve({
35
+ status: failure.status,
36
+ headers: toRecord(failure.headers),
37
+ body: text
38
+ }), reject);
39
+ return;
40
+ }
41
+ reject(failure);
42
+ };
43
+ const subscription = req.responseType === void 0 ? http.request(req.method, req.url, {
44
+ body,
31
45
  headers: req.headers,
32
46
  observe: "response",
33
47
  responseType: "text"
@@ -40,18 +54,24 @@ function httpClientTransport(http) {
40
54
  body: res.body ?? ""
41
55
  });
42
56
  },
43
- error: (error) => {
57
+ error
58
+ }) : http.request(req.method, req.url, {
59
+ body,
60
+ headers: req.headers,
61
+ observe: "response",
62
+ responseType: req.responseType
63
+ }).subscribe({
64
+ next: (res) => {
44
65
  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
- }
66
+ const out = {
67
+ status: res.status,
68
+ headers: toRecord(res.headers),
69
+ body: ""
70
+ };
71
+ if (res.body !== null) out.binary = res.body;
72
+ resolve(out);
73
+ },
74
+ error
55
75
  });
56
76
  unsubscribe = () => subscription.unsubscribe();
57
77
  });
@@ -64,9 +84,11 @@ function toRecord(headers) {
64
84
  }
65
85
  return out;
66
86
  }
67
- function errorBody(body) {
87
+ async function errorBody(body) {
68
88
  if (typeof body === "string") return body;
69
89
  if (body === null || body === void 0) return "";
90
+ if (typeof Blob !== "undefined" && body instanceof Blob) return body.text();
91
+ if (body instanceof ArrayBuffer) return new TextDecoder().decode(body);
70
92
  return JSON.stringify(body);
71
93
  }
72
94
  //#endregion
package/dist/index.d.cts CHANGED
@@ -206,7 +206,8 @@ declare function loadAll<T>(first: Collection<T> | PromiseLike<Collection<T>>, o
206
206
  //#region src/transport.d.ts
207
207
  /**
208
208
  * A transport that sends every request through Angular's `HttpClient`, so the app's interceptors
209
- * run. The body comes back as text for the client to parse, exactly as with `fetchTransport`.
209
+ * run. The body comes back as text for the client to parse, exactly as with `fetchTransport`, or
210
+ * as binary when the request sets `responseType` for a custom action's binary response.
210
211
  *
211
212
  * A non-2xx response resolves with its status, headers and body, because the client maps those to
212
213
  * a `JsonApiError` itself. Only a request that never got a response (status 0: network failure,
@@ -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":"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"}
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
@@ -206,7 +206,8 @@ declare function loadAll<T>(first: Collection<T> | PromiseLike<Collection<T>>, o
206
206
  //#region src/transport.d.ts
207
207
  /**
208
208
  * A transport that sends every request through Angular's `HttpClient`, so the app's interceptors
209
- * run. The body comes back as text for the client to parse, exactly as with `fetchTransport`.
209
+ * run. The body comes back as text for the client to parse, exactly as with `fetchTransport`, or
210
+ * as binary when the request sets `responseType` for a custom action's binary response.
210
211
  *
211
212
  * A non-2xx response resolves with its status, headers and body, because the client maps those to
212
213
  * a `JsonApiError` itself. Only a request that never got a response (status 0: network failure,
@@ -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":"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"}
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
@@ -4,7 +4,8 @@ import { HttpClient, HttpErrorResponse } from "@angular/common/http";
4
4
  //#region src/transport.ts
5
5
  /**
6
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`.
7
+ * run. The body comes back as text for the client to parse, exactly as with `fetchTransport`, or
8
+ * as binary when the request sets `responseType` for a custom action's binary response.
8
9
  *
9
10
  * A non-2xx response resolves with its status, headers and body, because the client maps those to
10
11
  * a `JsonApiError` itself. Only a request that never got a response (status 0: network failure,
@@ -25,8 +26,21 @@ function httpClientTransport(http) {
25
26
  };
26
27
  signal?.addEventListener("abort", onAbort, { once: true });
27
28
  const settle = () => signal?.removeEventListener("abort", onAbort);
28
- const subscription = http.request(req.method, req.url, {
29
- body: req.body ?? null,
29
+ const body = req.rawBody ?? req.body ?? null;
30
+ const error = (failure) => {
31
+ settle();
32
+ if (failure instanceof HttpErrorResponse && failure.status !== 0) {
33
+ errorBody(failure.error).then((text) => resolve({
34
+ status: failure.status,
35
+ headers: toRecord(failure.headers),
36
+ body: text
37
+ }), reject);
38
+ return;
39
+ }
40
+ reject(failure);
41
+ };
42
+ const subscription = req.responseType === void 0 ? http.request(req.method, req.url, {
43
+ body,
30
44
  headers: req.headers,
31
45
  observe: "response",
32
46
  responseType: "text"
@@ -39,18 +53,24 @@ function httpClientTransport(http) {
39
53
  body: res.body ?? ""
40
54
  });
41
55
  },
42
- error: (error) => {
56
+ error
57
+ }) : http.request(req.method, req.url, {
58
+ body,
59
+ headers: req.headers,
60
+ observe: "response",
61
+ responseType: req.responseType
62
+ }).subscribe({
63
+ next: (res) => {
43
64
  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
- }
65
+ const out = {
66
+ status: res.status,
67
+ headers: toRecord(res.headers),
68
+ body: ""
69
+ };
70
+ if (res.body !== null) out.binary = res.body;
71
+ resolve(out);
72
+ },
73
+ error
54
74
  });
55
75
  unsubscribe = () => subscription.unsubscribe();
56
76
  });
@@ -63,9 +83,11 @@ function toRecord(headers) {
63
83
  }
64
84
  return out;
65
85
  }
66
- function errorBody(body) {
86
+ async function errorBody(body) {
67
87
  if (typeof body === "string") return body;
68
88
  if (body === null || body === void 0) return "";
89
+ if (typeof Blob !== "undefined" && body instanceof Blob) return body.text();
90
+ if (body instanceof ArrayBuffer) return new TextDecoder().decode(body);
69
91
  return JSON.stringify(body);
70
92
  }
71
93
  //#endregion
@@ -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 { 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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"}
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`, or\n * as binary when the request sets `responseType` for a custom action's binary response.\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 body = req.rawBody ?? req.body ?? null\n const error = (failure: unknown): void => {\n settle()\n if (failure instanceof HttpErrorResponse && failure.status !== 0) {\n errorBody(failure.error).then(\n (text) =>\n resolve({ status: failure.status, headers: toRecord(failure.headers), body: text }),\n reject,\n )\n return\n }\n reject(failure)\n }\n const subscription =\n req.responseType === undefined\n ? http\n .request(req.method, req.url, {\n body,\n headers: req.headers,\n observe: 'response',\n responseType: 'text',\n })\n .subscribe({\n next: (res) => {\n settle()\n resolve({\n status: res.status,\n headers: toRecord(res.headers),\n body: res.body ?? '',\n })\n },\n error,\n })\n : http\n .request(req.method, req.url, {\n body,\n headers: req.headers,\n observe: 'response',\n // HttpClient's overloads need the literal, so pick the call per response type.\n responseType: req.responseType as 'blob',\n })\n .subscribe({\n next: (res) => {\n settle()\n const out: TransportResponse = {\n status: res.status,\n headers: toRecord(res.headers),\n body: '',\n }\n if (res.body !== null) {\n out.binary = res.body\n }\n resolve(out)\n },\n error,\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, and a binary request's error body is\n// decoded as text. Anything else (an interceptor that replaced it) is serialised so the client can\n// still try to parse a JSON:API error document.\nasync function errorBody(body: unknown): Promise<string> {\n if (typeof body === 'string') {\n return body\n }\n if (body === null || body === undefined) {\n return ''\n }\n if (typeof Blob !== 'undefined' && body instanceof Blob) {\n return body.text()\n }\n if (body instanceof ArrayBuffer) {\n return new TextDecoder().decode(body)\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":";;;;;;;;;;;;;;AAaA,SAAgB,oBAAoB,MAAoC;CACtE,QAAQ,QACN,IAAI,SAA4B,SAAS,WAAW;EAClD,MAAM,SAAS,IAAI;EACnB,IAAI,QAAQ,SAAS;GACnB,OAAO,OAAO,MAAM;GACpB;EACF;EAEA,IAAI;EACJ,MAAM,gBAAsB;GAC1B,cAAc;GACd,OAAO,QAAQ,MAAM;EACvB;EACA,QAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;EACzD,MAAM,eAAqB,QAAQ,oBAAoB,SAAS,OAAO;EAEvE,MAAM,OAAO,IAAI,WAAW,IAAI,QAAQ;EACxC,MAAM,SAAS,YAA2B;GACxC,OAAO;GACP,IAAI,mBAAmB,qBAAqB,QAAQ,WAAW,GAAG;IAChE,UAAU,QAAQ,KAAK,CAAC,CAAC,MACtB,SACC,QAAQ;KAAE,QAAQ,QAAQ;KAAQ,SAAS,SAAS,QAAQ,OAAO;KAAG,MAAM;IAAK,CAAC,GACpF,MACF;IACA;GACF;GACA,OAAO,OAAO;EAChB;EACA,MAAM,eACJ,IAAI,iBAAiB,KAAA,IACjB,KACG,QAAQ,IAAI,QAAQ,IAAI,KAAK;GAC5B;GACA,SAAS,IAAI;GACb,SAAS;GACT,cAAc;EAChB,CAAC,CAAC,CACD,UAAU;GACT,OAAO,QAAQ;IACb,OAAO;IACP,QAAQ;KACN,QAAQ,IAAI;KACZ,SAAS,SAAS,IAAI,OAAO;KAC7B,MAAM,IAAI,QAAQ;IACpB,CAAC;GACH;GACA;EACF,CAAC,IACH,KACG,QAAQ,IAAI,QAAQ,IAAI,KAAK;GAC5B;GACA,SAAS,IAAI;GACb,SAAS;GAET,cAAc,IAAI;EACpB,CAAC,CAAC,CACD,UAAU;GACT,OAAO,QAAQ;IACb,OAAO;IACP,MAAM,MAAyB;KAC7B,QAAQ,IAAI;KACZ,SAAS,SAAS,IAAI,OAAO;KAC7B,MAAM;IACR;IACA,IAAI,IAAI,SAAS,MACf,IAAI,SAAS,IAAI;IAEnB,QAAQ,GAAG;GACb;GACA;EACF,CAAC;EACT,oBAAoB,aAAa,YAAY;CAC/C,CAAC;AACL;AAEA,SAAS,SAAS,SAA8C;CAC9D,MAAM,MAA8B,CAAC;CACrC,KAAK,MAAM,QAAQ,QAAQ,KAAK,GAAG;EACjC,MAAM,SAAS,QAAQ,OAAO,IAAI;EAClC,IAAI,WAAW,MACb,IAAI,KAAK,YAAY,KAAK,OAAO,KAAK,IAAI;CAE9C;CACA,OAAO;AACT;AAKA,eAAe,UAAU,MAAgC;CACvD,IAAI,OAAO,SAAS,UAClB,OAAO;CAET,IAAI,SAAS,QAAQ,SAAS,KAAA,GAC5B,OAAO;CAET,IAAI,OAAO,SAAS,eAAe,gBAAgB,MACjD,OAAO,KAAK,KAAK;CAEnB,IAAI,gBAAgB,aAClB,OAAO,IAAI,YAAY,CAAC,CAAC,OAAO,IAAI;CAEtC,OAAO,KAAK,UAAU,IAAI;AAC5B;;;;ACzEA,MAAa,kBAAkB,IAAI,eAAwB,iBAAiB;;;;;AAM5E,SAAgB,yBAA4B,cAAc,iBAAoC;CAC5F,OAAO,IAAI,eAAkB,WAAW;AAC1C;;;;;;;;;AAUA,SAAgB,qBACd,cACA,SACA,QAA2B,iBACL;CACtB,OAAO,yBAAyB,CAC9B;EACE,SAAS;EACT,kBAAqB;GACnB,MAAM,WAAW,OAAO,YAAY,aAAa,QAAQ,IAAI;GAC7D,OAAO,aAAa;IAClB,GAAG;IACH,WAAW,SAAS,aAAa,oBAAoB,OAAO,UAAU,CAAC;GACzE,CAAC;EACH;CACF,CACF,CAAC;AACH;;;;;AAMA,SAAgB,oBACd,OACA,SACG;CACH,MAAM,WAAW,SAAU;CAC3B,IAAI,SAAS,aAAa,KAAA,GACxB,OAAO,QAAQ,SAAS,IAAI,QAAQ;CAEtC,yBAAyB,mBAAmB;CAC5C,OAAO,OAAO,QAAQ;AACxB;;;;;;;;;;;;;;AC5CA,SAAgB,gBACd,UACA,UAAqC,CAAC,GACI;CAC1C,IAAI,QAAQ,aAAa,KAAA,GACvB,yBAAyB,eAAe;CAG1C,MAAM,UADW,QAAQ,YAAY,OAAO,QAAQ,EAAA,CAC5B,IAAI,QAAQ,UAAW,eAAqC;CAEpF,MAAM,QAAQ,OAAsB,KAAA,CAAS;CAC7C,MAAM,QAAQ,OAAyC,KAAA,CAAS;CAChE,MAAM,UAAU,OAAO,CAAC;CAExB,IAAI,aAAa;CAEjB,MAAM,SAAS,OAAO,UAAyB;EAC7C,MAAM,UAAU,EAAE;EAClB,QAAQ,QAAQ,MAAM,IAAI,CAAC;EAC3B,MAAM,IAAI,KAAA,CAAS;EACnB,IAAI;GACF,MAAM,SAAS,MAAM,SAAS,QAAQ,KAAK;GAC3C,IAAI,YAAY,YACd,MAAM,IAAI,MAAM;GAElB,OAAO;EACT,SAAS,QAAQ;GACf,IAAI,YAAY,YACd,MAAM,IAAI,kBAAkB,QAAQ,SAAS,IAAI,MAAM,OAAO,MAAM,GAAG,EAAE,OAAO,OAAO,CAAC,CAAC;GAE3F,MAAM;EACR,UAAU;GACR,QAAQ,QAAQ,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC,CAAC;EAC1C;CACF;CAEA,OAAO;EACL;EACA,WAAW,eAAe,QAAQ,IAAI,CAAC;EACvC,OAAO,MAAM,WAAW;EACxB,cAAc,eAAe;GAC3B,MAAM,IAAI,MAAM;GAChB,OAAO,aAAa,eAAe,IAAI,KAAA;EACzC,CAAC;EACD,OAAO,MAAM,WAAW;EACxB,aAAa;GACX;GACA,MAAM,IAAI,KAAA,CAAS;GACnB,MAAM,IAAI,KAAA,CAAS;GACnB,QAAQ,IAAI,CAAC;EACf;CACF;AACF;;;;;;;;;;;;;;ACrFA,SAAgB,SACd,MAC2B;CAC3B,OAAO,WAAW,MAAM,OAAO,MAAM,MAAM,MAAM,IAAI;AACvD;;AAGA,SAAgB,SACd,MAC2B;CAC3B,OAAO,WAAW,MAAM,OAAO,MAAM,MAAM,MAAM,IAAI;AACvD;AAEA,MAAM,eAAkC,CAAC,SAAS,QAAQ;AAE1D,SAAS,WACP,MACA,MAC2B;CAC3B,IAAI,SAAS,KAAA,KAAa,SAAS,KAAA,GACjC;CAEF,MAAM,OAAO,OAAO,QAAQ,KAAK,IAAI;CACrC,MAAM,QAAQ,KAAK,SAAS,GAAG,IAAI,KAAK,MAAM,KAAK,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK;CACrF,MAAM,SAA0C,CAAC;CACjD,KAAK,MAAM,CAAC,MAAM,UAAU,IAAI,gBAAgB,KAAK,GAAG;EACtD,MAAM,SAAS,qBAAqB,KAAK,IAAI,CAAC,GAAG;EACjD,IAAI,WAAW,KAAA,GACb;EAEF,MAAM,OAAO,KAAK,MAAM,GAAG,SAAS,QAAQ,MAAM,CAAC,GAAG;EACtD,MAAM,QAAQ,OAAO,KAAK;EAC1B,OAAO,UACL,SAAS,KAAA,KAAa,CAAC,aAAa,SAAS,IAAI,KAAK,UAAU,MAAM,OAAO,SAAS,KAAK,IACvF,QACA;CACR;CACA,OAAO;AACT;;;;;;;;;;AAmBA,eAAsB,QACpB,OACA,UAA0B,CAAC,GACb;CACd,MAAM,EAAE,QAAQ,WAAW,OAAO,sBAAsB;CACxD,IAAI,OAAkC,MAAM;CAC5C,MAAM,MAAW,CAAC;CAClB,IAAI,QAAQ;CACZ,OAAO,SAAS,KAAA,KAAa,QAAQ,UAAU;EAC7C,IAAI,KAAK,GAAG,IAAI;EAChB;EACA,IAAI,SAAS,UACX;EAEF,QAAQ,eAAe;EACvB,OAAO,MAAM,KAAK,MAAM,WAAW,KAAA,IAAY,EAAE,OAAO,IAAI,KAAA,CAAS;CACvE;CACA,OAAO;AACT;;;ACDA,SAAgB,gBACd,SACmD;CACnD,IAAI,QAAQ,aAAa,KAAA,GACvB,yBAAyB,eAAe;CAE1C,MAAM,WAAW,QAAQ,YAAY,OAAO,QAAQ;CACpD,MAAM,SAAS,SAAS,IAAI,QAAQ,UAAW,eAAqC;CAEpF,MAAM,MAAM,SAAuC;EACjD,GAAI,QAAQ,WAAW,KAAA,IAAY,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC;EACjE,SAAS,EAAE,QAAQ,aAAa,eAC9B,QAAQ,OAAO;GAAE;GAAgB;GAAa;GAAa;EAAS,CAAC;EACvE,GAAI,QAAQ,iBAAiB,KAAA,IAAY,EAAE,cAAc,QAAQ,aAAa,IAAI,CAAC;EACnF,GAAI,QAAQ,UAAU,KAAA,IAClB,EAAE,OAAO,QAAQ,MAAwC,IACzD,CAAC;EACL,GAAI,QAAQ,cAAc,KAAA,IAAY,EAAE,WAAW,QAAQ,UAAU,IAAI,CAAC;EAC1E;CACF,CAAC;CAED,MAAM,eAAe,eAAe;EAClC,MAAM,QAAQ,IAAI,MAAM;EACxB,OAAO,iBAAiB,eAAe,QAAQ,KAAA;CACjD,CAAC;CACD,OAAO,eAAe,KAAK,gBAAgB;EAAE,OAAO;EAAc,YAAY;CAAK,CAAC;CACpF,OAAO;AACT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haddowg/json-api-angular",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
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.3.0"
51
+ "@haddowg/json-api-client": "^1.4.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "@angular/common": "^20.0.0 || ^21.0.0 || ^22.0.0",