feat!: add support for relative routes in nested SVRouter routers
This commit is contained in:
parent
a075c975af
commit
7af60aab0a
3 changed files with 143 additions and 11 deletions
|
@ -64,7 +64,9 @@ The _callback_ parameter has these arguments of the SVR.JS mod callback:
|
||||||
- _config_ - the SVR.JS configuration object
|
- _config_ - the SVR.JS configuration object
|
||||||
- _next_ - the callback which passes the execution to other routes, SVR.JS mods and SVR.JS internal handlers.
|
- _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).
|
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)
|
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:
|
The _callback_ parameter has these arguments of the SVR.JS mod callback:
|
||||||
- _req_ - the SVR.JS request object
|
- _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
|
- _config_ - the SVR.JS configuration object
|
||||||
- _next_ - the callback which passes the execution to other routes, SVR.JS mods and SVR.JS internal handlers.
|
- _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).
|
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)_
|
### _router.passExpressRouterMiddleware([path, ]middleware)_
|
||||||
|
|
37
src/index.js
37
src/index.js
|
@ -3,19 +3,25 @@ const http = require("http");
|
||||||
|
|
||||||
function svrouter() {
|
function svrouter() {
|
||||||
const routes = [];
|
const routes = [];
|
||||||
|
|
||||||
const router = (req, res, logFacilities, config, next) => {
|
const router = (req, res, logFacilities, config, next) => {
|
||||||
let index = 0;
|
let index = 0;
|
||||||
let previousReqParams = req.params;
|
let previousReqParams = req.params;
|
||||||
|
let previousReqSvrouterBase = req.svrouterBase;
|
||||||
let paramsPresent = false;
|
let paramsPresent = false;
|
||||||
|
let passPathPresent = false;
|
||||||
|
|
||||||
const nextRoute = () => {
|
const nextRoute = () => {
|
||||||
if (paramsPresent) req.params = previousReqParams;
|
if (paramsPresent) req.params = previousReqParams;
|
||||||
|
if (passPathPresent) req.svrouterBase = previousReqSvrouterBase;
|
||||||
let currentRoute = routes[index++];
|
let currentRoute = routes[index++];
|
||||||
let currentMatch =
|
let currentMatch =
|
||||||
currentRoute && currentRoute.pathFunction
|
currentRoute && currentRoute.pathFunction
|
||||||
? currentRoute.pathFunction(req.parsedURL.pathname)
|
? currentRoute.pathFunction(
|
||||||
: false;
|
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 (
|
while (
|
||||||
currentRoute &&
|
currentRoute &&
|
||||||
((currentRoute.method && req.method != currentRoute.method) ||
|
((currentRoute.method && req.method != currentRoute.method) ||
|
||||||
|
@ -24,17 +30,26 @@ function svrouter() {
|
||||||
currentRoute = routes[index++];
|
currentRoute = routes[index++];
|
||||||
currentMatch =
|
currentMatch =
|
||||||
currentRoute && currentRoute.pathFunction
|
currentRoute && currentRoute.pathFunction
|
||||||
? currentRoute.pathFunction(req.parsedURL.pathname)
|
? currentRoute.pathFunction(
|
||||||
|
req.parsedURL.pathname.substring(
|
||||||
|
(req.svrouterBase ? req.svrouterBase : "").length
|
||||||
|
)
|
||||||
|
)
|
||||||
: false;
|
: false;
|
||||||
}
|
}
|
||||||
if (currentRoute && currentRoute.callback) {
|
if (currentRoute && currentRoute.callback) {
|
||||||
try {
|
try {
|
||||||
paramsPresent = Boolean(currentMatch.params);
|
paramsPresent = Boolean(currentMatch.params);
|
||||||
|
passPathPresent = Boolean(currentRoute.passPath);
|
||||||
if (paramsPresent)
|
if (paramsPresent)
|
||||||
req.params =
|
req.params =
|
||||||
currentMatch && currentMatch.params
|
currentMatch && currentMatch.params
|
||||||
? currentMatch.params
|
? currentMatch.params
|
||||||
: Object.create(null);
|
: Object.create(null);
|
||||||
|
if (passPathPresent)
|
||||||
|
req.svrouterBase =
|
||||||
|
(req.svrouterBase ? req.svrouterBase : "") +
|
||||||
|
currentRoute.passPath;
|
||||||
currentRoute.callback(req, res, logFacilities, config, nextRoute);
|
currentRoute.callback(req, res, logFacilities, config, nextRoute);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.error(500, err);
|
res.error(500, err);
|
||||||
|
@ -59,7 +74,8 @@ function svrouter() {
|
||||||
routes.push({
|
routes.push({
|
||||||
method: method === "*" ? null : method.toUpperCase(),
|
method: method === "*" ? null : method.toUpperCase(),
|
||||||
pathFunction: match(path),
|
pathFunction: match(path),
|
||||||
callback: callback
|
callback: callback,
|
||||||
|
passPath: null
|
||||||
});
|
});
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
@ -86,7 +102,8 @@ function svrouter() {
|
||||||
}
|
}
|
||||||
: false
|
: false
|
||||||
: () => true,
|
: () => true,
|
||||||
callback: realCallback
|
callback: realCallback,
|
||||||
|
passPath: realPath
|
||||||
});
|
});
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
@ -105,16 +122,20 @@ function svrouter() {
|
||||||
const previousReqBaseUrl = req.baseUrl;
|
const previousReqBaseUrl = req.baseUrl;
|
||||||
const previousReqUrl = req.url;
|
const previousReqUrl = req.url;
|
||||||
const previousReqOriginalUrl = req.originalUrl;
|
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.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 = "/";
|
if (!req.url) req.url = "/";
|
||||||
|
req.svrouterBase = undefined;
|
||||||
|
|
||||||
const nextCallback = () => {
|
const nextCallback = () => {
|
||||||
req.baseUrl = previousReqBaseUrl;
|
req.baseUrl = previousReqBaseUrl;
|
||||||
req.url = previousReqUrl;
|
req.url = previousReqUrl;
|
||||||
req.originalUrl = previousReqOriginalUrl;
|
req.originalUrl = previousReqOriginalUrl;
|
||||||
|
req.svrouterBase = previousReqSvrouterBase;
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -228,7 +228,7 @@ describe("SVRouter", () => {
|
||||||
expect(res.end).toHaveBeenCalledWith("Middleware matched");
|
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 = {
|
const req = {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
parsedURL: { pathname: "/api/resource" },
|
parsedURL: { pathname: "/api/resource" },
|
||||||
|
@ -312,6 +312,113 @@ describe("SVRouter", () => {
|
||||||
expect(res.secondMiddlewareRan).toBe(true);
|
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", () => {
|
test("should throw an error if method is not a string in route", () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
router.route(123, "/path", () => {});
|
router.route(123, "/path", () => {});
|
||||||
|
|
Loading…
Reference in a new issue