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
8 changes: 6 additions & 2 deletions packages/common/interfaces/nest-application.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,15 @@ export interface INestApplication<
/**
* Registers a prefix for every HTTP route path.
*
* @param {string} prefix The prefix for every HTTP route path (for example `/v1/api`)
* @param {string | string[]} prefix The prefix for every HTTP route path (for example `/v1/api`).
* Can be an array of prefixes to register multiple prefixes (for example `['api', 'v1']`).
* @param {GlobalPrefixOptions} options Global prefix options object
* @returns {this}
*/
setGlobalPrefix(prefix: string, options?: GlobalPrefixOptions): this;
setGlobalPrefix(
prefix: string | string[],
options?: GlobalPrefixOptions,
): this;

/**
* Register Ws Adapter which will be used inside Gateways.
Expand Down
14 changes: 9 additions & 5 deletions packages/core/application-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { InstanceWrapper } from './injector/instance-wrapper';
import { ExcludeRouteMetadata } from './router/interfaces/exclude-route-metadata.interface';

export class ApplicationConfig {
private globalPrefix = '';
private globalPrefixes: string[] = [];
private globalPrefixOptions: GlobalPrefixOptions<ExcludeRouteMetadata> = {};
private globalPipes: Array<PipeTransform> = [];
private globalFilters: Array<ExceptionFilter> = [];
Expand All @@ -27,12 +27,16 @@ export class ApplicationConfig {

constructor(private ioAdapter: WebSocketAdapter | null = null) {}

public setGlobalPrefix(prefix: string) {
this.globalPrefix = prefix;
public setGlobalPrefix(prefix: string | string[]) {
this.globalPrefixes = Array.isArray(prefix) ? prefix : [prefix];
}

public getGlobalPrefix() {
return this.globalPrefix;
public getGlobalPrefix(): string {
return this.globalPrefixes[0] ?? '';
}

public getGlobalPrefixes(): string[] {
return this.globalPrefixes;
}

public setGlobalPrefixOptions(
Expand Down
41 changes: 27 additions & 14 deletions packages/core/middleware/route-info-path-extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,43 @@ import { RoutePathFactory } from './../router/route-path-factory';

export class RouteInfoPathExtractor {
private readonly routePathFactory: RoutePathFactory;
private readonly prefixPath: string;
private readonly prefixPaths: string[];
private readonly excludedGlobalPrefixRoutes: ExcludeRouteMetadata[];
private readonly versioningConfig?: VersioningOptions;

constructor(private readonly applicationConfig: ApplicationConfig) {
this.routePathFactory = new RoutePathFactory(applicationConfig);
this.prefixPath = stripEndSlash(
addLeadingSlash(this.applicationConfig.getGlobalPrefix()),
);
const prefixes = this.applicationConfig.getGlobalPrefixes();
this.prefixPaths =
prefixes.length > 0
? prefixes.map(p => stripEndSlash(addLeadingSlash(p)))
: [''];
this.excludedGlobalPrefixRoutes =
this.applicationConfig.getGlobalPrefixOptions().exclude!;
this.versioningConfig = this.applicationConfig.getVersioning();
}

private get prefixPath(): string {
return this.prefixPaths[0];
}

public extractPathsFrom({ path, method, version }: RouteInfo): string[] {
const versionPaths = this.extractVersionPathFrom(version);

if (this.isAWildcard(path)) {
const entries =
versionPaths.length > 0
? versionPaths
.map(versionPath => [
this.prefixPath + versionPath + '$',
this.prefixPath + versionPath + addLeadingSlash(path),
? this.prefixPaths.flatMap(prefixPath =>
versionPaths.flatMap(versionPath => [
prefixPath + versionPath + '$',
prefixPath + versionPath + addLeadingSlash(path),
]),
)
: this.prefixPaths[0]
? this.prefixPaths.flatMap(prefixPath => [
prefixPath + '$',
prefixPath + addLeadingSlash(path),
])
.flat()
: this.prefixPath
? [this.prefixPath + '$', this.prefixPath + addLeadingSlash(path)]
: [addLeadingSlash(path)];

return Array.isArray(this.excludedGlobalPrefixRoutes)
Expand Down Expand Up @@ -101,10 +110,14 @@ export class RouteInfoPathExtractor {
}

if (!versionPaths.length) {
return [this.prefixPath + addLeadingSlash(path)];
return this.prefixPaths.map(
prefixPath => prefixPath + addLeadingSlash(path),
);
}
return versionPaths.map(
versionPath => this.prefixPath + versionPath + addLeadingSlash(path),
return this.prefixPaths.flatMap(prefixPath =>
versionPaths.map(
versionPath => prefixPath + versionPath + addLeadingSlash(path),
),
);
}

Expand Down
14 changes: 10 additions & 4 deletions packages/core/nest-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,12 @@ export class NestApplication
public async registerRouter() {
await this.registerMiddleware(this.httpAdapter);

const prefix = this.config.getGlobalPrefix();
const basePath = addLeadingSlash(prefix);
this.routesResolver.resolve(this.httpAdapter, basePath);
const prefixes = this.config.getGlobalPrefixes();
const basePaths =
prefixes.length > 0
? prefixes.map(prefix => addLeadingSlash(prefix))
: [''];
this.routesResolver.resolve(this.httpAdapter, basePaths);
}

public async registerRouterHooks() {
Expand Down Expand Up @@ -374,7 +377,10 @@ export class NestApplication
return `${this.getProtocol()}://${host}:${address.port}`;
}

public setGlobalPrefix(prefix: string, options?: GlobalPrefixOptions): this {
public setGlobalPrefix(
prefix: string | string[],
options?: GlobalPrefixOptions,
): this {
this.config.setGlobalPrefix(prefix);
if (options) {
const exclude = options?.exclude
Expand Down
2 changes: 1 addition & 1 deletion packages/core/router/interfaces/resolver.interface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export interface Resolver {
resolve(instance: any, basePath: string): void;
resolve(instance: any, basePath: string | string[]): void;
registerNotFoundHandler(): void;
registerExceptionHandler(): void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ export interface RoutePathMetadata {

/**
* Global route prefix specified with the "NestApplication#setGlobalPrefix" method.
* Can be a single prefix or an array of prefixes.
*/
globalPrefix?: string;
globalPrefix?: string | string[];

/**
* Module-level path registered through the "RouterModule".
Expand Down
34 changes: 21 additions & 13 deletions packages/core/router/route-path-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,27 @@ export class RoutePathFactory {
paths = this.appendToAllIfDefined(paths, metadata.methodPath);

if (metadata.globalPrefix) {
paths = paths.map(path => {
if (
this.isExcludedFromGlobalPrefix(
path,
requestMethod,
versionOrVersions,
metadata.versioningOptions,
)
) {
return path;
}
return stripEndSlash(metadata.globalPrefix || '') + path;
});
const globalPrefixes = Array.isArray(metadata.globalPrefix)
? metadata.globalPrefix
: [metadata.globalPrefix];

paths = flatten(
paths.map(path => {
if (
this.isExcludedFromGlobalPrefix(
path,
requestMethod,
versionOrVersions,
metadata.versioningOptions,
)
) {
return [path];
}
return globalPrefixes.map(
prefix => stripEndSlash(prefix || '') + path,
);
}),
);
}

return paths
Expand Down
4 changes: 2 additions & 2 deletions packages/core/router/routes-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export class RoutesResolver implements Resolver {

public resolve<T extends HttpServer>(
applicationRef: T,
globalPrefix: string,
globalPrefix: string | string[],
) {
const modules = this.container.getModules();
modules.forEach(({ controllers, metatype }, moduleName) => {
Expand All @@ -88,7 +88,7 @@ export class RoutesResolver implements Resolver {
public registerRouters(
routes: Map<string | symbol | Function, InstanceWrapper<Controller>>,
moduleName: string,
globalPrefix: string,
globalPrefix: string | string[],
modulePath: string,
applicationRef: HttpServer,
) {
Expand Down
22 changes: 22 additions & 0 deletions packages/core/test/application-config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,25 @@ describe('ApplicationConfig', () => {

expect(appConfig.getGlobalPrefix()).to.be.eql(path);
});
it('should set global path as array', () => {
const paths = ['api', 'v1'];
appConfig.setGlobalPrefix(paths);

expect(appConfig.getGlobalPrefix()).to.be.eql('api');
expect(appConfig.getGlobalPrefixes()).to.be.eql(paths);
});
it('should return all prefixes via getGlobalPrefixes', () => {
const paths = ['prefix1', 'prefix2', 'prefix3'];
appConfig.setGlobalPrefix(paths);

expect(appConfig.getGlobalPrefixes()).to.be.eql(paths);
});
it('should convert single string to array in getGlobalPrefixes', () => {
const path = 'test';
appConfig.setGlobalPrefix(path);

expect(appConfig.getGlobalPrefixes()).to.be.eql([path]);
});
it('should set global path options', () => {
const options: GlobalPrefixOptions<ExcludeRouteMetadata> = {
exclude: [
Expand All @@ -34,6 +53,9 @@ describe('ApplicationConfig', () => {
it('should has empty string as a global path by default', () => {
expect(appConfig.getGlobalPrefix()).to.be.eql('');
});
it('should return empty array as global prefixes by default', () => {
expect(appConfig.getGlobalPrefixes()).to.be.eql([]);
});
it('should has empty string as a global path option by default', () => {
expect(appConfig.getGlobalPrefixOptions()).to.be.eql({});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe('RouteInfoPathExtractor', () => {
});

it(`should return correct paths when set global prefix`, () => {
Reflect.set(routeInfoPathExtractor, 'prefixPath', '/api');
Reflect.set(routeInfoPathExtractor, 'prefixPaths', ['/api']);

expect(
routeInfoPathExtractor.extractPathsFrom({
Expand All @@ -54,7 +54,7 @@ describe('RouteInfoPathExtractor', () => {
});

it(`should return correct paths when set global prefix and global prefix options`, () => {
Reflect.set(routeInfoPathExtractor, 'prefixPath', '/api');
Reflect.set(routeInfoPathExtractor, 'prefixPaths', ['/api']);
Reflect.set(
routeInfoPathExtractor,
'excludedGlobalPrefixRoutes',
Expand Down Expand Up @@ -124,7 +124,7 @@ describe('RouteInfoPathExtractor', () => {
});

it(`should return correct path when set global prefix`, () => {
Reflect.set(routeInfoPathExtractor, 'prefixPath', '/api');
Reflect.set(routeInfoPathExtractor, 'prefixPaths', ['/api']);

expect(
routeInfoPathExtractor.extractPathFrom({
Expand All @@ -143,7 +143,7 @@ describe('RouteInfoPathExtractor', () => {
});

it(`should return correct path when set global prefix and global prefix options`, () => {
Reflect.set(routeInfoPathExtractor, 'prefixPath', '/api');
Reflect.set(routeInfoPathExtractor, 'prefixPaths', ['/api']);
Reflect.set(
routeInfoPathExtractor,
'excludedGlobalPrefixRoutes',
Expand Down
55 changes: 55 additions & 0 deletions packages/core/test/router/route-path-factory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,61 @@ describe('RoutePathFactory', () => {
).to.deep.equal(['/ctrlPath']);
sinon.restore();
});

it('should return paths for each global prefix when array is provided', () => {
expect(
routePathFactory.create({
ctrlPath: '/ctrlPath/',
methodPath: '/methodPath/',
globalPrefix: ['api', 'v1'],
}),
).to.deep.equal(['/api/ctrlPath/methodPath', '/v1/ctrlPath/methodPath']);

expect(
routePathFactory.create({
ctrlPath: '/ctrlPath/',
methodPath: '/methodPath/',
modulePath: '/modulePath/',
globalPrefix: ['/prefix1', '/prefix2'],
}),
).to.deep.equal([
'/prefix1/modulePath/ctrlPath/methodPath',
'/prefix2/modulePath/ctrlPath/methodPath',
]);
});

it('should handle single-element array same as string', () => {
const resultArray = routePathFactory.create({
ctrlPath: '/ctrlPath/',
methodPath: '/methodPath/',
globalPrefix: ['api'],
});

const resultString = routePathFactory.create({
ctrlPath: '/ctrlPath/',
methodPath: '/methodPath/',
globalPrefix: 'api',
});

expect(resultArray).to.deep.equal(resultString);
});

it('should combine multiple prefixes with versioning', () => {
expect(
routePathFactory.create({
ctrlPath: '/ctrlPath/',
methodPath: '/methodPath/',
globalPrefix: ['api', 'v1'],
versioningOptions: {
type: VersioningType.URI,
},
controllerVersion: '1.0.0',
}),
).to.deep.equal([
'/api/v1.0.0/ctrlPath/methodPath',
'/v1/v1.0.0/ctrlPath/methodPath',
]);
});
});

describe('isExcludedFromGlobalPrefix', () => {
Expand Down
Loading