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

Feature/8844 api version in route info #10484

Merged
147 changes: 147 additions & 0 deletions integration/hello-world/e2e/middleware-with-versioning.spec.ts
@@ -0,0 +1,147 @@
import {
Controller,
Get,
INestApplication,
MiddlewareConsumer,
Module,
RequestMethod,
Version,
VersioningOptions,
VersioningType,
VERSION_NEUTRAL,
} from '@nestjs/common';
import { CustomVersioningOptions } from '@nestjs/common/interfaces';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';

const RETURN_VALUE = 'test';
const VERSIONED_VALUE = 'test_versioned';

@Controller()
class TestController {
@Version('1')
@Get('versioned')
versionedTest() {
return RETURN_VALUE;
}
}

@Module({
imports: [AppModule],
controllers: [TestController],
})
class TestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply((req, res, next) => res.send(VERSIONED_VALUE))
.forRoutes({
path: '/versioned',
version: '1',
method: RequestMethod.ALL,
});
}
}

describe('Middleware', () => {
let app: INestApplication;

describe('when using default URI versioning', () => {
beforeEach(async () => {
app = await createAppWithVersioning({
type: VersioningType.URI,
defaultVersion: VERSION_NEUTRAL,
});
});

it(`forRoutes({ path: '/versioned', version: '1', method: RequestMethod.ALL })`, () => {
return request(app.getHttpServer())
.get('/v1/versioned')
.expect(200, VERSIONED_VALUE);
});
});

describe('when default URI versioning with an alternative prefix', () => {
beforeEach(async () => {
app = await createAppWithVersioning({
type: VersioningType.URI,
defaultVersion: VERSION_NEUTRAL,
prefix: 'version',
});
});

it(`forRoutes({ path: '/versioned', version: '1', method: RequestMethod.ALL })`, () => {
return request(app.getHttpServer())
.get('/version1/versioned')
.expect(200, VERSIONED_VALUE);
});
});

describe('when using HEADER versioning', () => {
beforeEach(async () => {
app = await createAppWithVersioning({
type: VersioningType.HEADER,
header: 'version',
});
});

it(`forRoutes({ path: '/versioned', version: '1', method: RequestMethod.ALL })`, () => {
return request(app.getHttpServer())
.get('/versioned')
.set('version', '1')
.expect(200, VERSIONED_VALUE);
});
});

describe('when using MEDIA TYPE versioning', () => {
beforeEach(async () => {
app = await createAppWithVersioning({
type: VersioningType.MEDIA_TYPE,
key: 'v',
defaultVersion: VERSION_NEUTRAL,
});
});

it(`forRoutes({ path: '/versioned', version: '1', method: RequestMethod.ALL })`, () => {
return request(app.getHttpServer())
.get('/versioned')
.expect(200, VERSIONED_VALUE);
});
});

describe('when using CUSTOM TYPE versioning', () => {
beforeEach(async () => {
const extractor: CustomVersioningOptions['extractor'] = () => '1';

app = await createAppWithVersioning({
type: VersioningType.CUSTOM,
extractor,
});
});

it(`forRoutes({ path: '/versioned', version: '1', method: RequestMethod.ALL })`, () => {
return request(app.getHttpServer())
.get('/versioned')
.expect(200, VERSIONED_VALUE);
});
});

afterEach(async () => {
await app.close();
});
});

async function createAppWithVersioning(
versioningOptions: VersioningOptions,
): Promise<INestApplication> {
const app = (
await Test.createTestingModule({
imports: [TestModule],
}).compile()
).createNestApplication();

app.enableVersioning(versioningOptions);
await app.init();

return app;
}
@@ -1,9 +1,11 @@
import { RequestMethod } from '../../enums';
import { Type } from '../type.interface';
import { VersionValue } from '../version-options.interface';

export interface RouteInfo {
path: string;
method: RequestMethod;
version?: VersionValue;
}

export interface MiddlewareConfiguration<T = any> {
Expand Down
28 changes: 18 additions & 10 deletions packages/core/middleware/middleware-module.ts
@@ -1,4 +1,4 @@
import { HttpServer } from '@nestjs/common';
import { HttpServer, VersioningType } from '@nestjs/common';
import { RequestMethod } from '@nestjs/common/enums/request-method.enum';
import {
MiddlewareConfiguration,
Expand All @@ -20,6 +20,7 @@ import { Injector } from '../injector/injector';
import { InstanceWrapper } from '../injector/instance-wrapper';
import { InstanceToken, Module } from '../injector/module';
import { REQUEST_CONTEXT_ID } from '../router/request/request-constants';
import { RoutePathFactory } from '../router/route-path-factory';
import { RouterExceptionFilters } from '../router/router-exception-filters';
import { RouterProxy } from '../router/router-proxy';
import { isRequestMethodAll, isRouteExcluded } from '../router/utils';
Expand All @@ -40,6 +41,8 @@ export class MiddlewareModule {
private container: NestContainer;
private httpAdapter: HttpServer;

constructor(private readonly routePathFactory: RoutePathFactory) {}

public async register(
middlewareContainer: MiddlewareContainer,
container: NestContainer,
Expand Down Expand Up @@ -174,8 +177,7 @@ export class MiddlewareModule {
await this.bindHandler(
instanceWrapper,
applicationRef,
routeInfo.method,
routeInfo.path,
routeInfo,
moduleRef,
collection,
);
Expand All @@ -185,8 +187,7 @@ export class MiddlewareModule {
private async bindHandler(
wrapper: InstanceWrapper<NestMiddleware>,
applicationRef: HttpServer,
method: RequestMethod,
path: string,
routeInfo: RouteInfo,
moduleRef: Module,
collection: Map<InstanceToken, InstanceWrapper>,
) {
Expand All @@ -197,12 +198,11 @@ export class MiddlewareModule {
const isStatic = wrapper.isDependencyTreeStatic();
if (isStatic) {
const proxy = await this.createProxy(instance);
return this.registerHandler(applicationRef, method, path, proxy);
return this.registerHandler(applicationRef, routeInfo, proxy);
}
await this.registerHandler(
applicationRef,
method,
path,
routeInfo,
async <TRequest, TResponse>(
req: TRequest,
res: TResponse,
Expand Down Expand Up @@ -266,8 +266,7 @@ export class MiddlewareModule {

private async registerHandler(
applicationRef: HttpServer,
method: RequestMethod,
path: string,
{ path, method, version }: RouteInfo,
proxy: <TRequest, TResponse>(
req: TRequest,
res: TResponse,
Expand All @@ -291,6 +290,15 @@ export class MiddlewareModule {
}
path = basePath + path;
}

const applicationVersioningConfig = this.config.getVersioning();
if (version && applicationVersioningConfig.type === VersioningType.URI) {
const versionPrefix = this.routePathFactory.getVersionPrefix(
applicationVersioningConfig,
);
path = `/${versionPrefix}${version.toString()}${path}`;
}

const isMethodAll = isRequestMethodAll(method);
const requestMethod = RequestMethod[method];
const router = await applicationRef.createMiddlewareFactory(method);
Expand Down
63 changes: 44 additions & 19 deletions packages/core/middleware/routes-mapper.ts
@@ -1,5 +1,5 @@
import { MODULE_PATH, PATH_METADATA } from '@nestjs/common/constants';
import { RouteInfo, Type } from '@nestjs/common/interfaces';
import { RouteInfo, Type, VersionValue } from '@nestjs/common/interfaces';
import {
addLeadingSlash,
isString,
Expand All @@ -22,46 +22,71 @@ export class RoutesMapper {
route: Type<any> | RouteInfo | string,
): RouteInfo[] {
if (isString(route)) {
const defaultRequestMethod = -1;
return [
{
path: addLeadingSlash(route),
method: defaultRequestMethod,
},
];
return this.getRouteInfoFromPath(route);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I extracted many logic branches here to private methods to improve the public method's readability.

}
const routePathOrPaths = this.getRoutePath(route);
if (this.isRouteInfo(routePathOrPaths, route)) {
return [
{
path: addLeadingSlash(route.path),
method: route.method,
},
];
return this.getRouteInfoFromObject(route);
}

return this.getRouteInfoFromController(route, routePathOrPaths);
}

private getRouteInfoFromPath(routePath: string): RouteInfo[] {
const defaultRequestMethod = -1;
return [
{
path: addLeadingSlash(routePath),
method: defaultRequestMethod,
},
];
}

private getRouteInfoFromObject(routeInfoObject: RouteInfo): RouteInfo[] {
const routeInfo: RouteInfo = {
path: addLeadingSlash(routeInfoObject.path),
method: routeInfoObject.method,
};

if (routeInfoObject.version) {
routeInfo.version = routeInfoObject.version;
}
return [routeInfo];
}

private getRouteInfoFromController(
controller: Type<any>,
routePath: string,
): RouteInfo[] {
const controllerPaths = this.routerExplorer.scanForPaths(
Object.create(route),
route.prototype,
Object.create(controller),
controller.prototype,
);
const moduleRef = this.getHostModuleOfController(route);
const moduleRef = this.getHostModuleOfController(controller);
const modulePath = this.getModulePath(moduleRef?.metatype);

const concatPaths = <T>(acc: T[], currentValue: T[]) =>
acc.concat(currentValue);

return []
.concat(routePathOrPaths)
.concat(routePath)
.map(routePath =>
controllerPaths
.map(item =>
item.path?.map(p => {
let path = modulePath ?? '';
path += this.normalizeGlobalPath(routePath) + addLeadingSlash(p);

return {
const routeInfo: RouteInfo = {
path,
method: item.requestMethod,
};

if (item.version) {
routeInfo.version = item.version;
}

return routeInfo;
}),
)
.reduce(concatPaths, []),
Expand Down
4 changes: 3 additions & 1 deletion packages/core/nest-application.ts
Expand Up @@ -42,6 +42,7 @@ import { MiddlewareModule } from './middleware/middleware-module';
import { NestApplicationContext } from './nest-application-context';
import { ExcludeRouteMetadata } from './router/interfaces/exclude-route-metadata.interface';
import { Resolver } from './router/interfaces/resolver.interface';
import { RoutePathFactory } from './router/route-path-factory';
import { RoutesResolver } from './router/routes-resolver';

const { SocketModule } = optionalRequire(
Expand All @@ -63,7 +64,7 @@ export class NestApplication
private readonly logger = new Logger(NestApplication.name, {
timestamp: true,
});
private readonly middlewareModule = new MiddlewareModule();
private readonly middlewareModule: MiddlewareModule;
private readonly middlewareContainer = new MiddlewareContainer(
this.container,
);
Expand All @@ -85,6 +86,7 @@ export class NestApplication

this.selectContextModule();
this.registerHttpServer();
this.middlewareModule = new MiddlewareModule(new RoutePathFactory(config));

this.routesResolver = new RoutesResolver(
this.container,
Expand Down