honojs-middleware/packages/typia-validator/src/http.ts

188 lines
5.3 KiB
TypeScript

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
})
}