diff --git a/.changeset/quick-icons-add.md b/.changeset/quick-icons-add.md new file mode 100644 index 00000000..09732dae --- /dev/null +++ b/.changeset/quick-icons-add.md @@ -0,0 +1,5 @@ +--- +'@hono/valibot-validator': patch +--- + +Add supports async validation diff --git a/packages/valibot-validator/src/index.ts b/packages/valibot-validator/src/index.ts index 234fc8f3..15c3ef01 100644 --- a/packages/valibot-validator/src/index.ts +++ b/packages/valibot-validator/src/index.ts @@ -1,9 +1,9 @@ import type { Context, MiddlewareHandler, Env, ValidationTargets, Input as HonoInput } from 'hono' import { validator } from 'hono/validator' -import type { BaseSchema, Input, Output, SafeParseResult } from 'valibot' -import { safeParse } from 'valibot' +import type { BaseSchema, BaseSchemaAsync, Input, Output, SafeParseResult } from 'valibot' +import { safeParseAsync } from 'valibot' -type Hook = ( +type Hook = ( result: SafeParseResult, c: Context ) => Response | Promise | void | Promise @@ -11,7 +11,7 @@ type Hook = ( type HasUndefined = undefined extends T ? true : false export const vValidator = < - T extends BaseSchema, + T extends BaseSchema | BaseSchemaAsync, Target extends keyof ValidationTargets, E extends Env, P extends string, @@ -42,8 +42,8 @@ export const vValidator = < hook?: Hook ): MiddlewareHandler => // @ts-expect-error not typed well - validator(target, (value, c) => { - const result = safeParse(schema, value) + validator(target, async (value, c) => { + const result = await safeParseAsync(schema, value) if (hook) { const hookResult = hook(result, c) diff --git a/packages/valibot-validator/test/index.test.ts b/packages/valibot-validator/test/index.test.ts index a085b3d1..fd719c4a 100644 --- a/packages/valibot-validator/test/index.test.ts +++ b/packages/valibot-validator/test/index.test.ts @@ -1,6 +1,6 @@ import { Hono } from 'hono' import type { Equal, Expect } from 'hono/utils/types' -import { number, object, string, optional } from 'valibot' +import { number, object, string, optional, numberAsync, objectAsync, stringAsync, optionalAsync } from 'valibot' import { vValidator } from '../src' // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -162,3 +162,160 @@ describe('With Hook', () => { expect(res.status).toBe(400) }) }) + +describe('Async', () => { + const app = new Hono() + + const schemaAsync = objectAsync({ + name: stringAsync(), + age: numberAsync(), + }) + + const querySchemaAsync = optionalAsync( + objectAsync({ + search: optionalAsync(stringAsync()), + page: optionalAsync(numberAsync()), + }) + ) + + const route = app.post( + '/author', + vValidator('json', schemaAsync), + vValidator('query', querySchemaAsync), + (c) => { + const data = c.req.valid('json') + const query = c.req.valid('query') + + return c.json({ + success: true, + message: `${data.name} is ${data.age}, search is ${query?.search}`, + }) + } + ) + + type Actual = ExtractSchema + type Expected = { + '/author': { + $post: { + input: { + json: { + name: string + age: number + } + } & { + query?: + | { + search?: string | string[] | undefined + page?: string | string[] | undefined + } + | undefined + } + output: { + success: boolean + 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?search=hello', { + body: JSON.stringify({ + name: 'Superman', + age: 20, + }), + headers: { + 'Content-Type': 'application/json', + }, + 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, search is hello', + }) + }) + + it('Should return 400 response', async () => { + const req = new Request('http://localhost/author', { + body: JSON.stringify({ + name: 'Superman', + age: '20', + }), + headers: { + 'Content-Type': 'application/json', + }, + 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 Async', () => { + const app = new Hono() + + const schemaAsync = objectAsync({ + id: numberAsync(), + title: stringAsync(), + }) + + app.post( + '/post', + vValidator('json', schemaAsync, (result, c) => { + if (!result.success) { + return c.text('Invalid!', 400) + } + const data = result.output + 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', + }), + headers: { + 'Content-Type': 'application/json', + }, + 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', + }), + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + }) + const res = await app.request(req) + expect(res).not.toBeNull() + expect(res.status).toBe(400) + }) +})