Skip to content

Commit dac16e6

Browse files
Merge branch 'master' of https://github.com/nestjs/nest; branch 'controller-path-alias' of https://github.com/miZyind/nest into miZyind-controller-path-alias
2 parents 51e6b81 + bead1f4 commit dac16e6

11 files changed

Lines changed: 138 additions & 59 deletions

File tree

packages/common/decorators/core/controller.decorator.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export interface ControllerOptions extends ScopeOptions {
1818
*
1919
* @see [Routing](https://docs.nestjs.com/controllers#routing)
2020
*/
21-
path?: string;
21+
path?: string | string[];
2222

2323
/**
2424
* Specifies an optional HTTP Request host filter. When configured, methods
@@ -65,7 +65,7 @@ export function Controller(): ClassDecorator;
6565
* It defines a class that provides a context for one or more message or event
6666
* handlers.
6767
*
68-
* @param {string} prefix string that defines a `route path prefix`. The prefix
68+
* @param {string, Array} prefix string that defines a `route path prefix`. The prefix
6969
* is pre-pended to the path specified in any request decorator in the class.
7070
*
7171
* @see [Routing](https://docs.nestjs.com/controllers#routing)
@@ -74,7 +74,7 @@ export function Controller(): ClassDecorator;
7474
*
7575
* @publicApi
7676
*/
77-
export function Controller(prefix: string): ClassDecorator;
77+
export function Controller(prefix: string | string[]): ClassDecorator;
7878

7979
/**
8080
* Decorator that marks a class as a Nest controller that can receive inbound
@@ -137,12 +137,13 @@ export function Controller(options: ControllerOptions): ClassDecorator;
137137
* @publicApi
138138
*/
139139
export function Controller(
140-
prefixOrOptions?: string | ControllerOptions,
140+
prefixOrOptions?: string | string[] | ControllerOptions,
141141
): ClassDecorator {
142142
const defaultPath = '/';
143+
143144
const [path, host, scopeOptions] = isUndefined(prefixOrOptions)
144145
? [defaultPath, undefined, undefined]
145-
: isString(prefixOrOptions)
146+
: isString(prefixOrOptions) || Array.isArray(prefixOrOptions)
146147
? [prefixOrOptions, undefined, undefined]
147148
: [
148149
prefixOrOptions.path || defaultPath,

packages/common/test/utils/shared.utils.spec.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
isObject,
66
isString,
77
isConstructor,
8-
validatePath,
8+
addLeadingSlash,
99
isNil,
1010
isEmpty,
1111
isPlainObject,
@@ -81,17 +81,17 @@ describe('Shared utils', () => {
8181
expect(isConstructor('nope')).to.be.false;
8282
});
8383
});
84-
describe('validatePath', () => {
84+
describe('addLeadingSlash', () => {
8585
it('should returns validated path ("add / if not exists")', () => {
86-
expect(validatePath('nope')).to.be.eql('/nope');
86+
expect(addLeadingSlash('nope')).to.be.eql('/nope');
8787
});
8888
it('should returns same path', () => {
89-
expect(validatePath('/nope')).to.be.eql('/nope');
89+
expect(addLeadingSlash('/nope')).to.be.eql('/nope');
9090
});
9191
it('should returns empty path', () => {
92-
expect(validatePath('')).to.be.eql('');
93-
expect(validatePath(null)).to.be.eql('');
94-
expect(validatePath(undefined)).to.be.eql('');
92+
expect(addLeadingSlash('')).to.be.eql('');
93+
expect(addLeadingSlash(null)).to.be.eql('');
94+
expect(addLeadingSlash(undefined)).to.be.eql('');
9595
});
9696
});
9797
describe('isNil', () => {

packages/common/utils/shared.utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const isPlainObject = (fn: any): fn is object => {
2424
);
2525
};
2626

27-
export const validatePath = (path?: string): string =>
27+
export const addLeadingSlash = (path?: string): string =>
2828
path ? (path.charAt(0) !== '/' ? '/' + path : path) : '';
2929

3030
export const isFunction = (fn: any): boolean => typeof fn === 'function';

packages/core/middleware/middleware-module.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import {
66
} from '@nestjs/common/interfaces/middleware/middleware-configuration.interface';
77
import { NestMiddleware } from '@nestjs/common/interfaces/middleware/nest-middleware.interface';
88
import { NestModule } from '@nestjs/common/interfaces/modules/nest-module.interface';
9-
import { isUndefined, validatePath } from '@nestjs/common/utils/shared.utils';
9+
import {
10+
addLeadingSlash,
11+
isUndefined,
12+
} from '@nestjs/common/utils/shared.utils';
1013
import { ApplicationConfig } from '../application-config';
1114
import { InvalidMiddlewareException } from '../errors/exceptions/invalid-middleware.exception';
1215
import { RuntimeException } from '../errors/exceptions/runtime.exception';
@@ -265,7 +268,7 @@ export class MiddlewareModule {
265268
) => void,
266269
) {
267270
const prefix = this.config.getGlobalPrefix();
268-
const basePath = validatePath(prefix);
271+
const basePath = addLeadingSlash(prefix);
269272
if (basePath && path === '/*') {
270273
// strip slash when a wildcard is being used
271274
// and global prefix has been set

packages/core/middleware/routes-mapper.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import { RequestMethod } from '@nestjs/common';
22
import { PATH_METADATA } from '@nestjs/common/constants';
33
import { RouteInfo, Type } from '@nestjs/common/interfaces';
44
import {
5+
addLeadingSlash,
56
isString,
67
isUndefined,
7-
validatePath,
88
} from '@nestjs/common/utils/shared.utils';
9+
910
import { NestContainer } from '../injector/container';
1011
import { MetadataScanner } from '../metadata-scanner';
1112
import { RouterExplorer } from '../router/router-explorer';
@@ -64,11 +65,11 @@ export class RoutesMapper {
6465
}
6566

6667
private validateGlobalPath(path: string): string {
67-
const prefix = validatePath(path);
68+
const prefix = addLeadingSlash(path);
6869
return prefix === '/' ? '' : prefix;
6970
}
7071

7172
private validateRoutePath(path: string): string {
72-
return validatePath(path);
73+
return addLeadingSlash(path);
7374
}
7475
}

packages/core/nest-application.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { iterate } from 'iterare';
2+
import { platform } from 'os';
3+
14
import {
25
CanActivate,
36
ExceptionFilter,
@@ -13,9 +16,8 @@ import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.int
1316
import { NestApplicationOptions } from '@nestjs/common/interfaces/nest-application-options.interface';
1417
import { Logger } from '@nestjs/common/services/logger.service';
1518
import { loadPackage } from '@nestjs/common/utils/load-package.util';
16-
import { isObject, validatePath } from '@nestjs/common/utils/shared.utils';
17-
import { iterate } from 'iterare';
18-
import { platform } from 'os';
19+
import { addLeadingSlash, isObject } from '@nestjs/common/utils/shared.utils';
20+
1921
import { AbstractHttpAdapter } from './adapters';
2022
import { ApplicationConfig } from './application-config';
2123
import { MESSAGES } from './constants';
@@ -164,7 +166,7 @@ export class NestApplication
164166
await this.registerMiddleware(this.httpAdapter);
165167

166168
const prefix = this.config.getGlobalPrefix();
167-
const basePath = validatePath(prefix);
169+
const basePath = addLeadingSlash(prefix);
168170
this.routesResolver.resolve(this.httpAdapter, basePath);
169171
}
170172

packages/core/router/router-explorer.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import * as pathToRegexp from 'path-to-regexp';
2+
13
import { HttpServer } from '@nestjs/common';
24
import { METHOD_METADATA, PATH_METADATA } from '@nestjs/common/constants';
35
import { RequestMethod } from '@nestjs/common/enums/request-method.enum';
@@ -6,11 +8,11 @@ import { Controller } from '@nestjs/common/interfaces/controllers/controller.int
68
import { Type } from '@nestjs/common/interfaces/type.interface';
79
import { Logger } from '@nestjs/common/services/logger.service';
810
import {
11+
addLeadingSlash,
912
isString,
1013
isUndefined,
11-
validatePath,
1214
} from '@nestjs/common/utils/shared.utils';
13-
import * as pathToRegexp from 'path-to-regexp';
15+
1416
import { ApplicationConfig } from '../application-config';
1517
import { UnknownRequestMappingException } from '../errors/exceptions/unknown-request-mapping.exception';
1618
import { GuardsConsumer } from '../guards/guards-consumer';
@@ -87,20 +89,20 @@ export class RouterExplorer {
8789
);
8890
}
8991

90-
public extractRouterPath(
91-
metatype: Type<Controller>,
92-
prefix?: string,
93-
): string {
92+
public extractRouterPath(metatype: Type<Controller>, prefix = ''): string[] {
9493
let path = Reflect.getMetadata(PATH_METADATA, metatype);
95-
if (prefix) path = prefix + this.validateRoutePath(path);
96-
return this.validateRoutePath(path);
97-
}
9894

99-
public validateRoutePath(path: string): string {
10095
if (isUndefined(path)) {
10196
throw new UnknownRequestMappingException();
10297
}
103-
return validatePath(path);
98+
99+
if (Array.isArray(path)) {
100+
path = path.map(p => prefix + addLeadingSlash(p));
101+
} else {
102+
path = [prefix + addLeadingSlash(path)];
103+
}
104+
105+
return path.map(p => addLeadingSlash(p));
104106
}
105107

106108
public scanForPaths(
@@ -134,8 +136,8 @@ export class RouterExplorer {
134136
targetCallback,
135137
);
136138
const path = isString(routePath)
137-
? [this.validateRoutePath(routePath)]
138-
: routePath.map(p => this.validateRoutePath(p));
139+
? [addLeadingSlash(routePath)]
140+
: routePath.map(p => addLeadingSlash(p));
139141
return {
140142
path,
141143
requestMethod,

packages/core/router/routes-resolver.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,24 +60,27 @@ export class RoutesResolver implements Resolver {
6060
const { metatype } = instanceWrapper;
6161

6262
const host = this.getHostMetadata(metatype);
63-
const path = this.routerExplorer.extractRouterPath(
63+
const paths = this.routerExplorer.extractRouterPath(
6464
metatype as Type<any>,
6565
basePath,
6666
);
6767
const controllerName = metatype.name;
68-
this.logger.log(
69-
CONTROLLER_MAPPING_MESSAGE(
70-
controllerName,
71-
this.routerExplorer.stripEndSlash(path),
72-
),
73-
);
74-
this.routerExplorer.explore(
75-
instanceWrapper,
76-
moduleName,
77-
applicationRef,
78-
path,
79-
host,
80-
);
68+
69+
paths.forEach(path => {
70+
this.logger.log(
71+
CONTROLLER_MAPPING_MESSAGE(
72+
controllerName,
73+
this.routerExplorer.stripEndSlash(path),
74+
),
75+
);
76+
this.routerExplorer.explore(
77+
instanceWrapper,
78+
moduleName,
79+
applicationRef,
80+
path,
81+
host,
82+
);
83+
});
8184
});
8285
}
8386

packages/core/test/router/router-explorer.spec.ts

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,21 @@ describe('RouterExplorer', () => {
3232
public getTestUsingArray() {}
3333
}
3434

35+
@Controller(['global', 'global-alias'])
36+
class TestRouteAlias {
37+
@Get('test')
38+
public getTest() {}
39+
40+
@Post('test')
41+
public postTest() {}
42+
43+
@All('another-test')
44+
public anotherTest() {}
45+
46+
@Get(['foo', 'bar'])
47+
public getTestUsingArray() {}
48+
}
49+
3550
let routerBuilder: RouterExplorer;
3651
let injector: Injector;
3752
let exceptionsFilter: RouterExceptionFilters;
@@ -70,6 +85,22 @@ describe('RouterExplorer', () => {
7085
expect(paths[2].requestMethod).to.eql(RequestMethod.ALL);
7186
expect(paths[3].requestMethod).to.eql(RequestMethod.GET);
7287
});
88+
89+
it('should method return expected list of route paths alias', () => {
90+
const paths = routerBuilder.scanForPaths(new TestRouteAlias());
91+
92+
expect(paths).to.have.length(4);
93+
94+
expect(paths[0].path).to.eql(['/test']);
95+
expect(paths[1].path).to.eql(['/test']);
96+
expect(paths[2].path).to.eql(['/another-test']);
97+
expect(paths[3].path).to.eql(['/foo', '/bar']);
98+
99+
expect(paths[0].requestMethod).to.eql(RequestMethod.GET);
100+
expect(paths[1].requestMethod).to.eql(RequestMethod.POST);
101+
expect(paths[2].requestMethod).to.eql(RequestMethod.ALL);
102+
expect(paths[3].requestMethod).to.eql(RequestMethod.GET);
103+
});
73104
});
74105

75106
describe('exploreMethodMetadata', () => {
@@ -87,6 +118,20 @@ describe('RouterExplorer', () => {
87118
expect(route.requestMethod).to.eql(RequestMethod.GET);
88119
});
89120

121+
it('should method return expected object which represent single route with alias', () => {
122+
const instance = new TestRouteAlias();
123+
const instanceProto = Object.getPrototypeOf(instance);
124+
125+
const route = routerBuilder.exploreMethodMetadata(
126+
new TestRouteAlias(),
127+
instanceProto,
128+
'getTest',
129+
);
130+
131+
expect(route.path).to.eql(['/test']);
132+
expect(route.requestMethod).to.eql(RequestMethod.GET);
133+
});
134+
90135
it('should method return expected object which represent multiple routes', () => {
91136
const instance = new TestRoute();
92137
const instanceProto = Object.getPrototypeOf(instance);
@@ -100,6 +145,20 @@ describe('RouterExplorer', () => {
100145
expect(route.path).to.eql(['/foo', '/bar']);
101146
expect(route.requestMethod).to.eql(RequestMethod.GET);
102147
});
148+
149+
it('should method return expected object which represent multiple routes with alias', () => {
150+
const instance = new TestRouteAlias();
151+
const instanceProto = Object.getPrototypeOf(instance);
152+
153+
const route = routerBuilder.exploreMethodMetadata(
154+
new TestRouteAlias(),
155+
instanceProto,
156+
'getTestUsingArray',
157+
);
158+
159+
expect(route.path).to.eql(['/foo', '/bar']);
160+
expect(route.requestMethod).to.eql(RequestMethod.GET);
161+
});
103162
});
104163

105164
describe('applyPathsToRouterProxy', () => {
@@ -130,14 +189,20 @@ describe('RouterExplorer', () => {
130189

131190
describe('extractRouterPath', () => {
132191
it('should return expected path', () => {
133-
expect(routerBuilder.extractRouterPath(TestRoute)).to.be.eql('/global');
134-
expect(routerBuilder.extractRouterPath(TestRoute, '/module')).to.be.eql(
192+
expect(routerBuilder.extractRouterPath(TestRoute)).to.be.eql(['/global']);
193+
expect(routerBuilder.extractRouterPath(TestRoute, '/module')).to.be.eql([
135194
'/module/global',
136-
);
195+
]);
137196
});
138197

139-
it('should throw it a there is a bad path expected path', () => {
140-
expect(() => routerBuilder.validateRoutePath(undefined)).to.throw();
198+
it('should return expected path with alias', () => {
199+
expect(routerBuilder.extractRouterPath(TestRouteAlias)).to.be.eql([
200+
'/global',
201+
'/global-alias',
202+
]);
203+
expect(
204+
routerBuilder.extractRouterPath(TestRouteAlias, '/module'),
205+
).to.be.eql(['/module/global', '/module/global-alias']);
141206
});
142207
});
143208

packages/core/test/router/routes-resolver.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ describe('RoutesResolver', () => {
8888

8989
sinon
9090
.stub((routesResolver as any).routerExplorer, 'extractRouterPath')
91-
.callsFake(() => '');
91+
.callsFake(() => ['']);
9292
routesResolver.registerRouters(routes, moduleName, '', appInstance);
9393

9494
expect(exploreSpy.called).to.be.true;
@@ -114,7 +114,7 @@ describe('RoutesResolver', () => {
114114

115115
sinon
116116
.stub((routesResolver as any).routerExplorer, 'extractRouterPath')
117-
.callsFake(() => '');
117+
.callsFake(() => ['']);
118118
routesResolver.registerRouters(routes, moduleName, '', appInstance);
119119

120120
expect(exploreSpy.called).to.be.true;

0 commit comments

Comments
 (0)