Skip to content

Commit

Permalink
1.0.0
Browse files Browse the repository at this point in the history
* Update dependencies
* Replace fastify-swagger with @fastify/swagger
* Add support for querystring

* Add separate tests for issues

* Fix elierotenberg#14, elierotenberg#17
  • Loading branch information
elierotenberg committed Jun 6, 2022
1 parent 2c0be0c commit 4602f15
Show file tree
Hide file tree
Showing 8 changed files with 344 additions and 224 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,22 @@ const models = {
};
```

- Merge `fastify` types (as recommended by `fastify`)
- Register `fastify` types

```ts
import type { FastifyZod } from "fastify-zod";

// Global augmentation, as suggested by
// https://www.fastify.io/docs/latest/Reference/TypeScript/#creating-a-typescript-fastify-plugin
declare module "fastify" {
interface FastifyInstance {
readonly zod: FastifyZod<typeof models>;
}
}

// Local augmentation
// See below for register()
const f = register(fastify(), { jsonSchemas });
```

- Register `fastify-zod` with optional config for `fastify-swagger`
Expand Down
349 changes: 157 additions & 192 deletions package-lock.json

Large diffs are not rendered by default.

13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fastify-zod",
"version": "1.0.0-rc10",
"version": "1.0.0",
"description": "Zod integration with Fastify",
"main": "build/index.js",
"scripts": {
Expand All @@ -12,7 +12,7 @@
"build:babel": "babel src --out-dir build --extensions '.ts' --source-maps",
"build:openapi-spec": "node build/__tests__/generate-spec.fixtures.js",
"build:openapi-client": "rm -rf test-openapi-client && openapi-generator-cli generate && cd test-openapi-client && npm i && cd .. && npm i file:./test-openapi-client --save-dev",
"build": "npm run clean && npm run build:types && npm run build:babel && npm run build:openapi-spec && npm run build:openapi-client",
"build": "npm run clean && npm run build:babel && npm run build:openapi-spec && npm run build:openapi-client && npm run build:types",
"test": "jest"
},
"repository": {
Expand Down Expand Up @@ -44,6 +44,7 @@
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.0.0",
"fastify": "^3.29.0",
"fastify-zod-test-openapi-client": "file:test-openapi-client",
"http-errors": "^2.0.0",
"jest": "^28.1.0",
Expand All @@ -52,19 +53,19 @@
"prettier": "^2.6.2",
"typescript": "^4.7.3"
},
"peerDependencies": {
"fastify": "^3.29.0"
},
"dependencies": {
"@fastify/swagger": "^6.1.0",
"@openapitools/openapi-generator-cli": "^2.5.1",
"@types/change-case": "^2.3.1",
"@types/js-yaml": "^4.0.5",
"change-case": "^4.1.2",
"fast-deep-equal": "^3.1.3",
"fastify": "^3.29.0",
"fastify-swagger": "^5.2.0",
"js-yaml": "^4.1.0",
"tslib": "^2.4.0",
"typed-jest-expect": "^1.0.0",
"zod": "^3.17.3",
"zod-to-json-schema": "^3.17.0"
}
}
}
43 changes: 33 additions & 10 deletions src/FastifyZod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,21 @@ type RouteHandlerParams<
M extends Models,
Params extends void | SchemaKey<M>,
Body extends void | SchemaKey<M>,
> = FastifyRequest & {
readonly params: SchemaTypeOption<M, Params>;
readonly body: SchemaTypeOption<M, Body>;
};
Querystring extends void | SchemaKey<M>,
> = FastifyRequest<{
Params: SchemaTypeOption<M, Params>;
Body: SchemaTypeOption<M, Body>;
Querystring: SchemaTypeOption<M, Querystring>;
}>;

type RouteHandler<
M extends Models,
Params extends void | SchemaKey<M>,
Body extends void | SchemaKey<M>,
Reply extends void | SchemaKey<M>,
Querystring extends void | SchemaKey<M>,
> = (
params: RouteHandlerParams<M, Params, Body>,
params: RouteHandlerParams<M, Params, Body, Querystring>,
) => Promise<SchemaTypeOption<M, Reply>>;

type RouteConfig<
Expand All @@ -50,6 +53,7 @@ type RouteConfig<
Params extends void | SchemaKey<M> = void,
Body extends void | SchemaKey<M> = void,
Reply extends void | SchemaKey<M> = void,
Querystring extends void | SchemaKey<M> = void,
> = {
readonly url: string;
readonly method: Method;
Expand All @@ -73,24 +77,35 @@ type RouteConfig<
readonly description: string;
readonly key: Exclude<Reply, void>;
};
readonly handler: RouteHandler<M, Params, Body, Reply>;
readonly querystring?:
| Exclude<Querystring, void>
| {
readonly description: string;
readonly key: Exclude<Querystring, void>;
};
readonly handler: RouteHandler<M, Params, Body, Reply, Querystring>;
} & FastifySchema;

export type FastifyZod<M extends Models> = {
readonly [Method in Lowercase<HTTPMethods>]: <
Params extends void | SchemaKey<M> = void,
Body extends void | SchemaKey<M> = void,
Reply extends void | SchemaKey<M> = void,
Querystring extends void | SchemaKey<M> = void,
>(
url: string,
config: Omit<
RouteConfig<M, Method, Params, Body, Reply>,
RouteConfig<M, Method, Params, Body, Reply, Querystring>,
`url` | `method` | `schema` | `handler`
>,
handler: RouteHandler<M, Params, Body, Reply>,
handler: RouteHandler<M, Params, Body, Reply, Querystring>,
) => void;
};

export interface FastifyZodInstance<M extends Models> extends FastifyInstance {
readonly zod: FastifyZod<M>;
}

export const withRefResolver = (
options: FastifyDynamicSwaggerOptions,
): FastifyDynamicSwaggerOptions => ({
Expand All @@ -106,7 +121,7 @@ export const withRefResolver = (
export const register = <S extends Models>(
f: FastifyInstance,
{ jsonSchemas: { schemas, $ref }, swaggerOptions }: RegisterOptions<S>,
): void => {
): FastifyZodInstance<S> => {
for (const schema of schemas) {
f.addSchema(schema);
}
Expand Down Expand Up @@ -185,20 +200,23 @@ export const register = <S extends Models>(
Params extends void | SchemaKey<S> = void,
Body extends void | SchemaKey<S> = void,
Reply extends void | SchemaKey<S> = void,
Querystring extends void | SchemaKey<S> = void,
>({
method,
url,
operationId,
params,
body,
reply,
querystring,
handler,
...fastifySchema
}: RouteConfig<S, M, Params, Body, Reply>): void => {
}: RouteConfig<S, M, Params, Body, Reply, Querystring>): void => {
f[method]<{
Params: SchemaTypeOption<S, Params>;
Body: SchemaTypeOption<S, Body>;
Reply: SchemaTypeOption<S, Reply>;
Querystring: SchemaTypeOption<S, Querystring>;
}>(
url,
{
Expand All @@ -208,6 +226,9 @@ export const register = <S extends Models>(
? $ref(params as SchemaKeyOrDescription<S>)
: undefined,
body: body ? $ref(body as SchemaKeyOrDescription<S>) : undefined,
querystring: querystring
? $ref(querystring as SchemaKeyOrDescription<S>)
: undefined,
response: reply
? {
200: $ref(reply as SchemaKeyOrDescription<S>),
Expand Down Expand Up @@ -236,4 +257,6 @@ export const register = <S extends Models>(
};

f.decorate(`zod`, pluginInstance);

return f as FastifyZodInstance<S>;
};
8 changes: 4 additions & 4 deletions src/JsonSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@ export type BuildJsonSchemasOptions = {
readonly target?: `jsonSchema7` | `openApi3`;
};

type $ref<M extends Models> = (key: SchemaKeyOrDescription<M>) => {
type $Ref<M extends Models> = (key: SchemaKeyOrDescription<M>) => {
readonly $ref: string;
readonly description?: string;
};

type JsonSchema = {
export type JsonSchema = {
readonly $id: string;
};

export type BuildJsonSchemasResult<M extends Models> = {
readonly schemas: JsonSchema[];
readonly $ref: $ref<M>;
readonly $ref: $Ref<M>;
};

/**
Expand Down Expand Up @@ -48,7 +48,7 @@ export const buildJsonSchemas = <M extends Models>(
...zodJsonSchema,
};

const $ref: $ref<M> = (key) => {
const $ref: $Ref<M> = (key) => {
const $ref = `${$id}#/properties/${
typeof key === `string` ? key : key.key
}`;
Expand Down
133 changes: 133 additions & 0 deletions src/__tests__/issues.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import fastify from "fastify";
import { z } from "zod";

import { buildJsonSchemas, register } from "..";

test(`fix #8`, () => {
const productInput = {
title: z.string(),
price: z.number(),
content: z.string().optional(),
};

const productGenerated = {
id: z.number(),
createdAt: z.string(),
updatedAt: z.string(),
};

const createProductSchema = z.object({
...productInput,
});

const productResponseSchema = z.object({
...productInput,
...productGenerated,
});

const productsResponseSchema = z.array(productResponseSchema);

buildJsonSchemas({
createProductSchema,
productResponseSchema,
productsResponseSchema,
});

const userCoreSchema = {
email: z
.string({
required_error: `Email is required`,
invalid_type_error: `Email must be a string`,
})
.email(),
name: z.string(),
};

const createUserSchema = z.object({
...userCoreSchema,
password: z.string({
required_error: `Password is required`,
invalid_type_error: `Password must be a string`,
}),
});

const createUserResponseSchema = z.object({
...userCoreSchema,
id: z.number(),
});

const loginSchema = z.object({
email: z
.string({
required_error: `Email is required`,
invalid_type_error: `Email must be a string`,
})
.email(),
password: z.string(),
});

const loginResponseSchema = z.object({
accessToken: z.string(),
});

buildJsonSchemas({
createUserSchema,
createUserResponseSchema,
loginSchema,
loginResponseSchema,
});
});

test(`fix #14, #17`, async () => {
const Name = z.object({
kind: z.literal(`name`),
name: z.string(),
lastName: z.string(),
});

const Address = z.object({
kind: z.literal(`address`),
street: z.string(),
postcode: z.string(),
});

const UserDetails = z.union([Name, Address]);

const Unknown = z.unknown();

const jsonSchemas = buildJsonSchemas({ UserDetails, Unknown }, {});

const f = register(fastify(), {
jsonSchemas,
});

f.zod.get(
`/`,
{
operationId: `getUserDetails`,
querystring: `UserDetails`,
reply: `UserDetails`,
},
async ({ query }) => query,
);

const name = await f
.inject({ method: `get`, url: `/`, query: { kind: `name` } })
.then((res) => res.json());

expect(name).toEqual({
error: `Bad Request`,
message: `querystring should have required property 'name', querystring.kind should be equal to constant, querystring should match some schema in anyOf`,
statusCode: 400,
});

const address = await f
.inject({ method: `get`, url: `/`, query: { kind: `address` } })
.then((res) => res.json());

expect(address).toEqual({
error: `Bad Request`,
message: `querystring.kind should be equal to constant, querystring should have required property 'street', querystring should match some schema in anyOf`,
statusCode: 400,
});
});
13 changes: 2 additions & 11 deletions src/__tests__/server.fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,10 @@ import fastify, { FastifyInstance, FastifyServerOptions } from "fastify";
import { BadRequest, NotFound } from "http-errors";

import { register } from "..";
import { FastifyZod, RegisterOptions } from "../FastifyZod";
import { RegisterOptions } from "../FastifyZod";

import { models, TodoItems } from "./models.fixtures";

// eslint-disable-next-line quotes
declare module "fastify" {
interface FastifyInstance {
readonly zod: FastifyZod<typeof models>;
}
}

export const swaggerOptions: RegisterOptions<typeof models>[`swaggerOptions`] =
{
routePrefix: `/swagger`,
Expand Down Expand Up @@ -44,9 +37,7 @@ export const createTestServer = (
fastifyOptions: FastifyServerOptions,
registerOptions: RegisterOptions<typeof models>,
): FastifyInstance => {
const f = fastify(fastifyOptions);

register(f, registerOptions);
const f = register(fastify(fastifyOptions), registerOptions);

const state: TodoItems = {
todoItems: [],
Expand Down
Loading

0 comments on commit 4602f15

Please sign in to comment.