Skip to content

Commit

Permalink
Merge pull request #11785 from nestjs/fix/middleware-for-versioned-ctrls
Browse files Browse the repository at this point in the history
fix(core): apply middleware to versioned controllers (ctrl-level)
  • Loading branch information
kamilmysliwiec committed Jun 14, 2023
2 parents 74251a8 + 7aa7107 commit c01ea3f
Show file tree
Hide file tree
Showing 9 changed files with 251 additions and 38 deletions.
63 changes: 63 additions & 0 deletions integration/versioning/e2e/uri-versioning.spec.ts
Expand Up @@ -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();
});
});
});
24 changes: 20 additions & 4 deletions integration/versioning/src/app.module.ts
@@ -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: [],
Expand All @@ -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 changes: 18 additions & 0 deletions integration/versioning/src/middleware.controller.ts
@@ -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 changes: 12 additions & 0 deletions integration/versioning/src/multiple-middleware.controller.ts
@@ -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 changes: 12 additions & 0 deletions integration/versioning/src/neutral-middleware.controller.ts
@@ -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';
}
}
2 changes: 1 addition & 1 deletion packages/core/middleware/middleware-module.ts
Expand Up @@ -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;
Expand Down
95 changes: 67 additions & 28 deletions packages/core/middleware/routes-mapper.ts
@@ -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[] {
Expand Down Expand Up @@ -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, []);
Expand Down Expand Up @@ -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
);
}
}
}
11 changes: 9 additions & 2 deletions packages/core/test/middleware/builder.spec.ts
@@ -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';
Expand All @@ -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),
);
Expand Down

0 comments on commit c01ea3f

Please sign in to comment.