Skip to content

Commit

Permalink
feat: allow specifying document meta schema
Browse files Browse the repository at this point in the history
  • Loading branch information
DASPRiD committed Apr 22, 2024
1 parent de7e9aa commit 33f7152
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 28 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ import { createDataSelector, createResourceSelector } from "jsonapi-zod-query";
const articleSelector = createDataSelector(createResourceSelector(/**/));
```

### Typing document meta data

By default, document metadata are considered as an optional record of unknown properties. You can pass a
`documentMetaSchema` option to resource selector creators, which will enforce a specific schema.

### Handling pagination

This library assumes that you never actually use the `links` properties in the JSON:API documents, but are primarily
Expand Down
54 changes: 45 additions & 9 deletions src/deserializer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import type { z } from "zod";
import type { PageParams } from "./pagination.ts";
import type { AttributesSchema, DefaultLinks, DefaultMeta, RootLinks } from "./standard-schemas.ts";
import type { defaultMetaSchema } from "./standard-schemas.ts";
import type {
AttributesSchema,
DefaultLinks,
DefaultMeta,
MetaSchema,
RootLinks,
} from "./standard-schemas.ts";

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

Expand Down Expand Up @@ -41,16 +48,19 @@ export type ResourceDeserializer<
TType extends string,
TAttributesSchema extends AttributesSchema | undefined,
TRelationships extends Relationships | undefined,
TDocumentMetaSchema extends MetaSchema | undefined,
> = {
type: TType;
attributesSchema?: TAttributesSchema;
relationships?: TRelationships;
documentMetaSchema?: TDocumentMetaSchema;
};

export type AnyResourceDeserializer = ResourceDeserializer<
string,
AttributesSchema | undefined,
Relationships | undefined
Relationships | undefined,
MetaSchema | undefined
>;

export type InferResourceType<T> = T extends ReferenceRelationshipDeserializer<
Expand All @@ -74,20 +84,31 @@ export type InferInclude<T> = T extends IncludedRelationshipDeserializer<Relatio
export type InferType<T> = T extends ResourceDeserializer<
infer U,
AttributesSchema | undefined,
Relationships | undefined
Relationships | undefined,
MetaSchema | undefined
>
? U
: never;
export type InferAttributesSchema<T> = T extends ResourceDeserializer<
string,
infer U,
Relationships | undefined
Relationships | undefined,
MetaSchema | undefined
>
? U
: never;
export type InferRelationships<T> = T extends ResourceDeserializer<
string,
AttributesSchema | undefined,
infer U,
MetaSchema | undefined
>
? U
: never;
export type InferDocumentMetaSchema<T> = T extends ResourceDeserializer<
string,
AttributesSchema | undefined,
Relationships | undefined,
infer U
>
? U
Expand Down Expand Up @@ -137,18 +158,33 @@ export type ResourceResult<
TRelationships
>;

export type DocumentResult<TData> = {
export type FallbackMetaSchema<T extends MetaSchema> = T extends MetaSchema
? MetaSchema
: z.ZodOptional<typeof defaultMetaSchema>;

type MetaResult<TMeta extends MetaSchema | undefined> = TMeta extends MetaSchema
? z.output<TMeta>
: DefaultMeta | undefined;

export type DocumentResult<TData, TMeta> = {
data: TData;
links?: RootLinks;
meta?: DefaultMeta;
meta: TMeta;
};
export type ResourceDocumentResult<TDeserializer extends AnyResourceDeserializer> = DocumentResult<
ResourceResult<TDeserializer>
ResourceResult<TDeserializer>,
MetaResult<InferDocumentMetaSchema<TDeserializer>>
>;
export type NullableResourceDocumentResult<TDeserializer extends AnyResourceDeserializer> =
DocumentResult<ResourceResult<TDeserializer> | null>;
DocumentResult<
ResourceResult<TDeserializer> | null,
MetaResult<InferDocumentMetaSchema<TDeserializer>>
>;
export type ResourceCollectionDocumentResult<TDeserializer extends AnyResourceDeserializer> =
DocumentResult<ResourceResult<TDeserializer>[]> & {
DocumentResult<
ResourceResult<TDeserializer>[],
MetaResult<InferDocumentMetaSchema<TDeserializer>>
> & {
pageParams: {
first?: PageParams;
prev?: PageParams;
Expand Down
36 changes: 26 additions & 10 deletions src/parser-schemas.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from "zod";
import type { FallbackMetaSchema, InferDocumentMetaSchema } from "./deserializer.ts";
import type {
AnyRelationshipDeserializer,
AnyResourceDeserializer,
Expand Down Expand Up @@ -55,7 +56,7 @@ const createRelationshipSchema = <TDeserializer extends AnyRelationshipDeseriali
meta: defaultMetaSchema.optional(),
});

let dataSchema: z.ZodType<unknown>;
let dataSchema: z.ZodTypeAny;

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

type BaseDocumentOutput = {
links?: RootLinks;
meta?: DefaultMeta;
included?: z.output<typeof includedSchema>;
};

export type DocumentSchema<TDataSchema extends z.ZodType<unknown>> = z.ZodType<
export type DocumentSchema<
TDataSchema extends z.ZodTypeAny,
TMetaSchema extends z.ZodTypeAny,
> = z.ZodType<
BaseDocumentOutput & {
data: z.output<TDataSchema>;
}
} & (z.output<TMetaSchema> extends undefined
? {
meta?: z.output<TMetaSchema>;
}
: { meta: z.output<TMetaSchema> })
>;

export const createResourceDocumentSchema = <TDeserializer extends AnyResourceDeserializer>(
deserializer: TDeserializer,
): DocumentSchema<ResourceSchema<TDeserializer>> =>
): DocumentSchema<
ResourceSchema<TDeserializer>,
FallbackMetaSchema<InferDocumentMetaSchema<TDeserializer>>
> =>
z.object({
data: createResourceSchema(deserializer),
links: rootLinksSchema.optional(),
meta: defaultMetaSchema.optional(),
meta: deserializer.documentMetaSchema ?? defaultMetaSchema.optional(),
included: includedSchema.optional(),
});

export const createNullableResourceDocumentSchema = <TDeserializer extends AnyResourceDeserializer>(
deserializer: TDeserializer,
): DocumentSchema<z.ZodNullable<ResourceSchema<TDeserializer>>> =>
): DocumentSchema<
z.ZodNullable<ResourceSchema<TDeserializer>>,
FallbackMetaSchema<InferDocumentMetaSchema<TDeserializer>>
> =>
z.object({
data: createResourceSchema(deserializer).nullable(),
links: rootLinksSchema.optional(),
meta: defaultMetaSchema.optional(),
meta: deserializer.documentMetaSchema ?? defaultMetaSchema.optional(),
included: includedSchema.optional(),
});

export const createResourceCollectionDocumentSchema = <
TDeserializer extends AnyResourceDeserializer,
>(
deserializer: TDeserializer,
): DocumentSchema<z.ZodArray<ResourceSchema<TDeserializer>>> =>
): DocumentSchema<
z.ZodArray<ResourceSchema<TDeserializer>>,
FallbackMetaSchema<InferDocumentMetaSchema<TDeserializer>>
> =>
z.object({
data: z.array(createResourceSchema(deserializer)),
links: rootLinksSchema.optional(),
meta: defaultMetaSchema.optional(),
meta: deserializer.documentMetaSchema ?? defaultMetaSchema.optional(),
included: includedSchema.optional(),
});
14 changes: 7 additions & 7 deletions src/selector.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { z } from "zod";
import type { ZodTypeAny, z } from "zod";
import type {
AnyRelationshipDeserializer,
AnyResourceDeserializer,
Expand Down Expand Up @@ -28,7 +28,7 @@ type IncludedResource = {
type ResourceSchemaCache = Map<string, ResourceSchema<AnyResourceDeserializer>>;
type Included = Map<string, IncludedResource>;

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

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

const createFlattenedDocumentFromData = <TData>(
result: z.output<DocumentSchema<z.ZodType<unknown>>>,
const createFlattenedDocumentFromData = <TData, TMetaSchema extends ZodTypeAny>(
result: z.output<DocumentSchema<z.ZodTypeAny, TMetaSchema>>,
data: TData,
): DocumentResult<TData> => {
): DocumentResult<TData, z.output<TMetaSchema>> => {
const document: Record<string, unknown> = {
data,
};
Expand All @@ -167,7 +167,7 @@ const createFlattenedDocumentFromData = <TData>(
document.meta = result.meta;
}

return document as DocumentResult<TData>;
return document as DocumentResult<TData, z.output<TMetaSchema>>;
};

export const createResourceSelector = <TDeserializer extends AnyResourceDeserializer>(
Expand Down Expand Up @@ -234,6 +234,6 @@ export const createResourceCollectionSelector = <TDeserializer extends AnyResour
};

export const createDataSelector =
<TData>(documentSelector: Selector<DocumentResult<TData>>): Selector<TData> =>
<TData, TMeta>(documentSelector: Selector<DocumentResult<TData, TMeta>>): Selector<TData> =>
(raw: unknown) =>
documentSelector(raw).data;
3 changes: 2 additions & 1 deletion src/standard-schemas.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { z } from "zod";

export const defaultMetaSchema = z.record(z.unknown());
export type MetaSchema = z.ZodTypeAny;
export type DefaultMeta = z.output<typeof defaultMetaSchema>;

const linkObjectSchema = z.object({
Expand Down Expand Up @@ -30,4 +31,4 @@ export const rootLinksSchema = z.object({
});
export type RootLinks = z.output<typeof rootLinksSchema>;

export type AttributesSchema = z.AnyZodObject;
export type AttributesSchema = z.ZodTypeAny;
30 changes: 29 additions & 1 deletion test/selector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,34 @@ describe("createResourceSelector", () => {
});
});

it("should allow specifying document meta schemas", () => {
const selector = createResourceSelector({
type: "article",
documentMetaSchema: z.object({
foo: z.string(),
}),
});

const result = selector({
data: {
id: "ID-p",
type: "article",
},
meta: {
foo: "bar",
},
});

expect(result).toEqual({
data: {
id: "ID-p",
},
meta: {
foo: "bar",
},
});
});

it("should parse identifier relationship", () => {
const selector = createResourceSelector({
type: "article",
Expand Down Expand Up @@ -216,7 +244,7 @@ describe("createResourceCollectionSelector", () => {
});
});

describe("crateDataSelector", () => {
describe("createDataSelector", () => {
it("should extract data", () => {
const selector = createDataSelector(
createResourceSelector({
Expand Down

0 comments on commit 33f7152

Please sign in to comment.