import express, {IRouter, RequestHandler, Router} from "express"; import {PathParams} from "express-serve-static-core"; import config from "config"; import Logger from "./Logger"; import Validator, {FileError, ValidationBag} from "./db/Validator"; import FileUploadMiddleware from "./FileUploadMiddleware"; import * as querystring from "querystring"; import {ParsedUrlQueryInput} from "querystring"; export default abstract class Controller { private static readonly routes: { [p: string]: string } = {}; public static route(route: string, params: RouteParams = [], query: ParsedUrlQueryInput = {}, absolute: boolean = false): string { let path = this.routes[route]; if (path === undefined) throw new Error(`Unknown route for name ${route}.`); if (typeof params === 'string' || typeof params === 'number') { path = path.replace(/:[a-zA-Z_-]+\??/g, '' + params); } else if (Array.isArray(params)) { let i = 0; for (const match of path.matchAll(/:[a-zA-Z_-]+(\(.*\))?\??/g)) { if (match.length > 0) { path = path.replace(match[0], typeof params[i] !== 'undefined' ? params[i] : ''); } i++; } path = path.replace(/\/+/g, '/'); } else { for (const key in params) { if (params.hasOwnProperty(key)) { path = path.replace(new RegExp(`:${key}\\??`), params[key]); } } } const queryStr = querystring.stringify(query); return `${absolute ? config.get('base_url') : ''}${path}` + (queryStr.length > 0 ? '?' + queryStr : ''); } private readonly router: Router = express.Router(); private readonly fileUploadFormRouter: Router = express.Router(); public getGlobalHandlers(): RequestHandler[] { return []; } public hasGlobalHandlers(): boolean { return this.getGlobalHandlers().length > 0; } public setupGlobalHandlers(router: Router): void { for (const globalHandler of this.getGlobalHandlers()) { router.use(this.wrap(globalHandler)); } } public getRoutesPrefix(): string { return '/'; } public abstract routes(): void; public setupRoutes(): { mainRouter: Router, fileUploadFormRouter: Router } { this.routes(); return { mainRouter: this.router, fileUploadFormRouter: this.fileUploadFormRouter, }; } protected use(handler: RequestHandler) { this.router.use(handler); } protected get(path: PathParams, handler: RequestHandler, routeName?: string, ...middlewares: (RequestHandler | FileUploadMiddleware)[]) { this.handle('get', path, handler, routeName, ...middlewares); } protected post(path: PathParams, handler: RequestHandler, routeName?: string, ...middlewares: (RequestHandler | FileUploadMiddleware)[]) { this.handle('post', path, handler, routeName, ...middlewares); } protected put(path: PathParams, handler: RequestHandler, routeName?: string, ...middlewares: (RequestHandler | FileUploadMiddleware)[]) { this.handle('put', path, handler, routeName, ...middlewares); } protected delete(path: PathParams, handler: RequestHandler, routeName?: string, ...middlewares: (RequestHandler | FileUploadMiddleware)[]) { this.handle('delete', path, handler, routeName, ...middlewares); } private handle( action: Exclude, path: PathParams, handler: RequestHandler, routeName?: string, ...middlewares: (RequestHandler | FileUploadMiddleware)[] ): void { this.registerRoutes(path, handler, routeName); for (const middleware of middlewares) { if (middleware instanceof FileUploadMiddleware) { this.fileUploadFormRouter[action](path, this.wrap(FILE_UPLOAD_MIDDLEWARE(middleware))); } else { this.router[action](path, this.wrap(middleware)); } } this.router[action](path, this.wrap(handler)); } private wrap(handler: RequestHandler): RequestHandler { return async (req, res, next) => { try { await handler.call(this, req, res, next); } catch (e) { next(e); } }; } private registerRoutes(path: PathParams, handler: RequestHandler, routeName?: string) { if (typeof routeName !== 'string') { routeName = handler.name .replace(/(?:^|\.?)([A-Z])/g, (x, y) => '_' + y.toLowerCase()) .replace(/(^_|get_|post_)/g, ''); } if (routeName.length === 0) return; let routePath = null; if (path instanceof Array && path.length > 0) { path = path[0]; } if (typeof path === 'string') { const prefix = this.getRoutesPrefix(); routePath = (prefix !== '/' ? prefix : '') + path; } if (!Controller.routes[routeName]) { if (typeof routePath === 'string') { Logger.info(`Route ${routeName} has path ${routePath}`); Controller.routes[routeName] = routePath; } else { Logger.warn(`Cannot assign path to route ${routeName}.`); } } } protected async validate(validationMap: { [p: string]: Validator }, body: any): Promise { const bag = new ValidationBag(); for (const p in validationMap) { if (validationMap.hasOwnProperty(p)) { try { await validationMap[p].execute(p, body[p], false); } catch (e) { if (e instanceof ValidationBag) { bag.addBag(e); } else throw e; } } } if (bag.hasMessages()) throw bag; } } export type RouteParams = { [p: string]: string } | string[] | string | number; const FILE_UPLOAD_MIDDLEWARE: (fileUploadMiddleware: FileUploadMiddleware) => RequestHandler = (fileUploadMiddleware: FileUploadMiddleware) => { return async (req, res, next) => { const form = fileUploadMiddleware.formFactory(); try { await new Promise((resolve, reject) => { form.parse(req, (err, fields, files) => { if (err) { reject(err); return; } req.body = fields; req.files = files; resolve(); }); }); } catch (e) { const bag = new ValidationBag(); const fileError = new FileError(e); fileError.thingName = fileUploadMiddleware.defaultField; bag.addMessage(fileError); next(bag); return; } next(); }; };