feat: introduce zod-validator (#16)
* feat: introduce zod-validator * fixed `package.json` * changesetpull/18/head
parent
80592fe7db
commit
58e42aa8da
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@hono/zod-validator': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
first release
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"javascriptreact",
|
||||||
|
"typescript",
|
||||||
|
"typescriptreact"
|
||||||
|
],
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,9 @@
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"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",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|
|
@ -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 <https://github.com/yusukebe>
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require('../../jest.config.js')
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<T> = (
|
||||||
|
result: { success: true; data: T } | { success: false; error: ZodError },
|
||||||
|
c: Context
|
||||||
|
) => Response | Promise<Response> | void
|
||||||
|
|
||||||
|
export const zValidator = <T extends ZodSchema, Type extends ValidationTypes>(
|
||||||
|
type: Type,
|
||||||
|
schema: T,
|
||||||
|
hook?: Hook<z.infer<T>>
|
||||||
|
) =>
|
||||||
|
validator(type, (value, c) => {
|
||||||
|
const result = schema.safeParse(value)
|
||||||
|
|
||||||
|
if (hook) {
|
||||||
|
const hookResult = hook(result, c)
|
||||||
|
if (hookResult instanceof Response || hookResult instanceof Promise<Response>) {
|
||||||
|
return hookResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return c.json(result, 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = result.data as z.infer<T>
|
||||||
|
return data
|
||||||
|
})
|
|
@ -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<Equal<Actual, Expected>>
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "CommonJS",
|
||||||
|
"declaration": false,
|
||||||
|
"outDir": "./dist/cjs"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "ESNext",
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "./dist/esm"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "./src",
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts"
|
||||||
|
],
|
||||||
|
}
|
|
@ -10,7 +10,7 @@
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strictPropertyInitialization": true,
|
"strictPropertyInitialization": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"types": [
|
"types": [
|
||||||
"jest",
|
"jest",
|
||||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -2550,6 +2550,11 @@ has@^1.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind "^1.1.1"
|
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:
|
hono@^2.3.0, hono@^2.6.2:
|
||||||
version "2.6.2"
|
version "2.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/hono/-/hono-2.6.2.tgz#1e32a4b4b3bf557a8aa87e392b00b505a1ec1fdd"
|
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"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
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==
|
||||||
|
|
Loading…
Reference in New Issue