Skip to content

Commit c5d47bb

Browse files
test(core): add tests for exclude global prefix methods
2 parents c73adb6 + 52e14b0 commit c5d47bb

12 files changed

Lines changed: 345 additions & 12 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { INestApplication } from '@nestjs/common';
2+
import { RequestMethod } from '@nestjs/common/enums/request-method.enum';
3+
import { Test } from '@nestjs/testing';
4+
import * as request from 'supertest';
5+
import { AppModule } from '../src/app.module';
6+
7+
describe('Global prefix', () => {
8+
let server;
9+
let app: INestApplication;
10+
11+
beforeEach(async () => {
12+
const module = await Test.createTestingModule({
13+
imports: [AppModule],
14+
}).compile();
15+
16+
app = module.createNestApplication();
17+
});
18+
19+
it(`should use the global prefix`, async () => {
20+
app.setGlobalPrefix('/api/v1');
21+
22+
server = app.getHttpServer();
23+
await app.init();
24+
25+
await request(server).get('/health').expect(404);
26+
27+
await request(server).get('/api/v1/health').expect(200);
28+
});
29+
30+
it(`should exclude the path as string`, async () => {
31+
app.setGlobalPrefix('/api/v1', { exclude: ['/test'] });
32+
33+
server = app.getHttpServer();
34+
await app.init();
35+
36+
await request(server).get('/test').expect(200);
37+
await request(server).post('/test').expect(201);
38+
39+
await request(server).get('/api/v1/test').expect(404);
40+
await request(server).post('/api/v1/test').expect(404);
41+
});
42+
43+
it(`should exclude the path as RouteInfo`, async () => {
44+
app.setGlobalPrefix('/api/v1', {
45+
exclude: [{ path: '/health', method: RequestMethod.GET }],
46+
});
47+
48+
server = app.getHttpServer();
49+
await app.init();
50+
51+
await request(server).get('/health').expect(200);
52+
53+
await request(server).get('/api/v1/health').expect(404);
54+
});
55+
56+
it(`should only exclude the GET RequestMethod`, async () => {
57+
app.setGlobalPrefix('/api/v1', {
58+
exclude: [{ path: '/test', method: RequestMethod.GET }],
59+
});
60+
61+
server = app.getHttpServer();
62+
await app.init();
63+
64+
await request(server).get('/test').expect(200);
65+
66+
await request(server).post('/test').expect(404);
67+
68+
await request(server).post('/api/v1/test').expect(201);
69+
});
70+
71+
it(`should exclude the path as a mix of string and RouteInfo`, async () => {
72+
app.setGlobalPrefix('/api/v1', {
73+
exclude: ['test', { path: '/health', method: RequestMethod.GET }],
74+
});
75+
76+
server = app.getHttpServer();
77+
await app.init();
78+
79+
await request(server).get('/health').expect(200);
80+
81+
await request(server).get('/test').expect(200);
82+
});
83+
84+
it(`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).get('/hello/foo').expect(200);
91+
});
92+
93+
afterEach(async () => {
94+
await app.close();
95+
});
96+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Controller, Get, Post } from '@nestjs/common';
2+
3+
@Controller()
4+
export class AppController {
5+
@Get('hello/:name')
6+
getHello(): string {
7+
return 'hello';
8+
}
9+
10+
@Get('health')
11+
getHealth(): string {
12+
return 'up';
13+
}
14+
15+
@Get('test')
16+
getTest(): string {
17+
return 'test';
18+
}
19+
20+
@Post('test')
21+
postTest(): string {
22+
return 'test';
23+
}
24+
}
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: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
} from './external/cors-options.interface';
55
import { CanActivate } from './features/can-activate.interface';
66
import { NestInterceptor } from './features/nest-interceptor.interface';
7+
import { GlobalPrefixOptions } from './global-prefix-options.interface';
78
import { HttpServer } from './http/http-server.interface';
89
import {
910
ExceptionFilter,
@@ -71,9 +72,10 @@ export interface INestApplication extends INestApplicationContext {
7172
* Registers a prefix for every HTTP route path.
7273
*
7374
* @param {string} prefix The prefix for every HTTP route path (for example `/v1/api`)
75+
* @param {GlobalPrefixOptions} options Global prefix options object
7476
* @returns {this}
7577
*/
76-
setGlobalPrefix(prefix: string): this;
78+
setGlobalPrefix(prefix: string, options?: GlobalPrefixOptions): this;
7779

7880
/**
7981
* Register 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[] = [];
@@ -28,6 +30,14 @@ export class ApplicationConfig {
2830
return this.globalPrefix;
2931
}
3032

33+
public setGlobalPrefixOptions(options: GlobalPrefixOptions) {
34+
this.globalPrefixOptions = options;
35+
}
36+
37+
public getGlobalPrefixOptions(): GlobalPrefixOptions {
38+
return this.globalPrefixOptions;
39+
}
40+
3141
public setIoAdapter(ioAdapter: WebSocketAdapter) {
3242
this.ioAdapter = ioAdapter;
3343
}

packages/core/nest-application.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
CorsOptions,
1414
CorsOptionsDelegate,
1515
} from '@nestjs/common/interfaces/external/cors-options.interface';
16+
import { GlobalPrefixOptions } from '@nestjs/common/interfaces/global-prefix-options.interface';
1617
import { NestApplicationOptions } from '@nestjs/common/interfaces/nest-application-options.interface';
1718
import { Logger } from '@nestjs/common/services/logger.service';
1819
import { loadPackage } from '@nestjs/common/utils/load-package.util';
@@ -320,8 +321,11 @@ export class NestApplication
320321
return `${this.getProtocol()}://${host}:${address.port}`;
321322
}
322323

323-
public setGlobalPrefix(prefix: string): this {
324+
public setGlobalPrefix(prefix: string, options?: GlobalPrefixOptions): this {
324325
this.config.setGlobalPrefix(prefix);
326+
if (options) {
327+
this.config.setGlobalPrefixOptions(options);
328+
}
325329
return this;
326330
}
327331

packages/core/router/router-explorer.ts

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { HttpServer } from '@nestjs/common';
22
import { METHOD_METADATA, PATH_METADATA } from '@nestjs/common/constants';
33
import { RequestMethod } from '@nestjs/common/enums/request-method.enum';
44
import { InternalServerErrorException } from '@nestjs/common/exceptions';
5+
import { RouteInfo } from '@nestjs/common/interfaces';
56
import { Controller } from '@nestjs/common/interfaces/controllers/controller.interface';
67
import { Type } from '@nestjs/common/interfaces/type.interface';
78
import { Logger } from '@nestjs/common/services/logger.service';
@@ -56,7 +57,7 @@ export class RouterExplorer {
5657
private readonly injector?: Injector,
5758
private readonly routerProxy?: RouterProxy,
5859
private readonly exceptionsFilter?: ExceptionsFilter,
59-
config?: ApplicationConfig,
60+
private readonly config?: ApplicationConfig,
6061
) {
6162
this.executionContextCreator = new RouterExecutionContext(
6263
new RouteParamsFactory(),
@@ -154,7 +155,6 @@ export class RouterExplorer {
154155
host: string,
155156
) {
156157
(routePaths || []).forEach(pathProperties => {
157-
const { path, requestMethod } = pathProperties;
158158
this.applyCallbackToRouter(
159159
router,
160160
pathProperties,
@@ -163,17 +163,51 @@ export class RouterExplorer {
163163
basePath,
164164
host,
165165
);
166-
path.forEach(item => {
167-
const pathStr = this.stripEndSlash(basePath) + this.stripEndSlash(item);
168-
this.logger.log(ROUTE_MAPPED_MESSAGE(pathStr, requestMethod));
169-
});
170166
});
171167
}
172168

173169
public stripEndSlash(str: string) {
174170
return str[str.length - 1] === '/' ? str.slice(0, str.length - 1) : str;
175171
}
176172

173+
public removeGlobalPrefixFromPath(path: string) {
174+
const globalPrefix = addLeadingSlash(this.config.getGlobalPrefix());
175+
return path.replace(globalPrefix, '');
176+
}
177+
178+
public isRouteExcludedFromGlobalPrefix(
179+
path: string,
180+
requestMethod: RequestMethod,
181+
) {
182+
const options = this.config.getGlobalPrefixOptions();
183+
if (!options.exclude) {
184+
return false;
185+
}
186+
const excludedRouteInfos = options.exclude.map(
187+
(route: string | RouteInfo) => {
188+
if (isString(route)) {
189+
return {
190+
path: addLeadingSlash(route),
191+
method: RequestMethod.ALL,
192+
};
193+
}
194+
return {
195+
path: addLeadingSlash(route.path),
196+
method: route.method,
197+
};
198+
},
199+
);
200+
201+
return excludedRouteInfos.some((route: RouteInfo) => {
202+
if (route.path !== path) {
203+
return false;
204+
}
205+
return (
206+
route.method === RequestMethod.ALL || route.method === requestMethod
207+
);
208+
});
209+
}
210+
177211
private applyCallbackToRouter<T extends HttpServer>(
178212
router: T,
179213
pathProperties: RoutePathProperties,
@@ -210,10 +244,22 @@ export class RouterExplorer {
210244
requestMethod,
211245
);
212246

213-
const hostHandler = this.applyHostFilter(host, proxy);
247+
const routeHandler = this.applyHostFilter(host, proxy);
214248
paths.forEach(path => {
215-
const fullPath = this.stripEndSlash(basePath) + path;
216-
routerMethod(this.stripEndSlash(fullPath) || '/', hostHandler);
249+
let finalPath = this.stripEndSlash(basePath) + this.stripEndSlash(path);
250+
251+
const isGlobalPrefixSet = !!this.config.getGlobalPrefix();
252+
if (isGlobalPrefixSet) {
253+
const unprefixedFullPath = this.removeGlobalPrefixFromPath(finalPath);
254+
255+
const isExcludedOfGlobalPrefix = this.isRouteExcludedFromGlobalPrefix(
256+
unprefixedFullPath,
257+
requestMethod,
258+
);
259+
finalPath = isExcludedOfGlobalPrefix ? unprefixedFullPath : finalPath;
260+
}
261+
routerMethod(finalPath || '/', routeHandler);
262+
this.logger.log(ROUTE_MAPPED_MESSAGE(finalPath, requestMethod));
217263
});
218264
}
219265

0 commit comments

Comments
 (0)