From 22590596da41cce9e39acfbd330f14e92e907526 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 7 Jan 2026 11:37:43 +0200 Subject: [PATCH] feat(core): shared router between lightweight and server --- apps/client/src/lightweight/browser_router.ts | 12 +-- apps/client/src/lightweight/browser_routes.ts | 94 +++++++------------ apps/client/src/local-server-worker.ts | 4 +- apps/server/src/routes/routes.ts | 17 +--- packages/trilium-core/src/routes/index.ts | 28 +++++- 5 files changed, 69 insertions(+), 86 deletions(-) diff --git a/apps/client/src/lightweight/browser_router.ts b/apps/client/src/lightweight/browser_router.ts index 68f3c4860..5bbb95216 100644 --- a/apps/client/src/lightweight/browser_router.ts +++ b/apps/client/src/lightweight/browser_router.ts @@ -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 { const query: Record = {}; 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); } diff --git a/apps/client/src/lightweight/browser_routes.ts b/apps/client/src/lightweight/browser_routes.ts index 29c326c0e..cda9633c5 100644 --- a/apps/client/src/lightweight/browser_routes.ts +++ b/apps/client/src/lightweight/browser_routes.ts @@ -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; } diff --git a/apps/client/src/local-server-worker.ts b/apps/client/src/local-server-worker.ts index f808524e8..fb55ecde2 100644 --- a/apps/client/src/local-server-worker.ts +++ b/apps/client/src/local-server-worker.ts @@ -95,7 +95,7 @@ async function initialize(): Promise { 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(); diff --git a/apps/server/src/routes/routes.ts b/apps/server/src/routes/routes.ts index 25f5733b4..0db1eebe1 100644 --- a/apps/server/src/routes/routes.ts +++ b/apps/server/src/routes/routes.ts @@ -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); diff --git a/packages/trilium-core/src/routes/index.ts b/packages/trilium-core/src/routes/index.ts index fc76c6937..71ebd984b 100644 --- a/packages/trilium-core/src/routes/index.ts +++ b/packages/trilium-core/src/routes/index.ts @@ -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); +}