From 62a97fda6a784f11549fff442978677642d2b218 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Wed, 20 Sep 2023 06:15:33 +0900 Subject: [PATCH] fix(zod-openapi): use `z.output` for types after validation (#164) * fix(zod-openapi): use `z.output` for types after validation * changeset --- .changeset/nasty-bikes-guess.md | 5 +++ packages/zod-openapi/package.json | 2 +- packages/zod-openapi/src/index.ts | 39 ++++++++++---------- packages/zod-openapi/test/index.test.ts | 49 ++++++++++++++++--------- yarn.lock | 5 +++ 5 files changed, 61 insertions(+), 39 deletions(-) create mode 100644 .changeset/nasty-bikes-guess.md diff --git a/.changeset/nasty-bikes-guess.md b/.changeset/nasty-bikes-guess.md new file mode 100644 index 00000000..b4c4bdad --- /dev/null +++ b/.changeset/nasty-bikes-guess.md @@ -0,0 +1,5 @@ +--- +'@hono/zod-openapi': patch +--- + +fix(zod-openapi): use `z.output` for types after validation diff --git a/packages/zod-openapi/package.json b/packages/zod-openapi/package.json index e2ea8028..7d5e0c38 100644 --- a/packages/zod-openapi/package.json +++ b/packages/zod-openapi/package.json @@ -31,7 +31,7 @@ "zod": "3.*" }, "devDependencies": { - "hono": "^3.5.8", + "hono": "^3.6.3", "zod": "^3.22.1" }, "dependencies": { diff --git a/packages/zod-openapi/src/index.ts b/packages/zod-openapi/src/index.ts index 69cc38f2..3cb357db 100644 --- a/packages/zod-openapi/src/index.ts +++ b/packages/zod-openapi/src/index.ts @@ -6,7 +6,11 @@ import type { ZodContentObject, ZodRequestBody, } from '@asteasolutions/zod-to-openapi' -import { OpenApiGeneratorV3, OpenApiGeneratorV31, OpenAPIRegistry } from '@asteasolutions/zod-to-openapi' +import { + OpenApiGeneratorV3, + OpenApiGeneratorV31, + OpenAPIRegistry, +} from '@asteasolutions/zod-to-openapi' import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi' import type { OpenAPIObjectConfig } from '@asteasolutions/zod-to-openapi/dist/v3.0/openapi-generator' import { zValidator } from '@hono/zod-validator' @@ -60,7 +64,7 @@ type InputTypeBase< ? RequestPart extends AnyZodObject ? { in: { [K in Type]: z.input> } - out: { [K in Type]: z.input> } + out: { [K in Type]: z.output> } } : {} : {} @@ -78,7 +82,7 @@ type InputTypeJson = R['request'] extends RequestTypes > } out: { - json: z.input< + json: z.output< R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] > } @@ -101,7 +105,7 @@ type InputTypeForm = R['request'] extends RequestTypes > } out: { - form: z.input< + form: z.output< R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] > } @@ -147,7 +151,7 @@ type ConvertPathType = T extends `${infer _}/{${infer Param}}$ type HandlerResponse = TypedResponse | Promise> -type HonoInit = ConstructorParameters[0]; +type HonoInit = ConstructorParameters[0] export class OpenAPIHono< E extends Env = Env, @@ -281,33 +285,26 @@ export class OpenAPIHono< app.openAPIRegistry.definitions.forEach((def) => { switch (def.type) { case 'component': - return this.openAPIRegistry.registerComponent( - def.componentType, - def.name, - def.component - ) - + return this.openAPIRegistry.registerComponent(def.componentType, def.name, def.component) + case 'route': return this.openAPIRegistry.registerPath({ ...def.route, - path: `${path}${def.route.path}` + path: `${path}${def.route.path}`, }) case 'webhook': return this.openAPIRegistry.registerWebhook({ ...def.webhook, - path: `${path}${def.webhook.path}` + path: `${path}${def.webhook.path}`, }) case 'schema': - return this.openAPIRegistry.register( - def.schema._def.openapi._internal.refId, - def.schema - ) + return this.openAPIRegistry.register(def.schema._def.openapi._internal.refId, def.schema) case 'parameter': return this.openAPIRegistry.registerParameter( - def.schema._def.openapi._internal.refId, + def.schema._def.openapi._internal.refId, def.schema ) @@ -323,7 +320,9 @@ export class OpenAPIHono< } } -type RoutingPath

= P extends `${infer Head}/{${infer Param}}${infer Tail}` ? `${Head}/:${Param}${RoutingPath}` : P +type RoutingPath

= P extends `${infer Head}/{${infer Param}}${infer Tail}` + ? `${Head}/:${Param}${RoutingPath}` + : P export const createRoute =

& { path: P }>( routeConfig: R @@ -332,7 +331,7 @@ export const createRoute =

{ return routeConfig.path.replaceAll(/\/{(.+?)}/g, '/:$1') as RoutingPath

- } + }, } } diff --git a/packages/zod-openapi/test/index.test.ts b/packages/zod-openapi/test/index.test.ts index f3ed9ef0..4777f508 100644 --- a/packages/zod-openapi/test/index.test.ts +++ b/packages/zod-openapi/test/index.test.ts @@ -11,8 +11,7 @@ describe('Constructor', () => { it('Should accept init object', () => { const getPath = () => '' - const app = new OpenAPIHono({getPath}) - + const app = new OpenAPIHono({ getPath }) expect(app.getPath).toBe(getPath) }) }) @@ -21,20 +20,31 @@ describe('Basic - params', () => { const ParamsSchema = z.object({ id: z .string() - .min(3) + .transform((val, ctx) => { + const parsed = parseInt(val) + if (isNaN(parsed)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Not a number', + }) + return z.NEVER + } + return parsed + }) .openapi({ param: { name: 'id', in: 'path', }, - example: '1212121', + example: 123, + type: 'integer', }), }) const UserSchema = z .object({ - id: z.string().openapi({ - example: '123', + id: z.number().openapi({ + example: 123, }), name: z.string().openapi({ example: 'John Doe', @@ -116,14 +126,14 @@ describe('Basic - params', () => { const res = await app.request('/users/123') expect(res.status).toBe(200) expect(await res.json()).toEqual({ - id: '123', + id: 123, age: 20, name: 'Ultra-man', }) }) it('Should return 400 response with correct contents', async () => { - const res = await app.request('/users/1') + const res = await app.request('/users/abc') expect(res.status).toBe(400) expect(await res.json()).toEqual({ ok: false }) }) @@ -139,7 +149,7 @@ describe('Basic - params', () => { User: { type: 'object', properties: { - id: { type: 'string', example: '123' }, + id: { type: 'number', example: 123 }, name: { type: 'string', example: 'John Doe' }, age: { type: 'number', example: 42 }, }, @@ -158,7 +168,7 @@ describe('Basic - params', () => { get: { parameters: [ { - schema: { type: 'string', minLength: 3, example: '1212121' }, + schema: { type: 'integer', example: 123 }, required: true, name: 'id', in: 'path', @@ -626,23 +636,26 @@ describe('Routers', () => { }) it('Should include definitions from nested routers', () => { const router = new OpenAPIHono().openapi(route, (ctx) => { - return ctx.jsonT({id: 123}) + return ctx.jsonT({ id: 123 }) }) router.openAPIRegistry.register('Id', z.number()) - router.openAPIRegistry.registerParameter('Key', z.number().openapi({ - param: {in: 'path'} - })) + router.openAPIRegistry.registerParameter( + 'Key', + z.number().openapi({ + param: { in: 'path' }, + }) + ) router.openAPIRegistry.registerWebhook({ method: 'post', path: '/postback', responses: { 200: { - description: 'Receives a post back' - } - } + description: 'Receives a post back', + }, + }, }) const app = new OpenAPIHono().route('/api', router) @@ -653,7 +666,7 @@ describe('Routers', () => { version: '1.0.0', }, }) - + expect(json.components?.schemas).toHaveProperty('Id') expect(json.components?.schemas).toHaveProperty('Post') expect(json.components?.parameters).toHaveProperty('Key') diff --git a/yarn.lock b/yarn.lock index 3949f173..8f5d4619 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5864,6 +5864,11 @@ hono@^3.5.8: resolved "https://registry.yarnpkg.com/hono/-/hono-3.5.8.tgz#9bbc412f5a54183cf2a81a36a9b9ea56da10f785" integrity sha512-ZipTmGfHm43q5QOEBGog2wyejyNUcicjPt0BLDQ8yz9xij/y9RYXRpR1YPxMpQqeyNM7isvpsIAe9Ems51Wq0Q== +hono@^3.6.3: + version "3.6.3" + resolved "https://registry.yarnpkg.com/hono/-/hono-3.6.3.tgz#0dab94a9e49dadc0f99bf8b8ffc70b223f53ab9f" + integrity sha512-8WszeHGzUm45qJy2JcCXkEFXMsAysciGGQs+fbpdUYPO2bRMbjJznZE3LX8tCXBqR4f/3e6225B3YOX6pQZWvA== + hosted-git-info@^2.1.4: version "2.8.9" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"