UNPKG

@routejs/router

Version:

Fast and lightweight http routing engine for nodejs

414 lines (366 loc) 11.3 kB
import nodePath from "node:path"; import url from "node:url"; import Route from "./route.mjs"; import supportedMethod from "./supported-method.mjs"; export default class Router { #routes = []; #config = { caseSensitive: false, host: undefined, }; #route = null; constructor(options = {}) { if (options.caseSensitive === true) { this.#config.caseSensitive = true; } this.#config.host = options.host; } checkout(path, ...callbacks) { return this.#setRoute({ method: "CHECKOUT", path, callbacks }); } copy(path, ...callbacks) { return this.#setRoute({ method: "COPY", path, callbacks }); } delete(path, ...callbacks) { return this.#setRoute({ method: "DELETE", path, callbacks }); } get(path, ...callbacks) { return this.#setRoute({ method: "GET", path, callbacks }); } head(path, ...callbacks) { return this.#setRoute({ method: "HEAD", path, callbacks }); } lock(path, ...callbacks) { return this.#setRoute({ method: "LOCK", path, callbacks }); } merge(path, ...callbacks) { return this.#setRoute({ method: "MERGE", path, callbacks }); } mkactivity(path, ...callbacks) { return this.#setRoute({ method: "MKACTIVITY", path, callbacks }); } mkcol(path, ...callbacks) { return this.#setRoute({ method: "MKCOL", path, callbacks }); } move(path, ...callbacks) { return this.#setRoute({ method: "MOVE", path, callbacks }); } notify(path, ...callbacks) { return this.#setRoute({ method: "NOTIFY", path, callbacks }); } options(path, ...callbacks) { return this.#setRoute({ method: "OPTIONS", path, callbacks }); } patch(path, ...callbacks) { return this.#setRoute({ method: "PATCH", path, callbacks }); } post(path, ...callbacks) { return this.#setRoute({ method: "POST", path, callbacks }); } propfind(path, ...callbacks) { return this.#setRoute({ method: "PROPFIND", path, callbacks }); } purge(path, ...callbacks) { return this.#setRoute({ method: "PURGE", path, callbacks }); } put(path, ...callbacks) { return this.#setRoute({ method: "PUT", path, callbacks }); } report(path, ...callbacks) { return this.#setRoute({ method: "REPORT", path, callbacks }); } search(path, ...callbacks) { return this.#setRoute({ method: "SEARCH", path, callbacks }); } subscribe(path, ...callbacks) { return this.#setRoute({ method: "SUBSCRIBE", path, callbacks }); } trace(path, ...callbacks) { return this.#setRoute({ method: "TRACE", path, callbacks }); } unlock(path, ...callbacks) { return this.#setRoute({ method: "UNLOCK", path, callbacks }); } unsubscribe(path, ...callbacks) { return this.#setRoute({ method: "UNSUBSCRIBE", path, callbacks }); } view(path, ...callbacks) { return this.#setRoute({ method: "VIEW", path, callbacks }); } any(methods, path, ...callbacks) { return this.#setRoute({ method: methods, path, callbacks }); } all(path, ...callbacks) { return this.#setRoute({ method: supportedMethod, path, callbacks }); } use(...callbacks) { if (typeof callbacks[0] === "string" || callbacks[0] instanceof String) { if (callbacks.length < 2) { throw new TypeError( "Error: use function callback accepts function or router as an argument" ); } return this.#mergeRoute({ group: callbacks[0], callbacks: callbacks.slice(1), }); } else { return this.#mergeRoute({ callbacks: callbacks }); } } group(path, callback) { if (!(typeof path === "string" || path instanceof String)) { throw new TypeError( "Error: group path accepts only string as an argument" ); } if (typeof callback === "function") { const router = new Router(); callback(router); return this.#mergeRoute({ group: path, callbacks: router }); } else { return this.#mergeRoute({ group: path, callbacks: callback }); } } domain(host, callback) { if (!(typeof host === "string" || host instanceof String)) { throw new TypeError( "Error: domain host accepts only string as an argument" ); } if (typeof callback === "function") { const router = new Router(); callback(router); return this.#mergeRoute({ host, callbacks: router }); } else { return this.#mergeRoute({ host, callbacks: callback }); } } setName(name) { // Set route name if (this.#route instanceof Route) { this.#route.setName(name); } else { throw new TypeError("Error: setName can not set name for middleware"); } return this; } routes() { return this.#routes; } route(name, params = []) { let namedRoute = null; this.routes().map((route) => { if (route.name === name) { namedRoute = route; } }); let routePath = namedRoute && namedRoute.path; if (namedRoute.params) { if ( !Array.isArray(params) || namedRoute.params.length !== params.length ) { throw new TypeError( "Error: invalid route parameters, please provide all route parameters" ); } for (const param of params) { routePath = routePath.replace(/\{(.*?)\}/, param); } } return routePath; } #setRoute({ host, method, path, callbacks, group, name }) { const route = new Route({ host: host ?? this.#config.host, method, path, callbacks, group, name, caseSensitive: this.#config.caseSensitive, }); this.#routes.push(route); this.#route = route; return this; } #mergeRoute({ host, method, group, callbacks }) { if (callbacks instanceof Router) { callbacks.routes().forEach((route) => { this.#setRoute({ host: host ?? route.host, method: method ?? route.method, path: group ? route.path ? nodePath.join(group, route.path) : null : route.path, callbacks: route.callbacks, group: group ? nodePath.join(group, route.group ?? "") : route.group, name: route.name, }); }); } else if (Array.isArray(callbacks)) { for (const route of callbacks) { if (route instanceof Route) { this.#setRoute({ host: host ?? route.host, method: method ?? route.method, path: group ? route.path ? nodePath.join(group, route.path) : null : route.path, callbacks: route.callbacks, group: group ? nodePath.join(group, route.group ?? "") : route.group, name: route.name, }); } else if (Array.isArray(route) || route instanceof Router) { this.#mergeRoute({ host, method, group, callbacks: route }); } else { this.#setRoute({ host, method, group, callbacks }); break; } } } else { this.#setRoute({ host, method, group, callbacks }); } // Set name is not allowed on middleware if (this.#route) { this.#route = null; } return this; } handle({ requestHost, requestMethod, requestUrl, request, response }) { const parsedUrl = url.parse(requestUrl ? requestUrl : ""); const requestPath = parsedUrl.pathname; const callStack = { stack: this.routes(), index: 0, }; function runMiddleware(callbacks, error = null) { try { if (typeof callbacks.stack[callbacks.index] === "undefined") { // No more middlewares to execute // Execute next callstack callStack.index++; return runCallStack(error); } if (typeof callbacks.stack[callbacks.index] !== "function") { throw new TypeError( "Error: callback argument only accepts function as an argument" ); } // Execute callbacks if (error === null && callbacks.stack[callbacks.index].length <= 3) { callbacks.stack[callbacks.index]( request, response, function (err = null) { if (err === "skip") { // Skip all middlewares of current callstack and execute next callstack callStack.index++; runCallStack(); } else { // Execute next middleware callbacks.index++; runMiddleware(callbacks, err); } } ); } else if ( error !== null && callbacks.stack[callbacks.index].length > 3 ) { // Execute error handler callbacks.stack[callbacks.index]( error, request, response, function (err = null) { if (err === "skip") { // Skip all middlewares of current callstack and execute next callstack callStack.index++; runCallStack(); } else { // Execute next middleware callbacks.index++; runMiddleware(callbacks, err); } } ); } else { // Skip current middleware callbacks.index++; runMiddleware(callbacks, error); } } catch (exception) { callbacks.index++; if (typeof callStack.stack[callStack.index] !== "undefined") { runMiddleware(callbacks, exception); } else { throw exception; } } } function runCallStack(error = null) { if (typeof callStack.stack[callStack.index] === "undefined") { if (error !== null) { if (error instanceof Error) { throw error; } else { throw new Error(error); } } // Nothing to execute return; } // Execute callbacks const match = callStack.stack[callStack.index].match({ host: requestHost, method: requestMethod, path: requestPath, }); if (match !== false) { request.params = match.params; request.subdomains = match.subdomains; const callbacks = { stack: match.callbacks, index: 0, }; return runMiddleware(callbacks, error); } else { callStack.index++; return runCallStack(error); } } runCallStack(); } handler() { function requestHandler(request, response) { var requestHost = request.headers ? request.headers.host : null; var requestMethod = request.method; var requestUrl = request.url; if (!requestHost && "getHeader" in request) { requestHost = request.getHeader("host"); } if (!requestMethod && "getMethod" in request) { requestMethod = request.getMethod(); } if (!requestUrl && "getUrl" in request) { requestUrl = request.getUrl(); } this.handle({ requestHost, requestMethod, requestUrl, request, response, }); } return requestHandler.bind(this); } }