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 19 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
9 changes: 6 additions & 3 deletions src/event/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@
export class H3Event<
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_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;
context: H3EventContext = {};
context = {} as unknown extends Context
? H3EventContext
: H3EventContext & Context;

// Request
_request: Request | undefined;
Expand Down Expand Up @@ -56,7 +59,7 @@

get url() {
if (!this._url) {
this._url = getRequestURL(this);
this._url = getRequestURL(this as H3Event<EventHandlerRequest, unknown>);
}
return this._url;
}
Expand Down Expand Up @@ -132,7 +135,7 @@

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

Check warning on line 138 in src/event/event.ts

View check run for this annotation

Codecov / codecov/patch

src/event/event.ts#L138

Added line #L138 was not covered by tests
);
}

Expand Down
73 changes: 67 additions & 6 deletions src/event/utils.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
import { ParsedBodySymbol, ParsedQuerySymbol } from "../utils/symbols";
import type {
EventHandler,
LazyEventHandler,
EventHandlerRequest,
EventHandlerResponse,
EventHandlerObject,
EventValidateFunction,
EventValidatedRequest,
} from "../types";
import type { H3Event } from "./event";

export function defineEventHandler<
Request extends EventHandlerRequest = EventHandlerRequest,
Response = EventHandlerResponse,
_ValidateFunction extends
EventValidateFunction<Request> = EventValidateFunction<Request>,
_ValidatedRequest extends
EventValidatedRequest<_ValidateFunction> = EventValidatedRequest<_ValidateFunction>,
>(
handler:
| EventHandler<Request, Response>
| EventHandlerObject<Request, Response>,
): EventHandler<Request, Response>;
| EventHandlerObject<
Request,
Response,
_ValidateFunction,
_ValidatedRequest
>,
): EventHandler<_ValidatedRequest, Response>;
// TODO: remove when appropriate
// This signature provides backwards compatibility with previous signature where first generic was return type
export function defineEventHandler<
Expand All @@ -32,23 +44,35 @@
export function defineEventHandler<
Request extends EventHandlerRequest,
Response = EventHandlerResponse,
_ValidateFunction extends
EventValidateFunction<Request> = EventValidateFunction<Request>,
_ValidatedRequest extends
EventValidatedRequest<_ValidateFunction> = EventValidatedRequest<_ValidateFunction>,
>(
handler:
| EventHandler<Request, Response>
| EventHandlerObject<Request, Response>,
): EventHandler<Request, Response> {
| EventHandlerObject<
Request,
Response,
_ValidateFunction,
_ValidatedRequest
>,
): EventHandler<_ValidatedRequest, Response> {
// Function Syntax
if (typeof handler === "function") {
return Object.assign(handler, { __is_handler__: true });
}
// Object Syntax
const _handler: EventHandler<Request, any> = (event) => {
return _callHandler(event, handler);
return _callHandler(event, handler as EventHandlerObject);
};
return Object.assign(_handler, { __is_handler__: true });
}

async function _callHandler(event: H3Event, handler: EventHandlerObject) {
async function _callHandler(
event: H3Event<any, any>,
handler: EventHandlerObject,
) {
if (handler.before) {
for (const hook of handler.before) {
await hook(event);
Expand All @@ -57,6 +81,23 @@
}
}
}
if (handler.validate) {
const res = (await validateEvent(event, handler.validate)) as Record<
string,
any
>;
if (res && typeof res === "object") {
for (const property in res) {
if (property === "body") {
(event.node.req as any)[ParsedBodySymbol] = res.body;

Check warning on line 92 in src/event/utils.ts

View check run for this annotation

Codecov / codecov/patch

src/event/utils.ts#L92

Added line #L92 was not covered by tests
pi0 marked this conversation as resolved.
Show resolved Hide resolved
} else if ("query" in res) {
(event.node.req as any)[ParsedQuerySymbol] = res.query;

Check warning on line 94 in src/event/utils.ts

View check run for this annotation

Codecov / codecov/patch

src/event/utils.ts#L94

Added line #L94 was not covered by tests
} else {
event.context[property] = res[property];
}
}
}
}
const body = await handler.handler(event);
const response = { body };
if (handler.after) {
Expand Down Expand Up @@ -140,3 +181,23 @@
}) as Awaited<ReturnType<T>>;
}
export const lazyEventHandler = defineLazyEventHandler;

export async function validateEvent<
Request extends EventHandlerRequest = EventHandlerRequest,
_ValidateFunction extends
EventValidateFunction<Request> = EventValidateFunction<Request>,
_ValidatedRequest extends
EventValidatedRequest<_ValidateFunction> = EventValidatedRequest<_ValidateFunction>,
>(
event: H3Event<Request>,
validate: _ValidateFunction,
): Promise<H3Event<_ValidatedRequest>> {
await validate(event);
return event as H3Event<_ValidatedRequest>;
}

export function defineEventValidator<
_ValidateFunction extends EventValidateFunction = EventValidateFunction,
>(validate: _ValidateFunction): _ValidateFunction {
return validate;
}
45 changes: 40 additions & 5 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,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;

export interface EventHandler<
Request extends EventHandlerRequest = EventHandlerRequest,
Expand All @@ -69,14 +73,45 @@ export interface EventHandler<
(event: H3Event<Request>): Response;
}

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

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

export 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">>
>;

export type EventHandlerObject<
Request extends EventHandlerRequest = EventHandlerRequest,
Response extends EventHandlerResponse = EventHandlerResponse,
_ValidateFunction extends
EventValidateFunction<Request> = EventValidateFunction<Request>,
_ValidatedRequest extends
EventValidatedRequest<_ValidateFunction> = EventValidatedRequest<_ValidateFunction>,
> = {
handler: EventHandler<Request, Response>;
before?: ((event: H3Event<Request>) => void | Promise<void>)[];
validate?: _ValidateFunction;
handler: (event: EventFromValidatedRequest<_ValidatedRequest>) => Response;
before?: ((event: H3Event<EventHandlerRequest>) => void | Promise<void>)[];
after?: ((
event: H3Event<Request>,
event: H3Event<_ValidatedRequest>,
response: { body?: Response },
) => void | Promise<void>)[];
};
Expand Down
5 changes: 2 additions & 3 deletions src/utils/body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ import { createError } from "../error";
import { parse as parseMultipartData } from "./internal/multipart";
import { assertMethod, getRequestHeader } from "./request";
import { ValidateFunction, validateData } from "./internal/validate";
import { ParsedBodySymbol, RawBodySymbol } from "./symbols";

export type { MultiPartData } from "./internal/multipart";

const RawBodySymbol = Symbol.for("h3RawBody");
const ParsedBodySymbol = Symbol.for("h3ParsedBody");
type InternalRequest<T = any> = IncomingMessage & {
[RawBodySymbol]?: Promise<Buffer | undefined>;
[ParsedBodySymbol]?: T;
Expand Down Expand Up @@ -94,7 +93,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
4 changes: 4 additions & 0 deletions src/utils/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
import type { HTTPMethod, InferEventInput, RequestHeaders } from "../types";
import type { H3Event } from "../event";
import { validateData, ValidateFunction } from "./internal/validate";
import { ParsedQuerySymbol } from "./symbols";

export function getQuery<
T,
Event extends H3Event = H3Event,
_T = Exclude<InferEventInput<"query", Event, T>, undefined>,
>(event: Event): _T {
if (ParsedQuerySymbol in event) {
return event[ParsedQuerySymbol] as _T;
}

Check warning on line 15 in src/utils/request.ts

View check run for this annotation

Codecov / codecov/patch

src/utils/request.ts#L14-L15

Added lines #L14 - L15 were not covered by tests
return _getQuery(event.path || "") as _T;
}

Expand Down
3 changes: 3 additions & 0 deletions src/utils/symbols.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const RawBodySymbol = Symbol.for("h3RawBody");
export const ParsedBodySymbol = Symbol.for("h3ParsedBody");
export const ParsedQuerySymbol = Symbol.for("h3ParsedQuery");
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({
before: [
(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({
before: [
(event) => {
expectTypeOf(event).toEqualTypeOf<H3Event<EventHandlerRequest>>();
},
],
async validate(event) {
await Promise.resolve();
expectTypeOf(event).toEqualTypeOf<H3Event<EventHandlerRequest>>();
return event as H3Event<{ body: { id: string } }>;
},
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 }; other: true };
},
async handler(event) {
expectTypeOf(event).toEqualTypeOf<
H3Event<{ body: { id: string } }, { other: true }>
>();

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
Loading