Skip to content

Commit

Permalink
Merge pull request #10484 from thiagomini/feature/8844-api-version-in…
Browse files Browse the repository at this point in the history
…-route-info

Feature/8844 api version in route info
  • Loading branch information
kamilmysliwiec committed Nov 7, 2022
2 parents eaee2b9 + 4176ee5 commit ea4c1d8
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 38 deletions.
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);
}
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

0 comments on commit ea4c1d8

Please sign in to comment.