express-decorated-router
Version:
Define Express routes using TypeScript decorators
268 lines (267 loc) • 10.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const debug = require("debug");
const express_1 = require("express");
const forEach = require("lodash/forEach");
const isEmpty = require("lodash/isEmpty");
const ParentControllerError_1 = require("./errors/ParentControllerError");
const UnregisteredControllerError_1 = require("./errors/UnregisteredControllerError");
/** key = controller class */
const routeMap = new Map();
/** key = controller class */
const controllerMap = new Map();
/** key = controller class */
const controllerMiddlewareMap = new Map();
/** key = class method */
const routeMiddlewareMap = new Map();
/** key = controller class */
const routerMap = new Map();
/** key = child, value = parent */
const parentMap = new Map();
/** Debug logger */
const log = debug('express-decorated-router');
/** Public interface for the express-decorated-router library */
class ExpressDecoratedRouter {
/**
* Register a {@link Controller @Controller} decoration
* @internal
* @hidden
* @param clazz Controller class
* @param root Controller root path
* @param opts Options passed to {@link Router express.Router()}
*/
static addController(clazz, root, opts) {
controllerMap.set(clazz, { root, opts });
log('Decorating class %s as a controller with root %s and options %o', clazz.name, root, opts);
}
/**
* Register a {@link ControllerMiddleware @ControllerMiddleware} decoration
* @internal
* @hidden
* @param clazz Controller class
* @param middleware Middleware handlers
*/
static addControllerMiddleware(clazz, middleware) {
controllerMiddlewareMap.set(clazz, middleware);
log('Adding %d middleware functions to controller %s', middleware.length, clazz.name);
}
/**
* Register a {@link Parent @Parent} decoration
* @internal
* @hidden
* @param child Child controller
* @param parent Parent controller
*/
static addParent(child, parent) {
log('Setting %s as the parent controller of %s', parent.name, child.name);
parentMap.set(child, parent);
}
/**
* Register a route decoration
* @internal
* @hidden
* @param clazz Controller class
* @param httpMethod The HTTP method
* @param path The URL path
* @param handler The request handler
*/
static addRoute(clazz, httpMethod, path, handler) {
log('Adding %s %s route to controller %s', httpMethod.toUpperCase(), path, clazz.name);
let routeSpec = routeMap.get(clazz);
/* istanbul ignore else */
if (!routeSpec) {
log('Route spec object does not exist - creating');
routeSpec = {};
routeMap.set(clazz, routeSpec);
}
else {
log('Route spec object already exists');
}
let httpMethodSpec = routeSpec[httpMethod];
/* istanbul ignore else */
if (!httpMethodSpec) {
log('Http spec map does not exist - creating');
httpMethodSpec = new Map();
routeSpec[httpMethod] = httpMethodSpec;
}
else {
log('Http spec map already exists');
}
httpMethodSpec.set(path, handler);
}
/**
* Register a {@link RouteMiddleware @RouteMiddleware} decoration
* @internal
* @hidden
* @param route The decorated method
* @param middleware The middleware functions
*/
static addRouteMiddleware(route, middleware) {
routeMiddlewareMap.set(route, middleware);
log('Adding %s middleware functions to handler %s', middleware.length, route.name);
}
/**
* Apply routes to the Express application. You should call reset() after calling this.
* @param app The Express application
* @throws {ParentControllerError} If the input of a @Parent decoration has not been decorated with @Controller
* @throws {UnregisteredControllerError} If a class decorated with @Parent was not annotated with @Controller
*/
static applyRoutes(app) {
log('Applying routes to Express app');
for (const controllerMapEntry of controllerMap.entries()) {
ExpressDecoratedRouter.processController(app, controllerMapEntry[0], controllerMapEntry[1]);
}
for (const parentMapEntry of parentMap.entries()) {
ExpressDecoratedRouter.processParents(parentMapEntry[0], parentMapEntry[1]);
}
return ExpressDecoratedRouter;
}
/**
* Reset the library, freeing resources. You should call this method after calling applyRoutes()
*/
static reset() {
log('Resetting route map');
routeMap.clear();
log('Resetting controller map');
controllerMap.clear();
log('Resetting controller middleware map');
controllerMiddlewareMap.clear();
log('Resetting route middleware map');
routeMiddlewareMap.clear();
log('Resetting router map');
routerMap.clear();
log('Resetting parent map');
parentMap.clear();
return ExpressDecoratedRouter;
}
/**
* Process a Controller decoration
* @internal
* @hidden
* @param app The Express app
* @param controller The controller class
* @param controllerSpec The controller specification
*/
static processController(app, controller, controllerSpec) {
log('Resolved controller as %s, controller spec as %o', controller.name, controllerSpec);
if (!routeMap.has(controller)) {
log('Controller %s has no routes - skipping', controller.name);
return;
}
const routeSpec = routeMap.get(controller);
if (isEmpty(routeSpec)) {
log('Controller %s has an empty route spec - skipping', controller.name);
return;
}
const router = express_1.Router(controllerSpec.opts);
ExpressDecoratedRouter.processControllerMiddleware(router, controller);
log('Parsing route specs for controller %s', controller.name);
forEach(routeSpec, (httpMethodSpec, httpMethod) => {
ExpressDecoratedRouter.processRouteSpec(router, controller, httpMethod, httpMethodSpec);
});
log('Adding controller %s with root %s and options %o to app', controller.name, controllerSpec.root, controllerSpec.opts);
routerMap.set(controller, router);
if (!parentMap.has(controller)) {
app.use(controllerSpec.root, router);
}
}
/**
* Process a ControllerMiddleware decoration
* @internal
* @hidden
* @param router The Express router this will get applied to
* @param controller The controller class
*/
static processControllerMiddleware(router, controller) {
if (controllerMiddlewareMap.has(controller)) {
const controllerMiddleware = controllerMiddlewareMap.get(controller);
log('Controller %s has %d middleware functions assigned', controller.name, controllerMiddleware.length);
router.use(controllerMiddleware);
}
else {
log('Controller %s has no middleware functions assigned', controller.name);
}
}
/**
* Process a HTTP method spec
* @internal
* @hidden
* @param router The Express router this will get applied to
* @param pathParams The URL
* @param requestHandler The request handler
* @param httpMethod The HTTP method used
*/
static processHttpMethodSpec(router, pathParams, requestHandler, httpMethod) {
log('Method %s resolved to path %s', requestHandler.name, pathParams);
ExpressDecoratedRouter.processRouteMiddleware(router, pathParams, routeMiddlewareMap.get(requestHandler));
router[httpMethod](pathParams, requestHandler);
}
/**
* Process a Parent decoration
* @internal
* @hidden
* @param child Child controller
* @param parent Parent controller
* @throws {ParentControllerError} If the parent controller has not been registered
* @throws {UnregisteredControllerError} If the child controller hasn't been registered
*/
static processParents(child, parent) {
log('Processing parent %s of child %s', parent.name, child.name);
const parentRouter = routerMap.get(parent);
if (parentRouter) {
log('Parent router found');
const childRouter = routerMap.get(child);
if (childRouter) {
log('Child router found');
const childSpec = controllerMap.get(child);
const parentMiddleware = controllerMiddlewareMap.get(parent);
if (parentMiddleware && parentMiddleware.length) {
log('Parent router %s has %d middleware applied. Transferring to %s', parent.name, parentMiddleware.length, child.name);
childRouter.use(parentMiddleware);
}
parentRouter.use(childSpec.root, childRouter);
}
else {
throw new UnregisteredControllerError_1.UnregisteredControllerError(child);
}
}
else {
throw new ParentControllerError_1.ParentControllerError(child, parent);
}
}
/**
* Process a @RouteMiddleware decoration
* @internal
* @hidden
* @param router The Express router this will get applied to
* @param pathParams The URL
* @param routeMiddleware The middleware to apply
*/
static processRouteMiddleware(router, pathParams, routeMiddleware) {
if (routeMiddleware && routeMiddleware.length) {
log('And has %d middleware functions', routeMiddleware.length);
router.use(pathParams, routeMiddleware);
}
else {
log('And has no middleware functions');
}
}
/**
* Process a route specification
* @internal
* @hidden
* @param router The Express router this will get applied to
* @param controller The controller class
* @param httpMethod The HTTP method used
* @param httpMethodSpec The HTTP method specification
*/
static processRouteSpec(router, controller, httpMethod, httpMethodSpec) {
log('Parsing %s routes for controller %s', httpMethod.toUpperCase(), controller.name);
for (const httpMethodSpecEntry of httpMethodSpec.entries()) {
const pathParams = httpMethodSpecEntry[0];
const requestHandler = httpMethodSpecEntry[1];
ExpressDecoratedRouter.processHttpMethodSpec(router, pathParams, requestHandler, httpMethod);
}
}
}
exports.ExpressDecoratedRouter = ExpressDecoratedRouter;