@zeroxsolutions/jsonapi 0.11.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/CHANGELOG.md +367 -0
- package/README.md +70 -0
- package/dist/client/api-error.d.ts +14 -0
- package/dist/client/api-error.d.ts.map +1 -0
- package/dist/client/api-error.js +17 -0
- package/dist/client/deserialize.d.ts +11 -0
- package/dist/client/deserialize.d.ts.map +1 -0
- package/dist/client/deserialize.js +14 -0
- package/dist/client/index.d.ts +7 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +6 -0
- package/dist/client/read-document.d.ts +51 -0
- package/dist/client/read-document.d.ts.map +1 -0
- package/dist/client/read-document.js +40 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/negotiation/index.d.ts +3 -0
- package/dist/lib/negotiation/index.d.ts.map +1 -0
- package/dist/lib/negotiation/index.js +2 -0
- package/dist/lib/negotiation/media-type.d.ts +10 -0
- package/dist/lib/negotiation/media-type.d.ts.map +1 -0
- package/dist/lib/negotiation/media-type.js +16 -0
- package/dist/lib/negotiation/negotiate.d.ts +18 -0
- package/dist/lib/negotiation/negotiate.d.ts.map +1 -0
- package/dist/lib/negotiation/negotiate.js +47 -0
- package/package.json +35 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
## 0.11.0 (2026-09-13)
|
|
2
|
+
|
|
3
|
+
### 💅 Refactors
|
|
4
|
+
|
|
5
|
+
- ⚠️ **jsonapi:** list the names each entry publishes ([3746762](https://github.com/zeroxsolutions/zeroxsolutions/commit/3746762))
|
|
6
|
+
|
|
7
|
+
### ⚠️ Breaking Changes
|
|
8
|
+
|
|
9
|
+
- **jsonapi:** list the names each entry publishes ([3746762](https://github.com/zeroxsolutions/zeroxsolutions/commit/3746762))
|
|
10
|
+
the root no longer exports `parseMediaType`,
|
|
11
|
+
`isJsonApiWithDisallowedParams` or `ParsedMediaType`; `negotiateJsonApi`
|
|
12
|
+
is the negotiation decision. `./client` publishes the same names as
|
|
13
|
+
before, from `dist/client/index.js`.
|
|
14
|
+
|
|
15
|
+
### ❤️ Thank You
|
|
16
|
+
|
|
17
|
+
- Claude Sonnet 5
|
|
18
|
+
- Lương Văn Tú
|
|
19
|
+
|
|
20
|
+
## 0.10.4 (2026-09-13)
|
|
21
|
+
|
|
22
|
+
This was a version bump only for @zeroxsolutions/jsonapi to align it with other projects, there were no code changes.
|
|
23
|
+
|
|
24
|
+
## 0.10.3 (2026-09-13)
|
|
25
|
+
|
|
26
|
+
This was a version bump only for @zeroxsolutions/jsonapi to align it with other projects, there were no code changes.
|
|
27
|
+
|
|
28
|
+
## 0.10.2 (2026-09-13)
|
|
29
|
+
|
|
30
|
+
### 🧱 Updated Dependencies
|
|
31
|
+
|
|
32
|
+
- Updated @zeroxsolutions/response to 0.11.2
|
|
33
|
+
|
|
34
|
+
## 0.10.1 (2026-09-07)
|
|
35
|
+
|
|
36
|
+
Version bump only, to take the repackaged `@zeroxsolutions/response`.
|
|
37
|
+
|
|
38
|
+
## 0.10.0 (2026-09-07)
|
|
39
|
+
|
|
40
|
+
### 🚀 Features
|
|
41
|
+
|
|
42
|
+
- ⚠️ **server,jsonapi:** one surface, one error-code vocabulary ([32b4aed](https://github.com/zeroxsolutions/zeroxsolutions/commit/32b4aed))
|
|
43
|
+
|
|
44
|
+
### ⚠️ Breaking Changes
|
|
45
|
+
|
|
46
|
+
- **server,jsonapi:** one surface, one error-code vocabulary ([32b4aed](https://github.com/zeroxsolutions/zeroxsolutions/commit/32b4aed))
|
|
47
|
+
createErrorHandler, createValidationHook and
|
|
48
|
+
jsonApiContentNegotiation take a required `namespace`; negotiateJsonApi takes it
|
|
49
|
+
as a third argument. Codes these mint are now `<namespace>.<name>`.
|
|
50
|
+
|
|
51
|
+
### ❤️ Thank You
|
|
52
|
+
|
|
53
|
+
- Lương Văn Tú
|
|
54
|
+
|
|
55
|
+
## 0.9.2 (2026-09-07)
|
|
56
|
+
|
|
57
|
+
### 💅 Refactors
|
|
58
|
+
|
|
59
|
+
- ⚠️ **query,response,server:** one wire spelling, flat exports ([5c57b3a](https://github.com/zeroxsolutions/zeroxsolutions/commit/5c57b3a))
|
|
60
|
+
|
|
61
|
+
### ⚠️ Breaking Changes
|
|
62
|
+
|
|
63
|
+
- **query,response,server:** one wire spelling, flat exports ([5c57b3a](https://github.com/zeroxsolutions/zeroxsolutions/commit/5c57b3a))
|
|
64
|
+
`@zeroxsolutions/response/http`, `@zeroxsolutions/query/http`
|
|
65
|
+
and `@zeroxsolutions/server/hono/http` are gone, and the surviving `/jsonapi`
|
|
66
|
+
and `/hono` subpaths move to the package root. Every import drops its subpath.
|
|
67
|
+
|
|
68
|
+
### ❤️ Thank You
|
|
69
|
+
|
|
70
|
+
- Lương Văn Tú
|
|
71
|
+
|
|
72
|
+
## 0.9.1 (2026-09-07)
|
|
73
|
+
|
|
74
|
+
This was a version bump only for @zeroxsolutions/jsonapi to align it with other projects, there were no code changes.
|
|
75
|
+
|
|
76
|
+
## 0.9.0 (2026-09-07)
|
|
77
|
+
|
|
78
|
+
### 💅 Refactors
|
|
79
|
+
|
|
80
|
+
- ⚠️ **jsonapi:** widen negotiation, move its middleware out ([d28eb2b](https://github.com/zeroxsolutions/zeroxsolutions/commit/d28eb2b))
|
|
81
|
+
|
|
82
|
+
### ⚠️ Breaking Changes
|
|
83
|
+
|
|
84
|
+
- **jsonapi:** widen negotiation, move its middleware out ([d28eb2b](https://github.com/zeroxsolutions/zeroxsolutions/commit/d28eb2b))
|
|
85
|
+
jsonApiContentNegotiation is no longer exported here; it is
|
|
86
|
+
@zeroxsolutions/server. negotiateJsonApi answers 415 for any
|
|
87
|
+
Content-Type that is not the JSON:API media type, and 406 for an Accept naming
|
|
88
|
+
no range this surface answers, an empty one included - headers that previously
|
|
89
|
+
passed. Its errors now carry `detail` and `source`.
|
|
90
|
+
|
|
91
|
+
### ❤️ Thank You
|
|
92
|
+
|
|
93
|
+
- Claude Opus 5 (1M context)
|
|
94
|
+
- Lương Văn Tú
|
|
95
|
+
|
|
96
|
+
## 0.8.4 (2026-09-03)
|
|
97
|
+
|
|
98
|
+
### 🧱 Updated Dependencies
|
|
99
|
+
|
|
100
|
+
- Updated @zeroxsolutions/response to 0.7.3
|
|
101
|
+
|
|
102
|
+
## 0.8.3 (2026-09-03)
|
|
103
|
+
|
|
104
|
+
### 🧱 Updated Dependencies
|
|
105
|
+
|
|
106
|
+
- Updated @zeroxsolutions/response to 0.7.2
|
|
107
|
+
|
|
108
|
+
## 0.8.2 (2026-09-03)
|
|
109
|
+
|
|
110
|
+
### 🧱 Updated Dependencies
|
|
111
|
+
|
|
112
|
+
- Updated @zeroxsolutions/response to 0.7.1
|
|
113
|
+
|
|
114
|
+
## 0.8.1 (2026-09-02)
|
|
115
|
+
|
|
116
|
+
### 🚀 Features
|
|
117
|
+
|
|
118
|
+
- **jsonapi:** read the related model an include resolved ([6faf408](https://github.com/zeroxsolutions/zeroxsolutions/commit/6faf408))
|
|
119
|
+
|
|
120
|
+
### ❤️ Thank You
|
|
121
|
+
|
|
122
|
+
- Lương Văn Tú
|
|
123
|
+
|
|
124
|
+
## 0.8.0 (2026-09-02)
|
|
125
|
+
|
|
126
|
+
### 🚀 Features
|
|
127
|
+
|
|
128
|
+
- **jsonapi:** carry a failed response as ApiError on the client half ([71e9554](https://github.com/zeroxsolutions/zeroxsolutions/commit/71e9554))
|
|
129
|
+
|
|
130
|
+
### 🩹 Fixes
|
|
131
|
+
|
|
132
|
+
- ⚠️ **jsonapi:** shape the collection model on the document, not the other spelling ([4ae8d26](https://github.com/zeroxsolutions/zeroxsolutions/commit/4ae8d26))
|
|
133
|
+
- **jsonapi:** add the reader source the previous commit left untracked ([7399114](https://github.com/zeroxsolutions/zeroxsolutions/commit/7399114))
|
|
134
|
+
|
|
135
|
+
### ⚠️ Breaking Changes
|
|
136
|
+
|
|
137
|
+
- **jsonapi:** shape the collection model on the document, not the other spelling ([4ae8d26](https://github.com/zeroxsolutions/zeroxsolutions/commit/4ae8d26))
|
|
138
|
+
`Collection.items` is `Collection.data`, and `Collection.total`
|
|
139
|
+
is gone - read `meta` for it.
|
|
140
|
+
|
|
141
|
+
### ❤️ Thank You
|
|
142
|
+
|
|
143
|
+
- Lương Văn Tú
|
|
144
|
+
|
|
145
|
+
## 0.7.9 (2026-09-02)
|
|
146
|
+
|
|
147
|
+
### 🚀 Features
|
|
148
|
+
|
|
149
|
+
- **jsonapi:** read a document into a model typed off the route ([02d2205](https://github.com/zeroxsolutions/zeroxsolutions/commit/02d2205))
|
|
150
|
+
|
|
151
|
+
### ❤️ Thank You
|
|
152
|
+
|
|
153
|
+
- Lương Văn Tú
|
|
154
|
+
|
|
155
|
+
## 0.7.8 (2026-09-02)
|
|
156
|
+
|
|
157
|
+
### 🧱 Updated Dependencies
|
|
158
|
+
|
|
159
|
+
- Updated @zeroxsolutions/response to 0.7.0
|
|
160
|
+
|
|
161
|
+
## 0.7.7 (2026-09-02)
|
|
162
|
+
|
|
163
|
+
### 🧱 Updated Dependencies
|
|
164
|
+
|
|
165
|
+
- Updated @zeroxsolutions/response to 0.6.1
|
|
166
|
+
|
|
167
|
+
## 0.7.6 (2026-09-02)
|
|
168
|
+
|
|
169
|
+
### 🧱 Updated Dependencies
|
|
170
|
+
|
|
171
|
+
- Updated @zeroxsolutions/response to 0.6.0
|
|
172
|
+
|
|
173
|
+
## 0.7.5 (2026-09-02)
|
|
174
|
+
|
|
175
|
+
### 🧱 Updated Dependencies
|
|
176
|
+
|
|
177
|
+
- Updated @zeroxsolutions/response to 0.5.0
|
|
178
|
+
|
|
179
|
+
## 0.7.4 (2026-08-31)
|
|
180
|
+
|
|
181
|
+
### 🧱 Updated Dependencies
|
|
182
|
+
|
|
183
|
+
- Updated @zeroxsolutions/response to 0.4.0
|
|
184
|
+
|
|
185
|
+
## 0.7.3 (2026-08-31)
|
|
186
|
+
|
|
187
|
+
### 🧱 Updated Dependencies
|
|
188
|
+
|
|
189
|
+
- Updated @zeroxsolutions/response to 0.3.0
|
|
190
|
+
|
|
191
|
+
## 0.7.2 (2026-08-29)
|
|
192
|
+
|
|
193
|
+
### 💅 Refactors
|
|
194
|
+
|
|
195
|
+
- state an explicit return type on every function ([8ee4264](https://github.com/zeroxsolutions/zeroxsolutions/commit/8ee4264))
|
|
196
|
+
|
|
197
|
+
### 🧱 Updated Dependencies
|
|
198
|
+
|
|
199
|
+
- Updated @zeroxsolutions/response to 0.2.2
|
|
200
|
+
|
|
201
|
+
### ❤️ Thank You
|
|
202
|
+
|
|
203
|
+
- Lương Văn Tú
|
|
204
|
+
|
|
205
|
+
## 0.7.1 (2026-08-28)
|
|
206
|
+
|
|
207
|
+
### 🧱 Updated Dependencies
|
|
208
|
+
|
|
209
|
+
- Updated @zeroxsolutions/response to 0.2.1
|
|
210
|
+
|
|
211
|
+
## 0.6.0 (2026-08-16)
|
|
212
|
+
|
|
213
|
+
This was a version bump only for @zeroxsolutions/jsonapi to align it with other projects, there were no code changes.
|
|
214
|
+
|
|
215
|
+
## 0.5.0 (2026-08-15)
|
|
216
|
+
|
|
217
|
+
### 🚀 Features
|
|
218
|
+
|
|
219
|
+
- **jsonapi:** let the query guard raise a plain typed error ([3d2084d](https://github.com/zeroxsolutions/zeroxsolutions/commit/3d2084d))
|
|
220
|
+
|
|
221
|
+
### ❤️ Thank You
|
|
222
|
+
|
|
223
|
+
- Claude Opus 5
|
|
224
|
+
- Lương Văn Tú
|
|
225
|
+
|
|
226
|
+
## 0.4.1 (2026-08-15)
|
|
227
|
+
|
|
228
|
+
### 🚀 Features
|
|
229
|
+
|
|
230
|
+
- **jsonapi:** compose a serialized document, not just validate one ([2135153](https://github.com/zeroxsolutions/zeroxsolutions/commit/2135153))
|
|
231
|
+
|
|
232
|
+
### ❤️ Thank You
|
|
233
|
+
|
|
234
|
+
- Claude Opus 5
|
|
235
|
+
- Lương Văn Tú
|
|
236
|
+
|
|
237
|
+
## 0.4.0 (2026-08-14)
|
|
238
|
+
|
|
239
|
+
### 🚀 Features
|
|
240
|
+
|
|
241
|
+
- ⚠️ **jsonapi:** bind the permit-list to a row and an operator vocabulary, and pipe the schema through it ([5f824d0](https://github.com/zeroxsolutions/zeroxsolutions/commit/5f824d0))
|
|
242
|
+
- **jsonapi:** declare a list endpoint's query parameters from its permit-list ([bb3c3c8](https://github.com/zeroxsolutions/zeroxsolutions/commit/bb3c3c8))
|
|
243
|
+
|
|
244
|
+
### ⚠️ Breaking Changes
|
|
245
|
+
|
|
246
|
+
- **jsonapi:** bind the permit-list to a row and an operator vocabulary, and pipe the schema through it ([5f824d0](https://github.com/zeroxsolutions/zeroxsolutions/commit/5f824d0))
|
|
247
|
+
`jsonApiQuerySchema` now returns a piped schema whose output is
|
|
248
|
+
`GuardedJsonApiQuery`, not the raw query map. Unreleased, so no consumer is affected.
|
|
249
|
+
The two type-level checks are pinned by `@ts-expect-error` in a spec, watched failing
|
|
250
|
+
first: unbinding the generics turns both directives into "unused", which is the whole
|
|
251
|
+
failure mode a type test has.
|
|
252
|
+
|
|
253
|
+
### ❤️ Thank You
|
|
254
|
+
|
|
255
|
+
- Claude Opus 5 (1M context)
|
|
256
|
+
- Lương Văn Tú
|
|
257
|
+
|
|
258
|
+
## 0.3.1 (2026-08-08)
|
|
259
|
+
|
|
260
|
+
### 🩹 Fixes
|
|
261
|
+
|
|
262
|
+
- **jsonapi:** state that an included resource needs its own serializer built from the query ([803aa30](https://github.com/zeroxsolutions/zeroxsolutions/commit/803aa30))
|
|
263
|
+
- **db:** declare the query types once, in the package that owns the wire ([1c3498c](https://github.com/zeroxsolutions/zeroxsolutions/commit/1c3498c))
|
|
264
|
+
|
|
265
|
+
### ❤️ Thank You
|
|
266
|
+
|
|
267
|
+
- Claude Opus 5 (1M context)
|
|
268
|
+
- luongvantuit
|
|
269
|
+
|
|
270
|
+
## 0.3.0 (2026-08-08)
|
|
271
|
+
|
|
272
|
+
### 🚀 Features
|
|
273
|
+
|
|
274
|
+
- ⚠️ **jsonapi:** shape the document by the query, and close the escape hatch ([79da0f2](https://github.com/zeroxsolutions/zeroxsolutions/commit/79da0f2))
|
|
275
|
+
|
|
276
|
+
### ⚠️ Breaking Changes
|
|
277
|
+
|
|
278
|
+
- **jsonapi:** shape the document by the query, and close the escape hatch ([79da0f2](https://github.com/zeroxsolutions/zeroxsolutions/commit/79da0f2))
|
|
279
|
+
`customFamilies` removed from `JsonApiQuerySpec`, its
|
|
280
|
+
argument removed from `unprocessableQueryParams`, and `isCustomFamilyBase` no
|
|
281
|
+
longer exported. No declaration can accept a parameter outside the families
|
|
282
|
+
the wire standard defines.
|
|
283
|
+
|
|
284
|
+
### ❤️ Thank You
|
|
285
|
+
|
|
286
|
+
- Claude Opus 5 (1M context)
|
|
287
|
+
- luongvantuit
|
|
288
|
+
|
|
289
|
+
## 0.2.0 (2026-08-06)
|
|
290
|
+
|
|
291
|
+
### 🚀 Features
|
|
292
|
+
|
|
293
|
+
- ⚠️ **jsonapi:** bind the list query to a permit-list and parse the filter tree ([2c212b0](https://github.com/zeroxsolutions/zeroxsolutions/commit/2c212b0))
|
|
294
|
+
- **jsonapi:** screen query parameters against the standard's grammar ([e7bd26c](https://github.com/zeroxsolutions/zeroxsolutions/commit/e7bd26c))
|
|
295
|
+
- **jsonapi:** refuse a query the endpoint did not permit ([1dd1df6](https://github.com/zeroxsolutions/zeroxsolutions/commit/1dd1df6))
|
|
296
|
+
|
|
297
|
+
### ⚠️ Breaking Changes
|
|
298
|
+
|
|
299
|
+
- **jsonapi:** bind the list query to a permit-list and parse the filter tree ([2c212b0](https://github.com/zeroxsolutions/zeroxsolutions/commit/2c212b0))
|
|
300
|
+
`parseJsonApiQuery` and `parseFilter` are removed;
|
|
301
|
+
`filterable` entries are `{ operators, type }` rather than a bare operator
|
|
302
|
+
list; `filter` is a bracketed family, not a JSON-encoded array; queries
|
|
303
|
+
that previously passed unnoticed now receive 400.
|
|
304
|
+
|
|
305
|
+
### ❤️ Thank You
|
|
306
|
+
|
|
307
|
+
- Claude Opus 5 (1M context)
|
|
308
|
+
- luongvantuit
|
|
309
|
+
|
|
310
|
+
## 0.1.1 (2026-07-15)
|
|
311
|
+
|
|
312
|
+
### 🚀 Features
|
|
313
|
+
|
|
314
|
+
- **jsonapi:** allow typing the compound-document included array ([f1faa07](https://github.com/zeroxsolutions/zeroxsolutions/commit/f1faa07))
|
|
315
|
+
|
|
316
|
+
### ❤️ Thank You
|
|
317
|
+
|
|
318
|
+
- Claude Opus 4.8
|
|
319
|
+
- Lương Văn Tú
|
|
320
|
+
|
|
321
|
+
## 0.1.0 (2026-07-08)
|
|
322
|
+
|
|
323
|
+
This was a version bump only for @zeroxsolutions/jsonapi to align it with other projects, there were no code changes.
|
|
324
|
+
|
|
325
|
+
## 0.0.4 (2026-07-06)
|
|
326
|
+
|
|
327
|
+
### 🚀 Features
|
|
328
|
+
|
|
329
|
+
- **jsonapi:** add a server error handler + error response to /server ([18eedc2](https://github.com/zeroxsolutions/zeroxsolutions/commit/18eedc2))
|
|
330
|
+
|
|
331
|
+
### ❤️ Thank You
|
|
332
|
+
|
|
333
|
+
- Claude Opus 4.8 (1M context)
|
|
334
|
+
- Lương Văn Tú
|
|
335
|
+
|
|
336
|
+
## 0.0.3 (2026-07-05)
|
|
337
|
+
|
|
338
|
+
This was a version bump only for @zeroxsolutions/jsonapi to align it with other projects, there were no code changes.
|
|
339
|
+
|
|
340
|
+
## 0.0.2 (2026-07-05)
|
|
341
|
+
|
|
342
|
+
### 🚀 Features
|
|
343
|
+
|
|
344
|
+
- **jsonapi:** extract framework-neutral negotiateJsonApi() from the Hono middleware ([e88d8ea](https://github.com/zeroxsolutions/zeroxsolutions/commit/e88d8ea))
|
|
345
|
+
|
|
346
|
+
### ❤️ Thank You
|
|
347
|
+
|
|
348
|
+
- Claude Opus 4.8 (1M context)
|
|
349
|
+
- Lương Văn Tú
|
|
350
|
+
|
|
351
|
+
## 0.0.1 (2026-07-05)
|
|
352
|
+
|
|
353
|
+
### 🚀 Features
|
|
354
|
+
|
|
355
|
+
- ⚠️ add JSON:API v1.1 toolkit and routing lib, dissolve common ([53545a6](https://github.com/zeroxsolutions/zeroxsolutions/commit/53545a6))
|
|
356
|
+
|
|
357
|
+
### ⚠️ Breaking Changes
|
|
358
|
+
|
|
359
|
+
- add JSON:API v1.1 toolkit and routing lib, dissolve common ([53545a6](https://github.com/zeroxsolutions/zeroxsolutions/commit/53545a6))
|
|
360
|
+
@zeroxsolutions/common is removed. Its error/response
|
|
361
|
+
schemas move to @zeroxsolutions/jsonapi; its path/is-match helpers move to
|
|
362
|
+
@zeroxsolutions/routing.
|
|
363
|
+
|
|
364
|
+
### ❤️ Thank You
|
|
365
|
+
|
|
366
|
+
- Claude Opus 4.8 (1M context)
|
|
367
|
+
- Lương Văn Tú
|
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# @zeroxsolutions/jsonapi
|
|
2
|
+
|
|
3
|
+
The [JSON:API v1.1](https://jsonapi.org/) halves that are **not** a response body: content negotiation
|
|
4
|
+
on the request, and document deserialization on the client.
|
|
5
|
+
|
|
6
|
+
Everything a server puts *in* a response - the document schemas, the serializers, the `errors[]`
|
|
7
|
+
document - is `@zeroxsolutions/response`, and the central `onError` that renders it is
|
|
8
|
+
`@zeroxsolutions/server`. The **query families** are `@zeroxsolutions/query`, which speaks this
|
|
9
|
+
standard's spelling (`page[limit]`, a comma-separated `sort`, the labelled `filter` grammar,
|
|
10
|
+
`fields[TYPE]` and `include`).
|
|
11
|
+
|
|
12
|
+
Three packages for one standard, split by which side of the exchange each serves: what the client
|
|
13
|
+
asks for, what the server answers, and what the caller may address.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
pnpm add @zeroxsolutions/jsonapi @zeroxsolutions/response
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
No peer dep, and no framework: every export here is a plain function over strings and documents. The
|
|
22
|
+
Hono middleware that mounts the negotiation decision is `@zeroxsolutions/server`, which is
|
|
23
|
+
what keeps `hono` out of the package an SPA also loads.
|
|
24
|
+
|
|
25
|
+
## Entry points
|
|
26
|
+
|
|
27
|
+
| Import | Use in | What it holds |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| `@zeroxsolutions/jsonapi` | services, gateway | `negotiateJsonApi` - the whole decision, from two header strings to the `JsonApiError` to reject with or `null`: 415 unless a write's media type is JSON:API carrying at most `ext`/`profile`, 406 unless some `Accept` member names that same type or a wildcard range |
|
|
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; and `ApiError`, the non-2xx a caller throws once a response has come back |
|
|
31
|
+
|
|
32
|
+
A transport mounts the decision rather than calling it: `jsonApiContentNegotiation` is the Hono adapter
|
|
33
|
+
over it, and it lives with the other framework wiring. `negotiateJsonApi` itself takes the surface's
|
|
34
|
+
namespace, because the refusal it returns is an error object the surface publishes.
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// one middleware, before the routes - never a per-handler check
|
|
38
|
+
import { jsonApiContentNegotiation } from '@zeroxsolutions/server';
|
|
39
|
+
|
|
40
|
+
app.use('*', jsonApiContentNegotiation({ namespace: 'api' }));
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
A client flattens the envelope ONCE, at the layer that fetches, so nothing downstream reads
|
|
44
|
+
`data[i].attributes`. `ResourceOf` / `CollectionOf` derive that flattened shape from the route's own
|
|
45
|
+
response type, which is what keeps the model off a hand-written interface:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
// the query layer - the only place the document is spelled
|
|
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));
|
|
51
|
+
const { data, meta } = readCollection(await res.json());
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Only `data` is flattened. `links` and `meta` keep the names the standard gives them, and `meta` stays
|
|
55
|
+
free-form - the standard puts no member in it, so a row count there is one surface's own convention.
|
|
56
|
+
|
|
57
|
+
Negotiation raises `JsonApiError` from `@zeroxsolutions/response`, so the refusal renders
|
|
58
|
+
through the same central `onError` as every other failure rather than a shape of its own. It names the
|
|
59
|
+
offending header in `source` and, in `detail`, what the surface reads or answers in - never the value the
|
|
60
|
+
caller sent, which teaches the caller nothing it did not already have.
|
|
61
|
+
|
|
62
|
+
Two decisions the headers alone do not settle:
|
|
63
|
+
|
|
64
|
+
- **`Content-Type` is read only where the request carries a body.** RFC 9110 gives a GET payload no defined
|
|
65
|
+
semantics, so a read route has no representation to describe; reading the header there would turn a
|
|
66
|
+
client-wide default of `application/json` into a 415 on every collection.
|
|
67
|
+
- **An empty `Accept` is refused.** RFC 9110 12.5.1 gives an *absent* header any media type, but an empty
|
|
68
|
+
field-value is a zero-member list, and no member of it names a range this surface can answer. A weight
|
|
69
|
+
goes unread, so a wildcard weighted to zero - a client stating that nothing at all is acceptable - is let
|
|
70
|
+
through; every other unsatisfiable header is caught by the range alone.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ErrorDocument } from '@zeroxsolutions/response';
|
|
2
|
+
/**
|
|
3
|
+
* A non-2xx response, as the client that made the request holds it.
|
|
4
|
+
*
|
|
5
|
+
* The counterpart of `JsonApiError`, which a SERVER raises for its `onError` to render: this one
|
|
6
|
+
* is what a caller throws once a response has already come back. `body` is null wherever the failing
|
|
7
|
+
* tier emitted no document - a proxy, a gateway timeout, or an auth server with a shape of its own.
|
|
8
|
+
*/
|
|
9
|
+
export declare class ApiError extends Error {
|
|
10
|
+
readonly status: number;
|
|
11
|
+
readonly body: ErrorDocument | null;
|
|
12
|
+
constructor(status: number, body: ErrorDocument | null);
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=api-error.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A non-2xx response, as the client that made the request holds it.
|
|
3
|
+
*
|
|
4
|
+
* The counterpart of `JsonApiError`, which a SERVER raises for its `onError` to render: this one
|
|
5
|
+
* is what a caller throws once a response has already come back. `body` is null wherever the failing
|
|
6
|
+
* tier emitted no document - a proxy, a gateway timeout, or an auth server with a shape of its own.
|
|
7
|
+
*/
|
|
8
|
+
export class ApiError extends Error {
|
|
9
|
+
status;
|
|
10
|
+
body;
|
|
11
|
+
constructor(status, body) {
|
|
12
|
+
super(`Request failed with status ${status}`);
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.body = body;
|
|
15
|
+
this.name = 'ApiError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flattens a JSON:API document into plain model object(s) - `{ id, ...attributes }` - via jsona.
|
|
3
|
+
* Returns a single model or an array, mirroring the document's `data`.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* const student = deserializeDocument<Student>(document); // { id, ...attributes }
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
10
|
+
export declare function deserializeDocument<T = unknown>(document: unknown): T;
|
|
11
|
+
//# sourceMappingURL=deserialize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deserialize.d.ts","sourceRoot":"","sources":["../../src/client/deserialize.ts"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,GAAG,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,CAAC,CAErE"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Jsona } from 'jsona';
|
|
2
|
+
const formatter = new Jsona();
|
|
3
|
+
/**
|
|
4
|
+
* Flattens a JSON:API document into plain model object(s) - `{ id, ...attributes }` - via jsona.
|
|
5
|
+
* Returns a single model or an array, mirroring the document's `data`.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* const student = deserializeDocument<Student>(document); // { id, ...attributes }
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export function deserializeDocument(document) {
|
|
13
|
+
return formatter.deserialize(document);
|
|
14
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The client half of the standard: the document a response carries, flattened once, and the failure a caller throws.
|
|
3
|
+
*/
|
|
4
|
+
export { ApiError } from './api-error.js';
|
|
5
|
+
export { deserializeDocument } from './deserialize.js';
|
|
6
|
+
export { readCollection, readRelated, readResource, type Collection, type CollectionOf, type Resource, type ResourceOf, } from './read-document.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA;;GAEG;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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The client half of the standard: the document a response carries, flattened once, and the failure a caller throws.
|
|
3
|
+
*/
|
|
4
|
+
export { ApiError } from './api-error.js';
|
|
5
|
+
export { deserializeDocument } from './deserialize.js';
|
|
6
|
+
export { readCollection, readRelated, readResource, } from './read-document.js';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { CollectionDocument, Meta, PaginationLinks, ResourceDocument } from '@zeroxsolutions/response';
|
|
2
|
+
/** One resource as a client holds it - the attributes, with the resource's own `id` and `type` beside them. */
|
|
3
|
+
export type Resource<A> = A & {
|
|
4
|
+
id: string;
|
|
5
|
+
type: string;
|
|
6
|
+
};
|
|
7
|
+
/** A collection document with its `data` flattened; every other member keeps the name the standard gives it. */
|
|
8
|
+
export interface Collection<A> {
|
|
9
|
+
data: Resource<A>[];
|
|
10
|
+
links?: PaginationLinks;
|
|
11
|
+
meta?: Meta;
|
|
12
|
+
}
|
|
13
|
+
/** The client shape of a single-resource route body, derived from the document that route declares. */
|
|
14
|
+
export type ResourceOf<D> = D extends {
|
|
15
|
+
data: {
|
|
16
|
+
attributes: infer A;
|
|
17
|
+
};
|
|
18
|
+
} ? Resource<A> : never;
|
|
19
|
+
/** The client shape of a collection route body, derived from the document that route declares. */
|
|
20
|
+
export type CollectionOf<D> = D extends {
|
|
21
|
+
data: readonly {
|
|
22
|
+
attributes: infer A;
|
|
23
|
+
}[];
|
|
24
|
+
} ? Collection<A> : never;
|
|
25
|
+
/** Reads a single-resource document into the model a client holds. */
|
|
26
|
+
export declare function readResource<A>(document: ResourceDocument<A>): Resource<A>;
|
|
27
|
+
/**
|
|
28
|
+
* Reads a collection document, flattening `data` and carrying `links` and `meta` through.
|
|
29
|
+
*
|
|
30
|
+
* jsona deserializes `data` alone, so the two top-level members are read here rather than from what
|
|
31
|
+
* {@link deserializeDocument} returns. `meta` stays free-form: the standard puts no member in it, and a
|
|
32
|
+
* count there is one surface's own convention, not a shape this package can promise.
|
|
33
|
+
*/
|
|
34
|
+
export declare function readCollection<A>(document: CollectionDocument<A>): Collection<A>;
|
|
35
|
+
/**
|
|
36
|
+
* The related model an `include` resolved onto `name`, or null where the document carried none.
|
|
37
|
+
*
|
|
38
|
+
* A resource's declared attributes cannot name it: the standard puts a related resource in `included`
|
|
39
|
+
* and leaves `relationships.<name>` carrying linkage alone, so what an include resolved to is reachable
|
|
40
|
+
* at runtime and never from the type a route publishes. The caller therefore names the `type` it expects
|
|
41
|
+
* and gets null for anything else.
|
|
42
|
+
*
|
|
43
|
+
* A relationship the document declared but did NOT include deserializes to that linkage - `{ type, id }`
|
|
44
|
+
* and nothing more - which is answered null rather than handed back as a model missing every attribute.
|
|
45
|
+
* A related resource that declares no attributes of its own is indistinguishable from it.
|
|
46
|
+
*/
|
|
47
|
+
export declare function readRelated<M extends {
|
|
48
|
+
id: string;
|
|
49
|
+
type: string;
|
|
50
|
+
}>(model: unknown, name: string, type: string): M | null;
|
|
51
|
+
//# sourceMappingURL=read-document.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-document.d.ts","sourceRoot":"","sources":["../../src/client/read-document.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,IAAI,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAG5G,+GAA+G;AAC/G,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI,CAAC,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3D,gHAAgH;AAChH,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;IACpB,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,IAAI,CAAC,EAAE,IAAI,CAAC;CACb;AAED,uGAAuG;AACvG,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,CAAC,SAAS;IAAE,IAAI,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC,CAAA;KAAE,CAAA;CAAE,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;AAE9F,kGAAkG;AAClG,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,CAAC,SAAS;IAAE,IAAI,EAAE,SAAS;QAAE,UAAU,EAAE,MAAM,CAAC,CAAA;KAAE,EAAE,CAAA;CAAE,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;AAE7G,sEAAsE;AACtE,wBAAgB,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAE1E;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,QAAQ,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAMhF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAChE,KAAK,EAAE,OAAO,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,CAAC,GAAG,IAAI,CAMV"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { deserializeDocument } from './deserialize.js';
|
|
2
|
+
/** Reads a single-resource document into the model a client holds. */
|
|
3
|
+
export function readResource(document) {
|
|
4
|
+
return deserializeDocument(document);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Reads a collection document, flattening `data` and carrying `links` and `meta` through.
|
|
8
|
+
*
|
|
9
|
+
* jsona deserializes `data` alone, so the two top-level members are read here rather than from what
|
|
10
|
+
* {@link deserializeDocument} returns. `meta` stays free-form: the standard puts no member in it, and a
|
|
11
|
+
* count there is one surface's own convention, not a shape this package can promise.
|
|
12
|
+
*/
|
|
13
|
+
export function readCollection(document) {
|
|
14
|
+
return {
|
|
15
|
+
data: deserializeDocument(document),
|
|
16
|
+
...(document.links ? { links: document.links } : {}),
|
|
17
|
+
...(document.meta ? { meta: document.meta } : {}),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The related model an `include` resolved onto `name`, or null where the document carried none.
|
|
22
|
+
*
|
|
23
|
+
* A resource's declared attributes cannot name it: the standard puts a related resource in `included`
|
|
24
|
+
* and leaves `relationships.<name>` carrying linkage alone, so what an include resolved to is reachable
|
|
25
|
+
* at runtime and never from the type a route publishes. The caller therefore names the `type` it expects
|
|
26
|
+
* and gets null for anything else.
|
|
27
|
+
*
|
|
28
|
+
* A relationship the document declared but did NOT include deserializes to that linkage - `{ type, id }`
|
|
29
|
+
* and nothing more - which is answered null rather than handed back as a model missing every attribute.
|
|
30
|
+
* A related resource that declares no attributes of its own is indistinguishable from it.
|
|
31
|
+
*/
|
|
32
|
+
export function readRelated(model, name, type) {
|
|
33
|
+
const related = model?.[name];
|
|
34
|
+
if (related === null || typeof related !== 'object')
|
|
35
|
+
return null;
|
|
36
|
+
const candidate = related;
|
|
37
|
+
if (candidate['type'] !== type)
|
|
38
|
+
return null;
|
|
39
|
+
return Object.keys(candidate).some((key) => key !== 'type' && key !== 'id') ? candidate : null;
|
|
40
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { negotiateJsonApi } from './lib/negotiation/index.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/negotiation/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** A parsed media type: its base `type` (lower-cased) and the parameter names present. */
|
|
2
|
+
export interface ParsedMediaType {
|
|
3
|
+
type: string;
|
|
4
|
+
params: string[];
|
|
5
|
+
}
|
|
6
|
+
/** Parses one media-type string (e.g. `application/vnd.api+json; ext="..."`) into its type and param names. */
|
|
7
|
+
export declare function parseMediaType(value: string): ParsedMediaType;
|
|
8
|
+
/** True if the media type is JSON:API but carries a parameter other than `ext`/`profile`. */
|
|
9
|
+
export declare function isJsonApiWithDisallowedParams(parsed: ParsedMediaType): boolean;
|
|
10
|
+
//# sourceMappingURL=media-type.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-type.d.ts","sourceRoot":"","sources":["../../../src/lib/negotiation/media-type.ts"],"names":[],"mappings":"AAKA,0FAA0F;AAC1F,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,+GAA+G;AAC/G,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,eAAe,CAO7D;AAED,6FAA6F;AAC7F,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAE9E"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { JSON_API_MEDIA_TYPE } from '@zeroxsolutions/response';
|
|
2
|
+
/** Media-type parameters JSON:API v1.1 permits on its media type. */
|
|
3
|
+
const ALLOWED_PARAMS = new Set(['ext', 'profile']);
|
|
4
|
+
/** Parses one media-type string (e.g. `application/vnd.api+json; ext="..."`) into its type and param names. */
|
|
5
|
+
export function parseMediaType(value) {
|
|
6
|
+
const [rawType, ...rest] = value.split(';');
|
|
7
|
+
const type = (rawType ?? '').trim().toLowerCase();
|
|
8
|
+
const params = rest
|
|
9
|
+
.map((part) => part.split('=')[0]?.trim().toLowerCase())
|
|
10
|
+
.filter((name) => Boolean(name));
|
|
11
|
+
return { type, params };
|
|
12
|
+
}
|
|
13
|
+
/** True if the media type is JSON:API but carries a parameter other than `ext`/`profile`. */
|
|
14
|
+
export function isJsonApiWithDisallowedParams(parsed) {
|
|
15
|
+
return parsed.type === JSON_API_MEDIA_TYPE && parsed.params.some((param) => !ALLOWED_PARAMS.has(param));
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { JsonApiError } from '@zeroxsolutions/response';
|
|
2
|
+
/**
|
|
3
|
+
* Framework-neutral JSON:API v1.1 content-negotiation decision computed from request headers:
|
|
4
|
+
* - **415** unless `Content-Type` is the JSON:API media type carrying no parameter other than `ext`/`profile`;
|
|
5
|
+
* - **406** unless some `Accept` member names a wildcard range or that same clean JSON:API media type.
|
|
6
|
+
*
|
|
7
|
+
* An EMPTY `Accept` is refused, deliberately: RFC 9110 12.5.1 gives an ABSENT header any media type, and an
|
|
8
|
+
* empty field-value is a zero-member list rather than an absent header, so nothing in it names a range this
|
|
9
|
+
* surface can answer. A weight goes unread, so a wildcard weighted to zero - a client stating that nothing at
|
|
10
|
+
* all is acceptable - is let through; every other unsatisfiable header is caught by the range alone.
|
|
11
|
+
*
|
|
12
|
+
* @param contentType - `undefined` where the request carries no content: RFC 9110 gives a GET payload no
|
|
13
|
+
* defined semantics, so reading it there would make every 415 on a read route a false one
|
|
14
|
+
* @param namespace - the vocabulary this surface's own codes carry, so a refusal reads as one of them
|
|
15
|
+
* @returns the {@link JsonApiError} to reject with, or `null` when negotiation passes
|
|
16
|
+
*/
|
|
17
|
+
export declare function negotiateJsonApi(contentType: string | undefined, accept: string | undefined, namespace: string): JsonApiError | null;
|
|
18
|
+
//# sourceMappingURL=negotiate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"negotiate.d.ts","sourceRoot":"","sources":["../../../src/lib/negotiation/negotiate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,YAAY,EAAuB,MAAM,0BAA0B,CAAC;AAY/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,SAAS,EAAE,MAAM,GAChB,YAAY,GAAG,IAAI,CAuBrB"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { BASE_ERROR_CODES, JsonApiError, JSON_API_MEDIA_TYPE } from '@zeroxsolutions/response';
|
|
2
|
+
import { isJsonApiWithDisallowedParams, parseMediaType } from './media-type.js';
|
|
3
|
+
/** The ranges an `Accept` member may name and still be satisfied by the one type a JSON:API surface answers in. */
|
|
4
|
+
const WILDCARD_RANGES = new Set(['application/*', '*/*']);
|
|
5
|
+
/** True if this `Accept` member names a range the surface can answer: a wildcard, or clean JSON:API. */
|
|
6
|
+
function isAcceptable(parsed) {
|
|
7
|
+
if (WILDCARD_RANGES.has(parsed.type))
|
|
8
|
+
return true;
|
|
9
|
+
return parsed.type === JSON_API_MEDIA_TYPE && !isJsonApiWithDisallowedParams(parsed);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Framework-neutral JSON:API v1.1 content-negotiation decision computed from request headers:
|
|
13
|
+
* - **415** unless `Content-Type` is the JSON:API media type carrying no parameter other than `ext`/`profile`;
|
|
14
|
+
* - **406** unless some `Accept` member names a wildcard range or that same clean JSON:API media type.
|
|
15
|
+
*
|
|
16
|
+
* An EMPTY `Accept` is refused, deliberately: RFC 9110 12.5.1 gives an ABSENT header any media type, and an
|
|
17
|
+
* empty field-value is a zero-member list rather than an absent header, so nothing in it names a range this
|
|
18
|
+
* surface can answer. A weight goes unread, so a wildcard weighted to zero - a client stating that nothing at
|
|
19
|
+
* all is acceptable - is let through; every other unsatisfiable header is caught by the range alone.
|
|
20
|
+
*
|
|
21
|
+
* @param contentType - `undefined` where the request carries no content: RFC 9110 gives a GET payload no
|
|
22
|
+
* defined semantics, so reading it there would make every 415 on a read route a false one
|
|
23
|
+
* @param namespace - the vocabulary this surface's own codes carry, so a refusal reads as one of them
|
|
24
|
+
* @returns the {@link JsonApiError} to reject with, or `null` when negotiation passes
|
|
25
|
+
*/
|
|
26
|
+
export function negotiateJsonApi(contentType, accept, namespace) {
|
|
27
|
+
if (contentType !== undefined && !isAcceptable(parseMediaType(contentType))) {
|
|
28
|
+
return new JsonApiError({
|
|
29
|
+
// The entry FIRST: it carries no `code` today, and the explicit one has to win the day it does.
|
|
30
|
+
...BASE_ERROR_CODES.UNSUPPORTED_MEDIA_TYPE,
|
|
31
|
+
code: `${namespace}.unsupported_media_type`,
|
|
32
|
+
// Names what the surface reads rather than what arrived: a failure body is a client-facing projection
|
|
33
|
+
// too, and echoing the caller's own value teaches it nothing it did not send.
|
|
34
|
+
detail: `This surface reads ${JSON_API_MEDIA_TYPE}.`,
|
|
35
|
+
source: { header: 'Content-Type' },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (accept !== undefined && !accept.split(',').map(parseMediaType).some(isAcceptable)) {
|
|
39
|
+
return new JsonApiError({
|
|
40
|
+
...BASE_ERROR_CODES.NOT_ACCEPTABLE,
|
|
41
|
+
code: `${namespace}.not_acceptable`,
|
|
42
|
+
detail: `This surface answers ${JSON_API_MEDIA_TYPE}.`,
|
|
43
|
+
source: { header: 'Accept' },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zeroxsolutions/jsonapi",
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"description": "The JSON:API halves that are not the response body: v1.1 content negotiation (media type, 415/406/400) and the client-side document deserializer. The response document, its serializers and its error document live in @zeroxsolutions/response.",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"CHANGELOG.md",
|
|
15
|
+
"!**/*.tsbuildinfo"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
"./package.json": "./package.json",
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./client": {
|
|
25
|
+
"types": "./dist/client/index.d.ts",
|
|
26
|
+
"import": "./dist/client/index.js",
|
|
27
|
+
"default": "./dist/client/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"jsona": "^1.12.0",
|
|
32
|
+
"tslib": "^2.3.0",
|
|
33
|
+
"@zeroxsolutions/response": "0.12.0"
|
|
34
|
+
}
|
|
35
|
+
}
|