@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.
- package/CHANGELOG.md +330 -0
- package/README.md +237 -0
- package/dist/api-response.d.ts +24 -0
- package/dist/api-response.d.ts.map +1 -0
- package/dist/api-response.js +18 -0
- package/dist/document-shape.d.ts +17 -0
- package/dist/document-shape.d.ts.map +1 -0
- package/dist/document-shape.js +29 -0
- package/dist/document.d.ts +47 -0
- package/dist/document.d.ts.map +1 -0
- package/dist/document.js +54 -0
- package/dist/domain-error-resolver.d.ts +37 -0
- package/dist/domain-error-resolver.d.ts.map +1 -0
- package/dist/domain-error-resolver.js +39 -0
- package/dist/error-map.d.ts +14 -0
- package/dist/error-map.d.ts.map +1 -0
- package/dist/error-map.js +1 -0
- package/dist/errors/error-document.d.ts +189 -0
- package/dist/errors/error-document.d.ts.map +1 -0
- package/dist/errors/error-document.js +37 -0
- package/dist/errors/error-registry.d.ts +49 -0
- package/dist/errors/error-registry.d.ts.map +1 -0
- package/dist/errors/error-registry.js +13 -0
- package/dist/errors/index.d.ts +6 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +5 -0
- package/dist/errors/jsonapi-error.d.ts +27 -0
- package/dist/errors/jsonapi-error.d.ts.map +1 -0
- package/dist/errors/jsonapi-error.js +21 -0
- package/dist/errors/resolved-error.d.ts +7 -0
- package/dist/errors/resolved-error.d.ts.map +1 -0
- package/dist/errors/resolved-error.js +1 -0
- package/dist/errors/to-error-document.d.ts +11 -0
- package/dist/errors/to-error-document.d.ts.map +1 -0
- package/dist/errors/to-error-document.js +25 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/media-type.d.ts +5 -0
- package/dist/media-type.d.ts.map +1 -0
- package/dist/media-type.js +4 -0
- package/dist/primitives.d.ts +161 -0
- package/dist/primitives.d.ts.map +1 -0
- package/dist/primitives.js +47 -0
- package/dist/relationship.d.ts +37 -0
- package/dist/relationship.d.ts.map +1 -0
- package/dist/relationship.js +44 -0
- package/dist/resource.d.ts +35 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/resource.js +63 -0
- package/dist/serialize.d.ts +44 -0
- package/dist/serialize.d.ts.map +1 -0
- package/dist/serialize.js +77 -0
- package/dist/upstream-error.d.ts +34 -0
- package/dist/upstream-error.d.ts.map +1 -0
- package/dist/upstream-error.js +33 -0
- 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
|
+
}
|