Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: request validation using handler object syntax validate and validateEvent(event) #496

Draft
wants to merge 22 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/event/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@ export interface WebEventContext {

export class H3Event<
_RequestT extends EventHandlerRequest = EventHandlerRequest,
Context = unknown,
pi0 marked this conversation as resolved.
Show resolved Hide resolved
> implements Pick<FetchEvent, "respondWith">
{
"__is_event__" = true;

// Context
node: NodeEventContext; // Node
web?: WebEventContext; // Web
context: H3EventContext = {}; // Shared
context = {} as unknown extends Context
? H3EventContext
: H3EventContext & Context; // Shared

// Request
_method?: HTTPMethod;
Expand Down Expand Up @@ -74,7 +77,7 @@ export class H3Event<

respondWith(response: Response | PromiseLike<Response>): Promise<void> {
return Promise.resolve(response).then((_response) =>
sendWebResponse(this, _response),
sendWebResponse(this as H3Event<EventHandlerRequest, unknown>, _response),
);
}

Expand Down
54 changes: 45 additions & 9 deletions src/event/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type {
EventHandlerObject,
_RequestMiddleware,
_ResponseMiddleware,
EventValidateFunction,
ValidatedRequest,
} from "../types";
import { hasProp } from "../utils/internal/object";
import type { H3Event } from "./event";
Expand All @@ -18,11 +20,15 @@ type _EventHandlerHooks = {
export function defineEventHandler<
Request extends EventHandlerRequest = EventHandlerRequest,
Response = EventHandlerResponse,
_ValidateFunction extends
EventValidateFunction<Request> = EventValidateFunction<Request>,
_Request extends
ValidatedRequest<_ValidateFunction> = ValidatedRequest<_ValidateFunction>,
>(
handler:
| EventHandler<Request, Response>
| EventHandlerObject<Request, Response>,
): EventHandler<Request, Response>;
| EventHandlerObject<Request, Response, _ValidateFunction, _Request>,
): EventHandler<_Request, Response>;
// TODO: remove when appropriate
// This signature provides backwards compatibility with previous signature where first generic was return type
export function defineEventHandler<
Expand All @@ -40,11 +46,15 @@ export function defineEventHandler<
export function defineEventHandler<
Request extends EventHandlerRequest,
Response = EventHandlerResponse,
_ValidateFunction extends
EventValidateFunction<Request> = EventValidateFunction<Request>,
_Request extends
ValidatedRequest<_ValidateFunction> = ValidatedRequest<_ValidateFunction>,
>(
handler:
| EventHandler<Request, Response>
| EventHandlerObject<Request, Response>,
): EventHandler<Request, Response> {
| EventHandlerObject<Request, Response, _ValidateFunction, _Request>,
): EventHandler<_Request, Response> {
// Function Syntax
if (typeof handler === "function") {
handler.__is_handler__ = true;
Expand All @@ -56,7 +66,7 @@ export function defineEventHandler<
onBeforeResponse: _normalizeArray(handler.onBeforeResponse),
};
const _handler: EventHandler<Request, any> = (event) => {
return _callHandler(event, handler.handler, _hooks);
return _callObjectHandler(event, handler as EventHandlerObject, _hooks);
};
_handler.__is_handler__ = true;
_handler.__resolve__ = handler.handler.__resolve__;
Expand All @@ -68,9 +78,9 @@ function _normalizeArray<T>(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<any, any>,
handlerObj: EventHandlerObject,
hooks: _EventHandlerHooks,
) {
if (hooks.onRequest) {
Expand All @@ -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) {
Expand Down Expand Up @@ -186,3 +199,26 @@ export function defineLazyEventHandler<T extends LazyEventHandler>(
return handler;
}
export const lazyEventHandler = defineLazyEventHandler;

export async function validateEvent<
Request extends EventHandlerRequest = EventHandlerRequest,
_ValidateFunction extends
EventValidateFunction<Request> = EventValidateFunction<Request>,
_Request extends
ValidatedRequest<_ValidateFunction> = ValidatedRequest<_ValidateFunction>,
>(
event: H3Event<Request>,
validate: _ValidateFunction,
): Promise<H3Event<_Request>> {
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;
}
27 changes: 27 additions & 0 deletions src/types/_validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { EventHandlerRequest } from "./index";
import type { H3Event } from "../event";

export type EventValidateFunction<
Request extends EventHandlerRequest = EventHandlerRequest,
> = (
event: H3Event<Request>,
) => H3Event<Request> | Promise<H3Event<Request>> | Record<string, any>;

export type ValidatedRequest<ValidateFunction extends EventValidateFunction> =
Awaited<ReturnType<ValidateFunction>> extends H3Event<infer R>
? R
: Awaited<ReturnType<ValidateFunction>> extends EventHandlerRequest
? Awaited<ReturnType<ValidateFunction>>
: EventHandlerRequest;

type Simplify<TType> = TType extends any[] | Date
? TType
: { [K in keyof TType]: TType[K] };

export type EventFromValidatedRequest<Request extends EventHandlerRequest> =
keyof Request extends "body" | "query"
? H3Event<Request>
: H3Event<
Simplify<Pick<Request, keyof Request & ("body" | "query")>>,
Simplify<Omit<Request, "body" | "query">>
>;
28 changes: 22 additions & 6 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -46,9 +47,13 @@ export interface EventHandlerRequest {

export type InferEventInput<
Key extends keyof EventHandlerRequest,
Event extends H3Event,
Event extends H3Event<any, any>,
T,
> = void extends T ? (Event extends H3Event<infer E> ? E[Key] : never) : T;
> = unknown extends T
? Event extends H3Event<infer E, any>
? E[Key]
: never
: T;

type MaybePromise<T> = T | Promise<T>;

Expand Down Expand Up @@ -81,17 +86,28 @@ export type _ResponseMiddleware<
export type EventHandlerObject<
Request extends EventHandlerRequest = EventHandlerRequest,
Response extends EventHandlerResponse = EventHandlerResponse,
_ValidateFunction extends
EventValidateFunction<Request> = EventValidateFunction<Request>,
_Request extends
ValidatedRequest<_ValidateFunction> = ValidatedRequest<_ValidateFunction>,
> = {
onRequest?: _RequestMiddleware<Request> | _RequestMiddleware<Request>[];
validate?: _ValidateFunction;
onRequest?: _RequestMiddleware<_Request> | _RequestMiddleware<Request>[];
onBeforeResponse?:
| _ResponseMiddleware<Request, Response>
| _ResponseMiddleware<Request, Response>[];
| _ResponseMiddleware<_Request, Response>
| _ResponseMiddleware<_Request, Response>[];
/** @experimental */
websocket?: Partial<WSHooks>;
handler: EventHandler<Request, Response>;
handler: EventHandler<_Request, Response>;
};

export type LazyEventHandler = () => EventHandler | Promise<EventHandler>;

export type { MimeType } from "./_mimes";
export type { TypedHeaders, RequestHeaders, HTTPHeaderName } from "./_headers";

export type {
EventFromValidatedRequest,
EventValidateFunction,
ValidatedRequest,
} from "./_validate";
2 changes: 1 addition & 1 deletion src/utils/body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export function readRawBody<E extends Encoding = "utf8">(

export async function readBody<
T,
Event extends H3Event = H3Event,
Event extends H3Event<any, any> = H3Event<any, any>,
_T = InferEventInput<"body", Event, T>,
>(event: Event, options: { strict?: boolean } = {}): Promise<_T> {
const request = event.node.req as InternalRequest<T>;
Expand Down
76 changes: 74 additions & 2 deletions test/types.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
readBody,
readValidatedBody,
getValidatedQuery,
validateEvent,
EventHandlerRequest,
} from "../src";

describe("types", () => {
Expand All @@ -15,11 +17,11 @@ describe("types", () => {
const handler = eventHandler({
onRequest: [
(event) => {
expectTypeOf(event).toEqualTypeOf<H3Event>();
expectTypeOf(event).toEqualTypeOf<H3Event<EventHandlerRequest>>();
},
],
async handler(event) {
expectTypeOf(event).toEqualTypeOf<H3Event>();
expectTypeOf(event).toEqualTypeOf<H3Event<EventHandlerRequest>>();

const body = await readBody(event);
// TODO: Default to unknown in next major version
Expand All @@ -34,6 +36,7 @@ describe("types", () => {
foo: string;
}>();
});

it("return type (inferred)", () => {
const handler = eventHandler(() => {
return {
Expand All @@ -51,6 +54,75 @@ describe("types", () => {
const response = handler({} as H3Event);
expectTypeOf(response).toEqualTypeOf<string>();
});

it("inferred validation", async () => {
const handler = eventHandler({
async validate(event) {
await Promise.resolve();
expectTypeOf(event).toEqualTypeOf<H3Event<EventHandlerRequest>>();
return event as H3Event<{ body: { id: string } }>;
},
onBeforeResponse: [
(event) => {
expectTypeOf(event).toEqualTypeOf<H3Event<EventHandlerRequest>>();
},
],
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<H3Event>();
return event as H3Event<{ body: { id: string } }>;
});
expectTypeOf(event).toEqualTypeOf<H3Event<{ body: { id: string } }>>();
});
});

it("inferred validation without H3Event type requirement", async () => {
const handler = eventHandler({
async validate(event) {
await Promise.resolve();
expectTypeOf(event).toEqualTypeOf<H3Event>();
return {} as { body: { id: string } };
},
async handler(event) {
expectTypeOf(event).toEqualTypeOf<
H3Event<{ body: { id: string } }>
>();

// expectTypeOf(event.context.other).toEqualTypeOf<true>();
// 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<string>(event)).toEqualTypeOf<string>();

return { foo: "bar" };
},
});
expectTypeOf(await handler({} as H3Event)).toEqualTypeOf<{
foo: string;
}>();
});
});

describe("readBody", () => {
Expand Down
40 changes: 40 additions & 0 deletions test/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import {
readValidatedBody,
getValidatedQuery,
ValidateFunction,
createError,
validateEvent,
defineEventValidator,
} from "../src";

// Custom validator
Expand Down Expand Up @@ -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");
});
});
});
Loading