From c15de7ce3cbaa3b76ee259f266d480235a47e8fa Mon Sep 17 00:00:00 2001 From: Nico Franke Date: Sun, 30 Jul 2023 01:07:44 +0200 Subject: [PATCH] feat(valibot-validator): Add Valibot Validator Middleware (#102) * feat: add valibot validator * add changeset --- .changeset/fair-trains-hope.md | 5 + .github/workflows/ci-valibot-validator.yml | 25 ++++ package.json | 1 + packages/valibot-validator/README.md | 46 ++++++ packages/valibot-validator/jest.config.js | 1 + packages/valibot-validator/package.json | 38 +++++ packages/valibot-validator/src/index.ts | 44 ++++++ packages/valibot-validator/test/index.test.ts | 131 ++++++++++++++++++ packages/valibot-validator/tsconfig.cjs.json | 8 ++ packages/valibot-validator/tsconfig.esm.json | 8 ++ packages/valibot-validator/tsconfig.json | 9 ++ yarn.lock | 5 + 12 files changed, 321 insertions(+) create mode 100644 .changeset/fair-trains-hope.md create mode 100644 .github/workflows/ci-valibot-validator.yml create mode 100644 packages/valibot-validator/README.md create mode 100644 packages/valibot-validator/jest.config.js create mode 100644 packages/valibot-validator/package.json create mode 100644 packages/valibot-validator/src/index.ts create mode 100644 packages/valibot-validator/test/index.test.ts create mode 100644 packages/valibot-validator/tsconfig.cjs.json create mode 100644 packages/valibot-validator/tsconfig.esm.json create mode 100644 packages/valibot-validator/tsconfig.json diff --git a/.changeset/fair-trains-hope.md b/.changeset/fair-trains-hope.md new file mode 100644 index 00000000..19d76738 --- /dev/null +++ b/.changeset/fair-trains-hope.md @@ -0,0 +1,5 @@ +--- +'@hono/valibot-validator': minor +--- + +first release diff --git a/.github/workflows/ci-valibot-validator.yml b/.github/workflows/ci-valibot-validator.yml new file mode 100644 index 00000000..07cd9454 --- /dev/null +++ b/.github/workflows/ci-valibot-validator.yml @@ -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 diff --git a/package.json b/package.json index 961cefb4..d86b06cb 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/valibot-validator/README.md b/packages/valibot-validator/README.md new file mode 100644 index 00000000..f700de69 --- /dev/null +++ b/packages/valibot-validator/README.md @@ -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 + +## License + +MIT diff --git a/packages/valibot-validator/jest.config.js b/packages/valibot-validator/jest.config.js new file mode 100644 index 00000000..f697d831 --- /dev/null +++ b/packages/valibot-validator/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../jest.config.js') diff --git a/packages/valibot-validator/package.json b/packages/valibot-validator/package.json new file mode 100644 index 00000000..027c0c65 --- /dev/null +++ b/packages/valibot-validator/package.json @@ -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" + } +} diff --git a/packages/valibot-validator/src/index.ts b/packages/valibot-validator/src/index.ts new file mode 100644 index 00000000..20434ce7 --- /dev/null +++ b/packages/valibot-validator/src/index.ts @@ -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 = ( + result: { success: true; data: T } | { success: false; error: ValiError }, + c: Context +) => Response | Promise | void + +export const vValidator = < + T extends BaseSchema, + Target extends keyof ValidationTargets, + E extends Env, + P extends string, + V extends { + in: { [K in Target]: Input } + out: { [K in Target]: Output } + } = { + in: { [K in Target]: Input } + out: { [K in Target]: Output } + } +>( + target: Target, + schema: T, + hook?: Hook, E, P> +): MiddlewareHandler => + 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 + return data + }) diff --git a/packages/valibot-validator/test/index.test.ts b/packages/valibot-validator/test/index.test.ts new file mode 100644 index 00000000..f8aa109e --- /dev/null +++ b/packages/valibot-validator/test/index.test.ts @@ -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 extends Hono ? 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 + 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> + + 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) + }) +}) diff --git a/packages/valibot-validator/tsconfig.cjs.json b/packages/valibot-validator/tsconfig.cjs.json new file mode 100644 index 00000000..b8bf50ee --- /dev/null +++ b/packages/valibot-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/valibot-validator/tsconfig.esm.json b/packages/valibot-validator/tsconfig.esm.json new file mode 100644 index 00000000..8130f1a5 --- /dev/null +++ b/packages/valibot-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/valibot-validator/tsconfig.json b/packages/valibot-validator/tsconfig.json new file mode 100644 index 00000000..6c1a3990 --- /dev/null +++ b/packages/valibot-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/yarn.lock b/yarn.lock index dea0b95e..ccde5172 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"