cassava
Version:
AWS API Gateway Router
265 lines • 11.4 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Router = void 0;
const cookieLib = require("cookie");
const url = require("url");
const routes_1 = require("./routes");
const RestError_1 = require("./RestError");
const RouterEvent_1 = require("./RouterEvent");
const RouterResponse_1 = require("./RouterResponse");
const httpStatus_1 = require("./httpStatus");
class Router {
constructor() {
/**
* Both node 4.3 and 6.10 use the callback parameter to return a result.
* The ability to create new functions using Node.js 4.3 will be disabled July 31, 2018.
* Code updates to existing functions using Node.js v4.3 will be disabled on October 31, 2018.
* When the ability to update code in node 6.10 functions is disabled this can be removed.
*/
this.useLegacyCallbackHandler = process.version.startsWith("v4.3") || process.version.startsWith("v6.10");
/**
* Routes that will be tested against in order.
*/
this.routes = [];
/**
* The default route that will be matched if no other routes matched.
*
* The default implementation is to return a 404 response.
*/
this.defaultRoute = new routes_1.DefaultRoute();
/**
* The handler that will be called when non-RestErrors are thrown.
* The handler can return nothing, a RouterResponse, or a Promise that resolves
* such. If a RouterResponse or Promise of RouterResponse is returned that will
* be the response used.
*
* The handler will be called with: the Error thrown, the input ProxyEvent that
* caused the error and the Lambda context.
*
* The default implementation is to log the error.
*/
this.errorHandler = err => console.log("Error thrown during execution.\n", err);
}
route(path) {
if (!path) {
const route = new routes_1.BuildableRoute();
this.routes.push(route);
return route;
}
else if (typeof path === "string" || path instanceof RegExp) {
const route = new routes_1.BuildableRoute();
route.path(path);
this.routes.push(route);
return route;
}
else if (path.matches && (path.handle || path.postProcess)) {
this.routes.push(path);
return path;
}
throw new Error("Input must be a string or regex to create a new RouteBuilder, or an instance of Route.");
}
getLambdaHandler() {
if (this.useLegacyCallbackHandler) {
return (evt, ctx, callback) => {
this.routeProxyEvent(evt, ctx)
.then(res => {
callback(undefined, res);
}, err => {
this.errorToRouterResponse(err, evt, ctx)
.then(res => {
callback(undefined, this.routerResponseToProxyResponse(res));
})
.catch(() => {
console.error("Catastrophic error thrown during execution.\n", err);
callback(err);
});
});
};
}
else {
return (evt, ctx) => {
return this.routeProxyEvent(evt, ctx)
.catch(err => {
return this.errorToRouterResponse(err, evt, ctx)
.then(res => this.routerResponseToProxyResponse(res));
});
};
}
}
routeProxyEvent(pevt, ctx) {
return __awaiter(this, void 0, void 0, function* () {
// Non-functional programming for speeeeed.
const evt = this.proxyEventToRouterEvent(pevt);
let resp = null;
let handlingRoute;
const postProcessors = [];
for (let routeIx = 0; routeIx < this.routes.length && !resp; routeIx++) {
const route = this.routes[routeIx];
if (route.enabled !== false && route.matches(evt)) {
if (route.postProcess) {
postProcessors.push(route);
}
if (route.handle) {
handlingRoute = route;
try {
resp = yield route.handle(evt);
}
catch (err) {
resp = yield this.errorToRouterResponse(err, pevt, ctx);
}
}
}
}
if (!resp) {
try {
if (!this.defaultRoute.handle) {
throw new Error("Router's defaultRoute.handle is not defined.");
}
handlingRoute = this.defaultRoute;
resp = yield this.defaultRoute.handle(evt);
if (!resp) {
throw new Error("Router's defaultRoute.handle() did not return a response.");
}
}
catch (err) {
resp = yield this.errorToRouterResponse(err, pevt, ctx);
}
}
const handlingRoutes = [handlingRoute];
while (postProcessors.length) {
const route = postProcessors.pop();
try {
resp = (yield route.postProcess(evt, resp, handlingRoutes)) || resp;
}
catch (err) {
resp = yield this.errorToRouterResponse(err, pevt, ctx);
}
if (handlingRoutes[handlingRoutes.length - 1] !== route) {
handlingRoutes.push(route);
}
}
return this.routerResponseToProxyResponse(resp);
});
}
proxyEventToRouterEvent(evt) {
const r = new RouterEvent_1.RouterEvent();
r.requestContext = evt.requestContext;
r.headers = evt.headers || {};
r.multiValueHeaders = evt.multiValueHeaders || {};
r.httpMethod = evt.httpMethod;
r.meta = {};
r.path = this.proxyPathToRouterPath(evt.path);
r.queryStringParameters = evt.queryStringParameters || {};
r.multiValueQueryStringParameters = evt.multiValueQueryStringParameters || {};
r.pathParameters = evt.pathParameters || {};
r.stageVariables = evt.stageVariables || {};
r.bodyRaw = evt.body;
r.headersLowerCase = {};
for (const headerKey of Object.keys(r.headers)) {
r.headersLowerCase[headerKey.toLowerCase()] = r.headers[headerKey];
}
r.multiValueHeadersLowerCase = {};
for (const headerKey of Object.keys(r.multiValueHeaders)) {
r.multiValueHeadersLowerCase[headerKey.toLowerCase()] = r.multiValueHeaders[headerKey];
}
if (typeof evt.body === "string" && (!r.headersLowerCase["content-type"] || /(application|text)\/(x-)?json/.test(r.headersLowerCase["content-type"]))) {
try {
if (evt.isBase64Encoded) {
r.body = JSON.parse(Buffer.from(evt.body, "base64").toString());
}
else {
r.body = JSON.parse(evt.body);
}
}
catch (e) {
throw new RestError_1.RestError(httpStatus_1.httpStatusCode.clientError.BAD_REQUEST, `Unable to parse JSON body: ${e.message}`);
}
}
else {
r.body = evt.body;
}
r.cookies = {};
if (r.headersLowerCase["cookie"]) {
r.cookies = cookieLib.parse(r.headersLowerCase["cookie"]);
}
return r;
}
proxyPathToRouterPath(path) {
if (url.URL) {
// This constructor was added in Node v6.13.0.
return new url.URL(path, "http://host/").pathname.replace(/\/\/+/g, "/");
}
else if (url.parse) {
// This method was deprecated in Node v6.13.0.
return url.parse(path).pathname.replace(/\/\/+/g, "/");
}
else {
throw new Error("No suitable URL parsing method in the 'url' package found.");
}
}
errorToRouterResponse(err, pevt, ctx) {
return __awaiter(this, void 0, void 0, function* () {
if (err && err.isRestError) {
return {
statusCode: err.statusCode,
body: Object.assign({ message: err.message, statusCode: err.statusCode }, err.additionalParams)
};
}
if (this.errorHandler) {
const resp = yield this.errorHandler(err, pevt, ctx);
if (resp) {
return resp;
}
}
return {
statusCode: httpStatus_1.httpStatusCode.serverError.INTERNAL_SERVER_ERROR,
body: {
message: httpStatus_1.httpStatusString[httpStatus_1.httpStatusCode.serverError.INTERNAL_SERVER_ERROR],
statusCode: httpStatus_1.httpStatusCode.serverError.INTERNAL_SERVER_ERROR
}
};
});
}
routerResponseToProxyResponse(resp) {
if (resp.cookies) {
for (const key of Object.keys(resp.cookies)) {
const value = resp.cookies[key];
const cookieString = typeof value === "string" ? cookieLib.serialize(key, value) : cookieLib.serialize(key, value.value, value.options);
RouterResponse_1.RouterResponse.setHeader(resp, "Set-Cookie", cookieString);
}
}
let isBase64Encoded = false;
let body;
const contentType = RouterResponse_1.RouterResponse.getHeader(resp, "Content-Type");
if (resp.body instanceof Buffer) {
body = resp.body.toString("base64");
isBase64Encoded = true;
}
else if (!contentType) {
// Automatic serialization to JSON if Content-Type is not set.
body = JSON.stringify(resp.body);
RouterResponse_1.RouterResponse.setHeader(resp, "Content-Type", "application/json");
}
else {
body = resp.body;
}
return {
statusCode: resp.statusCode || httpStatus_1.httpStatusCode.success.OK,
headers: resp.headers || {},
multiValueHeaders: resp.multiValueHeaders || {},
body,
isBase64Encoded
};
}
}
exports.Router = Router;
//# sourceMappingURL=Router.js.map