@zeroxsolutions/jsonapi 0.13.7 → 0.13.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## 0.13.8 (2026-10-22)
2
+
3
+ ### 🚀 Features
4
+
5
+ - **jsonapi:** read a refused response into an api error ([723fa9a](https://github.com/zeroxsolutions/zeroxsolutions/commit/723fa9a))
6
+
7
+ ### 💅 Refactors
8
+
9
+ - **jsonapi:** name the refused-response read errorOf ([7bd0c02](https://github.com/zeroxsolutions/zeroxsolutions/commit/7bd0c02))
10
+
11
+ ### ❤️ Thank You
12
+
13
+ - Lương Văn Tú
14
+
1
15
  ## 0.13.7 (2026-10-21)
2
16
 
3
17
  This was a version bump only for @zeroxsolutions/jsonapi to align it with other projects, there were no code changes.
package/README.md CHANGED
@@ -27,7 +27,7 @@ what keeps `hono` out of the package an SPA also loads.
27
27
  | Import | Use in | What it holds |
28
28
  | -------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
29
29
  | `@zeroxsolutions/jsonapi` | services, gateway | `negotiateJsonApi` - the whole decision, from `Content-Type`, `Accept` and the `ext` URIs this surface supports (`supportedExtensions`), to the `JsonApiError` to reject with or `null`: 415 where `Content-Type` is the JSON:API media type carrying a parameter beyond `ext`/`profile` or an `ext` outside `supportedExtensions`; 406 where `Accept` holds JSON:API instances and every one carries such a parameter or `ext`; nothing else. `findOptionsOf` - a guarded query to the options `@zeroxsolutions/db`'s finders take |
30
- | `@zeroxsolutions/jsonapi/client` | SPA, external consumers | `deserializeDocument` (jsona), `readResource` / `readCollection` over it - the same flattening, typed off the route's own document - `readRelated` for what an `include` resolved, which no document type can name; `toRequestDocument`, the JSON:API request document a write sends, each relationship wrapped in its relationship object; and `ApiError`, the non-2xx a caller throws once a response has come back |
30
+ | `@zeroxsolutions/jsonapi/client` | SPA, external consumers | `deserializeDocument` (jsona), `readResource` / `readCollection` over it - the same flattening, typed off the route's own document - `readRelated` for what an `include` resolved, which no document type can name; `toRequestDocument`, the JSON:API request document a write sends, each relationship wrapped in its relationship object; `ApiError`, the non-2xx a caller throws once a response has come back, and `errorOf`, which reads one off that response |
31
31
 
32
32
  A transport mounts the decision rather than calling it: `jsonApiContentNegotiation` is the Hono adapter
33
33
  over it, and it lives with the other framework wiring. `negotiateJsonApi` itself takes the surface's
@@ -47,10 +47,42 @@ response type, which is what keeps the model off a hand-written interface:
47
47
  ```ts
48
48
  // the query layer - the only place the document is spelled
49
49
  type SubjectList = CollectionOf<InferResponseType<(typeof api.v1.orgs)[':orgId']['subjects']['$get'], 200>>;
50
- if (!res.ok) throw new ApiError(res.status, await res.json().catch(() => null));
50
+ if (!res.ok) throw await errorOf(res);
51
51
  const { data, meta } = readCollection(await res.json());
52
52
  ```
53
53
 
54
+ A refused response is read into an `ApiError` by `errorOf`, never by reading the body by hand. It reads the body
55
+ once and parses it as an error document, so `body` holds every error object the server sent, in its order, and is
56
+ `null` where the failing tier sent no error document: a proxy's HTML page, an empty body, or JSON of another shape.
57
+ Parsed rather than trusted, a body with no `errors` cannot make the throw itself throw:
58
+
59
+ ```ts
60
+ import { ApiError, errorOf } from '@zeroxsolutions/jsonapi/client';
61
+
62
+ async function read(res: Response): Promise<unknown> {
63
+ if (!res.ok) throw await errorOf(res);
64
+ return res.json();
65
+ }
66
+
67
+ const refusal = {
68
+ errors: [{ code: 'api.bad_request', source: { pointer: '/data/attributes/name' } }, { code: 'api.conflict' }],
69
+ };
70
+
71
+ try {
72
+ await read(new Response(JSON.stringify(refusal), { status: 422 }));
73
+ } catch (error) {
74
+ if (error instanceof ApiError) {
75
+ error.status; // 422
76
+ error.body?.errors.map((object) => object.code); // ['api.bad_request', 'api.conflict']
77
+ }
78
+ }
79
+
80
+ (await errorOf(new Response('<html>502 Bad Gateway</html>', { status: 502 }))).body; // null
81
+ ```
82
+
83
+ The body is read once, so a caller that reads it too hands `errorOf` a `clone()`. What a screen draws for each
84
+ code, and which field a pointer names, is the screen's, so neither is here.
85
+
54
86
  Only `data` is flattened. `links` and `meta` keep the names the standard gives them, and `meta` stays
55
87
  free-form - the standard puts no member in it, so a row count there is one surface's own convention.
56
88
 
@@ -1,4 +1,4 @@
1
- import type { ErrorDocument } from '@zeroxsolutions/response';
1
+ import { type ErrorDocument } from '@zeroxsolutions/response';
2
2
  /**
3
3
  * A non-2xx response, as the client that made the request holds it.
4
4
  *
@@ -11,4 +11,19 @@ export declare class ApiError extends Error {
11
11
  readonly body: ErrorDocument | null;
12
12
  constructor(status: number, body: ErrorDocument | null);
13
13
  }
14
+ /**
15
+ * The {@link ApiError} a refused response yields: its status, and the error document its body holds, every error
16
+ * object kept in the order the server sent them.
17
+ *
18
+ * Reads the body once, so the response must not have been read before; a caller that needs the body for anything
19
+ * else hands this a clone. `body` is null where the body is no JSON, such as a proxy's HTML page or an empty body,
20
+ * and where it is JSON but no error document. It never rejects.
21
+ *
22
+ * @example
23
+ * if (!response.ok) throw await errorOf(response);
24
+ */
25
+ export declare function errorOf(response: {
26
+ readonly status: number;
27
+ json(): Promise<unknown>;
28
+ }): Promise<ApiError>;
14
29
  //# sourceMappingURL=api-error.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"api-error.d.ts","sourceRoot":"","sources":["../../src/client/api-error.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D;;;;;;GAMG;AACH,qBAAa,QAAS,SAAQ,KAAK;IAE/B,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI;gBAD1B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,aAAa,GAAG,IAAI;CAKtC"}
1
+ {"version":3,"file":"api-error.d.ts","sourceRoot":"","sources":["../../src/client/api-error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAEnF;;;;;;GAMG;AACH,qBAAa,QAAS,SAAQ,KAAK;IAE/B,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI;gBAD1B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,aAAa,GAAG,IAAI;CAKtC;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,OAAO,CAAC,QAAQ,EAAE;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,CAIhH"}
@@ -1,3 +1,4 @@
1
+ import { errorDocumentSchema } from '@zeroxsolutions/response';
1
2
  /**
2
3
  * A non-2xx response, as the client that made the request holds it.
3
4
  *
@@ -15,3 +16,18 @@ export class ApiError extends Error {
15
16
  this.name = 'ApiError';
16
17
  }
17
18
  }
19
+ /**
20
+ * The {@link ApiError} a refused response yields: its status, and the error document its body holds, every error
21
+ * object kept in the order the server sent them.
22
+ *
23
+ * Reads the body once, so the response must not have been read before; a caller that needs the body for anything
24
+ * else hands this a clone. `body` is null where the body is no JSON, such as a proxy's HTML page or an empty body,
25
+ * and where it is JSON but no error document. It never rejects.
26
+ *
27
+ * @example
28
+ * if (!response.ok) throw await errorOf(response);
29
+ */
30
+ export async function errorOf(response) {
31
+ const document = errorDocumentSchema.safeParse(await response.json().catch(() => null));
32
+ return new ApiError(response.status, document.success ? document.data : null);
33
+ }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * The client half of the standard: the document a response carries, flattened once, the document a write sends, and
3
- * the failure a caller throws.
3
+ * the failure a caller throws, read off the response that refused it.
4
4
  */
5
- export { ApiError } from './api-error.js';
5
+ export { ApiError, errorOf } from './api-error.js';
6
6
  export { deserializeDocument } from './deserialize.js';
7
7
  export { readCollection, readRelated, readResource, type Collection, type CollectionOf, type Resource, type ResourceOf, } from './read-document.js';
8
8
  export { toRequestDocument, type RelationshipInput, type ToRequestDocument } from './request-document.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EACL,cAAc,EACd,WAAW,EACX,YAAY,EACZ,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,QAAQ,EACb,KAAK,UAAU,GAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EACL,cAAc,EACd,WAAW,EACX,YAAY,EACZ,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,QAAQ,EACb,KAAK,UAAU,GAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC"}
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * The client half of the standard: the document a response carries, flattened once, the document a write sends, and
3
- * the failure a caller throws.
3
+ * the failure a caller throws, read off the response that refused it.
4
4
  */
5
- export { ApiError } from './api-error.js';
5
+ export { ApiError, errorOf } from './api-error.js';
6
6
  export { deserializeDocument } from './deserialize.js';
7
7
  export { readCollection, readRelated, readResource, } from './read-document.js';
8
8
  export { toRequestDocument } from './request-document.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroxsolutions/jsonapi",
3
- "version": "0.13.7",
3
+ "version": "0.13.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,