mirror of
https://github.com/nestjs/nest.git
synced 2026-02-21 23:11:44 +00:00
fix(core): apply middleware to versioned controllers (ctrl-level)
This commit is contained in:
@@ -418,4 +418,67 @@ describe('URI Versioning', () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
// ======================================================================== //
|
||||
describe.only('with middleware applied', () => {
|
||||
before(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
app.enableVersioning({
|
||||
type: VersioningType.URI,
|
||||
defaultVersion: '1',
|
||||
});
|
||||
await app.init();
|
||||
});
|
||||
|
||||
describe('GET /middleware', () => {
|
||||
it('should return "Hello from middleware function!"', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/v1/middleware')
|
||||
.expect(200)
|
||||
.expect('Hello from middleware function!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /middleware/override', () => {
|
||||
it('should return "Hello from middleware function!"', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/v2/middleware/override')
|
||||
.expect(200)
|
||||
.expect('Hello from middleware function!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /middleware/multiple', () => {
|
||||
it('should return "Hello from middleware function!" (v1)', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/v1/middleware/multiple')
|
||||
.expect(200)
|
||||
.expect('Hello from middleware function!');
|
||||
});
|
||||
|
||||
it('should return "Hello from middleware function!" (v2)', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/v2/middleware/multiple')
|
||||
.expect(200)
|
||||
.expect('Hello from middleware function!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /middleware/neutral', () => {
|
||||
it('should return "Hello from middleware function!"', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/middleware/neutral')
|
||||
.expect(200)
|
||||
.expect('Hello from middleware function!');
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MiddlewareConsumer, Module } from '@nestjs/common';
|
||||
import { AppV1Controller } from './app-v1.controller';
|
||||
import { AppV2Controller } from './app-v2.controller';
|
||||
import { MiddlewareController } from './middleware.controller';
|
||||
import { MultipleMiddlewareVersionController } from './multiple-middleware.controller';
|
||||
import { MultipleVersionController } from './multiple.controller';
|
||||
import { NoVersioningController } from './no-versioning.controller';
|
||||
import { VersionNeutralMiddlewareController } from './neutral-middleware.controller';
|
||||
import { VersionNeutralController } from './neutral.controller';
|
||||
import { OverrideController } from './override.controller';
|
||||
import { NoVersioningController } from './no-versioning.controller';
|
||||
import { OverridePartialController } from './override-partial.controller';
|
||||
import { OverrideController } from './override.controller';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
@@ -17,6 +20,19 @@ import { OverridePartialController } from './override-partial.controller';
|
||||
VersionNeutralController,
|
||||
OverrideController,
|
||||
OverridePartialController,
|
||||
MiddlewareController,
|
||||
MultipleMiddlewareVersionController,
|
||||
VersionNeutralMiddlewareController,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer
|
||||
.apply((req, res) => res.end('Hello from middleware function!'))
|
||||
.forRoutes(
|
||||
MiddlewareController,
|
||||
MultipleMiddlewareVersionController,
|
||||
VersionNeutralMiddlewareController,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
18
integration/versioning/src/middleware.controller.ts
Normal file
18
integration/versioning/src/middleware.controller.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Controller, Get, Version } from '@nestjs/common';
|
||||
|
||||
@Controller({
|
||||
path: 'middleware',
|
||||
version: '1',
|
||||
})
|
||||
export class MiddlewareController {
|
||||
@Get('/')
|
||||
hello() {
|
||||
return 'Hello from "MiddlewareController"!';
|
||||
}
|
||||
|
||||
@Version('2')
|
||||
@Get('/override')
|
||||
hellov2() {
|
||||
return 'Hello from "MiddlewareController"!';
|
||||
}
|
||||
}
|
||||
12
integration/versioning/src/multiple-middleware.controller.ts
Normal file
12
integration/versioning/src/multiple-middleware.controller.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
|
||||
@Controller({
|
||||
version: ['1', '2'],
|
||||
path: 'middleware',
|
||||
})
|
||||
export class MultipleMiddlewareVersionController {
|
||||
@Get('/multiple')
|
||||
multiple() {
|
||||
return 'Multiple Versions 1 or 2';
|
||||
}
|
||||
}
|
||||
12
integration/versioning/src/neutral-middleware.controller.ts
Normal file
12
integration/versioning/src/neutral-middleware.controller.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Controller, Get, VERSION_NEUTRAL } from '@nestjs/common';
|
||||
|
||||
@Controller({
|
||||
path: 'middleware',
|
||||
version: VERSION_NEUTRAL,
|
||||
})
|
||||
export class VersionNeutralMiddlewareController {
|
||||
@Get('/neutral')
|
||||
neutral() {
|
||||
return 'Neutral';
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,7 @@ export class MiddlewareModule<
|
||||
config,
|
||||
appRef,
|
||||
);
|
||||
this.routesMapper = new RoutesMapper(container);
|
||||
this.routesMapper = new RoutesMapper(container, config);
|
||||
this.resolver = new MiddlewareResolver(middlewareContainer, injector);
|
||||
this.routeInfoPathExtractor = new RouteInfoPathExtractor(config);
|
||||
this.injector = injector;
|
||||
|
||||
@@ -1,35 +1,48 @@
|
||||
import { MODULE_PATH, PATH_METADATA } from '@nestjs/common/constants';
|
||||
import { RouteInfo, Type } from '@nestjs/common/interfaces';
|
||||
import {
|
||||
MODULE_PATH,
|
||||
PATH_METADATA,
|
||||
VERSION_METADATA,
|
||||
} from '@nestjs/common/constants';
|
||||
import {
|
||||
RouteInfo,
|
||||
Type,
|
||||
VERSION_NEUTRAL,
|
||||
VersionValue,
|
||||
} from '@nestjs/common/interfaces';
|
||||
import {
|
||||
addLeadingSlash,
|
||||
isString,
|
||||
isUndefined,
|
||||
} from '@nestjs/common/utils/shared.utils';
|
||||
import { ApplicationConfig } from '../application-config';
|
||||
import { NestContainer } from '../injector/container';
|
||||
import { Module } from '../injector/module';
|
||||
import { MetadataScanner } from '../metadata-scanner';
|
||||
import { PathsExplorer } from '../router/paths-explorer';
|
||||
import { PathsExplorer, RouteDefinition } from '../router/paths-explorer';
|
||||
import { targetModulesByContainer } from '../router/router-module';
|
||||
|
||||
export class RoutesMapper {
|
||||
private readonly pathsExplorer: PathsExplorer;
|
||||
|
||||
constructor(private readonly container: NestContainer) {
|
||||
constructor(
|
||||
private readonly container: NestContainer,
|
||||
private readonly applicationConfig: ApplicationConfig,
|
||||
) {
|
||||
this.pathsExplorer = new PathsExplorer(new MetadataScanner());
|
||||
}
|
||||
|
||||
public mapRouteToRouteInfo(
|
||||
route: Type<any> | RouteInfo | string,
|
||||
controllerOrRoute: Type<any> | RouteInfo | string,
|
||||
): RouteInfo[] {
|
||||
if (isString(route)) {
|
||||
return this.getRouteInfoFromPath(route);
|
||||
if (isString(controllerOrRoute)) {
|
||||
return this.getRouteInfoFromPath(controllerOrRoute);
|
||||
}
|
||||
const routePathOrPaths = this.getRoutePath(route);
|
||||
if (this.isRouteInfo(routePathOrPaths, route)) {
|
||||
return this.getRouteInfoFromObject(route);
|
||||
const routePathOrPaths = this.getRoutePath(controllerOrRoute);
|
||||
if (this.isRouteInfo(routePathOrPaths, controllerOrRoute)) {
|
||||
return this.getRouteInfoFromObject(controllerOrRoute);
|
||||
}
|
||||
|
||||
return this.getRouteInfoFromController(route, routePathOrPaths);
|
||||
return this.getRouteInfoFromController(controllerOrRoute, routePathOrPaths);
|
||||
}
|
||||
|
||||
private getRouteInfoFromPath(routePath: string): RouteInfo[] {
|
||||
@@ -62,33 +75,47 @@ export class RoutesMapper {
|
||||
Object.create(controller),
|
||||
controller.prototype,
|
||||
);
|
||||
const controllerVersion = this.getVersionMetadata(controller);
|
||||
const versioningConfig = this.applicationConfig.getVersioning();
|
||||
const moduleRef = this.getHostModuleOfController(controller);
|
||||
const modulePath = this.getModulePath(moduleRef?.metatype);
|
||||
|
||||
const concatPaths = <T>(acc: T[], currentValue: T[]) =>
|
||||
acc.concat(currentValue);
|
||||
|
||||
const toUndefinedIfNeural = (version: VersionValue) =>
|
||||
version === VERSION_NEUTRAL ? undefined : version;
|
||||
|
||||
const toRouteInfo = (item: RouteDefinition, prefix: string) =>
|
||||
item.path
|
||||
?.map(p => {
|
||||
let endpointPath = modulePath ?? '';
|
||||
endpointPath += this.normalizeGlobalPath(prefix) + addLeadingSlash(p);
|
||||
|
||||
const routeInfo: RouteInfo = {
|
||||
path: endpointPath,
|
||||
method: item.requestMethod,
|
||||
};
|
||||
const version = item.version ?? controllerVersion;
|
||||
if (version && versioningConfig) {
|
||||
if (typeof version !== 'string' && Array.isArray(version)) {
|
||||
return version.map(v => ({
|
||||
...routeInfo,
|
||||
version: toUndefinedIfNeural(v),
|
||||
}));
|
||||
}
|
||||
routeInfo.version = toUndefinedIfNeural(version);
|
||||
}
|
||||
|
||||
return routeInfo;
|
||||
})
|
||||
.flat() as RouteInfo[];
|
||||
|
||||
return []
|
||||
.concat(routePath)
|
||||
.map(routePath =>
|
||||
controllerPaths
|
||||
.map(item =>
|
||||
item.path?.map(p => {
|
||||
let path = modulePath ?? '';
|
||||
path += this.normalizeGlobalPath(routePath) + addLeadingSlash(p);
|
||||
|
||||
const routeInfo: RouteInfo = {
|
||||
path,
|
||||
method: item.requestMethod,
|
||||
};
|
||||
|
||||
if (item.version) {
|
||||
routeInfo.version = item.version;
|
||||
}
|
||||
|
||||
return routeInfo;
|
||||
}),
|
||||
)
|
||||
.map(item => toRouteInfo(item, routePath))
|
||||
.reduce(concatPaths, []),
|
||||
)
|
||||
.reduce(concatPaths, []);
|
||||
@@ -141,4 +168,16 @@ export class RoutesMapper {
|
||||
);
|
||||
return modulePath ?? Reflect.getMetadata(MODULE_PATH, metatype);
|
||||
}
|
||||
|
||||
private getVersionMetadata(
|
||||
metatype: Type<unknown> | Function,
|
||||
): VersionValue | undefined {
|
||||
const versioningConfig = this.applicationConfig.getVersioning();
|
||||
if (versioningConfig) {
|
||||
return (
|
||||
Reflect.getMetadata(VERSION_METADATA, metatype) ??
|
||||
versioningConfig.defaultVersion
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { expect } from 'chai';
|
||||
import { Controller, Get, RequestMethod, Version } from '../../../common';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
RequestMethod,
|
||||
Version,
|
||||
VersioningType,
|
||||
} from '../../../common';
|
||||
import { ApplicationConfig } from '../../application-config';
|
||||
import { NestContainer } from '../../injector/container';
|
||||
import { MiddlewareBuilder } from '../../middleware/builder';
|
||||
@@ -13,8 +19,9 @@ describe('MiddlewareBuilder', () => {
|
||||
beforeEach(() => {
|
||||
const container = new NestContainer();
|
||||
const appConfig = new ApplicationConfig();
|
||||
appConfig.enableVersioning({ type: VersioningType.URI });
|
||||
builder = new MiddlewareBuilder(
|
||||
new RoutesMapper(container),
|
||||
new RoutesMapper(container, appConfig),
|
||||
new NoopHttpAdapter({}),
|
||||
new RouteInfoPathExtractor(appConfig),
|
||||
);
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Version } from '../../../common';
|
||||
import { MiddlewareConfiguration } from '../../../common/interfaces';
|
||||
import { expect } from 'chai';
|
||||
import { Version, VersioningType } from '../../../common';
|
||||
import { Controller } from '../../../common/decorators/core/controller.decorator';
|
||||
import {
|
||||
Get,
|
||||
RequestMapping,
|
||||
} from '../../../common/decorators/http/request-mapping.decorator';
|
||||
import { RequestMethod } from '../../../common/enums/request-method.enum';
|
||||
import { MiddlewareConfiguration } from '../../../common/interfaces';
|
||||
import { ApplicationConfig } from '../../application-config';
|
||||
import { NestContainer } from '../../injector/container';
|
||||
import { RoutesMapper } from '../../middleware/routes-mapper';
|
||||
|
||||
@@ -26,7 +27,9 @@ describe('RoutesMapper', () => {
|
||||
|
||||
let mapper: RoutesMapper;
|
||||
beforeEach(() => {
|
||||
mapper = new RoutesMapper(new NestContainer());
|
||||
const appConfig = new ApplicationConfig();
|
||||
appConfig.enableVersioning({ type: VersioningType.URI });
|
||||
mapper = new RoutesMapper(new NestContainer(), appConfig);
|
||||
});
|
||||
|
||||
it('should map @Controller() to "ControllerMetadata" in forRoutes', () => {
|
||||
@@ -81,4 +84,47 @@ describe('RoutesMapper', () => {
|
||||
{ path: '/test2/another', method: RequestMethod.DELETE },
|
||||
]);
|
||||
});
|
||||
|
||||
@Controller({
|
||||
version: '1',
|
||||
path: 'versioned',
|
||||
})
|
||||
class VersionedController {
|
||||
@Get()
|
||||
hello() {
|
||||
return 'Hello from "VersionedController"!';
|
||||
}
|
||||
|
||||
@Version('2')
|
||||
@Get('/override')
|
||||
override() {
|
||||
return 'Hello from "VersionedController"!';
|
||||
}
|
||||
}
|
||||
|
||||
@Controller({
|
||||
version: ['1', '2'],
|
||||
})
|
||||
class MultipleVersionController {
|
||||
@Get('multiple')
|
||||
multiple() {
|
||||
return 'Multiple Versions 1 or 2';
|
||||
}
|
||||
}
|
||||
|
||||
it('should map a versioned controller to the corresponding route info objects (single version)', () => {
|
||||
expect(mapper.mapRouteToRouteInfo(VersionedController)).to.deep.equal([
|
||||
{ path: '/versioned/', version: '1', method: RequestMethod.GET },
|
||||
{ path: '/versioned/override', version: '2', method: RequestMethod.GET },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should map a versioned controller to the corresponding route info objects (multiple versions)', () => {
|
||||
expect(mapper.mapRouteToRouteInfo(MultipleVersionController)).to.deep.equal(
|
||||
[
|
||||
{ path: '/multiple', version: '1', method: RequestMethod.GET },
|
||||
{ path: '/multiple', version: '2', method: RequestMethod.GET },
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user