UNPKG

adba

Version:
269 lines (246 loc) 9.83 kB
/** * @file express-router.ts * @description This file provides functionalities for creating an Express router with dynamic routes based on ORM models. */ import { Model } from "objection"; import express from 'express'; import type { Request, Response, NextFunction } from 'express'; import moment from 'moment'; import { kebabCase } from "change-case-all"; import { v4 as uuidv4 } from 'uuid'; import { unflatten } from 'dbl-utils'; import type { IControllerMethods, IExpressRouterConf, IRouteConf, IRoutesObject, IStatusCode, IRouterMethods } from "./types"; import GenericController from "./controller"; import Controller from "./controller"; import getStatusCode from "./status-codes"; const aliasing: Record<string, string> = {}; /** * Predefined RESTful routes mapped to controller actions. */ const definedREST: Record<string, string> = { 'GET /': 'list', 'POST /': 'list', 'PUT /': 'insert', 'PATCH /': 'update', 'DELETE /': 'delete', 'GET /:id': 'selectById', 'PATCH /:id': 'update', 'DELETE /:id': 'delete', }; /** * Modifies predefined routes by either adding new ones or removing existing ones. * @param defs - The definitions to be added or removed. * @param remove - Whether to remove the definitions. */ export function modifyDefinedRoutes(defs: Record<string, string> | string[], remove: boolean = false) { if (remove && Array.isArray(defs)) { defs.forEach((k: string) => (delete definedREST[k])); } else { Object.assign(definedREST, defs); } } /** * Adds alias mappings for table names. * @param alias - The alias mappings. */ export function addTableAlias(alias: Record<string, string>) { Object.assign(aliasing, alias); } /** * Prepares the routes object by building route entries for each model. * @param routesObj - The routes object to populate. * @param TheModel - The model for which to create routes. * @param controllers - The controllers associated with the models. * @param includeTable - Whether to include the table in route definitions. */ function prepareRoutesObj( routesObj: IRoutesObject, TheModel: typeof Model, controllers: Record<string, typeof Controller>, includeTable: boolean | IRouteConf ) { if (includeTable === false) return; const tableName = TheModel.tableName; const TheController = Object.values(controllers).find(C => { const ctrl = new C(Model); return ctrl.Model.tableName === tableName; }) || GenericController; if (includeTable === true) { Object.entries(definedREST).forEach(([service, action]) => { buildRoutesObj(routesObj, tableName, service, action, TheController, TheModel); }); return; } const { defaultAction = 'includes', ...rest } = includeTable; if (defaultAction === 'excludes') { Object.entries(rest).forEach(([service, action]) => { if (!action) return; if (action === true) action = definedREST[service]; buildRoutesObj(routesObj, tableName, service, action as string, TheController, TheModel); }); } else { const cpRest = { ...rest }; Object.entries(definedREST).forEach(([service, action]) => { if (rest[service] === false) { delete cpRest[service]; return; } if (typeof rest[service] === 'string') { action = rest[service]; delete cpRest[service]; } buildRoutesObj(routesObj, tableName, service, action, TheController, TheModel); }); // Add custom routes not defined in definedREST Object.entries(cpRest).forEach(([service, action]) => { if (!action) return; if (action === true) action = definedREST[service]; buildRoutesObj(routesObj, tableName, service, action as string, TheController, TheModel); }); } } /** * Constructs the routes object with respective service paths, actions, and controllers. * @param routesObj - Object to store constructed routes. * @param tableName - The table name for the model. * @param service - The HTTP method and endpoint. * @param action - The action to be performed. * @param TheController - The controller class. * @param TheModel - The model class. */ function buildRoutesObj( routesObj: IRoutesObject, tableName: string, service: string, action: string, TheController: typeof Controller, TheModel: typeof Model ) { const [m, path] = service.split(' '); const slugTableName = aliasing[tableName] || kebabCase(tableName); const method = m.toUpperCase() as IRoutesObject[1][0]; const servicePath = `/${slugTableName}${path}`; routesObj[`${method} ${servicePath}`] = [method, servicePath, action, TheController, TheModel]; } /** * Generates an object containing routes for each model and controller based on provided configuration. * @param models - The models to generate routes for. * @param controllers - The associated controllers for each model. * @param config - The configuration for including or excluding routes. * @returns The constructed routes object. */ export function routesObject(models: Record<string, typeof Model>, controllers: Record<string, typeof GenericController> = {}, config: IExpressRouterConf = {}) { const routesObj: IRoutesObject = {}; if (config.filters) { const { defaultAction = 'includes', ...rest } = config.filters; if (defaultAction === 'excludes') { Object.entries(rest).forEach(([tableName, includeTable]) => { if (!includeTable) return; const TheModel = Object.values(models).find(M => M.tableName === tableName); if (!TheModel) throw new Error(`Model for **${tableName}** Not Found`); prepareRoutesObj(routesObj, TheModel, controllers, includeTable as any); }); } else { // Include logic Object.values(models).forEach(TheModel => { const includeTable = rest[TheModel.tableName] !== undefined ? rest[TheModel.tableName] : (rest['*'] !== undefined ? rest['*'] : true); prepareRoutesObj(routesObj, TheModel, controllers, includeTable as any); }); } } else { Object.values(models).forEach(TheModel => { prepareRoutesObj(routesObj, TheModel, controllers, true); }); } return routesObj; } /** * Lists the routes currently defined in the Express router. * @param router - The Express router. * @returns An array of strings representing each route method and path. */ export function listRoutes(router: express.Router) { const routes: string[] = []; router.stack.forEach((middleware: any) => { if (middleware.route) { // It's a defined route const method = Object.keys(middleware.route.methods)[0].toUpperCase(); const routePath = middleware.route.path; routes.push(`${method} ${routePath}`); } }); return routes; } /** * Main function to configure an Express router with dynamic routes. * @param routesObject - The routes object containing route definitions. * @param config - Configuration options for the router. * @returns The configured Express router. */ export default function expressRouter(routesObject: IRoutesObject, { router = express.Router(), beforeProcess = (tn: string, a: string, data: any, i: string) => data, afterProcess = (tn: string, a: string, data: any, i: string) => data, debugLog = false } = {}) { Object.values(routesObject).forEach((value: IRoutesObject[1]) => { const [method, path, action, TheController, TheModel] = value; const routerMethod = (router[method.toLowerCase() as IRouterMethods]).bind(router); routerMethod(path, async (req: Request, res: Response, next: NextFunction) => { const protocol = req.protocol; const host = req.get('host'); const _idx = uuidv4(); console.log(''); console.log(''); console.time(_idx); console.group(moment().format('YYYY-MM-DD HH:mm:ss')); console.log(_idx, '>', `${method} ${protocol}://${host}${req.originalUrl}`, Controller.name, action); if (debugLog) { console.debug('HEADERS', req.headers); console.debug('PARAMS', req.params); console.debug('QUERY', req.query); console.debug('BODY', req.body); console.debug('COOKIES', req.cookies); } try { const controller = new TheController(TheModel); const all = { ...req.body || {}, ...unflatten(req.query || {})!, ...req.params || {}, } const ctrlAction: Function = controller[action as IControllerMethods].bind(controller); const inputData = await beforeProcess(controller.Model.tableName, action, all, _idx); const outputData = await ctrlAction(inputData); const payload = await afterProcess(controller.Model.tableName, action, outputData, _idx); // Check if the output data is an error or if it needs a special response if (payload instanceof Error) throw payload; if (!payload) throw getStatusCode(503); else res.status(payload.status).json(payload); if (debugLog) { console.debug('RESPONSE', payload); } } catch (error) { if (error instanceof Error) { console.error(error); const e: any = error; const code = e.statusCode || e.code || 0; const payload = getStatusCode(500, code); payload.data = error.message; res.status(500).json(payload); } else if (typeof (error as IStatusCode).status === 'number') { res.status((error as IStatusCode).status).json(error); } } console.timeEnd(_idx); console.groupEnd(); }); }); // Setup a default GET route that lists all available routes router.get('/', (req, res) => { const availableRoutes = listRoutes(router); const success = getStatusCode(200); success.data = availableRoutes; res.status(success.status!).json(success); }); return router; }