UNPKG

routing-controllers

Version:

Create structured, declarative and beautifully organized class-based controllers with heavy decorators usage for Express / Koa using TypeScript.

437 lines 17.6 kB
import { ActionMetadata } from '../../metadata/ActionMetadata'; import { BaseDriver } from '../BaseDriver'; import { AccessDeniedError } from '../../error/AccessDeniedError'; import { AuthorizationCheckerNotDefinedError } from '../../error/AuthorizationCheckerNotDefinedError'; import { isPromiseLike } from '../../util/isPromiseLike'; import { getFromContainer } from '../../container'; import { AuthorizationRequiredError } from '../../error/AuthorizationRequiredError'; import { NotFoundError } from '../../index'; // eslint-disable-next-line @typescript-eslint/no-var-requires const cookie = require('cookie'); // eslint-disable-next-line @typescript-eslint/no-var-requires const templateUrl = require('template-url'); /** * Integration with express framework. */ export class ExpressDriver extends BaseDriver { // ------------------------------------------------------------------------- // Constructor // ------------------------------------------------------------------------- constructor(express) { super(); this.express = express; this.loadExpress(); this.app = this.express; } // ------------------------------------------------------------------------- // Public Methods // ------------------------------------------------------------------------- /** * Initializes the things driver needs before routes and middlewares registration. */ initialize() { if (this.cors) { // eslint-disable-next-line @typescript-eslint/no-var-requires const cors = require('cors'); if (this.cors === true) { this.express.use(cors()); } else { this.express.use(cors(this.cors)); } } } /** * Registers middleware that run before controller actions. */ registerMiddleware(middleware, options) { let middlewareWrapper; // if its an error handler then register it with proper signature in express if (middleware.instance.error) { middlewareWrapper = (error, request, response, next) => { middleware.instance.error(error, request, response, next); }; } // if its a regular middleware then register it as express middleware else if (middleware.instance.use) { middlewareWrapper = (request, response, next) => { try { const useResult = middleware.instance.use(request, response, next); if (isPromiseLike(useResult)) { useResult.catch((error) => { this.handleError(error, undefined, { request, response, next }); return error; }); } } catch (error) { this.handleError(error, undefined, { request, response, next }); } }; } if (middlewareWrapper) { // Name the function for better debugging Object.defineProperty(middlewareWrapper, 'name', { value: middleware.instance.constructor.name, writable: true, }); this.express.use(options.routePrefix || '/', middlewareWrapper); } } /** * Registers action in the driver. */ registerAction(actionMetadata, executeCallback) { // middlewares required for this action const defaultMiddlewares = []; if (actionMetadata.isBodyUsed) { if (actionMetadata.isJsonTyped) { defaultMiddlewares.push(this.loadBodyParser().json(actionMetadata.bodyExtraOptions)); } else { defaultMiddlewares.push(this.loadBodyParser().text(actionMetadata.bodyExtraOptions)); } } if (actionMetadata.isAuthorizedUsed) { defaultMiddlewares.push((request, response, next) => { if (!this.authorizationChecker) throw new AuthorizationCheckerNotDefinedError(); const action = { request, response, next }; try { const checkResult = this.authorizationChecker(action, actionMetadata.authorizedRoles); const handleError = (result) => { if (!result) { const error = actionMetadata.authorizedRoles.length === 0 ? new AuthorizationRequiredError(action) : new AccessDeniedError(action); this.handleError(error, actionMetadata, action); } else { next(); } }; if (isPromiseLike(checkResult)) { checkResult .then(result => handleError(result)) .catch(error => this.handleError(error, actionMetadata, action)); } else { handleError(checkResult); } } catch (error) { this.handleError(error, actionMetadata, action); } }); } if (actionMetadata.isFileUsed || actionMetadata.isFilesUsed) { const multer = this.loadMulter(); actionMetadata.params .filter(param => param.type === 'file') .forEach(param => { defaultMiddlewares.push(multer(param.extraOptions).single(param.name)); }); actionMetadata.params .filter(param => param.type === 'files') .forEach(param => { defaultMiddlewares.push(multer(param.extraOptions).array(param.name)); }); } // user used middlewares const uses = [...actionMetadata.controllerMetadata.uses, ...actionMetadata.uses]; const beforeMiddlewares = this.prepareMiddlewares(uses.filter(use => !use.afterAction)); const afterMiddlewares = this.prepareMiddlewares(uses.filter(use => use.afterAction)); // prepare route and route handler function const route = ActionMetadata.appendBaseRoute(this.routePrefix, actionMetadata.fullRoute); const routeHandler = function routeHandler(request, response, next) { return executeCallback({ request, response, next }); }; // This ensures that a request is only processed once to prevent unhandled rejections saying // "Can't set headers after they are sent" // Some examples of reasons a request may cause multiple route calls: // * Express calls the "get" route automatically when we call the "head" route: // Reference: https://expressjs.com/en/4x/api.html#router.METHOD // This causes a double execution on our side. // * Multiple routes match the request (e.g. GET /users/me matches both @All(/users/me) and @Get(/users/:id)). // The following middleware only starts an action processing if the request has not been processed before. const routeGuard = function routeGuard(request, response, next) { if (!request.routingControllersStarted) { request.routingControllersStarted = true; return next(); } }; // finally register action in express this.express[actionMetadata.type.toLowerCase()](...[route, routeGuard, ...beforeMiddlewares, ...defaultMiddlewares, routeHandler, ...afterMiddlewares]); } /** * Registers all routes in the framework. */ // eslint-disable-next-line @typescript-eslint/no-empty-function registerRoutes() { } /** * Gets param from the request. */ getParamFromRequest(action, param) { const request = action.request; switch (param.type) { case 'body': return request.body; case 'body-param': return request.body[param.name]; case 'param': return request.params[param.name]; case 'params': return request.params; case 'session-param': return request.session[param.name]; case 'session': return request.session; case 'state': throw new Error('@State decorators are not supported by express driver.'); case 'query': return request.query[param.name]; case 'queries': return request.query; case 'header': return request.headers[param.name.toLowerCase()]; case 'headers': return request.headers; case 'file': return request.file; case 'files': return request.files; case 'cookie': if (!request.headers.cookie) return; const cookies = cookie.parse(request.headers.cookie); return cookies[param.name]; case 'cookies': if (!request.headers.cookie) return {}; return cookie.parse(request.headers.cookie); } } /** * Handles result of successfully executed controller action. */ handleSuccess(result, action, options) { // if the action returned the response object itself, short-circuits if (result && result === options.response) { options.next(); return; } // transform result if needed result = this.transformResult(result, action, options); // set http status code if (result === undefined && action.undefinedResultCode) { if (action.undefinedResultCode instanceof Function) { throw new action.undefinedResultCode(options); } options.response.status(action.undefinedResultCode); } else if (result === null) { if (action.nullResultCode) { if (action.nullResultCode instanceof Function) { throw new action.nullResultCode(options); } options.response.status(action.nullResultCode); } else { options.response.status(204); } } else if (action.successHttpCode) { options.response.status(action.successHttpCode); } // apply http headers Object.keys(action.headers).forEach(name => { options.response.header(name, action.headers[name]); }); if (action.redirect) { // if redirect is set then do it if (typeof result === 'string') { options.response.redirect(result); } else if (result instanceof Object) { options.response.redirect(templateUrl(action.redirect, result)); } else { options.response.redirect(action.redirect); } options.next(); } else if (action.renderedTemplate) { // if template is set then render it const renderOptions = result && result instanceof Object ? result : {}; options.response.render(action.renderedTemplate, renderOptions, (err, html) => { if (err && action.isJsonTyped) { return options.next(err); } else if (err && !action.isJsonTyped) { return options.next(err); } else if (html) { options.response.send(html); } options.next(); }); } else if (result === undefined) { // throw NotFoundError on undefined response if (action.undefinedResultCode) { if (action.isJsonTyped) { options.response.json(); } else { options.response.send(); } options.next(); } else { throw new NotFoundError(); } } else if (result === null) { // send null response if (action.isJsonTyped) { options.response.json(null); } else { options.response.send(null); } options.next(); } else if (result instanceof Buffer) { // check if it's binary data (Buffer) options.response.end(result, 'binary'); } else if (result instanceof Uint8Array) { // check if it's binary data (typed array) options.response.end(Buffer.from(result), 'binary'); } else if (result.pipe instanceof Function) { result.pipe(options.response); } else { // send regular result if (action.isJsonTyped) { options.response.json(result); } else { options.response.send(result); } options.next(); } } /** * Handles result of failed executed controller action. */ handleError(error, action, options) { if (this.isDefaultErrorHandlingEnabled) { const response = options.response; // set http code // note that we can't use error instanceof HttpError properly anymore because of new typescript emit process if (error.httpCode) { response.status(error.httpCode); } else { response.status(500); } // apply http headers if (action) { Object.keys(action.headers).forEach(name => { response.header(name, action.headers[name]); }); } // send error content if (action && action.isJsonTyped) { response.json(this.processJsonError(error)); } else { response.send(this.processTextError(error)); // todo: no need to do it because express by default does it } } options.next(error); } // ------------------------------------------------------------------------- // Protected Methods // ------------------------------------------------------------------------- /** * Creates middlewares from the given "use"-s. */ prepareMiddlewares(uses) { const middlewareFunctions = []; uses.forEach((use) => { if (use.middleware.prototype && use.middleware.prototype.use) { // if this is function instance of MiddlewareInterface middlewareFunctions.push((request, response, next) => { try { const useResult = getFromContainer(use.middleware).use(request, response, next); if (isPromiseLike(useResult)) { useResult.catch((error) => { this.handleError(error, undefined, { request, response, next }); return error; }); } return useResult; } catch (error) { this.handleError(error, undefined, { request, response, next }); } }); } else if (use.middleware.prototype && use.middleware.prototype.error) { // if this is function instance of ErrorMiddlewareInterface middlewareFunctions.push(function (error, request, response, next) { return getFromContainer(use.middleware).error(error, request, response, next); }); } else { middlewareFunctions.push(use.middleware); } }); return middlewareFunctions; } /** * Dynamically loads express module. */ loadExpress() { if (require) { if (!this.express) { try { // eslint-disable-next-line @typescript-eslint/no-var-requires this.express = require('express')(); } catch (e) { throw new Error('express package was not found installed. Try to install it: npm install express --save'); } } } else { throw new Error('Cannot load express. Try to install all required dependencies.'); } } /** * Dynamically loads body-parser module. */ loadBodyParser() { try { return require('body-parser'); } catch (e) { throw new Error('body-parser package was not found installed. Try to install it: npm install body-parser --save'); } } /** * Dynamically loads multer module. */ loadMulter() { try { return require('multer'); } catch (e) { throw new Error('multer package was not found installed. Try to install it: npm install multer --save'); } } } //# sourceMappingURL=ExpressDriver.js.map