http-micro
Version:
Micro-framework on top of node's http module
302 lines (301 loc) • 10.1 kB
JavaScript
"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;