UNPKG

cassava

Version:
265 lines 11.4 kB
"use strict"; 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