Move route building to common subproject, fix Time export

This commit is contained in:
Alice Gaudon 2021-06-01 14:34:04 +02:00
parent 1b221c590f
commit 7ccd335649
19 changed files with 185 additions and 145 deletions

View File

@ -6,6 +6,7 @@ import * as path from "path";
import ApplicationComponent from "./ApplicationComponent.js";
import CacheProvider from "./CacheProvider.js";
import {route, setPublicUrl} from "./common/Routing.js";
import FrontendToolsComponent from "./components/FrontendToolsComponent.js";
import LogRequestsComponent from "./components/LogRequestsComponent.js";
import RedisComponent from "./components/RedisComponent.js";
@ -37,6 +38,8 @@ export default abstract class Application implements Extendable<ApplicationCompo
protected constructor(version: string, ignoreCommandLine: boolean = false) {
this.version = version;
this.ignoreCommandLine = ignoreCommandLine;
setPublicUrl(config.get<string>('app.public_url'));
}
protected abstract getMigrations(): MigrationType<Migration>[];
@ -163,7 +166,7 @@ export default abstract class Application implements Extendable<ApplicationCompo
},
html: () => {
req.flash('validation', bag.getMessages());
res.redirect(req.getPreviousUrl() || Controller.route('home'));
res.redirect(req.getPreviousUrl() || route('home'));
},
});
return;

View File

@ -1,55 +1,13 @@
import config from "config";
import express, {IRouter, RequestHandler, Router} from "express";
import {PathParams} from "express-serve-static-core";
import * as querystring from "querystring";
import {ParsedUrlQueryInput} from "querystring";
import Application from "./Application.js";
import {registerRoute} from "./common/Routing.js";
import FileUploadMiddleware from "./FileUploadMiddleware.js";
import {logger} from "./Logger.js";
import Middleware, {MiddlewareType} from "./Middleware.js";
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 } = {};
public static route(
route: string,
params: RouteParams = [],
query: ParsedUrlQueryInput = {},
absolute: boolean = false,
): string {
let path = this.routes[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') {
path = path.replace(regExp, '' + params);
} else if (Array.isArray(params)) {
let i = 0;
for (const match of path.matchAll(regExp)) {
if (match.length > 0) {
path = path.replace(match[0], typeof params[i] !== 'undefined' ? params[i] : '');
}
i++;
}
path = path.replace(/\/+/g, '/');
} else {
for (const key of Object.keys(params)) {
path = path.replace(this.getRouteParamRegExp(key), params[key].toString());
}
}
const queryStr = querystring.stringify(query);
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 fileUploadFormRouter: Router = express.Router();
private app?: Application;
@ -183,13 +141,15 @@ export default abstract class Controller {
routePath = (prefix !== '/' ? prefix : '') + path;
}
if (!Controller.routes[routeName]) {
if (typeof routePath === 'string') {
logger.info(`Route ${routeName} has path ${routePath}`);
Controller.routes[routeName] = routePath;
} else {
logger.warn(`Cannot assign path to route ${routeName}.`);
}
if (typeof routePath !== 'string') {
logger.warn(`Cannot assign path to route ${routeName}.`);
return;
}
if (registerRoute(routeName, routePath)) {
logger.info(`Route ${routeName} has path ${routePath}`);
} else {
logger.warn(`Couldn't register ${routeName} for path ${routePath}`);
}
}
@ -202,5 +162,3 @@ export default abstract class Controller {
this.app = app;
}
}
export type RouteParams = { [p: string]: string | number } | string[] | string | number;

View File

@ -6,6 +6,9 @@
<style>%css%</style>
<script type="module" defer>
import View from '/js/views/%canonicalViewName%.js';
import * as Routing from '/js/Routing.js';
const setRoutes = Routing.R.setRoutes;
const setPublicUrl = Routing.R.setPublicUrl;
import * as stores from '/js/stores.js';
const localStore = stores.s.locals;
@ -16,6 +19,9 @@
: `'${key}'`];
});
setRoutes(%routes%);
setPublicUrl(%publicUrl%);
new View({
hydrate: true,
target: document.body,

View File

@ -1,7 +1,8 @@
import config from "config";
import {Request, Response} from "express";
import Time from "../common/Time.js";
import {route} from "../common/Routing.js";
import {Time} from "../common/Time.js";
import Controller from "../Controller.js";
import ModelFactory from "../db/ModelFactory.js";
import Validator, {EMAIL_REGEX, InvalidFormatValidationError} from "../db/Validator.js";
@ -81,14 +82,14 @@ export default class AccountController extends Controller {
const nameChangedAt = userNameComponent.getNameChangedAt()?.getTime() || Date.now();
const nameChangeWaitPeriod = config.get<number>('auth.name_change_wait_period');
req.flash('error', `Your can't change your name until ${new Date(nameChangedAt + nameChangeWaitPeriod)}.`);
res.redirect(Controller.route('account'));
res.redirect(route('account'));
return;
}
await user.save();
req.flash('success', `Your name was successfully changed to ${req.body.name}.`);
res.redirect(Controller.route('account'));
res.redirect(route('account'));
}
protected async postChangePassword(req: Request, res: Response): Promise<void> {
@ -101,7 +102,7 @@ export default class AccountController extends Controller {
const passwordComponent = user.as(UserPasswordComponent);
if (passwordComponent.hasPassword() && !await passwordComponent.verifyPassword(req.body.current_password)) {
req.flash('error', 'Invalid current password.');
res.redirect(Controller.route('account'));
res.redirect(route('account'));
return;
}
@ -109,7 +110,7 @@ export default class AccountController extends Controller {
await user.save();
req.flash('success', 'Password changed successfully.');
res.redirect(Controller.route('account'));
res.redirect(route('account'));
}
protected async postRemovePassword(req: Request, res: Response): Promise<void> {
@ -117,7 +118,7 @@ export default class AccountController extends Controller {
const mainEmail = await user.mainEmail.get();
if (!mainEmail || !mainEmail.email) {
req.flash('error', 'You can\'t remove your password without adding an email address first.');
res.redirect(Controller.route('account'));
res.redirect(route('account'));
return;
}
@ -126,14 +127,14 @@ export default class AccountController extends Controller {
this.getApp(),
req.getSession().id,
AuthMagicLinkActionType.REMOVE_PASSWORD,
Controller.route('account'),
route('account'),
mainEmail.email,
this.removePasswordMailTemplate,
{},
);
res.redirect(Controller.route('magic_link_lobby', undefined, {
redirect_uri: Controller.route('account'),
res.redirect(route('magic_link_lobby', undefined, {
redirect_uri: route('account'),
}));
}
@ -156,7 +157,7 @@ export default class AccountController extends Controller {
this.getApp(),
req.getSession().id,
AuthMagicLinkActionType.ADD_EMAIL,
Controller.route('account'),
route('account'),
email,
this.addEmailMailTemplate,
{
@ -164,8 +165,8 @@ export default class AccountController extends Controller {
},
);
res.redirect(Controller.route('magic_link_lobby', undefined, {
redirect_uri: Controller.route('account'),
res.redirect(route('magic_link_lobby', undefined, {
redirect_uri: route('account'),
}));
}
@ -188,7 +189,7 @@ export default class AccountController extends Controller {
await user.save();
req.flash('success', 'This email was successfully set as your main address.');
res.redirect(Controller.route('account'));
res.redirect(route('account'));
}
protected async postRemoveEmail(req: Request, res: Response): Promise<void> {
@ -208,6 +209,6 @@ export default class AccountController extends Controller {
await userEmail.delete();
req.flash('success', 'This email was successfully removed from your account.');
res.redirect(Controller.route('account'));
res.redirect(route('account'));
}
}

View File

@ -2,7 +2,7 @@ import {NextFunction, Request, Response} from "express";
import Application from "../Application.js";
import ApplicationComponent from "../ApplicationComponent.js";
import Controller from "../Controller.js";
import {route} from "../common/Routing.js";
import {ForbiddenHttpError} from "../HttpError.js";
import Middleware from "../Middleware.js";
import AuthGuard from "./AuthGuard.js";
@ -66,7 +66,7 @@ export class RequireRequestAuthMiddleware extends Middleware {
}
req.flash('error', `You must be logged in to access ${req.url}.`);
res.redirect(Controller.route('auth', undefined, {
res.redirect(route('auth', undefined, {
redirect_uri: req.url,
}));
}
@ -102,7 +102,7 @@ export class RequireAuthMiddleware extends Middleware {
}
req.flash('error', `You must be logged in to access ${req.url}.`);
res.redirect(Controller.route('auth', undefined, {
res.redirect(route('auth', undefined, {
redirect_uri: req.url,
}));
}
@ -117,7 +117,7 @@ export class RequireGuestMiddleware extends Middleware {
protected async handle(req: Request, res: Response, next: NextFunction): Promise<void> {
const proofs = await req.as(AuthMiddleware).getAuthGuard().getProofsForSession(req.getSession());
if (proofs.length > 0) {
res.redirect(Controller.route('home'));
res.redirect(route('home'));
return;
}

View File

@ -4,8 +4,8 @@ import {Session, SessionData} from "express-session";
import {Connection} from "mysql";
import Application from "../Application.js";
import {route} from "../common/Routing.js";
import MailComponent from "../components/MailComponent.js";
import Controller from "../Controller.js";
import MysqlConnectionManager from "../db/MysqlConnectionManager.js";
import Mail from "../mail/Mail.js";
import {PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE} from "../Mails.js";
@ -150,7 +150,7 @@ export default class AuthGuard {
username: user.asOptional(UserNameComponent)?.getName() ||
(await user.mainEmail.get())?.getOrFail('email') ||
'Could not find an identifier',
link: config.get<string>('public_url') + Controller.route('accounts-approval'),
link: route('accounts-approval', {}, {}, true),
}), config.get<string>('app.contact_email'));
}
}

View File

@ -3,7 +3,7 @@ import {Session} from "express-session";
import geoip from "geoip-lite";
import Application from "../../Application.js";
import Controller from "../../Controller.js";
import {route} from "../../common/Routing.js";
import ModelFactory from "../../db/ModelFactory.js";
import {WhereTest} from "../../db/ModelQuery.js";
import Validator, {EMAIL_REGEX} from "../../db/Validator.js";
@ -55,7 +55,7 @@ export default class MagicLinkAuthMethod implements AuthMethod<MagicLink> {
if (pendingLink) {
if (await pendingLink.isValid()) {
res.redirect(Controller.route('magic_link_lobby', undefined, {
res.redirect(route('magic_link_lobby', undefined, {
redirect_uri: req.getIntendedUrl() || pendingLink.original_url || undefined,
}));
return true;
@ -105,7 +105,7 @@ export default class MagicLinkAuthMethod implements AuthMethod<MagicLink> {
this.app,
req.getSession().id,
actionType,
Controller.route('auth', undefined, {
route('auth', undefined, {
redirect_uri: req.getIntendedUrl() || undefined,
}),
email,
@ -120,7 +120,7 @@ export default class MagicLinkAuthMethod implements AuthMethod<MagicLink> {
},
);
res.redirect(Controller.route('magic_link_lobby', undefined, {
res.redirect(route('magic_link_lobby', undefined, {
redirect_uri: req.getIntendedUrl(),
}));
}

View File

@ -1,8 +1,8 @@
import config from "config";
import {Request, Response} from "express";
import {ParsedUrlQueryInput} from "querystring";
import Application from "../../Application.js";
import {QueryParamsRecord, route} from "../../common/Routing.js";
import MailComponent from "../../components/MailComponent.js";
import Controller from "../../Controller.js";
import {QueryVariable} from "../../db/MysqlConnectionManager.js";
@ -30,7 +30,7 @@ export default class MagicLinkController<A extends Application> extends Controll
original_url: string,
email: string,
mailTemplate: MailTemplate,
data: ParsedUrlQueryInput,
data: QueryParamsRecord,
magicLinkData: Record<string, QueryVariable> = {},
): Promise<void> {
Throttler.throttle('magic_link', process.env.NODE_ENV === 'test' ? 10 : 2, MagicLink.validityPeriod(),
@ -49,10 +49,10 @@ export default class MagicLinkController<A extends Application> extends Controll
// Send email
await app.as(MailComponent).sendMail(new Mail(mailTemplate, Object.assign(data, {
link: `${config.get<string>('public_url')}${Controller.route('magic_link', undefined, {
link: `${route('magic_link', undefined, {
id: link.id,
token: token,
})}`,
}, true)}`,
})), email);
}
@ -90,7 +90,7 @@ export default class MagicLinkController<A extends Application> extends Controll
} catch (e) {
if (e instanceof PendingApprovalAuthError) {
req.flash('warning', `Your account is pending review. You'll receive an email once you're approved.`);
res.redirect(Controller.route('auth'));
res.redirect(route('auth'));
return null;
} else {
throw e;
@ -191,7 +191,7 @@ export default class MagicLinkController<A extends Application> extends Controll
// Auth success
const name = user.asOptional(UserNameComponent)?.getName();
req.flash('success', `Authentication success. Welcome${name ? `, ${name}` : ''}`);
res.redirect(req.getIntendedUrl() || Controller.route('home'));
res.redirect(req.getIntendedUrl() || route('home'));
}
break;
}
@ -211,7 +211,7 @@ export default class MagicLinkController<A extends Application> extends Controll
if (await UserEmail.select().with('user').where('email', email).first()) {
req.flash('error', 'An account already exists with this email address.' +
' Please first remove it there before adding it here.');
res.redirect(Controller.route('account'));
res.redirect(route('account'));
return;
}
@ -228,7 +228,7 @@ export default class MagicLinkController<A extends Application> extends Controll
}
req.flash('success', `Email address ${userEmail.email} successfully added.`);
res.redirect(Controller.route('account'));
res.redirect(route('account'));
break;
}
@ -250,7 +250,7 @@ export default class MagicLinkController<A extends Application> extends Controll
}
req.flash('success', `Password successfully removed.`);
res.redirect(Controller.route('account'));
res.redirect(route('account'));
break;
}

View File

@ -2,7 +2,7 @@ import {Request, Response} from "express";
import {Session} from "express-session";
import Application from "../../Application.js";
import Controller from "../../Controller.js";
import {route} from "../../common/Routing.js";
import ModelFactory from "../../db/ModelFactory.js";
import Validator, {InvalidFormatValidationError} from "../../db/Validator.js";
import {ServerError} from "../../HttpError.js";
@ -73,7 +73,7 @@ export default class PasswordAuthMethod implements AuthMethod<PasswordAuthProof>
if (e instanceof PendingApprovalAuthError) {
req.flash('error', 'Your account is still being reviewed.');
res.redirect(Controller.route('auth'));
res.redirect(route('auth'));
return;
} else {
const err = new InvalidFormatValidationError('Invalid password.');
@ -86,7 +86,7 @@ export default class PasswordAuthMethod implements AuthMethod<PasswordAuthProof>
}
req.flash('success', `Welcome, ${user.name}.`);
res.redirect(req.getIntendedUrl() || Controller.route('home'));
res.redirect(req.getIntendedUrl() || route('home'));
}
public async attemptRegister(req: Request, res: Response, identifier: string): Promise<void> {
@ -124,7 +124,7 @@ export default class PasswordAuthMethod implements AuthMethod<PasswordAuthProof>
} catch (e) {
if (e instanceof PendingApprovalAuthError) {
req.flash('info', `Your account was successfully created and is pending review from an administrator.`);
res.redirect(Controller.route('auth'));
res.redirect(route('auth'));
return;
} else {
throw e;
@ -134,7 +134,7 @@ export default class PasswordAuthMethod implements AuthMethod<PasswordAuthProof>
const user = await passwordAuthProof.getResource();
req.flash('success', `Your account was successfully created! Welcome, ${user?.as(UserNameComponent).getName()}.`);
res.redirect(req.getIntendedUrl() || Controller.route('home'));
res.redirect(req.getIntendedUrl() || route('home'));
}
}

68
src/common/Routing.ts Normal file
View File

@ -0,0 +1,68 @@
export type RouteParams = { [p: string]: string | number } | string[] | string | number;
export type QueryParamsRecord = Record<string, string | number | boolean | null | undefined>;
export type QueryParams = string[][] | QueryParamsRecord | string | URLSearchParams;
export let routes: Record<string, string | undefined> = {};
export function setRoutes(newRoutes: Record<string, string | undefined>): void {
routes = newRoutes;
}
export function registerRoute(routeName: string, routePath: string): boolean {
if (!routes[routeName]) {
routes[routeName] = routePath;
return true;
}
return false;
}
export let publicUrl: string;
export function setPublicUrl(newPublicUrl: string): void {
publicUrl = newPublicUrl;
}
export function route(
route: string,
params: RouteParams = [],
query: QueryParams = '',
absolute: boolean = false,
): string {
let path = routes[route];
if (path === undefined) throw new Error(`Unknown route for name ${route}.`);
const regExp = getRouteParamRegExp('[a-zA-Z0-9_-]+', 'g');
if (typeof params === 'string' || typeof params === 'number') {
path = path.replace(regExp, '' + params);
} else if (Array.isArray(params)) {
let i = 0;
for (const match of path.matchAll(regExp)) {
if (match.length > 0) {
path = path.replace(match[0], typeof params[i] !== 'undefined' ? params[i] : '');
}
i++;
}
path = path.replace(/\/+/g, '/');
} else {
for (const key of Object.keys(params)) {
path = path.replace(getRouteParamRegExp(key), params[key].toString());
}
}
let filteredQuery: string[][] | Record<string, string> | string | URLSearchParams;
if (typeof query !== "string" && !(query instanceof URLSearchParams) && !Array.isArray(query)) {
for (const key of Object.keys(query)) {
if (query[key] === undefined || query[key] === null) delete query[key];
else if (typeof query[key] === "number") query[key] = (query[key] as number).toString();
else if (typeof query[key] === "boolean") query[key] = query[key] ? '1' : '0';
}
filteredQuery = query as Record<string, string>;
} else {
filteredQuery = query;
}
const queryStr = new URLSearchParams(filteredQuery).toString();
return `${absolute ? publicUrl : ''}${path}` + (queryStr.length > 0 ? '?' + queryStr : '');
}
export function getRouteParamRegExp(key: string, flags?: string): RegExp {
return new RegExp(`:${key}(\\(.+?\\))?\\??`, flags);
}

View File

@ -7,7 +7,7 @@ export class TimeUnit {
}
}
export default class Time {
export class Time {
public static readonly UNITS = {
MILLISECOND: {
milliseconds: 1,
@ -89,5 +89,4 @@ export default class Time {
return 'now';
}
}

View File

@ -1,11 +1,10 @@
import config from "config";
import {Express, Router} from "express";
import path from "path";
import {ParsedUrlQueryInput} from "querystring";
import util from "util";
import ApplicationComponent from "../ApplicationComponent.js";
import Controller, {RouteParams} from "../Controller.js";
import {QueryParams, route, RouteParams} from "../common/Routing.js";
import AssetCompiler from "../frontend/AssetCompiler.js";
import AssetPreCompiler from "../frontend/AssetPreCompiler.js";
import Globals from "../frontend/Globals.js";
@ -40,11 +39,11 @@ export default class FrontendToolsComponent extends ApplicationComponent {
public async init(): Promise<void> {
this.globals.set('route', (
route: string,
routeName: string,
params: RouteParams = [],
query: ParsedUrlQueryInput = {},
query: QueryParams = '',
absolute: boolean = false,
) => Controller.route(route, params, query, absolute));
) => route(routeName, params, query, absolute));
this.globals.set('app_version', this.getApp().getVersion());
this.globals.set('core_version', this.getApp().getCoreVersion());
this.globals.set('app', config.get('app'));

View File

@ -4,9 +4,8 @@ import nodemailer, {SentMessageInfo, Transporter} from "nodemailer";
import util from "util";
import ApplicationComponent from "../ApplicationComponent.js";
import Controller from "../Controller.js";
import {route} from "../common/Routing.js";
import MailViewEngine from "../frontend/MailViewEngine.js";
import ViewEngine from "../frontend/ViewEngine.js";
import {logger} from "../Logger.js";
import Mail from "../mail/Mail.js";
import MailError from "../mail/MailError.js";
@ -91,7 +90,7 @@ export default class MailComponent extends ApplicationComponent {
locals.mail_subject = options.subject;
locals.mail_to = options.to;
locals.mail_link = config.get<string>('public_url') +
Controller.route('mail', [template.template], locals);
route('mail', [template.template], locals);
Object.assign(locals, this.getApp().as(FrontendToolsComponent).getGlobals().get());
// Log

View File

@ -9,6 +9,7 @@ import requireFromString from "require-from-string";
import {compile, preprocess} from "svelte/compiler";
import {sveltePreprocess} from "svelte-preprocess/dist/autoProcess.js";
import {publicUrl, routes} from "../common/Routing.js";
import {logger} from "../Logger.js";
import FileCache from "../utils/FileCache.js";
import ViewEngine from "./ViewEngine.js";
@ -89,6 +90,9 @@ export default class SvelteViewEngine extends ViewEngine {
head: head,
html: html,
css: css,
routes: JSON.stringify(routes),
publicUrl: publicUrl,
};
return rawOutput.replace(
new RegExp(Object.keys(replaceMap).map(str => `%${str}%`).join('|'), 'g'),

View File

@ -1,4 +1,3 @@
import config from "config";
import {Request, Response} from "express";
import {RequireAdminMiddleware, RequireAuthMiddleware} from "../auth/AuthComponent.js";
@ -6,6 +5,7 @@ import User from "../auth/models/User.js";
import UserApprovedComponent from "../auth/models/UserApprovedComponent.js";
import UserEmail from "../auth/models/UserEmail.js";
import UserNameComponent from "../auth/models/UserNameComponent.js";
import {route} from "../common/Routing.js";
import MailComponent from "../components/MailComponent.js";
import Controller from "../Controller.js";
import ModelFactory from "../db/ModelFactory.js";
@ -24,7 +24,7 @@ export default class BackendController extends Controller {
super();
if (User.isApprovalMode()) {
BackendController.registerMenuElement({
getLink: async () => Controller.route('accounts-approval'),
getLink: async () => route('accounts-approval'),
getDisplayString: async () => {
const pendingUsersCount = (await User.select()
.where('approved', false)
@ -81,12 +81,12 @@ export default class BackendController extends Controller {
if (email && email.email) {
await this.getApp().as(MailComponent).sendMail(new Mail(ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE, {
approved: true,
link: config.get<string>('public_url') + Controller.route('auth'),
link: route('auth', {}, {}, true),
}), email.email);
}
req.flash('success', `Account successfully approved.`);
res.redirect(Controller.route('accounts-approval'));
res.redirect(route('accounts-approval'));
}
protected async postRejectAccount(req: Request, res: Response): Promise<void> {
@ -101,7 +101,7 @@ export default class BackendController extends Controller {
}
req.flash('success', `Account successfully deleted.`);
res.redirect(Controller.route('accounts-approval'));
res.redirect(route('accounts-approval'));
}
protected async accountRequest(req: Request): Promise<{
@ -122,7 +122,7 @@ export default class BackendController extends Controller {
export interface BackendMenuElement {
/**
* Returns the link of this menu element (usually using {@code Controller.route})
* Returns the link of this menu element (usually using {@code route})
*/
getLink(): Promise<string>;

View File

@ -1,6 +1,7 @@
import {Options} from "nodemailer/lib/mailer";
import {ParsedUrlQueryInput} from "querystring";
import {QueryParams, QueryParamsRecord} from "../common/Routing.js";
import MailError from "./MailError.js";
import MailTemplate from "./MailTemplate.js";
@ -9,7 +10,7 @@ export default class Mail {
public constructor(
private readonly template: MailTemplate,
private readonly data: ParsedUrlQueryInput = {},
private readonly data: QueryParamsRecord = {},
) {
this.options.subject = this.template.getSubject(data);
@ -30,7 +31,7 @@ export default class Mail {
return this.template;
}
public getData(): ParsedUrlQueryInput {
public getData(): QueryParamsRecord {
return {...this.data};
}

View File

@ -1,35 +0,0 @@
import Controller from "../src/Controller.js";
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');
});
});

View File

@ -0,0 +1,36 @@
import {route} from "../../src/common/Routing.js";
import Controller from "../../src/Controller.js";
describe('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(route('test.no-param')).toBe('/test/no-param');
expect(route('test.no-param-slash')).toBe('/test/no-param/');
expect(route('test.one-param')).toBe('/test/one-param/');
expect(route('test.two-params')).toBe('/test/two-params/');
expect(route('test.paginated')).toBe('/test/paginated/');
expect(route('test.slug')).toBe('/test/slug/');
});
test('Populated params', () => {
expect(route('test.no-param')).toBe('/test/no-param');
expect(route('test.no-param-slash')).toBe('/test/no-param/');
expect(route('test.one-param', {param: 'value'})).toBe('/test/one-param/value');
expect(route('test.two-params', {param1: 'value1', param2: 'value2'})).toBe('/test/two-params/value1/value2');
expect(route('test.paginated', {page: 5})).toBe('/test/paginated/5');
expect(route('test.slug', {slug: 'abc'})).toBe('/test/slug/abc');
});
});

View File

@ -21,7 +21,8 @@
"src/types"
],
"lib": [
"es2020"
"es2020",
"dom"
],
"resolveJsonModule": true,
"skipLibCheck": true,