diff --git a/README.md b/README.md index 5c40894..d697448 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,9 @@ The _callback_ parameter has these arguments of the SVR.JS mod callback: - _config_ - the SVR.JS configuration object - _next_ - the callback which passes the execution to other routes, SVR.JS mods and SVR.JS internal handlers. -The _req_ object has an additional _params_ parameter, which contains request parameters, for example if the request URL is `/api/task/1`, and the route path is `/api/task/:id`, then the _req.params_ object is a `null` prototype object with the `id` property set to `"1"`. +The _req_ object has an additional _params_ property, which contains request parameters, for example if the request URL is `/api/task/1`, and the route path is `/api/task/:id`, then the _req.params_ object is a `null` prototype object with the `id` property set to `"1"`. + +The _req_ object has another additional _svrouterBase_ property, which contains the route base used internally by a SVRouter router. It's not recommended to override this property, as doing it may result in a SVRouter router behaving erratically. You can read more about the SVR.JS mod callbacks in the [SVR.JS mod API documentation](https://svrjs.org/docs/api/svrjs-api). @@ -76,7 +78,7 @@ Parameters: Returns: the SVRouter router (so that you can chain the methods for routes or pass-throughs) -The function adds a pass-through (can be middleware) to the SVRouter router. The pass-through can be to an another SVRouter router (the absolute request URLs need to be provided for the _router.route_ function in the SVRouter router). Note that the request URL is not rewritten though, unlike in the `router` library, so if you write middleware for SVRouter, you may need to include the request URL prefix in the parameters of the function that returns the SVR.JS mod callback. +The function adds a pass-through (can be middleware) to the SVRouter router. The pass-through can be to an another SVRouter router (the request URLs relative to a parent route need to be provided for the _router.route_ function in the SVRouter router). The _callback_ parameter has these arguments of the SVR.JS mod callback: - _req_ - the SVR.JS request object @@ -85,6 +87,8 @@ The _callback_ parameter has these arguments of the SVR.JS mod callback: - _config_ - the SVR.JS configuration object - _next_ - the callback which passes the execution to other routes, SVR.JS mods and SVR.JS internal handlers. +The _req_ object has an additional _svrouterBase_ property, which contains the route base used internally by a SVRouter router. It's not recommended to override this property, as doing it may result in a SVRouter router behaving erratically. + You can read more about the SVR.JS mod callbacks in the [SVR.JS mod API documentation](https://svrjs.org/docs/api/svrjs-api). ### _router.passExpressRouterMiddleware([path, ]middleware)_ diff --git a/src/index.js b/src/index.js index 2a948ca..b275b12 100644 --- a/src/index.js +++ b/src/index.js @@ -3,19 +3,25 @@ const http = require("http"); function svrouter() { const routes = []; - const router = (req, res, logFacilities, config, next) => { let index = 0; let previousReqParams = req.params; + let previousReqSvrouterBase = req.svrouterBase; let paramsPresent = false; + let passPathPresent = false; const nextRoute = () => { if (paramsPresent) req.params = previousReqParams; + if (passPathPresent) req.svrouterBase = previousReqSvrouterBase; let currentRoute = routes[index++]; let currentMatch = currentRoute && currentRoute.pathFunction - ? currentRoute.pathFunction(req.parsedURL.pathname) - : false; + ? currentRoute.pathFunction( + req.parsedURL.pathname.substring( + (req.svrouterBase ? req.svrouterBase : "").length + ) + ) + : false; // Let's assume the request URL begins with the contents of the req.svrouterBase property. while ( currentRoute && ((currentRoute.method && req.method != currentRoute.method) || @@ -24,17 +30,26 @@ function svrouter() { currentRoute = routes[index++]; currentMatch = currentRoute && currentRoute.pathFunction - ? currentRoute.pathFunction(req.parsedURL.pathname) + ? currentRoute.pathFunction( + req.parsedURL.pathname.substring( + (req.svrouterBase ? req.svrouterBase : "").length + ) + ) : false; } if (currentRoute && currentRoute.callback) { try { paramsPresent = Boolean(currentMatch.params); + passPathPresent = Boolean(currentRoute.passPath); if (paramsPresent) req.params = currentMatch && currentMatch.params ? currentMatch.params : Object.create(null); + if (passPathPresent) + req.svrouterBase = + (req.svrouterBase ? req.svrouterBase : "") + + currentRoute.passPath; currentRoute.callback(req, res, logFacilities, config, nextRoute); } catch (err) { res.error(500, err); @@ -59,7 +74,8 @@ function svrouter() { routes.push({ method: method === "*" ? null : method.toUpperCase(), pathFunction: match(path), - callback: callback + callback: callback, + passPath: null }); return router; @@ -86,7 +102,8 @@ function svrouter() { } : false : () => true, - callback: realCallback + callback: realCallback, + passPath: realPath }); return router; @@ -105,16 +122,20 @@ function svrouter() { const previousReqBaseUrl = req.baseUrl; const previousReqUrl = req.url; const previousReqOriginalUrl = req.originalUrl; + const previousReqSvrouterBase = req.svrouterBase; - req.baseUrl = realPath; + const svrouterBase = req.svrouterBase ? req.svrouterBase : ""; + req.baseUrl = svrouterBase; req.originalUrl = req.url; - req.url = req.url.substr(realPath.length); // Let's assume the request URL begins with the contents of realPath variable. + req.url = req.url.substr(svrouterBase.length); // Let's assume the request URL begins with the contents of svrouterBase variable. if (!req.url) req.url = "/"; + req.svrouterBase = undefined; const nextCallback = () => { req.baseUrl = previousReqBaseUrl; req.url = previousReqUrl; req.originalUrl = previousReqOriginalUrl; + req.svrouterBase = previousReqSvrouterBase; next(); }; diff --git a/tests/index.test.js b/tests/index.test.js index e61d076..aa0dcb1 100644 --- a/tests/index.test.js +++ b/tests/index.test.js @@ -228,7 +228,7 @@ describe("SVRouter", () => { expect(res.end).toHaveBeenCalledWith("Middleware matched"); }); - test("should restore req.url and req.baseUrl after middleware runs", () => { + test("should restore req.url and req.baseUrl after middleware added with passExpressRouterMiddleware runs", () => { const req = { method: "GET", parsedURL: { pathname: "/api/resource" }, @@ -312,6 +312,113 @@ describe("SVRouter", () => { expect(res.secondMiddlewareRan).toBe(true); }); + test("should handle nested routers correctly", () => { + const childRouter = svrouter(); + + const req = { + method: "GET", + parsedURL: { pathname: "/parent/child/resource" }, + params: null + }; + const res = { + end: jest.fn() + }; + + childRouter.get("/child/resource", (req, res) => { + res.end("Child router route matched"); + }); + + router.pass("/parent", childRouter); + + router(req, res, null, null, () => { + res.end("No route matched"); + }); + + expect(res.end).toHaveBeenCalledWith("Child router route matched"); + }); + + test("should fallback to parent router if no route matches in child router", () => { + const childRouter = svrouter(); + + const req = { + method: "GET", + parsedURL: { pathname: "/parent/child/resource" }, + params: null + }; + const res = { + end: jest.fn() + }; + + router.pass("/parent", childRouter); + router.get("/parent/child/resource", (req, res) => { + res.end("Parent router fallback route matched"); + }); + + router(req, res, null, null, () => { + res.end("No route matched"); + }); + + expect(res.end).toHaveBeenCalledWith( + "Parent router fallback route matched" + ); + }); + + test("should handle parameterized routes in nested routers", () => { + const childRouter = svrouter(); + + const req = { + method: "GET", + parsedURL: { pathname: "/parent/user/42/details" }, + params: null + }; + const res = { + end: jest.fn() + }; + + childRouter.get("/user/:id/details", (req, res) => { + res.end(`User ID is ${req.params.id}`); + }); + + router.pass("/parent", childRouter); + + router(req, res, null, null, () => { + res.end("No route matched"); + }); + + expect(res.end).toHaveBeenCalledWith("User ID is 42"); + }); + + test("should handle multiple nested routers", () => { + const childRouter1 = svrouter(); + const childRouter2 = svrouter(); + + const req = { + method: "GET", + parsedURL: { pathname: "/parent/child2/resource" }, + params: null + }; + const res = { + end: jest.fn() + }; + + childRouter1.get("/resource", (req, res) => { + res.end("Child router 1 route matched"); + }); + + childRouter2.get("/resource", (req, res) => { + res.end("Child router 2 route matched"); + }); + + router.pass("/parent/child1", childRouter1); + router.pass("/parent/child2", childRouter2); + + router(req, res, null, null, () => { + res.end("No route matched"); + }); + + expect(res.end).toHaveBeenCalledWith("Child router 2 route matched"); + }); + test("should throw an error if method is not a string in route", () => { expect(() => { router.route(123, "/path", () => {});