Skip to content

Commit de40e62

Browse files
authored
Merge pull request #19 from wesreid/feature/add-proxy-handling
Fix prefilight
2 parents 04b8a34 + 3392460 commit de40e62

File tree

1 file changed

+128
-52
lines changed

1 file changed

+128
-52
lines changed

src/lib/lambda-route-proxy-entry-handler.ts

Lines changed: 128 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,49 @@
1-
2-
import { APIGatewayEvent, APIGatewayProxyEvent, APIGatewayProxyEventV2 } from 'aws-lambda';
3-
import { CustomError } from './custom-error';
4-
import { RouteConfig, ConfigRouteEntry, RouteArguments, RouteModule } from './types-and-interfaces';
5-
import { authorizeRoute } from './authorization-helper';
6-
7-
const getRouteConfigEntry = (config: RouteConfig, method: string, path: string) =>
8-
config.routes.find(r => r.path.toLowerCase() === path.toLowerCase() && r.method.toLowerCase() === method.toLowerCase()) as ConfigRouteEntry;
9-
10-
const shouldAuthorizeRoute = (routesConfig: RouteConfig, routeConfigEntry: ConfigRouteEntry) =>
11-
(routesConfig.authorizeAllRoutes && routeConfigEntry.authorizeRoute !== false)
12-
||
1+
import {
2+
APIGatewayEvent,
3+
APIGatewayProxyEvent,
4+
APIGatewayProxyEventV2,
5+
} from "aws-lambda";
6+
import { CustomError } from "./custom-error";
7+
import {
8+
RouteConfig,
9+
ConfigRouteEntry,
10+
RouteArguments,
11+
RouteModule,
12+
} from "./types-and-interfaces";
13+
import { authorizeRoute } from "./authorization-helper";
14+
15+
const getRouteConfigEntry = (
16+
config: RouteConfig,
17+
method: string,
18+
path: string
19+
) =>
20+
config.routes.find(
21+
(r) =>
22+
r.path.toLowerCase() === path.toLowerCase() &&
23+
r.method.toLowerCase() === method.toLowerCase()
24+
) as ConfigRouteEntry;
25+
26+
const shouldAuthorizeRoute = (
27+
routesConfig: RouteConfig,
28+
routeConfigEntry: ConfigRouteEntry
29+
) =>
30+
(routesConfig.authorizeAllRoutes &&
31+
routeConfigEntry.authorizeRoute !== false) ||
1332
routeConfigEntry.authorizeRoute === true;
1433

15-
16-
export const getRouteModule = (config: RouteConfig, method: string, path: string, availableRouteModules: { [key: string]: any }): RouteModule => {
34+
export const getRouteModule = (
35+
config: RouteConfig,
36+
method: string,
37+
path: string,
38+
availableRouteModules: { [key: string]: any }
39+
): RouteModule => {
1740
const routeEntry = getRouteConfigEntry(config, method, path);
1841
let routeModule = null;
1942
console.log(`route entry: ${JSON.stringify(routeEntry)}`);
2043
if (routeEntry) {
21-
const matchingRouteModuleMapKey = Object.keys(availableRouteModules).find((k: string) => routeEntry.handlerPath.endsWith(k));
44+
const matchingRouteModuleMapKey = Object.keys(availableRouteModules).find(
45+
(k: string) => routeEntry.handlerPath.endsWith(k)
46+
);
2247
// routeModule = availableRouteModules[routeEntry.handlerPath.split('/').reverse()[0]];
2348
routeModule = availableRouteModules[matchingRouteModuleMapKey!];
2449
}
@@ -33,7 +58,10 @@ interface RouteEvent {
3358
isBase64Encoded: boolean;
3459
}
3560

36-
export const getRouteModuleResult = async ({ routeChain }: RouteModule, incoming: RouteArguments): Promise<any> => {
61+
export const getRouteModuleResult = async (
62+
{ routeChain }: RouteModule,
63+
incoming: RouteArguments
64+
): Promise<any> => {
3765
let returnValue = incoming;
3866
for (const chainFn of routeChain) {
3967
returnValue = await chainFn(returnValue);
@@ -44,38 +72,47 @@ export const getRouteModuleResult = async ({ routeChain }: RouteModule, incoming
4472
function pathToRegex(path: string): string {
4573
// Convert route path to regex pattern
4674
return path
47-
.replace(/\//g, '\\/') // Escape forward slashes
48-
.replace(/{([^}]+)}/g, '(?<$1>[^/]+)'); // Convert {param} to named capture groups
75+
.replace(/\//g, "\\/") // Escape forward slashes
76+
.replace(/{([^}]+)}/g, "(?<$1>[^/]+)"); // Convert {param} to named capture groups
4977
}
5078

5179
const v2ApiGatewayEvent = (event: APIGatewayProxyEventV2): RouteEvent => {
5280
return {
5381
routeKey: event.routeKey,
54-
queryStringParameters: event.queryStringParameters?? ({} as RouteEvent['queryStringParameters']),
82+
queryStringParameters:
83+
event.queryStringParameters ??
84+
({} as RouteEvent["queryStringParameters"]),
5585
pathParameters: event.pathParameters ?? {},
5686
body: event.body,
5787
isBase64Encoded: event.isBase64Encoded,
5888
};
59-
}
89+
};
6090

61-
const v1ApiGatewayEvent = (event: APIGatewayProxyEvent, config: RouteConfig): RouteEvent => {
62-
const routeConfig = getRouteConfigByPath(event.path, event.httpMethod, config.routes);
91+
const v1ApiGatewayEvent = (
92+
event: APIGatewayProxyEvent,
93+
config: RouteConfig
94+
): RouteEvent => {
95+
const routeConfig = getRouteConfigByPath(
96+
event.path,
97+
event.httpMethod,
98+
config.routes
99+
);
63100
return {
64101
routeKey: `${event.httpMethod} ${routeConfig.path}`,
65102
queryStringParameters: event.queryStringParameters ?? {},
66103
pathParameters: routeConfig.params ?? {},
67104
body: event.body,
68105
isBase64Encoded: event.isBase64Encoded,
69106
};
70-
}
107+
};
71108

72109
export function getRouteConfigByPath(
73110
eventPath: string,
74111
method: string,
75-
configs: ConfigRouteEntry[],
112+
configs: ConfigRouteEntry[]
76113
): ConfigRouteEntry & { params?: { [key: string]: string } } {
77-
eventPath = eventPath.replace(/\?.*$/, ''); // Remove query string
78-
const normalizedPath = eventPath.replace(/^\//, ''); // Remove leading slash
114+
eventPath = eventPath.replace(/\?.*$/, ""); // Remove query string
115+
const normalizedPath = eventPath.replace(/^\//, ""); // Remove leading slash
79116
for (const config of configs) {
80117
const pattern = pathToRegex(config.path);
81118
const regex = new RegExp(`^${pattern}$`);
@@ -91,17 +128,22 @@ export function getRouteConfigByPath(
91128
}
92129
}
93130

94-
throw new CustomError(JSON.stringify({ message: 'path no found' }), 400);
131+
throw new CustomError(JSON.stringify({ message: "path no found" }), 400);
95132
}
96133

97-
export const lambdaRouteProxyEntryHandler = (config: RouteConfig, availableRouteModules: { [key: string]: any }) =>
98-
async (event: APIGatewayProxyEventV2 | APIGatewayProxyEvent | APIGatewayEvent) => {
134+
export const lambdaRouteProxyEntryHandler =
135+
(config: RouteConfig, availableRouteModules: { [key: string]: any }) =>
136+
async (
137+
event: APIGatewayProxyEventV2 | APIGatewayProxyEvent | APIGatewayEvent
138+
) => {
99139
console.log(`Event Data: ${JSON.stringify(event)}`);
100-
const isV2 = (event as APIGatewayProxyEventV2).version === '2.0';
140+
const isV2 = (event as APIGatewayProxyEventV2).version === "2.0";
101141

102-
const isProxied = !isV2 && event.hasOwnProperty('requestContext');
142+
const isProxied = !isV2 && event.hasOwnProperty("requestContext");
103143

104-
const newEvent = isV2? v2ApiGatewayEvent(event as APIGatewayProxyEventV2): v1ApiGatewayEvent(event as APIGatewayProxyEvent, config);
144+
const newEvent = isV2
145+
? v2ApiGatewayEvent(event as APIGatewayProxyEventV2)
146+
: v1ApiGatewayEvent(event as APIGatewayProxyEvent, config);
105147

106148
const {
107149
routeKey,
@@ -110,64 +152,98 @@ export const lambdaRouteProxyEntryHandler = (config: RouteConfig, availableRoute
110152
body,
111153
isBase64Encoded,
112154
} = newEvent;
113-
155+
114156
let retVal: any = {};
115157
try {
116-
const [method = '', path = ''] = routeKey.split(' ');
117-
if (shouldAuthorizeRoute(config, getRouteConfigEntry(config, method, path))) {
158+
const [method = "", path = ""] = routeKey.split(" ");
159+
if (
160+
shouldAuthorizeRoute(config, getRouteConfigEntry(config, method, path))
161+
) {
118162
await authorizeRoute(event);
119163
}
120164

121-
const routeModule = getRouteModule(config, method, path, availableRouteModules);
165+
const routeModule = getRouteModule(
166+
config,
167+
method,
168+
path,
169+
availableRouteModules
170+
);
122171

123172
console.log(`isBase64Encoded: ${isBase64Encoded}`);
124173
console.log(`body: ${body}`);
125-
const decodedBody = isBase64Encoded ? Buffer.from(body!, 'base64').toString('utf-8') : undefined;
174+
const decodedBody = isBase64Encoded
175+
? Buffer.from(body!, "base64").toString("utf-8")
176+
: undefined;
126177
console.log(`decodedBody:
127178
${decodedBody}`);
128179

129-
130180
retVal = await getRouteModuleResult(routeModule, {
131181
query: queryStringParameters,
132182
params: pathParameters,
133183
body: body ? decodedBody || JSON.parse(body) : undefined,
134184
rawEvent: event,
135185
});
136186

137-
if(isProxied) {
138-
if(retVal.statusCode && !retVal.body) {
139-
console.log('body must be included when status code is set', retVal);
140-
throw new CustomError('No body found', 500);
141-
} else if(retVal.statusCode && retVal.body) {
187+
if (isProxied) {
188+
if (retVal.statusCode && !retVal.body) {
189+
console.log("body must be included when status code is set", retVal);
190+
throw new CustomError("No body found", 500);
191+
} else if (retVal.statusCode && retVal.body) {
142192
retVal = {
143193
...retVal,
144194
isBase64Encoded: false,
145195
headers: {
146-
'Content-Type': 'application/json',
147-
...retVal.headers??{}
196+
"Content-Type": "application/json",
197+
...(retVal.headers ?? {}),
148198
},
149-
body: typeof retVal.body === 'object' ? JSON.stringify(retVal.body): retVal.body
199+
body:
200+
typeof retVal.body === "object"
201+
? JSON.stringify(retVal.body)
202+
: retVal.body,
150203
};
151204
} else {
152205
retVal = {
153206
statusCode: 200,
154207
body: JSON.stringify(retVal),
155-
'Content-Type': 'application/json',
156-
}
208+
"Content-Type": "application/json",
209+
};
157210
}
158211
}
159212
} catch (error: any) {
160213
console.error(JSON.stringify({ error, stack: error.stack }));
214+
let headers = {
215+
"Content-Type": "application/json",
216+
} as Record<string, string>;
217+
218+
let statusCode = 500;
219+
220+
if (isProxied) {
221+
const isOptions = (event.requestContext as any).httpMethod === "OPTIONS";
222+
if (isOptions) {
223+
statusCode = 200;
224+
} else {
225+
statusCode = error.httpStatusCode || 500;
226+
}
227+
headers = {
228+
...headers,
229+
"Access-Control-Allow-Origin": "*",
230+
"Access-Control-Allow-Methods":
231+
"GET, POST, PUT, DELETE, PATCH, OPTIONS",
232+
"Access-Control-Allow-Headers":
233+
"Content-Type, Authorization, X-Amz-Date, X-Api-Key, X-Amz-Security-Token",
234+
"Access-Control-Allow-Credentials": "true",
235+
};
236+
}
161237
if (error instanceof CustomError) {
162238
retVal = {
163-
statusCode: error.httpStatusCode,
164-
headers: { 'Content-Type': 'application/json' },
239+
statusCode,
240+
headers,
165241
body: error.message,
166242
};
167243
} else {
168244
retVal = {
169-
statusCode: 500,
170-
headers: { 'Content-Type': 'application/json' },
245+
statusCode,
246+
headers,
171247
body: error.message || JSON.stringify(error),
172248
};
173249
}

0 commit comments

Comments
 (0)