UNPKG

straydog-js

Version:

Drop-in API monitoring for any Node.js backend

196 lines (195 loc) 7.04 kB
"use strict"; /** * @typedef {Object} Route * @property {Object} methods * @property {string | string[]} path * @property {any[]} stack * * @typedef {Object} Endpoint * @property {string} path Path name * @property {string[]} methods Methods handled * @property {string[]} middlewares Mounted middlewares */ Object.defineProperty(exports, "__esModule", { value: true }); const regExpToParseExpressPathRegExp = /^\/\^\\?\/?(?:(:?[\w\\.-]*(?:\\\/:?[\w\\.-]*)*)|(\(\?:\\?\/?\([^)]+\)\)))\\\/.*/; const regExpToReplaceExpressPathRegExpParams = /\(\?:\\?\/?\([^)]+\)\)/; const regexpExpressParamRegexp = /\(\?:\\?\\?\/?\([^)]+\)\)/g; const regexpExpressPathParamRegexp = /(:[^)]+)\([^)]+\)/g; const EXPRESS_ROOT_PATH_REGEXP_VALUE = '/^\\/?(?=\\/|$)/i'; const STACK_ITEM_VALID_NAMES = [ 'router', 'bound dispatch', 'mounted_app' ]; /** * Returns all the verbs detected for the passed route * @param {Route} route */ const getRouteMethods = function (route) { let methods = Object.keys(route.methods); methods = methods.filter((method) => method !== '_all'); methods = methods.map((method) => method.toUpperCase()); return methods; }; /** * Returns the names (or anonymous) of all the middlewares attached to the * passed route * @param {Route} route * @returns {string[]} */ const getRouteMiddlewares = function (route) { return route.stack.map((item) => { return item.handle.name || 'anonymous'; }); }; /** * Returns true if found regexp related with express params * @param {string} expressPathRegExp * @returns {boolean} */ const hasParams = function (expressPathRegExp) { return regexpExpressParamRegexp.test(expressPathRegExp); }; /** * @param {Route} route Express route object to be parsed * @param {string} basePath The basePath the route is on * @return {Endpoint[]} Endpoints info */ const parseExpressRoute = function (route, basePath) { const paths = []; if (Array.isArray(route.path)) { paths.push(...route.path); } else { paths.push(route.path); } /** @type {Endpoint[]} */ const endpoints = paths.map((path) => { const completePath = basePath && path === '/' ? basePath : `${basePath}${path}`; /** @type {Endpoint} */ const endpoint = { path: completePath.replace(regexpExpressPathParamRegexp, '$1'), methods: getRouteMethods(route), middlewares: getRouteMiddlewares(route) }; return endpoint; }); return endpoints; }; /** * @param {RegExp} expressPathRegExp * @param {any[]} params * @returns {string} */ const parseExpressPath = function (expressPathRegExp, params) { let parsedRegExp = expressPathRegExp.toString(); let expressPathRegExpExec = regExpToParseExpressPathRegExp.exec(parsedRegExp); let paramIndex = 0; while (hasParams(parsedRegExp)) { const paramName = params[paramIndex].name; const paramId = `:${paramName}`; parsedRegExp = parsedRegExp .replace(regExpToReplaceExpressPathRegExpParams, (str) => { // Express >= 4.20.0 uses a different RegExp for parameters: it // captures the slash as part of the parameter. We need to check // for this case and add the slash to the value that will replace // the parameter in the path. if (str.startsWith('(?:\\/')) { return `\\/${paramId}`; } return paramId; }); paramIndex++; } if (parsedRegExp !== expressPathRegExp.toString()) { expressPathRegExpExec = regExpToParseExpressPathRegExp.exec(parsedRegExp); } const parsedPath = expressPathRegExpExec[1].replace(/\\\//g, '/'); return parsedPath; }; /** * @param {import('express').Express | import('express').Router | any} app * @param {string} [basePath] * @param {Endpoint[]} [endpoints] * @returns {Endpoint[]} */ const parseEndpoints = function (app, basePath, endpoints) { const stack = app.stack || (app._router && app._router.stack); endpoints = endpoints || []; basePath = basePath || ''; if (!stack) { if (endpoints.length) { endpoints = addEndpoints(endpoints, [{ path: basePath, methods: [], middlewares: [] }]); } } else { endpoints = parseStack(stack, basePath, endpoints); } return endpoints; }; /** * Ensures the path of the new endpoints isn't yet in the array. * If the path is already in the array merges the endpoints with the existing * one, if not, it adds them to the array. * * @param {Endpoint[]} currentEndpoints Array of current endpoints * @param {Endpoint[]} endpointsToAdd New endpoints to be added to the array * @returns {Endpoint[]} Updated endpoints array */ const addEndpoints = function (currentEndpoints, endpointsToAdd) { endpointsToAdd.forEach((newEndpoint) => { const existingEndpoint = currentEndpoints.find((endpoint) => endpoint.path === newEndpoint.path); if (existingEndpoint !== undefined) { const newMethods = newEndpoint.methods.filter((method) => !existingEndpoint.methods.includes(method)); existingEndpoint.methods = existingEndpoint.methods.concat(newMethods); } else { currentEndpoints.push(newEndpoint); } }); return currentEndpoints; }; /** * @param {any[]} stack * @param {string} basePath * @param {Endpoint[]} endpoints * @returns {Endpoint[]} */ const parseStack = function (stack, basePath, endpoints) { stack.forEach((stackItem) => { if (stackItem.route) { const newEndpoints = parseExpressRoute(stackItem.route, basePath); endpoints = addEndpoints(endpoints, newEndpoints); } else if (STACK_ITEM_VALID_NAMES.includes(stackItem.name)) { const isExpressPathRegexp = regExpToParseExpressPathRegExp.test(stackItem.regexp); let newBasePath = basePath; if (isExpressPathRegexp) { const parsedPath = parseExpressPath(stackItem.regexp, stackItem.keys); newBasePath += `/${parsedPath}`; } else if (!stackItem.path && stackItem.regexp && stackItem.regexp.toString() !== EXPRESS_ROOT_PATH_REGEXP_VALUE) { const regExpPath = ` RegExp(${stackItem.regexp}) `; newBasePath += `/${regExpPath}`; } endpoints = parseEndpoints(stackItem.handle, newBasePath, endpoints); } }); return endpoints; }; /** * Returns an array of strings with all the detected endpoints * @param {import('express').Express | import('express').Router | any} app The express/router instance to get the endpoints from * @returns {Endpoint[]} */ const expressListEndpoints = function (app) { const endpoints = parseEndpoints(app, null, null); return endpoints; }; exports.default = expressListEndpoints;