feat(typia-validator): support typia http module (#888)
* feature(typia-validator): support typia http module * feature(typia-validator): add change-set & update READMEpull/889/head
parent
f58f47ebd1
commit
c63470e491
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
'@hono/typia-validator': minor
|
||||
---
|
||||
|
||||
Enables handling of `number`, `boolean`, and `bigint` types in query parameters and headers.
|
||||
|
||||
```diff
|
||||
- import { typiaValidator } from '@hono/typia-validator';
|
||||
+ import { typiaValidator } from '@hono/typia-validator/http';
|
||||
import { Hono } from 'hono';
|
||||
import typia, { type tags } from 'typia';
|
||||
|
||||
interface Schema {
|
||||
- pages: `${number}`[];
|
||||
+ pages: (number & tags.Type<'uint32'>)[];
|
||||
}
|
||||
|
||||
const app = new Hono()
|
||||
.get(
|
||||
'/books',
|
||||
typiaValidator(
|
||||
- typia.createValidate<Schema>(),
|
||||
+ typia.http.createValidateQuery<Schema>(),
|
||||
async (result, c) => {
|
||||
if (!result.success)
|
||||
return c.text('Invalid query parameters', 400);
|
||||
- return { pages: result.data.pages.map(Number) };
|
||||
}
|
||||
),
|
||||
async c => {
|
||||
const { pages } = c.req.valid('query'); // { pages: number[] }
|
||||
//...
|
||||
}
|
||||
)
|
||||
```
|
|
@ -0,0 +1 @@
|
|||
/test-generated
|
|
@ -4,6 +4,12 @@ The validator middleware using [Typia](https://typia.io/docs/) for [Hono](https:
|
|||
|
||||
## Usage
|
||||
|
||||
You can use [Basic Validation](#basic-validation) and [HTTP Module Validation](#http-module-validation) with Typia Validator.
|
||||
|
||||
### Basic Validation
|
||||
|
||||
Use only the standard validator in typia.
|
||||
|
||||
```ts
|
||||
import typia, { tags } from 'typia'
|
||||
import { typiaValidator } from '@hono/typia-validator'
|
||||
|
@ -38,6 +44,60 @@ app.post(
|
|||
)
|
||||
```
|
||||
|
||||
### HTTP Module Validation
|
||||
|
||||
[Typia's HTTP module](https://typia.io/docs/misc/#http-module) allows you to validate query and header parameters with automatic type parsing.
|
||||
|
||||
- **Supported Parsers:** The HTTP module currently supports "query" and "header" validations.
|
||||
- **Parsing Differences:** The parsing mechanism differs slightly from Hono's native parsers. Ensure that your type definitions comply with Typia's HTTP module restrictions.
|
||||
|
||||
```typescript
|
||||
import { Hono } from 'hono'
|
||||
import typia from 'typia'
|
||||
import { typiaValidator } from '@hono/typia-validator/http'
|
||||
|
||||
interface Author {
|
||||
name: string
|
||||
age: number & tags.Type<'uint32'> & tags.Minimum<20> & tags.ExclusiveMaximum<100>
|
||||
}
|
||||
|
||||
interface IQuery {
|
||||
limit?: number
|
||||
enforce: boolean
|
||||
values?: string[]
|
||||
atomic: string | null
|
||||
indexes: number[]
|
||||
}
|
||||
interface IHeaders {
|
||||
'x-category': 'x' | 'y' | 'z'
|
||||
'x-memo'?: string
|
||||
'x-name'?: string
|
||||
'x-values': number[]
|
||||
'x-flags': boolean[]
|
||||
'x-descriptions': string[]
|
||||
}
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
const validate = typia.createValidate<Author>()
|
||||
const validateQuery = typia.http.createValidateQuery<IQuery>()
|
||||
const validateHeaders = typia.http.createValidateHeaders<IHeaders>()
|
||||
|
||||
app.get('/items',
|
||||
typiaValidator('json', validate),
|
||||
typiaValidator('query', validateQuery),
|
||||
typiaValidator('header', validateHeaders),
|
||||
(c) => {
|
||||
const query = c.req.valid('query')
|
||||
const headers = c.req.valid('header')
|
||||
return c.json({
|
||||
success: true,
|
||||
query,
|
||||
headers,
|
||||
})
|
||||
}
|
||||
)
|
||||
```
|
||||
## Author
|
||||
|
||||
Patryk Dwórznik <https://github.com/dworznik>
|
||||
|
|
|
@ -5,11 +5,25 @@
|
|||
"main": "dist/cjs/index.js",
|
||||
"module": "dist/esm/index.js",
|
||||
"types": "dist/esm/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"default": "./dist/cjs/index.js",
|
||||
"require": "./dist/cjs/index.js",
|
||||
"import": "./dist/esm/index.js",
|
||||
"types": "./dist/esm/index.d.ts"
|
||||
},
|
||||
"./http": {
|
||||
"default": "./dist/cjs/http.js",
|
||||
"require": "./dist/cjs/http.js",
|
||||
"import": "./dist/esm/http.js",
|
||||
"types": "./dist/esm/http.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"generate-test": "rimraf test-generated && typia generate --input test --output test-generated --project tsconfig.json",
|
||||
"generate-test": "rimraf test-generated && typia generate --input test --output test-generated --project tsconfig.json && node scripts/add-ts-ignore.cjs",
|
||||
"test": "npm run generate-test && jest",
|
||||
"build:cjs": "tsc -p tsconfig.cjs.json",
|
||||
"build:esm": "tsc -p tsconfig.esm.json",
|
||||
|
@ -29,12 +43,13 @@
|
|||
"homepage": "https://github.com/honojs/middleware",
|
||||
"peerDependencies": {
|
||||
"hono": ">=3.9.0",
|
||||
"typia": "^6.1.0"
|
||||
"typia": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"hono": "^3.11.7",
|
||||
"jest": "^29.7.0",
|
||||
"rimraf": "^5.0.5",
|
||||
"typia": "^5.0.4"
|
||||
"typescript": "^5.4.0",
|
||||
"typia": "^7.3.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
// @ts-check
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
|
||||
// https://github.com/samchon/typia/issues/1432
|
||||
// typia generated files have some type errors
|
||||
|
||||
const generatedFiles = fs
|
||||
.readdirSync(path.resolve(__dirname, '../test-generated'))
|
||||
.map((file) => path.resolve(__dirname, '../test-generated', file))
|
||||
|
||||
for (const file of generatedFiles) {
|
||||
const content = fs.readFileSync(file, 'utf8')
|
||||
const lines = content.split('\n')
|
||||
const distLines = []
|
||||
for (const line of lines) {
|
||||
if (
|
||||
line.includes('._httpHeaderReadNumber(') ||
|
||||
line.includes('._httpHeaderReadBigint(') ||
|
||||
line.includes('._httpHeaderReadBoolean(')
|
||||
)
|
||||
distLines.push(`// @ts-ignore`)
|
||||
distLines.push(line)
|
||||
}
|
||||
|
||||
fs.writeFileSync(file, distLines.join('\n'))
|
||||
}
|
|
@ -0,0 +1,181 @@
|
|||
import type { Context, MiddlewareHandler, Env, ValidationTargets, TypedResponse } from 'hono'
|
||||
import { validator } from 'hono/validator'
|
||||
import type { IReadableURLSearchParams, IValidation } from 'typia'
|
||||
|
||||
interface IFailure<T> {
|
||||
success: false
|
||||
errors: IValidation.IError[]
|
||||
data: T
|
||||
}
|
||||
|
||||
type BaseType<T> = T extends string
|
||||
? string
|
||||
: T extends number
|
||||
? number
|
||||
: T extends boolean
|
||||
? boolean
|
||||
: T extends symbol
|
||||
? symbol
|
||||
: T extends bigint
|
||||
? bigint
|
||||
: T
|
||||
type Parsed<T> = T extends Record<string | number, any>
|
||||
? {
|
||||
[K in keyof T]-?: T[K] extends (infer U)[]
|
||||
? (BaseType<U> | null | undefined)[] | undefined
|
||||
: BaseType<T[K]> | null | undefined
|
||||
}
|
||||
: BaseType<T>
|
||||
|
||||
export type QueryValidation<O extends Record<string | number, any> = any> = (
|
||||
input: string | URLSearchParams
|
||||
) => IValidation<O>
|
||||
export type QueryOutputType<T> = T extends QueryValidation<infer O> ? O : never
|
||||
type QueryStringify<T> = T extends Record<string | number, any>
|
||||
? {
|
||||
// Suppress to split union types
|
||||
[K in keyof T]: [T[K]] extends [bigint | number | boolean]
|
||||
? `${T[K]}`
|
||||
: T[K] extends (infer U)[]
|
||||
? [U] extends [bigint | number | boolean]
|
||||
? `${U}`[]
|
||||
: T[K]
|
||||
: T[K]
|
||||
}
|
||||
: T
|
||||
export type HeaderValidation<O extends Record<string | number, any> = any> = (
|
||||
input: Record<string, string | string[] | undefined>
|
||||
) => IValidation<O>
|
||||
export type HeaderOutputType<T> = T extends HeaderValidation<infer O> ? O : never
|
||||
type HeaderStringify<T> = T extends Record<string | number, any>
|
||||
? {
|
||||
// Suppress to split union types
|
||||
[K in keyof T]: [T[K]] extends [bigint | number | boolean]
|
||||
? `${T[K]}`
|
||||
: T[K] extends (infer U)[]
|
||||
? [U] extends [bigint | number | boolean]
|
||||
? `${U}`
|
||||
: U
|
||||
: T[K]
|
||||
}
|
||||
: T
|
||||
|
||||
export type HttpHook<T, E extends Env, P extends string, O = {}> = (
|
||||
result: IValidation.ISuccess<T> | IFailure<Parsed<T>>,
|
||||
c: Context<E, P>
|
||||
) => Response | Promise<Response> | void | Promise<Response | void> | TypedResponse<O>
|
||||
export type Hook<T, E extends Env, P extends string, O = {}> = (
|
||||
result: IValidation.ISuccess<T> | IFailure<T>,
|
||||
c: Context<E, P>
|
||||
) => Response | Promise<Response> | void | Promise<Response | void> | TypedResponse<O>
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type Validation<O = any> = (input: unknown) => IValidation<O>
|
||||
export type OutputType<T> = T extends Validation<infer O> ? O : never
|
||||
|
||||
interface TypiaValidator {
|
||||
<
|
||||
T extends QueryValidation,
|
||||
O extends QueryOutputType<T>,
|
||||
E extends Env,
|
||||
P extends string,
|
||||
V extends { in: { query: QueryStringify<O> }; out: { query: O } } = {
|
||||
in: { query: QueryStringify<O> }
|
||||
out: { query: O }
|
||||
}
|
||||
>(
|
||||
target: 'query',
|
||||
validate: T,
|
||||
hook?: HttpHook<O, E, P>
|
||||
): MiddlewareHandler<E, P, V>
|
||||
|
||||
<
|
||||
T extends HeaderValidation,
|
||||
O extends HeaderOutputType<T>,
|
||||
E extends Env,
|
||||
P extends string,
|
||||
V extends { in: { header: HeaderStringify<O> }; out: { header: O } } = {
|
||||
in: { header: HeaderStringify<O> }
|
||||
out: { header: O }
|
||||
}
|
||||
>(
|
||||
target: 'header',
|
||||
validate: T,
|
||||
hook?: HttpHook<O, E, P>
|
||||
): MiddlewareHandler<E, P, V>
|
||||
|
||||
<
|
||||
T extends Validation,
|
||||
O extends OutputType<T>,
|
||||
Target extends Exclude<keyof ValidationTargets, 'query' | 'queries' | 'header'>,
|
||||
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>
|
||||
}
|
||||
|
||||
export const typiaValidator: TypiaValidator = (
|
||||
target: keyof ValidationTargets,
|
||||
validate: (input: any) => IValidation<any>,
|
||||
hook?: Hook<any, any, any>
|
||||
): MiddlewareHandler => {
|
||||
if (target === 'query' || target === 'header')
|
||||
return async (c, next) => {
|
||||
let value: any
|
||||
if (target === 'query') {
|
||||
const queries = c.req.queries()
|
||||
value = {
|
||||
get: (key) => queries[key]?.[0] ?? null,
|
||||
getAll: (key) => queries[key] ?? [],
|
||||
} satisfies IReadableURLSearchParams
|
||||
} else {
|
||||
value = Object.create(null)
|
||||
for (const [key, headerValue] of c.req.raw.headers) value[key.toLowerCase()] = headerValue
|
||||
if (c.req.raw.headers.has('Set-Cookie'))
|
||||
value['Set-Cookie'] = c.req.raw.headers.getSetCookie()
|
||||
}
|
||||
const result = validate(value)
|
||||
|
||||
if (hook) {
|
||||
const res = await hook(result as never, c)
|
||||
if (res instanceof Response) return res
|
||||
}
|
||||
if (!result.success) {
|
||||
return c.json({ success: false, error: result.errors }, 400)
|
||||
}
|
||||
c.req.addValidatedData(target, result.data)
|
||||
|
||||
await next()
|
||||
}
|
||||
|
||||
return validator(target, async (value, c) => {
|
||||
const result = validate(value)
|
||||
|
||||
if (hook) {
|
||||
const hookResult = await 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
|
||||
})
|
||||
}
|
|
@ -29,11 +29,11 @@ export const typiaValidator = <
|
|||
validate: T,
|
||||
hook?: Hook<O, E, P>
|
||||
): MiddlewareHandler<E, P, V> =>
|
||||
validator(target, (value, c) => {
|
||||
validator(target, async (value, c) => {
|
||||
const result = validate(value)
|
||||
|
||||
if (hook) {
|
||||
const hookResult = hook({ ...result, data: value }, c)
|
||||
const hookResult = await hook({ ...result, data: value }, c)
|
||||
if (hookResult) {
|
||||
if (hookResult instanceof Response || hookResult instanceof Promise) {
|
||||
return hookResult
|
||||
|
|
|
@ -0,0 +1,424 @@
|
|||
import { Hono } from 'hono'
|
||||
import type { Equal, Expect } from 'hono/utils/types'
|
||||
import typia, { tags } from 'typia'
|
||||
import { typiaValidator } from '../src/http'
|
||||
|
||||
// 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 JsonSchema {
|
||||
name: string
|
||||
age: number
|
||||
}
|
||||
const validateJson = typia.createValidate<JsonSchema>()
|
||||
|
||||
interface QuerySchema {
|
||||
name?: string
|
||||
}
|
||||
const validateQuery = typia.http.createValidateQuery<QuerySchema>()
|
||||
interface HeaderSchema {
|
||||
'x-Category': ('x' | 'y' | 'z')[]
|
||||
}
|
||||
const validateHeader = typia.http.createValidateHeaders<HeaderSchema>()
|
||||
|
||||
const route = app.post(
|
||||
'/author',
|
||||
typiaValidator('json', validateJson),
|
||||
typiaValidator('query', validateQuery),
|
||||
typiaValidator('header', validateHeader),
|
||||
(c) => {
|
||||
const data = c.req.valid('json')
|
||||
const query = c.req.valid('query')
|
||||
const header = c.req.valid('header')
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
message: `${data.name} is ${data.age}`,
|
||||
queryName: query?.name,
|
||||
headerCategory: header['x-Category'],
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
type Actual = ExtractSchema<typeof route>
|
||||
type Expected = {
|
||||
'/author': {
|
||||
$post: {
|
||||
input: {
|
||||
json: {
|
||||
name: string
|
||||
age: number
|
||||
}
|
||||
} & {
|
||||
query: {
|
||||
name?: string | undefined
|
||||
}
|
||||
} & {
|
||||
header: {
|
||||
'x-Category': 'x' | 'y' | 'z'
|
||||
}
|
||||
}
|
||||
output: {
|
||||
success: boolean
|
||||
message: string
|
||||
queryName: string | undefined
|
||||
headerCategory: ('x' | 'y' | 'z')[]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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?name=Metallo', {
|
||||
body: JSON.stringify({
|
||||
name: 'Superman',
|
||||
age: 20,
|
||||
}),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Category': 'x, y, z',
|
||||
},
|
||||
})
|
||||
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',
|
||||
queryName: 'Metallo',
|
||||
headerCategory: ['x', 'y', 'z'],
|
||||
})
|
||||
})
|
||||
|
||||
it('Should return 400 response', async () => {
|
||||
const req = new Request('http://localhost/author', {
|
||||
body: JSON.stringify({
|
||||
name: 'Superman',
|
||||
age: '20',
|
||||
}),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
})
|
||||
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('transform', () => {
|
||||
const app = new Hono()
|
||||
|
||||
interface QuerySchema {
|
||||
page: (0 | 1 | 2)[]
|
||||
}
|
||||
const validateQuery = typia.http.createValidateQuery<QuerySchema>()
|
||||
|
||||
interface HeaderSchema {
|
||||
'X-Categories': (0 | 1 | 2)[]
|
||||
}
|
||||
const validateHeader = typia.http.createValidateHeaders<HeaderSchema>()
|
||||
|
||||
const route = app.get(
|
||||
'/page',
|
||||
typiaValidator('query', validateQuery),
|
||||
typiaValidator('header', validateHeader),
|
||||
(c) => {
|
||||
const { page } = c.req.valid('query')
|
||||
const { 'X-Categories': categories } = c.req.valid('header')
|
||||
return c.json({ page, categories })
|
||||
}
|
||||
)
|
||||
|
||||
type Actual = ExtractSchema<typeof route>
|
||||
type Expected = {
|
||||
'/page': {
|
||||
$get: {
|
||||
input: {
|
||||
query: {
|
||||
page: `${0 | 1 | 2}`[]
|
||||
}
|
||||
} & {
|
||||
header: {
|
||||
'X-Categories': `${0 | 1 | 2}`
|
||||
}
|
||||
}
|
||||
output: {
|
||||
page: (0 | 1 | 2)[]
|
||||
categories: (0 | 1 | 2)[]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
type verify = Expect<Equal<Expected, Actual>>
|
||||
|
||||
it('Should return 200 response', async () => {
|
||||
const res = await app.request('/page?page=1', {
|
||||
headers: {
|
||||
'X-Categories': '0, 1, 2',
|
||||
},
|
||||
})
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual({
|
||||
page: [1],
|
||||
categories: [0, 1, 2],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('With Hook', () => {
|
||||
const app = new Hono()
|
||||
|
||||
interface Schema {
|
||||
id: number
|
||||
title: string
|
||||
}
|
||||
const validateSchema = typia.createValidate<Schema>()
|
||||
|
||||
app.post(
|
||||
'/post',
|
||||
typiaValidator('json', validateSchema, (result, c) => {
|
||||
if (!result.success) {
|
||||
return c.text(`${result.data.id} is invalid!`, 400)
|
||||
}
|
||||
}),
|
||||
(c) => {
|
||||
const data = c.req.valid('json')
|
||||
return c.text(`${data.id} is valid!`)
|
||||
}
|
||||
)
|
||||
|
||||
it('Should return 200 response', async () => {
|
||||
const req = new Request('http://localhost/post', {
|
||||
body: JSON.stringify({
|
||||
id: 123,
|
||||
title: 'Hello',
|
||||
}),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
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',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
const res = await app.request(req)
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(400)
|
||||
expect(await res.text()).toBe('123 is invalid!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('With Async Hook', () => {
|
||||
const app = new Hono()
|
||||
|
||||
interface Schema {
|
||||
id: number & tags.Maximum<999>
|
||||
title: string
|
||||
}
|
||||
const validateSchema = typia.createValidate<Schema>()
|
||||
const validateQuery = typia.http.createValidateQuery<Schema>()
|
||||
const validateHeader = typia.http.createValidateHeaders<Schema>()
|
||||
|
||||
app.post(
|
||||
'/post',
|
||||
typiaValidator('json', validateSchema, async (result, c) => {
|
||||
if (!result.success) {
|
||||
return c.text(`${result.data.id} is invalid!`, 400)
|
||||
}
|
||||
}),
|
||||
typiaValidator('query', validateQuery, async (result, c) => {
|
||||
if (!result.success) {
|
||||
return c.text(`${result.data.id} is invalid!`, 400)
|
||||
}
|
||||
}),
|
||||
typiaValidator('header', validateHeader, async (result, c) => {
|
||||
if (!result.success) {
|
||||
return c.text(`${result.data.id} is invalid!`, 400)
|
||||
}
|
||||
}),
|
||||
(c) => {
|
||||
const data = c.req.valid('json')
|
||||
const query = c.req.valid('query')
|
||||
const header = c.req.valid('header')
|
||||
return c.json({ data, query, header })
|
||||
}
|
||||
)
|
||||
|
||||
it('Should return 200 response', async () => {
|
||||
const req = new Request('http://localhost/post?id=125&title=My', {
|
||||
body: JSON.stringify({
|
||||
id: 123,
|
||||
title: 'Hello',
|
||||
}),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Id: '124',
|
||||
Title: 'World',
|
||||
},
|
||||
})
|
||||
const res = await app.request(req)
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual({
|
||||
data: { id: 123, title: 'Hello' },
|
||||
query: { id: 125, title: 'My' },
|
||||
header: { id: 124, title: 'World' },
|
||||
})
|
||||
})
|
||||
|
||||
it('Should return 400 response', async () => {
|
||||
let req = new Request('http://localhost/post', {
|
||||
body: JSON.stringify({
|
||||
id: '123',
|
||||
title: 'Hello',
|
||||
}),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
let res = await app.request(req)
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(400)
|
||||
expect(await res.text()).toBe('123 is invalid!')
|
||||
|
||||
req = new Request('http://localhost/post?id=1000&title=My', {
|
||||
body: JSON.stringify({
|
||||
id: 123,
|
||||
title: 'Hello',
|
||||
}),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Id: '124',
|
||||
Title: 'World',
|
||||
},
|
||||
})
|
||||
res = await app.request(req)
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(400)
|
||||
expect(await res.text()).toBe('1000 is invalid!')
|
||||
|
||||
req = new Request('http://localhost/post?id=125&title=My', {
|
||||
body: JSON.stringify({
|
||||
id: 123,
|
||||
title: 'Hello',
|
||||
}),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Id: '1000',
|
||||
Title: 'World',
|
||||
},
|
||||
})
|
||||
res = await app.request(req)
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(400)
|
||||
expect(await res.text()).toBe('1000 is invalid!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('With target', () => {
|
||||
it('should call hook for correctly validated target', async () => {
|
||||
const app = new Hono()
|
||||
|
||||
interface Schema {
|
||||
id: number
|
||||
}
|
||||
const validateSchema = typia.createValidate<Schema>()
|
||||
const validateQuery = typia.http.createValidateQuery<Schema>()
|
||||
const validateHeader = typia.http.createValidateHeaders<Schema>()
|
||||
|
||||
const jsonHook = jest.fn()
|
||||
const headerHook = jest.fn()
|
||||
const queryHook = jest.fn()
|
||||
app.post(
|
||||
'/post',
|
||||
typiaValidator('json', validateSchema, jsonHook),
|
||||
typiaValidator('query', validateQuery, queryHook),
|
||||
typiaValidator('header', validateHeader, headerHook),
|
||||
(c) => {
|
||||
return c.text('ok')
|
||||
}
|
||||
)
|
||||
|
||||
const req = new Request('http://localhost/post?id=2', {
|
||||
body: JSON.stringify({
|
||||
id: 3,
|
||||
}),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
id: '1',
|
||||
},
|
||||
})
|
||||
|
||||
const res = await app.request(req)
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('ok')
|
||||
expect(headerHook).toHaveBeenCalledWith({ data: { id: 1 }, success: true }, expect.anything())
|
||||
expect(queryHook).toHaveBeenCalledWith({ data: { id: 2 }, success: true }, expect.anything())
|
||||
expect(jsonHook).toHaveBeenCalledWith({ data: { id: 3 }, success: true }, expect.anything())
|
||||
})
|
||||
})
|
||||
|
||||
describe('Case-Insensitive Headers', () => {
|
||||
it('Should ignore the case for headers in the Zod schema and return 200', () => {
|
||||
const app = new Hono()
|
||||
interface HeaderSchema {
|
||||
'Content-Type': string
|
||||
ApiKey: string
|
||||
onlylowercase: string
|
||||
ONLYUPPERCASE: string
|
||||
}
|
||||
const validateHeader = typia.http.createValidateHeaders<HeaderSchema>()
|
||||
|
||||
const route = app.get('/', typiaValidator('header', validateHeader), (c) => {
|
||||
const headers = c.req.valid('header')
|
||||
return c.json(headers)
|
||||
})
|
||||
|
||||
type Actual = ExtractSchema<typeof route>
|
||||
type Expected = {
|
||||
'/': {
|
||||
$get: {
|
||||
input: {
|
||||
header: HeaderSchema
|
||||
}
|
||||
output: HeaderSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
type verify = Expect<Equal<Expected, Actual>>
|
||||
})
|
||||
})
|
|
@ -144,3 +144,60 @@ describe('With Hook', () => {
|
|||
expect(await res.text()).toBe('123 is invalid!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('With Async Hook', () => {
|
||||
const app = new Hono()
|
||||
|
||||
interface Schema {
|
||||
id: number & tags.Maximum<999>
|
||||
title: string
|
||||
}
|
||||
const validateSchema = typia.createValidate<Schema>()
|
||||
|
||||
app.post(
|
||||
'/post',
|
||||
typiaValidator('json', validateSchema, async (result, c) => {
|
||||
if (!result.success) {
|
||||
return c.text(`${result.data.id} is invalid!`, 400)
|
||||
}
|
||||
}),
|
||||
(c) => {
|
||||
const data = c.req.valid('json')
|
||||
return c.text(`${data.id} is valid!`, 200)
|
||||
}
|
||||
)
|
||||
|
||||
it('Should return 200 response', async () => {
|
||||
const req = new Request('http://localhost/post', {
|
||||
body: JSON.stringify({
|
||||
id: 123,
|
||||
title: 'Hello',
|
||||
}),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
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',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
const res = await app.request(req)
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(400)
|
||||
expect(await res.text()).toBe('123 is invalid!')
|
||||
})
|
||||
})
|
||||
|
|
52
yarn.lock
52
yarn.lock
|
@ -2900,10 +2900,11 @@ __metadata:
|
|||
hono: "npm:^3.11.7"
|
||||
jest: "npm:^29.7.0"
|
||||
rimraf: "npm:^5.0.5"
|
||||
typia: "npm:^5.0.4"
|
||||
typescript: "npm:^5.4.0"
|
||||
typia: "npm:^7.3.0"
|
||||
peerDependencies:
|
||||
hono: ">=3.9.0"
|
||||
typia: ^6.1.0
|
||||
typia: ^7.0.0
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
|
@ -4589,6 +4590,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@samchon/openapi@npm:^2.1.2":
|
||||
version: 2.1.2
|
||||
resolution: "@samchon/openapi@npm:2.1.2"
|
||||
checksum: 3f4cdcaad90b67e90104282c950fc86cd67981bf1b5a15b08a8521358687dc5afcd24540b41b0e0f4807ba0a3ed75008777a91ae12abfde6f809a601f8515881
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@samverschueren/stream-to-observable@npm:^0.3.0, @samverschueren/stream-to-observable@npm:^0.3.1":
|
||||
version: 0.3.1
|
||||
resolution: "@samverschueren/stream-to-observable@npm:0.3.1"
|
||||
|
@ -16415,6 +16423,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"package-manager-detector@npm:^0.2.0":
|
||||
version: 0.2.7
|
||||
resolution: "package-manager-detector@npm:0.2.7"
|
||||
checksum: 0ea19abf11e251c3bffe2698450a4a2a5658528b88151943eff01c5f4b9bdc848abc96588c1fe5f01618887cf1154d6e72eb28edb263e46178397aa6ebd58ff0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"parent-module@npm:^1.0.0":
|
||||
version: 1.0.1
|
||||
resolution: "parent-module@npm:1.0.1"
|
||||
|
@ -20054,6 +20069,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@npm:^5.4.0":
|
||||
version: 5.7.2
|
||||
resolution: "typescript@npm:5.7.2"
|
||||
bin:
|
||||
tsc: bin/tsc
|
||||
tsserver: bin/tsserver
|
||||
checksum: a873118b5201b2ef332127ef5c63fb9d9c155e6fdbe211cbd9d8e65877283797cca76546bad742eea36ed7efbe3424a30376818f79c7318512064e8625d61622
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@npm:^5.4.4":
|
||||
version: 5.4.4
|
||||
resolution: "typescript@npm:5.4.4"
|
||||
|
@ -20094,6 +20119,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@patch:typescript@npm%3A^5.4.0#optional!builtin<compat/typescript>":
|
||||
version: 5.7.2
|
||||
resolution: "typescript@patch:typescript@npm%3A5.7.2#optional!builtin<compat/typescript>::version=5.7.2&hash=e012d7"
|
||||
bin:
|
||||
tsc: bin/tsc
|
||||
tsserver: bin/tsserver
|
||||
checksum: c891ccf04008bc1305ba34053db951f8a4584b4a1bf2f68fd972c4a354df3dc5e62c8bfed4f6ac2d12e5b3b1c49af312c83a651048f818cd5b4949d17baacd79
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@patch:typescript@npm%3A^5.4.4#optional!builtin<compat/typescript>":
|
||||
version: 5.4.4
|
||||
resolution: "typescript@patch:typescript@npm%3A5.4.4#optional!builtin<compat/typescript>::version=5.4.4&hash=e012d7"
|
||||
|
@ -20114,19 +20149,22 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typia@npm:^5.0.4":
|
||||
version: 5.3.5
|
||||
resolution: "typia@npm:5.3.5"
|
||||
"typia@npm:^7.3.0":
|
||||
version: 7.3.0
|
||||
resolution: "typia@npm:7.3.0"
|
||||
dependencies:
|
||||
"@samchon/openapi": "npm:^2.1.2"
|
||||
commander: "npm:^10.0.0"
|
||||
comment-json: "npm:^4.2.3"
|
||||
inquirer: "npm:^8.2.5"
|
||||
package-manager-detector: "npm:^0.2.0"
|
||||
randexp: "npm:^0.5.3"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.0 <5.4.0"
|
||||
"@samchon/openapi": ">=2.1.2 <3.0.0"
|
||||
typescript: ">=4.8.0 <5.8.0"
|
||||
bin:
|
||||
typia: lib/executable/typia.js
|
||||
checksum: 2707ccaa83b35647380adb361c90d27fe88d1a6652e14a37f261d4a661d92aad88bc15af42744b80ea7f1bf5ede5c93f1b55be16bff5071570f8f0f14d5a5c5b
|
||||
checksum: 5ef80aa41238ef082c3a73feaa6d59039a0298f3a93d52a725a22b7aa3a2437cc015f9c122a4e43188deb2b0422b8d3bb64f2e010be2f4dad64ee3bfc91d0717
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
Loading…
Reference in New Issue