UNPKG

http-micro

Version:

Micro-framework on top of node's http module

376 lines (330 loc) 12.3 kB
import { Middleware, NextMiddleware } from "./core"; import { Context } from "./context"; import { dispatch, addMiddlewares } from "./utils"; import { RouteContext } from "./route-context"; import * as debugModule from "debug"; import * as pathToRegexp from "path-to-regexp"; import * as createError from "http-errors"; export type PathRouteMap<T extends Context> = Map<string, RouteDescriptor<T>>; export type RegExpRouteMap<T extends Context> = Array<RouteDescriptor<T>>; export class RouteDescriptor<T extends Context> { test: RegExp; definitions: IRouteDefinitionMap<T>; constructor(test: RegExp = null, definitions: IRouteDefinitionMap<T> = {}) { this.test = test; this.definitions = definitions; } } export interface IRouteDefinitionMap<T extends Context> { [method: string]: RouteDefinition<T>; } export class RouteDefinition<T extends Context> { handler: Middleware<T>; paramKeys: any; constructor(handler: Middleware<T>, paramKeys: any = null) { this.handler = handler; this.paramKeys = paramKeys; } } export interface MatchResult<T extends Context> { path: string; router: Router<T>; route: RouteDefinition<T>; descriptor: RouteDescriptor<T>; data: RegExpMatchArray; params: any; } export type Route = string | RegExp; export type RouterOpts = { strict?: boolean; sensitive?: boolean; end?: boolean; delimiter?: string; exhaustive?: boolean; }; export class Router<T extends Context = Context> { static Defaults: RouterOpts = { strict: true, end: false, sensitive: true, exhaustive: false }; private _routes: RegExpRouteMap<T>; private _middlewares: Middleware<T>[]; private _opts: RouterOpts; constructor(opts: RouterOpts = 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<RouteDescriptor<T>>()); } /** * Add a route for all http action methods. Basically, * add the route for each method in HttpMethod.ActionMethods * * @param route * @param handler */ all(route: Route, handler: Middleware<T>, opts?: RouterOpts) { 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: Route, handler: Middleware<T>, opts?: RouterOpts) { return this.define(route, HttpMethod.Wildcard, handler, opts); } /** * Add a route for Http GET method * @param route * @param handler */ get(route: Route, handler: Middleware<T>, opts?: RouterOpts) { return this.define(route, HttpMethod.Get, handler, opts); } /** * Add a route for Http PUT method * @param route * @param handler */ put(route: Route, handler: Middleware<T>, opts?: RouterOpts) { return this.define(route, HttpMethod.Put, handler, opts); } /** * Add a route for Http POST method * @param route * @param handler */ post(route: Route, handler: Middleware<T>, opts?: RouterOpts) { return this.define(route, HttpMethod.Post, handler, opts); } /** * Add a route for Http DELETE method * @param route * @param handler */ delete(route: Route, handler: Middleware<T>, opts?: RouterOpts) { return this.define(route, HttpMethod.Delete, handler, opts); } /** * Add a route for Http PATCH method * @param route * @param handler */ patch(route: Route, handler: Middleware<T>, opts?: RouterOpts) { return this.define(route, HttpMethod.Patch, handler, opts); } /** * Adds a middleware to the router. This is just like adding a * middleware to the application itself, and is executed whenever * the execution reaches the router. * */ use(path: Route, router: Router<T>): Router<T>; use(path: Route, middleware: Middleware<T>): Router<T>; use(...middleware: (Middleware<T> | Router<T>)[]): Router<T>; use(...args: any[]) { 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 = []; 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: Route, method: string, handler: Middleware<T>, opts?: RouterOpts) { 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; } private _definePathMatchRoute(route: string, method: string, handler: Middleware<T>, opts?: RouterOpts) { let keys: pathToRegexp.Key[] = []; let re = pathToRegexp(route, keys, Object.assign({}, this.opts, opts)); this._defineRegExpRoute(re, method, handler, route, keys); } private _defineRegExpRoute(route: RegExp, method: string, handler: Middleware<T>, path: string, paramKeys: pathToRegexp.Key[]) { let routes = this.routes; let existing = routes.find(x => x.test.source === route.source); if (!existing) { existing = new RouteDescriptor<T>(route); routes.push(existing); } existing.definitions[method] = new RouteDefinition<T>(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: string, method: string): MatchResult<T> { return this._match(this._routes, method, path); } private _match(routes: RegExpRouteMap<T>, method: string, targetPath: string) : MatchResult<T> { 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() : Middleware<T> { return createRouteHandler(this); } } export function createRouteHandler<T extends Context>( router: Router<T>): Middleware<T> { return (context: T, next: NextMiddleware) => { return routeHandler(router, context, next); } } export function routeHandler<T extends Context>( router: Router<T>, context: T, next: NextMiddleware) { let routeContext = context.getRouteContext(); let method = context.getHttpMethod(); let path = routeContext.getPendingRoutePath(); let match = router.match(path, method); let middlewares = (router as any)._middlewares; if (match) { routeContext.push(match); let isMatchReset = false; let resetIfNeeded = (passthrough?: any) => { if (!isMatchReset) { if (routeContext.getCurrentMatch() === match) routeContext.pop(); isMatchReset = true; } return passthrough; }; let nextHandler = () => { resetIfNeeded(); return next(); }; let errorHandler = (err: Error) => { 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 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 dispatch(context, next, middlewares); return next(); } export class HttpMethod { static Get = "GET"; static Head = "HEAD"; static Post = "POST"; static Put = "PUT"; static Delete = "DELETE"; static Patch = "PATCH"; static Options = "OPTIONS"; static Trace = "TRACE"; static ActionMethods = [ HttpMethod.Get, HttpMethod.Post, HttpMethod.Put, HttpMethod.Delete, HttpMethod.Patch]; static Wildcard = "*"; } export function createRouteParams(match: RegExpMatchArray, keys: pathToRegexp.Key[], params?: any) { 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; } export function decodeRouteParam(param: string) { try { return decodeURIComponent(param); } catch (_) { throw createError(400, 'failed to decode param "' + param + '"'); } }