UNPKG

@vortex-js/core

Version:

A simple and powerful role-based access control (RBAC) middleware for Express.js, designed to be easy to use and integrate with your existing applications. It provides a flexible way to manage user permissions and roles, making it ideal for building secur

289 lines (288 loc) 11.4 kB
import { v4 as uuidv4 } from "uuid"; import { InvalidRouteError } from "../errors"; // eslint-disable-next-line @typescript-eslint/no-explicit-any class Route { constructor(r) { this.id = uuidv4(); this.method = r.method; this.path = r.path; this.middlewares = r.middlewares; this.name = r.name ? r.name : `[${this.method}] ${this.path}`; this.description = r.description ? r.description : ""; this.rai = r.rai; this.roles = r.roles; this.postman = r.postman ? r.postman : { headers: [], body: {}, params: [] }; /** * All the required fields must gevin * throw an error if not */ if (!this.method) { throw new InvalidRouteError("Route instance must have a method"); } if (!this.path) { throw new InvalidRouteError("Route instance must have a path"); } if (!this.rai) { throw new InvalidRouteError("Route instance must have a rai"); } if (!this.roles) { throw new InvalidRouteError("Route instance must have a roles"); } if (!Array.isArray(this.middlewares)) { throw new InvalidRouteError("Route instance middlewares must be an array"); } } /** * This function is used to register a module to the route * so that we can use the module name in the route * and also to access the module config if needed */ registerModule(module) { this.module = module; /** * If the module has a name, we can set it to the route */ if (module.name) { this.name = `${module.name} - ${this.name}`; } } buildRoute(router, route, prefix) { /** * After Making sure that the route is an instance of Routes * we need : * - check if the route has any params if so we need to add them to the router * - call the buildRouter method of the Routes class */ if (route.params) { if (Array.isArray(route.params)) { route.params.forEach((param) => { if (param.path) { if (typeof param.method === "function") { /** * Make sure that the param.path dosn't ':' in the beginning */ if (param.path[0] === ":") { param.path = param.path.slice(1); } router.param(param.path, param.method); } else { throw new InvalidRouteError(` INVALID params FIELD: params must have a method PREFIX: ${route.prefix} PARAM: [ ... { PATH: ${param.path}, METHOD: null } ... ] `); } } else { throw new InvalidRouteError(` INVALID params FIELD: params must have a path PREFIX: ${route.prefix} `); } }); } else { throw new InvalidRouteError(` INVALID params FIELD: params must be an array PREFIX: ${route.prefix} `); } } /** * We check that all the middlewares are valid function * and if not we throw an error */ this.middlewares.forEach((middleware) => { if (typeof middleware !== "function") { throw new InvalidRouteError(` INVALID MIDDLEWARE FIELD: middleware must be a function PATH: ${prefix.path + this.path} METHOD: ${this.method} RESOURCE: ${this.rai} MIDDLEWARES: [${this.middlewares.map((m) => typeof m === "function" ? " function " : " null ")}] `); } }); /** * Here we can use the switch case to handle the routes * based on the method */ switch (this.method) { case "GET": router.get(prefix.path + this.path, ...this.middlewares); break; case "POST": router.post(prefix.path + this.path, ...this.middlewares); break; case "PUT": router.put(prefix.path + this.path, ...this.middlewares); break; case "DELETE": router.delete(prefix.path + this.path, ...this.middlewares); break; } } /** * This function is used to generate the route for postman collection (route = request) */ generateRoute(pathPrefix = "") { let fullPath = pathPrefix + this.path; // Extract all express's router parameters from the path // This is to replace the parameters with Postman's variable syntax // For example, if the path is "/users/:userId", it will be replaced with "/users/{{ userId }}" const expressRouterParamRegex = /:([a-zA-Z0-9_]+)/g; // Extract path parameters from the route path fullPath = fullPath.replace(expressRouterParamRegex, "{{ $1 }}"); // Remove trailing slashes from the path const urlParts = fullPath.split("?")[0]; // Create an array of path segments, filtering out empty segments // This is to ensure that the path is correctly formatted for Postman // and does not contain any empty segments that could cause issues const pathSegments = urlParts .split("/") .filter((segment) => segment !== ""); // Construct Postman URL object const postmanUrl = { raw: `{{base_url}}${urlParts}`, host: ["{{base_url}}"], path: pathSegments, }; // Add query parameters if (this.postman && this.postman.params && this.postman.params?.length > 0) { postmanUrl.query = this.postman.params.map((param) => ({ key: param.key, value: param.value, description: param.description || "", })); } // Construct request body // let postmanBody = {}; // if ( // ["POST", "PUT", "PATCH"].includes(this.method.toUpperCase()) && // this.postman?.body // ) { // postmanBody = { // mode: "raw", // raw: JSON.stringify(this.postman.body, null, 2), // options: { raw: { language: "json" } }, // }; // } // the old way /** * Let's use the new way to handle postman body * by checkin if the headers are present, check if they have a content-type * - if they have a content-type of application/json we will build the body as a raw json object * - if they have a content-type of application/x-www-form-urlencoded we will build the body as a form object * - if they have a content-type of multipart/form-data we will build the body as a form-data object * - if they have a content-type of text/plain we will build the body as a text object */ let postmanBody = {}; /** * This is the default headers for the postman body * if the headers are not present we will use this */ const bodyConfig = { headers: this.postman?.headers ?.map((item) => { const allowedKeys = ["Content-Type"]; const allowedValues = [ "application/json", "application/xml", "text/html", "text/plain", "application/x-www-form-urlencoded", "multipart/form-data", "image/jpeg", "image/png", "image/gif", "application/pdf", "text/css", "application/javascript", ]; if (allowedKeys.includes(item.key) && allowedValues.includes(item.value)) { return { key: item.key, value: item.value, description: item.description || "", }; } return null; }) .filter(Boolean) || [ { key: "Content-Type", value: "application/json" }, ], }; const contentTypeHeader = bodyConfig.headers.find((header) => header?.key === "Content-Type"); if (contentTypeHeader) { switch (contentTypeHeader.value) { case "application/json": postmanBody = { mode: "raw", raw: JSON.stringify(this.postman?.body || {}, null, 2), options: { raw: { language: "json" } }, }; break; case "application/x-www-form-urlencoded": postmanBody = { mode: "urlencoded", urlencoded: Object.entries(this.postman?.body || {}).map(([key, value]) => ({ key, value: String(value) })), }; break; case "multipart/form-data": postmanBody = { mode: "formdata", formdata: Object.entries(this.postman?.body || {}).map( // eslint-disable-next-line @typescript-eslint/no-explicit-any ([key, value]) => { /** * Here is a tricky part, we need to check if the value is an object * if so we check if it has a type property * if it has a type property we use it as the type of the form-data * if not we use "text" as the type of the form-data */ if (typeof value === "object" && value !== null && value.type) { return { key, value: String(value.value || ""), type: value.type || "text", }; } return { key, value: String(value), type: "text" }; }), }; break; case "text/plain": postmanBody = { mode: "raw", raw: String(this.postman?.body || ""), options: { raw: { language: "text" } }, }; break; default: postmanBody = {}; } } return { name: this.name, request: { method: this.method.toUpperCase(), url: postmanUrl, description: this.description, body: postmanBody, }, }; } } export default Route;