UNPKG

@mieweb/wikigdrive

Version:

Google Drive to MarkDown synchronization

555 lines (488 loc) 15.8 kB
import process from 'node:process'; import type * as express from 'express'; import winston from 'winston'; import {instrumentAndWrap} from '../../../telemetry.ts'; export const HttpStatus = { OK: 200, CREATED: 201, NO_CONTENT: 204 }; function getMethods(obj) { const res = {}; for(const m of Object.getOwnPropertyNames(obj.constructor.prototype)) { res[m] = obj[m]; } return res; } export interface ControllerRouteParamUser { type: 'user'; parameterIndex: number; docs?: RouteDoc; } export interface ControllerRouteParamBody { type: 'body'; parameterIndex: number; docs?: RouteDoc; } export interface ControllerRouteParamHeaders { type: 'headers'; parameterIndex: number; docs?: RouteDoc; } export interface ControllerRouteParamStream { type: 'stream'; parameterIndex: number; docs?: RouteDoc; } export interface ControllerRouteParamGetAll { type: 'getAll'; parameterIndex: number; queryFields: string[] docs?: RouteDoc; } export interface ControllerRouteParamRelated { type: 'related'; parameterIndex: number; docs?: RouteDoc; } export interface ControllerRouteParamQuery { type: 'query'; parameterIndex: number; name: string; docs?: RouteDoc; } export interface ControllerRouteParamMethod { type: 'method'; parameterIndex: number; docs?: RouteDoc; } export interface ControllerRouteParamPath { type: 'node:path'; parameterIndex: number; name: string; docs?: RouteDoc; } type ControllerRouteParam = ControllerRouteParamGetAll | ControllerRouteParamQuery | ControllerRouteParamBody | ControllerRouteParamHeaders | ControllerRouteParamPath | ControllerRouteParamStream | ControllerRouteParamRelated | ControllerRouteParamUser | ControllerRouteParamMethod; export interface RouteDoc { description?: string; summary?: string; example?: string; } export class RouteFilter<K> implements ControllerCallContext { public readonly req: express.Request; public readonly res: express.Response; public readonly subPath: string; public readonly logger: winston.Logger; async filter(data: K): Promise<K> { return data; } } export class ErrorHandler implements ControllerCallContext { public readonly req: express.Request; public readonly res: express.Response; public readonly subPath: string; public readonly logger: winston.Logger; async catch(err) { throw err; } } export interface ControllerRoute { errorHandlers: ErrorHandler[]; inputFilters: RouteFilter<unknown>[]; outputFilters: RouteFilter<unknown>[]; roles: string[]; method?: string; routePath?: string; methodFunc: string; responseObjectType: string; responseContentType: string; responseStatus: number; params: ControllerRouteParam[]; hidden: boolean; routeDocs?: RouteDoc; responseDocs?: RouteDoc; } export interface ControllerCallContext { subPath: string; req: express.Request; res: express.Response; logger: winston.Logger; } function addSwaggerRoute(mainPath: string, route: ControllerRoute) { // SwaggerDocService.addRoute(mainPath, route); } export class Controller implements ControllerCallContext { private static routes: {[methodFunc: string]: ControllerRoute} = {}; public readonly req: express.Request; public readonly res: express.Response; public readonly logger: winston.Logger; private static counter = 1; constructor(public readonly subPath: string) { } getRoute(classType, methodFunc: string) { if (!classType.controllerId) { classType.controllerId = 'controller_' + Controller.counter; Controller.counter++; } const key = classType.controllerId + '.' + methodFunc; if (!Controller.routes[key]) { Controller.routes[key] = { hidden: false, roles: [], params: [], inputFilters: [], outputFilters: [], errorHandlers: [], methodFunc, responseObjectType: 'object', responseStatus: HttpStatus.OK, responseContentType: 'application/json; charset=utf-8' }; } return Controller.routes[key]; } async getRouter() { const controllerId = this.constructor.prototype.controllerId; const { Router} = await import('express'); const router = Router(); for (const key in Controller.routes) { if (!key.startsWith(controllerId + '.')) { continue; } const route = Controller.routes[key]; if (!route.hidden) { addSwaggerRoute(this.subPath, route); } const handlers = []; if (route.roles.length > 0) { handlers.push((req, res, next) => { if (req.user && route.roles.indexOf(req.user.global_role) > -1) { next(); } else { this.logger.error( 'User does not have any of those roles: ' + JSON.stringify(route.roles) + ', only: ' + req.user.global_role ); // throw Boom.forbidden(req.t('auth.youNeedToBeAuthorized')); } }); } // const data = await inputEntitiesFilter.getItemFilter().clearApiData(body, this.user); // const filteredData = await outputEntitiesFilter.clearApiData(created, this.user); handlers.push(async (req: express.Request, res: express.Response, next: express.NextFunction) => { try { const methods = getMethods(this.constructor.prototype); const bound = this[route.methodFunc].bind({ ...methods, ...this, subPath: this.subPath, req, res, logger: req['logger'] }); res.header('Content-type', route.responseContentType); const args = []; for (const param of route.params) { for (let idx = args.length; args.length <= param.parameterIndex; idx++) { args.push(undefined); } switch (param.type) { case 'user': { args[param.parameterIndex] = req.user; } break; case 'body': { let body = req.body; for (const inputFilter of route.inputFilters) { const boundFilter = inputFilter.filter.bind({ ...this, ...inputFilter, subPath: this.subPath, req, res }); body = await boundFilter(body); } args[param.parameterIndex] = body; } break; case 'headers': { const headers = req.headers; args[param.parameterIndex] = headers; } break; case 'stream': args[param.parameterIndex] = req; break; case 'getAll': // args[param.parameterIndex] = ApiUtils.buildOptions(req, param.queryFields); break; case 'path': args[param.parameterIndex] = req.params[param.name]; break; case 'query': args[param.parameterIndex] = req.query[param.name]; break; case 'method': args[param.parameterIndex] = req.method.toLowerCase(); break; } } let retVal; if (process.env.ZIPKIN_URL) { const spanName = req.originalUrl + '.' + route.methodFunc; await instrumentAndWrap(spanName, req, res, async () => { retVal = await bound(...args); }); } else { retVal = await bound(...args); } if ('stream' === route.responseObjectType) { return; } if ('void' === route.responseObjectType) { res.status(HttpStatus.NO_CONTENT).send(); return; } if ('html' === route.responseObjectType) { res.status(route.responseStatus).send(retVal); return; } res.status(route.responseStatus).json(retVal); } catch (err) { let err1 = err; for (const inputFilter of route.errorHandlers) { try { const boundErrorHandler = inputFilter.catch.bind({ ...this, ...inputFilter, subPath: this.subPath, req, res }); await boundErrorHandler(err); } catch (subErr) { err1 = subErr; } } next(err1); } }); switch (route.method) { case 'GET': router.get(route.routePath, ...handlers); break; case 'POST': router.post(route.routePath, ...handlers); break; case 'PUT': router.put(route.routePath, ...handlers); break; case 'DELETE': router.delete(route.routePath, ...handlers); break; case 'USE': router.use(route.routePath, ...handlers); break; } } return router; } } export function RouteUse(routePath: string, docs: RouteDoc = {}) { return function (controller: Controller, methodFunc: string) { const route = controller.getRoute(controller, methodFunc); route.routePath = routePath; route.method = 'USE'; route.routeDocs = docs; }; } export function RouteGet(routePath: string, docs: RouteDoc = {}) { return function (controller: Controller, methodFunc: string) { const route = controller.getRoute(controller, methodFunc); route.routePath = routePath; route.method = 'GET'; route.routeDocs = docs; }; } export function RoutePut(routePath: string, docs: RouteDoc = {}) { return function (controller: Controller, methodFunc: string) { const route = controller.getRoute(controller, methodFunc); route.routePath = routePath; route.method = 'PUT'; route.routeDocs = docs; }; } export function RoutePost(routePath: string, docs: RouteDoc = {}) { return function (controller: Controller, methodFunc: string) { const route = controller.getRoute(controller, methodFunc); route.routePath = routePath; route.method = 'POST'; route.routeDocs = docs; }; } export function RouteDelete(routePath: string, docs: RouteDoc = {}) { return function (controller: Controller, methodFunc: string) { const route = controller.getRoute(controller, methodFunc); route.routePath = routePath; route.method = 'DELETE'; route.routeDocs = docs; }; } export function RouteDocsHidden() { return function (controller, methodProp: string) { const route = controller.getRoute(controller, methodProp); route.hidden = true; }; } export function RouteHasRole(roles: string[]) { return function (controller, methodProp: string) { const route = controller.getRoute(controller, methodProp); route.roles = roles; }; } export function RouteResponse(objType = 'object', docs: RouteDoc = {}, contentType = 'application/json; charset=utf-8') { return function (controller: Controller, methodProp: string) { const route = controller.getRoute(controller, methodProp); route.responseObjectType = objType; route.responseContentType = contentType; route.responseDocs = docs; }; } export function RouteInputFilter<K>(filter: RouteFilter<K>) { return function (controller: Controller, methodProp: string) { const route = controller.getRoute(controller, methodProp); route.inputFilters.push(filter); }; } export function RouteOutputFilter<K>(filter: RouteFilter<K>) { return function (controller: Controller, methodProp: string) { const route = controller.getRoute(controller, methodProp); route.outputFilters.push(filter); }; } export function RouteErrorHandler(errorHandler: ErrorHandler) { return function (controller: Controller, methodProp: string) { const route = controller.getRoute(controller, methodProp); route.errorHandlers.push(errorHandler); }; } export function RouteResponseStatus(status: number = HttpStatus.OK) { return function (controller: Controller, methodProp: string) { const route = controller.getRoute(controller, methodProp); route.responseStatus = status; }; } export function RouteParamUser(docs: RouteDoc = {}) { return function (targetClass: Controller, methodProp: string, parameterIndex: number) { const route = targetClass.getRoute(targetClass, methodProp); const param: ControllerRouteParamUser = { type: 'user', parameterIndex, docs }; route.params.push(param); }; } export function RouteParamBody(docs: RouteDoc = {}) { return function (targetClass: Controller, methodProp: string, parameterIndex: number) { const route = targetClass.getRoute(targetClass, methodProp); const param: ControllerRouteParamBody = { type: 'body', parameterIndex, docs }; route.params.push(param); }; } export function RouteParamHeaders(docs: RouteDoc = {}) { return function (targetClass: Controller, methodProp: string, parameterIndex: number) { const route = targetClass.getRoute(targetClass, methodProp); const param: ControllerRouteParamHeaders = { type: 'headers', parameterIndex, docs }; route.params.push(param); }; } export function RouteParamStream(docs: RouteDoc = {}) { return function (targetClass: Controller, methodProp: string, parameterIndex: number) { const route = targetClass.getRoute(targetClass, methodProp); const param: ControllerRouteParamStream = { type: 'stream', parameterIndex, docs }; route.params.push(param); }; } export function RouteParamGetAll(queryFields = []) { return function (targetClass: Controller, methodProp: string, parameterIndex: number) { const route = targetClass.getRoute(targetClass, methodProp); const param: ControllerRouteParamGetAll = { type: 'getAll', parameterIndex, queryFields, docs: { summary: 'Sort and pagination' } }; route.params.push(param); }; } export function RouteParamRelated() { return function (targetClass: Controller, methodProp: string, parameterIndex: number) { const route = targetClass.getRoute(targetClass, methodProp); const param: ControllerRouteParamRelated = { type: 'related', parameterIndex, docs: { summary: 'Related fields' } }; route.params.push(param); }; } export function RouteParamPath(name: string, docs: RouteDoc = {}) { return function (targetClass: Controller, methodProp: string, parameterIndex: number) { const route = targetClass.getRoute(targetClass, methodProp); const param: ControllerRouteParamPath = { type: 'path', parameterIndex, name, docs }; route.params.push(param); }; } export function RouteParamQuery(name: string, docs: RouteDoc = {}) { return function (targetClass: Controller, methodProp: string, parameterIndex: number) { const route = targetClass.getRoute(targetClass, methodProp); const param: ControllerRouteParamQuery = { type: 'query', parameterIndex, name, docs }; route.params.push(param); }; } export function RouteParamMethod(docs: RouteDoc = {}) { return function (targetClass: Controller, methodProp: string, parameterIndex: number) { const route = targetClass.getRoute(targetClass, methodProp); const param: ControllerRouteParamMethod = { type: 'method', parameterIndex, docs }; route.params.push(param); }; }