From 9ba3da584cebaac8c6352a24be776cc43cf93cb4 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 29 Jun 2023 14:05:57 +0000 Subject: [PATCH 01/17] feat: add event handler generic for typed input body/query --- .github/workflows/ci.yml | 1 + package.json | 5 ++-- src/event/event.ts | 4 +-- src/event/utils.ts | 16 +++++++++--- src/types.ts | 9 +++++-- src/utils/body.ts | 6 ++--- src/utils/request.ts | 4 +-- test/types.test-d.ts | 54 ++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 5 +++- 9 files changed, 88 insertions(+), 16 deletions(-) create mode 100644 test/types.test-d.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 892321d4..ee63d8ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,5 +24,6 @@ jobs: - run: pnpm install - run: pnpm lint - run: pnpm build + - run: pnpm test:types - run: pnpm vitest --coverage && rm -rf coverage/tmp - uses: codecov/codecov-action@v3 diff --git a/package.json b/package.json index 96d765e8..55f85926 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "play": "jiti ./playground/index.ts", "profile": "0x -o -D .profile -P 'autocannon -c 100 -p 10 -d 40 http://localhost:$PORT' ./playground/server.cjs", "release": "pnpm test && pnpm build && changelogen --release && pnpm publish && git push --follow-tags", - "test": "pnpm lint && vitest run --coverage" + "test": "pnpm lint && vitest run --coverage", + "test:types": "vitest typecheck" }, "dependencies": { "cookie-es": "^1.0.0", @@ -61,4 +62,4 @@ "vitest": "^0.32.2" }, "packageManager": "pnpm@8.6.3" -} \ No newline at end of file +} diff --git a/src/event/event.ts b/src/event/event.ts index 6b7b9cc0..85f9ce41 100644 --- a/src/event/event.ts +++ b/src/event/event.ts @@ -1,4 +1,4 @@ -import type { H3EventContext } from "../types"; +import type { H3EventContext, TypedEventInputSignature } from "../types"; import type { NodeIncomingMessage, NodeServerResponse } from "../node"; import { MIMES, @@ -13,7 +13,7 @@ export interface NodeEventContext { res: NodeServerResponse; } -export class H3Event implements Pick { +export class H3Event<_Input extends TypedEventInputSignature = any> implements Pick { "__is_event__" = true; _handled = false; diff --git a/src/event/utils.ts b/src/event/utils.ts index edf224dc..46ce5279 100644 --- a/src/event/utils.ts +++ b/src/event/utils.ts @@ -1,8 +1,16 @@ -import type { EventHandler, LazyEventHandler } from "../types"; +import type { EventHandler, LazyEventHandler, TypedEventInputSignature } from "../types"; -export function defineEventHandler( - handler: EventHandler -): EventHandler { +export function defineEventHandler( + handler: EventHandler + ): EventHandler +// TODO: remove when appropriate +// This signature provides backwards compatibility with previous signature where first generic was return type +export function defineEventHandler( + handler: EventHandler +): EventHandler +export function defineEventHandler( + handler: EventHandler +): EventHandler { handler.__is_handler__ = true; return handler; } diff --git a/src/types.ts b/src/types.ts index c3fe7450..ec68daf4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -35,9 +35,14 @@ export interface H3EventContext extends Record { export type EventHandlerResponse = T | Promise; -export interface EventHandler { +export interface TypedEventInputSignature { + body?: any + query?: any +} + +export interface EventHandler { __is_handler__?: true; - (event: H3Event): EventHandlerResponse; + (event: H3Event): EventHandlerResponse; } export type LazyEventHandler = () => EventHandler | Promise; diff --git a/src/utils/body.ts b/src/utils/body.ts index 382d7b99..ad300a33 100644 --- a/src/utils/body.ts +++ b/src/utils/body.ts @@ -83,7 +83,7 @@ export function readRawBody( * const body = await readBody(req) * ``` */ -export async function readBody(event: H3Event): Promise { +export async function readBody(event: E): Promise ? Input['body'] : never : T> { if (ParsedBodySymbol in event.node.req) { return (event.node.req as any)[ParsedBodySymbol]; } @@ -106,12 +106,12 @@ export async function readBody(event: H3Event): Promise { parsedForm[key] = value; } } - return parsedForm as unknown as T; + return parsedForm as unknown as unknown extends T ? E extends H3Event ? Input['body'] : never : T; } const json = destr(body) as T; (event.node.req as any)[ParsedBodySymbol] = json; - return json; + return json as unknown extends T ? E extends H3Event ? Input['body'] : never : T; } export async function readMultipartFormData(event: H3Event) { diff --git a/src/utils/request.ts b/src/utils/request.ts index 4e81b5e1..8fb0b102 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -3,8 +3,8 @@ import { createError } from "../error"; import type { HTTPMethod, RequestHeaders } from "../types"; import type { H3Event } from "../event"; -export function getQuery(event: H3Event) { - return _getQuery(event.node.req.url || ""); +export function getQuery(event: E) { + return _getQuery(event.node.req.url || "") as E extends H3Event ? Input['query'] : any; } export function getRouterParams( diff --git a/test/types.test-d.ts b/test/types.test-d.ts new file mode 100644 index 00000000..d70e0b87 --- /dev/null +++ b/test/types.test-d.ts @@ -0,0 +1,54 @@ +import { describe, it, expectTypeOf } from "vitest"; +import { + eventHandler, + H3Event, + readBody, + getQuery, +} from "../src"; + +describe("types for event handlers", () => { + it("return type test", async () => { + const handler = eventHandler(() => { + return { + foo: 'bar' + } + }) + + expectTypeOf(handler({} as H3Event)).toEqualTypeOf<{ foo: string } | Promise<{ foo: string }>>() + }); + + it("input type test", () => { + eventHandler<{ body: { id: string } }>(async (event) => { + const body = await readBody(event) + expectTypeOf(body).toEqualTypeOf<{ id: string }>() + expectTypeOf(getQuery(event)).toBeUnknown() + + return null + }) + + eventHandler<{ query: { id: string } }>(async (event) => { + const query = getQuery(event) + expectTypeOf(query).toEqualTypeOf<{ id: string }>() + + return null + }) + }); + + it("allows backwards compatible generic for eventHandler definition", () => { + const handler = eventHandler(async () => { + return '' + }) + expectTypeOf(handler({} as H3Event)).toEqualTypeOf>() + }) + + // For backwards compatibility - this should likely become `unknown` in future + it("input types aren't applied when omitted", () => { + eventHandler(async (event) => { + const body = await readBody(event) + expectTypeOf(body).toBeAny() + expectTypeOf(getQuery(event)).toBeAny() + + return null + }) + }) +}); diff --git a/tsconfig.json b/tsconfig.json index d778c0e2..e4ae11ff 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,8 @@ "baseUrl": ".", "target": "ESNext", "module": "ESNext", + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, "moduleResolution": "Node", "lib": [ "WebWorker", @@ -16,6 +18,7 @@ ] }, "include": [ - "src" + "src", + "test/types.test-d.ts" ] } From 1d34f8f1890853bbea5d7ec820fbdb3657b5f423 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 29 Jun 2023 14:13:58 +0000 Subject: [PATCH 02/17] style: lint --- src/event/event.ts | 4 ++- src/event/utils.ts | 30 ++++++++++++++------- src/types.ts | 9 ++++--- src/utils/body.ts | 22 +++++++++++++--- src/utils/request.ts | 4 ++- test/types.test-d.ts | 63 ++++++++++++++++++++++---------------------- 6 files changed, 83 insertions(+), 49 deletions(-) diff --git a/src/event/event.ts b/src/event/event.ts index 85f9ce41..20b132ad 100644 --- a/src/event/event.ts +++ b/src/event/event.ts @@ -13,7 +13,9 @@ export interface NodeEventContext { res: NodeServerResponse; } -export class H3Event<_Input extends TypedEventInputSignature = any> implements Pick { +export class H3Event<_Input extends TypedEventInputSignature = any> + implements Pick +{ "__is_event__" = true; _handled = false; diff --git a/src/event/utils.ts b/src/event/utils.ts index 46ce5279..697c1349 100644 --- a/src/event/utils.ts +++ b/src/event/utils.ts @@ -1,16 +1,28 @@ -import type { EventHandler, LazyEventHandler, TypedEventInputSignature } from "../types"; +import type { + EventHandler, + LazyEventHandler, + TypedEventInputSignature, +} from "../types"; -export function defineEventHandler( - handler: EventHandler - ): EventHandler +export function defineEventHandler< + Input extends TypedEventInputSignature = any, + Return = any +>(handler: EventHandler): EventHandler; // TODO: remove when appropriate // This signature provides backwards compatibility with previous signature where first generic was return type export function defineEventHandler( - handler: EventHandler -): EventHandler -export function defineEventHandler( - handler: EventHandler -): EventHandler { + handler: EventHandler< + Input extends TypedEventInputSignature ? Input : any, + Input extends TypedEventInputSignature ? Return : Input + > +): EventHandler< + Input extends TypedEventInputSignature ? Input : any, + Input extends TypedEventInputSignature ? Return : Input +>; +export function defineEventHandler< + Input extends TypedEventInputSignature = any, + Return = any +>(handler: EventHandler): EventHandler { handler.__is_handler__ = true; return handler; } diff --git a/src/types.ts b/src/types.ts index ec68daf4..789173e6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -36,11 +36,14 @@ export interface H3EventContext extends Record { export type EventHandlerResponse = T | Promise; export interface TypedEventInputSignature { - body?: any - query?: any + body?: any; + query?: any; } -export interface EventHandler { +export interface EventHandler< + Input extends TypedEventInputSignature = any, + Return = any +> { __is_handler__?: true; (event: H3Event): EventHandlerResponse; } diff --git a/src/utils/body.ts b/src/utils/body.ts index ad300a33..96e7f820 100644 --- a/src/utils/body.ts +++ b/src/utils/body.ts @@ -83,7 +83,15 @@ export function readRawBody( * const body = await readBody(req) * ``` */ -export async function readBody(event: E): Promise ? Input['body'] : never : T> { +export async function readBody( + event: E +): Promise< + unknown extends T + ? E extends H3Event + ? Input["body"] + : never + : T +> { if (ParsedBodySymbol in event.node.req) { return (event.node.req as any)[ParsedBodySymbol]; } @@ -106,12 +114,20 @@ export async function readBody(event: parsedForm[key] = value; } } - return parsedForm as unknown as unknown extends T ? E extends H3Event ? Input['body'] : never : T; + return parsedForm as unknown as unknown extends T + ? E extends H3Event + ? Input["body"] + : never + : T; } const json = destr(body) as T; (event.node.req as any)[ParsedBodySymbol] = json; - return json as unknown extends T ? E extends H3Event ? Input['body'] : never : T; + return json as unknown extends T + ? E extends H3Event + ? Input["body"] + : never + : T; } export async function readMultipartFormData(event: H3Event) { diff --git a/src/utils/request.ts b/src/utils/request.ts index 8fb0b102..d3a31218 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -4,7 +4,9 @@ import type { HTTPMethod, RequestHeaders } from "../types"; import type { H3Event } from "../event"; export function getQuery(event: E) { - return _getQuery(event.node.req.url || "") as E extends H3Event ? Input['query'] : any; + return _getQuery(event.node.req.url || "") as E extends H3Event + ? Input["query"] + : any; } export function getRouterParams( diff --git a/test/types.test-d.ts b/test/types.test-d.ts index d70e0b87..d3081ed8 100644 --- a/test/types.test-d.ts +++ b/test/types.test-d.ts @@ -1,54 +1,53 @@ import { describe, it, expectTypeOf } from "vitest"; -import { - eventHandler, - H3Event, - readBody, - getQuery, -} from "../src"; +import { eventHandler, H3Event, readBody, getQuery } from "../src"; describe("types for event handlers", () => { - it("return type test", async () => { + it("return type test", () => { const handler = eventHandler(() => { return { - foo: 'bar' - } - }) + foo: "bar", + }; + }); - expectTypeOf(handler({} as H3Event)).toEqualTypeOf<{ foo: string } | Promise<{ foo: string }>>() + expectTypeOf(handler({} as H3Event)).toEqualTypeOf< + { foo: string } | Promise<{ foo: string }> + >(); }); it("input type test", () => { eventHandler<{ body: { id: string } }>(async (event) => { - const body = await readBody(event) - expectTypeOf(body).toEqualTypeOf<{ id: string }>() - expectTypeOf(getQuery(event)).toBeUnknown() + const body = await readBody(event); + expectTypeOf(body).toEqualTypeOf<{ id: string }>(); + expectTypeOf(getQuery(event)).toBeUnknown(); - return null - }) + return null; + }); - eventHandler<{ query: { id: string } }>(async (event) => { - const query = getQuery(event) - expectTypeOf(query).toEqualTypeOf<{ id: string }>() + eventHandler<{ query: { id: string } }>((event) => { + const query = getQuery(event); + expectTypeOf(query).toEqualTypeOf<{ id: string }>(); - return null - }) + return null; + }); }); it("allows backwards compatible generic for eventHandler definition", () => { - const handler = eventHandler(async () => { - return '' - }) - expectTypeOf(handler({} as H3Event)).toEqualTypeOf>() - }) + const handler = eventHandler(() => { + return ""; + }); + expectTypeOf(handler({} as H3Event)).toEqualTypeOf< + string | Promise + >(); + }); // For backwards compatibility - this should likely become `unknown` in future it("input types aren't applied when omitted", () => { eventHandler(async (event) => { - const body = await readBody(event) - expectTypeOf(body).toBeAny() - expectTypeOf(getQuery(event)).toBeAny() + const body = await readBody(event); + expectTypeOf(body).toBeAny(); + expectTypeOf(getQuery(event)).toBeAny(); - return null - }) - }) + return null; + }); + }); }); From ac3fceb9b698e0b8ce65930b5279d73e7073aa95 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Mon, 24 Jul 2023 17:43:00 +0200 Subject: [PATCH 03/17] add example readTypedBody from same impl (wip) --- src/utils/body.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/utils/body.ts b/src/utils/body.ts index c53b90a4..72ca3bda 100644 --- a/src/utils/body.ts +++ b/src/utils/body.ts @@ -1,6 +1,6 @@ import type { IncomingMessage } from "node:http"; import destr from "destr"; -import type { Encoding, HTTPMethod } from "../types"; +import type { Encoding, HTTPMethod, Input } from "../types"; import type { H3Event } from "../event"; import { createError } from "../error"; import { parse as parseMultipartData } from "./internal/multipart"; @@ -118,6 +118,14 @@ export async function readBody( return parsed; } +export function readTypedBody< + T = unknown, + E extends H3Event = H3Event, + _T = unknown extends T ? (E extends H3Event ? E["body"] : never) : T +>(event: E, options: { strict?: boolean } = {}): Promise<_T> { + return readBody(event, options) as Promise<_T>; +} + export async function readMultipartFormData(event: H3Event) { const contentType = getRequestHeader(event, "content-type"); if (!contentType || !contentType.startsWith("multipart/form-data")) { From 37126b8fcc0098d631664a803df82976c203a33a Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 15:43:44 +0000 Subject: [PATCH 04/17] [autofix.ci] apply automated fixes --- src/event/event.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/event/event.ts b/src/event/event.ts index b232e9fb..d1014972 100644 --- a/src/event/event.ts +++ b/src/event/event.ts @@ -1,5 +1,9 @@ -import type { H3EventContext, HTTPMethod, TypedEventInputSignature } from "../types"; import type { IncomingHttpHeaders } from "node:http"; +import type { + H3EventContext, + HTTPMethod, + TypedEventInputSignature, +} from "../types"; import type { NodeIncomingMessage, NodeServerResponse } from "../node"; import { MIMES, sanitizeStatusCode, sanitizeStatusMessage } from "../utils"; import { H3Response } from "./response"; From 4d10c087cc815e4346432cc1aa53b8efd36ac673 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Mon, 24 Jul 2023 17:49:15 +0200 Subject: [PATCH 05/17] small updates --- package.json | 2 +- src/event/event.ts | 1 + src/utils/body.ts | 2 +- test/types.test-d.ts | 10 ++++++++-- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 544df432..0f276412 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "play": "listhen ./playground/app.ts", "profile": "0x -o -D .profile -P 'autocannon -c 100 -p 10 -d 40 http://localhost:$PORT' ./playground/server.cjs", "release": "pnpm test && pnpm build && changelogen --release && pnpm publish && git push --follow-tags", - "test": "pnpm lint && vitest run --coverage", + "test": "pnpm lint && vitest --run typecheck && vitest --run --coverage", "test:types": "vitest typecheck" }, "dependencies": { diff --git a/src/event/event.ts b/src/event/event.ts index d1014972..01e7b4d1 100644 --- a/src/event/event.ts +++ b/src/event/event.ts @@ -15,6 +15,7 @@ export interface NodeEventContext { res: NodeServerResponse; } +// eslint-disable-next-line @typescript-eslint/no-unused-vars export class H3Event<_Input extends TypedEventInputSignature = any> implements Pick { diff --git a/src/utils/body.ts b/src/utils/body.ts index 72ca3bda..a2ace20e 100644 --- a/src/utils/body.ts +++ b/src/utils/body.ts @@ -1,6 +1,6 @@ import type { IncomingMessage } from "node:http"; import destr from "destr"; -import type { Encoding, HTTPMethod, Input } from "../types"; +import type { Encoding, HTTPMethod } from "../types"; import type { H3Event } from "../event"; import { createError } from "../error"; import { parse as parseMultipartData } from "./internal/multipart"; diff --git a/test/types.test-d.ts b/test/types.test-d.ts index d3081ed8..07335ca8 100644 --- a/test/types.test-d.ts +++ b/test/types.test-d.ts @@ -1,5 +1,11 @@ import { describe, it, expectTypeOf } from "vitest"; -import { eventHandler, H3Event, readBody, getQuery } from "../src"; +import { + eventHandler, + H3Event, + readBody, + getQuery, + readTypedBody, +} from "../src"; describe("types for event handlers", () => { it("return type test", () => { @@ -16,7 +22,7 @@ describe("types for event handlers", () => { it("input type test", () => { eventHandler<{ body: { id: string } }>(async (event) => { - const body = await readBody(event); + const body = await readTypedBody(event); expectTypeOf(body).toEqualTypeOf<{ id: string }>(); expectTypeOf(getQuery(event)).toBeUnknown(); From db9a0b7814c820aa71df0030f238d36a6511994b Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 26 Jul 2023 19:51:45 +0200 Subject: [PATCH 06/17] add type safety for readValidatedBody --- src/utils/body.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/utils/body.ts b/src/utils/body.ts index 99d0f4ba..246612d6 100644 --- a/src/utils/body.ts +++ b/src/utils/body.ts @@ -127,10 +127,11 @@ export function readTypedBody< return readBody(event, options) as Promise<_T>; } -export async function readValidatedBody( - event: H3Event, - validate: ValidateFunction -): Promise { +export async function readValidatedBody< + T = unknown, + E extends H3Event = H3Event, + _T = unknown extends T ? (E extends H3Event ? E["body"] : never) : T +>(event: E, validate: ValidateFunction<_T>): Promise<_T> { const _body = await readBody(event, { strict: true }); return validateData(_body, validate); } From b52066cd4e298fd4fae67d9e4ca1dcbb84a196b8 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 26 Jul 2023 19:53:33 +0200 Subject: [PATCH 07/17] gitignore tsconfig.vitest-temp.json --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5236dadb..08767158 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ coverage .profile .idea .eslintcache +tsconfig.vitest-temp.json From 0d84a828f59f38e0b88a3bdd72aafae8fb0c7f67 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 26 Jul 2023 21:09:02 +0200 Subject: [PATCH 08/17] update body.ts --- src/utils/body.ts | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/src/utils/body.ts b/src/utils/body.ts index 246612d6..25bbcb1e 100644 --- a/src/utils/body.ts +++ b/src/utils/body.ts @@ -91,13 +91,14 @@ export function readRawBody( * const body = await readBody(event) * ``` */ -export async function readBody( - event: H3Event, - options: { strict?: boolean } = {} -): Promise { +export async function readBody< + T, + _E extends H3Event = H3Event, + _T = void extends T ? (_E extends H3Event ? E["body"] : never) : T +>(event: _E, options: { strict?: boolean } = {}): Promise<_T> { const request = event.node.req as InternalRequest; if (ParsedBodySymbol in request) { - return request[ParsedBodySymbol]; + return request[ParsedBodySymbol] as _T; } const contentType = request.headers["content-type"] || ""; @@ -116,22 +117,14 @@ export async function readBody( } request[ParsedBodySymbol] = parsed; - return parsed; -} - -export function readTypedBody< - T = unknown, - E extends H3Event = H3Event, - _T = unknown extends T ? (E extends H3Event ? E["body"] : never) : T ->(event: E, options: { strict?: boolean } = {}): Promise<_T> { - return readBody(event, options) as Promise<_T>; + return parsed as unknown as _T; } export async function readValidatedBody< - T = unknown, - E extends H3Event = H3Event, - _T = unknown extends T ? (E extends H3Event ? E["body"] : never) : T ->(event: E, validate: ValidateFunction<_T>): Promise<_T> { + T, + _E extends H3Event = H3Event, + _T = void extends T ? (_E extends H3Event ? E["body"] : never) : T +>(event: _E, validate: ValidateFunction<_T>): Promise<_T> { const _body = await readBody(event, { strict: true }); return validateData(_body, validate); } From 414d97aed4da89446b3bc8078ba93d2e8d6bc780 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 26 Jul 2023 21:09:10 +0200 Subject: [PATCH 09/17] update request.ts --- src/utils/request.ts | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/utils/request.ts b/src/utils/request.ts index 989bcd2a..9504cafe 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -1,19 +1,26 @@ -import { getQuery as _getQuery } from "ufo"; +import { QueryObject, getQuery as _getQuery } from "ufo"; import { createError } from "../error"; import type { HTTPMethod, RequestHeaders } from "../types"; import type { H3Event } from "../event"; import { validateData, ValidateFunction } from "./internal/validate"; -export function getQuery(event: E) { - return _getQuery(event.path || "") as E extends H3Event - ? Input["query"] - : any; +export function getQuery< + T, + _E extends H3Event = H3Event, + _T = void extends T + ? _E extends H3Event + ? E["query"] + : QueryObject + : T +>(event: _E): _T { + return _getQuery(event.path || "") as _T; } -export function getValidatedQuery( - event: H3Event, - validate: ValidateFunction -): Promise { +export function getValidatedQuery< + T, + _E extends H3Event = H3Event, + _T = void extends T ? (_E extends H3Event ? E["query"] : never) : T +>(event: _E, validate: ValidateFunction<_T>): Promise<_T> { const query = getQuery(event); return validateData(query, validate); } From aa3fcaa9972a58a9547a344ff32d65389dc84dd6 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 26 Jul 2023 21:10:36 +0200 Subject: [PATCH 10/17] update type tests --- test/types.test-d.ts | 112 ++++++++++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 44 deletions(-) diff --git a/test/types.test-d.ts b/test/types.test-d.ts index 07335ca8..882d6aee 100644 --- a/test/types.test-d.ts +++ b/test/types.test-d.ts @@ -1,59 +1,83 @@ import { describe, it, expectTypeOf } from "vitest"; -import { - eventHandler, - H3Event, - readBody, - getQuery, - readTypedBody, -} from "../src"; - -describe("types for event handlers", () => { - it("return type test", () => { - const handler = eventHandler(() => { - return { - foo: "bar", - }; - }); - - expectTypeOf(handler({} as H3Event)).toEqualTypeOf< - { foo: string } | Promise<{ foo: string }> - >(); - }); +// import type { QueryObject } from "ufo"; +import { eventHandler, H3Event, getQuery, readBody } from "../src"; - it("input type test", () => { - eventHandler<{ body: { id: string } }>(async (event) => { - const body = await readTypedBody(event); - expectTypeOf(body).toEqualTypeOf<{ id: string }>(); - expectTypeOf(getQuery(event)).toBeUnknown(); +type MaybePromise = T | Promise; - return null; +describe("types", () => { + describe("eventHandler", () => { + it("return type (inferred)", () => { + const handler = eventHandler(() => { + return { + foo: "bar", + }; + }); + expectTypeOf(handler({} as H3Event)).toEqualTypeOf< + MaybePromise<{ foo: string }> + >(); }); - eventHandler<{ query: { id: string } }>((event) => { - const query = getQuery(event); - expectTypeOf(query).toEqualTypeOf<{ id: string }>(); - - return null; + it("return type (simple generic)", () => { + const handler = eventHandler(() => { + return ""; + }); + expectTypeOf(handler({} as H3Event)).toEqualTypeOf< + MaybePromise + >(); }); }); - it("allows backwards compatible generic for eventHandler definition", () => { - const handler = eventHandler(() => { - return ""; + describe("readBody", () => { + it("untyped", () => { + eventHandler(async (event) => { + const body = await readBody(event); + // For backwards compatibility - this should likely become `unknown` in future + expectTypeOf(body).toBeAny(); + }); + }); + + it("typed via generic", () => { + eventHandler(async (event) => { + const body = await readBody(event); + expectTypeOf(body).not.toBeAny(); + expectTypeOf(body).toBeString(); + }); + }); + + it("typed via event handler", () => { + eventHandler<{ body: { id: string } }>(async (event) => { + const body = await readBody(event); + expectTypeOf(body).not.toBeAny(); + expectTypeOf(body).toEqualTypeOf<{ id: string }>(); + }); }); - expectTypeOf(handler({} as H3Event)).toEqualTypeOf< - string | Promise - >(); }); - // For backwards compatibility - this should likely become `unknown` in future - it("input types aren't applied when omitted", () => { - eventHandler(async (event) => { - const body = await readBody(event); - expectTypeOf(body).toBeAny(); - expectTypeOf(getQuery(event)).toBeAny(); + describe("getQuery", () => { + it("untyped", () => { + eventHandler((event) => { + const query = getQuery(event); + // TODO: It should be QueryObject to avoid breaking changes! + expectTypeOf(query).toBeAny(); + // expectTypeOf(query).not.toBeAny(); + // expectTypeOf(query).toEqualTypeOf(); + }); + }); + + it("typed via generic", () => { + eventHandler((event) => { + const query = getQuery<{ id: string }>(event); + expectTypeOf(query).not.toBeAny(); + expectTypeOf(query).toEqualTypeOf<{ id: string }>(); + }); + }); - return null; + it("typed via event handler", () => { + eventHandler<{ query: { id: string } }>((event) => { + const query = getQuery(event); + expectTypeOf(query).not.toBeAny(); + expectTypeOf(query).toEqualTypeOf<{ id: string }>(); + }); }); }); }); From db28d005afdc795fdb1cf048e9a9e611fb1114bd Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 26 Jul 2023 21:17:41 +0200 Subject: [PATCH 11/17] refactor: InferEventInput utility --- src/types.ts | 6 ++++++ src/utils/body.ts | 15 ++++++++------- src/utils/request.ts | 20 ++++++++------------ 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/types.ts b/src/types.ts index 01b1d6a7..15e4a5aa 100644 --- a/src/types.ts +++ b/src/types.ts @@ -45,6 +45,12 @@ export interface TypedEventInputSignature { query?: any; } +export type InferEventInput< + Key extends keyof TypedEventInputSignature, + Event extends H3Event, + T +> = void extends T ? (Event extends H3Event ? E[Key] : never) : T; + export interface EventHandler< Input extends TypedEventInputSignature = any, Return = any diff --git a/src/utils/body.ts b/src/utils/body.ts index 25bbcb1e..92035a5e 100644 --- a/src/utils/body.ts +++ b/src/utils/body.ts @@ -1,6 +1,6 @@ import type { IncomingMessage } from "node:http"; import destr from "destr"; -import type { Encoding, HTTPMethod } from "../types"; +import type { Encoding, HTTPMethod, InferEventInput } from "../types"; import type { H3Event } from "../event"; import { createError } from "../error"; import { parse as parseMultipartData } from "./internal/multipart"; @@ -91,11 +91,12 @@ export function readRawBody( * const body = await readBody(event) * ``` */ + export async function readBody< T, - _E extends H3Event = H3Event, - _T = void extends T ? (_E extends H3Event ? E["body"] : never) : T ->(event: _E, options: { strict?: boolean } = {}): Promise<_T> { + Event extends H3Event = H3Event, + _T = InferEventInput<"body", Event, T> +>(event: Event, options: { strict?: boolean } = {}): Promise<_T> { const request = event.node.req as InternalRequest; if (ParsedBodySymbol in request) { return request[ParsedBodySymbol] as _T; @@ -122,9 +123,9 @@ export async function readBody< export async function readValidatedBody< T, - _E extends H3Event = H3Event, - _T = void extends T ? (_E extends H3Event ? E["body"] : never) : T ->(event: _E, validate: ValidateFunction<_T>): Promise<_T> { + Event extends H3Event = H3Event, + _T = InferEventInput<"body", Event, T> +>(event: Event, validate: ValidateFunction<_T>): Promise<_T> { const _body = await readBody(event, { strict: true }); return validateData(_body, validate); } diff --git a/src/utils/request.ts b/src/utils/request.ts index 9504cafe..5b00d6ad 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -1,26 +1,22 @@ -import { QueryObject, getQuery as _getQuery } from "ufo"; +import { getQuery as _getQuery } from "ufo"; import { createError } from "../error"; -import type { HTTPMethod, RequestHeaders } from "../types"; +import type { HTTPMethod, InferEventInput, RequestHeaders } from "../types"; import type { H3Event } from "../event"; import { validateData, ValidateFunction } from "./internal/validate"; export function getQuery< T, - _E extends H3Event = H3Event, - _T = void extends T - ? _E extends H3Event - ? E["query"] - : QueryObject - : T ->(event: _E): _T { + Event extends H3Event = H3Event, + _T = InferEventInput<"query", Event, T> +>(event: Event): _T { return _getQuery(event.path || "") as _T; } export function getValidatedQuery< T, - _E extends H3Event = H3Event, - _T = void extends T ? (_E extends H3Event ? E["query"] : never) : T ->(event: _E, validate: ValidateFunction<_T>): Promise<_T> { + Event extends H3Event = H3Event, + _T = InferEventInput<"query", Event, T> +>(event: Event, validate: ValidateFunction<_T>): Promise<_T> { const query = getQuery(event); return validateData(query, validate); } From 7d1939bdad48c88fa2f9488b2dbc9a1438ae1bb1 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 26 Jul 2023 21:24:30 +0200 Subject: [PATCH 12/17] fix query default --- src/event/utils.ts | 9 ++++++--- src/types.ts | 7 +++++-- src/utils/request.ts | 2 +- test/types.test-d.ts | 10 ++++------ 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/event/utils.ts b/src/event/utils.ts index c1b7a43a..0c231116 100644 --- a/src/event/utils.ts +++ b/src/event/utils.ts @@ -5,12 +5,15 @@ import type { } from "../types"; export function defineEventHandler< - Input extends TypedEventInputSignature = any, + Input extends TypedEventInputSignature = TypedEventInputSignature, Return = any >(handler: EventHandler): EventHandler; // TODO: remove when appropriate // This signature provides backwards compatibility with previous signature where first generic was return type -export function defineEventHandler( +export function defineEventHandler< + Input = TypedEventInputSignature, + Return = any +>( handler: EventHandler< Input extends TypedEventInputSignature ? Input : any, Input extends TypedEventInputSignature ? Return : Input @@ -20,7 +23,7 @@ export function defineEventHandler( Input extends TypedEventInputSignature ? Return : Input >; export function defineEventHandler< - Input extends TypedEventInputSignature = any, + Input extends TypedEventInputSignature = TypedEventInputSignature, Return = any >(handler: EventHandler): EventHandler { handler.__is_handler__ = true; diff --git a/src/types.ts b/src/types.ts index 15e4a5aa..005231a3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,6 @@ +import type { QueryObject } from "ufo"; import type { H3Event } from "./event"; -import { Session } from "./utils/session"; +import type { Session } from "./utils/session"; export type { ValidateFunction, @@ -41,8 +42,10 @@ export interface H3EventContext extends Record { export type EventHandlerResponse = T | Promise; export interface TypedEventInputSignature { + // TODO: Default to unknown in next major version body?: any; - query?: any; + + query?: QueryObject; } export type InferEventInput< diff --git a/src/utils/request.ts b/src/utils/request.ts index 5b00d6ad..11263fed 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -7,7 +7,7 @@ import { validateData, ValidateFunction } from "./internal/validate"; export function getQuery< T, Event extends H3Event = H3Event, - _T = InferEventInput<"query", Event, T> + _T = Exclude, undefined> >(event: Event): _T { return _getQuery(event.path || "") as _T; } diff --git a/test/types.test-d.ts b/test/types.test-d.ts index 882d6aee..9352477d 100644 --- a/test/types.test-d.ts +++ b/test/types.test-d.ts @@ -1,5 +1,5 @@ import { describe, it, expectTypeOf } from "vitest"; -// import type { QueryObject } from "ufo"; +import type { QueryObject } from "ufo"; import { eventHandler, H3Event, getQuery, readBody } from "../src"; type MaybePromise = T | Promise; @@ -31,7 +31,7 @@ describe("types", () => { it("untyped", () => { eventHandler(async (event) => { const body = await readBody(event); - // For backwards compatibility - this should likely become `unknown` in future + // TODO: Default to unknown in next major version expectTypeOf(body).toBeAny(); }); }); @@ -57,10 +57,8 @@ describe("types", () => { it("untyped", () => { eventHandler((event) => { const query = getQuery(event); - // TODO: It should be QueryObject to avoid breaking changes! - expectTypeOf(query).toBeAny(); - // expectTypeOf(query).not.toBeAny(); - // expectTypeOf(query).toEqualTypeOf(); + expectTypeOf(query).not.toBeAny(); + expectTypeOf(query).toEqualTypeOf(); }); }); From 051979343ed344de6df3fccd3e79e9e720f3a8df Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 26 Jul 2023 21:25:21 +0200 Subject: [PATCH 13/17] update (unsed) default for H3Event generic --- src/event/event.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/event/event.ts b/src/event/event.ts index 7a630739..23c36e79 100644 --- a/src/event/event.ts +++ b/src/event/event.ts @@ -29,9 +29,10 @@ export interface NodeEventContext { res: NodeServerResponse; } -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export class H3Event<_Input extends TypedEventInputSignature = any> - implements Pick +export class H3Event< + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _Input extends TypedEventInputSignature = TypedEventInputSignature +> implements Pick { "__is_event__" = true; From dd5ecf30f0dbacf6298ae138402567dcb20fec5a Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 26 Jul 2023 21:31:42 +0200 Subject: [PATCH 14/17] refactor: input => request --- src/event/event.ts | 8 ++------ src/event/utils.ts | 23 ++++++++++------------- src/types.ts | 8 ++++---- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/src/event/event.ts b/src/event/event.ts index 23c36e79..fcb10320 100644 --- a/src/event/event.ts +++ b/src/event/event.ts @@ -1,9 +1,5 @@ import type { IncomingHttpHeaders } from "node:http"; -import type { - H3EventContext, - HTTPMethod, - TypedEventInputSignature, -} from "../types"; +import type { H3EventContext, HTTPMethod, EventHandlerRequest } from "../types"; import type { NodeIncomingMessage, NodeServerResponse } from "../node"; import { MIMES, @@ -31,7 +27,7 @@ export interface NodeEventContext { export class H3Event< // eslint-disable-next-line @typescript-eslint/no-unused-vars - _Input extends TypedEventInputSignature = TypedEventInputSignature + Request extends EventHandlerRequest = EventHandlerRequest > implements Pick { "__is_event__" = true; diff --git a/src/event/utils.ts b/src/event/utils.ts index 0c231116..b7fab409 100644 --- a/src/event/utils.ts +++ b/src/event/utils.ts @@ -1,31 +1,28 @@ import type { EventHandler, LazyEventHandler, - TypedEventInputSignature, + EventHandlerRequest, } from "../types"; export function defineEventHandler< - Input extends TypedEventInputSignature = TypedEventInputSignature, + Request extends EventHandlerRequest = EventHandlerRequest, Return = any ->(handler: EventHandler): EventHandler; +>(handler: EventHandler): EventHandler; // TODO: remove when appropriate // This signature provides backwards compatibility with previous signature where first generic was return type -export function defineEventHandler< - Input = TypedEventInputSignature, - Return = any ->( +export function defineEventHandler( handler: EventHandler< - Input extends TypedEventInputSignature ? Input : any, - Input extends TypedEventInputSignature ? Return : Input + Request extends EventHandlerRequest ? Request : any, + Request extends EventHandlerRequest ? Return : Request > ): EventHandler< - Input extends TypedEventInputSignature ? Input : any, - Input extends TypedEventInputSignature ? Return : Input + Request extends EventHandlerRequest ? Request : any, + Request extends EventHandlerRequest ? Return : Request >; export function defineEventHandler< - Input extends TypedEventInputSignature = TypedEventInputSignature, + Request extends EventHandlerRequest = EventHandlerRequest, Return = any ->(handler: EventHandler): EventHandler { +>(handler: EventHandler): EventHandler { handler.__is_handler__ = true; return handler; } diff --git a/src/types.ts b/src/types.ts index 005231a3..bccd988c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -41,7 +41,7 @@ export interface H3EventContext extends Record { export type EventHandlerResponse = T | Promise; -export interface TypedEventInputSignature { +export interface EventHandlerRequest { // TODO: Default to unknown in next major version body?: any; @@ -49,17 +49,17 @@ export interface TypedEventInputSignature { } export type InferEventInput< - Key extends keyof TypedEventInputSignature, + Key extends keyof EventHandlerRequest, Event extends H3Event, T > = void extends T ? (Event extends H3Event ? E[Key] : never) : T; export interface EventHandler< - Input extends TypedEventInputSignature = any, + Request extends EventHandlerRequest = EventHandlerRequest, Return = any > { __is_handler__?: true; - (event: H3Event): EventHandlerResponse; + (event: H3Event): EventHandlerResponse; } export type LazyEventHandler = () => EventHandler | Promise; From 532df6c860852db1e2041c5b8388df58827683c0 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 26 Jul 2023 21:34:16 +0200 Subject: [PATCH 15/17] refactor: return ~> response --- src/event/event.ts | 2 +- src/event/utils.ts | 17 ++++++++++------- src/types.ts | 4 ++-- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/event/event.ts b/src/event/event.ts index fcb10320..c349eeac 100644 --- a/src/event/event.ts +++ b/src/event/event.ts @@ -27,7 +27,7 @@ export interface NodeEventContext { export class H3Event< // eslint-disable-next-line @typescript-eslint/no-unused-vars - Request extends EventHandlerRequest = EventHandlerRequest + _RequestT extends EventHandlerRequest = EventHandlerRequest > implements Pick { "__is_event__" = true; diff --git a/src/event/utils.ts b/src/event/utils.ts index b7fab409..b29ffc50 100644 --- a/src/event/utils.ts +++ b/src/event/utils.ts @@ -6,23 +6,26 @@ import type { export function defineEventHandler< Request extends EventHandlerRequest = EventHandlerRequest, - Return = any ->(handler: EventHandler): EventHandler; + Response = any +>(handler: EventHandler): EventHandler; // TODO: remove when appropriate // This signature provides backwards compatibility with previous signature where first generic was return type -export function defineEventHandler( +export function defineEventHandler< + Request = EventHandlerRequest, + Response = any +>( handler: EventHandler< Request extends EventHandlerRequest ? Request : any, - Request extends EventHandlerRequest ? Return : Request + Request extends EventHandlerRequest ? Response : Request > ): EventHandler< Request extends EventHandlerRequest ? Request : any, - Request extends EventHandlerRequest ? Return : Request + Request extends EventHandlerRequest ? Response : Request >; export function defineEventHandler< Request extends EventHandlerRequest = EventHandlerRequest, - Return = any ->(handler: EventHandler): EventHandler { + Response = any +>(handler: EventHandler): EventHandler { handler.__is_handler__ = true; return handler; } diff --git a/src/types.ts b/src/types.ts index bccd988c..733364cf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -56,10 +56,10 @@ export type InferEventInput< export interface EventHandler< Request extends EventHandlerRequest = EventHandlerRequest, - Return = any + Response = EventHandlerResponse > { __is_handler__?: true; - (event: H3Event): EventHandlerResponse; + (event: H3Event): EventHandlerResponse; } export type LazyEventHandler = () => EventHandler | Promise; From 721932feff38102e60bc14847138c10c3bb524e7 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 26 Jul 2023 21:38:25 +0200 Subject: [PATCH 16/17] use EventHandlerResponse when possible --- src/event/utils.ts | 5 +++-- src/types.ts | 4 ++-- test/types.test-d.ts | 12 ++++-------- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/event/utils.ts b/src/event/utils.ts index b29ffc50..9b489ebd 100644 --- a/src/event/utils.ts +++ b/src/event/utils.ts @@ -2,6 +2,7 @@ import type { EventHandler, LazyEventHandler, EventHandlerRequest, + EventHandlerResponse, } from "../types"; export function defineEventHandler< @@ -12,7 +13,7 @@ export function defineEventHandler< // This signature provides backwards compatibility with previous signature where first generic was return type export function defineEventHandler< Request = EventHandlerRequest, - Response = any + Response = EventHandlerResponse >( handler: EventHandler< Request extends EventHandlerRequest ? Request : any, @@ -24,7 +25,7 @@ export function defineEventHandler< >; export function defineEventHandler< Request extends EventHandlerRequest = EventHandlerRequest, - Response = any + Response = EventHandlerResponse >(handler: EventHandler): EventHandler { handler.__is_handler__ = true; return handler; diff --git a/src/types.ts b/src/types.ts index 733364cf..542ca441 100644 --- a/src/types.ts +++ b/src/types.ts @@ -56,10 +56,10 @@ export type InferEventInput< export interface EventHandler< Request extends EventHandlerRequest = EventHandlerRequest, - Response = EventHandlerResponse + Response extends EventHandlerResponse = EventHandlerResponse > { __is_handler__?: true; - (event: H3Event): EventHandlerResponse; + (event: H3Event): Response; } export type LazyEventHandler = () => EventHandler | Promise; diff --git a/test/types.test-d.ts b/test/types.test-d.ts index 9352477d..7fef91a5 100644 --- a/test/types.test-d.ts +++ b/test/types.test-d.ts @@ -2,8 +2,6 @@ import { describe, it, expectTypeOf } from "vitest"; import type { QueryObject } from "ufo"; import { eventHandler, H3Event, getQuery, readBody } from "../src"; -type MaybePromise = T | Promise; - describe("types", () => { describe("eventHandler", () => { it("return type (inferred)", () => { @@ -12,18 +10,16 @@ describe("types", () => { foo: "bar", }; }); - expectTypeOf(handler({} as H3Event)).toEqualTypeOf< - MaybePromise<{ foo: string }> - >(); + const response = handler({} as H3Event); + expectTypeOf(response).toEqualTypeOf<{ foo: string }>(); }); it("return type (simple generic)", () => { const handler = eventHandler(() => { return ""; }); - expectTypeOf(handler({} as H3Event)).toEqualTypeOf< - MaybePromise - >(); + const response = handler({} as H3Event); + expectTypeOf(response).toEqualTypeOf(); }); }); From 3a9fd8b73740b5cd8684915692016d72fe52e21c Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 26 Jul 2023 21:48:41 +0200 Subject: [PATCH 17/17] add typed validator tests --- test/types.test-d.ts | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/test/types.test-d.ts b/test/types.test-d.ts index 7fef91a5..f2029085 100644 --- a/test/types.test-d.ts +++ b/test/types.test-d.ts @@ -1,6 +1,13 @@ import { describe, it, expectTypeOf } from "vitest"; import type { QueryObject } from "ufo"; -import { eventHandler, H3Event, getQuery, readBody } from "../src"; +import { + eventHandler, + H3Event, + getQuery, + readBody, + readValidatedBody, + getValidatedQuery, +} from "../src"; describe("types", () => { describe("eventHandler", () => { @@ -40,6 +47,16 @@ describe("types", () => { }); }); + it("typed via validator", () => { + eventHandler(async (event) => { + // eslint-disable-next-line unicorn/consistent-function-scoping + const validator = (body: unknown) => body as { id: string }; + const body = await readValidatedBody(event, validator); + expectTypeOf(body).not.toBeAny(); + expectTypeOf(body).toEqualTypeOf<{ id: string }>(); + }); + }); + it("typed via event handler", () => { eventHandler<{ body: { id: string } }>(async (event) => { const body = await readBody(event); @@ -66,6 +83,16 @@ describe("types", () => { }); }); + it("typed via validator", () => { + eventHandler(async (event) => { + // eslint-disable-next-line unicorn/consistent-function-scoping + const validator = (body: unknown) => body as { id: string }; + const body = await getValidatedQuery(event, validator); + expectTypeOf(body).not.toBeAny(); + expectTypeOf(body).toEqualTypeOf<{ id: string }>(); + }); + }); + it("typed via event handler", () => { eventHandler<{ query: { id: string } }>((event) => { const query = getQuery(event);