From c63470e4915a0680c624bf97d52487572185a2d5 Mon Sep 17 00:00:00 2001 From: miyaji255 <84168445+miyaji255@users.noreply.github.com> Date: Sun, 15 Dec 2024 18:54:40 +0900 Subject: [PATCH] feat(typia-validator): support typia http module (#888) * feature(typia-validator): support typia http module * feature(typia-validator): add change-set & update README --- .changeset/support-http-module.md | 35 ++ packages/typia-validator/.gitignore | 1 + packages/typia-validator/README.md | 80 +++- packages/typia-validator/package.json | 21 +- .../typia-validator/scripts/add-ts-ignore.cjs | 27 ++ packages/typia-validator/src/http.ts | 181 ++++++++ packages/typia-validator/src/index.ts | 4 +- packages/typia-validator/test/http.test.ts | 424 ++++++++++++++++++ packages/typia-validator/test/index.test.ts | 57 +++ yarn.lock | 52 ++- 10 files changed, 860 insertions(+), 22 deletions(-) create mode 100644 .changeset/support-http-module.md create mode 100644 packages/typia-validator/.gitignore create mode 100644 packages/typia-validator/scripts/add-ts-ignore.cjs create mode 100644 packages/typia-validator/src/http.ts create mode 100644 packages/typia-validator/test/http.test.ts diff --git a/.changeset/support-http-module.md b/.changeset/support-http-module.md new file mode 100644 index 000000000..078c4f5ef --- /dev/null +++ b/.changeset/support-http-module.md @@ -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(), ++ typia.http.createValidateQuery(), + 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[] } + //... + } + ) +``` \ No newline at end of file diff --git a/packages/typia-validator/.gitignore b/packages/typia-validator/.gitignore new file mode 100644 index 000000000..6cfe364f1 --- /dev/null +++ b/packages/typia-validator/.gitignore @@ -0,0 +1 @@ +/test-generated \ No newline at end of file diff --git a/packages/typia-validator/README.md b/packages/typia-validator/README.md index b6482f889..79b950d31 100644 --- a/packages/typia-validator/README.md +++ b/packages/typia-validator/README.md @@ -4,24 +4,30 @@ 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' interface Author { - name: string - age: number & tags.Type<'uint32'> & tags.Minimum<20> & tags.ExclusiveMaximum<100> - } + name: string + age: number & tags.Type<'uint32'> & tags.Minimum<20> & tags.ExclusiveMaximum<100> +} - const validate = typia.createValidate() +const validate = typia.createValidate() - const route = app.post('/author', typiaValidator('json', validate), (c) => { - const data = c.req.valid('json') - return c.json({ - success: true, - message: `${data.name} is ${data.age}`, - }) +const route = app.post('/author', typiaValidator('json', validate), (c) => { + const data = c.req.valid('json') + return c.json({ + success: true, + message: `${data.name} is ${data.age}`, }) +}) ``` Hook: @@ -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() +const validateQuery = typia.http.createValidateQuery() +const validateHeaders = typia.http.createValidateHeaders() + +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 diff --git a/packages/typia-validator/package.json b/packages/typia-validator/package.json index 96789bce8..e1d68fcce 100644 --- a/packages/typia-validator/package.json +++ b/packages/typia-validator/package.json @@ -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" } } diff --git a/packages/typia-validator/scripts/add-ts-ignore.cjs b/packages/typia-validator/scripts/add-ts-ignore.cjs new file mode 100644 index 000000000..577f29c75 --- /dev/null +++ b/packages/typia-validator/scripts/add-ts-ignore.cjs @@ -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')) +} diff --git a/packages/typia-validator/src/http.ts b/packages/typia-validator/src/http.ts new file mode 100644 index 000000000..3eb930763 --- /dev/null +++ b/packages/typia-validator/src/http.ts @@ -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 { + success: false + errors: IValidation.IError[] + data: T +} + +type BaseType = T extends string + ? string + : T extends number + ? number + : T extends boolean + ? boolean + : T extends symbol + ? symbol + : T extends bigint + ? bigint + : T +type Parsed = T extends Record + ? { + [K in keyof T]-?: T[K] extends (infer U)[] + ? (BaseType | null | undefined)[] | undefined + : BaseType | null | undefined + } + : BaseType + +export type QueryValidation = any> = ( + input: string | URLSearchParams +) => IValidation +export type QueryOutputType = T extends QueryValidation ? O : never +type QueryStringify = T extends Record + ? { + // 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 = any> = ( + input: Record +) => IValidation +export type HeaderOutputType = T extends HeaderValidation ? O : never +type HeaderStringify = T extends Record + ? { + // 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 = ( + result: IValidation.ISuccess | IFailure>, + c: Context +) => Response | Promise | void | Promise | TypedResponse +export type Hook = ( + result: IValidation.ISuccess | IFailure, + c: Context +) => Response | Promise | void | Promise | TypedResponse + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type Validation = (input: unknown) => IValidation +export type OutputType = T extends Validation ? O : never + +interface TypiaValidator { + < + T extends QueryValidation, + O extends QueryOutputType, + E extends Env, + P extends string, + V extends { in: { query: QueryStringify }; out: { query: O } } = { + in: { query: QueryStringify } + out: { query: O } + } + >( + target: 'query', + validate: T, + hook?: HttpHook + ): MiddlewareHandler + + < + T extends HeaderValidation, + O extends HeaderOutputType, + E extends Env, + P extends string, + V extends { in: { header: HeaderStringify }; out: { header: O } } = { + in: { header: HeaderStringify } + out: { header: O } + } + >( + target: 'header', + validate: T, + hook?: HttpHook + ): MiddlewareHandler + + < + T extends Validation, + O extends OutputType, + Target extends Exclude, + 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 + ): MiddlewareHandler +} + +export const typiaValidator: TypiaValidator = ( + target: keyof ValidationTargets, + validate: (input: any) => IValidation, + hook?: Hook +): 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 + }) +} diff --git a/packages/typia-validator/src/index.ts b/packages/typia-validator/src/index.ts index e80912ed7..5ffb8a69b 100644 --- a/packages/typia-validator/src/index.ts +++ b/packages/typia-validator/src/index.ts @@ -29,11 +29,11 @@ export const typiaValidator = < validate: T, hook?: Hook ): MiddlewareHandler => - 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 diff --git a/packages/typia-validator/test/http.test.ts b/packages/typia-validator/test/http.test.ts new file mode 100644 index 000000000..61ff106d4 --- /dev/null +++ b/packages/typia-validator/test/http.test.ts @@ -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 extends Hono ? S : never + +describe('Basic', () => { + const app = new Hono() + + interface JsonSchema { + name: string + age: number + } + const validateJson = typia.createValidate() + + interface QuerySchema { + name?: string + } + const validateQuery = typia.http.createValidateQuery() + interface HeaderSchema { + 'x-Category': ('x' | 'y' | 'z')[] + } + const validateHeader = typia.http.createValidateHeaders() + + 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 + 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> + + 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() + + interface HeaderSchema { + 'X-Categories': (0 | 1 | 2)[] + } + const validateHeader = typia.http.createValidateHeaders() + + 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 + 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> + + 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() + + 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() + const validateQuery = typia.http.createValidateQuery() + const validateHeader = typia.http.createValidateHeaders() + + 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() + const validateQuery = typia.http.createValidateQuery() + const validateHeader = typia.http.createValidateHeaders() + + 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() + + const route = app.get('/', typiaValidator('header', validateHeader), (c) => { + const headers = c.req.valid('header') + return c.json(headers) + }) + + type Actual = ExtractSchema + type Expected = { + '/': { + $get: { + input: { + header: HeaderSchema + } + output: HeaderSchema + } + } + } + type verify = Expect> + }) +}) diff --git a/packages/typia-validator/test/index.test.ts b/packages/typia-validator/test/index.test.ts index f6296ad38..76db59c9d 100644 --- a/packages/typia-validator/test/index.test.ts +++ b/packages/typia-validator/test/index.test.ts @@ -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() + + 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!') + }) +}) diff --git a/yarn.lock b/yarn.lock index a5d5d156e..dd3c4665c 100644 --- a/yarn.lock +++ b/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": + version: 5.7.2 + resolution: "typescript@patch:typescript@npm%3A5.7.2#optional!builtin::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": version: 5.4.4 resolution: "typescript@patch:typescript@npm%3A5.4.4#optional!builtin::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