@ -1,21 +1,13 @@
import { isElectron , safeExtractMessageAndStackFromError } from "../services/utils.js" ;
import multer from "multer" ;
import log from "../services/log.js" ;
import { isElectron } from "../services/utils.js" ;
import express from "express" ;
const router = express . Router ( ) ;
import auth from "../services/auth.js" ;
import openID from '../services/open_id.js' ;
import totp from './api/totp.js' ;
import recoveryCodes from './api/recovery_codes.js' ;
import cls from "../services/cls.js" ;
import sql from "../services/sql.js" ;
import entityChangesService from "../services/entity_changes.js" ;
import { doubleCsrfProtection as csrfMiddleware } from "./csrf_protection.js" ;
import { createPartialContentHandler } from "@triliumnext/express-partial-content" ;
import rateLimit from "express-rate-limit" ;
import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js" ;
import NotFoundError from "../errors/not_found_error.js" ;
import ValidationError from "../errors/validation_error.js" ;
// page routes
import setupRoute from "./setup.js" ;
@ -77,33 +69,14 @@ import etapiSpecialNoteRoutes from "../etapi/special_notes.js";
import etapiSpecRoute from "../etapi/spec.js" ;
import etapiBackupRoute from "../etapi/backup.js" ;
import apiDocsRoute from "./api_docs.js" ;
import { apiResultHandler , apiRoute , asyncApiRoute , asyncRoute , route , router , uploadMiddlewareWithErrorHandling } from "./route_api.js" ;
const MAX_ALLOWED_FILE_SIZE_MB = 250 ;
const GET = "get" ,
PST = "post" ,
PUT = "put" ,
PATCH = "patch" ,
DEL = "delete" ;
export type ApiResultHandler = ( req : express.Request , res : express.Response , result : unknown ) = > number ;
export type ApiRequestHandler = ( req : express.Request , res : express.Response , next : express.NextFunction ) = > unknown ;
// TODO: Deduplicate with etapi_utils.ts afterwards.
type HttpMethod = "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head" ;
const uploadMiddleware = createUploadMiddleware ( ) ;
const uploadMiddlewareWithErrorHandling = function ( req : express.Request , res : express.Response , next : express.NextFunction ) {
uploadMiddleware ( req , res , function ( err ) {
if ( err ? . code === "LIMIT_FILE_SIZE" ) {
res . setHeader ( "Content-Type" , "text/plain" ) . status ( 400 ) . send ( ` Cannot upload file because it excceeded max allowed file size of ${ MAX_ALLOWED_FILE_SIZE_MB } MiB ` ) ;
} else {
next ( ) ;
}
} ) ;
} ;
function register ( app : express.Application ) {
route ( GET , "/" , [ auth . checkAuth , csrfMiddleware ] , indexRoute . index ) ;
route ( GET , "/login" , [ auth . checkAppInitialized , auth . checkPasswordSet ] , loginRoute . loginPage ) ;
@ -126,7 +99,7 @@ function register(app: express.Application) {
apiRoute ( GET , '/api/totp/get' , totp . getSecret ) ;
apiRoute ( GET , '/api/oauth/status' , openID . getOAuthStatus ) ;
a piRoute( GET , '/api/oauth/validate' , openID . isTokenValid ) ;
a syncA piRoute( GET , '/api/oauth/validate' , openID . isTokenValid ) ;
apiRoute ( PST , '/api/totp_recovery/set' , recoveryCodes . setRecoveryCodes ) ;
apiRoute ( PST , '/api/totp_recovery/verify' , recoveryCodes . verifyRecoveryCode ) ;
@ -156,7 +129,7 @@ function register(app: express.Application) {
apiRoute ( PUT , "/api/notes/:noteId/clone-after/:afterBranchId" , cloningApiRoute . cloneNoteAfter ) ;
route ( PUT , "/api/notes/:noteId/file" , [ auth . checkApiAuthOrElectron , uploadMiddlewareWithErrorHandling , csrfMiddleware ] , filesRoute . updateFile , apiResultHandler ) ;
route ( GET , "/api/notes/:noteId/open" , [ auth . checkApiAuthOrElectron ] , filesRoute . openFile ) ;
r oute(
asyncR oute(
GET ,
"/api/notes/:noteId/open-partial" ,
[ auth . checkApiAuthOrElectron ] ,
@ -192,7 +165,7 @@ function register(app: express.Application) {
apiRoute ( GET , "/api/attachments/:attachmentId/blob" , attachmentsApiRoute . getAttachmentBlob ) ;
route ( GET , "/api/attachments/:attachmentId/image/:filename" , [ auth . checkApiAuthOrElectron ] , imageRoute . returnAttachedImage ) ;
route ( GET , "/api/attachments/:attachmentId/open" , [ auth . checkApiAuthOrElectron ] , filesRoute . openAttachment ) ;
r oute(
asyncR oute(
GET ,
"/api/attachments/:attachmentId/open-partial" ,
[ auth . checkApiAuthOrElectron ] ,
@ -221,7 +194,7 @@ function register(app: express.Application) {
route ( GET , "/api/revisions/:revisionId/download" , [ auth . checkApiAuthOrElectron ] , revisionsApiRoute . downloadRevision ) ;
route ( GET , "/api/branches/:branchId/export/:type/:format/:version/:taskId" , [ auth . checkApiAuthOrElectron ] , exportRoute . exportBranch ) ;
r oute( PST , "/api/notes/:parentNoteId/notes-import" , [ auth . checkApiAuthOrElectron , uploadMiddlewareWithErrorHandling , csrfMiddleware ] , importRoute . importNotesToBranch , apiResultHandler ) ;
asyncR oute( PST , "/api/notes/:parentNoteId/notes-import" , [ auth . checkApiAuthOrElectron , uploadMiddlewareWithErrorHandling , csrfMiddleware ] , importRoute . importNotesToBranch , apiResultHandler ) ;
route ( PST , "/api/notes/:parentNoteId/attachments-import" , [ auth . checkApiAuthOrElectron , uploadMiddlewareWithErrorHandling , csrfMiddleware ] , importRoute . importAttachmentsToNote , apiResultHandler ) ;
apiRoute ( GET , "/api/notes/:noteId/attributes" , attributesRoute . getEffectiveNoteAttributes ) ;
@ -250,8 +223,8 @@ function register(app: express.Application) {
apiRoute ( PST , "/api/password/change" , passwordApiRoute . changePassword ) ;
apiRoute ( PST , "/api/password/reset" , passwordApiRoute . resetPassword ) ;
a piRoute( PST , "/api/sync/test" , syncApiRoute . testSync ) ;
a piRoute( PST , "/api/sync/now" , syncApiRoute . syncNow ) ;
a syncA piRoute( PST , "/api/sync/test" , syncApiRoute . testSync ) ;
a syncA piRoute( PST , "/api/sync/now" , syncApiRoute . syncNow ) ;
apiRoute ( PST , "/api/sync/fill-entity-changes" , syncApiRoute . fillEntityChanges ) ;
apiRoute ( PST , "/api/sync/force-full-sync" , syncApiRoute . forceFullSync ) ;
route ( GET , "/api/sync/check" , [ auth . checkApiAuth ] , syncApiRoute . checkSync , apiResultHandler ) ;
@ -270,10 +243,10 @@ function register(app: express.Application) {
// group of the services below are meant to be executed from the outside
route ( GET , "/api/setup/status" , [ ] , setupApiRoute . getStatus , apiResultHandler ) ;
r oute( PST , "/api/setup/new-document" , [ auth . checkAppNotInitialized ] , setupApiRoute . setupNewDocument , apiResultHandler , false ) ;
r oute( PST , "/api/setup/sync-from-server" , [ auth . checkAppNotInitialized ] , setupApiRoute . setupSyncFromServer , apiResultHandler , false ) ;
asyncR oute( PST , "/api/setup/new-document" , [ auth . checkAppNotInitialized ] , setupApiRoute . setupNewDocument , apiResultHandler ) ;
asyncR oute( PST , "/api/setup/sync-from-server" , [ auth . checkAppNotInitialized ] , setupApiRoute . setupSyncFromServer , apiResultHandler ) ;
route ( GET , "/api/setup/sync-seed" , [ auth . checkCredentials ] , setupApiRoute . getSyncSeed , apiResultHandler ) ;
r oute( PST , "/api/setup/sync-seed" , [ auth . checkAppNotInitialized ] , setupApiRoute . saveSyncSeed , apiResultHandler , false ) ;
asyncR oute( PST , "/api/setup/sync-seed" , [ auth . checkAppNotInitialized ] , setupApiRoute . saveSyncSeed , apiResultHandler ) ;
apiRoute ( GET , "/api/autocomplete" , autocompleteApiRoute . getAutocomplete ) ;
apiRoute ( GET , "/api/autocomplete/notesCount" , autocompleteApiRoute . getNotesCount ) ;
@ -304,21 +277,21 @@ function register(app: express.Application) {
const clipperMiddleware = isElectron ? [ ] : [ auth . checkEtapiToken ] ;
route ( GET , "/api/clipper/handshake" , clipperMiddleware , clipperRoute . handshake , apiResultHandler ) ;
r oute( PST , "/api/clipper/clippings" , clipperMiddleware , clipperRoute . addClipping , apiResultHandler ) ;
r oute( PST , "/api/clipper/notes" , clipperMiddleware , clipperRoute . createNote , apiResultHandler ) ;
asyncR oute( PST , "/api/clipper/clippings" , clipperMiddleware , clipperRoute . addClipping , apiResultHandler ) ;
asyncR oute( PST , "/api/clipper/notes" , clipperMiddleware , clipperRoute . createNote , apiResultHandler ) ;
route ( PST , "/api/clipper/open/:noteId" , clipperMiddleware , clipperRoute . openNote , apiResultHandler ) ;
r oute( GET , "/api/clipper/notes-by-url/:noteUrl" , clipperMiddleware , clipperRoute . findNotesByUrl , apiResultHandler ) ;
a piRoute( GET , "/api/special-notes/inbox/:date" , specialNotesRoute . getInboxNote ) ;
a piRoute( GET , "/api/special-notes/days/:date" , specialNotesRoute . getDayNote ) ;
a piRoute( GET , "/api/special-notes/week-first-day/:date" , specialNotesRoute . getWeekFirstDayNote ) ;
a piRoute( GET , "/api/special-notes/weeks/:week" , specialNotesRoute . getWeekNote ) ;
a piRoute( GET , "/api/special-notes/months/:month" , specialNotesRoute . getMonthNote ) ;
a piRoute( GET , "/api/special-notes/quarters/:quarter" , specialNotesRoute . getQuarterNote ) ;
asyncR oute( GET , "/api/clipper/notes-by-url/:noteUrl" , clipperMiddleware , clipperRoute . findNotesByUrl , apiResultHandler ) ;
a syncA piRoute( GET , "/api/special-notes/inbox/:date" , specialNotesRoute . getInboxNote ) ;
a syncA piRoute( GET , "/api/special-notes/days/:date" , specialNotesRoute . getDayNote ) ;
a syncA piRoute( GET , "/api/special-notes/week-first-day/:date" , specialNotesRoute . getWeekFirstDayNote ) ;
a syncA piRoute( GET , "/api/special-notes/weeks/:week" , specialNotesRoute . getWeekNote ) ;
a syncA piRoute( GET , "/api/special-notes/months/:month" , specialNotesRoute . getMonthNote ) ;
a syncA piRoute( GET , "/api/special-notes/quarters/:quarter" , specialNotesRoute . getQuarterNote ) ;
apiRoute ( GET , "/api/special-notes/years/:year" , specialNotesRoute . getYearNote ) ;
apiRoute ( GET , "/api/special-notes/notes-for-month/:month" , specialNotesRoute . getDayNotesForMonth ) ;
apiRoute ( PST , "/api/special-notes/sql-console" , specialNotesRoute . createSqlConsole ) ;
a piRoute( PST , "/api/special-notes/save-sql-console" , specialNotesRoute . saveSqlConsole ) ;
a syncA piRoute( PST , "/api/special-notes/save-sql-console" , specialNotesRoute . saveSqlConsole ) ;
apiRoute ( PST , "/api/special-notes/search-note" , specialNotesRoute . createSearchNote ) ;
apiRoute ( PST , "/api/special-notes/save-search-note" , specialNotesRoute . saveSearchNote ) ;
apiRoute ( PST , "/api/special-notes/launchers/:noteId/reset" , specialNotesRoute . resetLauncher ) ;
@ -327,25 +300,25 @@ function register(app: express.Application) {
apiRoute ( GET , "/api/sql/schema" , sqlRoute . getSchema ) ;
apiRoute ( PST , "/api/sql/execute/:noteId" , sqlRoute . execute ) ;
r oute( PST , "/api/database/anonymize/:type" , [ auth . checkApiAuthOrElectron , csrfMiddleware ] , databaseRoute . anonymize , apiResultHandler , false ) ;
asyncR oute( PST , "/api/database/anonymize/:type" , [ auth . checkApiAuthOrElectron , csrfMiddleware ] , databaseRoute . anonymize , apiResultHandler ) ;
apiRoute ( GET , "/api/database/anonymized-databases" , databaseRoute . getExistingAnonymizedDatabases ) ;
if ( process . env . TRILIUM_INTEGRATION_TEST === "memory" ) {
r oute( PST , "/api/database/rebuild/" , [ auth . checkApiAuthOrElectron ] , databaseRoute . rebuildIntegrationTestDatabase , apiResultHandler , false ) ;
asyncR oute( PST , "/api/database/rebuild/" , [ auth . checkApiAuthOrElectron ] , databaseRoute . rebuildIntegrationTestDatabase , apiResultHandler ) ;
}
// backup requires execution outside of transaction
r oute( PST , "/api/database/backup-database" , [ auth . checkApiAuthOrElectron , csrfMiddleware ] , databaseRoute . backupDatabase , apiResultHandler , false ) ;
asyncR oute( PST , "/api/database/backup-database" , [ auth . checkApiAuthOrElectron , csrfMiddleware ] , databaseRoute . backupDatabase , apiResultHandler ) ;
apiRoute ( GET , "/api/database/backups" , databaseRoute . getExistingBackups ) ;
// VACUUM requires execution outside of transaction
r oute( PST , "/api/database/vacuum-database" , [ auth . checkApiAuthOrElectron , csrfMiddleware ] , databaseRoute . vacuumDatabase , apiResultHandler , false ) ;
asyncR oute( PST , "/api/database/vacuum-database" , [ auth . checkApiAuthOrElectron , csrfMiddleware ] , databaseRoute . vacuumDatabase , apiResultHandler ) ;
r oute( PST , "/api/database/find-and-fix-consistency-issues" , [ auth . checkApiAuthOrElectron , csrfMiddleware ] , databaseRoute . findAndFixConsistencyIssues , apiResultHandler , false ) ;
asyncR oute( PST , "/api/database/find-and-fix-consistency-issues" , [ auth . checkApiAuthOrElectron , csrfMiddleware ] , databaseRoute . findAndFixConsistencyIssues , apiResultHandler ) ;
apiRoute ( GET , "/api/database/check-integrity" , databaseRoute . checkIntegrity ) ;
r oute( PST , "/api/script/exec" , [ auth . checkApiAuth , csrfMiddleware ] , scriptRoute . exec , apiResultHandler , false ) ;
asyncR oute( PST , "/api/script/exec" , [ auth . checkApiAuth , csrfMiddleware ] , scriptRoute . exec , apiResultHandler ) ;
apiRoute ( PST , "/api/script/run/:noteId" , scriptRoute . run ) ;
apiRoute ( GET , "/api/script/startup" , scriptRoute . getStartupBundles ) ;
@ -355,8 +328,8 @@ function register(app: express.Application) {
// no CSRF since this is called from android app
route ( PST , "/api/sender/login" , [ loginRateLimiter ] , loginApiRoute . token , apiResultHandler ) ;
r oute( PST , "/api/sender/image" , [ auth . checkEtapiToken , uploadMiddlewareWithErrorHandling ] , senderRoute . uploadImage , apiResultHandler ) ;
r oute( PST , "/api/sender/note" , [ auth . checkEtapiToken ] , senderRoute . saveNote , apiResultHandler ) ;
asyncR oute( PST , "/api/sender/image" , [ auth . checkEtapiToken , uploadMiddlewareWithErrorHandling ] , senderRoute . uploadImage , apiResultHandler ) ;
asyncR oute( PST , "/api/sender/note" , [ auth . checkEtapiToken ] , senderRoute . saveNote , apiResultHandler ) ;
apiRoute ( GET , "/api/keyboard-actions" , keysRoute . getKeyboardActions ) ;
apiRoute ( GET , "/api/keyboard-shortcuts-for-notes" , keysRoute . getShortcutsForNotes ) ;
@ -364,8 +337,8 @@ function register(app: express.Application) {
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 ) ;
a piRoute( GET , "/api/similar-notes/:noteId" , similarNotesRoute . getSimilarNotes ) ;
a piRoute( GET , "/api/backend-log" , backendLogRoute . getBackendLog ) ;
a syncA piRoute( GET , "/api/similar-notes/:noteId" , similarNotesRoute . getSimilarNotes ) ;
a syncA piRoute( GET , "/api/backend-log" , backendLogRoute . getBackendLog ) ;
apiRoute ( GET , "/api/stats/note-size/:noteId" , statsRoute . getNoteSize ) ;
apiRoute ( GET , "/api/stats/subtree-size/:noteId" , statsRoute . getSubtreeSize ) ;
apiRoute ( PST , "/api/delete-notes-preview" , notesApiRoute . getDeleteNotesPreview ) ;
@ -393,42 +366,42 @@ function register(app: express.Application) {
etapiBackupRoute . register ( router ) ;
// LLM Chat API
a piRoute( PST , "/api/llm/chat" , llmRoute . createSession ) ;
a piRoute( GET , "/api/llm/chat" , llmRoute . listSessions ) ;
a piRoute( GET , "/api/llm/chat/:sessionId" , llmRoute . getSession ) ;
a piRoute( PATCH , "/api/llm/chat/:sessionId" , llmRoute . updateSession ) ;
a piRoute( DEL , "/api/llm/chat/:chatNoteId" , llmRoute . deleteSession ) ;
a piRoute( PST , "/api/llm/chat/:chatNoteId/messages" , llmRoute . sendMessage ) ;
a piRoute( PST , "/api/llm/chat/:chatNoteId/messages/stream" , llmRoute . streamMessage ) ;
a syncA piRoute( PST , "/api/llm/chat" , llmRoute . createSession ) ;
a syncA piRoute( GET , "/api/llm/chat" , llmRoute . listSessions ) ;
a syncA piRoute( GET , "/api/llm/chat/:sessionId" , llmRoute . getSession ) ;
a syncA piRoute( PATCH , "/api/llm/chat/:sessionId" , llmRoute . updateSession ) ;
a syncA piRoute( DEL , "/api/llm/chat/:chatNoteId" , llmRoute . deleteSession ) ;
a syncA piRoute( PST , "/api/llm/chat/:chatNoteId/messages" , llmRoute . sendMessage ) ;
a syncA piRoute( PST , "/api/llm/chat/:chatNoteId/messages/stream" , llmRoute . streamMessage ) ;
// LLM index management endpoints - reorganized for REST principles
a piRoute( GET , "/api/llm/indexes/stats" , llmRoute . getIndexStats ) ;
a piRoute( PST , "/api/llm/indexes" , llmRoute . startIndexing ) ; // Create index process
a piRoute( GET , "/api/llm/indexes/failed" , llmRoute . getFailedIndexes ) ;
a piRoute( PUT , "/api/llm/indexes/notes/:noteId" , llmRoute . retryFailedIndex ) ; // Update index for note
a piRoute( PUT , "/api/llm/indexes/failed" , llmRoute . retryAllFailedIndexes ) ; // Update all failed indexes
a piRoute( GET , "/api/llm/indexes/notes/similar" , llmRoute . findSimilarNotes ) ; // Get similar notes
a piRoute( GET , "/api/llm/indexes/context" , llmRoute . generateQueryContext ) ; // Get context
a piRoute( PST , "/api/llm/indexes/notes/:noteId" , llmRoute . indexNote ) ; // Create index for specific note
a syncA piRoute( GET , "/api/llm/indexes/stats" , llmRoute . getIndexStats ) ;
a syncA piRoute( PST , "/api/llm/indexes" , llmRoute . startIndexing ) ; // Create index process
a syncA piRoute( GET , "/api/llm/indexes/failed" , llmRoute . getFailedIndexes ) ;
a syncA piRoute( PUT , "/api/llm/indexes/notes/:noteId" , llmRoute . retryFailedIndex ) ; // Update index for note
a syncA piRoute( PUT , "/api/llm/indexes/failed" , llmRoute . retryAllFailedIndexes ) ; // Update all failed indexes
a syncA piRoute( GET , "/api/llm/indexes/notes/similar" , llmRoute . findSimilarNotes ) ; // Get similar notes
a syncA piRoute( GET , "/api/llm/indexes/context" , llmRoute . generateQueryContext ) ; // Get context
a syncA piRoute( PST , "/api/llm/indexes/notes/:noteId" , llmRoute . indexNote ) ; // Create index for specific note
// LLM embeddings endpoints
a piRoute( GET , "/api/llm/embeddings/similar/:noteId" , embeddingsRoute . findSimilarNotes ) ;
a piRoute( PST , "/api/llm/embeddings/search" , embeddingsRoute . searchByText ) ;
a piRoute( GET , "/api/llm/embeddings/providers" , embeddingsRoute . getProviders ) ;
a piRoute( PATCH , "/api/llm/embeddings/providers/:providerId" , embeddingsRoute . updateProvider ) ;
a piRoute( PST , "/api/llm/embeddings/reprocess" , embeddingsRoute . reprocessAllNotes ) ;
a piRoute( GET , "/api/llm/embeddings/queue-status" , embeddingsRoute . getQueueStatus ) ;
a piRoute( GET , "/api/llm/embeddings/stats" , embeddingsRoute . getEmbeddingStats ) ;
a piRoute( GET , "/api/llm/embeddings/failed" , embeddingsRoute . getFailedNotes ) ;
a piRoute( PST , "/api/llm/embeddings/retry/:noteId" , embeddingsRoute . retryFailedNote ) ;
a piRoute( PST , "/api/llm/embeddings/retry-all-failed" , embeddingsRoute . retryAllFailedNotes ) ;
a piRoute( PST , "/api/llm/embeddings/rebuild-index" , embeddingsRoute . rebuildIndex ) ;
a piRoute( GET , "/api/llm/embeddings/index-rebuild-status" , embeddingsRoute . getIndexRebuildStatus ) ;
a syncA piRoute( GET , "/api/llm/embeddings/similar/:noteId" , embeddingsRoute . findSimilarNotes ) ;
a syncA piRoute( PST , "/api/llm/embeddings/search" , embeddingsRoute . searchByText ) ;
a syncA piRoute( GET , "/api/llm/embeddings/providers" , embeddingsRoute . getProviders ) ;
a syncA piRoute( PATCH , "/api/llm/embeddings/providers/:providerId" , embeddingsRoute . updateProvider ) ;
a syncA piRoute( PST , "/api/llm/embeddings/reprocess" , embeddingsRoute . reprocessAllNotes ) ;
a syncA piRoute( GET , "/api/llm/embeddings/queue-status" , embeddingsRoute . getQueueStatus ) ;
a syncA piRoute( GET , "/api/llm/embeddings/stats" , embeddingsRoute . getEmbeddingStats ) ;
a syncA piRoute( GET , "/api/llm/embeddings/failed" , embeddingsRoute . getFailedNotes ) ;
a syncA piRoute( PST , "/api/llm/embeddings/retry/:noteId" , embeddingsRoute . retryFailedNote ) ;
a syncA piRoute( PST , "/api/llm/embeddings/retry-all-failed" , embeddingsRoute . retryAllFailedNotes ) ;
a syncA piRoute( PST , "/api/llm/embeddings/rebuild-index" , embeddingsRoute . rebuildIndex ) ;
a syncA piRoute( GET , "/api/llm/embeddings/index-rebuild-status" , embeddingsRoute . getIndexRebuildStatus ) ;
// LLM provider endpoints - moved under /api/llm/providers hierarchy
a piRoute( GET , "/api/llm/providers/ollama/models" , ollamaRoute . listModels ) ;
a piRoute( GET , "/api/llm/providers/openai/models" , openaiRoute . listModels ) ;
a piRoute( GET , "/api/llm/providers/anthropic/models" , anthropicRoute . listModels ) ;
a syncA piRoute( GET , "/api/llm/providers/ollama/models" , ollamaRoute . listModels ) ;
a syncA piRoute( GET , "/api/llm/providers/openai/models" , openaiRoute . listModels ) ;
a syncA piRoute( GET , "/api/llm/providers/anthropic/models" , anthropicRoute . listModels ) ;
// API Documentation
apiDocsRoute ( app ) ;
@ -436,156 +409,6 @@ function register(app: express.Application) {
app . use ( "" , router ) ;
}
/** Handling common patterns. If entity is not caught, serialization to JSON will fail */
function convertEntitiesToPojo ( result : unknown ) {
if ( result instanceof AbstractBeccaEntity ) {
result = result . getPojo ( ) ;
} else if ( Array . isArray ( result ) ) {
for ( const idx in result ) {
if ( result [ idx ] instanceof AbstractBeccaEntity ) {
result [ idx ] = result [ idx ] . getPojo ( ) ;
}
}
} else if ( result && typeof result === "object" ) {
if ( "note" in result && result . note instanceof AbstractBeccaEntity ) {
result . note = result . note . getPojo ( ) ;
}
if ( "branch" in result && result . branch instanceof AbstractBeccaEntity ) {
result . branch = result . branch . getPojo ( ) ;
}
}
if ( result && typeof result === "object" && "executionResult" in result ) {
// from runOnBackend()
result . executionResult = convertEntitiesToPojo ( result . executionResult ) ;
}
return result ;
}
function apiResultHandler ( req : express.Request , res : express.Response , result : unknown ) {
res . setHeader ( "trilium-max-entity-change-id" , entityChangesService . getMaxEntityChangeId ( ) ) ;
result = convertEntitiesToPojo ( result ) ;
// if it's an array and the first element is integer, then we consider this to be [statusCode, response] format
if ( Array . isArray ( result ) && result . length > 0 && Number . isInteger ( result [ 0 ] ) ) {
const [ statusCode , response ] = result ;
if ( statusCode !== 200 && statusCode !== 201 && statusCode !== 204 ) {
log . info ( ` ${ req . method } ${ req . originalUrl } returned ${ statusCode } with response ${ JSON . stringify ( response ) } ` ) ;
}
return send ( res , statusCode , response ) ;
} else if ( result === undefined ) {
return send ( res , 204 , "" ) ;
} else {
return send ( res , 200 , result ) ;
}
}
function send ( res : express.Response , statusCode : number , response : unknown ) {
if ( typeof response === "string" ) {
if ( statusCode >= 400 ) {
res . setHeader ( "Content-Type" , "text/plain" ) ;
}
res . status ( statusCode ) . send ( response ) ;
return response . length ;
} else {
const json = JSON . stringify ( response ) ;
res . setHeader ( "Content-Type" , "application/json" ) ;
res . status ( statusCode ) . send ( json ) ;
return json . length ;
}
}
function apiRoute ( method : HttpMethod , path : string , routeHandler : ApiRequestHandler ) {
route ( method , path , [ auth . checkApiAuth , csrfMiddleware ] , routeHandler , apiResultHandler ) ;
}
function route ( method : HttpMethod , path : string , middleware : express.Handler [ ] , routeHandler : ApiRequestHandler , resultHandler : ApiResultHandler | null = null , transactional = true ) {
router [ method ] ( path , . . . ( middleware as express . Handler [ ] ) , ( req : express.Request , res : express.Response , next : express.NextFunction ) = > {
const start = Date . now ( ) ;
try {
cls . namespace . bindEmitter ( req ) ;
cls . namespace . bindEmitter ( res ) ;
const result = cls . init ( ( ) = > {
cls . set ( "componentId" , req . headers [ "trilium-component-id" ] ) ;
cls . set ( "localNowDateTime" , req . headers [ "trilium-local-now-datetime" ] ) ;
cls . set ( "hoistedNoteId" , req . headers [ "trilium-hoisted-note-id" ] || "root" ) ;
const cb = ( ) = > routeHandler ( req , res , next ) ;
return transactional ? sql . transactional ( cb ) : cb ( ) ;
} ) ;
if ( ! resultHandler ) {
return ;
}
if ( result ? . then ) {
// promise
result . then ( ( promiseResult : unknown ) = > handleResponse ( resultHandler , req , res , promiseResult , start ) ) . catch ( ( e : unknown ) = > handleException ( e , method , path , res ) ) ;
} else {
handleResponse ( resultHandler , req , res , result , start ) ;
}
} catch ( e ) {
handleException ( e , method , path , res ) ;
}
} ) ;
}
function handleResponse ( resultHandler : ApiResultHandler , req : express.Request , res : express.Response , result : unknown , start : number ) {
// Skip result handling if the response has already been handled
if ( ( res as any ) . triliumResponseHandled ) {
// Just log the request without additional processing
log . request ( req , res , Date . now ( ) - start , 0 ) ;
return ;
}
const responseLength = resultHandler ( req , res , result ) ;
log . request ( req , res , Date . now ( ) - start , responseLength ) ;
}
function handleException ( e : unknown | Error , method : HttpMethod , path : string , res : express.Response ) {
const [ errMessage , errStack ] = safeExtractMessageAndStackFromError ( e ) ;
log . error ( ` ${ method } ${ path } threw exception: ' ${ errMessage } ', stack: ${ errStack } ` ) ;
const resStatusCode = ( e instanceof ValidationError || e instanceof NotFoundError ) ? e.statusCode : 500 ;
res . status ( resStatusCode ) . json ( {
message : errMessage
} ) ;
}
function createUploadMiddleware() {
const multerOptions : multer.Options = {
fileFilter : ( req : express.Request , file , cb ) = > {
// UTF-8 file names are not well decoded by multer/busboy, so we handle the conversion on our side.
// See https://github.com/expressjs/multer/pull/1102.
file . originalname = Buffer . from ( file . originalname , "latin1" ) . toString ( "utf-8" ) ;
cb ( null , true ) ;
}
} ;
if ( ! process . env . TRILIUM_NO_UPLOAD_LIMIT ) {
multerOptions . limits = {
fileSize : MAX_ALLOWED_FILE_SIZE_MB * 1024 * 1024
} ;
}
return multer ( multerOptions ) . single ( "upload" ) ;
}
export default {
register
} ;