Skip to content

Commit f5d5280

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

File tree

7 files changed

+851
-428
lines changed

7 files changed

+851
-428
lines changed

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

Lines changed: 47 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,49 @@ abstract class BaseRouter {
133139
};
134140
}
135141

136-
public abstract resolve(
142+
public async resolve(
137143
event: unknown,
138144
context: Context,
139145
options?: ResolveOptions
140-
): Promise<unknown>;
146+
): Promise<APIGatewayProxyResult | undefined> {
147+
if (!isAPIGatewayProxyEvent(event)) {
148+
this.logger.warn(
149+
'Received an event that is not compatible with this resolver'
150+
);
151+
return;
152+
}
153+
154+
try {
155+
const request = proxyEventToWebRequest(event);
156+
const path = new URL(request.url).pathname as Path;
157+
const method = request.method.toUpperCase() as HttpMethod;
158+
159+
const route = this.routeRegistry.resolve(method, path);
160+
161+
if (route === null) {
162+
throw new NotFoundError(`Route ${path} for method ${method} not found`);
163+
}
164+
165+
const result = await route.handler.apply(options?.scope ?? this, [
166+
route.params,
167+
{
168+
event,
169+
context,
170+
request,
171+
},
172+
]);
173+
174+
return await handlerResultToProxyResult(result);
175+
} catch (error) {
176+
const result = await this.handleError(error as Error, {
177+
request: proxyEventToWebRequest(event),
178+
event,
179+
context,
180+
scope: options?.scope,
181+
});
182+
return await responseToProxyResult(result);
183+
}
184+
}
141185

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

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

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
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

35
const createBody = (body: string | null, isBase64Encoded: boolean) => {
46
if (body === null) return null;
@@ -9,7 +11,9 @@ const createBody = (body: string | null, isBase64Encoded: boolean) => {
911
return Buffer.from(body, 'base64').toString('utf8');
1012
};
1113

12-
export const proxyEventToWebRequest = (event: APIGatewayProxyEvent) => {
14+
export const proxyEventToWebRequest = (
15+
event: APIGatewayProxyEvent
16+
): Request => {
1317
const { httpMethod, path, domainName } = event.requestContext;
1418

1519
const headers = new Headers();
@@ -23,7 +27,7 @@ export const proxyEventToWebRequest = (event: APIGatewayProxyEvent) => {
2327
}
2428
}
2529
const hostname = headers.get('Host') ?? domainName;
26-
const protocol = headers.get('X-Forwarded-Proto') ?? 'http';
30+
const protocol = headers.get('X-Forwarded-Proto') ?? 'https';
2731

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

@@ -45,4 +49,45 @@ export const proxyEventToWebRequest = (event: APIGatewayProxyEvent) => {
4549
headers,
4650
body: createBody(event.body, event.isBase64Encoded),
4751
});
48-
}
52+
};
53+
54+
export const responseToProxyResult = async (
55+
response: Response
56+
): Promise<APIGatewayProxyResult> => {
57+
const headers: Record<string, string> = {};
58+
const multiValueHeaders: Record<string, Array<string>> = {};
59+
60+
for (const [key, value] of response.headers.entries()) {
61+
const values = value.split(',').map((v) => v.trimStart());
62+
if (values.length > 1) {
63+
multiValueHeaders[key] = values;
64+
} else {
65+
headers[key] = value;
66+
}
67+
}
68+
69+
return {
70+
statusCode: response.status,
71+
headers,
72+
multiValueHeaders,
73+
body: await response.text(),
74+
isBase64Encoded: false,
75+
};
76+
};
77+
78+
export const handlerResultToProxyResult = async (
79+
response: HandlerResponse
80+
): Promise<APIGatewayProxyResult> => {
81+
if (isAPIGatewayProxyResult(response)) {
82+
return response;
83+
}
84+
if (response instanceof Response) {
85+
return await responseToProxyResult(response);
86+
}
87+
return {
88+
statusCode: 200,
89+
body: JSON.stringify(response),
90+
headers: { 'Content-Type': 'application/json' },
91+
isBase64Encoded: false,
92+
};
93+
};

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)