somod-http-extension
Version:
SOMOD middleware to intercept and validate Lambda event for AWS APIGateway
185 lines (184 loc) • 6.99 kB
JavaScript
import { join } from "path";
import { FILE_ROUTES_HTTP_JSON, MIDDLEWARE_CONTEXT_KEY, PATH_HTTP_SCHEMAS } from "../../../lib/constants";
import { existsSync } from "fs";
import { readFile } from "fs/promises";
import { validate } from "decorated-ajv";
import { getHttpSchemaPath } from "../../../lib/utils";
import { decode } from "querystring";
import { BadRequestError, NoRouteFoundError } from "../../../lib/types";
import { pathToFileURL } from "url";
let configuredRoutes = null;
const getConfiguredRoutes = async () => {
if (configuredRoutes === null) {
// NOTE: To Match the path used in prepare stage
const routesJsonPath = join(__dirname, PATH_HTTP_SCHEMAS, FILE_ROUTES_HTTP_JSON);
if (!existsSync(routesJsonPath)) {
throw new Error("Found no routes at " + routesJsonPath);
}
const routesStr = await readFile(routesJsonPath, { encoding: "utf8" });
const routes = JSON.parse(routesStr);
if (typeof routes !== "object") {
throw new Error("Invalid routes configuration in " + routesJsonPath);
}
configuredRoutes = routes;
}
return configuredRoutes;
};
const validators = {};
const loadValidator = async (path, method, key) => {
const validatorPath = getHttpSchemaPath(path, method, key);
if (validators[validatorPath] === undefined) {
try {
validators[validatorPath] = (await import(pathToFileURL(join(__dirname, PATH_HTTP_SCHEMAS, validatorPath)).toString())).default;
}
catch (e) {
// eslint-disable-next-line no-console
console.error("Error in loading validator", e);
// @ts-expect-error this is okay to assign the default validate function here
validators[validatorPath] = () => {
return true;
};
}
}
return validators[validatorPath];
};
const getMethodAndPath = (event) => {
const [method, path] = event.routeKey.split(" ");
return { path, method };
};
const getRouteConfig = async (path, method) => {
const configuredRoutes = await getConfiguredRoutes();
const routeConfig = configuredRoutes[path]?.[method];
if (routeConfig === undefined) {
throw new NoRouteFoundError(`No route defined for ${method} ${path}`);
}
return routeConfig;
};
const validateParameter = async (path, method, name, _in, value, required) => {
if (required && value === undefined) {
throw new Error(`Parameter ${name} must be present in ${_in}`);
}
if (value !== undefined) {
const validator = await loadValidator(path, method, { name, in: _in });
const violations = await validate(validator, value);
if (violations.length > 0) {
throw new BadRequestError(JSON.stringify({
message: `Invalid Parameter ${name} in ${_in}`,
errors: violations
}));
}
}
return value;
};
const validateParameters = async (path, method, routeConfig, event) => {
const validatedParameters = {
path: {},
query: {},
header: {}
};
if (routeConfig.parameters) {
await Promise.all(routeConfig.parameters.map(async (parameter) => {
let value = undefined;
if (parameter.in == "path") {
value = event.pathParameters?.[parameter.name];
}
else if (parameter.in == "query") {
value = event.queryStringParameters?.[parameter.name];
}
else if (parameter.in == "header") {
value = event.headers[parameter.name];
}
validatedParameters[parameter.in][parameter.name] =
await validateParameter(path, method, parameter.name, parameter.in, value, parameter.required);
}));
}
return validatedParameters;
};
const parseBody = (routeConfig, event) => {
let contentType = routeConfig.body?.parser;
if (contentType === undefined) {
if (event.headers["content-type"]?.includes("application/json")) {
contentType = "json";
}
else if (event.headers["content-type"]?.includes("application/x-www-form-urlencoded")) {
contentType = "formdata";
}
else {
contentType = "text";
}
}
let parsedBody = event.body || "";
if (contentType == "json") {
parsedBody = JSON.parse(parsedBody);
}
else if (contentType == "formdata") {
parsedBody = decode(parsedBody);
}
return parsedBody;
};
const validateBody = async (path, method, routeConfig, event) => {
let validatedBody = undefined;
if (routeConfig.body) {
const validator = await loadValidator(path, method, "body");
const parsedBody = parseBody(routeConfig, event);
const violations = await validate(validator, parsedBody);
if (violations.length > 0) {
throw new BadRequestError(JSON.stringify({
message: `Invalid Request Body`,
errors: violations
}));
}
validatedBody = parsedBody;
}
return validatedBody;
};
const middleware = async (next, event) => {
try {
const { method, path } = getMethodAndPath(event);
const routeConfig = await getRouteConfig(path, method);
const validatedParameters = await validateParameters(path, method, routeConfig, event);
const validatedBody = await validateBody(path, method, routeConfig, event);
const somodHttpRequest = {
route: path,
method: method,
parameters: validatedParameters,
body: validatedBody
};
event.somodMiddlewareContext.set(MIDDLEWARE_CONTEXT_KEY, somodHttpRequest);
return await next();
}
catch (e) {
if (e instanceof NoRouteFoundError) {
if (process.env.SOMOD_HTTP_LOG_4XX === "true") {
const { method, path } = getMethodAndPath(event);
// eslint-disable-next-line no-console
console.error(method, path, e.message);
}
return {
statusCode: 404,
headers: { "Content-Type": "application/json" },
body: e.message
};
}
else if (e instanceof BadRequestError) {
if (process.env.SOMOD_HTTP_LOG_4XX === "true") {
const { method, path } = getMethodAndPath(event);
// eslint-disable-next-line no-console
console.error(method, path, e.message);
}
return {
statusCode: 400,
headers: { "Content-Type": "application/json" },
body: e.message
};
}
else {
// eslint-disable-next-line no-console
console.error(e.message);
return {
statusCode: 500
};
}
}
};
export default middleware;