Compare commits

...

9 Commits

Author SHA1 Message Date
Andrei faf12ffd0b
Merge 13d07079df into 928f8cd5b8 2025-04-27 20:35:13 +02:00
github-actions[bot] 928f8cd5b8
Version Packages (#1144)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-27 20:38:32 +09:00
Shotaro Nakamura 1765a9a3aa
fix(node-ws): make adapter uncrashable (#1141)
* fix(node-ws): make adapter uncrashable

* add changeset

* unnessesary diff

* update yarn.lock

* make changeset patch
2025-04-27 20:34:43 +09:00
github-actions[bot] 247f7705b3
Version Packages (#1143)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-27 20:30:24 +09:00
Yusuke Wada 8ed99d9d79
feat(zod-validator): add `validationFunction` option (#1140)
Co-authored-by: migawka <migawka@amadeustech.dev>
2025-04-27 20:12:13 +09:00
Yusuke Wada b9fa57530a
chore: format codes (#1142) 2025-04-27 19:28:24 +09:00
Andrei Bobkov 13d07079df
chore: add changeset 2025-04-25 19:35:34 +02:00
Andrew Bobkov 7831f2bf26 fix(arktype-validator): add restricted fields that are not returned in the "data" field of the error 2025-04-25 17:27:55 +00:00
Andrew Bobkov 1b7e645c3d fix(arktype-validator): add failing test for cookie header 2025-04-25 17:17:39 +00:00
30 changed files with 402 additions and 161 deletions

View File

@ -0,0 +1,5 @@
---
'@hono/arktype-validator': patch
---
Don't return restricted data fields on error responses

View File

@ -67,7 +67,7 @@ export function ajvValidator<
T, T,
Target extends keyof ValidationTargets, Target extends keyof ValidationTargets,
E extends Env = Env, E extends Env = Env,
P extends string = string P extends string = string,
>( >(
target: Target, target: Target,
schema: JSONSchemaType<T>, schema: JSONSchemaType<T>,

View File

@ -35,6 +35,17 @@ describe('Basic', () => {
} }
) )
app.get(
'/headers',
arktypeValidator(
'header',
type({
'User-Agent': 'string',
})
),
(c) => c.json({ success: true, userAgent: c.header('User-Agent') })
)
type Actual = ExtractSchema<typeof route> type Actual = ExtractSchema<typeof route>
type Expected = { type Expected = {
'/author': { '/author': {
@ -98,6 +109,22 @@ describe('Basic', () => {
const data = (await res.json()) as { success: boolean } const data = (await res.json()) as { success: boolean }
expect(data['success']).toBe(false) expect(data['success']).toBe(false)
}) })
it("doesn't return cookies after headers validation", async () => {
const req = new Request('http://localhost/headers', {
headers: {
'User-Agent': 'invalid',
Cookie: 'SECRET=123',
},
})
const res = await app.request(req)
expect(res).not.toBeNull()
expect(res.status).toBe(400)
const data = (await res.json()) as { succcess: false; errors: type.errors }
expect(data.errors).toHaveLength(1)
expect(data.errors[0].data).not.toHaveProperty('cookie')
})
}) })
describe('With Hook', () => { describe('With Hook', () => {

View File

@ -10,6 +10,10 @@ export type Hook<T, E extends Env, P extends string, O = {}> = (
type HasUndefined<T> = undefined extends T ? true : false type HasUndefined<T> = undefined extends T ? true : false
const RESTRICTED_DATA_FIELDS = {
header: ['cookie'],
}
export const arktypeValidator = < export const arktypeValidator = <
T extends Type, T extends Type,
Target extends keyof ValidationTargets, Target extends keyof ValidationTargets,
@ -23,7 +27,7 @@ export const arktypeValidator = <
} = { } = {
in: HasUndefined<I> extends true ? { [K in Target]?: I } : { [K in Target]: I } in: HasUndefined<I> extends true ? { [K in Target]?: I } : { [K in Target]: I }
out: { [K in Target]: O } out: { [K in Target]: O }
} },
>( >(
target: Target, target: Target,
schema: T, schema: T,
@ -54,7 +58,31 @@ export const arktypeValidator = <
return c.json( return c.json(
{ {
success: false, success: false,
errors: out, errors:
target in RESTRICTED_DATA_FIELDS
? out.map((error) => {
const restrictedFields =
RESTRICTED_DATA_FIELDS[target as keyof typeof RESTRICTED_DATA_FIELDS] || []
if (
error &&
typeof error === 'object' &&
'data' in error &&
typeof error.data === 'object' &&
error.data !== null &&
!Array.isArray(error.data)
) {
const dataCopy = { ...(error.data as Record<string, unknown>) }
for (const field of restrictedFields) {
delete dataCopy[field]
}
error.data = dataCopy
}
return error
})
: out,
}, },
400 400
) )

View File

@ -225,7 +225,7 @@ export function SessionProvider(props: SessionProviderProps) {
} }
return updatedSession return updatedSession
}, },
} as SessionContextValue), }) as SessionContextValue,
[session, loading, setSession] [session, loading, setSession]
) )

View File

@ -64,7 +64,7 @@ type Hook<
E extends Env, E extends Env,
P extends string, P extends string,
Target extends keyof ValidationTargets = keyof ValidationTargets, Target extends keyof ValidationTargets = keyof ValidationTargets,
O = object O = object,
> = ( > = (
result: ({ success: true } | { success: false; errors: ValidationError[] }) & { result: ({ success: true } | { success: false; errors: ValidationError[] }) & {
data: T data: T
@ -119,19 +119,19 @@ export const classValidator = <
[K in Target]?: K extends 'json' [K in Target]?: K extends 'json'
? In ? In
: HasUndefined<keyof ValidationTargets[K]> extends true : HasUndefined<keyof ValidationTargets[K]> extends true
? { [K2 in keyof In]?: ValidationTargets[K][K2] } ? { [K2 in keyof In]?: ValidationTargets[K][K2] }
: { [K2 in keyof In]: ValidationTargets[K][K2] } : { [K2 in keyof In]: ValidationTargets[K][K2] }
} }
: { : {
[K in Target]: K extends 'json' [K in Target]: K extends 'json'
? In ? In
: HasUndefined<keyof ValidationTargets[K]> extends true : HasUndefined<keyof ValidationTargets[K]> extends true
? { [K2 in keyof In]?: ValidationTargets[K][K2] } ? { [K2 in keyof In]?: ValidationTargets[K][K2] }
: { [K2 in keyof In]: ValidationTargets[K][K2] } : { [K2 in keyof In]: ValidationTargets[K][K2] }
} }
out: { [K in Target]: Output } out: { [K in Target]: Output }
}, },
V extends I = I V extends I = I,
>( >(
target: Target, target: Target,
dataType: T, dataType: T,

View File

@ -32,7 +32,7 @@ export const conformValidator = <
form: { [K in keyof In]: FormTargetValue } form: { [K in keyof In]: FormTargetValue }
} }
out: { form: GetSuccessSubmission<Out> } out: { form: GetSuccessSubmission<Out> }
} },
>( >(
parse: F, parse: F,
hook?: Hook<F, E, P> hook?: Hook<F, E, P>

View File

@ -19,18 +19,18 @@ export const effectValidator = <
[K in Target]?: K extends 'json' [K in Target]?: K extends 'json'
? In ? In
: HasUndefined<keyof ValidationTargets[K]> extends true : HasUndefined<keyof ValidationTargets[K]> extends true
? { [K2 in keyof In]?: ValidationTargets[K][K2] } ? { [K2 in keyof In]?: ValidationTargets[K][K2] }
: { [K2 in keyof In]: ValidationTargets[K][K2] } : { [K2 in keyof In]: ValidationTargets[K][K2] }
} }
: { : {
[K in Target]: K extends 'json' [K in Target]: K extends 'json'
? In ? In
: HasUndefined<keyof ValidationTargets[K]> extends true : HasUndefined<keyof ValidationTargets[K]> extends true
? { [K2 in keyof In]?: ValidationTargets[K][K2] } ? { [K2 in keyof In]?: ValidationTargets[K][K2] }
: { [K2 in keyof In]: ValidationTargets[K][K2] } : { [K2 in keyof In]: ValidationTargets[K][K2] }
} }
out: { [K in Target]: Out } out: { [K in Target]: Out }
} },
>( >(
target: Target, target: Target,
schema: S.Schema<Type, Encoded, never> schema: S.Schema<Type, Encoded, never>

View File

@ -35,7 +35,7 @@ export interface Emitter<EPMap extends EventPayloadMap> {
export const defineHandler = < export const defineHandler = <
EPMap extends EventPayloadMap, EPMap extends EventPayloadMap,
Key extends keyof EPMap, Key extends keyof EPMap,
E extends Env = Env E extends Env = Env,
>( >(
handler: EventHandler<EPMap[Key], E> handler: EventHandler<EPMap[Key], E>
): EventHandler<EPMap[Key], E> => { ): EventHandler<EPMap[Key], E> => {

View File

@ -1,5 +1,11 @@
# @hono/node-ws # @hono/node-ws
## 1.1.3
### Patch Changes
- [#1141](https://github.com/honojs/middleware/pull/1141) [`1765a9a3aa1ab6aa59b0efd2d161b7bfaa565ef7`](https://github.com/honojs/middleware/commit/1765a9a3aa1ab6aa59b0efd2d161b7bfaa565ef7) Thanks [@nakasyou](https://github.com/nakasyou)! - Make it uncrashable
## 1.1.2 ## 1.1.2
### Patch Changes ### Patch Changes

View File

@ -1,6 +1,6 @@
{ {
"name": "@hono/node-ws", "name": "@hono/node-ws",
"version": "1.1.2", "version": "1.1.3",
"description": "WebSocket helper for Node.js", "description": "WebSocket helper for Node.js",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

View File

@ -214,6 +214,26 @@ describe('WebSocket helper', () => {
}) })
}) })
it('Should onError works well', async () => {
const mainPromise = new Promise<unknown>((resolve) =>
app.get(
'/',
upgradeWebSocket(
() => {
throw 0
},
{
onError(err) {
resolve(err)
},
}
)
)
)
const ws = new WebSocket('ws://localhost:3030/')
expect(await mainPromise).toBe(0)
})
describe('Types', () => { describe('Types', () => {
it('Should not throw a type error with an app with Variables generics', () => { it('Should not throw a type error with an app with Variables generics', () => {
const app = new Hono<{ const app = new Hono<{

View File

@ -1,5 +1,5 @@
import type { Hono } from 'hono' import type { Hono } from 'hono'
import type { UpgradeWebSocket, WSContext } from 'hono/ws' import type { UpgradeWebSocket, WSContext, WSEvents } from 'hono/ws'
import type { WebSocket } from 'ws' import type { WebSocket } from 'ws'
import { WebSocketServer } from 'ws' import { WebSocketServer } from 'ws'
import type { IncomingMessage } from 'http' import type { IncomingMessage } from 'http'
@ -9,7 +9,12 @@ import type { Duplex } from 'node:stream'
import { CloseEvent } from './events' import { CloseEvent } from './events'
export interface NodeWebSocket { export interface NodeWebSocket {
upgradeWebSocket: UpgradeWebSocket<WebSocket> upgradeWebSocket: UpgradeWebSocket<
WebSocket,
{
onError: (err: unknown) => void
}
>
injectWebSocket(server: Server | Http2Server | Http2SecureServer): void injectWebSocket(server: Server | Http2Server | Http2SecureServer): void
} }
export interface NodeWebSocketInit { export interface NodeWebSocketInit {
@ -80,7 +85,7 @@ export const createNodeWebSocket = (init: NodeWebSocketInit): NodeWebSocket => {
}) })
}) })
}, },
upgradeWebSocket: (createEvents) => upgradeWebSocket: (createEvents, options) =>
async function upgradeWebSocket(c, next) { async function upgradeWebSocket(c, next) {
if (c.req.header('upgrade')?.toLowerCase() !== 'websocket') { if (c.req.header('upgrade')?.toLowerCase() !== 'websocket') {
// Not websocket // Not websocket
@ -91,7 +96,14 @@ export const createNodeWebSocket = (init: NodeWebSocketInit): NodeWebSocket => {
const response = new Response() const response = new Response()
;(async () => { ;(async () => {
const ws = await nodeUpgradeWebSocket(c.env.incoming, response) const ws = await nodeUpgradeWebSocket(c.env.incoming, response)
const events = await createEvents(c) let events: WSEvents<WebSocket>
try {
events = await createEvents(c)
} catch (e) {
;(options?.onError ?? console.error)(e)
ws.close()
return
}
const ctx: WSContext<WebSocket> = { const ctx: WSContext<WebSocket> = {
binaryType: 'arraybuffer', binaryType: 'arraybuffer',
@ -110,32 +122,48 @@ export const createNodeWebSocket = (init: NodeWebSocketInit): NodeWebSocket => {
}, },
url: new URL(c.req.url), url: new URL(c.req.url),
} }
events.onOpen?.(new Event('open'), ctx) try {
events?.onOpen?.(new Event('open'), ctx)
} catch (e) {
;(options?.onError ?? console.error)(e)
}
ws.on('message', (data, isBinary) => { ws.on('message', (data, isBinary) => {
const datas = Array.isArray(data) ? data : [data] const datas = Array.isArray(data) ? data : [data]
for (const data of datas) { for (const data of datas) {
events.onMessage?.( try {
new MessageEvent('message', { events?.onMessage?.(
data: isBinary new MessageEvent('message', {
? data instanceof ArrayBuffer data: isBinary
? data ? data instanceof ArrayBuffer
: data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) ? data
: data.toString('utf-8'), : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)
}), : data.toString('utf-8'),
ctx }),
) ctx
)
} catch (e) {
;(options?.onError ?? console.error)(e)
}
} }
}) })
ws.on('close', (code, reason) => { ws.on('close', (code, reason) => {
events.onClose?.(new CloseEvent('close', { code, reason: reason.toString() }), ctx) try {
events?.onClose?.(new CloseEvent('close', { code, reason: reason.toString() }), ctx)
} catch (e) {
;(options?.onError ?? console.error)(e)
}
}) })
ws.on('error', (error) => { ws.on('error', (error) => {
events.onError?.( try {
new ErrorEvent('error', { events?.onError?.(
error: error, new ErrorEvent('error', {
}), error: error,
ctx }),
) ctx
)
} catch (e) {
;(options?.onError ?? console.error)(e)
}
}) })
})() })()

View File

@ -162,7 +162,7 @@ export interface TwitchUserResponse {
view_count: number view_count: number
email: string email: string
created_at: string created_at: string
} },
] ]
} }

View File

@ -44,7 +44,7 @@ const getMetricConstructor = (type: MetricOptions['type']) =>
({ ({
counter: Counter, counter: Counter,
histogram: Histogram, histogram: Histogram,
}[type]) })[type]
export const createStandardMetrics = ({ export const createStandardMetrics = ({
registry, registry,

View File

@ -9,9 +9,8 @@ import * as zodSchemas from './__schemas__/zod'
import { sValidator } from '.' import { sValidator } from '.'
type ExtractSchema<T> = T extends Hono<infer _, infer S> ? S : never type ExtractSchema<T> = T extends Hono<infer _, infer S> ? S : never
type MergeDiscriminatedUnion<U> = UnionToIntersection<U> extends infer O type MergeDiscriminatedUnion<U> =
? { [K in keyof O]: O[K] } UnionToIntersection<U> extends infer O ? { [K in keyof O]: O[K] } : never
: never
const libs = ['valibot', 'zod', 'arktype'] as const const libs = ['valibot', 'zod', 'arktype'] as const
const schemasByLibrary = { const schemasByLibrary = {

View File

@ -10,7 +10,7 @@ type Hook<
E extends Env, E extends Env,
P extends string, P extends string,
Target extends keyof ValidationTargets = keyof ValidationTargets, Target extends keyof ValidationTargets = keyof ValidationTargets,
O = {} O = {},
> = ( > = (
result: ( result: (
| { success: true; data: T } | { success: true; data: T }
@ -42,7 +42,7 @@ const sValidator = <
} }
out: { [K in Target]: Out } out: { [K in Target]: Out }
}, },
V extends I = I V extends I = I,
>( >(
target: Target, target: Target,
schema: Schema, schema: Schema,

View File

@ -148,7 +148,7 @@ export const renderSwaggerUIOptions = (options: DistSwaggerUIOptions) => {
return '' return ''
} }
}) })
.filter(item => item !== '') .filter((item) => item !== '')
.join(',') .join(',')
return optionsStrings return optionsStrings

View File

@ -68,7 +68,7 @@ export function tbValidator<
V extends { V extends {
in: { [K in Target]: Static<T> } in: { [K in Target]: Static<T> }
out: { [K in Target]: ExcludeResponseType<Static<T>> } out: { [K in Target]: ExcludeResponseType<Static<T>> }
} },
>( >(
target: Target, target: Target,
schema: T, schema: T,

View File

@ -11,54 +11,57 @@ interface IFailure<T> {
type BaseType<T> = T extends string type BaseType<T> = T extends string
? string ? string
: T extends number : T extends number
? number ? number
: T extends boolean : T extends boolean
? boolean ? boolean
: T extends symbol : T extends symbol
? symbol ? symbol
: T extends bigint : T extends bigint
? bigint ? bigint
: T : T
type Parsed<T> = T extends Record<string | number, any> type Parsed<T> =
? { T extends Record<string | number, any>
[K in keyof T]-?: T[K] extends (infer U)[] ? {
? (BaseType<U> | null | undefined)[] | undefined [K in keyof T]-?: T[K] extends (infer U)[]
: BaseType<T[K]> | null | undefined ? (BaseType<U> | null | undefined)[] | undefined
} : BaseType<T[K]> | null | undefined
: BaseType<T> }
: BaseType<T>
export type QueryValidation<O extends Record<string | number, any> = any> = ( export type QueryValidation<O extends Record<string | number, any> = any> = (
input: string | URLSearchParams input: string | URLSearchParams
) => IValidation<O> ) => IValidation<O>
export type QueryOutputType<T> = T extends QueryValidation<infer O> ? O : never export type QueryOutputType<T> = T extends QueryValidation<infer O> ? O : never
type QueryStringify<T> = T extends Record<string | number, any> type QueryStringify<T> =
? { T extends Record<string | number, any>
// Suppress to split union types ? {
[K in keyof T]: [T[K]] extends [bigint | number | boolean] // Suppress to split union types
? `${T[K]}` [K in keyof T]: [T[K]] extends [bigint | number | boolean]
: T[K] extends (infer U)[] ? `${T[K]}`
? [U] extends [bigint | number | boolean] : T[K] extends (infer U)[]
? `${U}`[] ? [U] extends [bigint | number | boolean]
: T[K] ? `${U}`[]
: T[K] : T[K]
} : T[K]
: T }
: T
export type HeaderValidation<O extends Record<string | number, any> = any> = ( export type HeaderValidation<O extends Record<string | number, any> = any> = (
input: Record<string, string | string[] | undefined> input: Record<string, string | string[] | undefined>
) => IValidation<O> ) => IValidation<O>
export type HeaderOutputType<T> = T extends HeaderValidation<infer O> ? O : never export type HeaderOutputType<T> = T extends HeaderValidation<infer O> ? O : never
type HeaderStringify<T> = T extends Record<string | number, any> type HeaderStringify<T> =
? { T extends Record<string | number, any>
// Suppress to split union types ? {
[K in keyof T]: [T[K]] extends [bigint | number | boolean] // Suppress to split union types
? `${T[K]}` [K in keyof T]: [T[K]] extends [bigint | number | boolean]
: T[K] extends (infer U)[] ? `${T[K]}`
? [U] extends [bigint | number | boolean] : T[K] extends (infer U)[]
? `${U}` ? [U] extends [bigint | number | boolean]
: U ? `${U}`
: T[K] : U
} : T[K]
: T }
: T
export type HttpHook<T, E extends Env, P extends string, O = {}> = ( export type HttpHook<T, E extends Env, P extends string, O = {}> = (
result: IValidation.ISuccess<T> | IFailure<Parsed<T>>, result: IValidation.ISuccess<T> | IFailure<Parsed<T>>,
@ -82,7 +85,7 @@ interface TypiaValidator {
V extends { in: { query: QueryStringify<O> }; out: { query: O } } = { V extends { in: { query: QueryStringify<O> }; out: { query: O } } = {
in: { query: QueryStringify<O> } in: { query: QueryStringify<O> }
out: { query: O } out: { query: O }
} },
>( >(
target: 'query', target: 'query',
validate: T, validate: T,
@ -97,7 +100,7 @@ interface TypiaValidator {
V extends { in: { header: HeaderStringify<O> }; out: { header: O } } = { V extends { in: { header: HeaderStringify<O> }; out: { header: O } } = {
in: { header: HeaderStringify<O> } in: { header: HeaderStringify<O> }
out: { header: O } out: { header: O }
} },
>( >(
target: 'header', target: 'header',
validate: T, validate: T,
@ -116,7 +119,7 @@ interface TypiaValidator {
} = { } = {
in: { [K in Target]: O } in: { [K in Target]: O }
out: { [K in Target]: O } out: { [K in Target]: O }
} },
>( >(
target: Target, target: Target,
validate: T, validate: T,

View File

@ -23,7 +23,7 @@ export const typiaValidator = <
} = { } = {
in: { [K in Target]: O } in: { [K in Target]: O }
out: { [K in Target]: O } out: { [K in Target]: O }
} },
>( >(
target: Target, target: Target,
validate: T, validate: T,

View File

@ -14,7 +14,7 @@ export type Hook<
E extends Env, E extends Env,
P extends string, P extends string,
Target extends keyof ValidationTargets = keyof ValidationTargets, Target extends keyof ValidationTargets = keyof ValidationTargets,
O = {} O = {},
> = ( > = (
result: SafeParseResult<T> & { result: SafeParseResult<T> & {
target: Target target: Target
@ -45,7 +45,7 @@ export const vValidator = <
} }
out: { [K in Target]: Out } out: { [K in Target]: Out }
}, },
V extends I = I V extends I = I,
>( >(
target: Target, target: Target,
schema: T, schema: T,

View File

@ -1,5 +1,12 @@
# @hono/zod-openapi # @hono/zod-openapi
## 0.19.6
### Patch Changes
- Updated dependencies [[`8ed99d9d791ed6bd8b897c705289b0464947e632`](https://github.com/honojs/middleware/commit/8ed99d9d791ed6bd8b897c705289b0464947e632)]:
- @hono/zod-validator@0.5.0
## 0.19.5 ## 0.19.5
### Patch Changes ### Patch Changes

View File

@ -1,6 +1,6 @@
{ {
"name": "@hono/zod-openapi", "name": "@hono/zod-openapi",
"version": "0.19.5", "version": "0.19.6",
"description": "A wrapper class of Hono which supports OpenAPI.", "description": "A wrapper class of Hono which supports OpenAPI.",
"type": "module", "type": "module",
"module": "dist/index.js", "module": "dist/index.js",

View File

@ -73,7 +73,7 @@ type IsForm<T> = T extends string
type ReturnJsonOrTextOrResponse< type ReturnJsonOrTextOrResponse<
ContentType, ContentType,
Content, Content,
Status extends keyof StatusCodeRangeDefinitions | StatusCode Status extends keyof StatusCodeRangeDefinitions | StatusCode,
> = ContentType extends string > = ContentType extends string
? ContentType extends `application/${infer Start}json${infer _End}` ? ContentType extends `application/${infer Start}json${infer _End}`
? Start extends '' | `${string}+` | `vnd.${string}+` ? Start extends '' | `${string}+` | `vnd.${string}+`
@ -88,8 +88,8 @@ type ReturnJsonOrTextOrResponse<
> >
: never : never
: ContentType extends `text/plain${infer _Rest}` : ContentType extends `text/plain${infer _Rest}`
? TypedResponse<Content, ExtractStatusCode<Status>, 'text'> ? TypedResponse<Content, ExtractStatusCode<Status>, 'text'>
: Response : Response
: never : never
type RequestPart<R extends RouteConfig, Part extends string> = Part extends keyof R['request'] type RequestPart<R extends RouteConfig, Part extends string> = Part extends keyof R['request']
@ -101,7 +101,7 @@ type HasUndefined<T> = undefined extends T ? true : false
type InputTypeBase< type InputTypeBase<
R extends RouteConfig, R extends RouteConfig,
Part extends string, Part extends string,
Type extends keyof ValidationTargets Type extends keyof ValidationTargets,
> = R['request'] extends RequestTypes > = R['request'] extends RequestTypes
? RequestPart<R, Part> extends ZodType ? RequestPart<R, Part> extends ZodType
? { ? {
@ -125,22 +125,22 @@ type InputTypeJson<R extends RouteConfig> = R['request'] extends RequestTypes
? IsJson<keyof R['request']['body']['content']> extends never ? IsJson<keyof R['request']['body']['content']> extends never
? {} ? {}
: R['request']['body']['content'][keyof R['request']['body']['content']] extends Record< : R['request']['body']['content'][keyof R['request']['body']['content']] extends Record<
'schema', 'schema',
ZodSchema<any> ZodSchema<any>
> >
? { ? {
in: { in: {
json: z.input< json: z.input<
R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
> >
}
out: {
json: z.output<
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
>
}
} }
out: { : {}
json: z.output<
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
>
}
}
: {}
: {} : {}
: {} : {}
: {} : {}
@ -151,22 +151,22 @@ type InputTypeForm<R extends RouteConfig> = R['request'] extends RequestTypes
? IsForm<keyof R['request']['body']['content']> extends never ? IsForm<keyof R['request']['body']['content']> extends never
? {} ? {}
: R['request']['body']['content'][keyof R['request']['body']['content']] extends Record< : R['request']['body']['content'][keyof R['request']['body']['content']] extends Record<
'schema', 'schema',
ZodSchema<any> ZodSchema<any>
> >
? { ? {
in: { in: {
form: z.input< form: z.input<
R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
> >
}
out: {
form: z.output<
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
>
}
} }
out: { : {}
form: z.output<
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
>
}
}
: {}
: {} : {}
: {} : {}
: {} : {}
@ -249,8 +249,8 @@ type HonoInit<E extends Env> = ConstructorParameters<typeof Hono>[0] & OpenAPIHo
type AsArray<T> = T extends undefined // TODO move to utils? type AsArray<T> = T extends undefined // TODO move to utils?
? [] ? []
: T extends any[] : T extends any[]
? T ? T
: [T] : [T]
/** /**
* Like simplify but recursive * Like simplify but recursive
@ -265,17 +265,14 @@ export type DeepSimplify<T> = {
/** /**
* Helper to infer generics from {@link MiddlewareHandler} * Helper to infer generics from {@link MiddlewareHandler}
*/ */
export type OfHandlerType<T extends MiddlewareHandler> = T extends MiddlewareHandler< export type OfHandlerType<T extends MiddlewareHandler> =
infer E, T extends MiddlewareHandler<infer E, infer P, infer I>
infer P, ? {
infer I env: E
> path: P
? { input: I
env: E }
path: P : never
input: I
}
: never
/** /**
* Reduce a tuple of middleware handlers into a single * Reduce a tuple of middleware handlers into a single
@ -285,7 +282,7 @@ export type OfHandlerType<T extends MiddlewareHandler> = T extends MiddlewareHan
export type MiddlewareToHandlerType<M extends MiddlewareHandler<any, any, any>[]> = M extends [ export type MiddlewareToHandlerType<M extends MiddlewareHandler<any, any, any>[]> = M extends [
infer First, infer First,
infer Second, infer Second,
...infer Rest ...infer Rest,
] ]
? First extends MiddlewareHandler<any, any, any> ? First extends MiddlewareHandler<any, any, any>
? Second extends MiddlewareHandler<any, any, any> ? Second extends MiddlewareHandler<any, any, any>
@ -297,23 +294,22 @@ export type MiddlewareToHandlerType<M extends MiddlewareHandler<any, any, any>[]
OfHandlerType<First>['path'], // Keep path from First OfHandlerType<First>['path'], // Keep path from First
OfHandlerType<First>['input'] // Keep input from First OfHandlerType<First>['input'] // Keep input from First
>, >,
...Rest ...Rest,
] ]
> >
: never : never
: never : never
: never : never
: M extends [infer Last] : M extends [infer Last]
? Last // Return the last remaining handler in the array ? Last // Return the last remaining handler in the array
: MiddlewareHandler<Env> : MiddlewareHandler<Env>
type RouteMiddlewareParams<R extends RouteConfig> = OfHandlerType< type RouteMiddlewareParams<R extends RouteConfig> = OfHandlerType<
MiddlewareToHandlerType<AsArray<R['middleware']>> MiddlewareToHandlerType<AsArray<R['middleware']>>
> >
export type RouteConfigToEnv<R extends RouteConfig> = RouteMiddlewareParams<R> extends never export type RouteConfigToEnv<R extends RouteConfig> =
? Env RouteMiddlewareParams<R> extends never ? Env : RouteMiddlewareParams<R>['env']
: RouteMiddlewareParams<R>['env']
export type RouteHandler< export type RouteHandler<
R extends RouteConfig, R extends RouteConfig,
@ -324,7 +320,7 @@ export type RouteHandler<
InputTypeCookie<R> & InputTypeCookie<R> &
InputTypeForm<R> & InputTypeForm<R> &
InputTypeJson<R>, InputTypeJson<R>,
P extends string = ConvertPathType<R['path']> P extends string = ConvertPathType<R['path']>,
> = Handler< > = Handler<
E, E,
P, P,
@ -352,7 +348,7 @@ export type RouteHook<
InputTypeCookie<R> & InputTypeCookie<R> &
InputTypeForm<R> & InputTypeForm<R> &
InputTypeJson<R>, InputTypeJson<R>,
P extends string = ConvertPathType<R['path']> P extends string = ConvertPathType<R['path']>,
> = Hook< > = Hook<
I, I,
E, E,
@ -371,7 +367,7 @@ export type OpenAPIObjectConfigure<E extends Env, P extends string> =
export class OpenAPIHono< export class OpenAPIHono<
E extends Env = Env, E extends Env = Env,
S extends Schema = {}, S extends Schema = {},
BasePath extends string = '/' BasePath extends string = '/',
> extends Hono<E, S, BasePath> { > extends Hono<E, S, BasePath> {
openAPIRegistry: OpenAPIRegistry openAPIRegistry: OpenAPIRegistry
defaultHook?: OpenAPIHonoOptions<E>['defaultHook'] defaultHook?: OpenAPIHonoOptions<E>['defaultHook']
@ -421,7 +417,7 @@ export class OpenAPIHono<
InputTypeCookie<R> & InputTypeCookie<R> &
InputTypeForm<R> & InputTypeForm<R> &
InputTypeJson<R>, InputTypeJson<R>,
P extends string = ConvertPathType<R['path']> P extends string = ConvertPathType<R['path']>,
>( >(
{ middleware: routeMiddleware, hide, ...route }: R, { middleware: routeMiddleware, hide, ...route }: R,
handler: Handler< handler: Handler<
@ -609,7 +605,7 @@ export class OpenAPIHono<
SubPath extends string, SubPath extends string,
SubEnv extends Env, SubEnv extends Env,
SubSchema extends Schema, SubSchema extends Schema,
SubBasePath extends string SubBasePath extends string,
>( >(
path: SubPath, path: SubPath,
app: Hono<SubEnv, SubSchema, SubBasePath> app: Hono<SubEnv, SubSchema, SubBasePath>
@ -619,7 +615,7 @@ export class OpenAPIHono<
SubPath extends string, SubPath extends string,
SubEnv extends Env, SubEnv extends Env,
SubSchema extends Schema, SubSchema extends Schema,
SubBasePath extends string SubBasePath extends string,
>( >(
path: SubPath, path: SubPath,
app?: Hono<SubEnv, SubSchema, SubBasePath> app?: Hono<SubEnv, SubSchema, SubBasePath>

View File

@ -1,5 +1,11 @@
# @hono/zod-validator # @hono/zod-validator
## 0.5.0
### Minor Changes
- [#1140](https://github.com/honojs/middleware/pull/1140) [`8ed99d9d791ed6bd8b897c705289b0464947e632`](https://github.com/honojs/middleware/commit/8ed99d9d791ed6bd8b897c705289b0464947e632) Thanks [@yusukebe](https://github.com/yusukebe)! - feat: add `validationFunction` option
## 0.4.3 ## 0.4.3
### Patch Changes ### Patch Changes

View File

@ -68,6 +68,30 @@ app.post(
) )
``` ```
### Custom validation function
By default, this Validator validates values using `.safeParseAsync`.
```ts
await schema.safeParseAsync(value)
```
But, if you want to use the [`.passthrough`](https://zod.dev/?id=passthrough), you can specify your own function in `validationFunction`.
```ts
app.post(
'/',
zValidator('json', schema, undefined, {
validationFunction: async (schema, value) => {
return await schema.passthrough().safeParseAsync(value)
},
}),
(c) => {
// ...
}
)
```
## Author ## Author
Yusuke Wada <https://github.com/yusukebe> Yusuke Wada <https://github.com/yusukebe>

View File

@ -1,6 +1,6 @@
{ {
"name": "@hono/zod-validator", "name": "@hono/zod-validator",
"version": "0.4.3", "version": "0.5.0",
"description": "Validator middleware using Zod", "description": "Validator middleware using Zod",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

View File

@ -378,3 +378,85 @@ describe('Case-Insensitive Headers', () => {
type verify = Expect<Equal<Expected, Actual>> type verify = Expect<Equal<Expected, Actual>>
}) })
}) })
describe('With options + validationFunction', () => {
const app = new Hono()
const jsonSchema = z.object({
name: z.string(),
age: z.number(),
})
const route = app
.post('/', zValidator('json', jsonSchema), (c) => {
const data = c.req.valid('json')
return c.json({
success: true,
data,
})
})
.post(
'/extended',
zValidator('json', jsonSchema, undefined, {
validationFunction: async (schema, value) => {
return await schema.passthrough().safeParseAsync(value)
},
}),
(c) => {
const data = c.req.valid('json')
return c.json({
success: true,
data,
})
}
)
it('Should be ok due to passthrough schema', async () => {
const req = new Request('http://localhost/extended', {
body: JSON.stringify({
name: 'Superman',
age: 20,
length: 170,
weight: 55,
}),
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.json()).toEqual({
success: true,
data: {
name: 'Superman',
age: 20,
length: 170,
weight: 55,
},
})
})
it('Should be ok due to required schema', async () => {
const req = new Request('http://localhost', {
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(200)
expect(await res.json()).toEqual({
success: true,
data: {
name: 'Superman',
age: 20,
},
})
})
})

View File

@ -1,14 +1,14 @@
import type { Context, Env, Input, MiddlewareHandler, TypedResponse, ValidationTargets } from 'hono' import type { Context, Env, Input, MiddlewareHandler, TypedResponse, ValidationTargets } from 'hono'
import { validator } from 'hono/validator' import { validator } from 'hono/validator'
import { ZodObject } from 'zod' import { ZodObject } from 'zod'
import type { ZodError, ZodSchema, z } from 'zod' import type { SafeParseReturnType, ZodError, ZodSchema, z } from 'zod'
export type Hook< export type Hook<
T, T,
E extends Env, E extends Env,
P extends string, P extends string,
Target extends keyof ValidationTargets = keyof ValidationTargets, Target extends keyof ValidationTargets = keyof ValidationTargets,
O = {} O = {},
> = ( > = (
result: ({ success: true; data: T } | { success: false; error: ZodError; data: T }) & { result: ({ success: true; data: T } | { success: false; error: ZodError; data: T }) & {
target: Target target: Target
@ -39,11 +39,18 @@ export const zValidator = <
} }
out: { [K in Target]: Out } out: { [K in Target]: Out }
}, },
V extends I = I V extends I = I,
>( >(
target: Target, target: Target,
schema: T, schema: T,
hook?: Hook<z.infer<T>, E, P, Target> hook?: Hook<z.infer<T>, E, P, Target>,
options?: {
validationFunction: (
schema: T,
value: ValidationTargets[Target]
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) => SafeParseReturnType<any, any> | Promise<SafeParseReturnType<any, any>>
}
): MiddlewareHandler<E, P, V> => ): MiddlewareHandler<E, P, V> =>
// @ts-expect-error not typed well // @ts-expect-error not typed well
validator(target, async (value, c) => { validator(target, async (value, c) => {
@ -63,7 +70,10 @@ export const zValidator = <
) )
} }
const result = await schema.safeParseAsync(validatorValue) const result =
options && options.validationFunction
? await options.validationFunction(schema, validatorValue)
: await schema.safeParseAsync(validatorValue)
if (hook) { if (hook) {
const hookResult = await hook({ data: validatorValue, ...result, target }, c) const hookResult = await hook({ data: validatorValue, ...result, target }, c)