Skip to content

Commit 7a9b621

Browse files
committed
feat(event-handler): add resolution logic to base router
1 parent 613a9ae commit 7a9b621

File tree

7 files changed

+888
-431
lines changed

7 files changed

+888
-431
lines changed

packages/event-handler/src/rest/BaseRouter.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
getStringFromEnv,
44
isDevMode,
55
} from '@aws-lambda-powertools/commons/utils/env';
6-
import type { Context } from 'aws-lambda';
6+
import type { APIGatewayProxyResult, Context } from 'aws-lambda';
77
import type { ResolveOptions } from '../types/index.js';
88
import type {
99
ErrorConstructor,
@@ -16,6 +16,11 @@ import type {
1616
RouterOptions,
1717
} from '../types/rest.js';
1818
import { HttpVerbs } from './constants.js';
19+
import {
20+
handlerResultToProxyResult,
21+
proxyEventToWebRequest,
22+
responseToProxyResult,
23+
} from './converters.js';
1924
import { ErrorHandlerRegistry } from './ErrorHandlerRegistry.js';
2025
import {
2126
MethodNotAllowedError,
@@ -24,6 +29,7 @@ import {
2429
} from './errors.js';
2530
import { Route } from './Route.js';
2631
import { RouteHandlerRegistry } from './RouteHandlerRegistry.js';
32+
import { isAPIGatewayProxyEvent } from './utils.js';
2733

2834
abstract class BaseRouter {
2935
protected context: Record<string, unknown>;
@@ -133,11 +139,60 @@ abstract class BaseRouter {
133139
};
134140
}
135141

136-
public abstract resolve(
142+
/**
143+
* Resolves an API Gateway event by routing it to the appropriate handler
144+
* and converting the result to an API Gateway proxy result. Handles errors
145+
* using registered error handlers or falls back to default error handling
146+
* (500 Internal Server Error).
147+
*
148+
* @param event - The Lambda event to resolve
149+
* @param context - The Lambda context
150+
* @param options - Optional resolve options for scope binding
151+
* @returns An API Gateway proxy result or undefined for incompatible events
152+
*/
153+
public async resolve(
137154
event: unknown,
138155
context: Context,
139156
options?: ResolveOptions
140-
): Promise<unknown>;
157+
): Promise<APIGatewayProxyResult | undefined> {
158+
if (!isAPIGatewayProxyEvent(event)) {
159+
this.logger.warn(
160+
'Received an event that is not compatible with this resolver'
161+
);
162+
return;
163+
}
164+
165+
try {
166+
const request = proxyEventToWebRequest(event);
167+
const path = new URL(request.url).pathname as Path;
168+
const method = request.method.toUpperCase() as HttpMethod;
169+
170+
const route = this.routeRegistry.resolve(method, path);
171+
172+
if (route === null) {
173+
throw new NotFoundError(`Route ${path} for method ${method} not found`);
174+
}
175+
176+
const result = await route.handler.apply(options?.scope ?? this, [
177+
route.params,
178+
{
179+
event,
180+
context,
181+
request,
182+
},
183+
]);
184+
185+
return await handlerResultToProxyResult(result);
186+
} catch (error) {
187+
const result = await this.handleError(error as Error, {
188+
request: proxyEventToWebRequest(event),
189+
event,
190+
context,
191+
scope: options?.scope,
192+
});
193+
return await responseToProxyResult(result);
194+
}
195+
}
141196

142197
public route(handler: RouteHandler, options: RouteOptions): void {
143198
const { method, path } = options;

packages/event-handler/src/rest/converters.ts

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
1-
import type { APIGatewayProxyEvent } from 'aws-lambda';
1+
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
2+
import type { HandlerResponse } from '../types/rest.js';
3+
import { isAPIGatewayProxyResult } from './utils.js';
24

5+
/**
6+
* Creates a request body from API Gateway event body, handling base64 decoding if needed.
7+
*
8+
* @param body - The raw body from the API Gateway event
9+
* @param isBase64Encoded - Whether the body is base64 encoded
10+
* @returns The decoded body string or null
11+
*/
312
const createBody = (body: string | null, isBase64Encoded: boolean) => {
413
if (body === null) return null;
514

@@ -9,7 +18,15 @@ const createBody = (body: string | null, isBase64Encoded: boolean) => {
918
return Buffer.from(body, 'base64').toString('utf8');
1019
};
1120

12-
export const proxyEventToWebRequest = (event: APIGatewayProxyEvent) => {
21+
/**
22+
* Converts an API Gateway proxy event to a Web API Request object.
23+
*
24+
* @param event - The API Gateway proxy event
25+
* @returns A Web API Request object
26+
*/
27+
export const proxyEventToWebRequest = (
28+
event: APIGatewayProxyEvent
29+
): Request => {
1330
const { httpMethod, path, domainName } = event.requestContext;
1431

1532
const headers = new Headers();
@@ -23,7 +40,7 @@ export const proxyEventToWebRequest = (event: APIGatewayProxyEvent) => {
2340
}
2441
}
2542
const hostname = headers.get('Host') ?? domainName;
26-
const protocol = headers.get('X-Forwarded-Proto') ?? 'http';
43+
const protocol = headers.get('X-Forwarded-Proto') ?? 'https';
2744

2845
const url = new URL(path, `${protocol}://${hostname}/`);
2946

@@ -45,4 +62,58 @@ export const proxyEventToWebRequest = (event: APIGatewayProxyEvent) => {
4562
headers,
4663
body: createBody(event.body, event.isBase64Encoded),
4764
});
48-
}
65+
};
66+
67+
/**
68+
* Converts a Web API Response object to an API Gateway proxy result.
69+
*
70+
* @param response - The Web API Response object
71+
* @returns An API Gateway proxy result
72+
*/
73+
export const responseToProxyResult = async (
74+
response: Response
75+
): Promise<APIGatewayProxyResult> => {
76+
const headers: Record<string, string> = {};
77+
const multiValueHeaders: Record<string, Array<string>> = {};
78+
79+
for (const [key, value] of response.headers.entries()) {
80+
const values = value.split(',').map((v) => v.trimStart());
81+
if (values.length > 1) {
82+
multiValueHeaders[key] = values;
83+
} else {
84+
headers[key] = value;
85+
}
86+
}
87+
88+
return {
89+
statusCode: response.status,
90+
headers,
91+
multiValueHeaders,
92+
body: await response.text(),
93+
isBase64Encoded: false,
94+
};
95+
};
96+
97+
/**
98+
* Converts a handler response to an API Gateway proxy result.
99+
* Handles APIGatewayProxyResult, Response objects, and plain objects.
100+
*
101+
* @param response - The handler response (APIGatewayProxyResult, Response, or plain object)
102+
* @returns An API Gateway proxy result
103+
*/
104+
export const handlerResultToProxyResult = async (
105+
response: HandlerResponse
106+
): Promise<APIGatewayProxyResult> => {
107+
if (isAPIGatewayProxyResult(response)) {
108+
return response;
109+
}
110+
if (response instanceof Response) {
111+
return await responseToProxyResult(response);
112+
}
113+
return {
114+
statusCode: 200,
115+
body: JSON.stringify(response),
116+
headers: { 'Content-Type': 'application/json' },
117+
isBase64Encoded: false,
118+
};
119+
};

packages/event-handler/src/rest/utils.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { isRecord, isString } from '@aws-lambda-powertools/commons/typeutils';
2-
import type { APIGatewayProxyEvent } from 'aws-lambda';
2+
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
33
import type { CompiledRoute, Path, ValidationResult } from '../types/rest.js';
44
import { PARAM_PATTERN, SAFE_CHARS, UNSAFE_CHARS } from './constants.js';
55

@@ -68,3 +68,26 @@ export const isAPIGatewayProxyEvent = (
6868
(event.body === null || isString(event.body))
6969
);
7070
};
71+
72+
/**
73+
* Type guard to check if the provided result is an API Gateway Proxy result.
74+
*
75+
* We use this function to ensure that the result is an object and has the
76+
* required properties without adding a dependency.
77+
*
78+
* @param result - The result to check
79+
*/
80+
export const isAPIGatewayProxyResult = (
81+
result: unknown
82+
): result is APIGatewayProxyResult => {
83+
if (!isRecord(result)) return false;
84+
return (
85+
typeof result.statusCode === 'number' &&
86+
isString(result.body) &&
87+
(result.headers === undefined || isRecord(result.headers)) &&
88+
(result.multiValueHeaders === undefined ||
89+
isRecord(result.multiValueHeaders)) &&
90+
(result.isBase64Encoded === undefined ||
91+
typeof result.isBase64Encoded === 'boolean')
92+
);
93+
};

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,11 @@ interface CompiledRoute {
5353

5454
type DynamicRoute = Route & CompiledRoute;
5555

56+
type HandlerResponse = Response | JSONObject;
57+
5658
type RouteHandler<
5759
TParams = Record<string, unknown>,
58-
TReturn = Response | JSONObject,
60+
TReturn = HandlerResponse,
5961
> = (args: TParams, options?: RequestOptions) => Promise<TReturn>;
6062

6163
type HttpMethod = keyof typeof HttpVerbs;
@@ -106,6 +108,7 @@ export type {
106108
ErrorHandlerRegistryOptions,
107109
ErrorHandler,
108110
ErrorResolveOptions,
111+
HandlerResponse,
109112
HttpStatusCode,
110113
HttpMethod,
111114
Path,

0 commit comments

Comments
 (0)