Skip to content

Commit 33f7152

Browse files
committed
feat: allow specifying document meta schema
1 parent de7e9aa commit 33f7152

File tree

6 files changed

+114
-28
lines changed

6 files changed

+114
-28
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ import { createDataSelector, createResourceSelector } from "jsonapi-zod-query";
8989
const articleSelector = createDataSelector(createResourceSelector(/**/));
9090
```
9191

92+
### Typing document meta data
93+
94+
By default, document metadata are considered as an optional record of unknown properties. You can pass a
95+
`documentMetaSchema` option to resource selector creators, which will enforce a specific schema.
96+
9297
### Handling pagination
9398

9499
This library assumes that you never actually use the `links` properties in the JSON:API documents, but are primarily

src/deserializer.ts

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import type { z } from "zod";
22
import type { PageParams } from "./pagination.ts";
3-
import type { AttributesSchema, DefaultLinks, DefaultMeta, RootLinks } from "./standard-schemas.ts";
3+
import type { defaultMetaSchema } from "./standard-schemas.ts";
4+
import type {
5+
AttributesSchema,
6+
DefaultLinks,
7+
DefaultMeta,
8+
MetaSchema,
9+
RootLinks,
10+
} from "./standard-schemas.ts";
411

512
export type RelationshipType = "one" | "one_nullable" | "many";
613

@@ -41,16 +48,19 @@ export type ResourceDeserializer<
4148
TType extends string,
4249
TAttributesSchema extends AttributesSchema | undefined,
4350
TRelationships extends Relationships | undefined,
51+
TDocumentMetaSchema extends MetaSchema | undefined,
4452
> = {
4553
type: TType;
4654
attributesSchema?: TAttributesSchema;
4755
relationships?: TRelationships;
56+
documentMetaSchema?: TDocumentMetaSchema;
4857
};
4958

5059
export type AnyResourceDeserializer = ResourceDeserializer<
5160
string,
5261
AttributesSchema | undefined,
53-
Relationships | undefined
62+
Relationships | undefined,
63+
MetaSchema | undefined
5464
>;
5565

5666
export type InferResourceType<T> = T extends ReferenceRelationshipDeserializer<
@@ -74,20 +84,31 @@ export type InferInclude<T> = T extends IncludedRelationshipDeserializer<Relatio
7484
export type InferType<T> = T extends ResourceDeserializer<
7585
infer U,
7686
AttributesSchema | undefined,
77-
Relationships | undefined
87+
Relationships | undefined,
88+
MetaSchema | undefined
7889
>
7990
? U
8091
: never;
8192
export type InferAttributesSchema<T> = T extends ResourceDeserializer<
8293
string,
8394
infer U,
84-
Relationships | undefined
95+
Relationships | undefined,
96+
MetaSchema | undefined
8597
>
8698
? U
8799
: never;
88100
export type InferRelationships<T> = T extends ResourceDeserializer<
89101
string,
90102
AttributesSchema | undefined,
103+
infer U,
104+
MetaSchema | undefined
105+
>
106+
? U
107+
: never;
108+
export type InferDocumentMetaSchema<T> = T extends ResourceDeserializer<
109+
string,
110+
AttributesSchema | undefined,
111+
Relationships | undefined,
91112
infer U
92113
>
93114
? U
@@ -137,18 +158,33 @@ export type ResourceResult<
137158
TRelationships
138159
>;
139160

140-
export type DocumentResult<TData> = {
161+
export type FallbackMetaSchema<T extends MetaSchema> = T extends MetaSchema
162+
? MetaSchema
163+
: z.ZodOptional<typeof defaultMetaSchema>;
164+
165+
type MetaResult<TMeta extends MetaSchema | undefined> = TMeta extends MetaSchema
166+
? z.output<TMeta>
167+
: DefaultMeta | undefined;
168+
169+
export type DocumentResult<TData, TMeta> = {
141170
data: TData;
142171
links?: RootLinks;
143-
meta?: DefaultMeta;
172+
meta: TMeta;
144173
};
145174
export type ResourceDocumentResult<TDeserializer extends AnyResourceDeserializer> = DocumentResult<
146-
ResourceResult<TDeserializer>
175+
ResourceResult<TDeserializer>,
176+
MetaResult<InferDocumentMetaSchema<TDeserializer>>
147177
>;
148178
export type NullableResourceDocumentResult<TDeserializer extends AnyResourceDeserializer> =
149-
DocumentResult<ResourceResult<TDeserializer> | null>;
179+
DocumentResult<
180+
ResourceResult<TDeserializer> | null,
181+
MetaResult<InferDocumentMetaSchema<TDeserializer>>
182+
>;
150183
export type ResourceCollectionDocumentResult<TDeserializer extends AnyResourceDeserializer> =
151-
DocumentResult<ResourceResult<TDeserializer>[]> & {
184+
DocumentResult<
185+
ResourceResult<TDeserializer>[],
186+
MetaResult<InferDocumentMetaSchema<TDeserializer>>
187+
> & {
152188
pageParams: {
153189
first?: PageParams;
154190
prev?: PageParams;

src/parser-schemas.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { z } from "zod";
2+
import type { FallbackMetaSchema, InferDocumentMetaSchema } from "./deserializer.ts";
23
import type {
34
AnyRelationshipDeserializer,
45
AnyResourceDeserializer,
@@ -55,7 +56,7 @@ const createRelationshipSchema = <TDeserializer extends AnyRelationshipDeseriali
5556
meta: defaultMetaSchema.optional(),
5657
});
5758

58-
let dataSchema: z.ZodType<unknown>;
59+
let dataSchema: z.ZodTypeAny;
5960

6061
switch (deserializer.relationshipType) {
6162
case "one": {
@@ -149,44 +150,59 @@ const includedSchema = z.array(includedResourceSchema);
149150

150151
type BaseDocumentOutput = {
151152
links?: RootLinks;
152-
meta?: DefaultMeta;
153153
included?: z.output<typeof includedSchema>;
154154
};
155155

156-
export type DocumentSchema<TDataSchema extends z.ZodType<unknown>> = z.ZodType<
156+
export type DocumentSchema<
157+
TDataSchema extends z.ZodTypeAny,
158+
TMetaSchema extends z.ZodTypeAny,
159+
> = z.ZodType<
157160
BaseDocumentOutput & {
158161
data: z.output<TDataSchema>;
159-
}
162+
} & (z.output<TMetaSchema> extends undefined
163+
? {
164+
meta?: z.output<TMetaSchema>;
165+
}
166+
: { meta: z.output<TMetaSchema> })
160167
>;
161168

162169
export const createResourceDocumentSchema = <TDeserializer extends AnyResourceDeserializer>(
163170
deserializer: TDeserializer,
164-
): DocumentSchema<ResourceSchema<TDeserializer>> =>
171+
): DocumentSchema<
172+
ResourceSchema<TDeserializer>,
173+
FallbackMetaSchema<InferDocumentMetaSchema<TDeserializer>>
174+
> =>
165175
z.object({
166176
data: createResourceSchema(deserializer),
167177
links: rootLinksSchema.optional(),
168-
meta: defaultMetaSchema.optional(),
178+
meta: deserializer.documentMetaSchema ?? defaultMetaSchema.optional(),
169179
included: includedSchema.optional(),
170180
});
171181

172182
export const createNullableResourceDocumentSchema = <TDeserializer extends AnyResourceDeserializer>(
173183
deserializer: TDeserializer,
174-
): DocumentSchema<z.ZodNullable<ResourceSchema<TDeserializer>>> =>
184+
): DocumentSchema<
185+
z.ZodNullable<ResourceSchema<TDeserializer>>,
186+
FallbackMetaSchema<InferDocumentMetaSchema<TDeserializer>>
187+
> =>
175188
z.object({
176189
data: createResourceSchema(deserializer).nullable(),
177190
links: rootLinksSchema.optional(),
178-
meta: defaultMetaSchema.optional(),
191+
meta: deserializer.documentMetaSchema ?? defaultMetaSchema.optional(),
179192
included: includedSchema.optional(),
180193
});
181194

182195
export const createResourceCollectionDocumentSchema = <
183196
TDeserializer extends AnyResourceDeserializer,
184197
>(
185198
deserializer: TDeserializer,
186-
): DocumentSchema<z.ZodArray<ResourceSchema<TDeserializer>>> =>
199+
): DocumentSchema<
200+
z.ZodArray<ResourceSchema<TDeserializer>>,
201+
FallbackMetaSchema<InferDocumentMetaSchema<TDeserializer>>
202+
> =>
187203
z.object({
188204
data: z.array(createResourceSchema(deserializer)),
189205
links: rootLinksSchema.optional(),
190-
meta: defaultMetaSchema.optional(),
206+
meta: deserializer.documentMetaSchema ?? defaultMetaSchema.optional(),
191207
included: includedSchema.optional(),
192208
});

src/selector.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { z } from "zod";
1+
import type { ZodTypeAny, z } from "zod";
22
import type {
33
AnyRelationshipDeserializer,
44
AnyResourceDeserializer,
@@ -28,7 +28,7 @@ type IncludedResource = {
2828
type ResourceSchemaCache = Map<string, ResourceSchema<AnyResourceDeserializer>>;
2929
type Included = Map<string, IncludedResource>;
3030

31-
const prepareIncludedMap = (document: z.output<DocumentSchema<z.ZodType<unknown>>>) =>
31+
const prepareIncludedMap = (document: z.output<DocumentSchema<z.ZodTypeAny, z.ZodTypeAny>>) =>
3232
new Map<string, IncludedResource>(
3333
document.included?.map((resource) => [
3434
`${resource.type}:::${resource.id}`,
@@ -151,10 +151,10 @@ const flattenResource = <TDeserializer extends AnyResourceDeserializer>(
151151

152152
type Selector<T> = (raw: unknown) => T;
153153

154-
const createFlattenedDocumentFromData = <TData>(
155-
result: z.output<DocumentSchema<z.ZodType<unknown>>>,
154+
const createFlattenedDocumentFromData = <TData, TMetaSchema extends ZodTypeAny>(
155+
result: z.output<DocumentSchema<z.ZodTypeAny, TMetaSchema>>,
156156
data: TData,
157-
): DocumentResult<TData> => {
157+
): DocumentResult<TData, z.output<TMetaSchema>> => {
158158
const document: Record<string, unknown> = {
159159
data,
160160
};
@@ -167,7 +167,7 @@ const createFlattenedDocumentFromData = <TData>(
167167
document.meta = result.meta;
168168
}
169169

170-
return document as DocumentResult<TData>;
170+
return document as DocumentResult<TData, z.output<TMetaSchema>>;
171171
};
172172

173173
export const createResourceSelector = <TDeserializer extends AnyResourceDeserializer>(
@@ -234,6 +234,6 @@ export const createResourceCollectionSelector = <TDeserializer extends AnyResour
234234
};
235235

236236
export const createDataSelector =
237-
<TData>(documentSelector: Selector<DocumentResult<TData>>): Selector<TData> =>
237+
<TData, TMeta>(documentSelector: Selector<DocumentResult<TData, TMeta>>): Selector<TData> =>
238238
(raw: unknown) =>
239239
documentSelector(raw).data;

src/standard-schemas.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { z } from "zod";
22

33
export const defaultMetaSchema = z.record(z.unknown());
4+
export type MetaSchema = z.ZodTypeAny;
45
export type DefaultMeta = z.output<typeof defaultMetaSchema>;
56

67
const linkObjectSchema = z.object({
@@ -30,4 +31,4 @@ export const rootLinksSchema = z.object({
3031
});
3132
export type RootLinks = z.output<typeof rootLinksSchema>;
3233

33-
export type AttributesSchema = z.AnyZodObject;
34+
export type AttributesSchema = z.ZodTypeAny;

test/selector.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,34 @@ describe("createResourceSelector", () => {
3131
});
3232
});
3333

34+
it("should allow specifying document meta schemas", () => {
35+
const selector = createResourceSelector({
36+
type: "article",
37+
documentMetaSchema: z.object({
38+
foo: z.string(),
39+
}),
40+
});
41+
42+
const result = selector({
43+
data: {
44+
id: "ID-p",
45+
type: "article",
46+
},
47+
meta: {
48+
foo: "bar",
49+
},
50+
});
51+
52+
expect(result).toEqual({
53+
data: {
54+
id: "ID-p",
55+
},
56+
meta: {
57+
foo: "bar",
58+
},
59+
});
60+
});
61+
3462
it("should parse identifier relationship", () => {
3563
const selector = createResourceSelector({
3664
type: "article",
@@ -216,7 +244,7 @@ describe("createResourceCollectionSelector", () => {
216244
});
217245
});
218246

219-
describe("crateDataSelector", () => {
247+
describe("createDataSelector", () => {
220248
it("should extract data", () => {
221249
const selector = createDataSelector(
222250
createResourceSelector({

0 commit comments

Comments
 (0)