feat: Add Typia validator (#158)

* Add Typia validator

* Add CI config
pull/160/head
Patryk Dwórznik 2023-09-15 22:54:56 +09:00 committed by GitHub
parent 752e6b2fd3
commit 2e4eeb0b70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 407 additions and 3 deletions

View File

@ -0,0 +1,5 @@
---
'@hono/typia-validator': patch
---
Add Typia validator

View File

@ -0,0 +1,25 @@
name: ci-typia-validator
on:
push:
branches: [main]
paths:
- 'packages/typia-validator/**'
pull_request:
branches: ['*']
paths:
- 'packages/typia-validator/**'
jobs:
ci:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./packages/typia-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

@ -19,6 +19,7 @@
"build:medley-router": "yarn workspace @hono/medley-router build", "build:medley-router": "yarn workspace @hono/medley-router build",
"build:valibot-validator": "yarn workspace @hono/valibot-validator build", "build:valibot-validator": "yarn workspace @hono/valibot-validator build",
"build:zod-openapi": "yarn build:zod-validator && yarn workspace @hono/zod-openapi build", "build:zod-openapi": "yarn build:zod-validator && yarn workspace @hono/zod-openapi build",
"build:typia-validator": "yarn workspace @hono/typia-validator build",
"build": "run-p build:*" "build": "run-p build:*"
}, },
"license": "MIT", "license": "MIT",

View File

@ -0,0 +1,47 @@
# Typia validator middleware for Hono
The validator middleware using [Typia](https://typia.io/docs/) for [Hono](https://honojs.dev) applications.
## Usage
```ts
import typia, { tags } from 'typia'
import { typiaValidator } from '@hono/typia-validator'
interface Author {
name: string
age: number & tags.Type<'uint32'> & tags.Minimum<20> & tags.ExclusiveMaximum<100>
}
const validate = typia.createValidate<Author>()
const route = app.post('/author', typiaValidator('json', validate), (c) => {
const data = c.req.valid('json')
return c.jsonT({
success: true,
message: `${data.name} is ${data.age}`,
})
})
```
Hook:
```ts
app.post(
'/post',
typiaValidator('json', validate, (result, c) => {
if (!result.success) {
return c.text('Invalid!', 400)
}
})
//...
)
```
## Author
Patryk Dwórznik <https://github.com/dworznik>
## License
MIT

View File

@ -0,0 +1,9 @@
module.exports = {
testMatch: ['**/test-generated/**/*.+(ts|tsx|js)'],
testPathIgnorePatterns: ['/node_modules/', '/dist/', '/.history/'],
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
testEnvironment: 'miniflare',
}

View File

@ -0,0 +1,39 @@
{
"name": "@hono/typia-validator",
"version": "0.0.1",
"description": "Validator middleware using Typia",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"types": "dist/esm/index.d.ts",
"files": [
"dist"
],
"scripts": {
"generate-test": "rimraf test-generated && typia generate --input test --output test-generated --project tsconfig.json",
"test": "npm run generate-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.*",
"typia": "^5.0.4"
},
"devDependencies": {
"hono": "^3.5.8",
"typia": "^5.0.4"
}
}

View File

@ -0,0 +1,50 @@
import type { Context, MiddlewareHandler, Env, ValidationTargets, TypedResponse } from 'hono'
import { validator } from 'hono/validator'
import { IValidation, Primitive } from 'typia'
export type Hook<T, E extends Env, P extends string, O = {}> = (
result: IValidation.ISuccess<T> | { success: false; errors: IValidation.IError[]; data: T },
c: Context<E, P>
) => Response | Promise<Response> | void | Promise<Response | void> | TypedResponse<O>
export type Validation<O = any> = (input: unknown) => IValidation<O>
export type OutputType<T> = T extends Validation<infer O> ? O : never
export const typiaValidator = <
T extends Validation,
O extends OutputType<T>,
Target extends keyof ValidationTargets,
E extends Env,
P extends string,
V extends {
in: { [K in Target]: O }
out: { [K in Target]: O }
} = {
in: { [K in Target]: O }
out: { [K in Target]: O }
}
>(
target: Target,
validate: T,
hook?: Hook<O, E, P>
): MiddlewareHandler<E, P, V> =>
validator(target, (value, c) => {
const result = validate(value)
if (hook) {
const hookResult = hook({ ...result, data: value }, c)
if (hookResult) {
if (hookResult instanceof Response || hookResult instanceof Promise) {
return hookResult
}
if ('response' in hookResult) {
return hookResult.response
}
}
}
if (!result.success) {
return c.json({ success: false, error: result.errors }, 400)
}
return result.data
})

View File

@ -0,0 +1,133 @@
import { Hono } from 'hono'
import type { Equal, Expect } from 'hono/utils/types'
import typia, { tags } from 'typia'
import { typiaValidator } 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()
interface Author {
name: string
age: number & tags.Type<'uint32'> & tags.Minimum<20> & tags.ExclusiveMaximum<100>
}
const validate = typia.createValidate<Author>()
const route = app.post('/author', typiaValidator('json', validate), (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: Author
}
output: {
success: boolean
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: 30,
}),
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 30',
})
})
it('Should return 400 response', async () => {
const req = new Request('http://localhost/author', {
body: JSON.stringify({
name: 'Superman',
age: 18,
}),
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()
interface Item {
id: number & tags.ExclusiveMaximum<9999>
title: string
}
const validate = typia.createValidate<Item>()
app.post(
'/post',
typiaValidator('json', validate, (result, c) => {
if (!result.success) {
return c.text(`${result.data.id} is invalid!`, 400)
}
const data = result.data
return Promise.resolve(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)
expect(await res.text()).toBe('123 is invalid!')
})
})

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,14 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
},
"include": [
"src/**/*.ts"
],
"plugins": [
{
"transform": "typia/lib/transform"
}
],
}

View File

@ -2579,6 +2579,11 @@ array-includes@^3.1.6:
get-intrinsic "^1.1.3" get-intrinsic "^1.1.3"
is-string "^1.0.7" is-string "^1.0.7"
array-timsort@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/array-timsort/-/array-timsort-1.0.3.tgz#3c9e4199e54fb2b9c3fe5976396a21614ef0d926"
integrity sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==
array-union@^2.1.0: array-union@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
@ -3510,6 +3515,17 @@ comment-json@^3.0.2:
has-own-prop "^2.0.0" has-own-prop "^2.0.0"
repeat-string "^1.6.1" repeat-string "^1.6.1"
comment-json@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/comment-json/-/comment-json-4.2.3.tgz#50b487ebbf43abe44431f575ebda07d30d015365"
integrity sha512-SsxdiOf064DWoZLH799Ata6u7iV658A11PlWtZATDlXPpKGJnbJZ5Z24ybixAi+LUUqJ/GKowAejtC5GFUG7Tw==
dependencies:
array-timsort "^1.0.3"
core-util-is "^1.0.3"
esprima "^4.0.1"
has-own-prop "^2.0.0"
repeat-string "^1.6.1"
compress-commons@^4.1.0: compress-commons@^4.1.0:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.1.1.tgz#df2a09a7ed17447642bad10a85cc9a19e5c42a7d" resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.1.1.tgz#df2a09a7ed17447642bad10a85cc9a19e5c42a7d"
@ -3617,7 +3633,7 @@ core-util-is@1.0.2:
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==
core-util-is@^1.0.2, core-util-is@~1.0.0: core-util-is@^1.0.2, core-util-is@^1.0.3, core-util-is@~1.0.0:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
@ -4120,6 +4136,11 @@ dotenv@^8.1.0:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b"
integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g== integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==
drange@^1.0.2:
version "1.1.1"
resolved "https://registry.yarnpkg.com/drange/-/drange-1.1.1.tgz#b2aecec2aab82fcef11dbbd7b9e32b83f8f6c0b8"
integrity sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==
duplexer3@^0.1.4: duplexer3@^0.1.4:
version "0.1.5" version "0.1.5"
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.5.tgz#0b5e4d7bad5de8901ea4440624c8e1d20099217e" resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.5.tgz#0b5e4d7bad5de8901ea4440624c8e1d20099217e"
@ -6137,6 +6158,27 @@ inquirer@^8.2.0:
through "^2.3.6" through "^2.3.6"
wrap-ansi "^7.0.0" wrap-ansi "^7.0.0"
inquirer@^8.2.5:
version "8.2.6"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.6.tgz#733b74888195d8d400a67ac332011b5fae5ea562"
integrity sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==
dependencies:
ansi-escapes "^4.2.1"
chalk "^4.1.1"
cli-cursor "^3.1.0"
cli-width "^3.0.0"
external-editor "^3.0.3"
figures "^3.0.0"
lodash "^4.17.21"
mute-stream "0.0.8"
ora "^5.4.1"
run-async "^2.4.0"
rxjs "^7.5.5"
string-width "^4.1.0"
strip-ansi "^6.0.0"
through "^2.3.6"
wrap-ansi "^6.0.1"
install-artifact-from-github@^1.3.3: install-artifact-from-github@^1.3.3:
version "1.3.3" version "1.3.3"
resolved "https://registry.yarnpkg.com/install-artifact-from-github/-/install-artifact-from-github-1.3.3.tgz#57d89bacfa0f47d7307fe41b6247cda9f9a8079c" resolved "https://registry.yarnpkg.com/install-artifact-from-github/-/install-artifact-from-github-1.3.3.tgz#57d89bacfa0f47d7307fe41b6247cda9f9a8079c"
@ -9988,6 +10030,14 @@ quick-lru@^4.0.1:
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
randexp@^0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.5.3.tgz#f31c2de3148b30bdeb84b7c3f59b0ebb9fec3738"
integrity sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==
dependencies:
drange "^1.0.2"
ret "^0.2.0"
range-parser@~1.2.1: range-parser@~1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
@ -10327,6 +10377,11 @@ restore-cursor@^3.1.0:
onetime "^5.1.0" onetime "^5.1.0"
signal-exit "^3.0.2" signal-exit "^3.0.2"
ret@^0.2.0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/ret/-/ret-0.2.2.tgz#b6861782a1f4762dce43402a71eb7a283f44573c"
integrity sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==
retry-request@^5.0.0: retry-request@^5.0.0:
version "5.0.2" version "5.0.2"
resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-5.0.2.tgz#143d85f90c755af407fcc46b7166a4ba520e44da" resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-5.0.2.tgz#143d85f90c755af407fcc46b7166a4ba520e44da"
@ -11617,6 +11672,16 @@ typescript@^4.7.4:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
typia@^5.0.4:
version "5.0.4"
resolved "https://registry.yarnpkg.com/typia/-/typia-5.0.4.tgz#71fac044326a5cde5dc51c2bff5acc9978fc64b1"
integrity sha512-/Go9GK+l67votbhp77rh2CO4LC4fv9WQ9jzJSQsHRXP6EWJF8LjOZVHwuLRdVRZMygzdAihDD7KlCAZGgCkIkw==
dependencies:
commander "^10.0.0"
comment-json "^4.2.3"
inquirer "^8.2.5"
randexp "^0.5.3"
uc.micro@^1.0.1, uc.micro@^1.0.5: uc.micro@^1.0.1, uc.micro@^1.0.5:
version "1.0.6" version "1.0.6"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
@ -12225,7 +12290,7 @@ wrap-ansi@^3.0.1:
string-width "^2.1.1" string-width "^2.1.1"
strip-ansi "^4.0.0" strip-ansi "^4.0.0"
wrap-ansi@^6.2.0: wrap-ansi@^6.0.1, wrap-ansi@^6.2.0:
version "6.2.0" version "6.2.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==