adba
Version:
Any DataBase to API
271 lines (270 loc) • 11.8 kB
JavaScript
;
/**
* @file express-router.ts
* @description This file provides functionalities for creating an Express router with dynamic routes based on ORM models.
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.modifyDefinedRoutes = modifyDefinedRoutes;
exports.addTableAlias = addTableAlias;
exports.routesObject = routesObject;
exports.listRoutes = listRoutes;
exports.default = expressRouter;
const objection_1 = require("objection");
const express_1 = __importDefault(require("express"));
const moment_1 = __importDefault(require("moment"));
const change_case_all_1 = require("change-case-all");
const uuid_1 = require("uuid");
const dbl_utils_1 = require("dbl-utils");
const controller_1 = __importDefault(require("./controller"));
const controller_2 = __importDefault(require("./controller"));
const status_codes_1 = __importDefault(require("./status-codes"));
const aliasing = {};
/**
* Predefined RESTful routes mapped to controller actions.
*/
const definedREST = {
'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.
*/
function modifyDefinedRoutes(defs, remove = false) {
if (remove && Array.isArray(defs)) {
defs.forEach((k) => (delete definedREST[k]));
}
else {
Object.assign(definedREST, defs);
}
}
/**
* Adds alias mappings for table names.
* @param alias - The alias mappings.
*/
function addTableAlias(alias) {
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, TheModel, controllers, includeTable) {
if (includeTable === false)
return;
const tableName = TheModel.tableName;
const TheController = Object.values(controllers).find(C => {
const ctrl = new C(objection_1.Model);
return ctrl.Model.tableName === tableName;
}) || controller_1.default;
if (includeTable === true) {
Object.entries(definedREST).forEach(([service, action]) => {
buildRoutesObj(routesObj, tableName, service, action, TheController, TheModel);
});
return;
}
const { defaultAction = 'includes' } = includeTable, rest = __rest(includeTable, ["defaultAction"]);
if (defaultAction === 'excludes') {
Object.entries(rest).forEach(([service, action]) => {
if (!action)
return;
if (action === true)
action = definedREST[service];
buildRoutesObj(routesObj, tableName, service, action, TheController, TheModel);
});
}
else {
const cpRest = Object.assign({}, 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, 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, tableName, service, action, TheController, TheModel) {
const [m, path] = service.split(' ');
const slugTableName = aliasing[tableName] || (0, change_case_all_1.kebabCase)(tableName);
const method = m.toUpperCase();
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.
*/
function routesObject(models, controllers = {}, config = {}) {
const routesObj = {};
if (config.filters) {
const _a = config.filters, { defaultAction = 'includes' } = _a, rest = __rest(_a, ["defaultAction"]);
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);
});
}
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);
});
}
}
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.
*/
function listRoutes(router) {
const routes = [];
router.stack.forEach((middleware) => {
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.
*/
function expressRouter(routesObject, { router = express_1.default.Router(), beforeProcess = (tn, a, data, i) => data, afterProcess = (tn, a, data, i) => data, debugLog = false } = {}) {
Object.values(routesObject).forEach((value) => {
const [method, path, action, TheController, TheModel] = value;
const routerMethod = (router[method.toLowerCase()]).bind(router);
routerMethod(path, (req, res, next) => __awaiter(this, void 0, void 0, function* () {
const protocol = req.protocol;
const host = req.get('host');
const _idx = (0, uuid_1.v4)();
console.log('');
console.log('');
console.time(_idx);
console.group((0, moment_1.default)().format('YYYY-MM-DD HH:mm:ss'));
console.log(_idx, '>', `${method} ${protocol}://${host}${req.originalUrl}`, controller_2.default.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 = Object.assign(Object.assign(Object.assign({}, req.body || {}), (0, dbl_utils_1.unflatten)(req.query || {})), req.params || {});
const ctrlAction = controller[action].bind(controller);
const inputData = yield beforeProcess(controller.Model.tableName, action, all, _idx);
const outputData = yield ctrlAction(inputData);
const payload = yield 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 (0, status_codes_1.default)(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 = error;
const code = e.statusCode || e.code || 0;
const payload = (0, status_codes_1.default)(500, code);
payload.data = error.message;
res.status(500).json(payload);
}
else if (typeof error.status === 'number') {
res.status(error.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 = (0, status_codes_1.default)(200);
success.data = availableRoutes;
res.status(success.status).json(success);
});
return router;
}