Skip to content

Commit 624c61d

Browse files
committed
Update get method with the overloads as in the issue description
1 parent f0b1d40 commit 624c61d

File tree

6 files changed

+189
-30
lines changed

6 files changed

+189
-30
lines changed

packages/event-handler/src/http/Route.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
import type {
2+
HandlerResponse,
23
HttpMethod,
34
Middleware,
45
Path,
56
RouteHandler,
7+
TypedRouteHandler,
68
} from '../types/http.js';
79

8-
class Route {
10+
class Route<TReqBody = never, TResBody extends HandlerResponse = HandlerResponse> {
911
readonly id: string;
1012
readonly method: string;
1113
readonly path: Path;
12-
readonly handler: RouteHandler;
14+
readonly handler: RouteHandler | TypedRouteHandler<TReqBody, TResBody>;
1315
readonly middleware: Middleware[];
1416

1517
constructor(
1618
method: HttpMethod,
1719
path: Path,
18-
handler: RouteHandler,
20+
handler: RouteHandler | TypedRouteHandler<TReqBody, TResBody>,
1921
middleware: Middleware[] = []
2022
) {
2123
this.id = `${method}:${path}`;

packages/event-handler/src/http/RouteHandlerRegistry.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { GenericLogger } from '@aws-lambda-powertools/commons/types';
22
import { isRegExp } from '@aws-lambda-powertools/commons/typeutils';
33
import type {
44
DynamicRoute,
5+
HandlerResponse,
56
HttpMethod,
67
HttpRouteHandlerOptions,
78
Path,
@@ -94,7 +95,7 @@ class RouteHandlerRegistry {
9495
*
9596
* @param route - The route to register
9697
*/
97-
public register(route: Route): void {
98+
public register<TReqBody = never, TResBody extends HandlerResponse = HandlerResponse>(route: Route<TReqBody, TResBody>): void {
9899
this.#shouldSort = true;
99100
const { isValid, issues } = validatePathPattern(route.path);
100101
if (!isValid) {

packages/event-handler/src/http/Router.ts

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,14 @@ import type {
2828
HttpRouteOptions,
2929
HttpRouterOptions,
3030
Middleware,
31+
ValidationConfig,
3132
Path,
3233
RequestContext,
3334
ResolveStreamOptions,
3435
ResponseStream,
3536
RouteHandler,
3637
RouterResponse,
38+
TypedRouteHandler,
3739
} from '../types/http.js';
3840
import type { HandlerResponse, ResolveOptions } from '../types/index.js';
3941
import { HttpStatusCodes, HttpVerbs } from './constants.js';
@@ -443,14 +445,17 @@ class Router {
443445
}
444446
}
445447

446-
public route(handler: RouteHandler, options: HttpRouteOptions): void {
448+
public route<TReqBody = never, TResBody extends HandlerResponse = HandlerResponse>(
449+
handler: RouteHandler | TypedRouteHandler<TReqBody, TResBody>,
450+
options: HttpRouteOptions
451+
): void {
447452
const { method, path, middleware = [], validation } = options;
448453
const methods = Array.isArray(method) ? method : [method];
449454
const resolvedPath = resolvePrefixedPath(path, this.prefix);
450455

451456
// Create validation middleware if validation config provided
452457
const allMiddleware = validation
453-
? [...middleware, createValidationMiddleware(validation)]
458+
? [...middleware, createValidationMiddleware<TReqBody, TResBody>(validation as ValidationConfig<TReqBody, TResBody>)]
454459
: middleware;
455460

456461
for (const method of methods) {
@@ -570,15 +575,12 @@ class Router {
570575
);
571576
}
572577

573-
#handleHttpMethod<
574-
TReqBody = never,
575-
TResBody extends HandlerResponse = HandlerResponse
576-
>(
578+
#handleHttpMethod<TReqBody = never, TResBody extends HandlerResponse = HandlerResponse>(
577579
method: HttpMethod,
578580
path: Path,
579581
middlewareOrHandler?: Middleware[] | RouteHandler,
580-
handlerOrOptions?: RouteHandler | Omit<HttpRouteOptions, 'method' | 'path'>,
581-
options?: Omit<HttpRouteOptions, 'method' | 'path' | 'middleware'>
582+
handlerOrOptions?: RouteHandler | TypedRouteHandler<TReqBody, TResBody>,
583+
options?: { validation: ValidationConfig<TReqBody, TResBody> }
582584
): MethodDecorator | undefined {
583585
// Case 1: post(path, [middleware], handler, { validation })
584586
if (Array.isArray(middlewareOrHandler)) {
@@ -628,27 +630,31 @@ class Router {
628630
public get(
629631
path: Path,
630632
handler: RouteHandler,
631-
options?: Omit<HttpRouteOptions, 'method' | 'path'>
632633
): void;
633634
public get(
634635
path: Path,
635636
middleware: Middleware[],
636637
handler: RouteHandler,
637-
options?: Omit<HttpRouteOptions, 'method' | 'path' | 'middleware'>
638638
): void;
639639
public get(path: Path): MethodDecorator;
640640
public get(path: Path, middleware: Middleware[]): MethodDecorator;
641+
public get<TReqBody = never, TResBody extends HandlerResponse = HandlerResponse>(
642+
path: Path,
643+
middleware: Middleware[],
644+
handler: TypedRouteHandler<TReqBody, TResBody>,
645+
options: { validation: ValidationConfig<TReqBody, TResBody> }
646+
): void;
641647
public get<TReqBody = never, TResBody extends HandlerResponse = HandlerResponse>(
642648
path: Path,
643649
middlewareOrHandler?: Middleware[] | RouteHandler,
644-
handlerOrOptions?: RouteHandler | Omit<HttpRouteOptions, 'method' | 'path'>,
645-
options?: Omit<HttpRouteOptions, 'method' | 'path' | 'middleware'>
650+
handler?: RouteHandler | TypedRouteHandler<TReqBody, TResBody>,
651+
options?: { validation: ValidationConfig<TReqBody, TResBody> }
646652
): MethodDecorator | undefined {
647653
return this.#handleHttpMethod<TReqBody, TResBody>(
648654
HttpVerbs.GET,
649655
path,
650656
middlewareOrHandler,
651-
handlerOrOptions,
657+
handler,
652658
options
653659
);
654660
}
@@ -666,6 +672,12 @@ class Router {
666672
): void;
667673
public post(path: Path): MethodDecorator;
668674
public post(path: Path, middleware: Middleware[]): MethodDecorator;
675+
public post<TReqBody = never, TResBody extends HandlerResponse = HandlerResponse>(
676+
path: Path,
677+
middlewareOrHandler?: Middleware[] | RouteHandler,
678+
handlerOrOptions?: RouteHandler | Omit<HttpRouteOptions, 'method' | 'path'>,
679+
options?: Omit<HttpRouteOptions, 'method' | 'path' | 'middleware'>
680+
): MethodDecorator | undefined;
669681
public post<TReqBody = never, TResBody extends HandlerResponse = HandlerResponse>(
670682
path: Path,
671683
middlewareOrHandler?: Middleware[] | RouteHandler,

packages/event-handler/src/http/middleware/validation.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import type { StandardSchemaV1 } from '@standard-schema/spec';
2-
import type { HttpRouteOptions, Middleware } from '../../types/http.js';
2+
import type {
3+
HandlerResponse,
4+
Middleware,
5+
ValidatedRequest,
6+
ValidatedResponse,
7+
ValidationConfig,
8+
} from '../../types/http.js';
39
import { RequestValidationError, ResponseValidationError } from '../errors.js';
410

511
/**
@@ -8,13 +14,22 @@ import { RequestValidationError, ResponseValidationError } from '../errors.js';
814
* @param config - Validation configuration for request and response
915
* @returns Middleware function that validates request/response
1016
*/
11-
export const createValidationMiddleware = (
12-
config: HttpRouteOptions['validation']
17+
export const createValidationMiddleware = <
18+
TReqBody = unknown,
19+
TResBody extends HandlerResponse = HandlerResponse
20+
>(
21+
config: ValidationConfig<TReqBody, TResBody>
1322
): Middleware => {
1423
const reqSchemas = config?.req;
1524
const resSchemas = config?.res;
1625

1726
return async ({ reqCtx, next }) => {
27+
// Initialize valid object
28+
reqCtx.valid = {
29+
req: {} as ValidatedRequest<TReqBody>,
30+
res: {} as ValidatedResponse<TResBody>,
31+
};
32+
1833
// Validate request
1934
if (reqSchemas) {
2035
if (reqSchemas.body) {
@@ -33,20 +48,20 @@ export const createValidationMiddleware = (
3348
bodyData = await clonedRequest.text();
3449
}
3550

36-
await validateRequest(reqSchemas.body, bodyData, 'body');
51+
reqCtx.valid.req.body = await validateRequest(reqSchemas.body, bodyData, 'body') as TReqBody;
3752
}
3853
if (reqSchemas.headers) {
3954
const headers = Object.fromEntries(reqCtx.req.headers.entries());
40-
await validateRequest(reqSchemas.headers, headers, 'headers');
55+
reqCtx.valid.req.headers = await validateRequest(reqSchemas.headers, headers, 'headers') as Record<string, string>;
4156
}
4257
if (reqSchemas.path) {
43-
await validateRequest(reqSchemas.path, reqCtx.params, 'path');
58+
reqCtx.valid.req.path = await validateRequest(reqSchemas.path, reqCtx.params, 'path') as Record<string, string>;
4459
}
4560
if (reqSchemas.query) {
4661
const query = Object.fromEntries(
4762
new URL(reqCtx.req.url).searchParams.entries()
4863
);
49-
await validateRequest(reqSchemas.query, query, 'query');
64+
reqCtx.valid.req.query = await validateRequest(reqSchemas.query, query, 'query') as Record<string, string>;
5065
}
5166
}
5267

@@ -64,12 +79,12 @@ export const createValidationMiddleware = (
6479
? await clonedResponse.json()
6580
: await clonedResponse.text();
6681

67-
await validateResponse(resSchemas.body, bodyData, 'body');
82+
reqCtx.valid.res.body = await validateResponse(resSchemas.body, bodyData, 'body') as TResBody;
6883
}
6984

7085
if (resSchemas.headers) {
7186
const headers = Object.fromEntries(response.headers.entries());
72-
await validateResponse(resSchemas.headers, headers, 'headers');
87+
reqCtx.valid.res.headers = await validateResponse(resSchemas.headers, headers, 'headers') as Record<string, string>;
7388
}
7489
}
7590
};
@@ -79,7 +94,7 @@ async function validateRequest(
7994
schema: StandardSchemaV1,
8095
data: unknown,
8196
component: 'body' | 'headers' | 'path' | 'query'
82-
): Promise<void> {
97+
): Promise<unknown> {
8398
try {
8499
const result = await schema['~standard'].validate(data);
85100

@@ -88,6 +103,8 @@ async function validateRequest(
88103
const error = new Error('Validation failed');
89104
throw new RequestValidationError(message, component, error);
90105
}
106+
107+
return result.value;
91108
} catch (error) {
92109
// Handle schemas that throw errors instead of returning issues
93110
if (error instanceof RequestValidationError) {
@@ -106,7 +123,7 @@ async function validateResponse(
106123
schema: StandardSchemaV1,
107124
data: unknown,
108125
component: 'body' | 'headers'
109-
): Promise<void> {
126+
): Promise<unknown> {
110127
try {
111128
const result = await schema['~standard'].validate(data);
112129

@@ -115,6 +132,8 @@ async function validateResponse(
115132
const error = new Error('Validation failed');
116133
throw new ResponseValidationError(message, component, error);
117134
}
135+
136+
return result.value;
118137
} catch (error) {
119138
// Handle schemas that throw errors instead of returning issues
120139
if (error instanceof ResponseValidationError) {

packages/event-handler/src/types/http.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,36 @@ type ResponseTypeMap = {
2525
ALB: ALBResult;
2626
};
2727

28-
type RequestContext = {
28+
/**
29+
* Validated request data
30+
*/
31+
type ValidatedRequest<TBody = unknown> = {
32+
body: TBody;
33+
headers?: Record<string, string>;
34+
path?: Record<string, string>;
35+
query?: Record<string, string>;
36+
};
37+
38+
/**
39+
* Validated response data
40+
*/
41+
type ValidatedResponse<TBody extends HandlerResponse = HandlerResponse> = {
42+
body?: TBody;
43+
headers?: Record<string, string>;
44+
};
45+
46+
type RequestContext<TReqBody = never, TResBody extends HandlerResponse = HandlerResponse> = {
2947
req: Request;
3048
event: APIGatewayProxyEvent | APIGatewayProxyEventV2 | ALBEvent;
3149
context: Context;
3250
res: Response;
3351
params: Record<string, string>;
3452
responseType: ResponseType;
3553
isBase64Encoded?: boolean;
54+
valid?: {
55+
req: ValidatedRequest<TReqBody>;
56+
res: ValidatedResponse<TResBody>;
57+
};
3658
};
3759

3860
type HttpResolveOptions = ResolveOptions & { isHttpStreaming?: boolean };
@@ -46,7 +68,7 @@ type ErrorHandler<T extends Error = Error> = (
4668

4769
interface ErrorConstructor<T extends Error = Error> {
4870
// biome-ignore lint/suspicious/noExplicitAny: this is a generic type that is intentionally open
49-
new (...args: any[]): T;
71+
new(...args: any[]): T;
5072
prototype: T;
5173
}
5274

@@ -95,6 +117,10 @@ type RouteHandler<TReturn = HandlerResponse> = (
95117
reqCtx: RequestContext
96118
) => Promise<TReturn> | TReturn;
97119

120+
type TypedRouteHandler<TReqBody, TResBody extends HandlerResponse = HandlerResponse, TReturn = HandlerResponse> = (
121+
reqCtx: RequestContext<TReqBody, TResBody>
122+
) => Promise<TReturn> | TReturn;
123+
98124
type HttpMethod = keyof typeof HttpVerbs;
99125

100126
type HttpStatusCode = (typeof HttpStatusCodes)[keyof typeof HttpStatusCodes];
@@ -254,6 +280,7 @@ type RouterResponse =
254280
| APIGatewayProxyResult
255281
| APIGatewayProxyStructuredResultV2
256282
| ALBResult;
283+
257284
/**
258285
* Configuration for request validation
259286
*/
@@ -329,4 +356,7 @@ export type {
329356
ResponseValidationConfig,
330357
ValidationConfig,
331358
ValidationErrorDetail,
359+
ValidatedRequest,
360+
ValidatedResponse,
361+
TypedRouteHandler
332362
};

0 commit comments

Comments
 (0)