feat(valibot-validator): Add Valibot Validator Middleware (#102)

* feat: add valibot validator

* add changeset
pull/103/head
Nico Franke 2023-07-30 01:07:44 +02:00 committed by GitHub
parent 19f49fdfcc
commit c15de7ce3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 321 additions and 0 deletions

View File

@ -0,0 +1,5 @@
---
'@hono/valibot-validator': minor
---
first release

View File

@ -0,0 +1,25 @@
name: ci-valibot-validator
on:
push:
branches: [main]
paths:
- 'packages/valibot-validator/**'
pull_request:
branches: ['*']
paths:
- 'packages/valibot-validator/**'
jobs:
ci:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./packages/valibot-validator
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 18.x
- run: yarn install --frozen-lockfile
- run: yarn build
- run: yarn test

View File

@ -15,6 +15,7 @@
"build:trpc-server": "yarn workspace @hono/trpc-server build",
"build:typebox-validator": "yarn workspace @hono/typebox-validator build",
"build:medley-router": "yarn workspace @hono/medley-router build",
"build:valibot-validator": "yarn workspace @hono/valibot-validator build",
"build": "run-p build:*"
},
"license": "MIT",

View File

@ -0,0 +1,46 @@
# Valibot validator middleware for Hono
The validator middleware using [Valibot](https://valibot.dev) for [Hono](https://honojs.dev) applications.
You can write a schema with Valibot and validate the incoming values.
## Usage
```ts
import { number, object, string } from 'valibot'
import { vValidator } from '@hono/valibot-validator'
const schema = object({
name: string(),
age: number(),
})
app.post('/author', vValidator('json', schema), (c) => {
const data = c.req.valid('json')
return c.json({
success: true,
message: `${data.name} is ${data.age}`,
})
})
```
Hook:
```ts
app.post(
'/post',
vValidator('json', schema, (result, c) => {
if (!result.success) {
return c.text('Invalid!', 400)
}
})
//...
)
```
## Author
Nico Franke <https://github.com/ZerNico>
## License
MIT

View File

@ -0,0 +1 @@
module.exports = require('../../jest.config.js')

View File

@ -0,0 +1,38 @@
{
"name": "@hono/valibot-validator",
"version": "0.0.0",
"description": "Validator middleware using Valibot",
"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.*",
"valibot": "^0.5.0"
},
"devDependencies": {
"hono": "^3.1.0",
"valibot": "0.5.0"
}
}

View File

@ -0,0 +1,44 @@
import type { Context, MiddlewareHandler, Env, ValidationTargets } from 'hono'
import { validator } from 'hono/validator'
import type { BaseSchema, Input, Output, ValiError } from 'valibot'
import { safeParse } from 'valibot'
type Hook<T, E extends Env, P extends string> = (
result: { success: true; data: T } | { success: false; error: ValiError },
c: Context<E, P>
) => Response | Promise<Response> | void
export const vValidator = <
T extends BaseSchema,
Target extends keyof ValidationTargets,
E extends Env,
P extends string,
V extends {
in: { [K in Target]: Input<T> }
out: { [K in Target]: Output<T> }
} = {
in: { [K in Target]: Input<T> }
out: { [K in Target]: Output<T> }
}
>(
target: Target,
schema: T,
hook?: Hook<Output<T>, E, P>
): MiddlewareHandler<E, P, V> =>
validator(target, (value, c) => {
const result = safeParse(schema, 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 Output<T>
return data
})

View File

@ -0,0 +1,131 @@
import { Hono } from 'hono'
import type { Equal, Expect } from 'hono/utils/types'
import { number, object, string } from 'valibot'
import { vValidator } from '../src'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type ExtractSchema<T> = T extends Hono<infer _, infer S> ? S : never
describe('Basic', () => {
const app = new Hono()
const schema = object({
name: string(),
age: number(),
})
const route = app.post('/author', vValidator('json', schema), (c) => {
const data = c.req.valid('json')
return c.jsonT({
success: true,
message: `${data.name} is ${data.age}`,
})
})
type Actual = ExtractSchema<typeof route>
type Expected = {
'/author': {
$post: {
input: {
json: {
name: string
age: number
}
}
output: {
success: true
message: string
}
}
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type verify = Expect<Equal<Expected, Actual>>
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 = object({
id: number(),
title: string(),
})
app.post(
'/post',
vValidator('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('json')
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)
})
})

View File

@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"declaration": false,
"outDir": "./dist/cjs"
}
}

View File

@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "ESNext",
"declaration": true,
"outDir": "./dist/esm"
}
}

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
},
"include": [
"src/**/*.ts"
],
}

View File

@ -11053,6 +11053,11 @@ vali-date@^1.0.0:
resolved "https://registry.yarnpkg.com/vali-date/-/vali-date-1.0.0.tgz#1b904a59609fb328ef078138420934f6b86709a6"
integrity sha512-sgECfZthyaCKW10N0fm27cg8HYTFK5qMWgypqkXMQ4Wbl/zZKx7xZICgcoxIIE+WFAP/MBL2EFwC/YvLxw3Zeg==
valibot@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/valibot/-/valibot-0.5.0.tgz#027bdbfa9e64a6cca2f00fa8d0cd6e361877f6f4"
integrity sha512-xjL/zMuQloTyK6sVzGO4HVg3AcPb970kIi2H3yMgl7gadBKKzepje31DWRyvOxhXJnIrFX8rJM0mhkMcmWz4bg==
valid-url@^1:
version "1.0.9"
resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200"