Skip to content

Commit 17adc7a

Browse files
committed
fix(rest): handle overlapping paths with different vars
For example: - GET /users/{id}/products - GET /users/{id} - GET /users/{userId}/orders
1 parent 1bcdb5b commit 17adc7a

File tree

3 files changed

+86
-21
lines changed

3 files changed

+86
-21
lines changed

packages/rest/src/router/routing-table.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export class RoutingTable {
138138
* @param request
139139
*/
140140
find(request: Request): ResolvedRoute {
141-
debug('Finding route %s for %s %s', request.method, request.path);
141+
debug('Finding route for %s %s', request.method, request.path);
142142

143143
const found = this._router.find(request);
144144

packages/rest/src/router/trie.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -103,20 +103,19 @@ function traverse<T>(root: Node<T>, visitor: (node: NodeWithValue<T>) => void) {
103103
}
104104

105105
/**
106-
* Match the given key to a child of the parent node
106+
* Match the given key to one or more children of the parent node
107107
* @param key Key
108108
* @param parent Parent node
109109
*/
110-
function matchChild<T>(
111-
key: string,
112-
parent: Node<T>,
113-
): ResolvedNode<T> | undefined {
110+
function matchChildren<T>(key: string, parent: Node<T>): ResolvedNode<T>[] {
111+
const resolvedNodes: ResolvedNode<T>[] = [];
114112
// Match key literal first
115113
let child = parent.children[key];
116114
if (child) {
117-
return {
115+
resolvedNodes.push({
118116
node: child,
119-
};
117+
});
118+
return resolvedNodes;
120119
}
121120

122121
// Match templates
@@ -131,9 +130,10 @@ function matchChild<T>(
131130
const val = match[++i];
132131
resolved.params![n] = decodeURIComponent(val);
133132
}
134-
return resolved;
133+
resolvedNodes.push(resolved);
135134
}
136135
}
136+
return resolvedNodes;
137137
}
138138

139139
/**
@@ -154,10 +154,15 @@ function search<T>(
154154
const resolved: ResolvedNode<T> = {node: parent, params};
155155
if (key === undefined) return resolved;
156156

157-
const child = matchChild(key, parent);
158-
if (child) {
159-
Object.assign(params, child.params);
160-
return search(keys, index + 1, params, child.node);
157+
const children = matchChildren(key, parent);
158+
if (children.length === 0) return undefined;
159+
// There might be multiple matches, such as `/users/{id}` and `/users/{userId}`
160+
for (const child of children) {
161+
const result = search(keys, index + 1, params, child.node);
162+
if (result) {
163+
Object.assign(params, child.params);
164+
return result;
165+
}
161166
}
162167
// no matches found
163168
return undefined;

packages/rest/test/unit/router/trie-router.unit.ts

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@
33
// This file is licensed under the MIT License.
44
// License text available at https://opensource.org/licenses/MIT
55

6-
import {expect} from '@loopback/testlab';
7-
import {TrieRouter, RouteEntry} from '../../..';
6+
import {
7+
expect,
8+
ShotRequestOptions,
9+
stubExpressContext,
10+
} from '@loopback/testlab';
11+
import {TrieRouter, RouteEntry, Request} from '../../..';
812
import {anOperationSpec} from '@loopback/openapi-spec-builder';
13+
import {ResolvedRoute} from '../../../src';
914

1015
describe('trie router', () => {
1116
class TestTrieRouter extends TrieRouter {
@@ -14,10 +19,15 @@ describe('trie router', () => {
1419
}
1520
}
1621

17-
const getVerbAndPath = (r: RouteEntry) => ({verb: r.verb, path: r.path});
22+
let router: TestTrieRouter;
23+
beforeEach(givenTrieRouter);
24+
25+
const getVerbAndPath = (r?: RouteEntry) => ({
26+
verb: r && r.verb,
27+
path: r && r.path,
28+
});
1829

1930
it('adds routes to routesWithoutPathVars', () => {
20-
const router = givenTrieRouter();
2131
const staticRoutes = router.staticRoutes.map(getVerbAndPath);
2232

2333
for (const r of [
@@ -36,30 +46,76 @@ describe('trie router', () => {
3646
{verb: 'get', path: '/orders/{id}/exists'},
3747
{verb: 'get', path: '/orders/{id}'},
3848
{verb: 'delete', path: '/orders/{id}'},
49+
{verb: 'get', path: '/users/{id}'},
50+
{verb: 'get', path: '/users/{userId}/orders'},
51+
{verb: 'get', path: '/users/{id}/products'},
3952
]) {
4053
expect(staticRoutes).to.not.containEql(r);
4154
}
4255
});
4356

44-
it('list routes by order', () => {
45-
const router = givenTrieRouter();
46-
57+
it('lists routes by order', () => {
4758
expect(router.list().map(getVerbAndPath)).to.eql([
4859
{verb: 'post', path: '/orders'},
4960
{verb: 'put', path: '/orders/{id}'},
5061
{verb: 'patch', path: '/orders/{id}'},
5162
{verb: 'patch', path: '/orders'},
5263
{verb: 'get', path: '/orders/{id}/exists'},
64+
{verb: 'get', path: '/users/{userId}/orders'},
65+
{verb: 'get', path: '/users/{id}/products'},
5366
{verb: 'get', path: '/orders/count'},
5467
{verb: 'get', path: '/orders/{id}'},
68+
{verb: 'get', path: '/users/{id}'},
5569
{verb: 'get', path: '/orders'},
5670
{verb: 'delete', path: '/orders/{id}'},
5771
{verb: 'delete', path: '/orders'},
5872
]);
5973
});
6074

75+
it('finds route for GET /users/{id}', () => {
76+
const req = givenRequest({method: 'get', url: '/users/123'});
77+
const route = router.find(req);
78+
expect(getRouteInfo(route)).to.containEql({
79+
verb: 'get',
80+
path: '/users/{id}',
81+
params: {id: '123'},
82+
});
83+
});
84+
85+
it('finds route for GET /users/{id}/products', () => {
86+
const req = givenRequest({method: 'get', url: '/users/123/products'});
87+
const route = router.find(req);
88+
expect(getRouteInfo(route)).to.containEql({
89+
verb: 'get',
90+
path: '/users/{id}/products',
91+
params: {id: '123'},
92+
});
93+
});
94+
95+
it('finds route for GET /users/{userId}/orders', () => {
96+
const req = givenRequest({method: 'get', url: '/users/123/orders'});
97+
const route = router.find(req);
98+
expect(getRouteInfo(route)).to.containEql({
99+
verb: 'get',
100+
path: '/users/{userId}/orders',
101+
params: {userId: '123'},
102+
});
103+
});
104+
105+
function getRouteInfo(r?: ResolvedRoute) {
106+
return {
107+
verb: r && r.verb,
108+
path: r && r.path,
109+
params: r && r.pathParams,
110+
};
111+
}
112+
113+
function givenRequest(options?: ShotRequestOptions): Request {
114+
return stubExpressContext(options).request;
115+
}
116+
61117
function givenTrieRouter() {
62-
const router = new TestTrieRouter();
118+
router = new TestTrieRouter();
63119
for (const r of givenRoutes()) {
64120
router.add(r);
65121
}
@@ -92,6 +148,10 @@ describe('trie router', () => {
92148
addRoute('deleteAll', 'delete', '/orders');
93149
addRoute('updateAll', 'patch', '/orders');
94150

151+
addRoute('getUserById', 'get', '/users/{id}');
152+
addRoute('getUserOrders', 'get', '/users/{userId}/orders');
153+
addRoute('getUserRecommendation', 'get', '/users/{id}/products');
154+
95155
return routes;
96156
}
97157
});

0 commit comments

Comments
 (0)