Controller: fix route() parsing with regexp params

Also allow numbers in route param names
This commit is contained in:
Alice Gaudon 2021-03-30 10:45:14 +02:00
parent 4692a23696
commit 69e9f3ce9c
2 changed files with 47 additions and 4 deletions

View File

@ -9,6 +9,9 @@ import Middleware, {MiddlewareType} from "./Middleware";
import Application from "./Application"; import Application from "./Application";
export default abstract class Controller { export default abstract class Controller {
/**
* TODO: this should not be static, it should actually be bound to an app instance.
*/
private static readonly routes: { [p: string]: string | undefined } = {}; private static readonly routes: { [p: string]: string | undefined } = {};
public static route( public static route(
@ -20,11 +23,12 @@ export default abstract class Controller {
let path = this.routes[route]; let path = this.routes[route];
if (path === undefined) throw new Error(`Unknown route for name ${route}.`); if (path === undefined) throw new Error(`Unknown route for name ${route}.`);
const regExp = this.getRouteParamRegExp('[a-zA-Z0-9_-]+', 'g');
if (typeof params === 'string' || typeof params === 'number') { if (typeof params === 'string' || typeof params === 'number') {
path = path.replace(/:[a-zA-Z_-]+\??/g, '' + params); path = path.replace(regExp, '' + params);
} else if (Array.isArray(params)) { } else if (Array.isArray(params)) {
let i = 0; let i = 0;
for (const match of path.matchAll(/:[a-zA-Z_-]+(\(.*\))?\??/g)) { for (const match of path.matchAll(regExp)) {
if (match.length > 0) { if (match.length > 0) {
path = path.replace(match[0], typeof params[i] !== 'undefined' ? params[i] : ''); path = path.replace(match[0], typeof params[i] !== 'undefined' ? params[i] : '');
} }
@ -33,7 +37,7 @@ export default abstract class Controller {
path = path.replace(/\/+/g, '/'); path = path.replace(/\/+/g, '/');
} else { } else {
for (const key of Object.keys(params)) { for (const key of Object.keys(params)) {
path = path.replace(new RegExp(`:${key}\\??`), params[key]); path = path.replace(this.getRouteParamRegExp(key), params[key].toString());
} }
} }
@ -41,6 +45,10 @@ export default abstract class Controller {
return `${absolute ? config.get<string>('public_url') : ''}${path}` + (queryStr.length > 0 ? '?' + queryStr : ''); return `${absolute ? config.get<string>('public_url') : ''}${path}` + (queryStr.length > 0 ? '?' + queryStr : '');
} }
private static getRouteParamRegExp(key: string, flags?: string): RegExp {
return new RegExp(`:${key}(\\(.+?\\))?\\??`, flags);
}
private readonly router: Router = express.Router(); private readonly router: Router = express.Router();
private readonly fileUploadFormRouter: Router = express.Router(); private readonly fileUploadFormRouter: Router = express.Router();
private app?: Application; private app?: Application;
@ -194,4 +202,4 @@ export default abstract class Controller {
} }
} }
export type RouteParams = { [p: string]: string } | string[] | string | number; export type RouteParams = { [p: string]: string | number } | string[] | string | number;

35
test/Controller.test.ts Normal file
View File

@ -0,0 +1,35 @@
import Controller from "../src/Controller";
describe('Controller.route()', () => {
new class extends Controller {
public routes(): void {
const emptyHandler = () => {
//
};
this.get('/test/no-param', emptyHandler, 'test.no-param');
this.get('/test/no-param/', emptyHandler, 'test.no-param-slash');
this.get('/test/one-param/:param', emptyHandler, 'test.one-param');
this.get('/test/two-params/:param1/:param2', emptyHandler, 'test.two-params');
this.get('/test/paginated/:page([0-9]+)?', emptyHandler, 'test.paginated');
this.get('/test/slug/:slug([a-zA-Z0-9]+)?', emptyHandler, 'test.slug');
}
}().setupRoutes();
test('Empty params', () => {
expect(Controller.route('test.no-param')).toBe('/test/no-param');
expect(Controller.route('test.no-param-slash')).toBe('/test/no-param/');
expect(Controller.route('test.one-param')).toBe('/test/one-param/');
expect(Controller.route('test.two-params')).toBe('/test/two-params/');
expect(Controller.route('test.paginated')).toBe('/test/paginated/');
expect(Controller.route('test.slug')).toBe('/test/slug/');
});
test('Populated params', () => {
expect(Controller.route('test.no-param')).toBe('/test/no-param');
expect(Controller.route('test.no-param-slash')).toBe('/test/no-param/');
expect(Controller.route('test.one-param', {param: 'value'})).toBe('/test/one-param/value');
expect(Controller.route('test.two-params', {param1: 'value1', param2: 'value2'})).toBe('/test/two-params/value1/value2');
expect(Controller.route('test.paginated', {page: 5})).toBe('/test/paginated/5');
expect(Controller.route('test.slug', {slug: 'abc'})).toBe('/test/slug/abc');
});
});