actionhero
Version:
The reusable, scalable, and quick node.js API server for stateless and stateful applications
230 lines (203 loc) • 6.52 kB
text/typescript
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();
}
}