@zeroxsolutions/response 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +330 -0
- package/README.md +237 -0
- package/dist/api-response.d.ts +24 -0
- package/dist/api-response.d.ts.map +1 -0
- package/dist/api-response.js +18 -0
- package/dist/document-shape.d.ts +17 -0
- package/dist/document-shape.d.ts.map +1 -0
- package/dist/document-shape.js +29 -0
- package/dist/document.d.ts +47 -0
- package/dist/document.d.ts.map +1 -0
- package/dist/document.js +54 -0
- package/dist/domain-error-resolver.d.ts +37 -0
- package/dist/domain-error-resolver.d.ts.map +1 -0
- package/dist/domain-error-resolver.js +39 -0
- package/dist/error-map.d.ts +14 -0
- package/dist/error-map.d.ts.map +1 -0
- package/dist/error-map.js +1 -0
- package/dist/errors/error-document.d.ts +189 -0
- package/dist/errors/error-document.d.ts.map +1 -0
- package/dist/errors/error-document.js +37 -0
- package/dist/errors/error-registry.d.ts +49 -0
- package/dist/errors/error-registry.d.ts.map +1 -0
- package/dist/errors/error-registry.js +13 -0
- package/dist/errors/index.d.ts +6 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +5 -0
- package/dist/errors/jsonapi-error.d.ts +27 -0
- package/dist/errors/jsonapi-error.d.ts.map +1 -0
- package/dist/errors/jsonapi-error.js +21 -0
- package/dist/errors/resolved-error.d.ts +7 -0
- package/dist/errors/resolved-error.d.ts.map +1 -0
- package/dist/errors/resolved-error.js +1 -0
- package/dist/errors/to-error-document.d.ts +11 -0
- package/dist/errors/to-error-document.d.ts.map +1 -0
- package/dist/errors/to-error-document.js +25 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/media-type.d.ts +5 -0
- package/dist/media-type.d.ts.map +1 -0
- package/dist/media-type.js +4 -0
- package/dist/primitives.d.ts +161 -0
- package/dist/primitives.d.ts.map +1 -0
- package/dist/primitives.js +47 -0
- package/dist/relationship.d.ts +37 -0
- package/dist/relationship.d.ts.map +1 -0
- package/dist/relationship.js +44 -0
- package/dist/resource.d.ts +35 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/resource.js +63 -0
- package/dist/serialize.d.ts +44 -0
- package/dist/serialize.d.ts.map +1 -0
- package/dist/serialize.js +77 -0
- package/dist/upstream-error.d.ts +34 -0
- package/dist/upstream-error.d.ts.map +1 -0
- package/dist/upstream-error.js +33 -0
- package/package.json +38 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { JSON_API_VERSION } from '../media-type.js';
|
|
2
|
+
import { errorDocumentSchema } from './error-document.js';
|
|
3
|
+
import { JsonApiError } from './jsonapi-error.js';
|
|
4
|
+
/** Projects a {@link JsonApiError} onto a JSON:API error object (status stringified per spec), each optional member where set. */
|
|
5
|
+
export function toErrorObject(error) {
|
|
6
|
+
return {
|
|
7
|
+
...(error.links !== undefined ? { links: error.links } : {}),
|
|
8
|
+
status: String(error.status),
|
|
9
|
+
code: error.code,
|
|
10
|
+
title: error.title,
|
|
11
|
+
...(error.detail !== undefined ? { detail: error.detail } : {}),
|
|
12
|
+
...(error.source !== undefined ? { source: error.source } : {}),
|
|
13
|
+
...(error.meta !== undefined ? { meta: error.meta } : {}),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Renders one or more errors as a validated JSON:API error document, stamped with the spec version it is written to.
|
|
18
|
+
*
|
|
19
|
+
* @throws a `ZodError` on an empty array, which is not a legal error document
|
|
20
|
+
*/
|
|
21
|
+
export function toErrorDocument(errors) {
|
|
22
|
+
const list = Array.isArray(errors) ? errors : [errors];
|
|
23
|
+
const objects = list.map((error) => (error instanceof JsonApiError ? toErrorObject(error) : error));
|
|
24
|
+
return errorDocumentSchema.parse({ errors: objects, jsonapi: { version: JSON_API_VERSION } });
|
|
25
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The response half of the HTTP contract, in the one wire spelling this house answers: a JSON:API document.
|
|
3
|
+
*/
|
|
4
|
+
export { errorResponse, listResponse, okResponse, type JsonApiResponse } from './api-response.js';
|
|
5
|
+
export type { CollectionDocument, Relationship, ResourceDocument, ResourceObject } from './document.js';
|
|
6
|
+
export { declaredDocumentShape, type DeclaredDocumentShape } from './document-shape.js';
|
|
7
|
+
export { createDomainErrorResolver, defineErrorMap, errorEntry, type DomainErrorAnswer, type DomainErrorEntry, } from './domain-error-resolver.js';
|
|
8
|
+
export type { DomainErrorMap, ErrorClass, UpstreamErrorMap } from './error-map.js';
|
|
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
|
+
export { JSON_API_MEDIA_TYPE } from './media-type.js';
|
|
11
|
+
export type { JsonApiObject, Link, Meta, PaginationLinks, ResourceIdentifier, ResourceLinks } from './primitives.js';
|
|
12
|
+
export { toMany, toOne, type RelatedResource, type RelationshipDef } from './relationship.js';
|
|
13
|
+
export { jsonApiResource, type JsonApiResource, type ResourceOptions } from './resource.js';
|
|
14
|
+
export type { CollectionExtras, RequestedDocumentShape, ResourceSerializer, Row } from './serialize.js';
|
|
15
|
+
export { createUpstreamErrorTranslator, type UpstreamErrorAnswer } from './upstream-error.js';
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The response half of the HTTP contract, in the one wire spelling this house answers: a JSON:API document.
|
|
3
|
+
*/
|
|
4
|
+
export { errorResponse, listResponse, okResponse } from './api-response.js';
|
|
5
|
+
export { declaredDocumentShape } from './document-shape.js';
|
|
6
|
+
export { createDomainErrorResolver, defineErrorMap, errorEntry, } from './domain-error-resolver.js';
|
|
7
|
+
export { BASE_ERROR_CODES, errorDocumentSchema, JsonApiError, toErrorDocument, toErrorObject, } from './errors/index.js';
|
|
8
|
+
export { JSON_API_MEDIA_TYPE } from './media-type.js';
|
|
9
|
+
export { toMany, toOne } from './relationship.js';
|
|
10
|
+
export { jsonApiResource } from './resource.js';
|
|
11
|
+
export { createUpstreamErrorTranslator } from './upstream-error.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** The JSON:API media type - the one every response of this spelling is served as. */
|
|
2
|
+
export declare const JSON_API_MEDIA_TYPE = "application/vnd.api+json";
|
|
3
|
+
/** The spec version every document this package emits is written to; ts-japi's own default is `1.0`. */
|
|
4
|
+
export declare const JSON_API_VERSION = "1.1";
|
|
5
|
+
//# sourceMappingURL=media-type.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-type.d.ts","sourceRoot":"","sources":["../src/media-type.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,eAAO,MAAM,mBAAmB,6BAA6B,CAAC;AAE9D,wGAAwG;AACxG,eAAO,MAAM,gBAAgB,QAAQ,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** The JSON:API media type - the one every response of this spelling is served as. */
|
|
2
|
+
export const JSON_API_MEDIA_TYPE = 'application/vnd.api+json';
|
|
3
|
+
/** The spec version every document this package emits is written to; ts-japi's own default is `1.0`. */
|
|
4
|
+
export const JSON_API_VERSION = '1.1';
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** The free-form JSON:API `meta` member - any JSON object. */
|
|
3
|
+
export declare const metaSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
4
|
+
/** A free-form `meta` member - inferred from {@link metaSchema}. */
|
|
5
|
+
export type Meta = z.infer<typeof metaSchema>;
|
|
6
|
+
/** A JSON:API link - a URI-reference, a link object, or `null` where the link does not exist. */
|
|
7
|
+
export declare const linkSchema: z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
8
|
+
rel: z.ZodOptional<z.ZodString>;
|
|
9
|
+
title: z.ZodOptional<z.ZodString>;
|
|
10
|
+
type: z.ZodOptional<z.ZodString>;
|
|
11
|
+
hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
12
|
+
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
13
|
+
href: z.ZodString;
|
|
14
|
+
describedby: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
15
|
+
rel: z.ZodOptional<z.ZodString>;
|
|
16
|
+
title: z.ZodOptional<z.ZodString>;
|
|
17
|
+
type: z.ZodOptional<z.ZodString>;
|
|
18
|
+
hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
19
|
+
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
20
|
+
href: z.ZodString;
|
|
21
|
+
}, z.core.$strip>, z.ZodNull]>>;
|
|
22
|
+
}, z.core.$strip>, z.ZodNull]>;
|
|
23
|
+
/** A JSON:API link value - inferred from {@link linkSchema}. */
|
|
24
|
+
export type Link = z.infer<typeof linkSchema>;
|
|
25
|
+
/** The `self`/`related` pair a resource object and a relationship both carry. */
|
|
26
|
+
export declare const resourceLinksSchema: z.ZodObject<{
|
|
27
|
+
self: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
28
|
+
rel: z.ZodOptional<z.ZodString>;
|
|
29
|
+
title: z.ZodOptional<z.ZodString>;
|
|
30
|
+
type: z.ZodOptional<z.ZodString>;
|
|
31
|
+
hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
32
|
+
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
33
|
+
href: z.ZodString;
|
|
34
|
+
describedby: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
35
|
+
rel: z.ZodOptional<z.ZodString>;
|
|
36
|
+
title: z.ZodOptional<z.ZodString>;
|
|
37
|
+
type: z.ZodOptional<z.ZodString>;
|
|
38
|
+
hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
39
|
+
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
40
|
+
href: z.ZodString;
|
|
41
|
+
}, z.core.$strip>, z.ZodNull]>>;
|
|
42
|
+
}, z.core.$strip>, z.ZodNull]>>;
|
|
43
|
+
related: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
44
|
+
rel: z.ZodOptional<z.ZodString>;
|
|
45
|
+
title: z.ZodOptional<z.ZodString>;
|
|
46
|
+
type: z.ZodOptional<z.ZodString>;
|
|
47
|
+
hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
48
|
+
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
49
|
+
href: z.ZodString;
|
|
50
|
+
describedby: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
51
|
+
rel: z.ZodOptional<z.ZodString>;
|
|
52
|
+
title: z.ZodOptional<z.ZodString>;
|
|
53
|
+
type: z.ZodOptional<z.ZodString>;
|
|
54
|
+
hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
55
|
+
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
56
|
+
href: z.ZodString;
|
|
57
|
+
}, z.core.$strip>, z.ZodNull]>>;
|
|
58
|
+
}, z.core.$strip>, z.ZodNull]>>;
|
|
59
|
+
}, z.core.$strip>;
|
|
60
|
+
/** A resource object's links - inferred from {@link resourceLinksSchema}. */
|
|
61
|
+
export type ResourceLinks = z.infer<typeof resourceLinksSchema>;
|
|
62
|
+
/** Collection pagination links: `self` plus the `first`/`prev`/`next`/`last` navigation, each null at the page that has none. */
|
|
63
|
+
export declare const paginationLinksSchema: z.ZodObject<{
|
|
64
|
+
self: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
65
|
+
rel: z.ZodOptional<z.ZodString>;
|
|
66
|
+
title: z.ZodOptional<z.ZodString>;
|
|
67
|
+
type: z.ZodOptional<z.ZodString>;
|
|
68
|
+
hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
69
|
+
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
70
|
+
href: z.ZodString;
|
|
71
|
+
describedby: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
72
|
+
rel: z.ZodOptional<z.ZodString>;
|
|
73
|
+
title: z.ZodOptional<z.ZodString>;
|
|
74
|
+
type: z.ZodOptional<z.ZodString>;
|
|
75
|
+
hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
76
|
+
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
77
|
+
href: z.ZodString;
|
|
78
|
+
}, z.core.$strip>, z.ZodNull]>>;
|
|
79
|
+
}, z.core.$strip>, z.ZodNull]>>;
|
|
80
|
+
first: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
81
|
+
rel: z.ZodOptional<z.ZodString>;
|
|
82
|
+
title: z.ZodOptional<z.ZodString>;
|
|
83
|
+
type: z.ZodOptional<z.ZodString>;
|
|
84
|
+
hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
85
|
+
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
86
|
+
href: z.ZodString;
|
|
87
|
+
describedby: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
88
|
+
rel: z.ZodOptional<z.ZodString>;
|
|
89
|
+
title: z.ZodOptional<z.ZodString>;
|
|
90
|
+
type: z.ZodOptional<z.ZodString>;
|
|
91
|
+
hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
92
|
+
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
93
|
+
href: z.ZodString;
|
|
94
|
+
}, z.core.$strip>, z.ZodNull]>>;
|
|
95
|
+
}, z.core.$strip>, z.ZodNull]>>;
|
|
96
|
+
prev: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
97
|
+
rel: z.ZodOptional<z.ZodString>;
|
|
98
|
+
title: z.ZodOptional<z.ZodString>;
|
|
99
|
+
type: z.ZodOptional<z.ZodString>;
|
|
100
|
+
hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
101
|
+
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
102
|
+
href: z.ZodString;
|
|
103
|
+
describedby: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
104
|
+
rel: z.ZodOptional<z.ZodString>;
|
|
105
|
+
title: z.ZodOptional<z.ZodString>;
|
|
106
|
+
type: z.ZodOptional<z.ZodString>;
|
|
107
|
+
hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
108
|
+
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
109
|
+
href: z.ZodString;
|
|
110
|
+
}, z.core.$strip>, z.ZodNull]>>;
|
|
111
|
+
}, z.core.$strip>, z.ZodNull]>>;
|
|
112
|
+
next: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
113
|
+
rel: z.ZodOptional<z.ZodString>;
|
|
114
|
+
title: z.ZodOptional<z.ZodString>;
|
|
115
|
+
type: z.ZodOptional<z.ZodString>;
|
|
116
|
+
hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
117
|
+
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
118
|
+
href: z.ZodString;
|
|
119
|
+
describedby: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
120
|
+
rel: z.ZodOptional<z.ZodString>;
|
|
121
|
+
title: z.ZodOptional<z.ZodString>;
|
|
122
|
+
type: z.ZodOptional<z.ZodString>;
|
|
123
|
+
hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
124
|
+
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
125
|
+
href: z.ZodString;
|
|
126
|
+
}, z.core.$strip>, z.ZodNull]>>;
|
|
127
|
+
}, z.core.$strip>, z.ZodNull]>>;
|
|
128
|
+
last: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
129
|
+
rel: z.ZodOptional<z.ZodString>;
|
|
130
|
+
title: z.ZodOptional<z.ZodString>;
|
|
131
|
+
type: z.ZodOptional<z.ZodString>;
|
|
132
|
+
hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
133
|
+
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
134
|
+
href: z.ZodString;
|
|
135
|
+
describedby: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
136
|
+
rel: z.ZodOptional<z.ZodString>;
|
|
137
|
+
title: z.ZodOptional<z.ZodString>;
|
|
138
|
+
type: z.ZodOptional<z.ZodString>;
|
|
139
|
+
hreflang: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
140
|
+
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
141
|
+
href: z.ZodString;
|
|
142
|
+
}, z.core.$strip>, z.ZodNull]>>;
|
|
143
|
+
}, z.core.$strip>, z.ZodNull]>>;
|
|
144
|
+
}, z.core.$strip>;
|
|
145
|
+
/** Collection pagination links - inferred from {@link paginationLinksSchema}. */
|
|
146
|
+
export type PaginationLinks = z.infer<typeof paginationLinksSchema>;
|
|
147
|
+
/** A resource identifier object - the `{ type, id }` linkage that points at another resource. */
|
|
148
|
+
export declare const resourceIdentifierSchema: z.ZodObject<{
|
|
149
|
+
type: z.ZodString;
|
|
150
|
+
id: z.ZodString;
|
|
151
|
+
}, z.core.$strip>;
|
|
152
|
+
/** A type-and-id resource identifier - inferred from {@link resourceIdentifierSchema}. */
|
|
153
|
+
export type ResourceIdentifier = z.infer<typeof resourceIdentifierSchema>;
|
|
154
|
+
/** The top-level `jsonapi` member - the spec version this document is written to, plus meta. */
|
|
155
|
+
export declare const jsonApiObjectSchema: z.ZodObject<{
|
|
156
|
+
version: z.ZodOptional<z.ZodString>;
|
|
157
|
+
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
158
|
+
}, z.core.$strip>;
|
|
159
|
+
/** The top-level `jsonapi` member - inferred from {@link jsonApiObjectSchema}. */
|
|
160
|
+
export type JsonApiObject = z.infer<typeof jsonApiObjectSchema>;
|
|
161
|
+
//# sourceMappingURL=primitives.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["../src/primitives.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,8DAA8D;AAC9D,eAAO,MAAM,UAAU,wCAAoC,CAAC;AAC5D,oEAAoE;AACpE,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAsB9C,iGAAiG;AACjG,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;8BAIrB,CAAC;AACH,gEAAgE;AAChE,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAE9C,iFAAiF;AACjF,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAG9B,CAAC;AACH,6EAA6E;AAC7E,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,iIAAiI;AACjI,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAMhC,CAAC;AACH,iFAAiF;AACjF,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,iGAAiG;AACjG,eAAO,MAAM,wBAAwB;;;iBAAiD,CAAC;AACvF,0FAA0F;AAC1F,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,gGAAgG;AAChG,eAAO,MAAM,mBAAmB;;;iBAG9B,CAAC;AACH,kFAAkF;AAClF,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** The free-form JSON:API `meta` member - any JSON object. */
|
|
3
|
+
export const metaSchema = z.record(z.string(), z.unknown());
|
|
4
|
+
/** Everything a link object may say about its target except where it points and what describes it. */
|
|
5
|
+
const linkTarget = {
|
|
6
|
+
/** The link's relation type - a registered or extension relation, per RFC 8288. */
|
|
7
|
+
rel: z.string().optional(),
|
|
8
|
+
title: z.string().optional(),
|
|
9
|
+
/** The media type of the target. */
|
|
10
|
+
type: z.string().optional(),
|
|
11
|
+
hreflang: z.union([z.string(), z.array(z.string())]).optional(),
|
|
12
|
+
meta: metaSchema.optional(),
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The description document a link points at - a link itself, minus a `describedby` of its own.
|
|
16
|
+
*
|
|
17
|
+
* The standard nests these without limit, which a schema can only express through `z.lazy`. A lazy member renders as a
|
|
18
|
+
* `$defs` ref that the OpenAPI document has nowhere to resolve, and the gateway serving it answers 500
|
|
19
|
+
* (@hono/zod-openapi@1.4.0) - so the cycle stops at one level, which is every document anyone writes.
|
|
20
|
+
*/
|
|
21
|
+
const describedbySchema = z.union([z.string(), z.object({ href: z.string(), ...linkTarget }), z.null()]);
|
|
22
|
+
/** A JSON:API link - a URI-reference, a link object, or `null` where the link does not exist. */
|
|
23
|
+
export const linkSchema = z.union([
|
|
24
|
+
z.string(),
|
|
25
|
+
z.object({ href: z.string(), describedby: describedbySchema.optional(), ...linkTarget }),
|
|
26
|
+
z.null(),
|
|
27
|
+
]);
|
|
28
|
+
/** The `self`/`related` pair a resource object and a relationship both carry. */
|
|
29
|
+
export const resourceLinksSchema = z.object({
|
|
30
|
+
self: linkSchema.optional(),
|
|
31
|
+
related: linkSchema.optional(),
|
|
32
|
+
});
|
|
33
|
+
/** Collection pagination links: `self` plus the `first`/`prev`/`next`/`last` navigation, each null at the page that has none. */
|
|
34
|
+
export const paginationLinksSchema = z.object({
|
|
35
|
+
self: linkSchema.optional(),
|
|
36
|
+
first: linkSchema.optional(),
|
|
37
|
+
prev: linkSchema.optional(),
|
|
38
|
+
next: linkSchema.optional(),
|
|
39
|
+
last: linkSchema.optional(),
|
|
40
|
+
});
|
|
41
|
+
/** A resource identifier object - the `{ type, id }` linkage that points at another resource. */
|
|
42
|
+
export const resourceIdentifierSchema = z.object({ type: z.string(), id: z.string() });
|
|
43
|
+
/** The top-level `jsonapi` member - the spec version this document is written to, plus meta. */
|
|
44
|
+
export const jsonApiObjectSchema = z.object({
|
|
45
|
+
version: z.string().optional(),
|
|
46
|
+
meta: metaSchema.optional(),
|
|
47
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ZodObject, ZodType } from 'zod';
|
|
2
|
+
import type { RequestedDocumentShape, ResourceSerializer, Row } from './serialize.js';
|
|
3
|
+
/** What a relationship needs from the resource it points at, so this module never imports the descriptor. */
|
|
4
|
+
export interface RelatedResource {
|
|
5
|
+
readonly type: string;
|
|
6
|
+
readonly relationships: Readonly<Record<string, RelationshipDef>>;
|
|
7
|
+
/** This type's declared attributes - the keys are the projection, so they are also what a fieldset may name. */
|
|
8
|
+
readonly attributes: ZodObject;
|
|
9
|
+
/** This type's resource object schema - what a document validates its `data` and `included` against. */
|
|
10
|
+
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;
|
|
13
|
+
}
|
|
14
|
+
/** A relationship: the resource it points at, how many, and how to read it off a row. */
|
|
15
|
+
export interface RelationshipDef {
|
|
16
|
+
readonly cardinality: 'one' | 'many';
|
|
17
|
+
/** 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. */
|
|
20
|
+
readonly read: (row: Row) => unknown;
|
|
21
|
+
}
|
|
22
|
+
/** 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;
|
|
24
|
+
/** Declares a to-many relationship - its `data` is an array of identifiers. */
|
|
25
|
+
export declare function toMany(related: () => RelatedResource, read: (row: Row) => unknown): RelationshipDef;
|
|
26
|
+
/** 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;
|
|
28
|
+
/** 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;
|
|
30
|
+
/**
|
|
31
|
+
* Every resource reachable from `root` through its relationships, `root` first, each visited once.
|
|
32
|
+
*
|
|
33
|
+
* This is what calls the thunks, so it runs on first use rather than at declaration - two resources naming
|
|
34
|
+
* each other are both defined by then, which is the whole reason the declaration holds a thunk.
|
|
35
|
+
*/
|
|
36
|
+
export declare function reachableResources(root: RelatedResource): RelatedResource[];
|
|
37
|
+
//# sourceMappingURL=relationship.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { metaSchema, resourceIdentifierSchema, resourceLinksSchema } from './primitives.js';
|
|
3
|
+
/** 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 };
|
|
6
|
+
}
|
|
7
|
+
/** Declares a to-many relationship - its `data` is an array of identifiers. */
|
|
8
|
+
export function toMany(related, read) {
|
|
9
|
+
return { cardinality: 'many', related, read };
|
|
10
|
+
}
|
|
11
|
+
/** The linkage schema for one relationship, pinned to the related `type` so a wrong type fails the parse. */
|
|
12
|
+
export function relationshipSchema(def) {
|
|
13
|
+
const identifier = resourceIdentifierSchema.extend({ type: z.literal(def.related().type) });
|
|
14
|
+
return z.object({
|
|
15
|
+
data: (def.cardinality === 'one' ? identifier.nullable() : z.array(identifier)).optional(),
|
|
16
|
+
links: resourceLinksSchema.optional(),
|
|
17
|
+
meta: metaSchema.optional(),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/** The `relationships` member for a whole resource - one key per declaration, absent when it declares none. */
|
|
21
|
+
export function relationshipsSchema(relationships) {
|
|
22
|
+
const entries = Object.entries(relationships);
|
|
23
|
+
if (entries.length === 0)
|
|
24
|
+
return undefined;
|
|
25
|
+
return z.object(Object.fromEntries(entries.map(([name, def]) => [name, relationshipSchema(def).optional()])));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Every resource reachable from `root` through its relationships, `root` first, each visited once.
|
|
29
|
+
*
|
|
30
|
+
* This is what calls the thunks, so it runs on first use rather than at declaration - two resources naming
|
|
31
|
+
* each other are both defined by then, which is the whole reason the declaration holds a thunk.
|
|
32
|
+
*/
|
|
33
|
+
export function reachableResources(root) {
|
|
34
|
+
const seen = new Map();
|
|
35
|
+
const visit = (resource) => {
|
|
36
|
+
if (seen.has(resource.type))
|
|
37
|
+
return;
|
|
38
|
+
seen.set(resource.type, resource);
|
|
39
|
+
for (const def of Object.values(resource.relationships))
|
|
40
|
+
visit(def.related());
|
|
41
|
+
};
|
|
42
|
+
visit(root);
|
|
43
|
+
return [...seen.values()];
|
|
44
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { output, ZodObject, ZodType } from 'zod';
|
|
2
|
+
import { type CollectionDocument, type ResourceDocument } from './document.js';
|
|
3
|
+
import type { RelatedResource, RelationshipDef } from './relationship.js';
|
|
4
|
+
import { type CollectionExtras, type RequestedDocumentShape, type Row } from './serialize.js';
|
|
5
|
+
/** Per-entity overrides for a resource declaration. */
|
|
6
|
+
export interface ResourceOptions {
|
|
7
|
+
/** Relationships this resource declares, keyed by the member name they take in `relationships`. */
|
|
8
|
+
relationships?: Readonly<Record<string, RelationshipDef>>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* One resource type declared once: the schemas a route answers with, and the serializer that produces
|
|
12
|
+
* them. A route's `responses` and its handler's document come off this same value, so the document the
|
|
13
|
+
* handler emits is the one the route advertises.
|
|
14
|
+
*
|
|
15
|
+
* Its schemas are lazy getters because a relationship holds a THUNK: two resources may name each other,
|
|
16
|
+
* and the graph is only walkable once every module has evaluated.
|
|
17
|
+
*/
|
|
18
|
+
export interface JsonApiResource<A extends ZodObject> extends RelatedResource {
|
|
19
|
+
readonly attributes: A;
|
|
20
|
+
/** The single-resource document schema - `{ data, included?, links?, meta?, jsonapi? }`. */
|
|
21
|
+
readonly document: ZodType<ResourceDocument<output<A>>>;
|
|
22
|
+
/** The collection document schema - the same members with `data` an array. */
|
|
23
|
+
readonly collectionDocument: ZodType<CollectionDocument<output<A>>>;
|
|
24
|
+
/**
|
|
25
|
+
* Serializes one row into a single-resource document.
|
|
26
|
+
*
|
|
27
|
+
* @param requested - a sparse fieldset drops attributes at RUNTIME that the declared type still names.
|
|
28
|
+
*/
|
|
29
|
+
serialize(row: Row, requested?: RequestedDocumentShape): Promise<ResourceDocument<output<A>>>;
|
|
30
|
+
/** Serializes rows into a collection document, merging in pagination `links` and `meta`. */
|
|
31
|
+
serializeMany(rows: Row[], extras?: CollectionExtras, requested?: RequestedDocumentShape): Promise<CollectionDocument<output<A>>>;
|
|
32
|
+
}
|
|
33
|
+
/** 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>;
|
|
35
|
+
//# sourceMappingURL=resource.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/resource.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Relator } from 'ts-japi';
|
|
2
|
+
import { collectionDocumentSchema, resourceDocumentSchema, resourceObjectSchema, } from './document.js';
|
|
3
|
+
import { createSerializer, serializeCollection, serializeResource, } from './serialize.js';
|
|
4
|
+
/**
|
|
5
|
+
* How many requested shapes one resource keeps a serializer for. The shape is spelled by the caller's
|
|
6
|
+
* query, so past this bound a serializer is built for the call and not kept.
|
|
7
|
+
*/
|
|
8
|
+
const MAX_KEPT_SERIALIZERS = 64;
|
|
9
|
+
/**
|
|
10
|
+
* The cache key for a requested shape: its content, not its identity. A route parses a fresh object on
|
|
11
|
+
* every request, so an identity key keeps one serializer per request for the life of the process.
|
|
12
|
+
*/
|
|
13
|
+
function shapeKey(requested) {
|
|
14
|
+
return JSON.stringify([requested?.fields ?? null, requested?.include ?? null]);
|
|
15
|
+
}
|
|
16
|
+
/** Declares a JSON:API resource type - its `type`, the schema of its `attributes`, and its relationships. */
|
|
17
|
+
export function jsonApiResource(type, attributes, options = {}) {
|
|
18
|
+
const relationships = options.relationships ?? {};
|
|
19
|
+
// The attribute keys ARE the projection, so a row member the schema does not declare never reaches the wire.
|
|
20
|
+
const projection = Object.fromEntries(Object.keys(attributes.shape).map((name) => [name, 1]));
|
|
21
|
+
const serializers = new Map();
|
|
22
|
+
let object;
|
|
23
|
+
let one;
|
|
24
|
+
let many;
|
|
25
|
+
const resource = {
|
|
26
|
+
type,
|
|
27
|
+
attributes,
|
|
28
|
+
relationships,
|
|
29
|
+
get resourceObject() {
|
|
30
|
+
return (object ??= resourceObjectSchema(type, attributes, relationships));
|
|
31
|
+
},
|
|
32
|
+
get document() {
|
|
33
|
+
return (one ??= resourceDocumentSchema(resource));
|
|
34
|
+
},
|
|
35
|
+
get collectionDocument() {
|
|
36
|
+
return (many ??= collectionDocumentSchema(resource));
|
|
37
|
+
},
|
|
38
|
+
serializerFor(requested) {
|
|
39
|
+
const key = shapeKey(requested);
|
|
40
|
+
const built = serializers.get(key);
|
|
41
|
+
if (built)
|
|
42
|
+
return built;
|
|
43
|
+
const relators = Object.fromEntries(Object.entries(relationships).map(([name, def]) => [
|
|
44
|
+
name,
|
|
45
|
+
new Relator(async (row) => def.read(row),
|
|
46
|
+
// The lazy overload, so a pair of resources naming each other resolves on use, not on construction.
|
|
47
|
+
() => def.related().serializerFor(requested), { relatedName: name }),
|
|
48
|
+
]));
|
|
49
|
+
const serializer = createSerializer(type, { projection, relators }, requested);
|
|
50
|
+
if (serializers.size < MAX_KEPT_SERIALIZERS)
|
|
51
|
+
serializers.set(key, serializer);
|
|
52
|
+
return serializer;
|
|
53
|
+
},
|
|
54
|
+
serialize(row, requested) {
|
|
55
|
+
// ts-japi hands back an untyped object; the declared attributes are this resource's own schema.
|
|
56
|
+
return serializeResource(resource.serializerFor(requested), row);
|
|
57
|
+
},
|
|
58
|
+
serializeMany(rows, extras, requested) {
|
|
59
|
+
return serializeCollection(resource.serializerFor(requested), rows, extras);
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
return resource;
|
|
63
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Serializer, type SerializerOptions } from 'ts-japi';
|
|
2
|
+
import type { PaginationLinks } from './primitives.js';
|
|
3
|
+
/** One row as a serializer hands it over - the shape ts-japi works in. */
|
|
4
|
+
export type Row = Record<string, unknown>;
|
|
5
|
+
/** A ts-japi serializer over rows. */
|
|
6
|
+
export type ResourceSerializer = Serializer<Row>;
|
|
7
|
+
type SerializerConfig = Partial<SerializerOptions<Row>>;
|
|
8
|
+
/**
|
|
9
|
+
* The half of a parsed list query that shapes the response document, spelled with
|
|
10
|
+
* `@zeroxsolutions/query`'s own member names so its parse result passes straight through; structural
|
|
11
|
+
* rather than imported, because the dependency would be one-way and for two keys.
|
|
12
|
+
*/
|
|
13
|
+
export interface RequestedDocumentShape {
|
|
14
|
+
/** Sparse fieldsets keyed by resource type. A type absent here is unrestricted. */
|
|
15
|
+
fields?: Readonly<Record<string, readonly string[]>>;
|
|
16
|
+
/** Relationship paths to include. Absent means a flat document. */
|
|
17
|
+
include?: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A ts-japi serializer for a resource `type`, narrowed to the request's sparse fieldset and traversing
|
|
21
|
+
* the relationship paths it asked to include - both halves of a resource's fields, since the standard
|
|
22
|
+
* calls attributes and relationships alike its fields.
|
|
23
|
+
*
|
|
24
|
+
* The shape is applied by CONSTRUCTING the serializer, never by handing options to `serialize()`: in
|
|
25
|
+
* ts-japi 1.12.3 a narrower projection passed there leaves the constructed one in force and narrows
|
|
26
|
+
* nothing.
|
|
27
|
+
*/
|
|
28
|
+
export declare function createSerializer(type: string, options?: SerializerConfig, requested?: RequestedDocumentShape): ResourceSerializer;
|
|
29
|
+
/** Request-derived top-level members merged into a collection document. */
|
|
30
|
+
export interface CollectionExtras {
|
|
31
|
+
links?: PaginationLinks;
|
|
32
|
+
/** The document's top-level `meta`. */
|
|
33
|
+
meta?: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Serializes one row into a single-resource document.
|
|
37
|
+
*
|
|
38
|
+
* @param data - the row to serialize; its `id` member becomes the resource id
|
|
39
|
+
*/
|
|
40
|
+
export declare function serializeResource(serializer: ResourceSerializer, data: Row): Promise<Row>;
|
|
41
|
+
/** 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>;
|
|
43
|
+
export {};
|
|
44
|
+
//# sourceMappingURL=serialize.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Relator, Serializer } from 'ts-japi';
|
|
2
|
+
import { JSON_API_VERSION } from './media-type.js';
|
|
3
|
+
/**
|
|
4
|
+
* Whether the static projection publishes `name`, read so that a `0` counts as hidden under either mode.
|
|
5
|
+
*
|
|
6
|
+
* ts-japi 1.12.3 honours an all-`1` projection as a whitelist and an all-`0` one as a blacklist, but a
|
|
7
|
+
* MIXED one as neither: `{a:1,b:0}` emits both `a` and `b`. A `0` is answered `false` here regardless, so
|
|
8
|
+
* the projection built below is only ever a whitelist.
|
|
9
|
+
*
|
|
10
|
+
* An EMPTY projection emits no attribute at all in that version, so it is a whitelist of nothing rather
|
|
11
|
+
* than a blacklist of nothing - which is why the blacklist reading below requires the projection to list
|
|
12
|
+
* something.
|
|
13
|
+
*/
|
|
14
|
+
function publishes(projection, name) {
|
|
15
|
+
if (projection == null)
|
|
16
|
+
return true;
|
|
17
|
+
if (projection[name] === 0)
|
|
18
|
+
return false;
|
|
19
|
+
if (projection[name] === 1)
|
|
20
|
+
return true;
|
|
21
|
+
const configured = Object.values(projection);
|
|
22
|
+
return configured.length > 0 && configured.every((value) => value !== 1);
|
|
23
|
+
}
|
|
24
|
+
/** Keeps the named relators, preserving whichever of ts-japi's three forms was given. */
|
|
25
|
+
function narrowRelators(relators, permitted) {
|
|
26
|
+
if (relators === undefined)
|
|
27
|
+
return undefined;
|
|
28
|
+
if (Array.isArray(relators))
|
|
29
|
+
return relators.filter((one) => permitted.has(one.relatedName));
|
|
30
|
+
if (relators instanceof Relator)
|
|
31
|
+
return permitted.has(relators.relatedName) ? relators : undefined;
|
|
32
|
+
return Object.fromEntries(Object.entries(relators).filter(([name]) => permitted.has(name)));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A ts-japi serializer for a resource `type`, narrowed to the request's sparse fieldset and traversing
|
|
36
|
+
* the relationship paths it asked to include - both halves of a resource's fields, since the standard
|
|
37
|
+
* calls attributes and relationships alike its fields.
|
|
38
|
+
*
|
|
39
|
+
* The shape is applied by CONSTRUCTING the serializer, never by handing options to `serialize()`: in
|
|
40
|
+
* ts-japi 1.12.3 a narrower projection passed there leaves the constructed one in force and narrows
|
|
41
|
+
* nothing.
|
|
42
|
+
*/
|
|
43
|
+
export function createSerializer(type, options, requested) {
|
|
44
|
+
const fields = requested?.fields?.[type];
|
|
45
|
+
const include = requested?.include;
|
|
46
|
+
return new Serializer(type, {
|
|
47
|
+
...options,
|
|
48
|
+
...(include === undefined ? {} : { include: [...include] }),
|
|
49
|
+
...(fields === undefined
|
|
50
|
+
? {}
|
|
51
|
+
: {
|
|
52
|
+
projection: Object.fromEntries(fields.filter((name) => publishes(options?.projection, name)).map((name) => [name, 1])),
|
|
53
|
+
relators: narrowRelators(options?.relators, new Set(fields)),
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/** Raises the `jsonapi` member to {@link JSON_API_VERSION}, keeping whatever else ts-japi put there. */
|
|
58
|
+
function stamped(doc) {
|
|
59
|
+
const existing = (doc.jsonapi ?? {});
|
|
60
|
+
return { ...doc, jsonapi: { ...existing, version: JSON_API_VERSION } };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Serializes one row into a single-resource document.
|
|
64
|
+
*
|
|
65
|
+
* @param data - the row to serialize; its `id` member becomes the resource id
|
|
66
|
+
*/
|
|
67
|
+
export async function serializeResource(serializer, data) {
|
|
68
|
+
return stamped((await serializer.serialize(data)));
|
|
69
|
+
}
|
|
70
|
+
/** Serializes a list of rows into a collection document, merging in the pagination `links` and `meta`. */
|
|
71
|
+
export async function serializeCollection(serializer, items, extras = {}) {
|
|
72
|
+
return {
|
|
73
|
+
...stamped((await serializer.serialize(items))),
|
|
74
|
+
...(extras.links ? { links: extras.links } : {}),
|
|
75
|
+
...(extras.meta ? { meta: extras.meta } : {}),
|
|
76
|
+
};
|
|
77
|
+
}
|