diff --git a/README.md b/README.md index 2f8eb8e..47fa606 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,21 @@ The _callback_ parameter has these arguments of the SVR.JS mod callback: 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)_ + +Parameters: + - _path_ - the path (begins with "/"), for which the route applies. (optional, _String_) + - _middleware_ - the middleware compatible with the `router` library (_Function_) + +Returns: the SVRouter router (so that you can chain the methods for routes or pass-throughs) + +The function adds middleware compatible with the `router` library to the SVRouter router. + +The _middleware_ parameter has these arguments of middleware compatible with the `router` library: + - _req_ - the request object + - _res_ - the response object + - _next_ - the callback which passes the execution to other routes, SVR.JS mods and SVR.JS internal handlers. + ### _router.get(path, callback)_ An alias to the _router.route("GET", path, callback)_ function diff --git a/src/index.js b/src/index.js index 069a960..06b2fbd 100644 --- a/src/index.js +++ b/src/index.js @@ -92,6 +92,38 @@ function svrouter() { return router; }; + const passExpressRouterMiddleware = (path, middleware) => { + const realMiddleware = middleware ? middleware : path; + if (typeof realMiddleware !== "function") { + throw new Error("The passed middleware must be a function."); + } else if (middleware && typeof path !== "string") { + throw new Error("The path must be a string."); + } + const realPath = realMiddleware ? path.replace(/\/+$/, "") : ""; + + const callback = (req, res, logFacilities, config, next) => { + const previousReqBaseUrl = req.baseUrl; + const previousReqUrl = req.url; + const previousReqOriginalUrl = req.originalUrl; + + req.baseUrl = realPath; + req.originalUrl = req.url; + req.url = req.url.substr(realPath.length); // Let's assume the request URL begins with the contents of realPath variable. + if (!req.url) req.url = "/"; + + const nextCallback = () => { + req.baseUrl = previousReqBaseUrl; + req.url = previousReqUrl; + req.originalUrl = previousReqOriginalUrl; + next(); + }; + + realMiddleware(req, res, nextCallback); + }; + + return passRoute(realPath, callback); + }; + const methods = http.METHODS ? http.METHODS : ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]; @@ -104,6 +136,7 @@ function svrouter() { router.all = (path, callback) => addRoute("*", path, callback); router.route = addRoute; router.pass = passRoute; + router.passExpressRouterMiddleware = passExpressRouterMiddleware; return router; } diff --git a/tests/index.test.js b/tests/index.test.js index 13b4d87..e61d076 100644 --- a/tests/index.test.js +++ b/tests/index.test.js @@ -150,7 +150,7 @@ describe("SVRouter", () => { expect(res.end).toHaveBeenCalledWith("Pass-through matched"); }); - test("should work with chained adding of routes and pass-throughs", () => { + test("should support chaining with adding of routes and pass-throughs", () => { const req = { method: "GET", parsedURL: { pathname: "/anything" }, @@ -201,6 +201,117 @@ describe("SVRouter", () => { expect(passedThrough.config).toBe(config); }); + test("should handle middleware added with passExpressRouterMiddleware", () => { + const req = { + method: "GET", + parsedURL: { pathname: "/api/resource" }, + url: "/resource", + originalUrl: "/api/resource", + baseUrl: "", + params: null + }; + const res = { + end: jest.fn() + }; + const middleware = jest.fn((req, res, next) => { + res.end("Middleware matched"); + next(); + }); + + router.passExpressRouterMiddleware("/api", middleware); + + router(req, res, null, null, () => { + res.end("No middleware matched"); + }); + + expect(middleware).toHaveBeenCalled(); + expect(res.end).toHaveBeenCalledWith("Middleware matched"); + }); + + test("should restore req.url and req.baseUrl after middleware runs", () => { + const req = { + method: "GET", + parsedURL: { pathname: "/api/resource" }, + url: "/api/resource", + baseUrl: null, + originalUrl: null, + params: null + }; + const res = { + end: jest.fn(), + error: jest.fn() + }; + + router.passExpressRouterMiddleware("/api", (req, res, next) => { + expect(req.baseUrl).toBe("/api"); + expect(req.url).toBe("/resource"); + expect(req.originalUrl).toBe("/api/resource"); + next(); + }); + + router(req, res, null, null, () => { + expect(req.baseUrl).toBeNull(); + expect(req.url).toBe("/api/resource"); + expect(req.originalUrl).toBeNull(); + res.end("Middleware chain completed"); + }); + + expect(res.error).not.toHaveBeenCalled(); + expect(res.end).toHaveBeenCalledWith("Middleware chain completed"); + }); + + test("should call next if no middleware matches in passExpressRouterMiddleware", () => { + const req = { + method: "GET", + parsedURL: { pathname: "/nomatch/resource" }, + url: "/resource", + originalUrl: "/nomatch/resource", + baseUrl: "", + params: null + }; + const res = { + end: jest.fn() + }; + const next = jest.fn(); + + router.passExpressRouterMiddleware("/api", (req, res, next) => { + res.end("Middleware matched"); + next(); + }); + + router(req, res, null, null, next); + + expect(next).toHaveBeenCalled(); + expect(res.end).not.toHaveBeenCalled(); + }); + + test("should support chaining with passExpressRouterMiddleware", () => { + const req = { + method: "GET", + parsedURL: { pathname: "/api/resource" }, + url: "/resource", + originalUrl: "/api/resource", + baseUrl: "", + params: null + }; + const res = {}; + + router + .passExpressRouterMiddleware("/api", (req, res, next) => { + res.firstMiddlewareRan = true; + next(); + }) + .passExpressRouterMiddleware("/api", (req, res, next) => { + res.secondMiddlewareRan = true; + next(); + }); + + router(req, res, null, null, () => {}); + + expect(res.firstMiddlewareRan).toBe(true); + expect(res.secondMiddlewareRan).toBe(true); + }); + test("should throw an error if method is not a string in route", () => { expect(() => { router.route(123, "/path", () => {}); @@ -213,12 +324,18 @@ describe("SVRouter", () => { }).toThrow("The route callback must be a function."); }); - test("should throw an error if path is not a string in passRoute", () => { + test("should throw an error if path is not a string in pass", () => { expect(() => { router.pass(123, () => {}); }).toThrow("The path must be a string."); }); + test("should throw an error if path is not a string in passExpressRouterMiddleware", () => { + expect(() => { + router.passExpressRouterMiddleware(123, () => {}); + }).toThrow("The path must be a string."); + }); + test("should handle errors thrown in route callbacks gracefully", () => { const req = { method: "GET", @@ -237,4 +354,26 @@ describe("SVRouter", () => { expect(res.error).toHaveBeenCalledWith(500, expect.any(Error)); }); + + test("should correctly handle errors in middleware added with passExpressRouterMiddleware", () => { + const req = { + method: "GET", + parsedURL: { pathname: "/api/resource" }, + url: "/resource", + originalUrl: "/api/resource", + baseUrl: "", + params: null + }; + const res = { + error: jest.fn() + }; + + router.passExpressRouterMiddleware("/api", () => { + throw new Error("Middleware error"); + }); + + router(req, res, null, null, () => {}); + + expect(res.error).toHaveBeenCalledWith(500, expect.any(Error)); + }); });