Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): apply middleware to versioned controllers (ctrl-level) #11785

Merged
merged 1 commit into from Jun 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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