diff --git a/src/event/event.ts b/src/event/event.ts index 18526346..557922af 100644 --- a/src/event/event.ts +++ b/src/event/event.ts @@ -16,6 +16,7 @@ export interface WebEventContext { export class H3Event< _RequestT extends EventHandlerRequest = EventHandlerRequest, + Context = unknown, > implements Pick { "__is_event__" = true; @@ -23,7 +24,9 @@ export class H3Event< // Context node: NodeEventContext; // Node web?: WebEventContext; // Web - context: H3EventContext = {}; // Shared + context = {} as unknown extends Context + ? H3EventContext + : H3EventContext & Context; // Shared // Request _method?: HTTPMethod; @@ -74,7 +77,7 @@ export class H3Event< respondWith(response: Response | PromiseLike): Promise { return Promise.resolve(response).then((_response) => - sendWebResponse(this, _response), + sendWebResponse(this as H3Event, _response), ); } diff --git a/src/event/utils.ts b/src/event/utils.ts index 4ec7fb57..4172a2ff 100644 --- a/src/event/utils.ts +++ b/src/event/utils.ts @@ -6,6 +6,8 @@ import type { EventHandlerObject, _RequestMiddleware, _ResponseMiddleware, + EventValidateFunction, + ValidatedRequest, } from "../types"; import { hasProp } from "../utils/internal/object"; import type { H3Event } from "./event"; @@ -18,11 +20,15 @@ type _EventHandlerHooks = { export function defineEventHandler< Request extends EventHandlerRequest = EventHandlerRequest, Response = EventHandlerResponse, + _ValidateFunction extends + EventValidateFunction = EventValidateFunction, + _Request extends + ValidatedRequest<_ValidateFunction> = ValidatedRequest<_ValidateFunction>, >( handler: | EventHandler - | EventHandlerObject, -): EventHandler; + | EventHandlerObject, +): EventHandler<_Request, Response>; // TODO: remove when appropriate // This signature provides backwards compatibility with previous signature where first generic was return type export function defineEventHandler< @@ -40,11 +46,15 @@ export function defineEventHandler< export function defineEventHandler< Request extends EventHandlerRequest, Response = EventHandlerResponse, + _ValidateFunction extends + EventValidateFunction = EventValidateFunction, + _Request extends + ValidatedRequest<_ValidateFunction> = ValidatedRequest<_ValidateFunction>, >( handler: | EventHandler - | EventHandlerObject, -): EventHandler { + | EventHandlerObject, +): EventHandler<_Request, Response> { // Function Syntax if (typeof handler === "function") { handler.__is_handler__ = true; @@ -56,7 +66,7 @@ export function defineEventHandler< onBeforeResponse: _normalizeArray(handler.onBeforeResponse), }; const _handler: EventHandler = (event) => { - return _callHandler(event, handler.handler, _hooks); + return _callObjectHandler(event, handler as EventHandlerObject, _hooks); }; _handler.__is_handler__ = true; _handler.__resolve__ = handler.handler.__resolve__; @@ -68,9 +78,9 @@ function _normalizeArray(input?: T | T[]): T[] | undefined { return input ? (Array.isArray(input) ? input : [input]) : undefined; } -async function _callHandler( - event: H3Event, - handler: EventHandler, +async function _callObjectHandler( + event: H3Event, + handlerObj: EventHandlerObject, hooks: _EventHandlerHooks, ) { if (hooks.onRequest) { @@ -81,7 +91,10 @@ async function _callHandler( } } } - const body = await handler(event); + if (handlerObj.validate) { + await validateEvent(event, handlerObj.validate); + } + const body = await handlerObj.handler(event); const response = { body }; if (hooks.onBeforeResponse) { for (const hook of hooks.onBeforeResponse) { @@ -186,3 +199,26 @@ export function defineLazyEventHandler( return handler; } export const lazyEventHandler = defineLazyEventHandler; + +export async function validateEvent< + Request extends EventHandlerRequest = EventHandlerRequest, + _ValidateFunction extends + EventValidateFunction = EventValidateFunction, + _Request extends + ValidatedRequest<_ValidateFunction> = ValidatedRequest<_ValidateFunction>, +>( + event: H3Event, + validate: _ValidateFunction, +): Promise> { + const validatedContext = await validate(event); + if (validatedContext && typeof validatedContext === "object") { + Object.assign(event.context, validatedContext); + } + return event as H3Event<_Request>; +} + +export function defineEventValidator< + _ValidateFunction extends EventValidateFunction = EventValidateFunction, +>(validate: _ValidateFunction): _ValidateFunction { + return validate; +} diff --git a/src/types/_validate.ts b/src/types/_validate.ts new file mode 100644 index 00000000..81d4296c --- /dev/null +++ b/src/types/_validate.ts @@ -0,0 +1,27 @@ +import type { EventHandlerRequest } from "./index"; +import type { H3Event } from "../event"; + +export type EventValidateFunction< + Request extends EventHandlerRequest = EventHandlerRequest, +> = ( + event: H3Event, +) => H3Event | Promise> | Record; + +export type ValidatedRequest = + Awaited> extends H3Event + ? R + : Awaited> extends EventHandlerRequest + ? Awaited> + : EventHandlerRequest; + +type Simplify = TType extends any[] | Date + ? TType + : { [K in keyof TType]: TType[K] }; + +export type EventFromValidatedRequest = + keyof Request extends "body" | "query" + ? H3Event + : H3Event< + Simplify>, + Simplify> + >; diff --git a/src/types/index.ts b/src/types/index.ts index cda2d2b1..e35a7664 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -4,6 +4,7 @@ import type { H3Event } from "../event"; import type { Session } from "../utils/session"; import type { RouteNode } from "../router"; import type { AnyNumber } from "./_utils"; +import type { EventValidateFunction, ValidatedRequest } from "./_validate"; export type { ValidateFunction, @@ -46,9 +47,13 @@ export interface EventHandlerRequest { export type InferEventInput< Key extends keyof EventHandlerRequest, - Event extends H3Event, + Event extends H3Event, T, -> = void extends T ? (Event extends H3Event ? E[Key] : never) : T; +> = unknown extends T + ? Event extends H3Event + ? E[Key] + : never + : T; type MaybePromise = T | Promise; @@ -81,17 +86,28 @@ export type _ResponseMiddleware< export type EventHandlerObject< Request extends EventHandlerRequest = EventHandlerRequest, Response extends EventHandlerResponse = EventHandlerResponse, + _ValidateFunction extends + EventValidateFunction = EventValidateFunction, + _Request extends + ValidatedRequest<_ValidateFunction> = ValidatedRequest<_ValidateFunction>, > = { - onRequest?: _RequestMiddleware | _RequestMiddleware[]; + validate?: _ValidateFunction; + onRequest?: _RequestMiddleware<_Request> | _RequestMiddleware[]; onBeforeResponse?: - | _ResponseMiddleware - | _ResponseMiddleware[]; + | _ResponseMiddleware<_Request, Response> + | _ResponseMiddleware<_Request, Response>[]; /** @experimental */ websocket?: Partial; - handler: EventHandler; + handler: EventHandler<_Request, Response>; }; export type LazyEventHandler = () => EventHandler | Promise; export type { MimeType } from "./_mimes"; export type { TypedHeaders, RequestHeaders, HTTPHeaderName } from "./_headers"; + +export type { + EventFromValidatedRequest, + EventValidateFunction, + ValidatedRequest, +} from "./_validate"; diff --git a/src/utils/body.ts b/src/utils/body.ts index c89aacb4..11c01d25 100644 --- a/src/utils/body.ts +++ b/src/utils/body.ts @@ -148,7 +148,7 @@ export function readRawBody( export async function readBody< T, - Event extends H3Event = H3Event, + Event extends H3Event = H3Event, _T = InferEventInput<"body", Event, T>, >(event: Event, options: { strict?: boolean } = {}): Promise<_T> { const request = event.node.req as InternalRequest; diff --git a/test/types.test-d.ts b/test/types.test-d.ts index 9160b0ef..e4497b8a 100644 --- a/test/types.test-d.ts +++ b/test/types.test-d.ts @@ -7,6 +7,8 @@ import { readBody, readValidatedBody, getValidatedQuery, + validateEvent, + EventHandlerRequest, } from "../src"; describe("types", () => { @@ -15,11 +17,11 @@ describe("types", () => { const handler = eventHandler({ onRequest: [ (event) => { - expectTypeOf(event).toEqualTypeOf(); + expectTypeOf(event).toEqualTypeOf>(); }, ], async handler(event) { - expectTypeOf(event).toEqualTypeOf(); + expectTypeOf(event).toEqualTypeOf>(); const body = await readBody(event); // TODO: Default to unknown in next major version @@ -34,6 +36,7 @@ describe("types", () => { foo: string; }>(); }); + it("return type (inferred)", () => { const handler = eventHandler(() => { return { @@ -51,6 +54,75 @@ describe("types", () => { const response = handler({} as H3Event); expectTypeOf(response).toEqualTypeOf(); }); + + it("inferred validation", async () => { + const handler = eventHandler({ + async validate(event) { + await Promise.resolve(); + expectTypeOf(event).toEqualTypeOf>(); + return event as H3Event<{ body: { id: string } }>; + }, + onBeforeResponse: [ + (event) => { + expectTypeOf(event).toEqualTypeOf>(); + }, + ], + async handler(event) { + expectTypeOf(event).toEqualTypeOf< + H3Event<{ body: { id: string } }> + >(); + + const body = await readBody(event); + expectTypeOf(body).toEqualTypeOf<{ id: string }>(); + + return { foo: "bar" }; + }, + }); + expectTypeOf(await handler({} as H3Event)).toEqualTypeOf<{ + foo: string; + }>(); + }); + }); + + describe("validateEvent", () => { + it("inferred validation", () => { + eventHandler(async (_event) => { + const event = await validateEvent(_event, async (event) => { + await Promise.resolve(); + expectTypeOf(event).toEqualTypeOf(); + return event as H3Event<{ body: { id: string } }>; + }); + expectTypeOf(event).toEqualTypeOf>(); + }); + }); + + it("inferred validation without H3Event type requirement", async () => { + const handler = eventHandler({ + async validate(event) { + await Promise.resolve(); + expectTypeOf(event).toEqualTypeOf(); + return {} as { body: { id: string } }; + }, + async handler(event) { + expectTypeOf(event).toEqualTypeOf< + H3Event<{ body: { id: string } }> + >(); + + // expectTypeOf(event.context.other).toEqualTypeOf(); + // TODO: should be unknown in future version of h3 + expectTypeOf(event.context.body).toBeAny(); + + const body = await readBody(event); + expectTypeOf(body).toEqualTypeOf<{ id: string }>(); + expectTypeOf(await readBody(event)).toEqualTypeOf(); + + return { foo: "bar" }; + }, + }); + expectTypeOf(await handler({} as H3Event)).toEqualTypeOf<{ + foo: string; + }>(); + }); }); describe("readBody", () => { diff --git a/test/validate.test.ts b/test/validate.test.ts index 6afbc84e..12a61765 100644 --- a/test/validate.test.ts +++ b/test/validate.test.ts @@ -9,6 +9,9 @@ import { readValidatedBody, getValidatedQuery, ValidateFunction, + createError, + validateEvent, + defineEventValidator, } from "../src"; // Custom validator @@ -143,4 +146,41 @@ describe("Validate", () => { }); }); }); + + describe("event validation", () => { + const eventValidator = defineEventValidator((event) => { + if (event.path === "/invalid") { + throw createError({ message: "Invalid path", status: 400 }); + } + return undefined as any; + }); + + it("object syntax validate", async () => { + app.use( + eventHandler({ + validate: eventValidator, + handler: () => { + return "ok"; + }, + }), + ); + const res = await request.get("/invalid"); + expect(res.text).include("Invalid path"); + expect(res.status).toEqual(400); + expect((await request.get("/")).text).toBe("ok"); + }); + + it("validateEvent", async () => { + app.use( + eventHandler(async (_event) => { + await validateEvent(_event, eventValidator); + return "ok"; + }), + ); + const res = await request.get("/invalid"); + expect(res.text).include("Invalid path"); + expect(res.status).toEqual(400); + expect((await request.get("/")).text).toBe("ok"); + }); + }); });