@zeroxsolutions/response 0.14.2 → 0.14.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## 0.14.3 (2026-09-30)
2
+
3
+ ### 🚀 Features
4
+
5
+ - **response:** declare a write's request document off its resource ([3eea112](https://github.com/zeroxsolutions/zeroxsolutions/commit/3eea112))
6
+
7
+ ### 🩹 Fixes
8
+
9
+ - **response:** refuse an update that names an attribute with a default ([4e9ee0a](https://github.com/zeroxsolutions/zeroxsolutions/commit/4e9ee0a))
10
+ - **response:** accept the request members JSON:API defines ([35e8395](https://github.com/zeroxsolutions/zeroxsolutions/commit/35e8395))
11
+
12
+ ### ❤️ Thank You
13
+
14
+ - Claude Opus 5.5 (1M context)
15
+ - Lương Văn Tú
16
+
1
17
  ## 0.14.2 (2026-09-29)
2
18
 
3
19
  ### 🩹 Fixes
package/README.md CHANGED
@@ -271,6 +271,42 @@ It locates each issue by the slot it was read from: a body member as `source.poi
271
271
  `source.parameter`, a header as `source.header`. A path segment gets `meta` instead - the spec defines
272
272
  those three members and none of them means a path segment, so borrowing one would say something false.
273
273
 
274
+ ## A request document
275
+
276
+ A write's body is declared off the same resource the route answers with, so the document a client reads
277
+ and the one it writes cannot drift:
278
+
279
+ ```ts
280
+ import { readRequestDocument, requestDocumentSchema } from '@zeroxsolutions/response';
281
+
282
+ export const createItemDocument = requestDocumentSchema(itemResource, 'create', {
283
+ attributes: ['startsAt', 'durationMinutes'],
284
+ relationships: ['place'],
285
+ });
286
+
287
+ // in the handler; relationships.place is the id the linkage named, or null
288
+ const { attributes, relationships } = readRequestDocument(c.req.valid('json'));
289
+ ```
290
+
291
+ `attributes` and `relationships` name what a client may write, checked against the resource at compile
292
+ time. A `'create'` requires `data.type`, refuses `data.id`, and keeps each named attribute as optional as
293
+ its schema makes it; it requires `attributes` itself only where one named attribute is required. An `'update'` requires `data.type` and `data.id`, and every member is optional: a
294
+ missing one keeps its current value. So an update throws when it is declared if it names an attribute
295
+ whose schema fills a value for a missing key - a `.default()`, a `.prefault()` or a `.catch()`, beneath
296
+ any wrapper - since the fill would overwrite the stored value; drop the default from the resource's
297
+ attribute, or leave the attribute out of the update. An attribute whose schema is async is not checked,
298
+ since a sync parse cannot run it. The schema throws `RequestConflict` for a `type` other than the
299
+ endpoint's, and `RequestForbidden` for a client id on a create or a member the route does not write. It
300
+ throws from its first step, out of `safeParse` as well, so `@zeroxsolutions/server`'s error handler
301
+ answers them 409 and 403. A member that fails its own schema stays the validation hook's 422, at
302
+ `/data/attributes/<name>`. On an update, `readRequestDocument(document, { id })` throws `RequestConflict`
303
+ when `data.id` is not the id the URL names.
304
+
305
+ The other members JSON:API 1.1 defines for a request are accepted, and nothing reads them: `lid`, `meta`
306
+ and `links` on the resource object, `meta` and `links` on a relationship object, `lid` and `meta` on an
307
+ identifier, and the top-level `meta` and `jsonapi`. A member the standard does not define still fails the
308
+ schema, so it is a 422.
309
+
274
310
  ## Exports
275
311
 
276
312
  One root, no subpath, and `src/index.ts` lists every name it publishes:
@@ -281,6 +317,8 @@ One root, no subpath, and `src/index.ts` lists every name it publishes:
281
317
  passes to `listResponse`;
282
318
  - the resource declaration - `jsonApiResource`, `toOne` / `toMany`, `declaredDocumentShape` - and the
283
319
  types their signatures name;
320
+ - the request document - `requestDocumentSchema`, `readRequestDocument`, the types their signatures name,
321
+ and `RequestConflict` / `RequestForbidden`, which its checks throw;
284
322
  - the error document's types, `JsonApiError`, `BASE_ERROR_CODES`, `toErrorObject` / `toErrorDocument`;
285
323
  - `defineErrorMap` / `errorEntry` / `createDomainErrorResolver` and `createUpstreamErrorTranslator`, with
286
324
  the answer, entry and map types they take;
package/dist/index.d.ts CHANGED
@@ -16,4 +16,6 @@ export { toMany, toOne, type LoadContextArguments, type RelatedResource, type Re
16
16
  export { jsonApiResource, type JsonApiResource, type ResourceOptions } from './resource.js';
17
17
  export type { CollectionExtras, LoadedRows, RequestedDocumentShape, ResourceSerializer, Row } from './serialize.js';
18
18
  export { createUpstreamErrorTranslator, type UpstreamErrorAnswer } from './upstream-error.js';
19
+ export { readRequestDocument, requestDocumentSchema, type AttributeName, type ReadRequestDocument, type RelationshipName, type RequestDocument, type RequestDocumentSchema, type RequestMode, type WritableResource, } from './request-document.js';
20
+ export { RequestConflict, RequestForbidden } from './request-refusal.js';
19
21
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EACL,aAAa,EACb,YAAY,EACZ,UAAU,EACV,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,YAAY,EACV,kBAAkB,EAClB,qBAAqB,EACrB,YAAY,EACZ,gBAAgB,EAChB,cAAc,GACf,MAAM,eAAe,CAAC;AACvB,cAAc,qBAAqB,CAAC;AACpC,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EACL,yBAAyB,EACzB,cAAc,EACd,UAAU,EACV,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,GACtB,MAAM,4BAA4B,CAAC;AACpC,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACnF,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,aAAa,EACb,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,aAAa,GACnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,2BAA2B,EAAE,2BAA2B,EAAE,MAAM,iBAAiB,CAAC;AAC3F,YAAY,EACV,aAAa,EACb,IAAI,EACJ,IAAI,EACJ,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,aAAa,EACb,aAAa,GACd,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,MAAM,EACN,KAAK,EACL,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,GACzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAC5F,YAAY,EAAE,gBAAgB,EAAE,UAAU,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AACpH,OAAO,EAAE,6BAA6B,EAAE,KAAK,mBAAmB,EAAE,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EACL,aAAa,EACb,YAAY,EACZ,UAAU,EACV,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,YAAY,EACV,kBAAkB,EAClB,qBAAqB,EACrB,YAAY,EACZ,gBAAgB,EAChB,cAAc,GACf,MAAM,eAAe,CAAC;AACvB,cAAc,qBAAqB,CAAC;AACpC,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EACL,yBAAyB,EACzB,cAAc,EACd,UAAU,EACV,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,GACtB,MAAM,4BAA4B,CAAC;AACpC,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACnF,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,aAAa,EACb,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,aAAa,GACnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,2BAA2B,EAAE,2BAA2B,EAAE,MAAM,iBAAiB,CAAC;AAC3F,YAAY,EACV,aAAa,EACb,IAAI,EACJ,IAAI,EACJ,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,aAAa,EACb,aAAa,GACd,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,MAAM,EACN,KAAK,EACL,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,GACzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAC5F,YAAY,EAAE,gBAAgB,EAAE,UAAU,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AACpH,OAAO,EAAE,6BAA6B,EAAE,KAAK,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC9F,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,WAAW,EAChB,KAAK,gBAAgB,GACtB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC"}
package/dist/index.js CHANGED
@@ -12,3 +12,5 @@ export { countedCollectionMetaSchema, offsetPaginationLinksSchema } from './prim
12
12
  export { toMany, toOne, } from './relationship.js';
13
13
  export { jsonApiResource } from './resource.js';
14
14
  export { createUpstreamErrorTranslator } from './upstream-error.js';
15
+ export { readRequestDocument, requestDocumentSchema, } from './request-document.js';
16
+ export { RequestConflict, RequestForbidden } from './request-refusal.js';
@@ -61,10 +61,16 @@ export interface RelationshipDef<C = unknown, R extends RelatedResource<never> =
61
61
  *
62
62
  * The context is inferred from the loader and from the related resource. An explicit `toOne<Ctx>(...)`
63
63
  * would stop TypeScript inferring the related resource, which then widens and drops every literal below it.
64
+ *
65
+ * Its cardinality is the literal 'one', so a request document typed off the declaration tells a to-one from a to-many.
64
66
  */
65
- export declare function toOne<const R extends RelatedResource<never>, C = ContextOf<R>>(related: () => R, read: (row: Row) => unknown, options?: RelationshipOptions<C>): RelationshipDef<NoInfer<C>, R>;
67
+ export declare function toOne<const R extends RelatedResource<never>, C = ContextOf<R>>(related: () => R, read: (row: Row) => unknown, options?: RelationshipOptions<C>): RelationshipDef<NoInfer<C>, R> & {
68
+ readonly cardinality: 'one';
69
+ };
66
70
  /** Declares a to-many relationship - its `data` is an array of identifiers. The context is inferred as for `toOne`. */
67
- export declare function toMany<const R extends RelatedResource<never>, C = ContextOf<R>>(related: () => R, read: (row: Row) => unknown, options?: RelationshipOptions<C>): RelationshipDef<NoInfer<C>, R>;
71
+ export declare function toMany<const R extends RelatedResource<never>, C = ContextOf<R>>(related: () => R, read: (row: Row) => unknown, options?: RelationshipOptions<C>): RelationshipDef<NoInfer<C>, R> & {
72
+ readonly cardinality: 'many';
73
+ };
68
74
  /** The linkage schema for one relationship, pinned to the related `type` so a wrong type fails the parse. */
69
75
  export declare function relationshipSchema<C>(def: RelationshipDef<C>): ZodType;
70
76
  /** The `relationships` member for a whole resource - one key per declaration, absent when it declares none. */
@@ -1 +1 @@
1
- {"version":3,"file":"relationship.d.ts","sourceRoot":"","sources":["../src/relationship.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAE9C,OAAO,KAAK,EAAE,UAAU,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAElG;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,kBAAkB,CAAC,CAAC;IACnC,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,SAAS,GAAG,EAAE,CAAC,CAAC;CAC/D;AAED,kGAAkG;AAClG,MAAM,WAAW,mBAAmB,CAAC,CAAC;IACpC,kHAAkH;IAClH,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC,CAAC,CAAC;CACtC;AAED;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,CAAC,CAAC,IAAI,OAAO,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;AAE/F,sFAAsF;AACtF,wBAAgB,WAAW,CAAC,CAAC,EAAE,gBAAgB,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAAG,CAAC,CAG3E;AAED,6GAA6G;AAC7G,MAAM,WAAW,eAAe,CAAC,CAAC,GAAG,OAAO;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACrE,gHAAgH;IAChH,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,wGAAwG;IACxG,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;OAIG;IACH,aAAa,CAAC,SAAS,CAAC,EAAE,sBAAsB,EAAE,MAAM,CAAC,EAAE,UAAU,GAAG,kBAAkB,CAAC;CAC5F;AAED,6GAA6G;AAC7G,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS,eAAe,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC;AAE5E,uGAAuG;AACvG,MAAM,WAAW,eAAe,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,SAAS,eAAe,CAAC,KAAK,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC;IACjG,QAAQ,CAAC,WAAW,EAAE,KAAK,GAAG,MAAM,CAAC;IACrC,uFAAuF;IACvF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC1B,qGAAqG;IACrG,QAAQ,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC;IACrC,qEAAqE;IACrE,QAAQ,CAAC,IAAI,CAAC,EAAE,kBAAkB,CAAC,CAAC,CAAC,CAAC;CACvC;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,KAAK,CAAC,CAAC,SAAS,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,EAC5E,OAAO,EAAE,MAAM,CAAC,EAChB,IAAI,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,EAC3B,OAAO,CAAC,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAC/B,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAEhC;AAED,uHAAuH;AACvH,wBAAgB,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,EAC7E,OAAO,EAAE,MAAM,CAAC,EAChB,IAAI,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,EAC3B,OAAO,CAAC,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAC/B,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAEhC;AAED,6GAA6G;AAC7G,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,GAAG,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,OAAO,CAWtE;AAED,+GAA+G;AAC/G,wBAAgB,mBAAmB,CAAC,CAAC,EACnC,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,GAC1D,OAAO,GAAG,SAAS,CAIrB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,EAAE,CASpF"}
1
+ {"version":3,"file":"relationship.d.ts","sourceRoot":"","sources":["../src/relationship.ts"],"names":[],"mappings":"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"}
@@ -10,6 +10,8 @@ export function loadContext(contextArguments) {
10
10
  *
11
11
  * The context is inferred from the loader and from the related resource. An explicit `toOne<Ctx>(...)`
12
12
  * would stop TypeScript inferring the related resource, which then widens and drops every literal below it.
13
+ *
14
+ * Its cardinality is the literal 'one', so a request document typed off the declaration tells a to-one from a to-many.
13
15
  */
14
16
  export function toOne(related, read, options) {
15
17
  return { cardinality: 'one', related, read, load: options?.load };
@@ -0,0 +1,183 @@
1
+ import type { input, output, ZodObject, ZodType } from 'zod';
2
+ import { type JsonApiObject, type Meta, type RelationshipLinks, type ResourceIdentifier, type ResourceLinks } from './primitives.js';
3
+ /** Whether a request document creates its resource, or updates the one its URL names. */
4
+ export type RequestMode = 'create' | 'update';
5
+ /** What a request document needs of a relationship: how many it holds, and the type it points at. */
6
+ interface WritableRelationship {
7
+ readonly cardinality: 'one' | 'many';
8
+ readonly related: () => {
9
+ readonly type: string;
10
+ };
11
+ }
12
+ /** What a request document needs of a resource declaration - every `jsonApiResource` is one. */
13
+ export interface WritableResource {
14
+ readonly type: string;
15
+ readonly attributes: ZodObject;
16
+ readonly relationships: Readonly<Record<string, WritableRelationship>>;
17
+ }
18
+ /** The attribute names a resource declares. */
19
+ export type AttributeName<R extends WritableResource> = keyof R['attributes']['shape'] & string;
20
+ /** The relationship names a resource declares, and none where it declares no relationship. */
21
+ export type RelationshipName<R extends WritableResource> = string extends keyof R['relationships'] ? never : keyof R['relationships'] & string;
22
+ /** Which side of the schema a document type describes: what a client sends, or what a handler reads. */
23
+ type Side = 'input' | 'output';
24
+ /** `T`'s members named in `K`, each keeping the optionality `T` gives it. */
25
+ type PickMembers<T, K> = {
26
+ [P in keyof T as P extends K ? P : never]: T[P];
27
+ };
28
+ type AttributesOf<R extends WritableResource, K, M extends RequestMode, S extends Side> = M extends 'update' ? Partial<PickMembers<S extends 'input' ? input<R['attributes']> : output<R['attributes']>, K>> : PickMembers<S extends 'input' ? input<R['attributes']> : output<R['attributes']>, K>;
29
+ /**
30
+ * The identifier a relationship's linkage carries, typed by the resource it points at. Its `lid` and `meta` are
31
+ * members JSON:API 1.1 allows, which the schema accepts and nothing reads.
32
+ */
33
+ type IdentifierOf<D> = D extends {
34
+ related: () => {
35
+ type: infer T extends string;
36
+ };
37
+ } ? {
38
+ type: T;
39
+ id: string;
40
+ lid?: string;
41
+ meta?: Meta;
42
+ } : never;
43
+ /** The members JSON:API 1.1 lets a relationship object carry beside its `data`, accepted and never read. */
44
+ interface LinkageExtras {
45
+ meta?: Meta;
46
+ links?: RelationshipLinks;
47
+ }
48
+ /** A relationship as a request writes it: a to-one's identifier or `null`, a to-many's identifiers. */
49
+ type LinkageOf<D> = (D extends {
50
+ cardinality: 'many';
51
+ } ? {
52
+ data: readonly IdentifierOf<D>[];
53
+ } : D extends {
54
+ cardinality: 'one';
55
+ } ? {
56
+ data: IdentifierOf<D> | null;
57
+ } : {
58
+ data: IdentifierOf<D> | readonly IdentifierOf<D>[] | null;
59
+ }) & LinkageExtras;
60
+ type LinkagesOf<R extends WritableResource, K extends RelationshipName<R>, M extends RequestMode> = M extends 'update' ? {
61
+ [P in K]?: LinkageOf<R['relationships'][P]>;
62
+ } : {
63
+ [P in K]: LinkageOf<R['relationships'][P]>;
64
+ };
65
+ /** A member a route names nothing under, or any member of an update, may be left out; a create sends what it names. */
66
+ type Member<N extends string, K, V, M extends RequestMode> = [K] extends [never] ? {
67
+ [P in N]?: V;
68
+ } : M extends 'update' ? {
69
+ [P in N]?: V;
70
+ } : {
71
+ [P in N]: V;
72
+ };
73
+ /**
74
+ * A create's `attributes`, which it sends only where one attribute it names is required: JSON:API makes the member
75
+ * optional, so a create whose named attributes are all optional may leave it out.
76
+ */
77
+ type AttributesMember<R extends WritableResource, AK, M extends RequestMode, S extends Side> = Record<never, never> extends PickMembers<input<R['attributes']>, AK> ? {
78
+ attributes?: AttributesOf<R, AK, M, S>;
79
+ } : Member<'attributes', AK, AttributesOf<R, AK, M, S>, M>;
80
+ /** The members JSON:API 1.1 lets a resource object carry beside its type, id and fields, accepted and never read. */
81
+ interface ResourceObjectExtras {
82
+ lid?: string;
83
+ meta?: Meta;
84
+ links?: ResourceLinks;
85
+ }
86
+ /**
87
+ * The request document a route declares for `R`: one resource object under `data`, the attributes it names under
88
+ * `attributes`, and each relationship it names as a relationship object. `S` picks what a client sends (`'input'`) or
89
+ * what the handler reads (`'output'`).
90
+ */
91
+ export type RequestDocument<R extends WritableResource, M extends RequestMode, AK extends AttributeName<R> = never, RK extends RelationshipName<R> = never, S extends Side = 'output'> = {
92
+ data: {
93
+ type: R['type'];
94
+ } & ResourceObjectExtras & (M extends 'update' ? {
95
+ id: string;
96
+ } : {
97
+ id?: never;
98
+ }) & AttributesMember<R, AK, M, S> & Member<'relationships', RK, LinkagesOf<R, RK, M>, M>;
99
+ meta?: Meta;
100
+ jsonapi?: JsonApiObject;
101
+ };
102
+ /** The schema {@link requestDocumentSchema} builds: it reads what a client sends into what a handler reads. */
103
+ export type RequestDocumentSchema<R extends WritableResource, M extends RequestMode, AK extends AttributeName<R>, RK extends RelationshipName<R>> = ZodType<RequestDocument<R, M, AK, RK, 'output'>, RequestDocument<R, M, AK, RK, 'input'>>;
104
+ /**
105
+ * The body schema of a write, built from the same resource declaration the route answers with, so the document a
106
+ * client reads and the one it writes cannot drift.
107
+ *
108
+ * `'create'` requires `data.type`, refuses `data.id`, keeps each named attribute as optional as its schema makes it,
109
+ * and requires each named relationship (a to-one left empty is sent as `{ data: null }`). It requires `attributes`
110
+ * itself only where one named attribute is required, since JSON:API makes the member optional. `'update'` requires
111
+ * `data.type` and `data.id`, and makes every member optional, because JSON:API 1.1 has a server read a missing member
112
+ * as its current value. An update therefore refuses, at declaration, to name an attribute whose schema fills a value
113
+ * for a missing key, through a `.default()`, a `.prefault()` or a `.catch()` beneath any wrapper, since the fill would
114
+ * overwrite the stored value. An attribute whose schema is async is not asked, since a sync parse cannot run it, and
115
+ * is accepted as it is.
116
+ *
117
+ * Every other member JSON:API 1.1 defines for a request is accepted and read by nothing: `lid`, `meta` and `links` on
118
+ * the resource object, `meta` and `links` on a relationship object, `lid` and `meta` on an identifier, and the
119
+ * top-level `meta` and `jsonapi`. A member it does not define fails the schema.
120
+ *
121
+ * Call it once every related resource it names has been declared, since it reads each one's `type`.
122
+ *
123
+ * @param writes - the attributes and relationships a client may write; a name the resource does not declare fails to
124
+ * compile
125
+ * @throws Error at declaration on a name the resource does not declare, or on an update naming an attribute that
126
+ * fills a value for a missing key
127
+ * @throws RequestConflict on a `data.type` other than the resource's - out of `safeParse` as well, because a throwing
128
+ * transform escapes it, against what zod documents (zod@4.3.6)
129
+ * @throws RequestForbidden on a `data.id` in a create, or on an attribute or relationship `writes` does not name - out
130
+ * of `safeParse` as well
131
+ */
132
+ export declare function requestDocumentSchema<R extends WritableResource, M extends RequestMode, const AK extends AttributeName<R> = never, const RK extends RelationshipName<R> = never>(resource: R, mode: M, writes?: {
133
+ readonly attributes?: readonly AK[];
134
+ readonly relationships?: readonly RK[];
135
+ }): RequestDocumentSchema<R, M, AK, RK>;
136
+ /** What {@link readRequestDocument} reads: any document {@link requestDocumentSchema} produced. */
137
+ interface RequestDocumentLike {
138
+ data: {
139
+ id?: string;
140
+ attributes?: object;
141
+ relationships?: Readonly<Record<string, {
142
+ data: ResourceIdentifier | readonly ResourceIdentifier[] | null;
143
+ } | undefined>>;
144
+ };
145
+ }
146
+ /** The ids a relationship's linkage names: one or `null` for a to-one, a list for a to-many. */
147
+ type IdsOf<L> = L extends readonly {
148
+ id: string;
149
+ }[] ? string[] : L extends {
150
+ id: string;
151
+ } ? string : null;
152
+ /** What a handler writes from a request document: its attributes, and each relationship as the ids it names. */
153
+ export type ReadRequestDocument<D> = D extends {
154
+ data: infer Data;
155
+ } ? {
156
+ attributes: Data extends {
157
+ attributes?: infer A;
158
+ } ? Exclude<A, undefined> : Record<never, never>;
159
+ relationships: Data extends {
160
+ relationships?: infer L;
161
+ } ? {
162
+ [K in keyof Exclude<L, undefined>]: IdsOf<Exclude<Exclude<L, undefined>[K], undefined> extends {
163
+ data: infer X;
164
+ } ? X : never>;
165
+ } : Record<never, never>;
166
+ } : never;
167
+ /**
168
+ * Hands a handler what a request document writes: the attributes as sent, and each relationship reduced to the id it
169
+ * names, `null` for a cleared to-one, or a list. A member the document left out stays out, which on an update means
170
+ * keep its current value.
171
+ *
172
+ * @param options - on an update, the id the URL names, spelled as the path carries it; a create takes none
173
+ * @throws RequestConflict when an update's `data.id` is not the URL's, which JSON:API 1.1 answers 409
174
+ */
175
+ export declare function readRequestDocument<D extends RequestDocumentLike>(document: D, ...options: D extends {
176
+ data: {
177
+ id: string;
178
+ };
179
+ } ? [options: {
180
+ id: string;
181
+ }] : []): ReadRequestDocument<D>;
182
+ export {};
183
+ //# sourceMappingURL=request-document.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-document.d.ts","sourceRoot":"","sources":["../src/request-document.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAC7D,OAAO,EAKL,KAAK,aAAa,EAClB,KAAK,IAAI,EACT,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AAGzB,yFAAyF;AACzF,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE9C,qGAAqG;AACrG,UAAU,oBAAoB;IAC5B,QAAQ,CAAC,WAAW,EAAE,KAAK,GAAG,MAAM,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,MAAM;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACnD;AAED,gGAAgG;AAChG,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,QAAQ,CAAC,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAC;CACxE;AAED,+CAA+C;AAC/C,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,gBAAgB,IAAI,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,GAAG,MAAM,CAAC;AAEhG,8FAA8F;AAC9F,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,gBAAgB,IAAI,MAAM,SAAS,MAAM,CAAC,CAAC,eAAe,CAAC,GAC9F,KAAK,GACL,MAAM,CAAC,CAAC,eAAe,CAAC,GAAG,MAAM,CAAC;AAEtC,wGAAwG;AACxG,KAAK,IAAI,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE/B,6EAA6E;AAC7E,KAAK,WAAW,CAAC,CAAC,EAAE,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;CAAE,CAAC;AAE7E,KAAK,YAAY,CAAC,CAAC,SAAS,gBAAgB,EAAE,CAAC,EAAE,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,IAAI,IAAI,CAAC,SAAS,QAAQ,GACxG,OAAO,CAAC,WAAW,CAAC,CAAC,SAAS,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAC7F,WAAW,CAAC,CAAC,SAAS,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAEzF;;;GAGG;AACH,KAAK,YAAY,CAAC,CAAC,IAAI,CAAC,SAAS;IAAE,OAAO,EAAE,MAAM;QAAE,IAAI,EAAE,MAAM,CAAC,SAAS,MAAM,CAAA;KAAE,CAAA;CAAE,GAChF;IAAE,IAAI,EAAE,CAAC,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,IAAI,CAAA;CAAE,GAClD,KAAK,CAAC;AAEV,4GAA4G;AAC5G,UAAU,aAAa;IACrB,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,KAAK,CAAC,EAAE,iBAAiB,CAAC;CAC3B;AAED,uGAAuG;AACvG,KAAK,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,GAClD;IAAE,IAAI,EAAE,SAAS,YAAY,CAAC,CAAC,CAAC,EAAE,CAAA;CAAE,GACpC,CAAC,SAAS;IAAE,WAAW,EAAE,KAAK,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,IAAI,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,YAAY,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,CAAA;CAAE,CAAC,GAClE,aAAa,CAAC;AAEhB,KAAK,UAAU,CAAC,CAAC,SAAS,gBAAgB,EAAE,CAAC,SAAS,gBAAgB,CAAC,CAAC,CAAC,EAAE,CAAC,SAAS,WAAW,IAAI,CAAC,SAAS,QAAQ,GAClH;KAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,GAC/C;KAAG,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,CAAC;AAEnD,uHAAuH;AACvH,KAAK,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,SAAS,WAAW,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GAC5E;KAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;CAAE,GAChB,CAAC,SAAS,QAAQ,GAChB;KAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;CAAE,GAChB;KAAG,CAAC,IAAI,CAAC,GAAG,CAAC;CAAE,CAAC;AAEtB;;;GAGG;AACH,KAAK,gBAAgB,CAAC,CAAC,SAAS,gBAAgB,EAAE,EAAE,EAAE,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,IAAI,IACzF,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,SAAS,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,EAAE,EAAE,CAAC,GAChE;IAAE,UAAU,CAAC,EAAE,YAAY,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;CAAE,GAC1C,MAAM,CAAC,YAAY,EAAE,EAAE,EAAE,YAAY,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAE7D,qHAAqH;AACrH,UAAU,oBAAoB;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAED;;;;GAIG;AACH,MAAM,MAAM,eAAe,CACzB,CAAC,SAAS,gBAAgB,EAC1B,CAAC,SAAS,WAAW,EACrB,EAAE,SAAS,aAAa,CAAC,CAAC,CAAC,GAAG,KAAK,EACnC,EAAE,SAAS,gBAAgB,CAAC,CAAC,CAAC,GAAG,KAAK,EACtC,CAAC,SAAS,IAAI,GAAG,QAAQ,IACvB;IACF,IAAI,EAAE;QAAE,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,GAAG,oBAAoB,GAC9C,CAAC,CAAC,SAAS,QAAQ,GAAG;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,EAAE,CAAC,EAAE,KAAK,CAAA;KAAE,CAAC,GACtD,gBAAgB,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,GAC7B,MAAM,CAAC,eAAe,EAAE,EAAE,EAAE,UAAU,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACvD,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB,CAAC;AAEF,+GAA+G;AAC/G,MAAM,MAAM,qBAAqB,CAC/B,CAAC,SAAS,gBAAgB,EAC1B,CAAC,SAAS,WAAW,EACrB,EAAE,SAAS,aAAa,CAAC,CAAC,CAAC,EAC3B,EAAE,SAAS,gBAAgB,CAAC,CAAC,CAAC,IAC5B,OAAO,CAAC,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC;AAuD7F;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,qBAAqB,CACnC,CAAC,SAAS,gBAAgB,EAC1B,CAAC,SAAS,WAAW,EACrB,KAAK,CAAC,EAAE,SAAS,aAAa,CAAC,CAAC,CAAC,GAAG,KAAK,EACzC,KAAK,CAAC,EAAE,SAAS,gBAAgB,CAAC,CAAC,CAAC,GAAG,KAAK,EAE5C,QAAQ,EAAE,CAAC,EACX,IAAI,EAAE,CAAC,EACP,MAAM,GAAE;IAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,EAAE,EAAE,CAAC;IAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,EAAE,EAAE,CAAA;CAAO,GAC3F,qBAAqB,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,CAgFrC;AAED,mGAAmG;AACnG,UAAU,mBAAmB;IAC3B,IAAI,EAAE;QACJ,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,aAAa,CAAC,EAAE,QAAQ,CACtB,MAAM,CAAC,MAAM,EAAE;YAAE,IAAI,EAAE,kBAAkB,GAAG,SAAS,kBAAkB,EAAE,GAAG,IAAI,CAAA;SAAE,GAAG,SAAS,CAAC,CAChG,CAAC;KACH,CAAC;CACH;AAED,gGAAgG;AAChG,KAAK,KAAK,CAAC,CAAC,IAAI,CAAC,SAAS,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,MAAM,EAAE,GAAG,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,GAAG,IAAI,CAAC;AAE1G,gHAAgH;AAChH,MAAM,MAAM,mBAAmB,CAAC,CAAC,IAAI,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,IAAI,CAAA;CAAE,GAC/D;IACE,UAAU,EAAE,IAAI,SAAS;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC,CAAA;KAAE,GAAG,OAAO,CAAC,CAAC,EAAE,SAAS,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACjG,aAAa,EAAE,IAAI,SAAS;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC,CAAA;KAAE,GACnD;SACG,CAAC,IAAI,MAAM,OAAO,CAAC,CAAC,EAAE,SAAS,CAAC,GAAG,KAAK,CACvC,OAAO,CAAC,OAAO,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,SAAS;YAAE,IAAI,EAAE,MAAM,CAAC,CAAA;SAAE,GAAG,CAAC,GAAG,KAAK,CACnF;KACF,GACD,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;CAC1B,GACD,KAAK,CAAC;AASV;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,mBAAmB,EAC/D,QAAQ,EAAE,CAAC,EACX,GAAG,OAAO,EAAE,CAAC,SAAS;IAAE,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GAAG,CAAC,OAAO,EAAE;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,EAAE,GAC9E,mBAAmB,CAAC,CAAC,CAAC,CAaxB"}
@@ -0,0 +1,170 @@
1
+ import { z } from 'zod';
2
+ import { jsonApiObjectSchema, metaSchema, relationshipLinksSchema, resourceLinksSchema, } from './primitives.js';
3
+ import { RequestConflict, RequestForbidden } from './request-refusal.js';
4
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
5
+ /** One JSON Pointer segment: RFC 6901 escapes `~` as `~0` and `/` as `~1`, in that order. */
6
+ const pointerSegment = (name) => name.replaceAll('~', '~0').replaceAll('/', '~1');
7
+ /**
8
+ * Throws what JSON:API 1.1 answers 409 or 403, in the order the standard's checks come: the type, a client id, then
9
+ * each member the route does not write. It reads the body as sent, so it checks only what it can read and leaves
10
+ * every other defect to the schema's own issue, which the validation hook answers 422.
11
+ */
12
+ function refuseUnsupported(body, type, mode, writes) {
13
+ if (!isRecord(body) || !isRecord(body['data']))
14
+ return;
15
+ const data = body['data'];
16
+ const sentType = data['type'];
17
+ if (typeof sentType === 'string' && sentType !== type) {
18
+ throw new RequestConflict('/data/type', `This endpoint writes "${type}", and the document names "${sentType}".`);
19
+ }
20
+ if (mode === 'create' && Object.hasOwn(data, 'id')) {
21
+ throw new RequestForbidden('/data/id', `A "${type}" is given its id by the server, so a create names none.`);
22
+ }
23
+ for (const member of ['attributes', 'relationships']) {
24
+ const sent = data[member];
25
+ if (!isRecord(sent))
26
+ continue;
27
+ const refused = Object.keys(sent).find((name) => !writes[member].includes(name));
28
+ if (refused !== undefined) {
29
+ throw new RequestForbidden(`/data/${member}/${pointerSegment(refused)}`, `This endpoint does not write "${refused}" on "${type}".`);
30
+ }
31
+ }
32
+ }
33
+ /**
34
+ * Whether `schema` fills a value for a key left out, asked of the schema itself so any `.default()`, `.prefault()` or
35
+ * `.catch()`, beneath whatever wraps it, answers. An async schema cannot be asked in a sync parse and reads as `false`.
36
+ */
37
+ function fillsWhenMissing(schema) {
38
+ try {
39
+ const result = schema.safeParse(undefined);
40
+ return result.success && result.data !== undefined;
41
+ }
42
+ catch (error) {
43
+ if (error instanceof z.core.$ZodAsyncError)
44
+ return false;
45
+ throw error;
46
+ }
47
+ }
48
+ /**
49
+ * The body schema of a write, built from the same resource declaration the route answers with, so the document a
50
+ * client reads and the one it writes cannot drift.
51
+ *
52
+ * `'create'` requires `data.type`, refuses `data.id`, keeps each named attribute as optional as its schema makes it,
53
+ * and requires each named relationship (a to-one left empty is sent as `{ data: null }`). It requires `attributes`
54
+ * itself only where one named attribute is required, since JSON:API makes the member optional. `'update'` requires
55
+ * `data.type` and `data.id`, and makes every member optional, because JSON:API 1.1 has a server read a missing member
56
+ * as its current value. An update therefore refuses, at declaration, to name an attribute whose schema fills a value
57
+ * for a missing key, through a `.default()`, a `.prefault()` or a `.catch()` beneath any wrapper, since the fill would
58
+ * overwrite the stored value. An attribute whose schema is async is not asked, since a sync parse cannot run it, and
59
+ * is accepted as it is.
60
+ *
61
+ * Every other member JSON:API 1.1 defines for a request is accepted and read by nothing: `lid`, `meta` and `links` on
62
+ * the resource object, `meta` and `links` on a relationship object, `lid` and `meta` on an identifier, and the
63
+ * top-level `meta` and `jsonapi`. A member it does not define fails the schema.
64
+ *
65
+ * Call it once every related resource it names has been declared, since it reads each one's `type`.
66
+ *
67
+ * @param writes - the attributes and relationships a client may write; a name the resource does not declare fails to
68
+ * compile
69
+ * @throws Error at declaration on a name the resource does not declare, or on an update naming an attribute that
70
+ * fills a value for a missing key
71
+ * @throws RequestConflict on a `data.type` other than the resource's - out of `safeParse` as well, because a throwing
72
+ * transform escapes it, against what zod documents (zod@4.3.6)
73
+ * @throws RequestForbidden on a `data.id` in a create, or on an attribute or relationship `writes` does not name - out
74
+ * of `safeParse` as well
75
+ */
76
+ export function requestDocumentSchema(resource, mode, writes = {}) {
77
+ const attributes = writes.attributes ?? [];
78
+ const relationships = writes.relationships ?? [];
79
+ for (const name of attributes) {
80
+ if (!Object.hasOwn(resource.attributes.shape, name)) {
81
+ throw new Error(`"${resource.type}" declares no attribute "${name}".`);
82
+ }
83
+ }
84
+ for (const name of relationships) {
85
+ if (!Object.hasOwn(resource.relationships, name)) {
86
+ throw new Error(`"${resource.type}" declares no relationship "${name}".`);
87
+ }
88
+ }
89
+ if (mode === 'update') {
90
+ const filling = attributes.find((name) => fillsWhenMissing(resource.attributes.shape[name]));
91
+ if (filling !== undefined) {
92
+ throw new Error(`The update names "${filling}", whose schema fills a value when the key is missing; a PATCH would overwrite ` +
93
+ `the stored value. Drop the default from the resource's attribute, or leave "${filling}" out of the update.`);
94
+ }
95
+ }
96
+ // A member is left out only where the route names nothing under it that a create must send, or on an update.
97
+ const present = (schema, required) => mode === 'update' || !required ? schema.optional() : schema;
98
+ const mask = Object.fromEntries(attributes.map((name) => [name, true]));
99
+ const picked = resource.attributes.pick(mask);
100
+ const attributesSchema = (mode === 'update' ? picked.partial() : picked).strict();
101
+ // What zod documents as the test of an optional schema, which is also what an object runs on a missing key.
102
+ const someAttributeRequired = mode === 'create' && Object.values(picked.shape).some((schema) => !schema.safeParse(undefined).success);
103
+ const relationshipsSchema = z
104
+ .object(Object.fromEntries(relationships.map((name) => {
105
+ const def = resource.relationships[name];
106
+ const identifier = z
107
+ .object({
108
+ type: z.literal(def.related().type),
109
+ id: z.string(),
110
+ lid: z.string().optional(),
111
+ meta: metaSchema.optional(),
112
+ })
113
+ .strict();
114
+ const linkage = z
115
+ .object({
116
+ data: def.cardinality === 'one' ? identifier.nullable() : z.array(identifier),
117
+ meta: metaSchema.optional(),
118
+ links: relationshipLinksSchema.optional(),
119
+ })
120
+ .strict();
121
+ return [name, mode === 'update' ? linkage.optional() : linkage];
122
+ })))
123
+ .strict();
124
+ const document = z
125
+ .object({
126
+ data: z
127
+ .object({
128
+ type: z.literal(resource.type),
129
+ ...(mode === 'update' ? { id: z.string() } : {}),
130
+ lid: z.string().optional(),
131
+ attributes: present(attributesSchema, someAttributeRequired),
132
+ relationships: present(relationshipsSchema, relationships.length > 0),
133
+ meta: metaSchema.optional(),
134
+ links: resourceLinksSchema.optional(),
135
+ })
136
+ .strict(),
137
+ meta: metaSchema.optional(),
138
+ jsonapi: jsonApiObjectSchema.optional(),
139
+ })
140
+ .strict();
141
+ const checked = z.preprocess((body) => {
142
+ refuseUnsupported(body, resource.type, mode, { attributes, relationships });
143
+ return body;
144
+ }, document);
145
+ // zod types a preprocessed input as `unknown`; what a client sends is the document itself, which a typed client reads.
146
+ return checked;
147
+ }
148
+ const idsOf = (data) => data === null
149
+ ? null
150
+ : Array.isArray(data)
151
+ ? data.map((one) => one.id)
152
+ : data.id;
153
+ /**
154
+ * Hands a handler what a request document writes: the attributes as sent, and each relationship reduced to the id it
155
+ * names, `null` for a cleared to-one, or a list. A member the document left out stays out, which on an update means
156
+ * keep its current value.
157
+ *
158
+ * @param options - on an update, the id the URL names, spelled as the path carries it; a create takes none
159
+ * @throws RequestConflict when an update's `data.id` is not the URL's, which JSON:API 1.1 answers 409
160
+ */
161
+ export function readRequestDocument(document, ...options) {
162
+ const { data } = document;
163
+ const [url] = options;
164
+ if (url !== undefined && data.id !== url.id) {
165
+ throw new RequestConflict('/data/id', `The document names "${String(data.id)}", and the URL names "${url.id}".`);
166
+ }
167
+ const relationships = Object.fromEntries(Object.entries(data.relationships ?? {}).flatMap(([name, linkage]) => linkage === undefined ? [] : [[name, idsOf(linkage.data)]]));
168
+ // The shape is the document's own, which the route's schema declared; the reduction above is what differs.
169
+ return { attributes: data.attributes ?? {}, relationships };
170
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * A request document naming another resource than its endpoint: a `type` outside the collection, or an `id` other
3
+ * than the one its URL names. It carries no status and no wire `code`, as `QueryNotPermitted` carries none: the
4
+ * transport that catches it answers 409, which JSON:API 1.1 requires of both.
5
+ */
6
+ export declare class RequestConflict extends Error {
7
+ /** The JSON Pointer (RFC 6901) to the member that conflicts. */
8
+ readonly pointer: string;
9
+ constructor(
10
+ /** The JSON Pointer (RFC 6901) to the member that conflicts. */
11
+ pointer: string, message: string);
12
+ }
13
+ /**
14
+ * A request document asking for a write its endpoint does not support: a client-generated id on a create, or an
15
+ * attribute or relationship the route does not write. The transport answers 403, which JSON:API 1.1 requires of both.
16
+ */
17
+ export declare class RequestForbidden extends Error {
18
+ /** The JSON Pointer (RFC 6901) to the member refused. */
19
+ readonly pointer: string;
20
+ constructor(
21
+ /** The JSON Pointer (RFC 6901) to the member refused. */
22
+ pointer: string, message: string);
23
+ }
24
+ //# sourceMappingURL=request-refusal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-refusal.d.ts","sourceRoot":"","sources":["../src/request-refusal.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,qBAAa,eAAgB,SAAQ,KAAK;IAEtC,gEAAgE;IAChE,QAAQ,CAAC,OAAO,EAAE,MAAM;;IADxB,gEAAgE;IACvD,OAAO,EAAE,MAAM,EACxB,OAAO,EAAE,MAAM;CAKlB;AAED;;;GAGG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;IAEvC,yDAAyD;IACzD,QAAQ,CAAC,OAAO,EAAE,MAAM;;IADxB,yDAAyD;IAChD,OAAO,EAAE,MAAM,EACxB,OAAO,EAAE,MAAM;CAKlB"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * A request document naming another resource than its endpoint: a `type` outside the collection, or an `id` other
3
+ * than the one its URL names. It carries no status and no wire `code`, as `QueryNotPermitted` carries none: the
4
+ * transport that catches it answers 409, which JSON:API 1.1 requires of both.
5
+ */
6
+ export class RequestConflict extends Error {
7
+ pointer;
8
+ constructor(
9
+ /** The JSON Pointer (RFC 6901) to the member that conflicts. */
10
+ pointer, message) {
11
+ super(message);
12
+ this.pointer = pointer;
13
+ this.name = 'RequestConflict';
14
+ }
15
+ }
16
+ /**
17
+ * A request document asking for a write its endpoint does not support: a client-generated id on a create, or an
18
+ * attribute or relationship the route does not write. The transport answers 403, which JSON:API 1.1 requires of both.
19
+ */
20
+ export class RequestForbidden extends Error {
21
+ pointer;
22
+ constructor(
23
+ /** The JSON Pointer (RFC 6901) to the member refused. */
24
+ pointer, message) {
25
+ super(message);
26
+ this.pointer = pointer;
27
+ this.name = 'RequestForbidden';
28
+ }
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroxsolutions/response",
3
- "version": "0.14.2",
3
+ "version": "0.14.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,