swaf/src/Controller.ts

197 lines
6.9 KiB
TypeScript

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<string>('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<keyof IRouter, 'stack' | 'param' | 'route' | 'use'>,
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<any> }, body: any): Promise<void> {
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<any>((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();
};
};