import 'reflect-metadata' import type { ClassConstructor, ClassTransformOptions } from 'class-transformer' import { plainToClass } from 'class-transformer' import type { ValidationError } from 'class-validator' import { validate } from 'class-validator' import type { Context, Env, Input, MiddlewareHandler, TypedResponse, ValidationTargets } from 'hono' import { validator } from 'hono/validator' /** * Hono middleware that validates incoming data using class-validator(https://github.com/typestack/class-validator). * * --- * * No Hook * * ```ts * import { classValidator } from '@hono/class-validator' * import { IsInt, IsString } from 'class-validator' * * class CreateUserDto { * @IsString() * name!: string; * * @IsInt() * age!: number; * } * * * const route = app.post('/user', classValidator('json', CreateUserDto), (c) => { * const user = c.req.valid('json') * return c.json({ success: true, message: `${user.name} is ${user.age}` }) * }) * ``` * * --- * Hook * * ```ts * import { classValidator } from '@hono/class-validator' * import { IsInt, IsString } from 'class-validator' * * class CreateUserDto { * @IsString() * name!: string; * * @IsInt() * age!: number; * } * * app.post( * '/user', * classValidator('json', CreateUserDto, (result, c) => { * if (!result.success) { * return c.text('Invalid!', 400) * } * }) * //... * ) * ``` */ type Hook< T, E extends Env, P extends string, Target extends keyof ValidationTargets = keyof ValidationTargets, O = object, > = ( result: ({ success: true } | { success: false; errors: ValidationError[] }) & { data: T target: Target }, c: Context ) => Response | void | TypedResponse | Promise> type HasUndefined = undefined extends T ? true : false type HasClassConstructor = ClassConstructor extends T ? true : false export type StaticObject> = { [K in keyof InstanceType]: HasClassConstructor[K]> extends true ? StaticObject[K]> : InstanceType[K] } const parseAndValidate = async >( dto: T, obj: object, options: ClassTransformOptions ): Promise< { success: false; errors: ValidationError[] } | { success: true; output: InstanceType } > => { // tranform the literal object to class object const objInstance = plainToClass(dto, obj, options) // validating and check the errors, throw the errors if exist const errors = await validate(objInstance) // errors is an array of validation errors if (errors.length > 0) { return { success: false, errors, } } return { success: true, output: objInstance as InstanceType } } export const classValidator = < T extends ClassConstructor, Output extends InstanceType = InstanceType, Target extends keyof ValidationTargets = keyof ValidationTargets, E extends Env = Env, P extends string = string, In = StaticObject, I extends Input = { in: HasUndefined extends true ? { [K in Target]?: K extends 'json' ? In : HasUndefined extends true ? { [K2 in keyof In]?: ValidationTargets[K][K2] } : { [K2 in keyof In]: ValidationTargets[K][K2] } } : { [K in Target]: K extends 'json' ? In : HasUndefined extends true ? { [K2 in keyof In]?: ValidationTargets[K][K2] } : { [K2 in keyof In]: ValidationTargets[K][K2] } } out: { [K in Target]: Output } }, V extends I = I, >( target: Target, dataType: T, hook?: Hook, options: ClassTransformOptions = { enableImplicitConversion: false } ): MiddlewareHandler => // @ts-expect-error not typed well validator(target, async (data, c) => { const result = await parseAndValidate(dataType, data, options) if (hook) { const hookResult = hook({ ...result, data, target }, c) if (hookResult instanceof Response || hookResult instanceof Promise) { if ('response' in hookResult) { return hookResult.response } return hookResult } } if (!result.success) { return c.json({ errors: result.errors }, 400) } return result.output })