UNPKG

inversify-express-utils

Version:

Some utilities for the development of express applications with Inversify

331 lines 14.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.InversifyExpressServer = void 0; require("reflect-metadata"); const express_1 = __importStar(require("express")); const base_middleware_1 = require("./base_middleware"); const constants_1 = require("./constants"); const httpResponseMessage_1 = require("./httpResponseMessage"); const utils_1 = require("./utils"); class InversifyExpressServer { _router; _container; _app; _configFn; _errorConfigFn; _routingConfig; _authProvider; _forceControllers; /** * Wrapper for the express server. * * @param container Container loaded with all controllers and their dependencies. * @param customRouter optional express.Router custom router * @param routingConfig optional interfaces.RoutingConfig routing config * @param customApp optional express.Application custom app * @param authProvider optional interfaces.AuthProvider auth provider * @param forceControllers optional boolean setting to force controllers (defaults do true) */ constructor(container, customRouter, routingConfig, customApp, authProvider, forceControllers = true) { this._container = container; this._forceControllers = forceControllers; this._router = customRouter || (0, express_1.Router)(); this._routingConfig = routingConfig || { rootPath: constants_1.DEFAULT_ROUTING_ROOT_PATH, }; this._app = customApp || (0, express_1.default)(); if (authProvider) { this._authProvider = authProvider; container.bind(constants_1.TYPE.AuthProvider).to(this._authProvider); } } /** * Sets the configuration function to be applied to the application. * Note that the config function is not actually executed until a call to * InversifyExpresServer.build(). * * This method is chainable. * * @param fn Function in which app-level middleware can be registered. */ setConfig(fn) { this._configFn = fn; return this; } /** * Sets the error handler configuration function to be applied to the application. * Note that the error config function is not actually executed until a call to * InversifyExpresServer.build(). * * This method is chainable. * * @param fn Function in which app-level error handlers can be registered. */ setErrorConfig(fn) { this._errorConfigFn = fn; return this; } /** * Applies all routes and configuration to the server, returning the express application. */ build() { // The very first middleware to be invoked // it creates a new httpContext and attaches it to the // current request as metadata using Reflect this._app.all('*', (req, res, next) => { void (async () => { const httpContext = await this._createHttpContext(req, res, next); Reflect.defineMetadata(constants_1.METADATA_KEY.httpContext, httpContext, req); next(); })(); }); // register server-level middleware before anything else // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/strict-boolean-expressions if (this._configFn) { this._configFn.apply(undefined, [this._app]); } this.registerControllers(); // register error handlers after controllers // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/strict-boolean-expressions if (this._errorConfigFn) { this._errorConfigFn.apply(undefined, [this._app]); } return this._app; } registerControllers() { // Fake HttpContext is needed during registration this._container .bind(constants_1.TYPE.HttpContext) .toConstantValue({}); const constructors = (0, utils_1.getControllersFromMetadata)(); constructors.forEach((constructor) => { const { name } = constructor; if (this._container.isBoundNamed(constants_1.TYPE.Controller, name)) { throw new Error((0, constants_1.DUPLICATED_CONTROLLER_NAME)(name)); } this._container .bind(constants_1.TYPE.Controller) .to(constructor) .whenTargetNamed(name); }); const controllers = (0, utils_1.getControllersFromContainer)(this._container, this._forceControllers); controllers.forEach((controller) => { const controllerMetadata = (0, utils_1.getControllerMetadata)(controller.constructor); const methodMetadata = (0, utils_1.getControllerMethodMetadata)(controller.constructor); const parameterMetadata = (0, utils_1.getControllerParameterMetadata)(controller.constructor); // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/no-unnecessary-condition if (controllerMetadata && methodMetadata) { const controllerMiddleware = this.resolveMiddlewere(...controllerMetadata.middleware); // Priorirties for HTTP methods. Lower value means higher priority. Default is 0. const methodToPriorityMap = { [constants_1.HTTP_VERBS_ENUM.head]: -1, }; const sortedMethodMetadata = methodMetadata.sort((a, b) => { const aPriority = methodToPriorityMap[a.method] ?? 0; const bPriority = methodToPriorityMap[b.method] ?? 0; return aPriority - bPriority; }); sortedMethodMetadata.forEach((metadata) => { let paramList = []; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/strict-boolean-expressions if (parameterMetadata) { paramList = parameterMetadata[metadata.key] || []; } const handler = this.handlerFactory(controllerMetadata.target.name, metadata.key, paramList); const routeMiddleware = this.resolveMiddlewere(...metadata.middleware); const path = this.mergePaths(controllerMetadata.path, metadata.path); this._router[metadata.method](path, ...controllerMiddleware, ...routeMiddleware, handler); }); } }); this._app.use(this._routingConfig.rootPath, this._router); } mergePaths(...paths) { return paths .map((path) => { let finalPath = path.startsWith('/') || path.startsWith('.') ? path : `/${path}`; if (path.endsWith('/')) { finalPath = finalPath.substring(0, finalPath.length - 1); } return finalPath; }) .join(''); } resolveMiddlewere(...middleware) { return middleware.map((middlewareItem) => { if (!this._container.isBound(middlewareItem)) { return middlewareItem; } const middlewareInstance = this._container.get(middlewareItem); if (middlewareInstance instanceof base_middleware_1.BaseMiddleware) { return (req, res, next) => { const mReq = this._container.get(middlewareItem); mReq.httpContext = this._getHttpContext(req); return mReq.handler(req, res, next); }; } return middlewareInstance; }); } copyHeadersTo(headers, target) { for (const name of Object.keys(headers)) { const headerValue = headers[name]; target.append(name, typeof headerValue === 'number' ? headerValue.toString() : headerValue); } } async handleHttpResponseMessage(message, res) { this.copyHeadersTo(message.headers, res); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (message.content !== undefined) { this.copyHeadersTo(message.content.headers, res); res .status(message.statusCode) // If the content is a number, ensure we change it to a string, else our content is // treated as a statusCode rather than as the content of the Response .send(await message.content.readAsync()); } else { res.sendStatus(message.statusCode); } } handlerFactory(controllerName, key, parameterMetadata) { return (async (req, res, next) => { try { const args = this.extractParameters(req, res, next, parameterMetadata); const httpContext = this._getHttpContext(req); httpContext.container .bind(constants_1.TYPE.HttpContext) .toConstantValue(httpContext); // invoke controller's action const value = await httpContext.container.getNamed(constants_1.TYPE.Controller, controllerName)[key](...args); if (value instanceof httpResponseMessage_1.HttpResponseMessage) { await this.handleHttpResponseMessage(value, res); } else if ((0, utils_1.instanceOfIhttpActionResult)(value)) { const httpResponseMessage = await value.executeAsync(); await this.handleHttpResponseMessage(httpResponseMessage, res); } else if (value instanceof Function) { // eslint-disable-next-line @typescript-eslint/no-unsafe-call value(); } else if (!res.headersSent) { if (value !== undefined) { res.send(value); } } } catch (err) { next(err); } }); } _getHttpContext(req) { return Reflect.getMetadata(constants_1.METADATA_KEY.httpContext, req); } async _createHttpContext(req, res, next) { const principal = await this._getCurrentUser(req, res, next); return { // We use a childContainer for each request so we can be // sure that the binding is unique for each HTTP request container: this._container.createChild(), request: req, response: res, user: principal, }; } async _getCurrentUser(req, res, next) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (this._authProvider !== undefined) { const authProvider = this._container.get(constants_1.TYPE.AuthProvider); return authProvider.getUser(req, res, next); } return Promise.resolve({ details: null, isAuthenticated: async () => false, isInRole: async (_role) => false, isResourceOwner: async (_resourceId) => false, }); } extractParameters(req, res, next, params) { const args = []; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/strict-boolean-expressions if (!params || !params.length) { return [req, res, next]; } params.forEach(({ type, index, parameterName, injectRoot }) => { switch (type) { case constants_1.PARAMETER_TYPE.REQUEST: args[index] = req; break; case constants_1.PARAMETER_TYPE.NEXT: args[index] = next; break; case constants_1.PARAMETER_TYPE.PARAMS: args[index] = this.getParam(req, 'params', injectRoot, parameterName); break; case constants_1.PARAMETER_TYPE.QUERY: args[index] = this.getParam(req, 'query', injectRoot, parameterName); break; case constants_1.PARAMETER_TYPE.BODY: args[index] = req.body; break; case constants_1.PARAMETER_TYPE.HEADERS: args[index] = this.getParam(req, 'headers', injectRoot, parameterName); break; case constants_1.PARAMETER_TYPE.COOKIES: args[index] = this.getParam(req, 'cookies', injectRoot, parameterName); break; case constants_1.PARAMETER_TYPE.PRINCIPAL: args[index] = this._getPrincipal(req); break; default: args[index] = res; break; // response } }); args.push(req, res, next); return args; } getParam(source, paramType, injectRoot, name) { const key = paramType === 'headers' ? typeof name === 'symbol' ? name.toString() : name?.toLowerCase() : name; const param = source[paramType]; if (injectRoot) { return param; } // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/no-unnecessary-condition return param && key ? param[key] : undefined; } _getPrincipal(req) { return this._getHttpContext(req).user; } } exports.InversifyExpressServer = InversifyExpressServer; //# sourceMappingURL=server.js.map