@zeroxsolutions/response 0.12.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +330 -0
  2. package/README.md +237 -0
  3. package/dist/api-response.d.ts +24 -0
  4. package/dist/api-response.d.ts.map +1 -0
  5. package/dist/api-response.js +18 -0
  6. package/dist/document-shape.d.ts +17 -0
  7. package/dist/document-shape.d.ts.map +1 -0
  8. package/dist/document-shape.js +29 -0
  9. package/dist/document.d.ts +47 -0
  10. package/dist/document.d.ts.map +1 -0
  11. package/dist/document.js +54 -0
  12. package/dist/domain-error-resolver.d.ts +37 -0
  13. package/dist/domain-error-resolver.d.ts.map +1 -0
  14. package/dist/domain-error-resolver.js +39 -0
  15. package/dist/error-map.d.ts +14 -0
  16. package/dist/error-map.d.ts.map +1 -0
  17. package/dist/error-map.js +1 -0
  18. package/dist/errors/error-document.d.ts +189 -0
  19. package/dist/errors/error-document.d.ts.map +1 -0
  20. package/dist/errors/error-document.js +37 -0
  21. package/dist/errors/error-registry.d.ts +49 -0
  22. package/dist/errors/error-registry.d.ts.map +1 -0
  23. package/dist/errors/error-registry.js +13 -0
  24. package/dist/errors/index.d.ts +6 -0
  25. package/dist/errors/index.d.ts.map +1 -0
  26. package/dist/errors/index.js +5 -0
  27. package/dist/errors/jsonapi-error.d.ts +27 -0
  28. package/dist/errors/jsonapi-error.d.ts.map +1 -0
  29. package/dist/errors/jsonapi-error.js +21 -0
  30. package/dist/errors/resolved-error.d.ts +7 -0
  31. package/dist/errors/resolved-error.d.ts.map +1 -0
  32. package/dist/errors/resolved-error.js +1 -0
  33. package/dist/errors/to-error-document.d.ts +11 -0
  34. package/dist/errors/to-error-document.d.ts.map +1 -0
  35. package/dist/errors/to-error-document.js +25 -0
  36. package/dist/index.d.ts +16 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +11 -0
  39. package/dist/media-type.d.ts +5 -0
  40. package/dist/media-type.d.ts.map +1 -0
  41. package/dist/media-type.js +4 -0
  42. package/dist/primitives.d.ts +161 -0
  43. package/dist/primitives.d.ts.map +1 -0
  44. package/dist/primitives.js +47 -0
  45. package/dist/relationship.d.ts +37 -0
  46. package/dist/relationship.d.ts.map +1 -0
  47. package/dist/relationship.js +44 -0
  48. package/dist/resource.d.ts +35 -0
  49. package/dist/resource.d.ts.map +1 -0
  50. package/dist/resource.js +63 -0
  51. package/dist/serialize.d.ts +44 -0
  52. package/dist/serialize.d.ts.map +1 -0
  53. package/dist/serialize.js +77 -0
  54. package/dist/upstream-error.d.ts +34 -0
  55. package/dist/upstream-error.d.ts.map +1 -0
  56. package/dist/upstream-error.js +33 -0
  57. package/package.json +38 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,330 @@
1
+ ## 0.12.0 (2026-09-13)
2
+
3
+ ### 💅 Refactors
4
+
5
+ - ⚠️ **response:** list the names the package root publishes ([7b8a95d](https://github.com/zeroxsolutions/zeroxsolutions/commit/7b8a95d))
6
+ - ⚠️ **response:** answer mapped errors with links and meta ([b78bc08](https://github.com/zeroxsolutions/zeroxsolutions/commit/b78bc08))
7
+ - ⚠️ **response:** render every member of a JSON:API error object ([ca0c551](https://github.com/zeroxsolutions/zeroxsolutions/commit/ca0c551))
8
+
9
+ ### ⚠️ Breaking Changes
10
+
11
+ - **response:** list the names the package root publishes ([7b8a95d](https://github.com/zeroxsolutions/zeroxsolutions/commit/7b8a95d))
12
+ the root no longer exports `createSerializer`,
13
+ `serializeResource`, `serializeCollection`, `reachableResources`,
14
+ `resourceObjectSchema`, `resourceDocumentSchema`,
15
+ `collectionDocumentSchema`, `relationshipSchema`, `relationshipsSchema`,
16
+ `linkSchema`, `metaSchema`, `resourceLinksSchema`, `paginationLinksSchema`,
17
+ `resourceIdentifierSchema`, `jsonApiObjectSchema`, `errorSourceSchema` or
18
+ `errorObjectSchema`. Their inferred types stay listed. A resource's own
19
+ `document`, `collectionDocument`, `serialize` and `serializeMany` declare
20
+ and render its documents; the schema consts have no public replacement.
21
+ - **response:** answer mapped errors with links and meta ([b78bc08](https://github.com/zeroxsolutions/zeroxsolutions/commit/b78bc08))
22
+ `ErrorMeta` is `DomainErrorAnswer`, whose `code` is
23
+ required. `defineErrorMap` takes `DomainErrorEntry` objects, which
24
+ `errorEntry(type, answer)` builds, in place of `[type, meta]` tuples.
25
+ `ErrorMap<TMeta>` is `DomainErrorMap`, and `UpstreamErrorMap` takes no
26
+ type argument. `UpstreamErrorMeta` is `UpstreamErrorAnswer`, and
27
+ `toUpstreamError(map, document, fallback)` is
28
+ `createUpstreamErrorTranslator(map, fallback)(document)`.
29
+ - **response:** render every member of a JSON:API error object ([ca0c551](https://github.com/zeroxsolutions/zeroxsolutions/commit/ca0c551))
30
+ `buildErrorDocument` is `toErrorDocument`, and the
31
+ document it renders carries `jsonapi: { version: '1.1' }`.
32
+ `ErrorCodeMeta` is `ErrorStatus`; `ErrorRegistry` and `ErrorLike` are
33
+ removed. An error object's `links.about` and `links.type` are link values
34
+ rather than strings. A `JsonApiError` whose `detail` equals its `title`
35
+ now renders that `detail`.
36
+
37
+ ### ❤️ Thank You
38
+
39
+ - Claude Sonnet 5
40
+ - Lương Văn Tú
41
+
42
+ ## 0.11.4 (2026-09-13)
43
+
44
+ This was a version bump only for @zeroxsolutions/response to align it with other projects, there were no code changes.
45
+
46
+ ## 0.11.3 (2026-09-13)
47
+
48
+ ### 🚀 Features
49
+
50
+ - **response:** publish relationship names in the declared fieldsets ([211ab15](https://github.com/zeroxsolutions/zeroxsolutions/commit/211ab15))
51
+
52
+ ### ❤️ Thank You
53
+
54
+ - Claude Opus 5 (1M context)
55
+ - Lương Văn Tú
56
+
57
+ ## 0.11.2 (2026-09-13)
58
+
59
+ ### 🩹 Fixes
60
+
61
+ - **response:** key a resource's kept serializers by the shape asked for, and bound them ([f746e8d](https://github.com/zeroxsolutions/zeroxsolutions/commit/f746e8d))
62
+ - **response,repo:** stop shipping the outputs a rename left in dist ([e64aa02](https://github.com/zeroxsolutions/zeroxsolutions/commit/e64aa02))
63
+
64
+ ### ❤️ Thank You
65
+
66
+ - Claude Opus 5 (1M context)
67
+ - Lương Văn Tú
68
+
69
+ ## 0.11.1 (2026-09-07)
70
+
71
+ Repackaged only, no source change: the previous tarball still carried the pre-flatten `dist/http/`, `dist/jsonapi/` and `dist/lib/shared/` tree, which `tsc --build` leaves behind when a source file is renamed away. 138 entries down to 57.
72
+
73
+ ## 0.11.0 (2026-09-07)
74
+
75
+ ### 🚀 Features
76
+
77
+ - ⚠️ **server,jsonapi:** one surface, one error-code vocabulary ([32b4aed](https://github.com/zeroxsolutions/zeroxsolutions/commit/32b4aed))
78
+
79
+ ### 💅 Refactors
80
+
81
+ - ⚠️ **query,response:** drop the seams the second spelling paid for ([40f1bca](https://github.com/zeroxsolutions/zeroxsolutions/commit/40f1bca))
82
+
83
+ ### ⚠️ Breaking Changes
84
+
85
+ - **server,jsonapi:** one surface, one error-code vocabulary ([32b4aed](https://github.com/zeroxsolutions/zeroxsolutions/commit/32b4aed))
86
+ createErrorHandler, createValidationHook and
87
+ jsonApiContentNegotiation take a required `namespace`; negotiateJsonApi takes it
88
+ as a third argument. Codes these mint are now `<namespace>.<name>`.
89
+ - **query,response:** drop the seams the second spelling paid for ([40f1bca](https://github.com/zeroxsolutions/zeroxsolutions/commit/40f1bca))
90
+ @zeroxsolutions/query no longer exports CollectionQueryParser,
91
+ CollectionQueryRenderer, collectionLinksWith, collectionQueryParser, parseSort,
92
+ parseFields, parseInclude or parsePagination. collectionLinks and
93
+ parseCollectionQuery / parseResourceQuery replace them. No in-house repo
94
+ imported any of the eight, but the caret range would otherwise carry the
95
+ removal into one as a patch.
96
+
97
+ ### ❤️ Thank You
98
+
99
+ - Lương Văn Tú
100
+
101
+ ## 0.10.0 (2026-09-07)
102
+
103
+ ### 💅 Refactors
104
+
105
+ - ⚠️ **query,response,server:** one wire spelling, flat exports ([5c57b3a](https://github.com/zeroxsolutions/zeroxsolutions/commit/5c57b3a))
106
+
107
+ ### ⚠️ Breaking Changes
108
+
109
+ - **query,response,server:** one wire spelling, flat exports ([5c57b3a](https://github.com/zeroxsolutions/zeroxsolutions/commit/5c57b3a))
110
+ `@zeroxsolutions/response/http`, `@zeroxsolutions/query/http`
111
+ and `@zeroxsolutions/server/hono/http` are gone, and the surviving `/jsonapi`
112
+ and `/hono` subpaths move to the package root. Every import drops its subpath.
113
+
114
+ ### ❤️ Thank You
115
+
116
+ - Lương Văn Tú
117
+
118
+ ## 0.9.0 (2026-09-07)
119
+
120
+ ### 💅 Refactors
121
+
122
+ - ⚠️ **response:** key the upstream map by string, drop its builder ([d5c5e6b](https://github.com/zeroxsolutions/zeroxsolutions/commit/d5c5e6b))
123
+
124
+ ### ⚠️ Breaking Changes
125
+
126
+ - **response:** key the upstream map by string, drop its builder ([d5c5e6b](https://github.com/zeroxsolutions/zeroxsolutions/commit/d5c5e6b))
127
+ defineUpstreamErrorMap is gone from both spellings. Declare the
128
+ map as an object literal with `satisfies UpstreamErrorMap<UpstreamErrorMeta>`;
129
+ toUpstreamError takes it unchanged.
130
+
131
+ ### ❤️ Thank You
132
+
133
+ - Claude Opus 5 (1M context)
134
+ - Lương Văn Tú
135
+
136
+ ## 0.8.0 (2026-09-07)
137
+
138
+ ### 💅 Refactors
139
+
140
+ - ⚠️ **response:** move the Hono handlers out, translate upstream errors ([ce60636](https://github.com/zeroxsolutions/zeroxsolutions/commit/ce60636))
141
+
142
+ ### ⚠️ Breaking Changes
143
+
144
+ - **response:** move the Hono handlers out, translate upstream errors ([ce60636](https://github.com/zeroxsolutions/zeroxsolutions/commit/ce60636))
145
+ the ./http/server and ./jsonapi/server subpaths are gone -
146
+ createErrorHandler and createValidationHook are @zeroxsolutions/server/http
147
+ and @zeroxsolutions/server. Both remaining subpaths resolve to
148
+ dist/<entry> rather than dist/lib/<entry>, so a consumer reaching past the
149
+ exports map into dist/ breaks.
150
+
151
+ ### ❤️ Thank You
152
+
153
+ - Claude Opus 5 (1M context)
154
+ - Lương Văn Tú
155
+
156
+ ## 0.7.3 (2026-09-03)
157
+
158
+ ### 🚀 Features
159
+
160
+ - **response:** let an error map name the request slot it blames ([27cc0c6](https://github.com/zeroxsolutions/zeroxsolutions/commit/27cc0c6))
161
+
162
+ ### ❤️ Thank You
163
+
164
+ - Lương Văn Tú
165
+
166
+ ## 0.7.2 (2026-09-03)
167
+
168
+ ### 🚀 Features
169
+
170
+ - **response:** bring the link value up to the version its documents claim ([b8e3a25](https://github.com/zeroxsolutions/zeroxsolutions/commit/b8e3a25))
171
+
172
+ ### ❤️ Thank You
173
+
174
+ - Claude Opus 5 (1M context)
175
+ - Lương Văn Tú
176
+
177
+ ## 0.7.1 (2026-09-03)
178
+
179
+ ### 🚀 Features
180
+
181
+ - **response:** read a resource declaration back as a document permit-list ([2b6763a](https://github.com/zeroxsolutions/zeroxsolutions/commit/2b6763a))
182
+
183
+ ### ❤️ Thank You
184
+
185
+ - Claude Opus 5 (1M context)
186
+ - Lương Văn Tú
187
+
188
+ ## 0.7.0 (2026-09-02)
189
+
190
+ ### 🚀 Features
191
+
192
+ - ⚠️ **response:** type /jsonapi's documents off the resource's attributes ([1dcf923](https://github.com/zeroxsolutions/zeroxsolutions/commit/1dcf923))
193
+
194
+ ### ⚠️ Breaking Changes
195
+
196
+ - **response:** type /jsonapi's documents off the resource's attributes ([1dcf923](https://github.com/zeroxsolutions/zeroxsolutions/commit/1dcf923))
197
+ `serialize` returns `ResourceDocument<A>` and `serializeMany`
198
+ `CollectionDocument<A>` instead of `Row`, and `okResponse`/`listResponse` are
199
+ generic in the resource's attribute schema.
200
+
201
+ ### ❤️ Thank You
202
+
203
+ - Lương Văn Tú
204
+
205
+ ## 0.6.1 (2026-09-02)
206
+
207
+ ### 🩹 Fixes
208
+
209
+ - **response:** export the response object /jsonapi's three helpers return ([f45fd15](https://github.com/zeroxsolutions/zeroxsolutions/commit/f45fd15))
210
+
211
+ ### ❤️ Thank You
212
+
213
+ - Claude Opus 5 (1M context)
214
+ - Lương Văn Tú
215
+
216
+ ## 0.6.0 (2026-09-02)
217
+
218
+ ### 🚀 Features
219
+
220
+ - ⚠️ **response:** bring /jsonapi's server half up to /http's ([a40fed4](https://github.com/zeroxsolutions/zeroxsolutions/commit/a40fed4))
221
+
222
+ ### ⚠️ Breaking Changes
223
+
224
+ - **response:** bring /jsonapi's server half up to /http's ([a40fed4](https://github.com/zeroxsolutions/zeroxsolutions/commit/a40fed4))
225
+ /jsonapi/server's createErrorHandler now requires `category`.
226
+
227
+ ### ❤️ Thank You
228
+
229
+ - Claude Opus 5 (1M context)
230
+ - Lương Văn Tú
231
+
232
+ ## 0.5.0 (2026-09-02)
233
+
234
+ ### 🚀 Features
235
+
236
+ - ⚠️ **response:** declare each JSON:API resource once ([748e006](https://github.com/zeroxsolutions/zeroxsolutions/commit/748e006))
237
+ - ⚠️ **db:** let each table declare its own id column ([eca4aca](https://github.com/zeroxsolutions/zeroxsolutions/commit/eca4aca))
238
+
239
+ ### ⚠️ Breaking Changes
240
+
241
+ - **response:** declare each JSON:API resource once ([748e006](https://github.com/zeroxsolutions/zeroxsolutions/commit/748e006))
242
+ `/jsonapi`'s `okResponse` / `listResponse` take a resource
243
+ declaration instead of `(type, attributesSchema)`. `resourceDocument`,
244
+ `resourceCollectionDocument`, `applyFields`, `mergeIncluded`,
245
+ `documentResources` and `ResourceObject` are removed - the descriptor covers
246
+ them. `/jsonapi/server` now exports only `createErrorHandler`; the serializer
247
+ entry points moved behind the descriptor.
248
+ - **db:** let each table declare its own id column ([eca4aca](https://github.com/zeroxsolutions/zeroxsolutions/commit/eca4aca))
249
+ `baseTable` is removed - take `idColumn` and spread
250
+ `timestamps`. `defaultIndexedBaseColumns` is now `timestampIndexes` and
251
+ `BaseTableShape` is `TimestampColumns`; both were named for the removed concept.
252
+
253
+ ### ❤️ Thank You
254
+
255
+ - Claude Opus 5 (1M context)
256
+ - Lương Văn Tú
257
+
258
+ ## 0.4.0 (2026-08-31)
259
+
260
+ ### 🚀 Features
261
+
262
+ - ⚠️ **response:** take the isolate's own logging category, not the library's ([3409798](https://github.com/zeroxsolutions/zeroxsolutions/commit/3409798))
263
+
264
+ ### ⚠️ Breaking Changes
265
+
266
+ - **response:** take the isolate's own logging category, not the library's ([3409798](https://github.com/zeroxsolutions/zeroxsolutions/commit/3409798))
267
+ `createErrorHandler` takes one options object and `category` is
268
+ required - `createErrorHandler(errors)` becomes
269
+ `createErrorHandler({ category, errors })`. Required rather than defaulted
270
+ because a default is what made the silence invisible.
271
+
272
+ ### ❤️ Thank You
273
+
274
+ - Claude Opus 5 (1M context)
275
+ - Lương Văn Tú
276
+
277
+ ## 0.3.0 (2026-08-31)
278
+
279
+ ### 🚀 Features
280
+
281
+ - ⚠️ **response:** write the unhandled 500 through logtape, under the caller's id ([fdcb75b](https://github.com/zeroxsolutions/zeroxsolutions/commit/fdcb75b))
282
+
283
+ ### ⚠️ Breaking Changes
284
+
285
+ - **response:** write the unhandled 500 through logtape, under the caller's id ([fdcb75b](https://github.com/zeroxsolutions/zeroxsolutions/commit/fdcb75b))
286
+ `@logtape/logtape` is a new peer of `./http/server`, and the
287
+ unhandled-500 line is silent in an isolate that never calls `configureSync`.
288
+ It previously always reached stdout.
289
+
290
+ ### ❤️ Thank You
291
+
292
+ - Claude Opus 5 (1M context)
293
+ - Lương Văn Tú
294
+
295
+ ## 0.2.2 (2026-08-29)
296
+
297
+ ### 💅 Refactors
298
+
299
+ - state an explicit return type on every function ([8ee4264](https://github.com/zeroxsolutions/zeroxsolutions/commit/8ee4264))
300
+
301
+ ### ❤️ Thank You
302
+
303
+ - Lương Văn Tú
304
+
305
+ ## 0.2.1 (2026-08-28)
306
+
307
+ ### 🚀 Features
308
+
309
+ - **response:** render a rejected request in the surface's own error shape ([bdece2a](https://github.com/zeroxsolutions/zeroxsolutions/commit/bdece2a))
310
+
311
+ ### 🩹 Fixes
312
+
313
+ - **response:** treat an empty projection as publishing nothing ([ffb0bb5](https://github.com/zeroxsolutions/zeroxsolutions/commit/ffb0bb5))
314
+ - **db,response:** stop advertising a root entry neither package has ([18abe7e](https://github.com/zeroxsolutions/zeroxsolutions/commit/18abe7e))
315
+
316
+ ### ❤️ Thank You
317
+
318
+ - Claude Opus 5
319
+ - Lương Văn Tú
320
+
321
+ ## 0.1.0 (2026-08-15)
322
+
323
+ ### 🚀 Features
324
+
325
+ - **http-error:** the HTTP error contract every transport answers with ([6d94faa](https://github.com/zeroxsolutions/zeroxsolutions/commit/6d94faa))
326
+
327
+ ### ❤️ Thank You
328
+
329
+ - Claude Opus 5
330
+ - Lương Văn Tú
package/README.md ADDED
@@ -0,0 +1,237 @@
1
+ # @zeroxsolutions/response
2
+
3
+ The response half of the HTTP contract a service and its edge gateway both answer with - the sibling
4
+ of `@zeroxsolutions/query`, which owns the query-string half, and shaped the same way: one entry, one
5
+ wire spelling.
6
+
7
+ ```ts
8
+ import { errorResponse, jsonApiResource, listResponse, okResponse, toOne } from '@zeroxsolutions/response';
9
+
10
+ export const subjects = jsonApiResource('subjects', subjectAttributesSchema, {
11
+ relationships: { org: toOne(() => orgs, (row) => row.org) },
12
+ });
13
+
14
+ responses: {
15
+ 200: listResponse('Subjects', subjects), // { data: [...], included?, links? }
16
+ 201: okResponse('Subject created', subjects), // { data: {...}, included?, ... }
17
+ 401: errorResponse('Authentication required'), // { errors: [...] }
18
+ }
19
+
20
+ return c.json(await subjects.serializeMany(rows, { links }, c.req.valid('query')));
21
+ ```
22
+
23
+ Both take the **item** on a list and wrap it, so no route restates the carrier. They take a resource
24
+ DECLARATION rather than a bare schema, because a document carries a `type` and its relationships, which
25
+ a schema alone has nowhere to put.
26
+
27
+ The declaration is one value, and the schema a route advertises and the document its handler emits both
28
+ come off it: the attribute keys ARE the serializer's projection, so a row member the schema does not
29
+ declare cannot reach the wire, and the request's sparse fieldset reaches an `included` resource because
30
+ the descriptor builds the related type's serializer from that same request. Two resources may name each
31
+ other - a relationship holds a thunk - and one side of such a pair carries an explicit
32
+ `JsonApiResource<typeof attributes>`, which is the only thing tsc cannot infer through the cycle.
33
+
34
+ `declaredDocumentShape` reads that same declaration back as the permit-list a query guard needs: every
35
+ reachable type with the fields it publishes - its attributes and its relationships, as the standard counts
36
+ them - and every relationship path a document can compound.
37
+ Deriving it is the point - a list written by hand beside the declaration permits a column the resource
38
+ stopped publishing, with both halves compiling and both suites green.
39
+
40
+ ```ts
41
+ const { fields, include } = declaredDocumentShape(classes); // feeds `@zeroxsolutions/query`'s permits
42
+ ```
43
+
44
+ ## Errors
45
+
46
+ The domain raises a plain `Error` subclass carrying only domain data; the transport decides what it
47
+ means on the wire, from a table keyed on the error's **constructor identity**.
48
+
49
+ ```ts
50
+ // entrypoints/http/error-map.ts - the transport's type -> wire table
51
+ import { defineErrorMap, errorEntry } from '@zeroxsolutions/response';
52
+
53
+ export const academicsErrors = defineErrorMap([
54
+ errorEntry(ClassNotFound, { status: 404, title: 'Not Found', code: 'academics.class.not_found' }),
55
+ errorEntry(ClassFull, {
56
+ status: 409,
57
+ title: 'Conflict',
58
+ code: 'academics.class.full',
59
+ meta: (error) => ({ capacity: error.capacity }), // `capacity` is typed off ClassFull
60
+ }),
61
+ ]);
62
+ ```
63
+
64
+ Map membership is the discriminator between a known failure and a bug: an error type the map does not
65
+ name answers `500` carrying nothing of itself, so a forgotten entry is loud rather than a lenient 4xx. A
66
+ subclass of a mapped class is not mapped either - the lookup is identity, never `instanceof`.
67
+
68
+ An answer is `{ status, title, code }`, all three required: an error object needs a `title`, and the
69
+ `code` is this transport's own word for the failure, never the domain's. `errorEntry` infers the class, so
70
+ an answer's optional members read what that class carries:
71
+
72
+ - `meta` renders the error's own data a client acts on, in the member the standard gives it, rather than
73
+ a `detail` a client would have to parse;
74
+ - `source` names the request slot to blame;
75
+ - `links` points at a page about the problem.
76
+
77
+ The rendered error object carries the error's `message` as `detail`.
78
+
79
+ The `onError` handler itself is not in this package. It is a `hono` `ErrorHandler`, and keeping it out is
80
+ what leaves this one with no framework peer: `createErrorHandler` and `createValidationHook` ship from
81
+ `@zeroxsolutions/server`, and the map reaches the handler through `createDomainErrorResolver(map)`.
82
+
83
+ ```ts
84
+ // entrypoints/http/app.ts - one central onError per transport
85
+ import { createDomainErrorResolver } from '@zeroxsolutions/response';
86
+ import { createErrorHandler } from '@zeroxsolutions/server';
87
+
88
+ app.onError(
89
+ createErrorHandler({
90
+ namespace: 'academics', // every code THESE handlers mint carries it, so one surface has one vocabulary
91
+ category: ['academics', 'http'],
92
+ resolve: createDomainErrorResolver(academicsErrors),
93
+ }),
94
+ );
95
+ ```
96
+
97
+ `JsonApiError` is for a failure whose wire answer is already decided at the throw site - a guard refusing
98
+ a caller, or a gateway translating what a fronted service returned. It takes a `BASE_ERROR_CODES` entry
99
+ for its `{ status, title }` and this transport's own `code` beside it, and may carry `detail`, `source`,
100
+ `links` and `meta`.
101
+
102
+ ```ts
103
+ throw new JsonApiError({ ...BASE_ERROR_CODES.FORBIDDEN, code: '<context>.forbidden' });
104
+ // the spread FIRST: the entry carries no `code` today, and the explicit one has to win the day it does
105
+ ```
106
+
107
+ Every error document renders through `toErrorDocument`, which stamps `jsonapi: { version: '1.1' }` as a
108
+ success document carries it. No producer sets an error object's `id`: the handler stamps the request id,
109
+ so a client quotes the id the request's log lines carry.
110
+
111
+ ## A fronted service's failure
112
+
113
+ A gateway never re-emits the body a service below it rendered: that `code` names a namespace the public
114
+ contract never declared, and that message was written for the gateway, which is the service's only caller.
115
+ So the gateway owns a **second** map - keyed by the upstream `code`, because a rendered document carries no
116
+ class and `defineErrorMap` resolves by constructor identity, which cannot reach one.
117
+
118
+ It is a plain object, not a `Map`: the key is a string the source spells literally, so an object literal
119
+ already carries the lookup and the literal keys. An entry earns its place by producing an answer no other
120
+ entry produces - two keys reaching one `{ status, code }` is the fallback's job.
121
+
122
+ ```ts
123
+ // entrypoints/http/error-map.ts - the gateway answers in ITS OWN vocabulary, and the status moves with it:
124
+ // a lifecycle the public contract never published is no conflict to the caller, it is an absence.
125
+ import {
126
+ createUpstreamErrorTranslator,
127
+ type Meta,
128
+ type UpstreamErrorAnswer,
129
+ type UpstreamErrorMap,
130
+ } from '@zeroxsolutions/response';
131
+
132
+ // 502, not 500: reaching the fallback means the gateway did not RECOGNISE what its dependency said, which is
133
+ // what 502 states - 500 would claim the gateway itself broke, and a client retries the two differently.
134
+ export const PLACE_UPSTREAM_FALLBACK = {
135
+ status: 502,
136
+ title: 'Bad Gateway',
137
+ code: 'api.upstream_failed',
138
+ } as const satisfies UpstreamErrorAnswer;
139
+
140
+ export const PLACE_UPSTREAM_ERRORS = {
141
+ 'place.place.not_published': {
142
+ status: 404,
143
+ title: 'Not Found',
144
+ code: 'api.place.unavailable',
145
+ detail: 'This place is not available.',
146
+ },
147
+ 'place.place.full': {
148
+ status: 409,
149
+ title: 'Conflict',
150
+ code: 'api.place.full',
151
+ // the return type is spelled: a callback in an object checked by `satisfies` alone is not a typed function
152
+ // expression to @typescript-eslint/explicit-function-return-type
153
+ meta: (upstream): Meta | undefined => {
154
+ const parsed = placeFullMetaSchema.safeParse(upstream); // what the service promises, and nothing else
155
+ return parsed.success ? parsed.data : undefined;
156
+ },
157
+ },
158
+ } satisfies UpstreamErrorMap;
159
+
160
+ export const translatePlaceUpstreamError = createUpstreamErrorTranslator(PLACE_UPSTREAM_ERRORS, PLACE_UPSTREAM_FALLBACK);
161
+ ```
162
+
163
+ A route throws what the translator returns, so the one `onError` renders it like every other failure:
164
+
165
+ ```ts
166
+ // entrypoints/http/routes/places.ts - the route declares EVERY status it can emit, including its own 500, or
167
+ // `ok` has nothing to narrow to. `ok` is typed `U extends SuccessStatusCode ? true : false` per declared
168
+ // status (hono@4.13.3 dist/types/client/types.d.ts:108), so `!ok` IS the error arm - a `status !== 200` test
169
+ // calls a declared 201 or 204 a failure.
170
+ import type { ErrorDocument } from '@zeroxsolutions/response';
171
+
172
+ const upstream = await c.var.place.v1.places[':placeId'].$get({ param });
173
+ if (!upstream.ok) {
174
+ const body: ErrorDocument = await upstream.json();
175
+ throw translatePlaceUpstreamError(body);
176
+ }
177
+ ```
178
+
179
+ The translator reads the code off the document's FIRST error object, because `createErrorHandler` renders
180
+ exactly one error per failure. Of that error it keeps only what an answer's `meta` reads: its `source`
181
+ pointers name the SERVICE's request shape, not the one this caller sent, so echoing them would point a
182
+ client at members it has no way to correct.
183
+
184
+ A `meta` callback returns its parse, never `upstream` as received - the schema is what keeps a member the
185
+ service adds later from crossing. Returning `undefined` answers the fallback: a mapped code missing the
186
+ data its contract promises is an answer the gateway does not recognise.
187
+
188
+ A request the route's own schema rejects reaches that same `onError`. `@hono/zod-validator` (0.9.0)
189
+ answers a rejected parse with its own `c.json(result, 400)` unless a hook answers or throws first, so
190
+ `createValidationHook` is passed to the constructor, and it throws the rejection for `createErrorHandler`
191
+ to render.
192
+
193
+ ```ts
194
+ // entrypoints/http/app.ts - the same shape for a request the route's own schema rejects
195
+ import { createValidationHook } from '@zeroxsolutions/server';
196
+
197
+ const app = new OpenAPIHono<AppEnvironment>({
198
+ defaultHook: createValidationHook<AppEnvironment>({ namespace: 'academics' }),
199
+ });
200
+ ```
201
+
202
+ It reads the status off the validation target - a `json` or `form` target is `422`, a
203
+ param/query/header/cookie one is `400` - and renders **every** offending member as its own error object,
204
+ not just the first. The target is the validator's own fact rather than a product's choice, so nothing here
205
+ is configurable: an option would let two transports answer one failure two ways, which is the uniformity
206
+ this package exists to hold.
207
+
208
+ It locates each issue by the slot it was read from: a body member as `source.pointer`, a query member as
209
+ `source.parameter`, a header as `source.header`. A path segment gets `meta` instead - the spec defines
210
+ those three members and none of them means a path segment, so borrowing one would say something false.
211
+
212
+ ## Exports
213
+
214
+ One root, no subpath, and `src/index.ts` lists every name it publishes:
215
+
216
+ - `okResponse` / `listResponse` / `errorResponse`, the `JsonApiResponse` they return (exported so a
217
+ consumer's declaration emit can name it), and `errorDocumentSchema`, which `errorResponse` returns
218
+ inside it;
219
+ - the resource declaration - `jsonApiResource`, `toOne` / `toMany`, `declaredDocumentShape` - and the
220
+ types their signatures name;
221
+ - the error document's types, `JsonApiError`, `BASE_ERROR_CODES`, `toErrorObject` / `toErrorDocument`;
222
+ - `defineErrorMap` / `errorEntry` / `createDomainErrorResolver` and `createUpstreamErrorTranslator`, with
223
+ the answer, entry and map types they take;
224
+ - `JSON_API_MEDIA_TYPE`.
225
+
226
+ The document and resource-object schemas, the link and identifier primitives and the serializer
227
+ functions are what `jsonApiResource` is built from, and the root does not publish them: a resource's own
228
+ `document`, `collectionDocument`, `serialize` and `serializeMany` are the one way to declare and render a
229
+ document.
230
+
231
+ `zod` is the only peer. `hono` left with the plain spelling: nothing here needs its
232
+ `ContentfulStatusCode`, because a JSON:API status is carried by the error object.
233
+
234
+ The content-negotiation DECISION for the JSON:API media type and the client-side document deserializer
235
+ are **not** here - they are request- and client-side, and live in `@zeroxsolutions/jsonapi`. Everything
236
+ that is a Hono handler - the `onError`, the `defaultHook`, the negotiation middleware - is
237
+ `@zeroxsolutions/server`.
@@ -0,0 +1,24 @@
1
+ import type { output, ZodObject, ZodType } from 'zod';
2
+ import type { CollectionDocument, ResourceDocument } from './document.js';
3
+ import { errorDocumentSchema } from './errors/index.js';
4
+ import { JSON_API_MEDIA_TYPE } from './media-type.js';
5
+ import type { JsonApiResource } from './resource.js';
6
+ /**
7
+ * An OpenAPI response object for this spelling - one media type, one schema.
8
+ *
9
+ * Exported because a consumer emitting declarations has to NAME it: a route built from these helpers
10
+ * is an exported const whose type mentions it, and a private one fails that emit with TS4023.
11
+ */
12
+ export interface JsonApiResponse<S extends ZodType> {
13
+ description: string;
14
+ content: Record<typeof JSON_API_MEDIA_TYPE, {
15
+ schema: S;
16
+ }>;
17
+ }
18
+ /** The response object for a failure status - the `errors[]` document. */
19
+ export declare function errorResponse(description: string): JsonApiResponse<typeof errorDocumentSchema>;
20
+ /** The response object for a single resource, off the resource's own declaration. */
21
+ export declare function okResponse<A extends ZodObject>(description: string, resource: JsonApiResource<A>): JsonApiResponse<ZodType<ResourceDocument<output<A>>>>;
22
+ /** The response object for a collection, off the resource's own declaration. */
23
+ export declare function listResponse<A extends ZodObject>(description: string, resource: JsonApiResource<A>): JsonApiResponse<ZodType<CollectionDocument<output<A>>>>;
24
+ //# sourceMappingURL=api-response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-response.d.ts","sourceRoot":"","sources":["../src/api-response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AACtD,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD;;;;;GAKG;AACH,MAAM,WAAW,eAAe,CAAC,CAAC,SAAS,OAAO;IAChD,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC,OAAO,mBAAmB,EAAE;QAAE,MAAM,EAAE,CAAC,CAAA;KAAE,CAAC,CAAC;CAC5D;AAOD,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,eAAe,CAAC,OAAO,mBAAmB,CAAC,CAE9F;AAED,qFAAqF;AACrF,wBAAgB,UAAU,CAAC,CAAC,SAAS,SAAS,EAC5C,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,eAAe,CAAC,CAAC,CAAC,GAC3B,eAAe,CAAC,OAAO,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAEvD;AAED,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,CAAC,SAAS,SAAS,EAC9C,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,eAAe,CAAC,CAAC,CAAC,GAC3B,eAAe,CAAC,OAAO,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAEzD"}
@@ -0,0 +1,18 @@
1
+ import { errorDocumentSchema } from './errors/index.js';
2
+ import { JSON_API_MEDIA_TYPE } from './media-type.js';
3
+ const answers = (description, schema) => ({
4
+ description,
5
+ content: { [JSON_API_MEDIA_TYPE]: { schema } },
6
+ });
7
+ /** The response object for a failure status - the `errors[]` document. */
8
+ export function errorResponse(description) {
9
+ return answers(description, errorDocumentSchema);
10
+ }
11
+ /** The response object for a single resource, off the resource's own declaration. */
12
+ export function okResponse(description, resource) {
13
+ return answers(description, resource.document);
14
+ }
15
+ /** The response object for a collection, off the resource's own declaration. */
16
+ export function listResponse(description, resource) {
17
+ return answers(description, resource.collectionDocument);
18
+ }
@@ -0,0 +1,17 @@
1
+ import { type RelatedResource } from './relationship.js';
2
+ /** The fieldsets and relationship paths one resource declaration can answer. */
3
+ export interface DeclaredDocumentShape {
4
+ /** Every reachable type with the fields it publishes: its attribute names, then its relationship names. */
5
+ fields: Record<string, string[]>;
6
+ /** Every relationship path from the root, dotted for a nested one. */
7
+ include: string[];
8
+ }
9
+ /**
10
+ * What a client may ask a document to be shaped as, read off the declaration itself - the permit-list a query guard
11
+ * needs, without a second copy of it.
12
+ *
13
+ * The attribute keys ARE the projection, so a list written by hand beside them permits a column the resource stopped
14
+ * publishing, or refuses one it started, with both halves compiling and both suites green.
15
+ */
16
+ export declare function declaredDocumentShape(root: RelatedResource): DeclaredDocumentShape;
17
+ //# sourceMappingURL=document-shape.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-shape.d.ts","sourceRoot":"","sources":["../src/document-shape.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAE7E,gFAAgF;AAChF,MAAM,WAAW,qBAAqB;IACpC,2GAA2G;IAC3G,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACjC,sEAAsE;IACtE,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,eAAe,GAAG,qBAAqB,CAuBlF"}
@@ -0,0 +1,29 @@
1
+ import { reachableResources } from './relationship.js';
2
+ /**
3
+ * What a client may ask a document to be shaped as, read off the declaration itself - the permit-list a query guard
4
+ * needs, without a second copy of it.
5
+ *
6
+ * The attribute keys ARE the projection, so a list written by hand beside them permits a column the resource stopped
7
+ * publishing, or refuses one it started, with both halves compiling and both suites green.
8
+ */
9
+ export function declaredDocumentShape(root) {
10
+ const fields = Object.fromEntries(reachableResources(root).map((resource) => [
11
+ resource.type,
12
+ [...Object.keys(resource.attributes.shape), ...Object.keys(resource.relationships)],
13
+ ]));
14
+ const include = [];
15
+ // A path never revisits a type: two resources naming each other make `class.subject.class...` infinite, and the
16
+ // second lap addresses the same rows the first one already compounded.
17
+ const walk = (resource, prefix, seen) => {
18
+ for (const [name, def] of Object.entries(resource.relationships)) {
19
+ const related = def.related();
20
+ if (seen.has(related.type))
21
+ continue;
22
+ const path = prefix === '' ? name : `${prefix}.${name}`;
23
+ include.push(path);
24
+ walk(related, path, new Set([...seen, related.type]));
25
+ }
26
+ };
27
+ walk(root, '', new Set([root.type]));
28
+ return { fields, include };
29
+ }