UNPKG

roads

Version:

An isomophic http framework

253 lines 10.8 kB
"use strict"; /** * basicRouter.ts * Copyright(c) 2021 Aaron Hedges <aaron@dashron.com> * MIT Licensed * * This is a basic router middleware for roads. * It allows you to easily attach functionality to HTTP methods and paths. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BasicRouter = void 0; const url_parse_1 = __importDefault(require("url-parse")); const response_1 = __importDefault(require("../core/response")); const requestChain_1 = require("../core/requestChain"); /** * This is a basic router middleware for roads. * You can assign functions to url paths, and those paths can have some very basic variable templating * * Templating is basic. Each URI is considered to be a series of "path parts" separated by slashes. * If a path part starts with a #, it is assumed to be a numeric variable. Non-numbers will not match this route * If a path part starts with a $, it is considered to be an alphanumeric variabe. * All non-slash values will match this route. * * Any variables will be added to the route's request url object under the "args" object. * * e.g. * /users/#user_id will match /users/12345, not /users/abcde. If a request is made to /users/12345 * the route's requestUrl object will contain { args: {user_id: 12345}} along with all other url object values * * @name BasicRouter */ class BasicRouter { _routes; /** * @param {Road} [road] - The road that will receive the BasicRouter middleware */ constructor(road) { this._routes = []; if (road) { this.applyMiddleware(road); } } /** * If you don't provide a road to the SimpleRouter constructor, your routes will not be executed. * If you have reason not to assign the road off the bat, you can assign it later with this function. * * @param {Road} road - The road that will receive the BasicRouter middleware */ applyMiddleware(road) { // We need to alias because "this" for the middleware function must // be the this applied by road.use, not the BasicRouter // eslint-disable-next-line @typescript-eslint/no-this-alias const _self = this; // We do this to ensure we have access to the BasicRouter once we lose this due to road's context road.use((function (request_method, request_url, request_body, request_headers, next) { return _self._middleware.call(this, _self._routes, request_method, request_url, request_body, request_headers, next); })); } /** * This is where you want to write the majority of your webservice. The `fn` parameter should contain * the actions you want to perform when a certain `path` and HTTP `method` are accessed via the `road` object. * * The path supports a very basic templating system. The values inbetween each slash can be interpreted * in one of three ways * - If a path part starts with a #, it is assumed to be a numeric variable. Non-numbers will not match this route * - If a path part starts with a $, it is considered to be an alphanumeric variabe. All non-slash values * will match this route. * - If a path starts with anything but a # or a $, it is assumed to be a literal. Only that value will match * this route. * * e.g. /users/#userId will match /users/12345, not /users/abcde. If a request is made to /users/12345 the * route's requestUrl object will include the key value pair of `args: { userId: 12345 }` * Any variables will be added to the route's request url object under the "args" object. * * * @param {string} method - The HTTP method that will trigger the provided function * @param {(string|array)} paths - One or many URL paths that will trigger the provided function * @param {function} fn - The function containing all of your route logic */ addRoute(method, paths, fn) { if (!Array.isArray(paths)) { paths = [paths]; } paths.forEach((path) => { this._routes.push({ path: path, method: method, route: Array.isArray(fn) ? new requestChain_1.RequestChain(fn) : fn }); }); } /** * Add an entire file worth of routes. * * The file should be a node module that exposes an object. * Each key should be an HTTP path, each value should be an object. * In that object, each key should be an HTTP method, and the value should be your route function. * * @param {string} filePath - The file path * @param {string} [prefix] - A string that will help namespace this file. e.g. if you call this on a file * with a route of "/posts", and the prefix "/users", the route will be assigned to "/users/posts" */ addRouteFile(filePath, prefix) { return import(filePath).then(routes => { for (const path in routes) { for (const method in routes[path]) { this.addRoute(method, buildRouterPath(path, prefix), routes[path][method]); } } }); } /** * Slightly non-standard roads middleware to execute the functions in this router when requests are received by the road * The first method is the routes to ensure that we can properly use this router once we loose the "this" value * from the roads context */ async _middleware(routes, request_method, request_url, request_body, request_headers, next) { let realMethod = request_method; let response = null; let hit = false; let routeHitMethodFail = false; const parsed_url = (0, url_parse_1.default)(request_url, true); // Only override on POST methods if (realMethod === 'POST') { const methodOverrideHeader = request_headers?.['x-http-method-override']; const methodOverrideQuery = parsed_url.query?.['_method']; if (methodOverrideHeader) { realMethod = Array.isArray(methodOverrideHeader) ? methodOverrideHeader.join('') : methodOverrideHeader; } else if (methodOverrideQuery) { realMethod = Array.isArray(methodOverrideQuery) ? methodOverrideQuery.join('') : methodOverrideQuery; } } for (let i = 0; i < routes.length; i++) { const route = routes[i]; if (compareRouteAndApplyArgs(route, parsed_url)) { // Check the method last, so we can give proper status codes if (route.method === realMethod) { if (route.route instanceof requestChain_1.RequestChain) { response = route.route.getChainStart()(this, realMethod, parsed_url, request_body, request_headers); } else { response = (route.route).call(this, realMethod, parsed_url, request_body, request_headers, next); } hit = true; break; } else { routeHitMethodFail = true; } } } if (hit) { return response; } if (routeHitMethodFail) { return new response_1.default('Method Not Allowed', 405); } return next(); } } exports.BasicRouter = BasicRouter; /** * Checks to see if the route matches the request, and if true assigns any applicable url variables and returns the route * * @param {object} route - Route object from this basic router class * @param {object} route.method - HTTP method associated with this route * @param {object} route.path - HTTP path associated with this route * @param {object} request_url - Parsed HTTP request url * @param {string} request_method - HTTP request method * @returns {boolean} */ function compareRouteAndApplyArgs(route, request_url) { if (!request_url.pathname) { return false; } let template = route.path.split('/'); if (template[0] === '') { template = template.slice(1); // Slice kills the emptystring before the leading slash } let actual = request_url.pathname.split('/'); if (actual[0] === '') { actual = actual.slice(1); // Slice kills the emptystring before the leading slash } if (template.length != actual.length) { return false; } for (let i = 0; i < template.length; i++) { const actual_part = actual[i]; const template_part = template[i]; // Process variables first if (template_part[0] === '#') { // # templates only accept numbers if (isNaN(Number(actual_part))) { return false; } // TODO: get rid of this `as` applyArg(request_url, template_part.substring(1), Number(actual_part)); continue; } if (template_part[0] === '$') { // $ templates accept any non-slash alphanumeric character // TODO: get rid of this `as` applyArg(request_url, template_part.substring(1), String(actual_part)); // Continue so that continue; } // Process exact matches second if (actual_part === template_part) { continue; } return false; } return true; } /** * Assigns a value to the parsed request urls args parameter * * @param {object} request_url - The parsed url object * @param {string} template_part - The template variable * @param {*} actual_part - The url value */ function applyArg(request_url, template_part, actual_part) { if (typeof (request_url.args) === 'undefined') { request_url.args = {}; } if (typeof request_url.args !== 'object') { throw new Error(`The request url's args have already been defined as a ${typeof request_url.args} and we expected an object. For safety we are throwing this error instead of overwriting your existing data. Please use a different field name in your code`); } request_url.args[template_part] = actual_part; } /** * Applies a prefix to paths of route files * * @param {string} path - The HTTP path of a route * @param {string} [prefix] - An optional prefix for the HTTP path * @returns {string} */ function buildRouterPath(path, prefix) { if (!prefix) { prefix = ''; } if (prefix.length && path === '/') { return prefix; } return prefix + path; } //# sourceMappingURL=basicRouter.js.map