@zeroxsolutions/response 0.13.0 → 0.14.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 CHANGED
@@ -1,3 +1,75 @@
1
+ ## 0.14.0 (2026-09-28)
2
+
3
+ ### 🚀 Features
4
+
5
+ - **response:** answer a created resource with its location ([e9c0086](https://github.com/zeroxsolutions/zeroxsolutions/commit/e9c0086))
6
+ - ⚠️ **response:** refuse a declaration the standard's field rules forbid ([30e6b77](https://github.com/zeroxsolutions/zeroxsolutions/commit/30e6b77))
7
+
8
+ ### 🩹 Fixes
9
+
10
+ - ⚠️ **response:** walk a pipe's input when checking member names ([0949d22](https://github.com/zeroxsolutions/zeroxsolutions/commit/0949d22))
11
+ - **response:** match link types to their schemas ([0f7c79b](https://github.com/zeroxsolutions/zeroxsolutions/commit/0f7c79b))
12
+ - ⚠️ **response:** refuse a resource id that is not a string ([57f60ab](https://github.com/zeroxsolutions/zeroxsolutions/commit/57f60ab))
13
+ - **response:** keep a primary resource out of included ([c38859f](https://github.com/zeroxsolutions/zeroxsolutions/commit/c38859f))
14
+ - ⚠️ **response:** read only the document members the standard defines ([fc5c51a](https://github.com/zeroxsolutions/zeroxsolutions/commit/fc5c51a))
15
+ - ⚠️ **response:** refuse an error object that carries no member ([7d888ed](https://github.com/zeroxsolutions/zeroxsolutions/commit/7d888ed))
16
+
17
+ ### ⚠️ Breaking Changes
18
+
19
+ - **response:** walk a pipe's input when checking member names ([0949d22](https://github.com/zeroxsolutions/zeroxsolutions/commit/0949d22))
20
+ a nested attribute member name that breaks the
21
+ standard's member-name rule, and sits behind a .transform() or
22
+ .pipe(), now throws at jsonApiResource's own declaration where it
23
+ previously passed unchecked.
24
+ - **response:** refuse a declaration the standard's field rules forbid ([30e6b77](https://github.com/zeroxsolutions/zeroxsolutions/commit/30e6b77))
25
+ jsonApiResource throws, and fails to compile, on a field named type or id, a name shared by an attribute and a relationship, an illegal member name, or a foreign-key attribute beside its relationship.
26
+ - **response:** refuse a resource id that is not a string ([57f60ab](https://github.com/zeroxsolutions/zeroxsolutions/commit/57f60ab))
27
+ Row['id'] is typed string, and serializing a row, embedded row or foreign key whose id is not a string throws TypeError.
28
+ - **response:** read only the document members the standard defines ([fc5c51a](https://github.com/zeroxsolutions/zeroxsolutions/commit/fc5c51a))
29
+ a data document carrying errors, an error document carrying data, and an empty relationship object are refused.
30
+ - **response:** refuse an error object that carries no member ([7d888ed](https://github.com/zeroxsolutions/zeroxsolutions/commit/7d888ed))
31
+ toErrorDocument throws on an empty error object.
32
+
33
+ ### ❤️ Thank You
34
+
35
+ - Claude Opus 5.5 (1M context)
36
+ - Lương Văn Tú
37
+
38
+ ## 0.13.0 (2026-09-27)
39
+
40
+ ### 🚀 Features
41
+
42
+ - ⚠️ **response:** type the fieldsets and include paths a document answers ([98bbb58](https://github.com/zeroxsolutions/zeroxsolutions/commit/98bbb58))
43
+ - ⚠️ **response:** keep a resource's type, attributes and relationships as literals ([738889e](https://github.com/zeroxsolutions/zeroxsolutions/commit/738889e))
44
+
45
+ ### 🩹 Fixes
46
+
47
+ - ⚠️ **response:** pin an include path stopping at a wide edge ([515a3e9](https://github.com/zeroxsolutions/zeroxsolutions/commit/515a3e9))
48
+
49
+ ### ⚠️ Breaking Changes
50
+
51
+ - **response:** pin an include path stopping at a wide edge ([515a3e9](https://github.com/zeroxsolutions/zeroxsolutions/commit/515a3e9))
52
+ jsonApiResource's type parameters moved from <A, C> to
53
+ <T, A, R, C>, so an explicit `jsonApiResource<typeof attrs, Ctx>` no
54
+ longer compiles; remove the type arguments and let inference type it.
55
+ The exported ResourceOptions<C> is now ResourceOptions<R>.
56
+ BREAKING CHANGE: an explicit toOne<Ctx> or toMany<Ctx> no longer
57
+ compiles; remove the type argument.
58
+ - **response:** type the fieldsets and include paths a document answers ([98bbb58](https://github.com/zeroxsolutions/zeroxsolutions/commit/98bbb58))
59
+ declaredDocumentShape answers typed fieldsets and include
60
+ paths, walks at most three relationships by default, and answers a fieldset
61
+ only for a type a kept path reaches. DeclaredDocumentShape is removed;
62
+ TypedDocumentShape replaces it.
63
+ - **response:** keep a resource's type, attributes and relationships as literals ([738889e](https://github.com/zeroxsolutions/zeroxsolutions/commit/738889e))
64
+ toOne and toMany take the related resource as their first type
65
+ parameter and infer the loader context; a call passing the context explicitly,
66
+ toOne<Ctx>(...), no longer compiles and drops the explicit argument.
67
+
68
+ ### ❤️ Thank You
69
+
70
+ - Claude Opus 5.5 (1M context)
71
+ - Lương Văn Tú
72
+
1
73
  ## 0.12.2 (2026-09-27)
2
74
 
3
75
  ### 🚀 Features
package/README.md CHANGED
@@ -134,6 +134,7 @@ app.onError(
134
134
  namespace: 'academics', // every code THESE handlers mint carries it, so one surface has one vocabulary
135
135
  category: ['academics', 'http'],
136
136
  resolve: createDomainErrorResolver(academicsErrors),
137
+ authenticationChallenge: 'Bearer',
137
138
  }),
138
139
  );
139
140
  ```
@@ -0,0 +1,23 @@
1
+ import type { Link } from './primitives.js';
2
+ /**
3
+ * The arguments of a 201 that created `document`'s resource: the document, the status, and the `Location` the standard
4
+ * asks a 201 to carry. Spread into `c.json(...)`.
5
+ *
6
+ * `type` and `id` sit in the bound alongside `links`, both of which every `ResourceObject` carries: a document
7
+ * whose resource has no `links` at all would otherwise share no property with a bound of `links` alone, which
8
+ * is a "weak type" TypeScript refuses to accept a value for - and a resource with no self link is the common case.
9
+ *
10
+ * @throws Error when the resource's `self` link names another location, since the standard requires the two to match
11
+ */
12
+ export declare function createdResponse<D extends {
13
+ data: {
14
+ type: string;
15
+ id: string;
16
+ links?: {
17
+ self?: Link;
18
+ };
19
+ };
20
+ }>(document: D, location: string): readonly [D, 201, {
21
+ Location: string;
22
+ }];
23
+ //# sourceMappingURL=created-response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"created-response.d.ts","sourceRoot":"","sources":["../src/created-response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAM5C;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS;IAAE,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE;YAAE,IAAI,CAAC,EAAE,IAAI,CAAA;SAAE,CAAA;KAAE,CAAA;CAAE,EACvG,QAAQ,EAAE,CAAC,EACX,QAAQ,EAAE,MAAM,GACf,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAMzC"}
@@ -0,0 +1,19 @@
1
+ /** Where a link points, whichever of the standard's forms it takes. */
2
+ const hrefOf = (link) => link === null || link === undefined ? undefined : typeof link === 'string' ? link : link.href;
3
+ /**
4
+ * The arguments of a 201 that created `document`'s resource: the document, the status, and the `Location` the standard
5
+ * asks a 201 to carry. Spread into `c.json(...)`.
6
+ *
7
+ * `type` and `id` sit in the bound alongside `links`, both of which every `ResourceObject` carries: a document
8
+ * whose resource has no `links` at all would otherwise share no property with a bound of `links` alone, which
9
+ * is a "weak type" TypeScript refuses to accept a value for - and a resource with no self link is the common case.
10
+ *
11
+ * @throws Error when the resource's `self` link names another location, since the standard requires the two to match
12
+ */
13
+ export function createdResponse(document, location) {
14
+ const self = hrefOf(document.data.links?.self);
15
+ if (self !== undefined && self !== location) {
16
+ throw new Error(`The created resource's self link "${self}" differs from its Location "${location}".`);
17
+ }
18
+ return [document, 201, { Location: location }];
19
+ }
@@ -4,7 +4,7 @@
4
4
  * relationship declares belongs to `relationship.ts`.
5
5
  */
6
6
  import type { RelatedResource, RelationshipDef } from './relationship.js';
7
- import type { LoadedRows, RequestedDocumentShape, Row } from './serialize.js';
7
+ import { type LoadedRows, type RequestedDocumentShape, type Row } from './serialize.js';
8
8
  /**
9
9
  * What one loaded relationship hands the serializer for a row: the row its loader answered, or the
10
10
  * identifier alone where it answered none.
@@ -1 +1 @@
1
- {"version":3,"file":"document-load.d.ts","sourceRoot":"","sources":["../src/document-load.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC1E,OAAO,KAAK,EAAE,UAAU,EAAE,sBAAsB,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AA0B9E;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAC7B,GAAG,EAAE,eAAe,CAAC,CAAC,CAAC,EACvB,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,UAAU,GAAG,SAAS,GAC7B,GAAG,GAAG,GAAG,EAAE,GAAG,IAAI,CAIpB;AA0BD;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EACtC,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC,EACxB,IAAI,EAAE,SAAS,GAAG,EAAE,EACpB,SAAS,EAAE,sBAAsB,GAAG,SAAS,EAC7C,OAAO,EAAE,CAAC,GACT,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CA8BjC"}
1
+ {"version":3,"file":"document-load.d.ts","sourceRoot":"","sources":["../src/document-load.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC1E,OAAO,EAAe,KAAK,UAAU,EAAE,KAAK,sBAAsB,EAAE,KAAK,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAmCrG;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAC7B,GAAG,EAAE,eAAe,CAAC,CAAC,CAAC,EACvB,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,UAAU,GAAG,SAAS,GAC7B,GAAG,GAAG,GAAG,EAAE,GAAG,IAAI,CAKpB;AA2BD;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EACtC,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC,EACxB,IAAI,EAAE,SAAS,GAAG,EAAE,EACpB,SAAS,EAAE,sBAAsB,GAAG,SAAS,EAC7C,OAAO,EAAE,CAAC,GACT,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CA8BjC"}
@@ -1,7 +1,18 @@
1
- /** The ids one row carries for a loaded relationship. A null or absent key asks for nothing. */
2
- function readIds(value) {
1
+ import { assertRowId } from './serialize.js';
2
+ /**
3
+ * The ids one row carries for a loaded relationship. A null or absent key asks for nothing; any other
4
+ * non-string is refused rather than silently dropped, naming the relationship's own related `type`.
5
+ */
6
+ function readIds(value, type) {
3
7
  const values = Array.isArray(value) ? value : [value];
4
- return values.filter((one) => typeof one === 'string');
8
+ const ids = [];
9
+ for (const one of values) {
10
+ if (one === null || one === undefined)
11
+ continue;
12
+ assertRowId(type, { id: one });
13
+ ids.push(one);
14
+ }
15
+ return ids;
5
16
  }
6
17
  /** The rows one row already carries for a relationship that declares no loader. */
7
18
  function readRows(value) {
@@ -16,8 +27,9 @@ function readRows(value) {
16
27
  * linkage a client reads - only what lands under `included`.
17
28
  */
18
29
  export function loadedRelated(def, row, loaded) {
19
- const answered = loaded?.get(def.related().type);
20
- const rows = readIds(def.read(row)).map((id) => answered?.get(id) ?? { id });
30
+ const type = def.related().type;
31
+ const answered = loaded?.get(type);
32
+ const rows = readIds(def.read(row), type).map((id) => answered?.get(id) ?? { id });
21
33
  return def.cardinality === 'many' ? rows : (rows.at(0) ?? null);
22
34
  }
23
35
  /** Runs one relationship's loader, and answers the rows the next depth reads its ids off. */
@@ -29,13 +41,13 @@ async function loadRelated(def, rows, loaded, context) {
29
41
  const type = def.related().type;
30
42
  const answered = loaded.get(type) ?? new Map();
31
43
  loaded.set(type, answered);
32
- const ids = [...new Set(rows.flatMap((row) => readIds(def.read(row))))];
44
+ const ids = [...new Set(rows.flatMap((row) => readIds(def.read(row), type)))];
33
45
  if (ids.length === 0)
34
46
  return [];
35
47
  const related = await load(ids, context);
36
48
  for (const row of related) {
37
- if (typeof row.id === 'string')
38
- answered.set(row.id, row);
49
+ assertRowId(type, row);
50
+ answered.set(row.id, row);
39
51
  }
40
52
  return related;
41
53
  }
@@ -1,10 +1,10 @@
1
1
  import type { ZodObject, ZodType } from 'zod';
2
- import { type JsonApiObject, type Meta, type PaginationLinks, type ResourceIdentifier, type ResourceLinks } from './primitives.js';
2
+ import { type JsonApiObject, type Meta, type PaginationLinks, type RelationshipLinks, type ResourceIdentifier, type ResourceLinks, type TopLevelLinks } from './primitives.js';
3
3
  import { type RelatedResource, type RelationshipDef } from './relationship.js';
4
4
  /** One relationship as a document carries it - the linkage, plus its own links and meta. */
5
5
  export interface Relationship {
6
6
  data?: ResourceIdentifier | ResourceIdentifier[] | null;
7
- links?: ResourceLinks;
7
+ links?: RelationshipLinks;
8
8
  meta?: Meta;
9
9
  }
10
10
  /** A resource object, with `attributes` carrying the declared attribute type. */
@@ -20,7 +20,7 @@ export interface ResourceObject<A> {
20
20
  export interface ResourceDocument<A> {
21
21
  data: ResourceObject<A>;
22
22
  included?: ResourceObject<unknown>[];
23
- links?: ResourceLinks;
23
+ links?: TopLevelLinks;
24
24
  meta?: Meta;
25
25
  jsonapi?: JsonApiObject;
26
26
  }
@@ -32,7 +32,12 @@ export interface CollectionDocument<A> {
32
32
  meta?: Meta;
33
33
  jsonapi?: JsonApiObject;
34
34
  }
35
- /** Wraps `attributes` into a resource object - `{ type, id, attributes, relationships?, links?, meta? }`. */
35
+ /**
36
+ * Wraps `attributes` into a resource object - `{ type, id, attributes?, relationships?, links?, meta? }`.
37
+ *
38
+ * `attributes` parses as absent here, unlike {@link ResourceObject}'s own required member: this schema
39
+ * also validates a document read off another server, which may publish a resource with none.
40
+ */
36
41
  export declare function resourceObjectSchema<C>(type: string, attributes: ZodObject, relationships?: Readonly<Record<string, RelationshipDef<C>>>): ZodType;
37
42
  /**
38
43
  * The document for a SINGLE resource - `{ data, included?, links?, meta?, jsonapi? }`.
@@ -40,8 +45,15 @@ export declare function resourceObjectSchema<C>(type: string, attributes: ZodObj
40
45
  * `A` is asserted rather than inferred: the schema is assembled from a relationship graph reached
41
46
  * through thunks, so nothing static carries the attribute type, and a client typing a route off this
42
47
  * schema would otherwise read every member as `unknown`.
48
+ *
49
+ * Strict at the top level: the standard forbids any top-level member beyond these (extension members
50
+ * aside), which is what refuses a `data` document that also carries `errors`.
43
51
  */
44
52
  export declare function resourceDocumentSchema<A = unknown, C = unknown>(root: RelatedResource<C>): ZodType<ResourceDocument<A>>;
45
- /** The document for a COLLECTION - the same members, with `data` an array and paginated `links`. */
53
+ /**
54
+ * The document for a COLLECTION - the same members, with `data` an array and paginated `links`.
55
+ *
56
+ * Strict at the top level for the same reason as {@link resourceDocumentSchema}.
57
+ */
46
58
  export declare function collectionDocumentSchema<A = unknown, C = unknown>(root: RelatedResource<C>): ZodType<CollectionDocument<A>>;
47
59
  //# sourceMappingURL=document.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAC9C,OAAO,EAKL,KAAK,aAAa,EAClB,KAAK,IAAI,EACT,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAA2C,KAAK,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAExH,4FAA4F;AAC5F,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,kBAAkB,GAAG,kBAAkB,EAAE,GAAG,IAAI,CAAC;IACxD,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,IAAI,CAAC,EAAE,IAAI,CAAC;CACb;AAED,iFAAiF;AACjF,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,CAAC,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC7C,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,IAAI,CAAC,EAAE,IAAI,CAAC;CACb;AAED,yGAAyG;AACzG,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC;IACxB,QAAQ,CAAC,EAAE,cAAc,CAAC,OAAO,CAAC,EAAE,CAAC;IACrC,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,2FAA2F;AAC3F,MAAM,WAAW,kBAAkB,CAAC,CAAC;IACnC,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1B,QAAQ,CAAC,EAAE,cAAc,CAAC,OAAO,CAAC,EAAE,CAAC;IACrC,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,6GAA6G;AAC7G,wBAAgB,oBAAoB,CAAC,CAAC,EACpC,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,SAAS,EACrB,aAAa,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,CAAM,GAC/D,OAAO,CAUT;AAgBD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO,EAC7D,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC,GACvB,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAQ9B;AAED,oGAAoG;AACpG,wBAAgB,wBAAwB,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO,EAC/D,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC,GACvB,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAQhC"}
1
+ {"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAC9C,OAAO,EAML,KAAK,aAAa,EAClB,KAAK,IAAI,EACT,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAA2C,KAAK,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAExH,4FAA4F;AAC5F,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,kBAAkB,GAAG,kBAAkB,EAAE,GAAG,IAAI,CAAC;IACxD,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B,IAAI,CAAC,EAAE,IAAI,CAAC;CACb;AAED,iFAAiF;AACjF,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IAGX,UAAU,EAAE,CAAC,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC7C,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,IAAI,CAAC,EAAE,IAAI,CAAC;CACb;AAED,yGAAyG;AACzG,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC;IACxB,QAAQ,CAAC,EAAE,cAAc,CAAC,OAAO,CAAC,EAAE,CAAC;IACrC,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,2FAA2F;AAC3F,MAAM,WAAW,kBAAkB,CAAC,CAAC;IACnC,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1B,QAAQ,CAAC,EAAE,cAAc,CAAC,OAAO,CAAC,EAAE,CAAC;IACrC,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EACpC,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,SAAS,EACrB,aAAa,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,CAAM,GAC/D,OAAO,CAUT;AAgBD;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO,EAC7D,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC,GACvB,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAQ9B;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO,EAC/D,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC,GACvB,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAQhC"}
package/dist/document.js CHANGED
@@ -1,13 +1,18 @@
1
1
  import { z } from 'zod';
2
- import { jsonApiObjectSchema, metaSchema, paginationLinksSchema, resourceLinksSchema, } from './primitives.js';
2
+ import { jsonApiObjectSchema, metaSchema, paginationLinksSchema, resourceLinksSchema, topLevelLinksSchema, } from './primitives.js';
3
3
  import { relationshipsSchema, reachableResources } from './relationship.js';
4
- /** Wraps `attributes` into a resource object - `{ type, id, attributes, relationships?, links?, meta? }`. */
4
+ /**
5
+ * Wraps `attributes` into a resource object - `{ type, id, attributes?, relationships?, links?, meta? }`.
6
+ *
7
+ * `attributes` parses as absent here, unlike {@link ResourceObject}'s own required member: this schema
8
+ * also validates a document read off another server, which may publish a resource with none.
9
+ */
5
10
  export function resourceObjectSchema(type, attributes, relationships = {}) {
6
11
  const linkage = relationshipsSchema(relationships);
7
12
  return z.object({
8
13
  type: z.literal(type),
9
14
  id: z.string(),
10
- attributes,
15
+ attributes: attributes.optional(),
11
16
  ...(linkage ? { relationships: linkage.optional() } : {}),
12
17
  links: resourceLinksSchema.optional(),
13
18
  meta: metaSchema.optional(),
@@ -32,19 +37,26 @@ function includedSchema(root) {
32
37
  * `A` is asserted rather than inferred: the schema is assembled from a relationship graph reached
33
38
  * through thunks, so nothing static carries the attribute type, and a client typing a route off this
34
39
  * schema would otherwise read every member as `unknown`.
40
+ *
41
+ * Strict at the top level: the standard forbids any top-level member beyond these (extension members
42
+ * aside), which is what refuses a `data` document that also carries `errors`.
35
43
  */
36
44
  export function resourceDocumentSchema(root) {
37
- return z.object({
45
+ return z.strictObject({
38
46
  data: root.resourceObject,
39
47
  included: includedSchema(root).optional(),
40
- links: resourceLinksSchema.optional(),
48
+ links: topLevelLinksSchema.optional(),
41
49
  meta: metaSchema.optional(),
42
50
  jsonapi: jsonApiObjectSchema.optional(),
43
51
  });
44
52
  }
45
- /** The document for a COLLECTION - the same members, with `data` an array and paginated `links`. */
53
+ /**
54
+ * The document for a COLLECTION - the same members, with `data` an array and paginated `links`.
55
+ *
56
+ * Strict at the top level for the same reason as {@link resourceDocumentSchema}.
57
+ */
46
58
  export function collectionDocumentSchema(root) {
47
- return z.object({
59
+ return z.strictObject({
48
60
  data: z.array(root.resourceObject),
49
61
  included: includedSchema(root).optional(),
50
62
  links: paginationLinksSchema.optional(),
@@ -42,7 +42,7 @@ export declare const errorLinksSchema: z.ZodObject<{
42
42
  }, z.core.$strip>, z.ZodNull]>>;
43
43
  }, z.core.$strip>;
44
44
  export type ErrorLinks = z.infer<typeof errorLinksSchema>;
45
- /** A single JSON:API error object; every member is optional, and nothing here enforces the spec's at-least-one. */
45
+ /** A single JSON:API error object: every member is optional, and at least one is present. */
46
46
  export declare const errorObjectSchema: z.ZodObject<{
47
47
  id: z.ZodOptional<z.ZodString>;
48
48
  links: z.ZodOptional<z.ZodObject<{
@@ -94,6 +94,9 @@ export type ErrorObject = z.infer<typeof errorObjectSchema>;
94
94
  /**
95
95
  * A JSON:API error document - a top-level `errors` array of at least one member, which never carries `data`.
96
96
  *
97
+ * Strict at the top level: the standard forbids any top-level member beyond these (extension members
98
+ * aside), which is what refuses a document that carries both `errors` and `data`.
99
+ *
97
100
  * Nothing here renders the top-level `meta` or `links`. They are declared so the schema `errorResponse` publishes is
98
101
  * the whole document, and a client parsing another server's document keeps them.
99
102
  */
@@ -182,8 +185,10 @@ export declare const errorDocumentSchema: z.ZodObject<{
182
185
  }, z.core.$strip>>;
183
186
  jsonapi: z.ZodOptional<z.ZodObject<{
184
187
  version: z.ZodOptional<z.ZodString>;
188
+ ext: z.ZodOptional<z.ZodArray<z.ZodString>>;
189
+ profile: z.ZodOptional<z.ZodArray<z.ZodString>>;
185
190
  meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
186
191
  }, z.core.$strip>>;
187
- }, z.core.$strip>;
192
+ }, z.core.$strict>;
188
193
  export type ErrorDocument = z.infer<typeof errorDocumentSchema>;
189
194
  //# sourceMappingURL=error-document.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"error-document.d.ts","sourceRoot":"","sources":["../../src/errors/error-document.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,sFAAsF;AACtF,eAAO,MAAM,iBAAiB;;;;iBAMlB,CAAC;AACb,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,qGAAqG;AACrG,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAA8D,CAAC;AAC5F,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,mHAAmH;AACnH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAWlB,CAAC;AACb,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAK9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC"}
1
+ {"version":3,"file":"error-document.d.ts","sourceRoot":"","sources":["../../src/errors/error-document.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,sFAAsF;AACtF,eAAO,MAAM,iBAAiB;;;;iBAMlB,CAAC;AACb,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,qGAAqG;AACrG,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAA8D,CAAC;AAC5F,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,6FAA6F;AAC7F,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAYoF,CAAC;AACnH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAK9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC"}
@@ -10,7 +10,7 @@ export const errorSourceSchema = z
10
10
  .partial();
11
11
  /** An error object's `links`: a page about this occurrence, and a page about its type of problem. */
12
12
  export const errorLinksSchema = z.object({ about: linkSchema, type: linkSchema }).partial();
13
- /** A single JSON:API error object; every member is optional, and nothing here enforces the spec's at-least-one. */
13
+ /** A single JSON:API error object: every member is optional, and at least one is present. */
14
14
  export const errorObjectSchema = z
15
15
  .object({
16
16
  id: z.string(),
@@ -22,14 +22,18 @@ export const errorObjectSchema = z
22
22
  source: errorSourceSchema,
23
23
  meta: metaSchema,
24
24
  })
25
- .partial();
25
+ .partial()
26
+ .refine((object) => Object.keys(object).length > 0, { message: 'An error object carries at least one member.' });
26
27
  /**
27
28
  * A JSON:API error document - a top-level `errors` array of at least one member, which never carries `data`.
28
29
  *
30
+ * Strict at the top level: the standard forbids any top-level member beyond these (extension members
31
+ * aside), which is what refuses a document that carries both `errors` and `data`.
32
+ *
29
33
  * Nothing here renders the top-level `meta` or `links`. They are declared so the schema `errorResponse` publishes is
30
34
  * the whole document, and a client parsing another server's document keeps them.
31
35
  */
32
- export const errorDocumentSchema = z.object({
36
+ export const errorDocumentSchema = z.strictObject({
33
37
  errors: z.array(errorObjectSchema).min(1),
34
38
  meta: metaSchema.optional(),
35
39
  links: z.object({ self: linkSchema, describedby: linkSchema }).partial().optional(),
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  * The response half of the HTTP contract, in the one wire spelling this house answers: a JSON:API document.
3
3
  */
4
4
  export { errorResponse, listResponse, okResponse, type JsonApiResponse, type RetryAfterHeader, } from './api-response.js';
5
+ export { createdResponse } from './created-response.js';
5
6
  export type { CollectionDocument, Relationship, ResourceDocument, ResourceObject } from './document.js';
6
7
  export * from './document-graph.js';
7
8
  export { declaredDocumentShape } from './document-shape.js';
@@ -9,7 +10,7 @@ export { createDomainErrorResolver, defineErrorMap, errorEntry, type DomainError
9
10
  export type { DomainErrorMap, ErrorClass, UpstreamErrorMap } from './error-map.js';
10
11
  export { BASE_ERROR_CODES, errorDocumentSchema, JsonApiError, toErrorDocument, toErrorObject, type ErrorDocument, type ErrorLinks, type ErrorObject, type ErrorSource, type ErrorStatus, type JsonApiErrorInit, type ResolvedError, } from './errors/index.js';
11
12
  export { JSON_API_MEDIA_TYPE } from './media-type.js';
12
- export type { JsonApiObject, Link, Meta, PaginationLinks, ResourceIdentifier, ResourceLinks } from './primitives.js';
13
+ export type { JsonApiObject, Link, Meta, PaginationLinks, RelationshipLinks, ResourceIdentifier, ResourceLinks, TopLevelLinks, } from './primitives.js';
13
14
  export { toMany, toOne, type LoadContextArguments, type RelatedResource, type RelationshipDef, type RelationshipLoader, type RelationshipOptions, } from './relationship.js';
14
15
  export { jsonApiResource, type JsonApiResource, type ResourceOptions } from './resource.js';
15
16
  export type { CollectionExtras, LoadedRows, RequestedDocumentShape, ResourceSerializer, Row } from './serialize.js';
@@ -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,YAAY,EAAE,kBAAkB,EAAE,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACxG,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,YAAY,EAAE,aAAa,EAAE,IAAI,EAAE,IAAI,EAAE,eAAe,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrH,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,EAAE,kBAAkB,EAAE,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACxG,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,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"}
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  * The response half of the HTTP contract, in the one wire spelling this house answers: a JSON:API document.
3
3
  */
4
4
  export { errorResponse, listResponse, okResponse, } from './api-response.js';
5
+ export { createdResponse } from './created-response.js';
5
6
  export * from './document-graph.js';
6
7
  export { declaredDocumentShape } from './document-shape.js';
7
8
  export { createDomainErrorResolver, defineErrorMap, errorEntry, } from './domain-error-resolver.js';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Whether a string is a legal JSON:API member name.
3
+ *
4
+ * Copied rather than imported at `packages/query/src/bracket-params.ts`'s own `isMemberName`: this
5
+ * package may not import `@zeroxsolutions/query` (nor the reverse), so each side of that boundary
6
+ * keeps its own copy of the one rule instead.
7
+ */
8
+ export declare function isMemberName(name: string): boolean;
9
+ //# sourceMappingURL=member-name.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"member-name.d.ts","sourceRoot":"","sources":["../src/member-name.ts"],"names":[],"mappings":"AAQA;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAElD"}
@@ -0,0 +1,15 @@
1
+ /** Characters a member name may start or end with: the standard's globally allowed characters. */
2
+ const GLOBAL = 'a-zA-Z0-9\\u0080-\\u{10FFFF}';
3
+ /** Additionally allowed between the first and last character: hyphen, low line and space. */
4
+ const MIDDLE = '\\-_ ';
5
+ const MEMBER_NAME = new RegExp(`^[${GLOBAL}](?:[${GLOBAL}${MIDDLE}]*[${GLOBAL}])?$`, 'u');
6
+ /**
7
+ * Whether a string is a legal JSON:API member name.
8
+ *
9
+ * Copied rather than imported at `packages/query/src/bracket-params.ts`'s own `isMemberName`: this
10
+ * package may not import `@zeroxsolutions/query` (nor the reverse), so each side of that boundary
11
+ * keeps its own copy of the one rule instead.
12
+ */
13
+ export function isMemberName(name) {
14
+ return MEMBER_NAME.test(name);
15
+ }
@@ -59,7 +59,60 @@ export declare const resourceLinksSchema: z.ZodObject<{
59
59
  }, z.core.$strip>;
60
60
  /** A resource object's links - inferred from {@link resourceLinksSchema}. */
61
61
  export type ResourceLinks = z.infer<typeof resourceLinksSchema>;
62
- /** Collection pagination links: `self` plus the `first`/`prev`/`next`/`last` navigation, each null at the page that has none. */
62
+ /** A document's own top-level `self`/`related`, plus the description of the document itself. */
63
+ export declare const topLevelLinksSchema: z.ZodObject<{
64
+ self: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
65
+ rel: z.ZodOptional<z.ZodString>;
66
+ title: z.ZodOptional<z.ZodString>;
67
+ type: z.ZodOptional<z.ZodString>;
68
+ hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
69
+ meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
70
+ href: z.ZodString;
71
+ describedby: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
72
+ rel: z.ZodOptional<z.ZodString>;
73
+ title: z.ZodOptional<z.ZodString>;
74
+ type: z.ZodOptional<z.ZodString>;
75
+ hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
76
+ meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
77
+ href: z.ZodString;
78
+ }, z.core.$strip>, z.ZodNull]>>;
79
+ }, z.core.$strip>, z.ZodNull]>>;
80
+ related: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
81
+ rel: z.ZodOptional<z.ZodString>;
82
+ title: z.ZodOptional<z.ZodString>;
83
+ type: z.ZodOptional<z.ZodString>;
84
+ hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
85
+ meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
86
+ href: z.ZodString;
87
+ describedby: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
88
+ rel: z.ZodOptional<z.ZodString>;
89
+ title: z.ZodOptional<z.ZodString>;
90
+ type: z.ZodOptional<z.ZodString>;
91
+ hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
92
+ meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
93
+ href: z.ZodString;
94
+ }, z.core.$strip>, z.ZodNull]>>;
95
+ }, z.core.$strip>, z.ZodNull]>>;
96
+ describedby: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
97
+ rel: z.ZodOptional<z.ZodString>;
98
+ title: z.ZodOptional<z.ZodString>;
99
+ type: z.ZodOptional<z.ZodString>;
100
+ hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
101
+ meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
102
+ href: z.ZodString;
103
+ describedby: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
104
+ rel: z.ZodOptional<z.ZodString>;
105
+ title: z.ZodOptional<z.ZodString>;
106
+ type: z.ZodOptional<z.ZodString>;
107
+ hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
108
+ meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
109
+ href: z.ZodString;
110
+ }, z.core.$strip>, z.ZodNull]>>;
111
+ }, z.core.$strip>, z.ZodNull]>>;
112
+ }, z.core.$strip>;
113
+ /** A single-resource document's top-level links - inferred from {@link topLevelLinksSchema}. */
114
+ export type TopLevelLinks = z.infer<typeof topLevelLinksSchema>;
115
+ /** Collection pagination links, plus the description of the document itself. */
63
116
  export declare const paginationLinksSchema: z.ZodObject<{
64
117
  self: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
65
118
  rel: z.ZodOptional<z.ZodString>;
@@ -141,9 +194,126 @@ export declare const paginationLinksSchema: z.ZodObject<{
141
194
  href: z.ZodString;
142
195
  }, z.core.$strip>, z.ZodNull]>>;
143
196
  }, z.core.$strip>, z.ZodNull]>>;
197
+ describedby: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
198
+ rel: z.ZodOptional<z.ZodString>;
199
+ title: z.ZodOptional<z.ZodString>;
200
+ type: z.ZodOptional<z.ZodString>;
201
+ hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
202
+ meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
203
+ href: z.ZodString;
204
+ describedby: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
205
+ rel: z.ZodOptional<z.ZodString>;
206
+ title: z.ZodOptional<z.ZodString>;
207
+ type: z.ZodOptional<z.ZodString>;
208
+ hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
209
+ meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
210
+ href: z.ZodString;
211
+ }, z.core.$strip>, z.ZodNull]>>;
212
+ }, z.core.$strip>, z.ZodNull]>>;
144
213
  }, z.core.$strip>;
145
214
  /** Collection pagination links - inferred from {@link paginationLinksSchema}. */
146
215
  export type PaginationLinks = z.infer<typeof paginationLinksSchema>;
216
+ /** A relationship's links: its own `self`/`related`, and the pages of a to-many linkage. */
217
+ export declare const relationshipLinksSchema: z.ZodObject<{
218
+ self: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
219
+ rel: z.ZodOptional<z.ZodString>;
220
+ title: z.ZodOptional<z.ZodString>;
221
+ type: z.ZodOptional<z.ZodString>;
222
+ hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
223
+ meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
224
+ href: z.ZodString;
225
+ describedby: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
226
+ rel: z.ZodOptional<z.ZodString>;
227
+ title: z.ZodOptional<z.ZodString>;
228
+ type: z.ZodOptional<z.ZodString>;
229
+ hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
230
+ meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
231
+ href: z.ZodString;
232
+ }, z.core.$strip>, z.ZodNull]>>;
233
+ }, z.core.$strip>, z.ZodNull]>>;
234
+ related: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
235
+ rel: z.ZodOptional<z.ZodString>;
236
+ title: z.ZodOptional<z.ZodString>;
237
+ type: z.ZodOptional<z.ZodString>;
238
+ hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
239
+ meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
240
+ href: z.ZodString;
241
+ describedby: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
242
+ rel: z.ZodOptional<z.ZodString>;
243
+ title: z.ZodOptional<z.ZodString>;
244
+ type: z.ZodOptional<z.ZodString>;
245
+ hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
246
+ meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
247
+ href: z.ZodString;
248
+ }, z.core.$strip>, z.ZodNull]>>;
249
+ }, z.core.$strip>, z.ZodNull]>>;
250
+ first: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
251
+ rel: z.ZodOptional<z.ZodString>;
252
+ title: z.ZodOptional<z.ZodString>;
253
+ type: z.ZodOptional<z.ZodString>;
254
+ hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
255
+ meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
256
+ href: z.ZodString;
257
+ describedby: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
258
+ rel: z.ZodOptional<z.ZodString>;
259
+ title: z.ZodOptional<z.ZodString>;
260
+ type: z.ZodOptional<z.ZodString>;
261
+ hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
262
+ meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
263
+ href: z.ZodString;
264
+ }, z.core.$strip>, z.ZodNull]>>;
265
+ }, z.core.$strip>, z.ZodNull]>>;
266
+ last: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
267
+ rel: z.ZodOptional<z.ZodString>;
268
+ title: z.ZodOptional<z.ZodString>;
269
+ type: z.ZodOptional<z.ZodString>;
270
+ hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
271
+ meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
272
+ href: z.ZodString;
273
+ describedby: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
274
+ rel: z.ZodOptional<z.ZodString>;
275
+ title: z.ZodOptional<z.ZodString>;
276
+ type: z.ZodOptional<z.ZodString>;
277
+ hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
278
+ meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
279
+ href: z.ZodString;
280
+ }, z.core.$strip>, z.ZodNull]>>;
281
+ }, z.core.$strip>, z.ZodNull]>>;
282
+ prev: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
283
+ rel: z.ZodOptional<z.ZodString>;
284
+ title: z.ZodOptional<z.ZodString>;
285
+ type: z.ZodOptional<z.ZodString>;
286
+ hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
287
+ meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
288
+ href: z.ZodString;
289
+ describedby: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
290
+ rel: z.ZodOptional<z.ZodString>;
291
+ title: z.ZodOptional<z.ZodString>;
292
+ type: z.ZodOptional<z.ZodString>;
293
+ hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
294
+ meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
295
+ href: z.ZodString;
296
+ }, z.core.$strip>, z.ZodNull]>>;
297
+ }, z.core.$strip>, z.ZodNull]>>;
298
+ next: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
299
+ rel: z.ZodOptional<z.ZodString>;
300
+ title: z.ZodOptional<z.ZodString>;
301
+ type: z.ZodOptional<z.ZodString>;
302
+ hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
303
+ meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
304
+ href: z.ZodString;
305
+ describedby: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
306
+ rel: z.ZodOptional<z.ZodString>;
307
+ title: z.ZodOptional<z.ZodString>;
308
+ type: z.ZodOptional<z.ZodString>;
309
+ hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
310
+ meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
311
+ href: z.ZodString;
312
+ }, z.core.$strip>, z.ZodNull]>>;
313
+ }, z.core.$strip>, z.ZodNull]>>;
314
+ }, z.core.$strip>;
315
+ /** A relationship's links - inferred from {@link relationshipLinksSchema}. */
316
+ export type RelationshipLinks = z.infer<typeof relationshipLinksSchema>;
147
317
  /** A resource identifier object - the `{ type, id }` linkage that points at another resource. */
148
318
  export declare const resourceIdentifierSchema: z.ZodObject<{
149
319
  type: z.ZodString;
@@ -151,9 +321,11 @@ export declare const resourceIdentifierSchema: z.ZodObject<{
151
321
  }, z.core.$strip>;
152
322
  /** A type-and-id resource identifier - inferred from {@link resourceIdentifierSchema}. */
153
323
  export type ResourceIdentifier = z.infer<typeof resourceIdentifierSchema>;
154
- /** The top-level `jsonapi` member - the spec version this document is written to, plus meta. */
324
+ /** The top-level `jsonapi` member: the version this document is written to, the extensions and profiles it applies. */
155
325
  export declare const jsonApiObjectSchema: z.ZodObject<{
156
326
  version: z.ZodOptional<z.ZodString>;
327
+ ext: z.ZodOptional<z.ZodArray<z.ZodString>>;
328
+ profile: z.ZodOptional<z.ZodArray<z.ZodString>>;
157
329
  meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
158
330
  }, z.core.$strip>;
159
331
  /** The top-level `jsonapi` member - inferred from {@link jsonApiObjectSchema}. */
@@ -1 +1 @@
1
- {"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["../src/primitives.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,8DAA8D;AAC9D,eAAO,MAAM,UAAU,wCAAoC,CAAC;AAC5D,oEAAoE;AACpE,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAsB9C,iGAAiG;AACjG,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;8BAIrB,CAAC;AACH,gEAAgE;AAChE,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAE9C,iFAAiF;AACjF,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAG9B,CAAC;AACH,6EAA6E;AAC7E,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,iIAAiI;AACjI,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAMhC,CAAC;AACH,iFAAiF;AACjF,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,iGAAiG;AACjG,eAAO,MAAM,wBAAwB;;;iBAAiD,CAAC;AACvF,0FAA0F;AAC1F,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,gGAAgG;AAChG,eAAO,MAAM,mBAAmB;;;iBAG9B,CAAC;AACH,kFAAkF;AAClF,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC"}
1
+ {"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["../src/primitives.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,8DAA8D;AAC9D,eAAO,MAAM,UAAU,wCAAoC,CAAC;AAC5D,oEAAoE;AACpE,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAsB9C,iGAAiG;AACjG,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;8BAIrB,CAAC;AACH,gEAAgE;AAChE,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAE9C,iFAAiF;AACjF,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAG9B,CAAC;AACH,6EAA6E;AAC7E,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,gGAAgG;AAChG,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAqE,CAAC;AACtG,gGAAgG;AAChG,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,gFAAgF;AAChF,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAOhC,CAAC;AACH,iFAAiF;AACjF,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,4FAA4F;AAC5F,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAKlC,CAAC;AACH,8EAA8E;AAC9E,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE,iGAAiG;AACjG,eAAO,MAAM,wBAAwB;;;iBAAiD,CAAC;AACvF,0FAA0F;AAC1F,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,uHAAuH;AACvH,eAAO,MAAM,mBAAmB;;;;;iBAK9B,CAAC;AACH,kFAAkF;AAClF,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC"}
@@ -30,18 +30,30 @@ export const resourceLinksSchema = z.object({
30
30
  self: linkSchema.optional(),
31
31
  related: linkSchema.optional(),
32
32
  });
33
- /** Collection pagination links: `self` plus the `first`/`prev`/`next`/`last` navigation, each null at the page that has none. */
33
+ /** A document's own top-level `self`/`related`, plus the description of the document itself. */
34
+ export const topLevelLinksSchema = resourceLinksSchema.extend({ describedby: linkSchema.optional() });
35
+ /** Collection pagination links, plus the description of the document itself. */
34
36
  export const paginationLinksSchema = z.object({
35
37
  self: linkSchema.optional(),
36
38
  first: linkSchema.optional(),
37
39
  prev: linkSchema.optional(),
38
40
  next: linkSchema.optional(),
39
41
  last: linkSchema.optional(),
42
+ describedby: linkSchema.optional(),
43
+ });
44
+ /** A relationship's links: its own `self`/`related`, and the pages of a to-many linkage. */
45
+ export const relationshipLinksSchema = resourceLinksSchema.extend({
46
+ first: linkSchema.optional(),
47
+ last: linkSchema.optional(),
48
+ prev: linkSchema.optional(),
49
+ next: linkSchema.optional(),
40
50
  });
41
51
  /** A resource identifier object - the `{ type, id }` linkage that points at another resource. */
42
52
  export const resourceIdentifierSchema = z.object({ type: z.string(), id: z.string() });
43
- /** The top-level `jsonapi` member - the spec version this document is written to, plus meta. */
53
+ /** The top-level `jsonapi` member: the version this document is written to, the extensions and profiles it applies. */
44
54
  export const jsonApiObjectSchema = z.object({
45
55
  version: z.string().optional(),
56
+ ext: z.array(z.string()).optional(),
57
+ profile: z.array(z.string()).optional(),
46
58
  meta: metaSchema.optional(),
47
59
  });
@@ -1 +1 @@
1
- {"version":3,"file":"relationship.d.ts","sourceRoot":"","sources":["../src/relationship.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"relationship.d.ts","sourceRoot":"","sources":["../src/relationship.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { metaSchema, resourceIdentifierSchema, resourceLinksSchema } from './primitives.js';
2
+ import { metaSchema, relationshipLinksSchema, resourceIdentifierSchema } from './primitives.js';
3
3
  /** The context a serialize call was handed, for the load pass to give the loaders. */
4
4
  export function loadContext(contextArguments) {
5
5
  // A conditional tuple's element does not narrow back to `C`, and this is the one place that reads it.
@@ -21,10 +21,14 @@ export function toMany(related, read, options) {
21
21
  /** The linkage schema for one relationship, pinned to the related `type` so a wrong type fails the parse. */
22
22
  export function relationshipSchema(def) {
23
23
  const identifier = resourceIdentifierSchema.extend({ type: z.literal(def.related().type) });
24
- return z.object({
24
+ return z
25
+ .object({
25
26
  data: (def.cardinality === 'one' ? identifier.nullable() : z.array(identifier)).optional(),
26
- links: resourceLinksSchema.optional(),
27
+ links: relationshipLinksSchema.optional(),
27
28
  meta: metaSchema.optional(),
29
+ })
30
+ .refine((relationship) => Object.keys(relationship).length > 0, {
31
+ message: 'A relationship object carries links, data or meta.',
28
32
  });
29
33
  }
30
34
  /** The `relationships` member for a whole resource - one key per declaration, absent when it declares none. */
@@ -39,7 +39,16 @@ export interface JsonApiResource<A extends ZodObject, C = unknown, T extends str
39
39
  /** Serializes rows into a collection document, merging in pagination `links` and `meta`. */
40
40
  serializeMany(rows: Row[], extras?: CollectionExtras, requested?: RequestedDocumentShape, ...context: LoadContextArguments<C>): Promise<CollectionDocument<output<A>>>;
41
41
  }
42
+ /** A relationship map's own names, and none where the map is the empty default. */
43
+ type RelationshipName<R> = string extends keyof R ? never : keyof R & string;
44
+ /**
45
+ * `never` where a name breaks the standard's one field namespace (an attribute or relationship named `type` or `id`,
46
+ * or a name that is both), which turns the declaring call into a compile error.
47
+ */
48
+ type FieldNamespace<A extends ZodObject, R> = [
49
+ (keyof A['shape'] & string & ('type' | 'id' | RelationshipName<R>)) | (RelationshipName<R> & ('type' | 'id'))
50
+ ] extends [never] ? unknown : never;
42
51
  /** Declares a JSON:API resource type - its `type`, the schema of its `attributes`, and its relationships. */
43
- export declare function jsonApiResource<const T extends string, A extends ZodObject, const R extends Readonly<Record<string, RelationshipDef<any, any>>> = Readonly<Record<string, never>>, C = ContextOfRelationships<R>>(type: T, attributes: A, options?: ResourceOptions<R>): JsonApiResource<A, C, T, R>;
52
+ export declare function jsonApiResource<const T extends string, A extends ZodObject, const R extends Readonly<Record<string, RelationshipDef<any, any>>> = Readonly<Record<string, never>>, C = ContextOfRelationships<R>>(type: T, attributes: A & FieldNamespace<A, R>, options?: ResourceOptions<R>): JsonApiResource<A, C, T, R>;
44
53
  export {};
45
54
  //# sourceMappingURL=resource.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AACtD,OAAO,EAIL,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACtB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAe,KAAK,oBAAoB,EAAE,KAAK,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACvH,OAAO,EAIL,KAAK,gBAAgB,EAErB,KAAK,sBAAsB,EAE3B,KAAK,GAAG,EACT,MAAM,gBAAgB,CAAC;AAExB,iEAAiE;AAEjE,KAAK,sBAAsB,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,SAAS,eAAe,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC;AAEhG,uDAAuD;AACvD,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,mGAAmG;IACnG,aAAa,CAAC,EAAE,CAAC,CAAC;CACnB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,eAAe,CAC9B,CAAC,SAAS,SAAS,EACnB,CAAC,GAAG,OAAO,EACX,CAAC,SAAS,MAAM,GAAG,MAAM,EAEzB,CAAC,SAAS,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAC5G,SAAQ,eAAe,CAAC,CAAC,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACjB,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;IACvB,QAAQ,CAAC,aAAa,EAAE,CAAC,CAAC;IAC1B,4FAA4F;IAC5F,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACxD,8EAA8E;IAC9E,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACpE;;;;;;OAMG;IACH,SAAS,CACP,GAAG,EAAE,GAAG,EACR,SAAS,CAAC,EAAE,sBAAsB,EAClC,GAAG,OAAO,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAClC,OAAO,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACxC,4FAA4F;IAC5F,aAAa,CACX,IAAI,EAAE,GAAG,EAAE,EACX,MAAM,CAAC,EAAE,gBAAgB,EACzB,SAAS,CAAC,EAAE,sBAAsB,EAClC,GAAG,OAAO,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAClC,OAAO,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAC3C;AAgBD,6GAA6G;AAC7G,wBAAgB,eAAe,CAC7B,KAAK,CAAC,CAAC,SAAS,MAAM,EACtB,CAAC,SAAS,SAAS,EAEnB,KAAK,CAAC,CAAC,SAAS,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,EACrG,CAAC,GAAG,sBAAsB,CAAC,CAAC,CAAC,EAC7B,IAAI,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,OAAO,GAAE,eAAe,CAAC,CAAC,CAAM,GAAG,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAwEvF"}
1
+ {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"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"}
package/dist/resource.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { Relator } from 'ts-japi';
2
+ import { z } from 'zod';
2
3
  import { collectionDocumentSchema, resourceDocumentSchema, resourceObjectSchema, } from './document.js';
3
4
  import { loadedRelated, loadIncludedRows } from './document-load.js';
5
+ import { isMemberName } from './member-name.js';
4
6
  import { loadContext } from './relationship.js';
5
- import { createSerializer, serializeCollection, serializeResource, } from './serialize.js';
7
+ import { assertRowId, createSerializer, serializeCollection, serializeResource, } from './serialize.js';
6
8
  /**
7
9
  * How many requested shapes one resource keeps a serializer for. The shape is spelled by the caller's
8
10
  * query, so past this bound a serializer is built for the call and not kept.
@@ -15,11 +17,67 @@ const MAX_KEPT_SERIALIZERS = 64;
15
17
  function shapeKey(requested) {
16
18
  return JSON.stringify([requested?.fields ?? null, requested?.include ?? null]);
17
19
  }
20
+ /**
21
+ * Steps past the wrappers that change presence or transport, not shape, as a declaration's schemas nest:
22
+ * optional/nullable/default, an array's element, a union's members, and a pipe's `in` - which is what a
23
+ * `.transform()` or `.pipe()` compiles to, so a nested object behind either is still checked.
24
+ *
25
+ * A `z.record()` and a `z.lazy()` schema are left unwalked on purpose: a record's keys are the caller's
26
+ * own data, not members this declaration names, and a lazy schema resolves its inner type only on use,
27
+ * which is past the point this walk runs.
28
+ */
29
+ function membersOf(schema) {
30
+ const def = schema.def;
31
+ if (def.innerType)
32
+ return membersOf(def.innerType);
33
+ if (def.type === 'array' && def.element)
34
+ return membersOf(def.element);
35
+ if (def.type === 'union' && def.options)
36
+ return def.options.flatMap(membersOf);
37
+ if (def.type === 'pipe' && def.in)
38
+ return membersOf(def.in);
39
+ return [schema];
40
+ }
41
+ /** Every member name an attribute schema declares, at every depth. */
42
+ function memberNames(schema) {
43
+ return membersOf(schema).flatMap((member) => member instanceof z.ZodObject
44
+ ? Object.entries(member.shape).flatMap(([name, inner]) => [name, ...memberNames(inner)])
45
+ : []);
46
+ }
47
+ /**
48
+ * @throws Error on a type or member name the standard does not allow, a name outside the one field namespace, or an
49
+ * attribute that republishes a relationship's own reference as a foreign key
50
+ */
51
+ function assertDeclaration(type, attributes,
52
+ // Only `cardinality` is read here; the full `RelationshipDef<C, R>` is contravariant in `C` through its
53
+ // optional `load`, which makes a map typed with concrete C/R fail against one typed `any`/`any`.
54
+ relationships) {
55
+ if (!isMemberName(type))
56
+ throw new Error(`"${type}" is not a legal JSON:API type.`);
57
+ const attributeNames = Object.keys(attributes.shape);
58
+ for (const name of [...memberNames(attributes), ...Object.keys(relationships)]) {
59
+ if (!isMemberName(name))
60
+ throw new Error(`"${name}" is not a legal JSON:API member name on "${type}".`);
61
+ }
62
+ for (const name of [...attributeNames, ...Object.keys(relationships)]) {
63
+ if (name === 'type' || name === 'id')
64
+ throw new Error(`"${type}" cannot declare a field named "${name}".`);
65
+ }
66
+ for (const [name, def] of Object.entries(relationships)) {
67
+ if (attributeNames.includes(name))
68
+ throw new Error(`"${type}" declares "${name}" as an attribute and a relationship.`);
69
+ const keys = def.cardinality === 'one' ? [`${name}Id`, `${name}_id`] : [`${name}Ids`, `${name}_ids`];
70
+ const foreignKey = keys.find((key) => attributeNames.includes(key));
71
+ if (foreignKey)
72
+ throw new Error(`"${type}" publishes "${foreignKey}"; the "${name}" relationship already carries it.`);
73
+ }
74
+ }
18
75
  /** Declares a JSON:API resource type - its `type`, the schema of its `attributes`, and its relationships. */
19
76
  export function jsonApiResource(type, attributes, options = {}) {
20
77
  // Safe: when `relationships` is omitted, R is inferred as its own empty-relationships default above,
21
78
  // which `{}` already satisfies.
22
79
  const relationships = (options.relationships ?? {});
80
+ assertDeclaration(type, attributes, relationships);
23
81
  // The attribute keys ARE the projection, so a row member the schema does not declare never reaches the wire.
24
82
  const projection = Object.fromEntries(Object.keys(attributes.shape).map((name) => [name, 1]));
25
83
  const serializers = new Map();
@@ -46,7 +104,14 @@ export function jsonApiResource(type, attributes, options = {}) {
46
104
  return built;
47
105
  const relators = Object.fromEntries(Object.entries(relationships).map(([name, def]) => [
48
106
  name,
49
- new Relator(async (row) => def.load === undefined ? def.read(row) : loadedRelated(def, row, loaded),
107
+ new Relator(async (row) => {
108
+ const related = def.load === undefined ? def.read(row) : loadedRelated(def, row, loaded);
109
+ // A relationship absent on the row reads back `undefined`, not the `null` its own type promises.
110
+ for (const one of Array.isArray(related) ? related : related == null ? [] : [related]) {
111
+ assertRowId(def.related().type, one);
112
+ }
113
+ return related;
114
+ },
50
115
  // The lazy overload, so a pair of resources naming each other resolves on use, not on construction.
51
116
  () => def.related().serializerFor(requested, loaded), { relatedName: name }),
52
117
  ]));
@@ -57,11 +122,14 @@ export function jsonApiResource(type, attributes, options = {}) {
57
122
  return serializer;
58
123
  },
59
124
  async serialize(row, requested, ...context) {
125
+ assertRowId(type, row);
60
126
  const loaded = await loadIncludedRows(resource, [row], requested, loadContext(context));
61
127
  // ts-japi hands back an untyped object; the declared attributes are this resource's own schema.
62
128
  return serializeResource(resource.serializerFor(requested, loaded), row, loaded);
63
129
  },
64
130
  async serializeMany(rows, extras, requested, ...context) {
131
+ for (const row of rows)
132
+ assertRowId(type, row);
65
133
  const loaded = await loadIncludedRows(resource, rows, requested, loadContext(context));
66
134
  return serializeCollection(resource.serializerFor(requested, loaded), rows, extras, loaded);
67
135
  },
@@ -1,7 +1,17 @@
1
1
  import { Serializer, type SerializerOptions } from 'ts-japi';
2
2
  import type { PaginationLinks } from './primitives.js';
3
- /** One row as a serializer hands it over - the shape ts-japi works in. */
4
- export type Row = Record<string, unknown>;
3
+ /** One row as a serializer hands it over. Its `id` is the resource id, which the standard makes a string. */
4
+ export interface Row {
5
+ id: string;
6
+ [member: string]: unknown;
7
+ }
8
+ /**
9
+ * Refuses a row whose id is not a string at runtime. The type is the defence that matters: on a create this fires
10
+ * after the write committed, so it only stops a wrong document from going out.
11
+ *
12
+ * @throws TypeError naming the resource type
13
+ */
14
+ export declare function assertRowId(type: string, row: unknown): void;
5
15
  /**
6
16
  * The rows one document's relationship loaders answered, keyed by related type and then by id.
7
17
  *
@@ -46,8 +56,8 @@ export interface CollectionExtras {
46
56
  * @param data - the row to serialize; its `id` member becomes the resource id
47
57
  * @param loaded - what this document's relationship loaders answered, which decides its `included`
48
58
  */
49
- export declare function serializeResource(serializer: ResourceSerializer, data: Row, loaded?: LoadedRows): Promise<Row>;
59
+ export declare function serializeResource(serializer: ResourceSerializer, data: Row, loaded?: LoadedRows): Promise<Record<string, unknown>>;
50
60
  /** Serializes a list of rows into a collection document, merging in the pagination `links` and `meta`. */
51
- export declare function serializeCollection(serializer: ResourceSerializer, items: Row[], extras?: CollectionExtras, loaded?: LoadedRows): Promise<Row>;
61
+ export declare function serializeCollection(serializer: ResourceSerializer, items: Row[], extras?: CollectionExtras, loaded?: LoadedRows): Promise<Record<string, unknown>>;
52
62
  export {};
53
63
  //# sourceMappingURL=serialize.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../src/serialize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,UAAU,EAAqB,KAAK,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEzF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,0EAA0E;AAC1E,MAAM,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;AAEvE,sCAAsC;AACtC,MAAM,MAAM,kBAAkB,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;AAEjD,KAAK,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC;AAExD;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,mFAAmF;IACnF,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;IACrD,mEAAmE;IACnE,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAgCD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,gBAAgB,EAC1B,SAAS,CAAC,EAAE,sBAAsB,GACjC,kBAAkB,CAgBpB;AAED,2EAA2E;AAC3E,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,uCAAuC;IACvC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AA0BD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,UAAU,EAAE,kBAAkB,EAC9B,IAAI,EAAE,GAAG,EACT,MAAM,CAAC,EAAE,UAAU,GAClB,OAAO,CAAC,GAAG,CAAC,CAEd;AAED,0GAA0G;AAC1G,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,kBAAkB,EAC9B,KAAK,EAAE,GAAG,EAAE,EACZ,MAAM,GAAE,gBAAqB,EAC7B,MAAM,CAAC,EAAE,UAAU,GAClB,OAAO,CAAC,GAAG,CAAC,CAMd"}
1
+ {"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../src/serialize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,UAAU,EAAqB,KAAK,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEzF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,6GAA6G;AAC7G,MAAM,WAAW,GAAG;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;CAC3B;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,IAAI,CAK5D;AAED;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;AAEvE,sCAAsC;AACtC,MAAM,MAAM,kBAAkB,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;AAEjD,KAAK,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC;AAExD;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,mFAAmF;IACnF,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;IACrD,mEAAmE;IACnE,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAgCD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,gBAAgB,EAC1B,SAAS,CAAC,EAAE,sBAAsB,GACjC,kBAAkB,CAgBpB;AAED,2EAA2E;AAC3E,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,uCAAuC;IACvC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AA8CD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,UAAU,EAAE,kBAAkB,EAC9B,IAAI,EAAE,GAAG,EACT,MAAM,CAAC,EAAE,UAAU,GAClB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAElC;AAED,0GAA0G;AAC1G,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,kBAAkB,EAC9B,KAAK,EAAE,GAAG,EAAE,EACZ,MAAM,GAAE,gBAAqB,EAC7B,MAAM,CAAC,EAAE,UAAU,GAClB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAMlC"}
package/dist/serialize.js CHANGED
@@ -1,5 +1,17 @@
1
1
  import { Relator, Serializer } from 'ts-japi';
2
2
  import { JSON_API_VERSION } from './media-type.js';
3
+ /**
4
+ * Refuses a row whose id is not a string at runtime. The type is the defence that matters: on a create this fires
5
+ * after the write committed, so it only stops a wrong document from going out.
6
+ *
7
+ * @throws TypeError naming the resource type
8
+ */
9
+ export function assertRowId(type, row) {
10
+ const id = row?.id;
11
+ if (typeof id !== 'string') {
12
+ throw new TypeError(`A "${type}" row carries a ${typeof id} id; a JSON:API id is a string.`);
13
+ }
14
+ }
3
15
  /**
4
16
  * Whether the static projection publishes `name`, read so that a `0` counts as hidden under either mode.
5
17
  *
@@ -54,7 +66,11 @@ export function createSerializer(type, options, requested) {
54
66
  }),
55
67
  });
56
68
  }
57
- /** Raises the `jsonapi` member to {@link JSON_API_VERSION}, keeping whatever else ts-japi put there. */
69
+ /**
70
+ * Raises the `jsonapi` member to {@link JSON_API_VERSION}, keeping whatever else ts-japi put there.
71
+ *
72
+ * Typed over a document, not a {@link Row}: a document carries no `id` of its own, unlike the row serialized into it.
73
+ */
58
74
  function stamped(doc) {
59
75
  const existing = (doc.jsonapi ?? {});
60
76
  return { ...doc, jsonapi: { ...existing, version: JSON_API_VERSION } };
@@ -74,6 +90,19 @@ function includedAsLoaded(document, loaded) {
74
90
  included: document.included.filter((resource) => loaded.get(resource.type)?.has(resource.id) ?? true),
75
91
  };
76
92
  }
93
+ /** The `type` and `id` pair a resource object is identified by. */
94
+ const identity = (resource) => JSON.stringify([resource.type, resource.id]);
95
+ /**
96
+ * Drops from `included` every resource already present as primary data. A compound document carries one resource
97
+ * object per type and id, and a client indexing both members by that pair reads the second as a conflict.
98
+ */
99
+ function withoutPrimary(document) {
100
+ if (document.included === undefined || document.data == null)
101
+ return document;
102
+ const data = Array.isArray(document.data) ? document.data : [document.data];
103
+ const primary = new Set(data.map(identity));
104
+ return { ...document, included: document.included.filter((resource) => !primary.has(identity(resource))) };
105
+ }
77
106
  /**
78
107
  * Serializes one row into a single-resource document.
79
108
  *
@@ -81,12 +110,12 @@ function includedAsLoaded(document, loaded) {
81
110
  * @param loaded - what this document's relationship loaders answered, which decides its `included`
82
111
  */
83
112
  export async function serializeResource(serializer, data, loaded) {
84
- return stamped(includedAsLoaded(await serializer.serialize(data), loaded));
113
+ return stamped(withoutPrimary(includedAsLoaded(await serializer.serialize(data), loaded)));
85
114
  }
86
115
  /** Serializes a list of rows into a collection document, merging in the pagination `links` and `meta`. */
87
116
  export async function serializeCollection(serializer, items, extras = {}, loaded) {
88
117
  return {
89
- ...stamped(includedAsLoaded(await serializer.serialize(items), loaded)),
118
+ ...stamped(withoutPrimary(includedAsLoaded(await serializer.serialize(items), loaded))),
90
119
  ...(extras.links ? { links: extras.links } : {}),
91
120
  ...(extras.meta ? { meta: extras.meta } : {}),
92
121
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroxsolutions/response",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,