UNPKG

actionhero

Version:

The reusable, scalable, and quick node.js API server for stateless and stateful applications

230 lines (203 loc) 6.52 kB
import * as path from "path"; import { api, config, log, utils, route, Initializer, RouteType, Connection, RouteMethod, } from "../index"; import { routerMethods } from "../modules/route"; export interface RoutesApi { routes: { [method in RouteMethod]: RouteType[] }; processRoute: RoutesInitializer["processRoute"]; matchURL: RoutesInitializer["matchURL"]; loadRoutes: RoutesInitializer["loadRoutes"]; } /** * Contains routing options for web clients. Can associate routes with actions or files. */ export class RoutesInitializer extends Initializer { constructor() { super(); this.name = "routes"; this.loadPriority = 500; } processRoute = (connection: Connection, pathParts: string[]) => { if ( connection.params.action === undefined || api.actions.actions[connection.params.action] === undefined ) { let method = connection.rawConnection.method.toLowerCase() as RouteMethod; if (method === "head" && !api.routes.routes.head) method = "get"; for (const i in api.routes.routes[method]) { const route = api.routes.routes[method][i]; const match = api.routes.matchURL( pathParts, route.path, route.matchTrailingPathParts, ); if (match.match) { if (route.apiVersion) { connection.params.apiVersion ||= route.apiVersion; } for (const param in match.params) { try { const decodedName = decodeURIComponent(param.replace(/\+/g, " ")); const decodedValue = decodeURIComponent( match.params[param].replace(/\+/g, " "), ); connection.params[decodedName] = decodedValue; } catch (e) { // malformed URL } } connection.matchedRoute = route; if (route.dir) { const requestedFile = connection.rawConnection.parsedURL.pathname.substring( route.path.length, connection.rawConnection.parsedURL.pathname.length, ); connection.params.file = path.normalize( route.dir + "/" + requestedFile, ); } else { connection.params.action = route.action; } break; } } } }; matchURL = ( pathParts: string[], match: string, matchTrailingPathParts: boolean, ) => { const response: { match: boolean; params: { [key: string]: any } } = { match: false, params: {}, }; const matchParts = match.split("/"); let regexpMatch: string = null; let variable = ""; if (matchParts[0] === "") matchParts.splice(0, 1); if (matchParts[matchParts.length - 1] === "") matchParts.pop(); if (matchParts.length !== pathParts.length && !matchTrailingPathParts) { return response; } for (const i in matchParts) { const matchPart = matchParts[i]; let pathPart = pathParts[i]; if (matchTrailingPathParts && parseInt(i) === matchParts.length - 1) { for (const j in pathParts) { if (j > i) pathPart = pathPart + "/" + pathParts[j]; } } if (!pathPart) return response; if (matchPart.includes(":")) { const trimmedMatchParts = matchPart.split(":"); const trimmedMatchPart = trimmedMatchParts[trimmedMatchParts.length - 1]; const replacement = trimmedMatchParts[trimmedMatchParts.length - 2]; if (replacement) { if (!pathPart.includes(replacement)) return response; pathPart = pathPart.replace(replacement, ""); } if (!trimmedMatchPart.includes("(")) { variable = trimmedMatchPart; response.params[variable] = pathPart; } else { variable = trimmedMatchPart.replace(":", "").split("(")[0]; regexpMatch = trimmedMatchPart.substring( trimmedMatchPart.indexOf("(") + 1, trimmedMatchPart.length - 1, ); const matches = pathPart.match(new RegExp(regexpMatch, "g")); if (matches) { response.params[variable] = pathPart; } else { return response; } } } else { if ( pathPart === null || pathPart === undefined || pathParts[i].toLowerCase() !== matchPart.toLowerCase() ) { return response; } } } response.match = true; return response; }; loadRoutes = (rawRoutes?: (typeof config)["routes"]) => { let counter = 0; if (!rawRoutes) if (config.routes) rawRoutes = config.routes; for (const [method, collection] of Object.entries(rawRoutes)) { for (const configRoute of collection as RouteType[]) { if (method === "all") { for (const verb of routerMethods) { route.registerRoute( verb as RouteMethod, configRoute.path, configRoute.action, configRoute.apiVersion, configRoute.matchTrailingPathParts, configRoute.dir, ); } } else { route.registerRoute( method as RouteMethod, configRoute.path, configRoute.action, configRoute.apiVersion, configRoute.matchTrailingPathParts, configRoute.dir, ); } counter++; } } api.params.postVariables = utils.arrayUnique(api.params.postVariables); if (config.web && Array.isArray(config.web.automaticRoutes)) { config.web.automaticRoutes.forEach((verb: RouteMethod) => { if (!routerMethods.includes(verb)) { throw new Error(`${verb} is not an HTTP verb`); } log( `creating routes automatically for all actions responding to ${verb.toUpperCase()} HTTP verb`, ); for (const action in api.actions.actions) { route.registerRoute(verb, "/" + action, action, null); } }); } log("routes:", "debug", api.routes.routes); return counter; }; async initialize() { api.routes = { routes: { all: [], head: [], get: [], patch: [], post: [], put: [], delete: [], }, processRoute: this.processRoute, matchURL: this.matchURL, loadRoutes: this.loadRoutes, }; api.routes.loadRoutes(); } }