@zeroxsolutions/response 0.12.0 → 0.12.2

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,27 @@
1
+ ## 0.12.2 (2026-09-27)
2
+
3
+ ### 🚀 Features
4
+
5
+ - **response:** declare a refusal's Retry-After off errorResponse ([53cc8d1](https://github.com/zeroxsolutions/zeroxsolutions/commit/53cc8d1))
6
+ - **response:** add the status a refused burst answers with ([910c52d](https://github.com/zeroxsolutions/zeroxsolutions/commit/910c52d))
7
+
8
+ ### ❤️ Thank You
9
+
10
+ - Claude Opus 5 (1M context)
11
+ - Claude Opus 5.5 (1M context)
12
+ - Lương Văn Tú
13
+
14
+ ## 0.12.1 (2026-09-18)
15
+
16
+ ### 🚀 Features
17
+
18
+ - **response:** load a relationship from the service that holds it ([98cef38](https://github.com/zeroxsolutions/zeroxsolutions/commit/98cef38))
19
+
20
+ ### ❤️ Thank You
21
+
22
+ - Claude Opus 5 (1M context)
23
+ - Lương Văn Tú
24
+
1
25
  ## 0.12.0 (2026-09-13)
2
26
 
3
27
  ### 💅 Refactors
package/README.md CHANGED
@@ -41,6 +41,50 @@ stopped publishing, with both halves compiling and both suites green.
41
41
  const { fields, include } = declaredDocumentShape(classes); // feeds `@zeroxsolutions/query`'s permits
42
42
  ```
43
43
 
44
+ ## A relationship another service holds
45
+
46
+ Where the related rows are not on the row this handler loaded, the relationship declares how to load
47
+ them, and the serializer calls that loader when a request includes it. Nothing composes in the route.
48
+
49
+ ```ts
50
+ export const trips = jsonApiResource('trips', tripAttributesSchema, {
51
+ relationships: {
52
+ place: toOne(
53
+ () => places, // the resource the loaded rows are published as
54
+ (row) => row.placeId, // beside a loader this reads the ID off the row, not the row
55
+ { load: (ids, context: TripDocumentContext) => context.catalogue.places.byIds(ids) },
56
+ ),
57
+ },
58
+ });
59
+
60
+ // the serialize call carries what the loaders may call, and the compiler asks for it
61
+ return c.json(await trips.serializeMany(rows, { links }, query, { catalogue: c.var.catalogue }));
62
+ ```
63
+
64
+ The loader is called ONCE per document, with every id the page carries for that relationship,
65
+ deduplicated and in no order, so a page of fifty trips costs one call upstream rather than fifty. It is
66
+ called only where the request's `include` names the relationship: a document that includes nothing makes
67
+ no call at all, and still publishes the identifier, which is the key its own row carried.
68
+
69
+ | The loader | What the document carries |
70
+ | --- | --- |
71
+ | answers a row for an id | that identifier, and the row under `included` |
72
+ | answers no row for an id it was given | that identifier, and nothing under `included` |
73
+ | throws | nothing; the failure comes out of `serialize` for the route to answer |
74
+
75
+ An id with no row back is how a row this caller may not see stays out of the document. Dropping the
76
+ identifier with it would tell the client the trip has no place, which the row it was read off does not
77
+ say, so including a relationship never changes the `data` a client reads. A loader's failure is left
78
+ alone because the loader is the route's own call on another service, and the upstream error map below
79
+ is what turns it into an answer.
80
+
81
+ The context is how a loader reaches a client bound to THIS request. A resource is declared once at
82
+ module scope and can close over nothing request-shaped, so the serialize call hands it over, and its
83
+ type is inferred from the loaders the declaration reaches: a resource that reaches one does not compile
84
+ without it, and a resource that reaches none takes no such argument. One document therefore loads under
85
+ one context type, including through a relationship whose own rows came off the row above it
86
+ (`include=days.place` loads every day's place in one call).
87
+
44
88
  ## Errors
45
89
 
46
90
  The domain raises a plain `Error` subclass carrying only domain data; the transport decides what it
@@ -4,7 +4,8 @@ import { errorDocumentSchema } from './errors/index.js';
4
4
  import { JSON_API_MEDIA_TYPE } from './media-type.js';
5
5
  import type { JsonApiResource } from './resource.js';
6
6
  /**
7
- * An OpenAPI response object for this spelling - one media type, one schema.
7
+ * An OpenAPI response object for this spelling - one media type, one schema, and the `Retry-After` a
8
+ * refusal that tells its caller how long to wait declares.
8
9
  *
9
10
  * Exported because a consumer emitting declarations has to NAME it: a route built from these helpers
10
11
  * is an exported const whose type mentions it, and a private one fails that emit with TS4023.
@@ -14,11 +15,36 @@ export interface JsonApiResponse<S extends ZodType> {
14
15
  content: Record<typeof JSON_API_MEDIA_TYPE, {
15
16
  schema: S;
16
17
  }>;
18
+ headers?: {
19
+ 'Retry-After': RetryAfterHeader;
20
+ };
17
21
  }
18
- /** The response object for a failure status - the `errors[]` document. */
19
- export declare function errorResponse(description: string): JsonApiResponse<typeof errorDocumentSchema>;
22
+ /**
23
+ * The `Retry-After` header object a refusal declares: whole seconds, sent on every answer it gives.
24
+ *
25
+ * A type alias, not an interface: `@hono/zod-openapi` types a header as openapi3-ts's `HeaderObject`,
26
+ * which carries an `x-${string}` index signature, and an interface is not assignable to one.
27
+ */
28
+ export type RetryAfterHeader = {
29
+ description: string;
30
+ required: true;
31
+ schema: {
32
+ type: 'integer';
33
+ minimum: 1;
34
+ };
35
+ };
36
+ /**
37
+ * The response object for a failure status - the `errors[]` document.
38
+ *
39
+ * @param options.retryAfterSeconds - how long a refused caller is told to wait, in whole seconds.
40
+ * Pass it only where the refusal always sets `Retry-After`, since the header is declared required.
41
+ * @throws {RangeError} when `retryAfterSeconds` is not a positive integer.
42
+ */
43
+ export declare function errorResponse(description: string, options?: {
44
+ retryAfterSeconds: number;
45
+ }): JsonApiResponse<typeof errorDocumentSchema>;
20
46
  /** The response object for a single resource, off the resource's own declaration. */
21
- export declare function okResponse<A extends ZodObject>(description: string, resource: JsonApiResource<A>): JsonApiResponse<ZodType<ResourceDocument<output<A>>>>;
47
+ export declare function okResponse<A extends ZodObject, C = unknown>(description: string, resource: JsonApiResource<A, C>): JsonApiResponse<ZodType<ResourceDocument<output<A>>>>;
22
48
  /** The response object for a collection, off the resource's own declaration. */
23
- export declare function listResponse<A extends ZodObject>(description: string, resource: JsonApiResource<A>): JsonApiResponse<ZodType<CollectionDocument<output<A>>>>;
49
+ export declare function listResponse<A extends ZodObject, C = unknown>(description: string, resource: JsonApiResource<A, C>): JsonApiResponse<ZodType<CollectionDocument<output<A>>>>;
24
50
  //# sourceMappingURL=api-response.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"api-response.d.ts","sourceRoot":"","sources":["../src/api-response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AACtD,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD;;;;;GAKG;AACH,MAAM,WAAW,eAAe,CAAC,CAAC,SAAS,OAAO;IAChD,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC,OAAO,mBAAmB,EAAE;QAAE,MAAM,EAAE,CAAC,CAAA;KAAE,CAAC,CAAC;CAC5D;AAOD,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,eAAe,CAAC,OAAO,mBAAmB,CAAC,CAE9F;AAED,qFAAqF;AACrF,wBAAgB,UAAU,CAAC,CAAC,SAAS,SAAS,EAC5C,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,eAAe,CAAC,CAAC,CAAC,GAC3B,eAAe,CAAC,OAAO,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAEvD;AAED,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,CAAC,SAAS,SAAS,EAC9C,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,eAAe,CAAC,CAAC,CAAC,GAC3B,eAAe,CAAC,OAAO,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAEzD"}
1
+ {"version":3,"file":"api-response.d.ts","sourceRoot":"","sources":["../src/api-response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AACtD,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD;;;;;;GAMG;AACH,MAAM,WAAW,eAAe,CAAC,CAAC,SAAS,OAAO;IAChD,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC,OAAO,mBAAmB,EAAE;QAAE,MAAM,EAAE,CAAC,CAAA;KAAE,CAAC,CAAC;IAC3D,OAAO,CAAC,EAAE;QAAE,aAAa,EAAE,gBAAgB,CAAA;KAAE,CAAC;CAC/C;AAED;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,IAAI,CAAC;IACf,MAAM,EAAE;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,OAAO,EAAE,CAAC,CAAA;KAAE,CAAC;CACzC,CAAC;AAOF;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE;IAAE,iBAAiB,EAAE,MAAM,CAAA;CAAE,GACtC,eAAe,CAAC,OAAO,mBAAmB,CAAC,CAqB7C;AAED,qFAAqF;AACrF,wBAAgB,UAAU,CAAC,CAAC,SAAS,SAAS,EAAE,CAAC,GAAG,OAAO,EACzD,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,GAC9B,eAAe,CAAC,OAAO,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAEvD;AAED,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,CAAC,SAAS,SAAS,EAAE,CAAC,GAAG,OAAO,EAC3D,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,GAC9B,eAAe,CAAC,OAAO,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAEzD"}
@@ -4,9 +4,32 @@ const answers = (description, schema) => ({
4
4
  description,
5
5
  content: { [JSON_API_MEDIA_TYPE]: { schema } },
6
6
  });
7
- /** The response object for a failure status - the `errors[]` document. */
8
- export function errorResponse(description) {
9
- return answers(description, errorDocumentSchema);
7
+ /**
8
+ * The response object for a failure status - the `errors[]` document.
9
+ *
10
+ * @param options.retryAfterSeconds - how long a refused caller is told to wait, in whole seconds.
11
+ * Pass it only where the refusal always sets `Retry-After`, since the header is declared required.
12
+ * @throws {RangeError} when `retryAfterSeconds` is not a positive integer.
13
+ */
14
+ export function errorResponse(description, options) {
15
+ const response = answers(description, errorDocumentSchema);
16
+ if (!options) {
17
+ return response;
18
+ }
19
+ const { retryAfterSeconds } = options;
20
+ if (!Number.isInteger(retryAfterSeconds) || retryAfterSeconds < 1) {
21
+ throw new RangeError(`retryAfterSeconds must be a positive integer, got ${retryAfterSeconds}`);
22
+ }
23
+ return {
24
+ ...response,
25
+ headers: {
26
+ 'Retry-After': {
27
+ description: `Seconds to wait before calling again: ${retryAfterSeconds}`,
28
+ required: true,
29
+ schema: { type: 'integer', minimum: 1 },
30
+ },
31
+ },
32
+ };
10
33
  }
11
34
  /** The response object for a single resource, off the resource's own declaration. */
12
35
  export function okResponse(description, resource) {
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The pass that fills a document's relationships from the services that hold them, before it is
3
+ * serialized. What a relationship's rows look like once here belongs to `serialize.ts`; what a
4
+ * relationship declares belongs to `relationship.ts`.
5
+ */
6
+ import type { RelatedResource, RelationshipDef } from './relationship.js';
7
+ import type { LoadedRows, RequestedDocumentShape, Row } from './serialize.js';
8
+ /**
9
+ * What one loaded relationship hands the serializer for a row: the row its loader answered, or the
10
+ * identifier alone where it answered none.
11
+ *
12
+ * The identifier is read off the primary row either way, so including a relationship never changes the
13
+ * linkage a client reads - only what lands under `included`.
14
+ */
15
+ export declare function loadedRelated<C>(def: RelationshipDef<C>, row: Row, loaded: LoadedRows | undefined): Row | Row[] | null;
16
+ /**
17
+ * Runs every loader the request's `include` reaches, one call per relationship, with the whole page's
18
+ * ids.
19
+ *
20
+ * A relationship the request did not include is never loaded, so a document that asks for none makes no
21
+ * call at all and answers `undefined`. Two include paths through one relationship (`place` and
22
+ * `place.region`) still load it once. A loader's failure is not caught here.
23
+ *
24
+ * @param rows - the primary rows of the document, which is the whole page on a collection
25
+ */
26
+ export declare function loadIncludedRows<C>(root: RelatedResource<C>, rows: readonly Row[], requested: RequestedDocumentShape | undefined, context: C): Promise<LoadedRows | undefined>;
27
+ //# sourceMappingURL=document-load.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,80 @@
1
+ /** The ids one row carries for a loaded relationship. A null or absent key asks for nothing. */
2
+ function readIds(value) {
3
+ const values = Array.isArray(value) ? value : [value];
4
+ return values.filter((one) => typeof one === 'string');
5
+ }
6
+ /** The rows one row already carries for a relationship that declares no loader. */
7
+ function readRows(value) {
8
+ const values = Array.isArray(value) ? value : [value];
9
+ return values.filter((one) => typeof one === 'object' && one !== null);
10
+ }
11
+ /**
12
+ * What one loaded relationship hands the serializer for a row: the row its loader answered, or the
13
+ * identifier alone where it answered none.
14
+ *
15
+ * The identifier is read off the primary row either way, so including a relationship never changes the
16
+ * linkage a client reads - only what lands under `included`.
17
+ */
18
+ 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 });
21
+ return def.cardinality === 'many' ? rows : (rows.at(0) ?? null);
22
+ }
23
+ /** Runs one relationship's loader, and answers the rows the next depth reads its ids off. */
24
+ async function loadRelated(def, rows, loaded, context) {
25
+ const { load } = def;
26
+ if (load === undefined)
27
+ return rows.flatMap((row) => readRows(def.read(row)));
28
+ // The type is registered before the call, so a loader that answers nothing still means "these are all of them".
29
+ const type = def.related().type;
30
+ const answered = loaded.get(type) ?? new Map();
31
+ loaded.set(type, answered);
32
+ const ids = [...new Set(rows.flatMap((row) => readIds(def.read(row))))];
33
+ if (ids.length === 0)
34
+ return [];
35
+ const related = await load(ids, context);
36
+ for (const row of related) {
37
+ if (typeof row.id === 'string')
38
+ answered.set(row.id, row);
39
+ }
40
+ return related;
41
+ }
42
+ /**
43
+ * Runs every loader the request's `include` reaches, one call per relationship, with the whole page's
44
+ * ids.
45
+ *
46
+ * A relationship the request did not include is never loaded, so a document that asks for none makes no
47
+ * call at all and answers `undefined`. Two include paths through one relationship (`place` and
48
+ * `place.region`) still load it once. A loader's failure is not caught here.
49
+ *
50
+ * @param rows - the primary rows of the document, which is the whole page on a collection
51
+ */
52
+ export async function loadIncludedRows(root, rows, requested, context) {
53
+ const paths = requested?.include?.map((path) => path.split('.')) ?? [];
54
+ if (paths.length === 0)
55
+ return undefined;
56
+ const loaded = new Map();
57
+ let depth = new Map([['', { resource: root, rows }]]);
58
+ for (let index = 0; index < Math.max(...paths.map((segments) => segments.length)); index += 1) {
59
+ const reached = new Map();
60
+ for (const segments of paths) {
61
+ const name = segments.at(index);
62
+ const path = segments.slice(0, index + 1).join('.');
63
+ if (name === undefined || reached.has(path))
64
+ continue;
65
+ const parent = depth.get(segments.slice(0, index).join('.'));
66
+ const def = parent?.resource.relationships[name];
67
+ // An include path naming no declared relationship is the query guard's refusal, not this pass's.
68
+ if (parent === undefined || def === undefined)
69
+ continue;
70
+ reached.set(path, { parent, def });
71
+ }
72
+ if (reached.size === 0)
73
+ break;
74
+ // The relationships reached at one depth are separate services, so their loads run together.
75
+ const entries = [...reached];
76
+ const walked = await Promise.all(entries.map(([, { parent, def }]) => loadRelated(def, parent.rows, loaded, context)));
77
+ depth = new Map(entries.map(([path, { def }], at) => [path, { resource: def.related(), rows: walked[at] ?? [] }]));
78
+ }
79
+ return loaded.size === 0 ? undefined : loaded;
80
+ }
@@ -13,5 +13,5 @@ export interface DeclaredDocumentShape {
13
13
  * The attribute keys ARE the projection, so a list written by hand beside them permits a column the resource stopped
14
14
  * publishing, or refuses one it started, with both halves compiling and both suites green.
15
15
  */
16
- export declare function declaredDocumentShape(root: RelatedResource): DeclaredDocumentShape;
16
+ export declare function declaredDocumentShape<C>(root: RelatedResource<C>): DeclaredDocumentShape;
17
17
  //# 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,IAAI,EAAE,eAAe,GAAG,qBAAqB,CAuBlF"}
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"}
@@ -33,7 +33,7 @@ export interface CollectionDocument<A> {
33
33
  jsonapi?: JsonApiObject;
34
34
  }
35
35
  /** Wraps `attributes` into a resource object - `{ type, id, attributes, relationships?, links?, meta? }`. */
36
- export declare function resourceObjectSchema(type: string, attributes: ZodObject, relationships?: Readonly<Record<string, RelationshipDef>>): ZodType;
36
+ export declare function resourceObjectSchema<C>(type: string, attributes: ZodObject, relationships?: Readonly<Record<string, RelationshipDef<C>>>): ZodType;
37
37
  /**
38
38
  * The document for a SINGLE resource - `{ data, included?, links?, meta?, jsonapi? }`.
39
39
  *
@@ -41,7 +41,7 @@ export declare function resourceObjectSchema(type: string, attributes: ZodObject
41
41
  * through thunks, so nothing static carries the attribute type, and a client typing a route off this
42
42
  * schema would otherwise read every member as `unknown`.
43
43
  */
44
- export declare function resourceDocumentSchema<A = unknown>(root: RelatedResource): ZodType<ResourceDocument<A>>;
44
+ export declare function resourceDocumentSchema<A = unknown, C = unknown>(root: RelatedResource<C>): ZodType<ResourceDocument<A>>;
45
45
  /** The document for a COLLECTION - the same members, with `data` an array and paginated `links`. */
46
- export declare function collectionDocumentSchema<A = unknown>(root: RelatedResource): ZodType<CollectionDocument<A>>;
46
+ export declare function collectionDocumentSchema<A = unknown, C = unknown>(root: RelatedResource<C>): ZodType<CollectionDocument<A>>;
47
47
  //# 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,CAClC,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,SAAS,EACrB,aAAa,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAM,GAC5D,OAAO,CAUT;AAgBD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAQvG;AAED,oGAAoG;AACpG,wBAAgB,wBAAwB,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAQ3G"}
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"}
@@ -37,6 +37,10 @@ export declare const BASE_ERROR_CODES: {
37
37
  readonly status: 422;
38
38
  readonly title: "Unprocessable Entity";
39
39
  };
40
+ readonly TOO_MANY_REQUESTS: {
41
+ readonly status: 429;
42
+ readonly title: "Too Many Requests";
43
+ };
40
44
  readonly INTERNAL_SERVER_ERROR: {
41
45
  readonly status: 500;
42
46
  readonly title: "Internal Server Error";
@@ -1 +1 @@
1
- {"version":3,"file":"error-registry.d.ts","sourceRoot":"","sources":["../../src/errors/error-registry.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED,sHAAsH;AACtH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWmB,CAAC"}
1
+ {"version":3,"file":"error-registry.d.ts","sourceRoot":"","sources":["../../src/errors/error-registry.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED,sHAAsH;AACtH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAYmB,CAAC"}
@@ -8,6 +8,7 @@ export const BASE_ERROR_CODES = {
8
8
  CONFLICT: { status: 409, title: 'Conflict' },
9
9
  UNSUPPORTED_MEDIA_TYPE: { status: 415, title: 'Unsupported Media Type' },
10
10
  UNPROCESSABLE_ENTITY: { status: 422, title: 'Unprocessable Entity' },
11
+ TOO_MANY_REQUESTS: { status: 429, title: 'Too Many Requests' },
11
12
  INTERNAL_SERVER_ERROR: { status: 500, title: 'Internal Server Error' },
12
13
  BAD_GATEWAY: { status: 502, title: 'Bad Gateway' },
13
14
  };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The response half of the HTTP contract, in the one wire spelling this house answers: a JSON:API document.
3
3
  */
4
- export { errorResponse, listResponse, okResponse, type JsonApiResponse } from './api-response.js';
4
+ export { errorResponse, listResponse, okResponse, type JsonApiResponse, type RetryAfterHeader, } from './api-response.js';
5
5
  export type { CollectionDocument, Relationship, ResourceDocument, ResourceObject } from './document.js';
6
6
  export { declaredDocumentShape, type DeclaredDocumentShape } from './document-shape.js';
7
7
  export { createDomainErrorResolver, defineErrorMap, errorEntry, type DomainErrorAnswer, type DomainErrorEntry, } from './domain-error-resolver.js';
@@ -9,8 +9,8 @@ export type { DomainErrorMap, ErrorClass, UpstreamErrorMap } from './error-map.j
9
9
  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
10
  export { JSON_API_MEDIA_TYPE } from './media-type.js';
11
11
  export type { JsonApiObject, Link, Meta, PaginationLinks, ResourceIdentifier, ResourceLinks } from './primitives.js';
12
- export { toMany, toOne, type RelatedResource, type RelationshipDef } from './relationship.js';
12
+ export { toMany, toOne, type LoadContextArguments, type RelatedResource, type RelationshipDef, type RelationshipLoader, type RelationshipOptions, } from './relationship.js';
13
13
  export { jsonApiResource, type JsonApiResource, type ResourceOptions } from './resource.js';
14
- export type { CollectionExtras, RequestedDocumentShape, ResourceSerializer, Row } from './serialize.js';
14
+ export type { CollectionExtras, LoadedRows, RequestedDocumentShape, ResourceSerializer, Row } from './serialize.js';
15
15
  export { createUpstreamErrorTranslator, type UpstreamErrorAnswer } from './upstream-error.js';
16
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAClG,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,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC9F,OAAO,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAC5F,YAAY,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AACxG,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,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"}
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * The response half of the HTTP contract, in the one wire spelling this house answers: a JSON:API document.
3
3
  */
4
- export { errorResponse, listResponse, okResponse } from './api-response.js';
4
+ export { errorResponse, listResponse, okResponse, } from './api-response.js';
5
5
  export { declaredDocumentShape } from './document-shape.js';
6
6
  export { createDomainErrorResolver, defineErrorMap, errorEntry, } from './domain-error-resolver.js';
7
7
  export { BASE_ERROR_CODES, errorDocumentSchema, JsonApiError, toErrorDocument, toErrorObject, } from './errors/index.js';
8
8
  export { JSON_API_MEDIA_TYPE } from './media-type.js';
9
- export { toMany, toOne } from './relationship.js';
9
+ export { toMany, toOne, } from './relationship.js';
10
10
  export { jsonApiResource } from './resource.js';
11
11
  export { createUpstreamErrorTranslator } from './upstream-error.js';
@@ -1,37 +1,72 @@
1
1
  import type { ZodObject, ZodType } from 'zod';
2
- import type { RequestedDocumentShape, ResourceSerializer, Row } from './serialize.js';
2
+ import type { LoadedRows, RequestedDocumentShape, ResourceSerializer, Row } from './serialize.js';
3
+ /**
4
+ * Loads the rows of one relationship whose service is not the one answering this document.
5
+ *
6
+ * It is called ONCE per document, with every id the page carries for that relationship, deduplicated
7
+ * and in no order, so a page of fifty rows costs one call and not fifty. It runs only where the
8
+ * request's `include` names the relationship.
9
+ *
10
+ * @param ids - the ids to read; never empty, and each one appears once
11
+ * @param context - what the serialize call was handed, which is how a load reaches THIS request's client
12
+ * @returns the rows, in any order; an id it answers no row for keeps its identifier and is not included
13
+ * @throws whatever the call upstream throws, which leaves `serialize` unchanged for the route to map
14
+ */
15
+ export interface RelationshipLoader<C> {
16
+ (ids: readonly string[], context: C): Promise<readonly Row[]>;
17
+ }
18
+ /** What a relationship declares beyond the resource it points at and how to read it off a row. */
19
+ export interface RelationshipOptions<C> {
20
+ /** Loads the related rows; declared where another service holds them, and left out where the row carries them. */
21
+ readonly load: RelationshipLoader<C>;
22
+ }
23
+ /**
24
+ * The context argument a serialize call takes, which is the argument list and not the context itself.
25
+ *
26
+ * A resource no relationship of which declares a loader takes none. One that reaches a loader takes
27
+ * that loader's own context, and the call does not compile without it.
28
+ */
29
+ export type LoadContextArguments<C> = unknown extends C ? [context?: undefined] : [context: C];
30
+ /** The context a serialize call was handed, for the load pass to give the loaders. */
31
+ export declare function loadContext<C>(contextArguments: LoadContextArguments<C>): C;
3
32
  /** What a relationship needs from the resource it points at, so this module never imports the descriptor. */
4
- export interface RelatedResource {
33
+ export interface RelatedResource<C = unknown> {
5
34
  readonly type: string;
6
- readonly relationships: Readonly<Record<string, RelationshipDef>>;
35
+ readonly relationships: Readonly<Record<string, RelationshipDef<C>>>;
7
36
  /** This type's declared attributes - the keys are the projection, so they are also what a fieldset may name. */
8
37
  readonly attributes: ZodObject;
9
38
  /** This type's resource object schema - what a document validates its `data` and `included` against. */
10
39
  readonly resourceObject: ZodType;
11
- /** This type's serializer under one request, which is what carries a sparse fieldset into `included`. */
12
- serializerFor(requested?: RequestedDocumentShape): ResourceSerializer;
40
+ /**
41
+ * This type's serializer under one request, which is what carries a sparse fieldset into `included`.
42
+ *
43
+ * @param loaded - one document's loaded rows; a serializer built over them serves that document alone
44
+ */
45
+ serializerFor(requested?: RequestedDocumentShape, loaded?: LoadedRows): ResourceSerializer;
13
46
  }
14
- /** A relationship: the resource it points at, how many, and how to read it off a row. */
15
- export interface RelationshipDef {
47
+ /** A relationship: the resource it points at, how many, how to read it off a row, and who loads it. */
48
+ export interface RelationshipDef<C = unknown> {
16
49
  readonly cardinality: 'one' | 'many';
17
50
  /** A thunk, so two resources may name each other without a module-evaluation cycle. */
18
- readonly related: () => RelatedResource;
19
- /** Reads the related row(s) off the primary row; what ts-japi's `Relator` fetches. */
51
+ readonly related: () => RelatedResource<C>;
52
+ /** Reads the related row(s) off the primary row, or the related id(s) where a loader is declared. */
20
53
  readonly read: (row: Row) => unknown;
54
+ /** Absent where the primary row already carries the related rows. */
55
+ readonly load?: RelationshipLoader<C>;
21
56
  }
22
57
  /** Declares a to-one relationship - its `data` is a single identifier, or null. */
23
- export declare function toOne(related: () => RelatedResource, read: (row: Row) => unknown): RelationshipDef;
58
+ export declare function toOne<C = unknown>(related: () => RelatedResource<C>, read: (row: Row) => unknown, options?: RelationshipOptions<C>): RelationshipDef<C>;
24
59
  /** Declares a to-many relationship - its `data` is an array of identifiers. */
25
- export declare function toMany(related: () => RelatedResource, read: (row: Row) => unknown): RelationshipDef;
60
+ export declare function toMany<C = unknown>(related: () => RelatedResource<C>, read: (row: Row) => unknown, options?: RelationshipOptions<C>): RelationshipDef<C>;
26
61
  /** The linkage schema for one relationship, pinned to the related `type` so a wrong type fails the parse. */
27
- export declare function relationshipSchema(def: RelationshipDef): ZodType;
62
+ export declare function relationshipSchema<C>(def: RelationshipDef<C>): ZodType;
28
63
  /** The `relationships` member for a whole resource - one key per declaration, absent when it declares none. */
29
- export declare function relationshipsSchema(relationships: Readonly<Record<string, RelationshipDef>>): ZodType | undefined;
64
+ export declare function relationshipsSchema<C>(relationships: Readonly<Record<string, RelationshipDef<C>>>): ZodType | undefined;
30
65
  /**
31
66
  * Every resource reachable from `root` through its relationships, `root` first, each visited once.
32
67
  *
33
68
  * This is what calls the thunks, so it runs on first use rather than at declaration - two resources naming
34
69
  * each other are both defined by then, which is the whole reason the declaration holds a thunk.
35
70
  */
36
- export declare function reachableResources(root: RelatedResource): RelatedResource[];
71
+ export declare function reachableResources<C>(root: RelatedResource<C>): RelatedResource<C>[];
37
72
  //# sourceMappingURL=relationship.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"relationship.d.ts","sourceRoot":"","sources":["../src/relationship.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAE9C,OAAO,KAAK,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAEtF,6GAA6G;AAC7G,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC;IAClE,gHAAgH;IAChH,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,wGAAwG;IACxG,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,yGAAyG;IACzG,aAAa,CAAC,SAAS,CAAC,EAAE,sBAAsB,GAAG,kBAAkB,CAAC;CACvE;AAED,yFAAyF;AACzF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,WAAW,EAAE,KAAK,GAAG,MAAM,CAAC;IACrC,uFAAuF;IACvF,QAAQ,CAAC,OAAO,EAAE,MAAM,eAAe,CAAC;IACxC,sFAAsF;IACtF,QAAQ,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC;CACtC;AAED,mFAAmF;AACnF,wBAAgB,KAAK,CAAC,OAAO,EAAE,MAAM,eAAe,EAAE,IAAI,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,GAAG,eAAe,CAElG;AAED,+EAA+E;AAC/E,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,eAAe,EAAE,IAAI,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,GAAG,eAAe,CAEnG;AAED,6GAA6G;AAC7G,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAOhE;AAED,+GAA+G;AAC/G,wBAAgB,mBAAmB,CACjC,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,GACvD,OAAO,GAAG,SAAS,CAIrB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,eAAe,GAAG,eAAe,EAAE,CAS3E"}
1
+ {"version":3,"file":"relationship.d.ts","sourceRoot":"","sources":["../src/relationship.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAE9C,OAAO,KAAK,EAAE,UAAU,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAElG;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,kBAAkB,CAAC,CAAC;IACnC,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,SAAS,GAAG,EAAE,CAAC,CAAC;CAC/D;AAED,kGAAkG;AAClG,MAAM,WAAW,mBAAmB,CAAC,CAAC;IACpC,kHAAkH;IAClH,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC,CAAC,CAAC;CACtC;AAED;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,CAAC,CAAC,IAAI,OAAO,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;AAE/F,sFAAsF;AACtF,wBAAgB,WAAW,CAAC,CAAC,EAAE,gBAAgB,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAAG,CAAC,CAG3E;AAED,6GAA6G;AAC7G,MAAM,WAAW,eAAe,CAAC,CAAC,GAAG,OAAO;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACrE,gHAAgH;IAChH,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,wGAAwG;IACxG,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;OAIG;IACH,aAAa,CAAC,SAAS,CAAC,EAAE,sBAAsB,EAAE,MAAM,CAAC,EAAE,UAAU,GAAG,kBAAkB,CAAC;CAC5F;AAED,uGAAuG;AACvG,MAAM,WAAW,eAAe,CAAC,CAAC,GAAG,OAAO;IAC1C,QAAQ,CAAC,WAAW,EAAE,KAAK,GAAG,MAAM,CAAC;IACrC,uFAAuF;IACvF,QAAQ,CAAC,OAAO,EAAE,MAAM,eAAe,CAAC,CAAC,CAAC,CAAC;IAC3C,qGAAqG;IACrG,QAAQ,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC;IACrC,qEAAqE;IACrE,QAAQ,CAAC,IAAI,CAAC,EAAE,kBAAkB,CAAC,CAAC,CAAC,CAAC;CACvC;AAED,mFAAmF;AACnF,wBAAgB,KAAK,CAAC,CAAC,GAAG,OAAO,EAC/B,OAAO,EAAE,MAAM,eAAe,CAAC,CAAC,CAAC,EACjC,IAAI,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,EAC3B,OAAO,CAAC,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAC/B,eAAe,CAAC,CAAC,CAAC,CAEpB;AAED,+EAA+E;AAC/E,wBAAgB,MAAM,CAAC,CAAC,GAAG,OAAO,EAChC,OAAO,EAAE,MAAM,eAAe,CAAC,CAAC,CAAC,EACjC,IAAI,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,EAC3B,OAAO,CAAC,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAC/B,eAAe,CAAC,CAAC,CAAC,CAEpB;AAED,6GAA6G;AAC7G,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,GAAG,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,OAAO,CAOtE;AAED,+GAA+G;AAC/G,wBAAgB,mBAAmB,CAAC,CAAC,EACnC,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,GAC1D,OAAO,GAAG,SAAS,CAIrB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,EAAE,CASpF"}
@@ -1,12 +1,17 @@
1
1
  import { z } from 'zod';
2
2
  import { metaSchema, resourceIdentifierSchema, resourceLinksSchema } from './primitives.js';
3
+ /** The context a serialize call was handed, for the load pass to give the loaders. */
4
+ export function loadContext(contextArguments) {
5
+ // A conditional tuple's element does not narrow back to `C`, and this is the one place that reads it.
6
+ return contextArguments[0];
7
+ }
3
8
  /** Declares a to-one relationship - its `data` is a single identifier, or null. */
4
- export function toOne(related, read) {
5
- return { cardinality: 'one', related, read };
9
+ export function toOne(related, read, options) {
10
+ return { cardinality: 'one', related, read, load: options?.load };
6
11
  }
7
12
  /** Declares a to-many relationship - its `data` is an array of identifiers. */
8
- export function toMany(related, read) {
9
- return { cardinality: 'many', related, read };
13
+ export function toMany(related, read, options) {
14
+ return { cardinality: 'many', related, read, load: options?.load };
10
15
  }
11
16
  /** The linkage schema for one relationship, pinned to the related `type` so a wrong type fails the parse. */
12
17
  export function relationshipSchema(def) {
@@ -1,11 +1,11 @@
1
1
  import type { output, ZodObject, ZodType } from 'zod';
2
2
  import { type CollectionDocument, type ResourceDocument } from './document.js';
3
- import type { RelatedResource, RelationshipDef } from './relationship.js';
3
+ import { type LoadContextArguments, type RelatedResource, type RelationshipDef } from './relationship.js';
4
4
  import { type CollectionExtras, type RequestedDocumentShape, type Row } from './serialize.js';
5
5
  /** Per-entity overrides for a resource declaration. */
6
- export interface ResourceOptions {
6
+ export interface ResourceOptions<C> {
7
7
  /** Relationships this resource declares, keyed by the member name they take in `relationships`. */
8
- relationships?: Readonly<Record<string, RelationshipDef>>;
8
+ relationships?: Readonly<Record<string, RelationshipDef<C>>>;
9
9
  }
10
10
  /**
11
11
  * One resource type declared once: the schemas a route answers with, and the serializer that produces
@@ -14,8 +14,11 @@ export interface ResourceOptions {
14
14
  *
15
15
  * Its schemas are lazy getters because a relationship holds a THUNK: two resources may name each other,
16
16
  * and the graph is only walkable once every module has evaluated.
17
+ *
18
+ * `C` is the context its loaders are called with, inferred from the relationships it declares and from
19
+ * the resources they point at, so every resource in one document's graph loads under one context.
17
20
  */
18
- export interface JsonApiResource<A extends ZodObject> extends RelatedResource {
21
+ export interface JsonApiResource<A extends ZodObject, C = unknown> extends RelatedResource<C> {
19
22
  readonly attributes: A;
20
23
  /** The single-resource document schema - `{ data, included?, links?, meta?, jsonapi? }`. */
21
24
  readonly document: ZodType<ResourceDocument<output<A>>>;
@@ -24,12 +27,14 @@ export interface JsonApiResource<A extends ZodObject> extends RelatedResource {
24
27
  /**
25
28
  * Serializes one row into a single-resource document.
26
29
  *
27
- * @param requested - a sparse fieldset drops attributes at RUNTIME that the declared type still names.
30
+ * @param requested - a sparse fieldset drops attributes at RUNTIME that the declared type still names,
31
+ * and an included relationship that declares a loader is loaded before the document is built
32
+ * @param context - what the loaders are given; the call takes it only where a loader is reachable
28
33
  */
29
- serialize(row: Row, requested?: RequestedDocumentShape): Promise<ResourceDocument<output<A>>>;
34
+ serialize(row: Row, requested?: RequestedDocumentShape, ...context: LoadContextArguments<C>): Promise<ResourceDocument<output<A>>>;
30
35
  /** Serializes rows into a collection document, merging in pagination `links` and `meta`. */
31
- serializeMany(rows: Row[], extras?: CollectionExtras, requested?: RequestedDocumentShape): Promise<CollectionDocument<output<A>>>;
36
+ serializeMany(rows: Row[], extras?: CollectionExtras, requested?: RequestedDocumentShape, ...context: LoadContextArguments<C>): Promise<CollectionDocument<output<A>>>;
32
37
  }
33
38
  /** Declares a JSON:API resource type - its `type`, the schema of its `attributes`, and its relationships. */
34
- export declare function jsonApiResource<A extends ZodObject>(type: string, attributes: A, options?: ResourceOptions): JsonApiResource<A>;
39
+ export declare function jsonApiResource<A extends ZodObject, C = unknown>(type: string, attributes: A, options?: ResourceOptions<C>): JsonApiResource<A, C>;
35
40
  //# 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;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC1E,OAAO,EAIL,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAE3B,KAAK,GAAG,EACT,MAAM,gBAAgB,CAAC;AAExB,uDAAuD;AACvD,MAAM,WAAW,eAAe;IAC9B,mGAAmG;IACnG,aAAa,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC;CAC3D;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe,CAAC,CAAC,SAAS,SAAS,CAAE,SAAQ,eAAe;IAC3E,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;IACvB,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;;;;OAIG;IACH,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9F,4FAA4F;IAC5F,aAAa,CACX,IAAI,EAAE,GAAG,EAAE,EACX,MAAM,CAAC,EAAE,gBAAgB,EACzB,SAAS,CAAC,EAAE,sBAAsB,GACjC,OAAO,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAC3C;AAgBD,6GAA6G;AAC7G,wBAAgB,eAAe,CAAC,CAAC,SAAS,SAAS,EACjD,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,CAAC,EACb,OAAO,GAAE,eAAoB,GAC5B,eAAe,CAAC,CAAC,CAAC,CA0DpB"}
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,uDAAuD;AACvD,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,mGAAmG;IACnG,aAAa,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAC9D;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,eAAe,CAAC,CAAC,SAAS,SAAS,EAAE,CAAC,GAAG,OAAO,CAAE,SAAQ,eAAe,CAAC,CAAC,CAAC;IAC3F,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;IACvB,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,CAAC,CAAC,SAAS,SAAS,EAAE,CAAC,GAAG,OAAO,EAC9D,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,CAAC,EACb,OAAO,GAAE,eAAe,CAAC,CAAC,CAAM,GAC/B,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAsEvB"}
package/dist/resource.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { Relator } from 'ts-japi';
2
2
  import { collectionDocumentSchema, resourceDocumentSchema, resourceObjectSchema, } from './document.js';
3
+ import { loadedRelated, loadIncludedRows } from './document-load.js';
4
+ import { loadContext } from './relationship.js';
3
5
  import { createSerializer, serializeCollection, serializeResource, } from './serialize.js';
4
6
  /**
5
7
  * How many requested shapes one resource keeps a serializer for. The shape is spelled by the caller's
@@ -35,28 +37,31 @@ export function jsonApiResource(type, attributes, options = {}) {
35
37
  get collectionDocument() {
36
38
  return (many ??= collectionDocumentSchema(resource));
37
39
  },
38
- serializerFor(requested) {
40
+ serializerFor(requested, loaded) {
39
41
  const key = shapeKey(requested);
40
- const built = serializers.get(key);
42
+ const built = loaded === undefined ? serializers.get(key) : undefined;
41
43
  if (built)
42
44
  return built;
43
45
  const relators = Object.fromEntries(Object.entries(relationships).map(([name, def]) => [
44
46
  name,
45
- new Relator(async (row) => def.read(row),
47
+ new Relator(async (row) => def.load === undefined ? def.read(row) : loadedRelated(def, row, loaded),
46
48
  // The lazy overload, so a pair of resources naming each other resolves on use, not on construction.
47
- () => def.related().serializerFor(requested), { relatedName: name }),
49
+ () => def.related().serializerFor(requested, loaded), { relatedName: name }),
48
50
  ]));
49
51
  const serializer = createSerializer(type, { projection, relators }, requested);
50
- if (serializers.size < MAX_KEPT_SERIALIZERS)
52
+ // A serializer holding one document's loaded rows would answer the next request with them.
53
+ if (loaded === undefined && serializers.size < MAX_KEPT_SERIALIZERS)
51
54
  serializers.set(key, serializer);
52
55
  return serializer;
53
56
  },
54
- serialize(row, requested) {
57
+ async serialize(row, requested, ...context) {
58
+ const loaded = await loadIncludedRows(resource, [row], requested, loadContext(context));
55
59
  // ts-japi hands back an untyped object; the declared attributes are this resource's own schema.
56
- return serializeResource(resource.serializerFor(requested), row);
60
+ return serializeResource(resource.serializerFor(requested, loaded), row, loaded);
57
61
  },
58
- serializeMany(rows, extras, requested) {
59
- return serializeCollection(resource.serializerFor(requested), rows, extras);
62
+ async serializeMany(rows, extras, requested, ...context) {
63
+ const loaded = await loadIncludedRows(resource, rows, requested, loadContext(context));
64
+ return serializeCollection(resource.serializerFor(requested, loaded), rows, extras, loaded);
60
65
  },
61
66
  };
62
67
  return resource;
@@ -2,6 +2,14 @@ import { Serializer, type SerializerOptions } from 'ts-japi';
2
2
  import type { PaginationLinks } from './primitives.js';
3
3
  /** One row as a serializer hands it over - the shape ts-japi works in. */
4
4
  export type Row = Record<string, unknown>;
5
+ /**
6
+ * The rows one document's relationship loaders answered, keyed by related type and then by id.
7
+ *
8
+ * A type listed here was loaded for this document, so it publishes exactly the rows its loader
9
+ * answered. An id the loader was asked for and did not answer is linkage the parent row carried, and
10
+ * there is no resource object for it.
11
+ */
12
+ export type LoadedRows = ReadonlyMap<string, ReadonlyMap<string, Row>>;
5
13
  /** A ts-japi serializer over rows. */
6
14
  export type ResourceSerializer = Serializer<Row>;
7
15
  type SerializerConfig = Partial<SerializerOptions<Row>>;
@@ -36,9 +44,10 @@ export interface CollectionExtras {
36
44
  * Serializes one row into a single-resource document.
37
45
  *
38
46
  * @param data - the row to serialize; its `id` member becomes the resource id
47
+ * @param loaded - what this document's relationship loaders answered, which decides its `included`
39
48
  */
40
- export declare function serializeResource(serializer: ResourceSerializer, data: Row): Promise<Row>;
49
+ export declare function serializeResource(serializer: ResourceSerializer, data: Row, loaded?: LoadedRows): Promise<Row>;
41
50
  /** Serializes a list of rows into a collection document, merging in the pagination `links` and `meta`. */
42
- export declare function serializeCollection(serializer: ResourceSerializer, items: Row[], extras?: CollectionExtras): Promise<Row>;
51
+ export declare function serializeCollection(serializer: ResourceSerializer, items: Row[], extras?: CollectionExtras, loaded?: LoadedRows): Promise<Row>;
43
52
  export {};
44
53
  //# 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,EAAE,KAAK,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEtE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,0EAA0E;AAC1E,MAAM,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE1C,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;AAQD;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,UAAU,EAAE,kBAAkB,EAAE,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAE/F;AAED,0GAA0G;AAC1G,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,kBAAkB,EAC9B,KAAK,EAAE,GAAG,EAAE,EACZ,MAAM,GAAE,gBAAqB,GAC5B,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,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"}
package/dist/serialize.js CHANGED
@@ -59,18 +59,34 @@ function stamped(doc) {
59
59
  const existing = (doc.jsonapi ?? {});
60
60
  return { ...doc, jsonapi: { ...existing, version: JSON_API_VERSION } };
61
61
  }
62
+ /**
63
+ * Drops from `included` every resource of a loaded type that its loader did not answer.
64
+ *
65
+ * A loaded relationship hands the serializer the identifier its parent row carried whenever the loader
66
+ * answered no row for it, so that the linkage stays. That identifier is not a resource, and a document
67
+ * carrying it under `included` would publish a resource object with no attributes.
68
+ */
69
+ function includedAsLoaded(document, loaded) {
70
+ if (loaded === undefined || document.included === undefined)
71
+ return document;
72
+ return {
73
+ ...document,
74
+ included: document.included.filter((resource) => loaded.get(resource.type)?.has(resource.id) ?? true),
75
+ };
76
+ }
62
77
  /**
63
78
  * Serializes one row into a single-resource document.
64
79
  *
65
80
  * @param data - the row to serialize; its `id` member becomes the resource id
81
+ * @param loaded - what this document's relationship loaders answered, which decides its `included`
66
82
  */
67
- export async function serializeResource(serializer, data) {
68
- return stamped((await serializer.serialize(data)));
83
+ export async function serializeResource(serializer, data, loaded) {
84
+ return stamped(includedAsLoaded(await serializer.serialize(data), loaded));
69
85
  }
70
86
  /** Serializes a list of rows into a collection document, merging in the pagination `links` and `meta`. */
71
- export async function serializeCollection(serializer, items, extras = {}) {
87
+ export async function serializeCollection(serializer, items, extras = {}, loaded) {
72
88
  return {
73
- ...stamped((await serializer.serialize(items))),
89
+ ...stamped(includedAsLoaded(await serializer.serialize(items), loaded)),
74
90
  ...(extras.links ? { links: extras.links } : {}),
75
91
  ...(extras.meta ? { meta: extras.meta } : {}),
76
92
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroxsolutions/response",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,