@zeroxsolutions/response 0.12.2 → 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
+ }
@@ -0,0 +1,58 @@
1
+ import type { ZodObject } from 'zod';
2
+ import type { RelatedResource } from './relationship.js';
3
+ /** Counts a depth down by one; index 0 is where a walk stops. */
4
+ type Down = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8];
5
+ type NoMembers = Record<never, never>;
6
+ /**
7
+ * A resource's relationship map. One declaring none carries `Record<string, never>`, whose keys are every string, so
8
+ * it is read as having no keys at all.
9
+ */
10
+ type RelationshipsOf<R> = R extends {
11
+ readonly relationships: infer M;
12
+ } ? [M[keyof M]] extends [never] ? NoMembers : M : NoMembers;
13
+ type RelatedOf<D> = D extends {
14
+ readonly related: () => infer X;
15
+ } ? X : never;
16
+ type TypeOf<X> = X extends {
17
+ readonly type: infer T extends string;
18
+ } ? T : never;
19
+ type AttributesOf<X> = X extends {
20
+ readonly attributes: ZodObject<infer S>;
21
+ } ? S : NoMembers;
22
+ /** True where an edge was declared against the wide `RelatedResource`, which knows no type name. */
23
+ type IsWide<X> = string extends TypeOf<X> ? true : false;
24
+ /**
25
+ * Every dotted relationship path from `R`, at most `Depth` relationships long. A path never revisits a type,
26
+ * which is the rule `declaredDocumentShape` walks by, and a wide edge contributes its own name and nothing below.
27
+ *
28
+ * It names a resource's relationship paths, not the drizzle relation paths `@zeroxsolutions/db` exports under the same
29
+ * name `IncludePath`; a module importing both must alias one.
30
+ */
31
+ export type IncludePath<R, Depth extends number = 3, Seen extends string = TypeOf<R>> = Depth extends 0 ? never : {
32
+ [K in keyof RelationshipsOf<R> & string]: RelatedOf<RelationshipsOf<R>[K]> extends infer X ? IsWide<X> extends true ? K : TypeOf<X> extends Seen ? never : K | `${K}.${IncludePath<X, Down[Depth], Seen | TypeOf<X>>}` : never;
33
+ }[keyof RelationshipsOf<R> & string];
34
+ /** The resources those paths reach, `R` first. A wide edge reaches nothing a type could name. */
35
+ export type ReachableResource<R, Depth extends number = 3, Seen extends string = TypeOf<R>> = R | (Depth extends 0 ? never : {
36
+ [K in keyof RelationshipsOf<R> & string]: RelatedOf<RelationshipsOf<R>[K]> extends infer X ? IsWide<X> extends true ? never : TypeOf<X> extends Seen ? never : ReachableResource<X, Down[Depth], Seen | TypeOf<X>> : never;
37
+ }[keyof RelationshipsOf<R> & string]);
38
+ /** The type names a document rooted at `R` can carry. */
39
+ export type ReachableType<R, Depth extends number = 3> = TypeOf<ReachableResource<R, Depth>>;
40
+ /** What a fieldset of `X` may name: its attributes and its relationships, which the standard calls its fields. */
41
+ export type FieldName<X> = (keyof AttributesOf<X> | keyof RelationshipsOf<X>) & string;
42
+ /** A sparse fieldset per reachable type. */
43
+ export type Fieldsets<R, Depth extends number = 3> = {
44
+ [X in ReachableResource<R, Depth> as TypeOf<X>]?: readonly FieldName<X>[];
45
+ };
46
+ /** What `declaredDocumentShape` answers, typed off the same graph. */
47
+ export interface TypedDocumentShape<R, Depth extends number> {
48
+ /** Every reachable type with the fields it publishes: its attribute names, then its relationship names. */
49
+ fields: {
50
+ [X in ReachableResource<R, Depth> as TypeOf<X>]: FieldName<X>[];
51
+ };
52
+ /** Every relationship path from the root, dotted for a nested one. */
53
+ include: IncludePath<R, Depth>[];
54
+ }
55
+ /** A resource as far as the graph types care; `RelatedResource` already is one. */
56
+ export type GraphRoot = RelatedResource<never>;
57
+ export {};
58
+ //# sourceMappingURL=document-graph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-graph.d.ts","sourceRoot":"","sources":["../src/document-graph.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -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,17 +1,19 @@
1
- import { type RelatedResource } from './relationship.js';
2
- /** The fieldsets and relationship paths one resource declaration can answer. */
3
- export interface DeclaredDocumentShape {
4
- /** Every reachable type with the fields it publishes: its attribute names, then its relationship names. */
5
- fields: Record<string, string[]>;
6
- /** Every relationship path from the root, dotted for a nested one. */
7
- include: string[];
8
- }
1
+ import type { GraphRoot, TypedDocumentShape } from './document-graph.js';
9
2
  /**
10
- * What a client may ask a document to be shaped as, read off the declaration itself - the permit-list a query guard
3
+ * What a client may ask a document to be shaped as, read off the declaration itself - the permit list a query guard
11
4
  * needs, without a second copy of it.
12
5
  *
13
6
  * The attribute keys ARE the projection, so a list written by hand beside them permits a column the resource stopped
14
7
  * publishing, or refuses one it started, with both halves compiling and both suites green.
8
+ *
9
+ * `maxIncludeDepth` bounds a path in relationships, 3 by default: the depth a route's own relation load reaches. A
10
+ * fieldset is answered only for a type some kept path reaches, the root included.
11
+ *
12
+ * The answer's type is bounded by `maxIncludeDepth` only where it is a literal; a `number` variable leaves the type
13
+ * at its default depth of 3. Below an edge whose thunk is annotated as the wide `RelatedResource`, the walk still
14
+ * descends while the type stops at that edge, so the value can permit paths and types the type does not offer.
15
15
  */
16
- export declare function declaredDocumentShape<C>(root: RelatedResource<C>): DeclaredDocumentShape;
16
+ export declare function declaredDocumentShape<R extends GraphRoot, const D extends number = 3>(root: R, options?: {
17
+ maxIncludeDepth?: D;
18
+ }): TypedDocumentShape<R, D>;
17
19
  //# sourceMappingURL=document-shape.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"document-shape.d.ts","sourceRoot":"","sources":["../src/document-shape.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAE7E,gFAAgF;AAChF,MAAM,WAAW,qBAAqB;IACpC,2GAA2G;IAC3G,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACjC,sEAAsE;IACtE,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,qBAAqB,CAuBxF"}
1
+ {"version":3,"file":"document-shape.d.ts","sourceRoot":"","sources":["../src/document-shape.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAGzE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,SAAS,EAAE,KAAK,CAAC,CAAC,SAAS,MAAM,GAAG,CAAC,EACnF,IAAI,EAAE,CAAC,EACP,OAAO,GAAE;IAAE,eAAe,CAAC,EAAE,CAAC,CAAA;CAAO,GACpC,kBAAkB,CAAC,CAAC,EAAE,CAAC,CAAC,CAyB1B"}
@@ -1,29 +1,40 @@
1
- import { reachableResources } from './relationship.js';
2
1
  /**
3
- * What a client may ask a document to be shaped as, read off the declaration itself - the permit-list a query guard
2
+ * What a client may ask a document to be shaped as, read off the declaration itself - the permit list a query guard
4
3
  * needs, without a second copy of it.
5
4
  *
6
5
  * The attribute keys ARE the projection, so a list written by hand beside them permits a column the resource stopped
7
6
  * publishing, or refuses one it started, with both halves compiling and both suites green.
7
+ *
8
+ * `maxIncludeDepth` bounds a path in relationships, 3 by default: the depth a route's own relation load reaches. A
9
+ * fieldset is answered only for a type some kept path reaches, the root included.
10
+ *
11
+ * The answer's type is bounded by `maxIncludeDepth` only where it is a literal; a `number` variable leaves the type
12
+ * at its default depth of 3. Below an edge whose thunk is annotated as the wide `RelatedResource`, the walk still
13
+ * descends while the type stops at that edge, so the value can permit paths and types the type does not offer.
8
14
  */
9
- export function declaredDocumentShape(root) {
10
- const fields = Object.fromEntries(reachableResources(root).map((resource) => [
11
- resource.type,
12
- [...Object.keys(resource.attributes.shape), ...Object.keys(resource.relationships)],
13
- ]));
15
+ export function declaredDocumentShape(root, options = {}) {
16
+ const maxDepth = options.maxIncludeDepth ?? 3;
17
+ const reached = new Map([[root.type, root]]);
14
18
  const include = [];
15
19
  // A path never revisits a type: two resources naming each other make `class.subject.class...` infinite, and the
16
20
  // second lap addresses the same rows the first one already compounded.
17
- const walk = (resource, prefix, seen) => {
21
+ const walk = (resource, prefix, seen, depth) => {
22
+ if (depth >= maxDepth)
23
+ return;
18
24
  for (const [name, def] of Object.entries(resource.relationships)) {
19
25
  const related = def.related();
20
26
  if (seen.has(related.type))
21
27
  continue;
22
28
  const path = prefix === '' ? name : `${prefix}.${name}`;
23
29
  include.push(path);
24
- walk(related, path, new Set([...seen, related.type]));
30
+ reached.set(related.type, related);
31
+ walk(related, path, new Set([...seen, related.type]), depth + 1);
25
32
  }
26
33
  };
27
- walk(root, '', new Set([root.type]));
34
+ walk(root, '', new Set([root.type]), 0);
35
+ const fields = Object.fromEntries([...reached.values()].map((resource) => [
36
+ resource.type,
37
+ [...Object.keys(resource.attributes.shape), ...Object.keys(resource.relationships)],
38
+ ]));
28
39
  return { fields, include };
29
40
  }
@@ -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,13 +2,15 @@
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
- export { declaredDocumentShape, type DeclaredDocumentShape } from './document-shape.js';
7
+ export * from './document-graph.js';
8
+ export { declaredDocumentShape } from './document-shape.js';
7
9
  export { createDomainErrorResolver, defineErrorMap, errorEntry, type DomainErrorAnswer, type DomainErrorEntry, } from './domain-error-resolver.js';
8
10
  export type { DomainErrorMap, ErrorClass, UpstreamErrorMap } from './error-map.js';
9
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';
10
12
  export { JSON_API_MEDIA_TYPE } from './media-type.js';
11
- 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';
12
14
  export { toMany, toOne, type LoadContextArguments, type RelatedResource, type RelationshipDef, type RelationshipLoader, type RelationshipOptions, } from './relationship.js';
13
15
  export { jsonApiResource, type JsonApiResource, type ResourceOptions } from './resource.js';
14
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,OAAO,EAAE,qBAAqB,EAAE,KAAK,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACxF,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,8 @@
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';
6
+ export * from './document-graph.js';
5
7
  export { declaredDocumentShape } from './document-shape.js';
6
8
  export { createDomainErrorResolver, defineErrorMap, errorEntry, } from './domain-error-resolver.js';
7
9
  export { BASE_ERROR_CODES, errorDocumentSchema, JsonApiError, toErrorDocument, toErrorObject, } from './errors/index.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
+ }