feat(core): shared router between lightweight and server

This commit is contained in:
Elian Doran 2026-01-07 11:37:43 +02:00
parent 8274f9a220
commit 22590596da
No known key found for this signature in database
5 changed files with 69 additions and 86 deletions

View File

@ -32,14 +32,14 @@ const encoder = new TextEncoder();
/**
* Convert an Express-style path pattern to a RegExp.
* Supports :param syntax for path parameters.
*
*
* Examples:
* /api/notes/:noteId -> /^\/api\/notes\/([^\/]+)$/
* /api/notes/:noteId/revisions -> /^\/api\/notes\/([^\/]+)\/revisions$/
*/
function pathToRegex(path: string): { pattern: RegExp; paramNames: string[] } {
const paramNames: string[] = [];
// Escape special regex characters except for :param patterns
const regexPattern = path
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape special chars
@ -47,7 +47,7 @@ function pathToRegex(path: string): { pattern: RegExp; paramNames: string[] } {
paramNames.push(paramName);
return '([^/]+)';
});
return {
pattern: new RegExp(`^${regexPattern}$`),
paramNames
@ -60,7 +60,7 @@ function pathToRegex(path: string): { pattern: RegExp; paramNames: string[] } {
function parseQuery(search: string): Record<string, string | undefined> {
const query: Record<string, string | undefined> = {};
if (!search || search === '?') return query;
const params = new URLSearchParams(search);
for (const [key, value] of params) {
query[key] = value;
@ -206,11 +206,11 @@ export class BrowserRouter {
// Check for known error types
if (error && typeof error === 'object') {
const err = error as { constructor?: { name?: string }; message?: string };
if (err.constructor?.name === 'NotFoundError') {
return jsonResponse({ message: err.message || 'Not found' }, 404);
}
if (err.constructor?.name === 'ValidationError') {
return jsonResponse({ message: err.message || 'Validation error' }, 400);
}

View File

@ -1,80 +1,54 @@
/**
* Browser route definitions.
* This mirrors the server's routes.ts but for the browser worker.
* This integrates with the shared route builder from @triliumnext/core.
*/
import type { routes as coreRoutes } from '@triliumnext/core';
import { routes } from '@triliumnext/core';
import { BrowserRouter, type BrowserRequest } from './browser_router';
type CoreRoutes = typeof coreRoutes;
type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';
/**
* Register all API routes on the browser router.
*
* @param router - The browser router instance
* @param routes - The core routes module from @triliumnext/core
* Wraps a core route handler to work with the BrowserRouter.
* Core handlers expect an Express-like request object with params, query, and body.
*/
export function registerRoutes(router: BrowserRouter, routes: CoreRoutes): void {
const { optionsApiRoute, treeApiRoute, keysApiRoute } = routes;
// Tree routes
router.get('/api/tree', (req) =>
treeApiRoute.getTree({
query: {
subTreeNoteId: req.query.subTreeNoteId
}
} as any)
);
router.post('/api/tree/load', (req) =>
treeApiRoute.load({
function wrapHandler(handler: (req: any) => unknown) {
return (req: BrowserRequest) => {
// Create an Express-like request object
const expressLikeReq = {
params: req.params,
query: req.query,
body: req.body
} as any)
);
};
return handler(expressLikeReq);
};
}
// Options routes
router.get('/api/options', () =>
optionsApiRoute.getOptions()
);
router.put('/api/options/:name/:value', (req) =>
optionsApiRoute.updateOption({
params: req.params
} as any)
);
router.put('/api/options', (req) =>
optionsApiRoute.updateOptions({
body: req.body
} as any)
);
router.get('/api/options/user-themes', () =>
optionsApiRoute.getUserThemes()
);
router.get('/api/options/locales', () =>
optionsApiRoute.getSupportedLocales()
);
/**
* Creates an apiRoute function compatible with buildSharedApiRoutes.
* This bridges the core's route registration to the BrowserRouter.
*/
function createApiRoute(router: BrowserRouter) {
return (method: HttpMethod, path: string, handler: (req: any) => unknown) => {
router.register(method, path, wrapHandler(handler));
};
}
// Keyboard actions routes
router.get('/api/keyboard-actions', () =>
keysApiRoute.getKeyboardActions()
);
router.get('/api/keyboard-shortcuts-for-notes', () =>
keysApiRoute.getShortcutsForNotes()
);
// Add more routes here as they are implemented in @triliumnext/core
// Follow the pattern from apps/server/src/routes/routes.ts
/**
* Register all API routes on the browser router using the shared builder.
*
* @param router - The browser router instance
*/
export function registerRoutes(router: BrowserRouter): void {
const apiRoute = createApiRoute(router);
routes.buildSharedApiRoutes(apiRoute);
}
/**
* Create and configure a router with all routes registered.
*/
export function createConfiguredRouter(routes: CoreRoutes): BrowserRouter {
export function createConfiguredRouter(): BrowserRouter {
const router = new BrowserRouter();
registerRoutes(router, routes);
registerRoutes(router);
return router;
}

View File

@ -95,7 +95,7 @@ async function initialize(): Promise<void> {
console.log("[Worker] Supported routes", Object.keys(coreModule.routes));
// Create and configure the router
router = createConfiguredRouter(coreModule.routes);
router = createConfiguredRouter();
console.log("[Worker] Router configured");
console.log("[Worker] Initializing becca...");
@ -200,7 +200,7 @@ async function dispatch(request: LocalRequest) {
const url = new URL(request.url);
console.log("[Worker] Dispatch:", url.pathname);
// Bootstrap is handled specially before the router is ready
if (request.method === "GET" && url.pathname === "/bootstrap") {
return handleBootstrap();

View File

@ -1,4 +1,3 @@
import { routes } from "@triliumnext/core";
import { createPartialContentHandler } from "@triliumnext/express-partial-content";
import express from "express";
import rateLimit from "express-rate-limit";
@ -66,6 +65,7 @@ import loginRoute from "./login.js";
import { apiResultHandler, apiRoute, asyncApiRoute, asyncRoute, route, router, uploadMiddlewareWithErrorHandling } from "./route_api.js";
// page routes
import setupRoute from "./setup.js";
import { routes } from "@triliumnext/core";
const GET = "get",
PST = "post",
@ -103,9 +103,7 @@ function register(app: express.Application) {
apiRoute(GET, '/api/totp_recovery/enabled', recoveryCodes.checkForRecoveryKeys);
apiRoute(GET, '/api/totp_recovery/used', recoveryCodes.getUsedRecoveryCodes);
const { treeApiRoute } = routes;
apiRoute(GET, '/api/tree', treeApiRoute.getTree);
apiRoute(PST, '/api/tree/load', treeApiRoute.load);
routes.buildSharedApiRoutes(apiRoute);
apiRoute(GET, "/api/notes/:noteId", notesApiRoute.getNote);
apiRoute(GET, "/api/notes/:noteId/blob", notesApiRoute.getNoteBlob);
@ -210,14 +208,6 @@ function register(app: express.Application) {
route(GET, "/api/images/:noteId/:filename", [auth.checkApiAuthOrElectron], imageRoute.returnImageFromNote);
route(PUT, "/api/images/:noteId", [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], imageRoute.updateImage, apiResultHandler);
const { optionsApiRoute } = routes;
apiRoute(GET, "/api/options", optionsApiRoute.getOptions);
// FIXME: possibly change to sending value in the body to avoid host of HTTP server issues with slashes
apiRoute(PUT, "/api/options/:name/:value", optionsApiRoute.updateOption);
apiRoute(PUT, "/api/options", optionsApiRoute.updateOptions);
apiRoute(GET, "/api/options/user-themes", optionsApiRoute.getUserThemes);
apiRoute(GET, "/api/options/locales", optionsApiRoute.getSupportedLocales);
apiRoute(PST, "/api/password/change", passwordApiRoute.changePassword);
apiRoute(PST, "/api/password/reset", passwordApiRoute.resetPassword);
@ -331,9 +321,6 @@ function register(app: express.Application) {
asyncRoute(PST, "/api/sender/image", [auth.checkEtapiToken, uploadMiddlewareWithErrorHandling], senderRoute.uploadImage, apiResultHandler);
asyncRoute(PST, "/api/sender/note", [auth.checkEtapiToken], senderRoute.saveNote, apiResultHandler);
apiRoute(GET, "/api/keyboard-actions", routes.keysApiRoute.getKeyboardActions);
apiRoute(GET, "/api/keyboard-shortcuts-for-notes", routes.keysApiRoute.getShortcutsForNotes);
apiRoute(PST, "/api/relation-map", relationMapApiRoute.getRelationMap);
apiRoute(PST, "/api/notes/erase-deleted-notes-now", notesApiRoute.eraseDeletedNotesNow);
apiRoute(PST, "/api/notes/erase-unused-attachments-now", notesApiRoute.eraseUnusedAttachmentsNow);

View File

@ -1,3 +1,25 @@
export { default as optionsApiRoute } from "./api/options";
export { default as treeApiRoute } from "./api/tree";
export { default as keysApiRoute } from "./api/keys";
import optionsApiRoute from "./api/options";
import treeApiRoute from "./api/tree";
import keysApiRoute from "./api/keys";
// TODO: Deduplicate with routes.ts
const GET = "get",
PST = "post",
PUT = "put",
PATCH = "patch",
DEL = "delete";
export function buildSharedApiRoutes(apiRoute: any) {
apiRoute(GET, '/api/tree', treeApiRoute.getTree);
apiRoute(PST, '/api/tree/load', treeApiRoute.load);
apiRoute(GET, "/api/options", optionsApiRoute.getOptions);
// FIXME: possibly change to sending value in the body to avoid host of HTTP server issues with slashes
apiRoute(PUT, "/api/options/:name/:value", optionsApiRoute.updateOption);
apiRoute(PUT, "/api/options", optionsApiRoute.updateOptions);
apiRoute(GET, "/api/options/user-themes", optionsApiRoute.getUserThemes);
apiRoute(GET, "/api/options/locales", optionsApiRoute.getSupportedLocales);
apiRoute(GET, "/api/keyboard-actions", keysApiRoute.getKeyboardActions);
apiRoute(GET, "/api/keyboard-shortcuts-for-notes", keysApiRoute.getShortcutsForNotes);
}