From 2dcdeb4d8807070d1767b5b080efeefb2762fa7d Mon Sep 17 00:00:00 2001 From: Sam Chung Date: Tue, 17 Dec 2024 10:53:20 +1100 Subject: [PATCH] Support ZodEffects (#211) --- package.json | 4 +- pnpm-lock.yaml | 10 +-- src/transformer.test.ts | 144 ++++++++++++++++++++++++++++++++++++++++ src/transformer.ts | 38 +++++++---- 4 files changed, 175 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 5515df2..dededb6 100644 --- a/package.json +++ b/package.json @@ -66,14 +66,14 @@ "fastify": "5.2.0", "skuba": "9.1.0", "zod": "3.24.1", - "zod-openapi": "4.1.0" + "zod-openapi": "4.2.0" }, "peerDependencies": { "@fastify/swagger": "^9.0.0", "@fastify/swagger-ui": "^5.0.1", "fastify": "5", "zod": "^3.21.4", - "zod-openapi": ">=3.2.0 <5.0.0" + "zod-openapi": "^4.2.0" }, "peerDependenciesMeta": { "@fastify/swagger": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 223e992..9e21066 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,8 +43,8 @@ importers: specifier: 3.24.1 version: 3.24.1 zod-openapi: - specifier: 4.1.0 - version: 4.1.0(zod@3.24.1) + specifier: 4.2.0 + version: 4.2.0(zod@3.24.1) packages: @@ -5519,8 +5519,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zod-openapi@4.1.0: - resolution: {integrity: sha512-bRCwRYhEO9CmFLyKgJX8h6j1dRtRiwOe+TLzMVPyV0pRW5vRIgb1rLgIGcuRZ5z3MmSVrZqbv3yva4IJrtZK4g==} + zod-openapi@4.2.0: + resolution: {integrity: sha512-lnm1e2X5/Ymox5mYxdspy/y52Hb4rPxG1bcwOuHS+YIpxQNEt9s8G4zo34jm0M3+q71TAxgqzcbPLiBXrsfsGA==} engines: {node: '>=18'} peerDependencies: zod: ^3.21.4 @@ -11803,7 +11803,7 @@ snapshots: yocto-queue@0.1.0: {} - zod-openapi@4.1.0(zod@3.24.1): + zod-openapi@4.2.0(zod@3.24.1): dependencies: zod: 3.24.1 diff --git a/src/transformer.test.ts b/src/transformer.test.ts index 1cecd10..9adf910 100644 --- a/src/transformer.test.ts +++ b/src/transformer.test.ts @@ -435,6 +435,150 @@ describe('fastifyZodOpenApiTransform', () => { `); }); + it('should support creating parameters using Zod Effects', async () => { + const app = fastify(); + + app.setValidatorCompiler(validatorCompiler); + + await app.register(fastifyZodOpenApiPlugin); + await app.register(fastifySwagger, { + openapi: { + info: { + title: 'hello world', + version: '1.0.0', + }, + openapi: '3.1.0', + }, + transform: fastifyZodOpenApiTransform, + }); + await app.register(fastifySwaggerUI, { + routePrefix: '/documentation', + }); + + app.withTypeProvider().post( + '/', + { + schema: { + body: z + .object({ + jobId: z.string().openapi({ + description: 'Job ID', + example: '60002023', + }), + }) + .refine(() => true), + querystring: z + .object({ + jobId: z.string().openapi({ + description: 'Job ID', + example: '60002023', + }), + }) + .refine(() => true), + params: z + .object({ + jobId: z.string().openapi({ + description: 'Job ID', + example: '60002023', + }), + }) + .refine(() => true), + headers: z + .object({ + jobId: z.string().openapi({ + description: 'Job ID', + example: '60002023', + }), + }) + .refine(() => true), + } satisfies FastifyZodOpenApiSchema, + }, + async (_req, res) => + res.send({ + jobId: '60002023', + }), + ); + await app.ready(); + + const result = await app.inject().get('/documentation/json'); + + expect(result.json()).toMatchInlineSnapshot(` +{ + "components": { + "schemas": {}, + }, + "info": { + "title": "hello world", + "version": "1.0.0", + }, + "openapi": "3.1.0", + "paths": { + "/": { + "post": { + "parameters": [ + { + "description": "Job ID", + "in": "query", + "name": "jobId", + "required": true, + "schema": { + "example": "60002023", + "type": "string", + }, + }, + { + "description": "Job ID", + "in": "path", + "name": "jobId", + "required": true, + "schema": { + "example": "60002023", + "type": "string", + }, + }, + { + "description": "Job ID", + "in": "header", + "name": "jobId", + "required": true, + "schema": { + "example": "60002023", + "type": "string", + }, + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "jobId": { + "description": "Job ID", + "example": "60002023", + "type": "string", + }, + }, + "required": [ + "jobId", + ], + "type": "object", + }, + }, + }, + "required": true, + }, + "responses": { + "200": { + "description": "Default Response", + }, + }, + }, + }, + }, +} +`); + }); + it('should support creating an openapi header parameter', async () => { const app = fastify(); diff --git a/src/transformer.ts b/src/transformer.ts index 3bb24e8..301298a 100644 --- a/src/transformer.ts +++ b/src/transformer.ts @@ -1,9 +1,10 @@ import type { FastifyDynamicSwaggerOptions } from '@fastify/swagger'; import type { FastifySchema } from 'fastify'; import type { OpenAPIV3 } from 'openapi-types'; -import type { AnyZodObject, ZodObject, ZodRawShape, ZodType } from 'zod'; +import type { ZodObject, ZodRawShape, ZodType } from 'zod'; import type { CreateDocumentOptions, + ZodObjectInputType, ZodOpenApiComponentsObject, ZodOpenApiParameters, ZodOpenApiResponsesObject, @@ -15,6 +16,7 @@ import { createComponents, createMediaTypeSchema, createParamOrRef, + getZodObject, } from 'zod-openapi/api'; import { @@ -37,10 +39,10 @@ export type FastifyZodOpenApiSchema = Omit< 'response' | 'headers' | 'querystring' | 'body' | 'params' > & { response?: ZodOpenApiResponsesObject; - headers?: AnyZodObject; - querystring?: AnyZodObject; - body?: AnyZodObject; - params?: AnyZodObject; + headers?: ZodObjectInputType; + querystring?: ZodObjectInputType; + body?: ZodObjectInputType; + params?: ZodObjectInputType; }; export const isZodType = (object: unknown): object is ZodType => @@ -244,9 +246,13 @@ export const fastifyZodOpenApiTransform: Transform = ({ transformedSchema.response = maybeResponse; } - if (isZodObject(querystring)) { + if (isZodType(querystring)) { + const queryStringSchema = getZodObject( + querystring as ZodObjectInputType, + 'input', + ); transformedSchema.querystring = createParams( - querystring, + queryStringSchema, 'query', components, [url, 'querystring'], @@ -254,18 +260,22 @@ export const fastifyZodOpenApiTransform: Transform = ({ ); } - if (isZodObject(params)) { - transformedSchema.params = createParams(params, 'path', components, [ + if (isZodType(params)) { + const paramsSchema = getZodObject(params as ZodObjectInputType, 'input'); + transformedSchema.params = createParams(paramsSchema, 'path', components, [ url, 'params', ]); } - if (isZodObject(headers)) { - transformedSchema.headers = createParams(headers, 'header', components, [ - url, - 'headers', - ]); + if (isZodType(headers)) { + const headersSchema = getZodObject(headers as ZodObjectInputType, 'input'); + transformedSchema.headers = createParams( + headersSchema, + 'header', + components, + [url, 'headers'], + ); } return {