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

feat: optional wildcard routing in routePrefix #1499

Closed
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
87 changes: 85 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,11 +331,12 @@ createExpressServer({

#### Prefix all controllers routes

If you want to prefix all your routes, e.g. `/api` you can use `routePrefix` option:
You can prefix all your routes using the `routePrefix` option. This supports three types of patterns:

1. Simple string prefix:

```typescript
import { createExpressServer } from 'routing-controllers';
import { UserController } from './controller/UserController';

createExpressServer({
routePrefix: '/api',
Expand All @@ -345,6 +346,88 @@ createExpressServer({

> koa users must use `createKoaServer` instead of `createExpressServer`

2. Wildcard patterns for dynamic segments:

**Match single segment: /dev/api, /staging/api, etc.**

```typescript
import { createExpressServer } from 'routing-controllers';

createExpressServer({
routePrefix: '/*/api',
controllers: [UserController],
}).listen(3000);
```

> koa users must use `createKoaServer` instead of `createExpressServer`

**Match an optional segment: /api or /staging/api**

```typescript
import { createExpressServer } from 'routing-controllers';

createExpressServer({
routePrefix: '/*?',
controllers: [UserController],
}).listen(3000);
```

> koa users must use `createKoaServer` instead of `createExpressServer`

**Match multiple segments: /v1/staging/api, /v2/prod/api, etc.**

```typescript
import { createExpressServer } from 'routing-controllers';

createExpressServer({
routePrefix: '/*/*/api',
controllers: [UserController],
}).listen(3000);
```

> koa users must use `createKoaServer` instead of `createExpressServer`

**Mix fixed and wildcard segments: /api/region1/v2/instance2/service**

```typescript
import { createExpressServer } from 'routing-controllers';

createExpressServer({
routePrefix: '/api/*/v2/*/service',
controllers: [UserController],
}).listen(3000);
```

> koa users must use `createKoaServer` instead of `createExpressServer`

3. Regular expressions for complex patterns:

**Match version numbers: /v1/api, /v2/api, etc.**

```typescript
import { createExpressServer } from 'routing-controllers';

createExpressServer({
routePrefix: /\/v[0-9]+\/api/,
controllers: [UserController],
}).listen(3000);
```

> koa users must use `createKoaServer` instead of `createExpressServer`

**Optional segments: /api or /api/v1**

```typescript
import { createExpressServer } from 'routing-controllers';

createExpressServer({
routePrefix: /\/api(\/v[0-9]+)?/,
controllers: [UserController],
}).listen(3000);
```

> koa users must use `createKoaServer` instead of `createExpressServer`

#### Prefix controller with base route

You can prefix all specific controller's actions with base route:
Expand Down
8 changes: 6 additions & 2 deletions src/RoutingControllersOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@ export interface RoutingControllersOptions {
cors?: boolean | Object;

/**
* Global route prefix, for example '/api'.
* Global route prefix.
* Can be either:
* - a string (e.g. '/api')
* - a RegExp pattern for dynamic matching
* - a wildcard string (e.g. "/*\/api")
*/
routePrefix?: string;
routePrefix?: string | RegExp;

/**
* List of controllers to register in the framework or directories from where to import all your controllers.
Expand Down
5 changes: 3 additions & 2 deletions src/decorator/Get.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { HandlerOptions } from '../decorator-options/HandlerOptions';
import type { HandlerOptions } from '../decorator-options/HandlerOptions';
import { getMetadataArgsStorage } from '../index';
import type { ActionMetadataArgs } from '../metadata/args/ActionMetadataArgs';

/**
* Registers an action to be executed when GET request comes on a given route.
Expand All @@ -19,7 +20,7 @@ export function Get(route?: string, options?: HandlerOptions): Function;
*/
export function Get(route?: string | RegExp, options?: HandlerOptions): Function {
return function (object: Object, methodName: string) {
getMetadataArgsStorage().actions.push({
getMetadataArgsStorage().actions.push(<ActionMetadataArgs>{
type: 'get',
target: object.constructor,
method: methodName,
Expand Down
2 changes: 1 addition & 1 deletion src/driver/BaseDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export abstract class BaseDriver {
/**
* Global application prefix.
*/
routePrefix: string = '';
routePrefix: string | RegExp = '';

/**
* Indicates if cors are enabled.
Expand Down
11 changes: 6 additions & 5 deletions src/driver/express/ExpressDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { isPromiseLike } from '../../util/isPromiseLike';
import { getFromContainer } from '../../container';
import { AuthorizationRequiredError } from '../../error/AuthorizationRequiredError';
import { NotFoundError, RoutingControllersOptions } from '../../index';
import type { Express } from 'express';

// eslint-disable-next-line @typescript-eslint/no-var-requires
const cookie = require('cookie');
Expand All @@ -26,7 +27,7 @@ export class ExpressDriver extends BaseDriver {
// Constructor
// -------------------------------------------------------------------------

constructor(public express?: any) {
constructor(public express?: Express) {
super();
this.loadExpress();
this.app = this.express;
Expand All @@ -44,9 +45,9 @@ export class ExpressDriver extends BaseDriver {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const cors = require('cors');
if (this.cors === true) {
this.express.use(cors());
this.express?.use(cors());
} else {
this.express.use(cors(this.cors));
this.express?.use(cors(this.cors));
}
}
}
Expand Down Expand Up @@ -88,7 +89,7 @@ export class ExpressDriver extends BaseDriver {
writable: true,
});

this.express.use(options.routePrefix || '/', middlewareWrapper);
this.express?.use(options.routePrefix || '/', middlewareWrapper);
}
}

Expand Down Expand Up @@ -181,7 +182,7 @@ export class ExpressDriver extends BaseDriver {
};

// finally register action in express
this.express[actionMetadata.type.toLowerCase()](
this.express?.[actionMetadata.type.toLowerCase()](
...[route, routeGuard, ...beforeMiddlewares, ...defaultMiddlewares, routeHandler, ...afterMiddlewares]
);
}
Expand Down
15 changes: 8 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { RoutingControllers } from './RoutingControllers';
import { RoutingControllersOptions } from './RoutingControllersOptions';
import { ValidationOptions } from 'class-validator';
import { importClassesFromDirectories } from './util/importClassesFromDirectories';
import type { Express } from 'express';

// -------------------------------------------------------------------------
// Main exports
Expand Down Expand Up @@ -116,7 +117,7 @@ export function getMetadataArgsStorage(): MetadataArgsStorage {
* Registers all loaded actions in your express application.
*/
export function useExpressServer<T>(expressServer: T, options?: RoutingControllersOptions): T {
const driver = new ExpressDriver(expressServer);
const driver = new ExpressDriver(expressServer as Express);
return createServer(driver, options);
}

Expand Down Expand Up @@ -157,19 +158,19 @@ export function createServer<T extends BaseDriver>(driver: T, options?: RoutingC
*/
export function createExecutor<T extends BaseDriver>(driver: T, options: RoutingControllersOptions = {}): void {
// import all controllers and middlewares and error handlers (new way)
let controllerClasses: Function[];
let controllerClasses: Function[] = [];
if (options && options.controllers && options.controllers.length) {
controllerClasses = (options.controllers as any[]).filter(controller => controller instanceof Function);
const controllerDirs = (options.controllers as any[]).filter(controller => typeof controller === 'string');
controllerClasses.push(...importClassesFromDirectories(controllerDirs));
}
let middlewareClasses: Function[];
let middlewareClasses: Function[] = [];
if (options && options.middlewares && options.middlewares.length) {
middlewareClasses = (options.middlewares as any[]).filter(controller => controller instanceof Function);
const middlewareDirs = (options.middlewares as any[]).filter(controller => typeof controller === 'string');
middlewareClasses.push(...importClassesFromDirectories(middlewareDirs));
}
let interceptorClasses: Function[];
let interceptorClasses: Function[] = [];
if (options && options.interceptors && options.interceptors.length) {
interceptorClasses = (options.interceptors as any[]).filter(controller => controller instanceof Function);
const interceptorDirs = (options.interceptors as any[]).filter(controller => typeof controller === 'string');
Expand Down Expand Up @@ -201,8 +202,8 @@ export function createExecutor<T extends BaseDriver>(driver: T, options: Routing
driver.enableValidation = true;
}

driver.classToPlainTransformOptions = options.classToPlainTransformOptions;
driver.plainToClassTransformOptions = options.plainToClassTransformOptions;
if (options.classToPlainTransformOptions) driver.classToPlainTransformOptions = options.classToPlainTransformOptions;
if (options.plainToClassTransformOptions) driver.plainToClassTransformOptions = options.plainToClassTransformOptions;

if (options.errorOverridingMap !== undefined) driver.errorOverridingMap = options.errorOverridingMap;

Expand Down Expand Up @@ -234,7 +235,7 @@ export function createParamDecorator(options: CustomParameterDecorator) {
method: method,
index: index,
parse: false,
required: options.required,
required: !!options.required,
transform: options.value,
});
};
Expand Down
7 changes: 5 additions & 2 deletions src/metadata-builder/MetadataArgsStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export class MetadataArgsStorage {
/**
* Filters registered "use middlewares" by a given target class and method name.
*/
filterUsesWithTargetAndMethod(target: Function, methodName: string): UseMetadataArgs[] {
filterUsesWithTargetAndMethod(target: Function, methodName: string | undefined): UseMetadataArgs[] {
return this.uses.filter(use => {
return use.target === target && use.method === methodName;
});
Expand All @@ -103,7 +103,10 @@ export class MetadataArgsStorage {
/**
* Filters registered "use interceptors" by a given target class and method name.
*/
filterInterceptorUsesWithTargetAndMethod(target: Function, methodName: string): UseInterceptorMetadataArgs[] {
filterInterceptorUsesWithTargetAndMethod(
target: Function,
methodName: string | undefined
): UseInterceptorMetadataArgs[] {
return this.useInterceptors.filter(use => {
return use.target === target && use.method === methodName;
});
Expand Down
21 changes: 12 additions & 9 deletions src/metadata-builder/MetadataBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,21 @@ export class MetadataBuilder {
* Creates middleware metadatas.
*/
protected createMiddlewares(classes?: Function[]): MiddlewareMetadata[] {
const middlewares = !classes
? getMetadataArgsStorage().middlewares
: getMetadataArgsStorage().filterMiddlewareMetadatasForClasses(classes);
const middlewares =
!classes || classes.length === 0
? getMetadataArgsStorage().middlewares
: getMetadataArgsStorage().filterMiddlewareMetadatasForClasses(classes);
return middlewares.map(middlewareArgs => new MiddlewareMetadata(middlewareArgs));
}

/**
* Creates interceptor metadatas.
*/
protected createInterceptors(classes?: Function[]): InterceptorMetadata[] {
const interceptors = !classes
? getMetadataArgsStorage().interceptors
: getMetadataArgsStorage().filterInterceptorMetadatasForClasses(classes);
const interceptors =
!classes || classes.length === 0
? getMetadataArgsStorage().interceptors
: getMetadataArgsStorage().filterInterceptorMetadatasForClasses(classes);
return interceptors.map(
interceptorArgs =>
new InterceptorMetadata({
Expand All @@ -74,9 +76,10 @@ export class MetadataBuilder {
* Creates controller metadatas.
*/
protected createControllers(classes?: Function[]): ControllerMetadata[] {
const controllers = !classes
? getMetadataArgsStorage().controllers
: getMetadataArgsStorage().filterControllerMetadatasForClasses(classes);
const controllers =
!classes || classes.length === 0
? getMetadataArgsStorage().controllers
: getMetadataArgsStorage().filterControllerMetadatasForClasses(classes);
return controllers.map(controllerArgs => {
const controller = new ControllerMetadata(controllerArgs);
controller.build(this.createControllerResponseHandlers(controller));
Expand Down
43 changes: 41 additions & 2 deletions src/metadata/ActionMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,17 +175,56 @@ export class ActionMetadata {
/**
* Appends base route to a given regexp route.
*/
static appendBaseRoute(baseRoute: string, route: RegExp | string) {
/**
* Appends base route to a given regexp route.
*/
static appendBaseRoute(baseRoute: string | RegExp, route: RegExp | string): string | RegExp {
// If baseRoute is RegExp
if (baseRoute instanceof RegExp) {
const basePattern = baseRoute.source;

if (typeof route === 'string') {
// RegExp base + string route
return new RegExp(`${basePattern}${route}`, baseRoute.flags);
}
// RegExp base + RegExp route
return new RegExp(`${basePattern}${route.source}`, `${baseRoute.flags}${route.flags}`);
}

// Check for wildcard in string baseRoute
if (typeof baseRoute === 'string' && baseRoute.includes('*')) {
const regexBaseRoute = this.convertWildcardToRegex(baseRoute);

if (regexBaseRoute) {
return this.appendBaseRoute(regexBaseRoute, route);
}
}

const prefix = `${baseRoute.length > 0 && baseRoute.indexOf('/') < 0 ? '/' : ''}${baseRoute}`;
if (typeof route === 'string') return `${prefix}${route}`;

if (!baseRoute || baseRoute === '') return route;

const fullPath = `^${prefix}${route.toString().substr(1)}?$`;

return new RegExp(fullPath, route.flags);
}

/**
* Converts Express-style wildcard patterns to RegExp
*/
static convertWildcardToRegex(pattern: string): RegExp | null {
if (!pattern.includes('*')) return null;

// If we see *?, make that entire segment optional
if (pattern.includes('*?')) {
return new RegExp('^(?:/[^/]+)?');
}

const escapedPattern = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]+');

return new RegExp(`^${escapedPattern}`);
}

// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
Expand Down
Loading