From 0b528e993705d7a2c444afc80acf2bcd11182b05 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 6 Jan 2026 15:57:36 +0200 Subject: [PATCH] chore(core): integrate handlers --- apps/server/src/services/handlers.ts | 253 +----------------- packages/trilium-core/src/index.ts | 1 + .../trilium-core/src/services/handlers.ts | 252 +++++++++++++++++ .../src/services/one_time_timer.ts | 0 4 files changed, 255 insertions(+), 251 deletions(-) create mode 100644 packages/trilium-core/src/services/handlers.ts rename {apps/server => packages/trilium-core}/src/services/one_time_timer.ts (100%) diff --git a/apps/server/src/services/handlers.ts b/apps/server/src/services/handlers.ts index 8a7d6d12f..0562603fb 100644 --- a/apps/server/src/services/handlers.ts +++ b/apps/server/src/services/handlers.ts @@ -1,251 +1,2 @@ -import { DefinitionObject } from "@triliumnext/commons"; -import { type AbstractBeccaEntity, events as eventService } from "@triliumnext/core"; - -import becca from "../becca/becca.js"; -import BAttribute from "../becca/entities/battribute.js"; -import type BNote from "../becca/entities/bnote.js"; -import hiddenSubtreeService from "./hidden_subtree.js"; -import noteService from "./notes.js"; -import oneTimeTimer from "./one_time_timer.js"; -import scriptService from "./script.js"; -import treeService from "./tree.js"; - -type Handler = (definition: DefinitionObject, note: BNote, targetNote: BNote) => void; - -function runAttachedRelations(note: BNote, relationName: string, originEntity: AbstractBeccaEntity) { - if (!note) { - return; - } - - // the same script note can get here with multiple ways, but execute only once - const notesToRun = new Set( - note - .getRelations(relationName) - .map((relation) => relation.getTargetNote()) - .filter((note) => !!note) as BNote[] - ); - - for (const noteToRun of notesToRun) { - scriptService.executeNoteNoException(noteToRun, { originEntity }); - } -} - -eventService.subscribe(eventService.NOTE_TITLE_CHANGED, (note) => { - runAttachedRelations(note, "runOnNoteTitleChange", note); - - if (!note.isRoot()) { - const noteFromCache = becca.notes[note.noteId]; - - if (!noteFromCache) { - return; - } - - for (const parentNote of noteFromCache.parents) { - if (parentNote.hasLabel("sorted")) { - treeService.sortNotesIfNeeded(parentNote.noteId); - } - } - } -}); - -eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED], ({ entityName, entity }) => { - if (entityName === "attributes") { - runAttachedRelations(entity.getNote(), "runOnAttributeChange", entity); - - if (entity.type === "label" && ["sorted", "sortDirection", "sortFoldersFirst", "sortNatural", "sortLocale"].includes(entity.name)) { - handleSortedAttribute(entity); - } else if (entity.type === "label") { - handleMaybeSortingLabel(entity); - } - } else if (entityName === "notes") { - // ENTITY_DELETED won't trigger anything since all branches/attributes are already deleted at this point - runAttachedRelations(entity, "runOnNoteChange", entity); - } -}); - -eventService.subscribe(eventService.ENTITY_CHANGED, ({ entityName, entity }) => { - if (entityName === "branches") { - const parentNote = becca.getNote(entity.parentNoteId); - - if (parentNote?.hasLabel("sorted")) { - treeService.sortNotesIfNeeded(parentNote.noteId); - } - - const childNote = becca.getNote(entity.noteId); - - if (childNote) { - runAttachedRelations(childNote, "runOnBranchChange", entity); - } - } -}); - -eventService.subscribe(eventService.NOTE_CONTENT_CHANGE, ({ entity }) => { - runAttachedRelations(entity, "runOnNoteContentChange", entity); -}); - -eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) => { - if (entityName === "attributes") { - runAttachedRelations(entity.getNote(), "runOnAttributeCreation", entity); - - if (entity.type === "relation" && entity.name === "template") { - const note = becca.getNote(entity.noteId); - if (!note) { - return; - } - - const templateNote = becca.getNote(entity.value); - - if (!templateNote) { - return; - } - - const content = note.getContent(); - - if ( - ["text", "code", "mermaid", "canvas", "relationMap", "mindMap", "webView"].includes(note.type) && - typeof content === "string" && - // if the note has already content we're not going to overwrite it with template's one - (!content || content.trim().length === 0) && - templateNote.hasStringContent() - ) { - const templateNoteContent = templateNote.getContent(); - - if (templateNoteContent) { - note.setContent(templateNoteContent); - } - - note.type = templateNote.type; - note.mime = templateNote.mime; - note.save(); - } - - // we'll copy the children notes only if there's none so far - // this protects against e.g. multiple assignment of template relation resulting in having multiple copies of the subtree - if (note.getChildNotes().length === 0 && !note.isDescendantOfNote(templateNote.noteId)) { - noteService.duplicateSubtreeWithoutRoot(templateNote.noteId, note.noteId); - } - } else if (entity.type === "label" && ["sorted", "sortDirection", "sortFoldersFirst", "sortNatural", "sortLocale"].includes(entity.name)) { - handleSortedAttribute(entity); - } else if (entity.type === "label") { - handleMaybeSortingLabel(entity); - } - } else if (entityName === "branches") { - runAttachedRelations(entity.getNote(), "runOnBranchCreation", entity); - - if (entity.parentNote?.hasLabel("sorted")) { - treeService.sortNotesIfNeeded(entity.parentNoteId); - } - } else if (entityName === "notes") { - runAttachedRelations(entity, "runOnNoteCreation", entity); - } -}); - -eventService.subscribe(eventService.CHILD_NOTE_CREATED, ({ parentNote, childNote }) => { - runAttachedRelations(parentNote, "runOnChildNoteCreation", childNote); -}); - -function processInverseRelations(entityName: string, entity: BAttribute, handler: Handler) { - if (entityName === "attributes" && entity.type === "relation") { - const note = entity.getNote(); - const relDefinitions = note.getLabels(`relation:${entity.name}`); - - for (const relDefinition of relDefinitions) { - const definition = relDefinition.getDefinition(); - - if (definition.inverseRelation && definition.inverseRelation.trim()) { - const targetNote = entity.getTargetNote(); - - if (targetNote) { - handler(definition, note, targetNote); - } - } - } - } -} - -function handleSortedAttribute(entity: BAttribute) { - treeService.sortNotesIfNeeded(entity.noteId); - - if (entity.isInheritable) { - const note = becca.notes[entity.noteId]; - - if (note) { - for (const noteId of note.getSubtreeNoteIds()) { - treeService.sortNotesIfNeeded(noteId); - } - } - } -} - -function handleMaybeSortingLabel(entity: BAttribute) { - // check if this label is used for sorting, if yes force re-sort - const note = becca.notes[entity.noteId]; - - // this will not work on deleted notes, but in that case we don't really need to re-sort - if (note) { - for (const parentNote of note.getParentNotes()) { - const sorted = parentNote.getLabelValue("sorted"); - if (sorted === null) { - // checking specifically for null since that means the label doesn't exist - // empty valued "sorted" is still valid - continue; - } - - if ( - sorted.includes(entity.name) || // hacky check if this label is used in the sort - entity.name === "top" || - entity.name === "bottom" - ) { - treeService.sortNotesIfNeeded(parentNote.noteId); - } - } - } -} - -eventService.subscribe(eventService.ENTITY_CHANGED, ({ entityName, entity }) => { - processInverseRelations(entityName, entity, (definition, note, targetNote) => { - // we need to make sure that also target's inverse attribute exists and if not, then create it - // inverse attribute has to target our note as well - const hasInverseAttribute = targetNote.getRelations(definition.inverseRelation).some((attr) => attr.value === note.noteId); - - if (!hasInverseAttribute) { - new BAttribute({ - noteId: targetNote.noteId, - type: "relation", - name: definition.inverseRelation || "", - value: note.noteId, - isInheritable: entity.isInheritable - }).save(); - - // becca will not be updated before we'll check from the other side which would create infinite relation creation (#2269) - targetNote.invalidateThisCache(); - } - }); -}); - -eventService.subscribe(eventService.ENTITY_DELETED, ({ entityName, entity }) => { - processInverseRelations(entityName, entity, (definition: DefinitionObject, note: BNote, targetNote: BNote) => { - // if one inverse attribute is deleted, then the other should be deleted as well - const relations = targetNote.getOwnedRelations(definition.inverseRelation); - - for (const relation of relations) { - if (relation.value === note.noteId) { - relation.markAsDeleted(); - } - } - }); - - if (entityName === "branches") { - runAttachedRelations(entity.getNote(), "runOnBranchDeletion", entity); - } - - if (entityName === "notes" && entity.noteId.startsWith("_")) { - // "named" note has been deleted, we will probably need to rebuild the hidden subtree - // scheduling so that bulk deletes won't trigger so many checks - oneTimeTimer.scheduleExecution("hidden-subtree-check", 1000, () => hiddenSubtreeService.checkHiddenSubtree()); - } -}); - -export default { - runAttachedRelations -}; +import { handlers } from "@triliumnext/core"; +export default handlers; diff --git a/packages/trilium-core/src/index.ts b/packages/trilium-core/src/index.ts index 9bc718b96..128574ab3 100644 --- a/packages/trilium-core/src/index.ts +++ b/packages/trilium-core/src/index.ts @@ -27,6 +27,7 @@ export type { CryptoProvider } from "./services/encryption/crypto"; export { default as note_types } from "./services/note_types"; export { default as tree } from "./services/tree"; export { default as cloning } from "./services/cloning"; +export { default as handlers } from "./services/handlers"; export { default as becca } from "./becca/becca"; export { default as becca_loader } from "./becca/becca_loader"; diff --git a/packages/trilium-core/src/services/handlers.ts b/packages/trilium-core/src/services/handlers.ts new file mode 100644 index 000000000..ab2a34524 --- /dev/null +++ b/packages/trilium-core/src/services/handlers.ts @@ -0,0 +1,252 @@ +import { DefinitionObject } from "@triliumnext/commons"; + +import becca from "../becca/becca.js"; +import BAttribute from "../becca/entities/battribute.js"; +import type BNote from "../becca/entities/bnote.js"; +import hiddenSubtreeService from "./hidden_subtree.js"; +import noteService from "./notes.js"; +import oneTimeTimer from "./one_time_timer.js"; +import scriptService from "./script.js"; +import treeService from "./tree.js"; +import eventService from "./events.js"; +import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js"; + +type Handler = (definition: DefinitionObject, note: BNote, targetNote: BNote) => void; + +function runAttachedRelations(note: BNote, relationName: string, originEntity: AbstractBeccaEntity) { + if (!note) { + return; + } + + // the same script note can get here with multiple ways, but execute only once + const notesToRun = new Set( + note + .getRelations(relationName) + .map((relation) => relation.getTargetNote()) + .filter((note) => !!note) as BNote[] + ); + + for (const noteToRun of notesToRun) { + scriptService.executeNoteNoException(noteToRun, { originEntity }); + } +} + +eventService.subscribe(eventService.NOTE_TITLE_CHANGED, (note) => { + runAttachedRelations(note, "runOnNoteTitleChange", note); + + if (!note.isRoot()) { + const noteFromCache = becca.notes[note.noteId]; + + if (!noteFromCache) { + return; + } + + for (const parentNote of noteFromCache.parents) { + if (parentNote.hasLabel("sorted")) { + treeService.sortNotesIfNeeded(parentNote.noteId); + } + } + } +}); + +eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED], ({ entityName, entity }) => { + if (entityName === "attributes") { + runAttachedRelations(entity.getNote(), "runOnAttributeChange", entity); + + if (entity.type === "label" && ["sorted", "sortDirection", "sortFoldersFirst", "sortNatural", "sortLocale"].includes(entity.name)) { + handleSortedAttribute(entity); + } else if (entity.type === "label") { + handleMaybeSortingLabel(entity); + } + } else if (entityName === "notes") { + // ENTITY_DELETED won't trigger anything since all branches/attributes are already deleted at this point + runAttachedRelations(entity, "runOnNoteChange", entity); + } +}); + +eventService.subscribe(eventService.ENTITY_CHANGED, ({ entityName, entity }) => { + if (entityName === "branches") { + const parentNote = becca.getNote(entity.parentNoteId); + + if (parentNote?.hasLabel("sorted")) { + treeService.sortNotesIfNeeded(parentNote.noteId); + } + + const childNote = becca.getNote(entity.noteId); + + if (childNote) { + runAttachedRelations(childNote, "runOnBranchChange", entity); + } + } +}); + +eventService.subscribe(eventService.NOTE_CONTENT_CHANGE, ({ entity }) => { + runAttachedRelations(entity, "runOnNoteContentChange", entity); +}); + +eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) => { + if (entityName === "attributes") { + runAttachedRelations(entity.getNote(), "runOnAttributeCreation", entity); + + if (entity.type === "relation" && entity.name === "template") { + const note = becca.getNote(entity.noteId); + if (!note) { + return; + } + + const templateNote = becca.getNote(entity.value); + + if (!templateNote) { + return; + } + + const content = note.getContent(); + + if ( + ["text", "code", "mermaid", "canvas", "relationMap", "mindMap", "webView"].includes(note.type) && + typeof content === "string" && + // if the note has already content we're not going to overwrite it with template's one + (!content || content.trim().length === 0) && + templateNote.hasStringContent() + ) { + const templateNoteContent = templateNote.getContent(); + + if (templateNoteContent) { + note.setContent(templateNoteContent); + } + + note.type = templateNote.type; + note.mime = templateNote.mime; + note.save(); + } + + // we'll copy the children notes only if there's none so far + // this protects against e.g. multiple assignment of template relation resulting in having multiple copies of the subtree + if (note.getChildNotes().length === 0 && !note.isDescendantOfNote(templateNote.noteId)) { + noteService.duplicateSubtreeWithoutRoot(templateNote.noteId, note.noteId); + } + } else if (entity.type === "label" && ["sorted", "sortDirection", "sortFoldersFirst", "sortNatural", "sortLocale"].includes(entity.name)) { + handleSortedAttribute(entity); + } else if (entity.type === "label") { + handleMaybeSortingLabel(entity); + } + } else if (entityName === "branches") { + runAttachedRelations(entity.getNote(), "runOnBranchCreation", entity); + + if (entity.parentNote?.hasLabel("sorted")) { + treeService.sortNotesIfNeeded(entity.parentNoteId); + } + } else if (entityName === "notes") { + runAttachedRelations(entity, "runOnNoteCreation", entity); + } +}); + +eventService.subscribe(eventService.CHILD_NOTE_CREATED, ({ parentNote, childNote }) => { + runAttachedRelations(parentNote, "runOnChildNoteCreation", childNote); +}); + +function processInverseRelations(entityName: string, entity: BAttribute, handler: Handler) { + if (entityName === "attributes" && entity.type === "relation") { + const note = entity.getNote(); + const relDefinitions = note.getLabels(`relation:${entity.name}`); + + for (const relDefinition of relDefinitions) { + const definition = relDefinition.getDefinition(); + + if (definition.inverseRelation && definition.inverseRelation.trim()) { + const targetNote = entity.getTargetNote(); + + if (targetNote) { + handler(definition, note, targetNote); + } + } + } + } +} + +function handleSortedAttribute(entity: BAttribute) { + treeService.sortNotesIfNeeded(entity.noteId); + + if (entity.isInheritable) { + const note = becca.notes[entity.noteId]; + + if (note) { + for (const noteId of note.getSubtreeNoteIds()) { + treeService.sortNotesIfNeeded(noteId); + } + } + } +} + +function handleMaybeSortingLabel(entity: BAttribute) { + // check if this label is used for sorting, if yes force re-sort + const note = becca.notes[entity.noteId]; + + // this will not work on deleted notes, but in that case we don't really need to re-sort + if (note) { + for (const parentNote of note.getParentNotes()) { + const sorted = parentNote.getLabelValue("sorted"); + if (sorted === null) { + // checking specifically for null since that means the label doesn't exist + // empty valued "sorted" is still valid + continue; + } + + if ( + sorted.includes(entity.name) || // hacky check if this label is used in the sort + entity.name === "top" || + entity.name === "bottom" + ) { + treeService.sortNotesIfNeeded(parentNote.noteId); + } + } + } +} + +eventService.subscribe(eventService.ENTITY_CHANGED, ({ entityName, entity }) => { + processInverseRelations(entityName, entity, (definition, note, targetNote) => { + // we need to make sure that also target's inverse attribute exists and if not, then create it + // inverse attribute has to target our note as well + const hasInverseAttribute = targetNote.getRelations(definition.inverseRelation).some((attr) => attr.value === note.noteId); + + if (!hasInverseAttribute) { + new BAttribute({ + noteId: targetNote.noteId, + type: "relation", + name: definition.inverseRelation || "", + value: note.noteId, + isInheritable: entity.isInheritable + }).save(); + + // becca will not be updated before we'll check from the other side which would create infinite relation creation (#2269) + targetNote.invalidateThisCache(); + } + }); +}); + +eventService.subscribe(eventService.ENTITY_DELETED, ({ entityName, entity }) => { + processInverseRelations(entityName, entity, (definition: DefinitionObject, note: BNote, targetNote: BNote) => { + // if one inverse attribute is deleted, then the other should be deleted as well + const relations = targetNote.getOwnedRelations(definition.inverseRelation); + + for (const relation of relations) { + if (relation.value === note.noteId) { + relation.markAsDeleted(); + } + } + }); + + if (entityName === "branches") { + runAttachedRelations(entity.getNote(), "runOnBranchDeletion", entity); + } + + if (entityName === "notes" && entity.noteId.startsWith("_")) { + // "named" note has been deleted, we will probably need to rebuild the hidden subtree + // scheduling so that bulk deletes won't trigger so many checks + oneTimeTimer.scheduleExecution("hidden-subtree-check", 1000, () => hiddenSubtreeService.checkHiddenSubtree()); + } +}); + +export default { + runAttachedRelations +}; diff --git a/apps/server/src/services/one_time_timer.ts b/packages/trilium-core/src/services/one_time_timer.ts similarity index 100% rename from apps/server/src/services/one_time_timer.ts rename to packages/trilium-core/src/services/one_time_timer.ts