Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion integration/hello-world/e2e/router-module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ describe('RouterModule', () => {
},
];
const routes2: Routes = [
{ path: 'v1', children: [AuthModule, PaymentsModule, NoSlashModule] },
{
path: ['v1', 'v2'],
children: [AuthModule, PaymentsModule, NoSlashModule],
},
];

@Module({
Expand Down Expand Up @@ -93,6 +96,12 @@ describe('RouterModule', () => {
.expect(200, 'NoSlashController');
});

it('should hit the "NoSlashController on v2"', async () => {
return request(app.getHttpServer())
.get('/v2/no-slash-controller')
.expect(200, 'NoSlashController');
});

afterEach(async () => {
await app.close();
});
Expand Down
7 changes: 7 additions & 0 deletions packages/common/utils/shared.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ export const addLeadingSlash = (path?: string): string =>
: path
: '';

export const normalizePaths = (paths: string | string[]): string[] => {
if (Array.isArray(paths)) {
return paths.map(path => normalizePath(path));
}
return [normalizePath(paths)];
};

export const normalizePath = (path?: string): string =>
path
? path.startsWith('/')
Expand Down
42 changes: 24 additions & 18 deletions packages/core/middleware/routes-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,25 +91,31 @@ export class RoutesMapper {

const toRouteInfo = (item: RouteDefinition, prefix: string) =>
item.path?.flatMap(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),
}));
const endpointPaths = modulePath ?? [''];

return endpointPaths.flatMap(endpointPath => {
const fullPath =
endpointPath +
this.normalizeGlobalPath(prefix) +
addLeadingSlash(p);

const routeInfo: RouteInfo = {
path: fullPath,
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);
}
routeInfo.version = toUndefinedIfNeural(version);
}

return routeInfo;
return routeInfo;
});
});

return ([] as string[])
Expand Down Expand Up @@ -158,7 +164,7 @@ export class RoutesMapper {

private getModulePath(
metatype: Type<unknown> | undefined,
): string | undefined {
): string[] | undefined {
if (!metatype) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ export interface RoutePathMetadata {
globalPrefix?: string;

/**
* Module-level path registered through the "RouterModule".
* Module-level path or paths registered through the "RouterModule".
*/
modulePath?: string;
modulePath?: string | string[];

/**
* Controller-level version (e.g., @Controller({ version: '1.0' }) = "1.0").
Expand Down
2 changes: 1 addition & 1 deletion packages/core/router/interfaces/routes.interface.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Type } from '@nestjs/common';

export interface RouteTree {
path: string;
path: string | string[];
module?: Type<any>;
children?: (RouteTree | Type<any>)[];
}
Expand Down
8 changes: 4 additions & 4 deletions packages/core/router/router-module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DynamicModule, Inject, Module, Type } from '@nestjs/common';
import { MODULE_PATH } from '@nestjs/common/constants';
import { normalizePath } from '@nestjs/common/utils/shared.utils';
import { normalizePaths } from '@nestjs/common/utils/shared.utils';
import { Module as ModuleClass } from '../injector/module';
import { ModulesContainer } from '../injector/modules-container';
import { Routes, RouteTree } from './interfaces';
Expand Down Expand Up @@ -58,15 +58,15 @@ export class RouterModule {
private initialize() {
const flattenedRoutes = flattenRoutePaths(this.routes);
flattenedRoutes.forEach(route => {
const modulePath = normalizePath(route.path);
this.registerModulePathMetadata(route.module, modulePath);
const modulePaths = normalizePaths(route.path);
this.registerModulePathMetadata(route.module, modulePaths);
this.updateTargetModulesCache(route.module);
});
}

private registerModulePathMetadata(
moduleCtor: Type<unknown>,
modulePath: string,
modulePath: string | string[],
) {
Reflect.defineMetadata(
MODULE_PATH + this.modulesContainer.applicationId,
Expand Down
6 changes: 4 additions & 2 deletions packages/core/router/routes-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export class RoutesResolver implements Resolver {
routes: Map<string | symbol | Function, InstanceWrapper<Controller>>,
moduleName: string,
globalPrefix: string,
modulePath: string,
modulePath: string | string[],
applicationRef: HttpServer,
) {
routes.forEach(instanceWrapper => {
Expand Down Expand Up @@ -207,7 +207,9 @@ export class RoutesResolver implements Resolver {
);
}

private getModulePathMetadata(metatype: Type<unknown>): string | undefined {
private getModulePathMetadata(
metatype: Type<unknown>,
): string | string[] | undefined {
const modulesContainer = this.container.getModules();
const modulePath = Reflect.getMetadata(
MODULE_PATH + modulesContainer.applicationId,
Expand Down
30 changes: 25 additions & 5 deletions packages/core/router/utils/flatten-route-paths.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Routes } from '../interfaces/routes.interface';
export function flattenRoutePaths(routes: Routes) {
const result: Array<{
module: Type;
path: string;
path: string | string[];
}> = [];
routes.forEach(item => {
if (item.module && item.path) {
Expand All @@ -14,10 +14,30 @@ export function flattenRoutePaths(routes: Routes) {
if (item.children) {
const childrenRef = item.children as Routes;
childrenRef.forEach(child => {
if (!isString(child) && isString(child.path)) {
child.path = normalizePath(
normalizePath(item.path) + normalizePath(child.path),
);
if (
!isString(child) &&
(isString(child.path) || Array.isArray(child.path))
) {
const normalizedChildPaths: string[] = [];
const parentPaths = Array.isArray(item.path)
? item.path
: [item.path];
for (const parentPath of parentPaths) {
const childPaths = Array.isArray(child.path)
? child.path
: [child.path];
for (const childPath of childPaths) {
normalizedChildPaths.push(
normalizePath(
normalizePath(parentPath) + normalizePath(childPath),
),
);
}
}
child.path =
normalizedChildPaths.length === 1
? normalizedChildPaths[0]
: normalizedChildPaths;
} else {
result.push({ path: item.path, module: child as any as Type });
}
Expand Down
52 changes: 52 additions & 0 deletions packages/core/test/middleware/routes-mapper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,56 @@ describe('RoutesMapper', () => {
],
);
});

@Controller('test')
class TestControllerWithMultipleModulePaths {
@Get('hello')
hello() {
return 'Hello';
}

@Version('2')
@Get('versioned')
versioned() {
return 'Versioned';
}
}

it('should map a controller with multiple module paths to the corresponding route info objects', () => {
// Simulate a controller that is registered under multiple module paths
const mockModuleRef = {
metatype: class MockModule {},
controllers: new Map(),
} as any;

mockModuleRef.controllers.set(TestControllerWithMultipleModulePaths, {});

// Mock the getModulePath to return multiple paths
const originalGetModulePath = (mapper as any).getModulePath;
(mapper as any).getModulePath = () => ['/api/v1', '/api/v2'];

try {
const result = mapper.mapRouteToRouteInfo(
TestControllerWithMultipleModulePaths,
);

expect(result).to.deep.equal([
{ path: '/api/v1/test/hello', method: RequestMethod.GET },
{ path: '/api/v2/test/hello', method: RequestMethod.GET },
{
path: '/api/v1/test/versioned',
method: RequestMethod.GET,
version: '2',
},
{
path: '/api/v2/test/versioned',
method: RequestMethod.GET,
version: '2',
},
]);
} finally {
// Restore original method
(mapper as any).getModulePath = originalGetModulePath;
}
});
});
50 changes: 49 additions & 1 deletion packages/core/test/router/utils/flat-routes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ describe('flattenRoutePaths', () => {
@Module({})
class ChildModule6 {}
@Module({})
class ChildModule7 {}
@Module({})
class ChildModule8 {}
@Module({})
class ChildModule9 {}
@Module({})
class ChildParentPathModule {}
@Module({})
class ParentChildModule {}
Expand Down Expand Up @@ -95,6 +101,23 @@ describe('flattenRoutePaths', () => {
},
],
},
{
path: ['/child5', '/child6', '/child7'],
children: [
{
path: '',
module: ChildModule7,
},
{
path: '/child8',
module: ChildModule8,
},
{
path: ['/child9', '/child10'],
module: ChildModule9,
},
],
},
],
},
{ path: '/v1', children: [AuthModule, CatsModule, DogsModule] },
Expand All @@ -119,6 +142,29 @@ describe('flattenRoutePaths', () => {
{ path: '/parent/child3', module: ChildModule5 },
{ path: '/parent/child3/child', module: ChildParentPathModule },
{ path: '/parent/child4', module: ChildModule6 },
{
path: ['/parent/child5', '/parent/child6', '/parent/child7'],
module: ChildModule7,
},
{
path: [
'/parent/child5/child8',
'/parent/child6/child8',
'/parent/child7/child8',
],
module: ChildModule8,
},
{
path: [
'/parent/child5/child9',
'/parent/child5/child10',
'/parent/child6/child9',
'/parent/child6/child10',
'/parent/child7/child9',
'/parent/child7/child10',
],
module: ChildModule9,
},
{ path: '/v1', module: AuthModule },
{ path: '/v1', module: CatsModule },
{ path: '/v1', module: DogsModule },
Expand All @@ -127,6 +173,8 @@ describe('flattenRoutePaths', () => {
{ path: '/v3', module: AuthModule3 },
{ path: '/v3', module: CatsModule3 },
];
expect(flattenRoutePaths(routes)).to.be.eql(expectedRoutes);
const result = flattenRoutePaths(routes);
console.log(result);
expect(result).to.be.eql(expectedRoutes);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ export interface Response<T> {
}

@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>>
{
export class TransformInterceptor<T> implements NestInterceptor<
T,
Response<T>
> {
intercept(
context: ExecutionContext,
next: CallHandler<T>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ export interface Response<T> {
}

@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>>
{
export class TransformInterceptor<T> implements NestInterceptor<
T,
Response<T>
> {
intercept(
context: ExecutionContext,
next: CallHandler<T>,
Expand Down