UNPKG

http-micro

Version:

Micro-framework on top of node's http module

302 lines (301 loc) 10.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("./utils"); const pathToRegexp = require("path-to-regexp"); const createError = require("http-errors"); class RouteDescriptor { constructor(test = null, definitions = {}) { this.test = test; this.definitions = definitions; } } exports.RouteDescriptor = RouteDescriptor; class RouteDefinition { constructor(handler, paramKeys = null) { this.handler = handler; this.paramKeys = paramKeys; } } exports.RouteDefinition = RouteDefinition; class Router { constructor(opts = null) { if (opts) this._opts = Object.assign({}, Router.Defaults, opts); } get opts() { return this._opts || Router.Defaults; } get routes() { return this._routes || (this._routes = new Array()); } /** * Add a route for all http action methods. Basically, * add the route for each method in HttpMethod.ActionMethods * * @param route * @param handler */ all(route, handler, opts) { HttpMethod.ActionMethods.forEach(x => { this.define(route, x, handler); }); return this; } /** * Add a route for any action regardless of the method. * If a specific method for the same path is also provided, * that always takes precedence. * @param route * @param handler */ any(route, handler, opts) { return this.define(route, HttpMethod.Wildcard, handler, opts); } /** * Add a route for Http GET method * @param route * @param handler */ get(route, handler, opts) { return this.define(route, HttpMethod.Get, handler, opts); } /** * Add a route for Http PUT method * @param route * @param handler */ put(route, handler, opts) { return this.define(route, HttpMethod.Put, handler, opts); } /** * Add a route for Http POST method * @param route * @param handler */ post(route, handler, opts) { return this.define(route, HttpMethod.Post, handler, opts); } /** * Add a route for Http DELETE method * @param route * @param handler */ delete(route, handler, opts) { return this.define(route, HttpMethod.Delete, handler, opts); } /** * Add a route for Http PATCH method * @param route * @param handler */ patch(route, handler, opts) { return this.define(route, HttpMethod.Patch, handler, opts); } use(...args) { if (args.length === 2) { let first = args[0]; let second = args[1]; if (typeof first === "string" || first instanceof RegExp) { let h = second instanceof Router ? second.build() : second; this.any(first, h); return this; } } if (!this._middlewares) this._middlewares = []; utils_1.addMiddlewares(args, this._middlewares); return this; } /** * Add a definition for a route for a Http method. This is the method * that's internally called for each of the 'get', 'post' etc, methods * on the router. They are just convenience methods for this method. * * @param route * @param method Preferably, use the HttpMethod helpers instead * of using raw strings. For breviety, only the common standard HTTP 1.1 * methods are given there. * * @param handler */ define(route, method, handler, opts) { if (typeof handler !== "function") throw new TypeError("handler not valid"); let m = method.toString(); typeof route === "string" ? this._definePathMatchRoute(route, m, handler, opts) : this._defineRegExpRoute(route, m, handler, null, null); return this; } _definePathMatchRoute(route, method, handler, opts) { let keys = []; let re = pathToRegexp(route, keys, Object.assign({}, this.opts, opts)); this._defineRegExpRoute(re, method, handler, route, keys); } _defineRegExpRoute(route, method, handler, path, paramKeys) { let routes = this.routes; let existing = routes.find(x => x.test.source === route.source); if (!existing) { existing = new RouteDescriptor(route); routes.push(existing); } existing.definitions[method] = new RouteDefinition(handler, paramKeys); } /** * Check if a path and method pair matches a defined route in * the router, and if so return the route. * * It tries to match a specific route first, and if no routes are * found, tries to see if there's an 'any' route that's provided, * to match all routes. * * If there are no matches, returns null. * * @param {string} path * @param {string} method * @returns {MatchResult<T>|null} * */ match(path, method) { return this._match(this._routes, method, path); } _match(routes, method, targetPath) { if (!routes) return null; let breakEarly = this.opts.exhaustive; for (let i = 0; i < routes.length; i++) { let x = routes[i]; let match = targetPath.match(x.test); if (match) { let methodResult = x.definitions[method]; if (!methodResult && method === HttpMethod.Head) { methodResult = x.definitions[HttpMethod.Get]; } if (!methodResult) { methodResult = x.definitions[HttpMethod.Wildcard]; } if (methodResult) { let paramKeys = methodResult.paramKeys; let params = paramKeys ? createRouteParams(match, paramKeys) : null; let result = { router: this, route: methodResult, descriptor: x, data: match, params, path: match[0], }; return result; } if (breakEarly) break; } } return null; } /** * Returns a middleware function that executes the router. It composes * the middlewares of the router when called and returns one middleware * that executes the router pipeline. * */ build() { return createRouteHandler(this); } } Router.Defaults = { strict: true, end: false, sensitive: true, exhaustive: false }; exports.Router = Router; function createRouteHandler(router) { return (context, next) => { return routeHandler(router, context, next); }; } exports.createRouteHandler = createRouteHandler; function routeHandler(router, context, next) { let routeContext = context.getRouteContext(); let method = context.getHttpMethod(); let path = routeContext.getPendingRoutePath(); let match = router.match(path, method); let middlewares = router._middlewares; if (match) { routeContext.push(match); let isMatchReset = false; let resetIfNeeded = (passthrough) => { if (!isMatchReset) { if (routeContext.getCurrentMatch() === match) routeContext.pop(); isMatchReset = true; } return passthrough; }; let nextHandler = () => { resetIfNeeded(); return next(); }; let errorHandler = (err) => { resetIfNeeded(); throw err; }; // 1. Use `nextHandler` to ensure that reset is done before `next` is called, so // the next middleware has correct match context before it executes. // 2. Use a `resetIfNeeded` with `then`, to ensure that if the `next` was not called, // the reset still works as expected. // 3. Use a `catch` handler to ensure that it's reset on error. if (middlewares && middlewares.length > 0) { return utils_1.dispatch(context, () => match.route.handler(context, nextHandler), middlewares) .then(resetIfNeeded) .catch(errorHandler); } return match.route.handler(context, nextHandler) .then(resetIfNeeded) .catch(errorHandler); } if (middlewares && middlewares.length > 0) return utils_1.dispatch(context, next, middlewares); return next(); } exports.routeHandler = routeHandler; class HttpMethod { } HttpMethod.Get = "GET"; HttpMethod.Head = "HEAD"; HttpMethod.Post = "POST"; HttpMethod.Put = "PUT"; HttpMethod.Delete = "DELETE"; HttpMethod.Patch = "PATCH"; HttpMethod.Options = "OPTIONS"; HttpMethod.Trace = "TRACE"; HttpMethod.ActionMethods = [ HttpMethod.Get, HttpMethod.Post, HttpMethod.Put, HttpMethod.Delete, HttpMethod.Patch ]; HttpMethod.Wildcard = "*"; exports.HttpMethod = HttpMethod; function createRouteParams(match, keys, params) { params = params || {}; var key, param; for (var i = 0; i < keys.length; i++) { key = keys[i]; param = match[i + 1]; if (!param) continue; params[key.name] = decodeRouteParam(param); if (key.repeat) params[key.name] = params[key.name].split(key.delimiter); } return params; } exports.createRouteParams = createRouteParams; function decodeRouteParam(param) { try { return decodeURIComponent(param); } catch (_) { throw createError(400, 'failed to decode param "' + param + '"'); } } exports.decodeRouteParam = decodeRouteParam;