Skip to content

Commit c7ac493

Browse files
Merge pull request #10390 from CodyTseng/fix-middleware-global-prefix
fix(core): let the middleware can get the params in the global prefix
2 parents 7b7b8b9 + 6d64d91 commit c7ac493

19 files changed

+300
-87
lines changed

integration/hello-world/e2e/middleware-with-versioning.spec.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,26 @@ describe('Middleware', () => {
7777
});
7878
});
7979

80+
describe('when using default URI versioning with the global prefix', () => {
81+
beforeEach(async () => {
82+
app = await createAppWithVersioning(
83+
{
84+
type: VersioningType.URI,
85+
defaultVersion: VERSION_NEUTRAL,
86+
},
87+
async (app: INestApplication) => {
88+
app.setGlobalPrefix('api');
89+
},
90+
);
91+
});
92+
93+
it(`forRoutes({ path: '/versioned', version: '1', method: RequestMethod.ALL })`, () => {
94+
return request(app.getHttpServer())
95+
.get('/api/v1/versioned')
96+
.expect(200, VERSIONED_VALUE);
97+
});
98+
});
99+
80100
describe('when using HEADER versioning', () => {
81101
beforeEach(async () => {
82102
app = await createAppWithVersioning({
@@ -133,6 +153,7 @@ describe('Middleware', () => {
133153

134154
async function createAppWithVersioning(
135155
versioningOptions: VersioningOptions,
156+
beforeInit?: (app: INestApplication) => Promise<void>,
136157
): Promise<INestApplication> {
137158
const app = (
138159
await Test.createTestingModule({
@@ -141,6 +162,9 @@ async function createAppWithVersioning(
141162
).createNestApplication();
142163

143164
app.enableVersioning(versioningOptions);
165+
if (beforeInit) {
166+
await beforeInit(app);
167+
}
144168
await app.init();
145169

146170
return app;

integration/nest-application/global-prefix/e2e/global-prefix.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,17 @@ describe('Global prefix', () => {
119119
await request(server).get('/api/v1/middleware/foo').expect(404);
120120
});
121121

122+
it(`should get the params in the global prefix`, async () => {
123+
app.setGlobalPrefix('/api/:tenantId');
124+
125+
server = app.getHttpServer();
126+
await app.init();
127+
128+
await request(server)
129+
.get('/api/test/params')
130+
.expect(200, { '0': 'params', tenantId: 'test' });
131+
});
132+
122133
afterEach(async () => {
123134
await app.close();
124135
});

integration/nest-application/global-prefix/src/app.controller.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ export class AppController {
77
return 'Hello: ' + req.extras?.data;
88
}
99

10+
@Get('params')
11+
getParams(@Req() req): any {
12+
return req.middlewareParams;
13+
}
14+
1015
@Get('health')
1116
getHealth(): string {
1217
return 'up';

integration/nest-application/global-prefix/src/app.module.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ export class AppModule {
2222
req.extras = { data: 'Data attached in middleware' };
2323
next();
2424
})
25+
.forRoutes({ path: '*', method: RequestMethod.GET })
26+
.apply((req, res, next) => {
27+
req.middlewareParams = req.params;
28+
next();
29+
})
2530
.forRoutes({ path: '*', method: RequestMethod.GET });
2631
}
2732
}

packages/common/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const ENHANCER_KEY_TO_SUBTYPE_MAP = {
3535
} as const;
3636

3737
export type EnhancerSubtype =
38-
typeof ENHANCER_KEY_TO_SUBTYPE_MAP[keyof typeof ENHANCER_KEY_TO_SUBTYPE_MAP];
38+
(typeof ENHANCER_KEY_TO_SUBTYPE_MAP)[keyof typeof ENHANCER_KEY_TO_SUBTYPE_MAP];
3939

4040
export const RENDER_METADATA = '__renderTemplate__';
4141
export const HTTP_CODE_METADATA = '__httpCode__';

packages/common/exceptions/http.exception.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,9 @@ export class HttpException extends Error {
9999
) {
100100
this.message = (this.response as Record<string, any>).message;
101101
} else if (this.constructor) {
102-
this.message = this.constructor.name
103-
.match(/[A-Z][a-z]+|[0-9]+/g)
104-
?.join(' ') ?? 'Error';
102+
this.message =
103+
this.constructor.name.match(/[A-Z][a-z]+|[0-9]+/g)?.join(' ') ??
104+
'Error';
105105
}
106106
}
107107

packages/core/injector/injector.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,9 @@ export class Injector {
353353
}
354354

355355
public reflectConstructorParams<T>(type: Type<T>): any[] {
356-
const paramtypes = [...(Reflect.getMetadata(PARAMTYPES_METADATA, type) || [])];
356+
const paramtypes = [
357+
...(Reflect.getMetadata(PARAMTYPES_METADATA, type) || []),
358+
];
357359
const selfParams = this.reflectSelfParams<T>(type);
358360

359361
selfParams.forEach(({ index, param }) => (paramtypes[index] = param));

packages/core/middleware/middleware-module.ts

Lines changed: 10 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
import { HttpServer, Logger, VersioningType } from '@nestjs/common';
1+
import { HttpServer, Logger } from '@nestjs/common';
22
import { RequestMethod } from '@nestjs/common/enums/request-method.enum';
33
import {
44
MiddlewareConfiguration,
55
NestMiddleware,
66
RouteInfo,
77
} from '@nestjs/common/interfaces/middleware';
88
import { NestApplicationContextOptions } from '@nestjs/common/interfaces/nest-application-context-options.interface';
9-
import {
10-
addLeadingSlash,
11-
isUndefined,
12-
} from '@nestjs/common/utils/shared.utils';
9+
import { isUndefined } from '@nestjs/common/utils/shared.utils';
1310
import { ApplicationConfig } from '../application-config';
1411
import { InvalidMiddlewareException } from '../errors/exceptions/invalid-middleware.exception';
1512
import { RuntimeException } from '../errors/exceptions/runtime.exception';
@@ -26,13 +23,13 @@ import {
2623
MiddlewareEntrypointMetadata,
2724
} from '../inspector/interfaces/entrypoint.interface';
2825
import { REQUEST_CONTEXT_ID } from '../router/request/request-constants';
29-
import { RoutePathFactory } from '../router/route-path-factory';
3026
import { RouterExceptionFilters } from '../router/router-exception-filters';
3127
import { RouterProxy } from '../router/router-proxy';
32-
import { isRequestMethodAll, isRouteExcluded } from '../router/utils';
28+
import { isRequestMethodAll } from '../router/utils';
3329
import { MiddlewareBuilder } from './builder';
3430
import { MiddlewareContainer } from './container';
3531
import { MiddlewareResolver } from './resolver';
32+
import { RouteInfoPathExtractor } from './route-info-path-extractor';
3633
import { RoutesMapper } from './routes-mapper';
3734

3835
export class MiddlewareModule<
@@ -46,13 +43,11 @@ export class MiddlewareModule<
4643
private routerExceptionFilter: RouterExceptionFilters;
4744
private routesMapper: RoutesMapper;
4845
private resolver: MiddlewareResolver;
49-
private config: ApplicationConfig;
5046
private container: NestContainer;
5147
private httpAdapter: HttpServer;
5248
private graphInspector: GraphInspector;
5349
private appOptions: TAppOptions;
54-
55-
constructor(private readonly routePathFactory: RoutePathFactory) {}
50+
private routeInfoPathExtractor: RouteInfoPathExtractor;
5651

5752
public async register(
5853
middlewareContainer: MiddlewareContainer,
@@ -73,8 +68,7 @@ export class MiddlewareModule<
7368
);
7469
this.routesMapper = new RoutesMapper(container);
7570
this.resolver = new MiddlewareResolver(middlewareContainer, injector);
76-
77-
this.config = config;
71+
this.routeInfoPathExtractor = new RouteInfoPathExtractor(config);
7872
this.injector = injector;
7973
this.container = container;
8074
this.httpAdapter = httpAdapter;
@@ -307,44 +301,18 @@ export class MiddlewareModule<
307301

308302
private async registerHandler(
309303
applicationRef: HttpServer,
310-
{ path, method, version }: RouteInfo,
304+
routeInfo: RouteInfo,
311305
proxy: <TRequest, TResponse>(
312306
req: TRequest,
313307
res: TResponse,
314308
next: () => void,
315309
) => void,
316310
) {
317-
const prefix = this.config.getGlobalPrefix();
318-
const excludedRoutes = this.config.getGlobalPrefixOptions().exclude;
319-
const isAWildcard = ['*', '/*', '(.*)', '/(.*)'].includes(path);
320-
if (
321-
(Array.isArray(excludedRoutes) &&
322-
isRouteExcluded(excludedRoutes, path, method)) ||
323-
isAWildcard
324-
) {
325-
path = addLeadingSlash(path);
326-
} else {
327-
const basePath = addLeadingSlash(prefix);
328-
if (basePath?.endsWith('/') && path?.startsWith('/')) {
329-
// strip slash when a wildcard is being used
330-
// and global prefix has been set
331-
path = path?.slice(1);
332-
}
333-
path = basePath + path;
334-
}
335-
336-
const applicationVersioningConfig = this.config.getVersioning();
337-
if (version && applicationVersioningConfig.type === VersioningType.URI) {
338-
const versionPrefix = this.routePathFactory.getVersionPrefix(
339-
applicationVersioningConfig,
340-
);
341-
path = `/${versionPrefix}${version.toString()}${path}`;
342-
}
343-
311+
const { method } = routeInfo;
312+
const paths = this.routeInfoPathExtractor.extractPathsFrom(routeInfo);
344313
const isMethodAll = isRequestMethodAll(method);
345314
const requestMethod = RequestMethod[method];
346315
const router = await applicationRef.createMiddlewareFactory(method);
347-
348316
const middlewareFunction = isMethodAll
349317
? proxy
350318
: <TRequest, TResponse>(
@@ -357,8 +325,7 @@ export class MiddlewareModule<
357325
}
358326
return next();
359327
};
360-
361-
router(path, middlewareFunction);
328+
paths.forEach(path => router(path, middlewareFunction));
362329
}
363330

364331
private getContextId(request: unknown, isTreeDurable: boolean): ContextId {
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { VersioningType } from '@nestjs/common';
2+
import { RouteInfo } from '@nestjs/common/interfaces';
3+
import {
4+
addLeadingSlash,
5+
stripEndSlash,
6+
} from '@nestjs/common/utils/shared.utils';
7+
import { ApplicationConfig } from '../application-config';
8+
import { isRouteExcluded } from '../router/utils';
9+
import { RoutePathFactory } from './../router/route-path-factory';
10+
11+
export class RouteInfoPathExtractor {
12+
private routePathFactory: RoutePathFactory;
13+
14+
constructor(private readonly applicationConfig: ApplicationConfig) {
15+
this.routePathFactory = new RoutePathFactory(applicationConfig);
16+
}
17+
18+
public extractPathsFrom({ path, method, version }: RouteInfo) {
19+
const prefixPath = stripEndSlash(
20+
addLeadingSlash(this.applicationConfig.getGlobalPrefix()),
21+
);
22+
const excludedRoutes =
23+
this.applicationConfig.getGlobalPrefixOptions().exclude;
24+
25+
const applicationVersioningConfig = this.applicationConfig.getVersioning();
26+
let versionPath = '';
27+
if (version && applicationVersioningConfig?.type === VersioningType.URI) {
28+
const versionPrefix = this.routePathFactory.getVersionPrefix(
29+
applicationVersioningConfig,
30+
);
31+
versionPath = addLeadingSlash(versionPrefix + version.toString());
32+
}
33+
34+
const isAWildcard = ['*', '/*', '/*/', '(.*)', '/(.*)'].includes(path);
35+
if (isAWildcard) {
36+
return Array.isArray(excludedRoutes)
37+
? [
38+
prefixPath + versionPath + addLeadingSlash(path),
39+
...excludedRoutes.map(
40+
route => versionPath + addLeadingSlash(route.path),
41+
),
42+
]
43+
: [prefixPath + versionPath + addLeadingSlash(path)];
44+
}
45+
46+
if (
47+
Array.isArray(excludedRoutes) &&
48+
isRouteExcluded(excludedRoutes, path, method)
49+
) {
50+
return [versionPath + addLeadingSlash(path)];
51+
}
52+
53+
return [prefixPath + versionPath + addLeadingSlash(path)];
54+
}
55+
}

packages/core/middleware/utils.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,33 @@
11
import { RequestMethod } from '@nestjs/common';
22
import { HttpServer, RouteInfo, Type } from '@nestjs/common/interfaces';
3-
import { isFunction } from '@nestjs/common/utils/shared.utils';
3+
import {
4+
addLeadingSlash,
5+
isFunction,
6+
isString,
7+
} from '@nestjs/common/utils/shared.utils';
48
import { iterate } from 'iterare';
59
import * as pathToRegexp from 'path-to-regexp';
610
import { v4 as uuid } from 'uuid';
711
import { ExcludeRouteMetadata } from '../router/interfaces/exclude-route-metadata.interface';
812
import { isRouteExcluded } from '../router/utils';
913

1014
export const mapToExcludeRoute = (
11-
routes: RouteInfo[],
15+
routes: (string | RouteInfo)[],
1216
): ExcludeRouteMetadata[] => {
13-
return routes.map(({ path, method }) => ({
14-
pathRegex: pathToRegexp(path),
15-
requestMethod: method,
16-
path,
17-
}));
17+
return routes.map(route => {
18+
if (isString(route)) {
19+
return {
20+
path: route,
21+
requestMethod: RequestMethod.ALL,
22+
pathRegex: pathToRegexp(addLeadingSlash(route)),
23+
};
24+
}
25+
return {
26+
path: route.path,
27+
requestMethod: route.method,
28+
pathRegex: pathToRegexp(addLeadingSlash(route.path)),
29+
};
30+
});
1831
};
1932

2033
export const filterMiddleware = <T extends Function | Type<any> = any>(

0 commit comments

Comments
 (0)