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/*"
|
||||
],
|
||||
"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,
|
||||
|
|
|
@ -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,
|
||||
"strictPropertyInitialization": true,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": true,
|
||||
"types": [
|
||||
"jest",
|
||||
|
|
10
yarn.lock
10
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==
|
||||
|
|
Loading…
Reference in New Issue