feat: add compatiblity with Express's router middleware

This commit is contained in:
Dorian Niemiec 2025-01-01 21:55:32 +01:00
parent c0c69246d2
commit 17188eb870
3 changed files with 189 additions and 2 deletions

View file

@ -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). 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)_ ### _router.get(path, callback)_
An alias to the _router.route("GET", path, callback)_ function An alias to the _router.route("GET", path, callback)_ function

View file

@ -92,6 +92,38 @@ function svrouter() {
return router; 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 const methods = http.METHODS
? http.METHODS ? http.METHODS
: ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]; : ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"];
@ -104,6 +136,7 @@ function svrouter() {
router.all = (path, callback) => addRoute("*", path, callback); router.all = (path, callback) => addRoute("*", path, callback);
router.route = addRoute; router.route = addRoute;
router.pass = passRoute; router.pass = passRoute;
router.passExpressRouterMiddleware = passExpressRouterMiddleware;
return router; return router;
} }

View file

@ -150,7 +150,7 @@ describe("SVRouter", () => {
expect(res.end).toHaveBeenCalledWith("Pass-through matched"); 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 = { const req = {
method: "GET", method: "GET",
parsedURL: { pathname: "/anything" }, parsedURL: { pathname: "/anything" },
@ -201,6 +201,117 @@ describe("SVRouter", () => {
expect(passedThrough.config).toBe(config); 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", () => { test("should throw an error if method is not a string in route", () => {
expect(() => { expect(() => {
router.route(123, "/path", () => {}); router.route(123, "/path", () => {});
@ -213,12 +324,18 @@ describe("SVRouter", () => {
}).toThrow("The route callback must be a function."); }).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(() => { expect(() => {
router.pass(123, () => {}); router.pass(123, () => {});
}).toThrow("The path must be a string."); }).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", () => { test("should handle errors thrown in route callbacks gracefully", () => {
const req = { const req = {
method: "GET", method: "GET",
@ -237,4 +354,26 @@ describe("SVRouter", () => {
expect(res.error).toHaveBeenCalledWith(500, expect.any(Error)); 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));
});
}); });