import express, {IRouter, RequestHandler, Router} from "express"; import {PathParams} from "express-serve-static-core"; import config from "config"; import {logger} from "./Logger"; import FileUploadMiddleware from "./FileUploadMiddleware"; import * as querystring from "querystring"; import {ParsedUrlQueryInput} from "querystring"; import Middleware, {MiddlewareType} from "./Middleware"; import Application from "./Application"; export default abstract class Controller { private static readonly routes: { [p: string]: string | undefined } = {}; 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 of Object.keys(params)) { path = path.replace(new RegExp(`:${key}\\??`), params[key]); } } const queryStr = querystring.stringify(query); return `${absolute ? config.get('public_url') : ''}${path}` + (queryStr.length > 0 ? '?' + queryStr : ''); } private readonly router: Router = express.Router(); private readonly fileUploadFormRouter: Router = express.Router(); private app?: Application; public getGlobalMiddlewares(): Middleware[] { return []; } public hasGlobalMiddlewares(): boolean { return this.getGlobalMiddlewares().length > 0; } public setupGlobalHandlers(router: Router): void { for (const middleware of this.getGlobalMiddlewares()) { router.use(this.wrap(middleware.getRequestHandler())); } } 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): void { this.router.use(this.wrap(handler)); logger.info('Installed anonymous middleware on ' + this.getRoutesPrefix()); } protected useMiddleware(...middlewares: MiddlewareType[]): void { for (const middleware of middlewares) { const instance = new middleware(this.getApp()); if (instance instanceof FileUploadMiddleware) { this.fileUploadFormRouter.use(this.wrap(instance.getRequestHandler())); } else { this.router.use(this.wrap(instance.getRequestHandler())); } logger.info('Installed ' + middleware.name + ' on ' + this.getRoutesPrefix()); } } protected get( path: PathParams, handler: RequestHandler, routeName?: string, ...middlewares: (MiddlewareType)[] ): void { this.handle('get', path, handler, routeName, ...middlewares); } protected post( path: PathParams, handler: RequestHandler, routeName?: string, ...middlewares: (MiddlewareType)[] ): void { this.handle('post', path, handler, routeName, ...middlewares); } protected put( path: PathParams, handler: RequestHandler, routeName?: string, ...middlewares: (MiddlewareType)[] ): void { this.handle('put', path, handler, routeName, ...middlewares); } protected delete( path: PathParams, handler: RequestHandler, routeName?: string, ...middlewares: (MiddlewareType)[] ): void { this.handle('delete', path, handler, routeName, ...middlewares); } private handle( action: Exclude, path: PathParams, handler: RequestHandler, routeName?: string, ...middlewares: (MiddlewareType)[] ): void { this.registerRoutes(path, handler, routeName); for (const middleware of middlewares) { const instance = new middleware(this.getApp()); if (instance instanceof FileUploadMiddleware) { this.fileUploadFormRouter[action](path, this.wrap(instance.getRequestHandler())); } else { this.router[action](path, this.wrap(instance.getRequestHandler())); } } 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 getApp(): Application { if (!this.app) throw new Error('Application not initialized.'); return this.app; } public setApp(app: Application): void { this.app = app; } } export type RouteParams = { [p: string]: string } | string[] | string | number;