diff --git a/.changeset/twenty-starfishes-cover.md b/.changeset/twenty-starfishes-cover.md new file mode 100644 index 00000000..58bad5c6 --- /dev/null +++ b/.changeset/twenty-starfishes-cover.md @@ -0,0 +1,5 @@ +--- +'@hono/zod-validator': patch +--- + +first release diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..e1ce1aa9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact" + ], + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + } +} \ No newline at end of file diff --git a/package.json b/package.json index 57933649..0eba8649 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "packages/*" ], "scripts": { - "build": "yarn workspace @hono/hello build" + "build:hello": "yarn workspace @hono/hello build", + "build:zod-validator": "yarn workspace @hono/zod-validator build", + "build": "yarn build:hello && yarn:zod-validator" }, "license": "MIT", "private": true, diff --git a/packages/zod-validator/README.md b/packages/zod-validator/README.md new file mode 100644 index 00000000..e974e171 --- /dev/null +++ b/packages/zod-validator/README.md @@ -0,0 +1,34 @@ +# Zod validator middleware for Hono + +**WIP** + +The validator middleware using [Zod](https://zod.dev) for [Hono](https://honojs.dev) applications. +You can write a schema with Zod and validate the incoming values. + +## Usage + +```ts +import { z } from 'zod' +import { zValidator } from '../src' + +const schema = z.object({ + name: z.string(), + age: z.number(), +}) + +app.post('/author', zValidator('json', schema), (c) => { + const data = c.req.valid() + return c.json({ + success: true, + message: `${data.name} is ${data.age}`, + }) +}) +``` + +## Author + +Yusuke Wada + +## License + +MIT diff --git a/packages/zod-validator/jest.config.js b/packages/zod-validator/jest.config.js new file mode 100644 index 00000000..f697d831 --- /dev/null +++ b/packages/zod-validator/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../jest.config.js') diff --git a/packages/zod-validator/package.json b/packages/zod-validator/package.json new file mode 100644 index 00000000..de98b10f --- /dev/null +++ b/packages/zod-validator/package.json @@ -0,0 +1,38 @@ +{ + "name": "@hono/zod-validator", + "version": "0.0.0", + "description": "Validator middleware using Zod", + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "types": "dist/esm/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "test": "jest", + "build:cjs": "tsc -p tsconfig.cjs.json", + "build:esm": "tsc -p tsconfig.esm.json", + "build": "rimraf dist && yarn build:cjs && yarn build:esm", + "prerelease": "yarn build && yarn test", + "release": "yarn publish" + }, + "license": "MIT", + "private": false, + "publishConfig": { + "registry": "https://registry.npmjs.org", + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/honojs/middleware.git" + }, + "homepage": "https://github.com/honojs/middleware", + "peerDependencies": { + "hono": "^3.0.0-rc.3", + "zod": "^3.19.1" + }, + "devDependencies": { + "hono": "3.0.0-rc.3", + "zod": "3.19.1" + } +} \ No newline at end of file diff --git a/packages/zod-validator/src/index.ts b/packages/zod-validator/src/index.ts new file mode 100644 index 00000000..38323e07 --- /dev/null +++ b/packages/zod-validator/src/index.ts @@ -0,0 +1,33 @@ +import type { Context } from 'hono' +import { validator } from 'hono/validator' +import type { z } from 'zod' +import type { ZodSchema, ZodError } from 'zod' + +type ValidationTypes = 'json' | 'form' | 'query' | 'queries' +type Hook = ( + result: { success: true; data: T } | { success: false; error: ZodError }, + c: Context +) => Response | Promise | void + +export const zValidator = ( + type: Type, + schema: T, + hook?: Hook> +) => + validator(type, (value, c) => { + const result = schema.safeParse(value) + + if (hook) { + const hookResult = hook(result, c) + if (hookResult instanceof Response || hookResult instanceof Promise) { + return hookResult + } + } + + if (!result.success) { + return c.json(result, 400) + } + + const data = result.data as z.infer + return data + }) diff --git a/packages/zod-validator/test/index.test.ts b/packages/zod-validator/test/index.test.ts new file mode 100644 index 00000000..b89b558c --- /dev/null +++ b/packages/zod-validator/test/index.test.ts @@ -0,0 +1,132 @@ +import { Hono } from 'hono' +import type { Equal, Expect } from 'hono/utils/types' +import { z } from 'zod' +import { zValidator } from '../src' + +describe('Basic', () => { + const app = new Hono() + + const schema = z.object({ + name: z.string(), + age: z.number(), + }) + + const route = app + .post('/author', zValidator('json', schema), (c) => { + const data = c.req.valid() + return c.jsonT({ + success: true, + message: `${data.name} is ${data.age}`, + }) + }) + .build() + + type Actual = typeof route + type Expected = { + post: { + '/author': { + input: { + json: { + name: string + age: number + } + } + output: { + json: { + success: boolean + message: string + } + } + } + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type verify = Expect> + + it('Should return 200 response', async () => { + const req = new Request('http://localhost/author', { + body: JSON.stringify({ + name: 'Superman', + age: 20, + }), + method: 'POST', + }) + const res = await app.request(req) + expect(res).not.toBeNull() + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ + success: true, + message: 'Superman is 20', + }) + }) + + it('Should return 400 response', async () => { + const req = new Request('http://localhost/author', { + body: JSON.stringify({ + name: 'Superman', + age: '20', + }), + method: 'POST', + }) + const res = await app.request(req) + expect(res).not.toBeNull() + expect(res.status).toBe(400) + const data = (await res.json()) as { success: boolean } + expect(data['success']).toBe(false) + }) +}) + +describe('With Hook', () => { + const app = new Hono() + + const schema = z.object({ + id: z.number(), + title: z.string(), + }) + + app.post( + '/post', + zValidator('json', schema, (result, c) => { + if (!result.success) { + return c.text('Invalid!', 400) + } + const data = result.data + return c.text(`${data.id} is valid!`) + }), + (c) => { + const data = c.req.valid() + return c.json({ + success: true, + message: `${data.id} is ${data.title}`, + }) + } + ) + + it('Should return 200 response', async () => { + const req = new Request('http://localhost/post', { + body: JSON.stringify({ + id: 123, + title: 'Hello', + }), + method: 'POST', + }) + const res = await app.request(req) + expect(res).not.toBeNull() + expect(res.status).toBe(200) + expect(await res.text()).toBe('123 is valid!') + }) + + it('Should return 400 response', async () => { + const req = new Request('http://localhost/post', { + body: JSON.stringify({ + id: '123', + title: 'Hello', + }), + method: 'POST', + }) + const res = await app.request(req) + expect(res).not.toBeNull() + expect(res.status).toBe(400) + }) +}) diff --git a/packages/zod-validator/tsconfig.cjs.json b/packages/zod-validator/tsconfig.cjs.json new file mode 100644 index 00000000..b8bf50ee --- /dev/null +++ b/packages/zod-validator/tsconfig.cjs.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "declaration": false, + "outDir": "./dist/cjs" + } +} \ No newline at end of file diff --git a/packages/zod-validator/tsconfig.esm.json b/packages/zod-validator/tsconfig.esm.json new file mode 100644 index 00000000..8130f1a5 --- /dev/null +++ b/packages/zod-validator/tsconfig.esm.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "ESNext", + "declaration": true, + "outDir": "./dist/esm" + } +} \ No newline at end of file diff --git a/packages/zod-validator/tsconfig.json b/packages/zod-validator/tsconfig.json new file mode 100644 index 00000000..6c1a3990 --- /dev/null +++ b/packages/zod-validator/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + }, + "include": [ + "src/**/*.ts" + ], +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 06a70f01..860f26e9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,7 @@ "skipLibCheck": true, "strictPropertyInitialization": true, "strictNullChecks": true, - "noUnusedLocals": true, + "noUnusedLocals": false, "noUnusedParameters": true, "types": [ "jest", diff --git a/yarn.lock b/yarn.lock index a3551634..1e067f2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2550,6 +2550,11 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hono@3.0.0-rc.3: + version "3.0.0-rc.3" + resolved "https://registry.yarnpkg.com/hono/-/hono-3.0.0-rc.3.tgz#8f4cfc03816114bc3541c1802f0c65e0730327e8" + integrity sha512-j5GLwfTb9smHcneQ8hm1I7bwnLGxKHAZYuCOZuq+CGpEg/SrWW0niIiuhLE4FIIFAfeL3vUsjy0BzOWS6Hur0w== + hono@^2.3.0, hono@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/hono/-/hono-2.6.2.tgz#1e32a4b4b3bf557a8aa87e392b00b505a1ec1fdd" @@ -4723,3 +4728,8 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zod@3.19.1: + version "3.19.1" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.19.1.tgz#112f074a97b50bfc4772d4ad1576814bd8ac4473" + integrity sha512-LYjZsEDhCdYET9ikFu6dVPGp2YH9DegXjdJToSzD9rO6fy4qiRYFoyEYwps88OseJlPyl2NOe2iJuhEhL7IpEA==