10000 feat(core): exclude route from global prefix · ITCSsDeveloper/nest@47f05de · GitHub
Skip to content

Commit 47f05de

Browse files
committed
feat(core): exclude route from global prefix
New global prefix option to exclude some routes as a string or a RouteInfo
1 parent a361df4 commit 47f05de

12 files changed

Lines changed: 272 additions & 10 deletions

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { INestApplication } from '@nestjs/common';
2+
import { Test } from '@nestjs/testing';
3+
import { AppModule } from '../src/app.module';
4+
import * as request from 'supertest';
5+
import { RequestMethod } from '@nestjs/common/enums/request-method.enum'
6+
describe('Global prefix', () => {
7+
let server;
8+
let app: INestApplication;
9+
10+
beforeEach(async () => {
11+
const module = await Test.createTestingModule({
12+
imports: [AppModule],
13+
}).compile();
14+
15+
app = module.createNestApplication();
16+
});
17+
18+
it.only(`should use the global prefix`, async () => {
19+
app.setGlobalPrefix('/api/v1')
20+
21+
server = app.getHttpServer();
22+
await app.init();
23+
24+
await request(server)
25+
.get('/health')
26+
.expect(404)
27+
28+
await request(server)
29+
.get('/api/v1/health')
30+
.expect(200)
31+
});
32+
33+
it.only(`should exclude the path as string`, async () => {
34+
app.setGlobalPrefix('/api/v1', {exclude: ['/test']})
35+
36+
server = app.getHttpServer();
37+
await app.init();
38+
39+
await request(server)
40+
.get('/test')
41+
.expect(200)
42+
await request(server)
43+
.post('/test')
44+
.expect(201)
45+
46+
await request(server)
47+
.get('/api/v1/test')
48+
.expect(404)
49+
await request(server)
50+
.post('/api/v1/test')
51+
.expect(404)
52+
});
53+
54+
it.only(`should exclude the path as RouteInfo`, async () => {
55+
app.setGlobalPrefix('/api/v1', {exclude: [{path: '/health', method: RequestMethod.GET}]})
56+
57+
server = app.getHttpServer();
58+
await app.init();
59+
60+
await request(server)
61+
.get('/health')
62+
.expect(200)
63+
64+
await request(server)
65+
.get('/api/v1/health')
66+
.expect(404)
67+
});
68+
69+
it.only(`should exclude the path as a mix of string and RouteInfo`, async () => {
70+
app.setGlobalPrefix('/api/v1', {exclude: ["test", {path: '/health', method: RequestMethod.GET}]})
71+
72+
server = app.getHttpServer();
73+
await app.init();
74+
75+
await request(server)
76+
.get('/health')
77+
.expect(200)
78+
79+
await request(server)
80+
.get('/test')
81+
.expect(200)
82+
});
83+
84+
it.only(`should exclude the path with route param`, async () => {
85+
app.setGlobalPrefix('/api/v1', {exclude: ['/hello/:name']})
86+
87+
server = app.getHttpServer();
88+
await app.init();
89+
90+
await request(server)
91+
.get('/hello/foo')
92+
.expect(200)
93+
});
94+
95+
afterEach(async () => {
96+
await app.close();
97+
});
98+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Controller, Get, Post } from '@nestjs/common';
2+
3+
@Controller()
4+
export class AppController {
5+
6+
@Get('hello/:name')
7+
getHello(): string {
8+
return 'hello';
9+
}
10+
11+
@Get('health')
12+
getHealth(): string {
13+
return 'up';
14+
}
15+
16+
@Get('test')
17+
getTest(): string {
18+
return 'test';
19+
}
20+
21+
@Post('test')
22+
postTest(): string {
23+
return 'test';
24+
}
25+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Module } from '@nestjs/common';
2+
import { AppController } from './app.controller';
3+
4+
@Module({
5+
controllers: [AppController],
6+
})
7+
export class AppModule {}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"compilerOptions": {
3+
"module": "commonjs",
4+
"declaration": false,
5+
"noImplicitAny": false,
6+
"removeComments": true,
7+
"noLib": false,
8+
"emitDecoratorMetadata": true,
9+
"experimentalDecorators": true,
10+
"target": "es6",
11+
"sourceMap": true,
12+
"allowJs": true,
13+
"outDir": "./dist"
14+
},
15+
"include": [
16+
"src/**/*",
17+
"e2e/**/*"
18+
],
19+
"exclude": [
20+
"node_modules"
21+
]
22+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { RouteInfo } from './middleware';
2+
3+
/**
4+
* @publicApi
5+
*/
6+
export interface GlobalPrefixOptions {
7+
exclude?: Array<string | RouteInfo>;
8+
}

packages/common/interfaces/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ export * from './nest-microservice.interface';
2525
export * from './scope-options.interface';
2626
export * from './type.interface';
2727
export * from './websockets/web-socket-adapter.interface';
28+
export * from './global-prefix-options.interface';

packages/common/interfaces/nest-application.interface.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from './index';
1111
import { INestApplicationContext } from './nest-application-context.interface';
1212
import { WebSocketAdapter } from './websockets/web-socket-adapter.interface';
13+
import { GlobalPrefixOptions } from './global-prefix-options.interface';
1314

1415
/**
1516
* Interface defining the core NestApplication object.
@@ -67,9 +68,13 @@ export interface INestApplication extends INestApplicationContext {
6768
* Registers a prefix for every HTTP route path.
6869
*
6970
* @param {string} prefix The prefix for every HTTP route path (for example `/v1/api`)
71+
* @param {NestApplicationGlobalPrefixOptions} globalPrefixOptions GlobalPrefix options object
7072
* @returns {void}
7173
*/
72-
setGlobalPrefix(prefix: string): this;
74+
setGlobalPrefix(
75+
prefix: string,
76+
globalPrefixOptions?: GlobalPrefixOptions,
77+
): this;
7378

7479
/**
7580
* Setup Ws Adapter which will be used inside Gateways.

packages/core/application-config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import {
66
WebSocketAdapter,
77
} from '@nestjs/common';
88
import { InstanceWrapper } from './injector/instance-wrapper';
9+
import { GlobalPrefixOptions } from '@nestjs/common/interfaces';
910

1011
export class ApplicationConfig {
1112
private globalPrefix = '';
13+
private globalPrefixOptions: GlobalPrefixOptions = {};
1214
private globalPipes: PipeTransform[] = [];
1315
private globalFilters: ExceptionFilter[] = [];
1416
private globalInterceptors: NestInterceptor[] = [];
@@ -32,6 +34,14 @@ export class ApplicationConfig {
3234
return this.globalPrefix;
3335
}
3436

37+
public setGlobalPrefixOptions(options: GlobalPrefixOptions) {
38+
this.globalPrefixOptions = options;
39+
}
40+
41+
public getGlobalPrefixOptions(): GlobalPrefixOptions {
42+
return this.globalPrefixOptions;
43+
}
44+
3545
public setIoAdapter(ioAdapter: WebSocketAdapter) {
3646
this.ioAdapter = ioAdapter;
3747
}

packages/core/nest-application.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
WebSocketAdapter,
1111
} from '@nestjs/common';
1212
import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface';
13+
import { GlobalPrefixOptions } from '@nestjs/common/interfaces/global-prefix-options.interface';
1314
import { NestApplicationOptions } from '@nestjs/common/interfaces/nest-application-options.interface';
1415
import { Logger } from '@nestjs/common/services/logger.service';
1516
import { loadPackage } from '@nestjs/common/utils/load-package.util';
@@ -279,8 +280,14 @@ export class NestApplication extends NestApplicationContext
279280
});
280281
}
281282

282-
public setGlobalPrefix(prefix: string): this {
283+
public setGlobalPrefix(
284+
prefix: string,
285+
globalPrefixOptions?: GlobalPrefixOptions,
286+
): this {
283287
this.config.setGlobalPrefix(prefix);
288+
if (globalPrefixOptions) {
289+
this.config.setGlobalPrefixOptions(globalPrefixOptions);
290+
}
284291
return this;
285292
}
286293

packages/core/router/router-explorer.ts

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export class RouterExplorer {
5454
private readonly injector?: Injector,
5555
private readonly routerProxy?: RouterProxy,
5656
private readonly exceptionsFilter?: ExceptionsFilter,
57-
config?: ApplicationConfig,
57+
private readonly config?: ApplicationConfig,
5858
) {
5959
this.executionContextCreator = new RouterExecutionContext(
6060
new RouteParamsFactory(),
@@ -162,10 +162,6 @@ export class RouterExplorer {
162162
basePath,
163163
host,
164164
);
165-
path.forEach(item => {
166-
const pathStr = this.stripEndSlash(basePath) + this.stripEndSlash(item);
167-
this.logger.log(ROUTE_MAPPED_MESSAGE(pathStr, requestMethod));
168-
});
169165
});
170166
}
171167

@@ -211,8 +207,58 @@ export class RouterExplorer {
211207

212208
const hostHandler = this.applyHostFilter(host, proxy);
213209
paths.forEach(path => {
214-
const fullPath = this.stripEndSlash(basePath) + path;
215-
routerMethod(this.stripEndSlash(fullPath) || '/', hostHandler);
210+
const fullPath = this.stripEndSlash(basePath) + this.stripEndSlash(path);
211+
const unprefixedFullPath = this.removeGlobalPrefixFromPath(fullPath);
212+
213+
const isExcludedOfGlobalPrefix = this.isExcludedOfGlobalPrefix(
214+
unprefixedFullPath,
215+
requestMethod,
216+
);
217+
const finalPath = isExcludedOfGlobalPrefix
218+
? unprefixedFullPath
219+
: fullPath;
220+
221+
8026 this.logger.log(ROUTE_MAPPED_MESSAGE(finalPath, requestMethod));
222+
routerMethod(finalPath || '/', hostHandler);
223+
});
224+
}
225+
226+
public removeGlobalPrefixFromPath(path: string) {
227+
const globalPrefix = validatePath(this.config.getGlobalPrefix());
228+
return path.replace(globalPrefix, '');
229+
}
230+
231+
private isExcludedOfGlobalPrefix(path: string, requestMethod: RequestMethod) {
232+
const options = this.config.getGlobalPrefixOptions();
233+
if (!options.exclude) {
234+
return false;
235+
}
236+
237+
const excludedRouteInfos = options.exclude.map(route => {
238+
if (isString(route)) {
239+
return {
240+
path: this.validateRoutePath(route),
241+
method: RequestMethod.ALL,
242+
};
243+
}
244+
return {
245+
path: this.validateRoutePath(route.path),
246+
method: route.method,
247+
};
248+
});
249+
250+
return excludedRouteInfos.some(route => {
251+
if (route.path !== path) {
252+
return false;
253+
}
254+
if (
255+
route.method &&
256+
route.method !== RequestMethod.ALL &&
257+
route.method !== requestMethod
258+
) {
259+
return false;
260+
}
261+
return true;
216262
});
217263
}
218264

0 commit comments

Comments
 (0)