diff --git a/apps/server/src/services/bulk_actions.ts b/apps/server/src/services/bulk_actions.ts index 7387dc64a4..f624961e82 100644 --- a/apps/server/src/services/bulk_actions.ts +++ b/apps/server/src/services/bulk_actions.ts @@ -1,177 +1,2 @@ -import { ActionHandlers, BulkAction, BulkActionData } from "@triliumnext/commons"; -import { branches as branchService, erase as eraseService } from "@triliumnext/core"; - -import becca from "../becca/becca.js"; -import type BNote from "../becca/entities/bnote.js"; -import cloningService from "./cloning.js"; -import log from "./log.js"; -import { randomString } from "./utils.js"; - -type ActionHandler = (action: T, note: BNote) => void; - -type ActionHandlerMap = { - [K in keyof ActionHandlers]: ActionHandler>; -}; - -const ACTION_HANDLERS: ActionHandlerMap = { - addLabel: (action, note) => { - note.addLabel(action.labelName, action.labelValue); - }, - addRelation: (action, note) => { - note.addRelation(action.relationName, action.targetNoteId); - }, - deleteNote: (action, note) => { - const deleteId = `searchbulkaction-${randomString(10)}`; - - note.deleteNote(deleteId); - }, - deleteRevisions: (action, note) => { - const revisionIds = note - .getRevisions() - .map((rev) => rev.revisionId) - .filter((rev) => !!rev) as string[]; - eraseService.eraseRevisions(revisionIds); - }, - deleteLabel: (action, note) => { - for (const label of note.getOwnedLabels(action.labelName)) { - label.markAsDeleted(); - } - }, - deleteRelation: (action, note) => { - for (const relation of note.getOwnedRelations(action.relationName)) { - relation.markAsDeleted(); - } - }, - renameNote: (action, note) => { - // "officially" injected value: - // - note - - const newTitle = eval(`\`${action.newTitle}\``); - - if (note.title !== newTitle) { - note.title = newTitle; - note.save(); - } - }, - renameLabel: (action, note) => { - for (const label of note.getOwnedLabels(action.oldLabelName)) { - // attribute name is immutable, renaming means delete old + create new - const newLabel = label.createClone("label", action.newLabelName, label.value); - - newLabel.save(); - label.markAsDeleted(); - } - }, - renameRelation: (action, note) => { - for (const relation of note.getOwnedRelations(action.oldRelationName)) { - // attribute name is immutable, renaming means delete old + create new - const newRelation = relation.createClone("relation", action.newRelationName, relation.value); - - newRelation.save(); - relation.markAsDeleted(); - } - }, - updateLabelValue: (action, note) => { - for (const label of note.getOwnedLabels(action.labelName)) { - label.value = action.labelValue; - label.save(); - } - }, - updateRelationTarget: (action, note) => { - for (const relation of note.getOwnedRelations(action.relationName)) { - relation.value = action.targetNoteId; - relation.save(); - } - }, - moveNote: (action, note) => { - const targetParentNote = becca.getNote(action.targetParentNoteId); - - if (!targetParentNote) { - log.info(`Cannot execute moveNote because note ${action.targetParentNoteId} doesn't exist.`); - - return; - } - - let res; - - if (note.getParentBranches().length > 1) { - res = cloningService.cloneNoteToParentNote(note.noteId, action.targetParentNoteId); - } else { - res = branchService.moveBranchToNote(note.getParentBranches()[0], action.targetParentNoteId); - } - - if ("success" in res && !res.success) { - log.info(`Moving/cloning note ${note.noteId} to ${action.targetParentNoteId} failed with error ${JSON.stringify(res)}`); - } - }, - executeScript: (action, note) => { - if (!action.script || !action.script.trim()) { - log.info("Ignoring executeScript since the script is empty."); - return; - } - - const scriptFunc = new Function("note", action.script); - scriptFunc(note); - - note.save(); - } -} as const; - -function getActions(note: BNote) { - return note - .getLabels("action") - .map((actionLabel) => { - let action; - - try { - action = JSON.parse(actionLabel.value); - } catch (e) { - log.error(`Cannot parse '${actionLabel.value}' into search action, skipping.`); - return null; - } - - if (!(action.name in ACTION_HANDLERS)) { - log.error(`Cannot find '${action.name}' search action handler, skipping.`); - return null; - } - - return action as BulkAction; - }) - .filter((a) => !!a); -} - -/** - * Executes the bulk actions defined in the note against the provided search result note IDs. - * @param note the note containing the bulk actions, read from the `action` label. - * @param noteIds the IDs of the notes to apply the actions to. - */ -function executeActionsFromNote(note: BNote, noteIds: string[] | Set) { - const actions = getActions(note); - return executeActions(actions, noteIds); -} - -function executeActions(actions: BulkAction[], noteIds: string[] | Set) { - for (const resultNoteId of noteIds) { - const resultNote = becca.getNote(resultNoteId); - - if (!resultNote) { - continue; - } - - for (const action of actions) { - try { - log.info(`Applying action handler to note ${resultNote.noteId}: ${JSON.stringify(action)}`); - - const handler = ACTION_HANDLERS[action.name] as (a: typeof action, n: BNote) => void; - handler(action, resultNote); - } catch (e: any) { - log.error(`ExecuteScript search action failed with ${e.message}`); - } - } - } -} - -export default { - executeActions, - executeActionsFromNote -}; +import { bulk_actions } from "@triliumnext/core"; +export default bulk_actions; diff --git a/packages/trilium-core/src/index.ts b/packages/trilium-core/src/index.ts index d3648f0f24..5f4c3e82f0 100644 --- a/packages/trilium-core/src/index.ts +++ b/packages/trilium-core/src/index.ts @@ -39,6 +39,7 @@ export { default as revisions } from "./services/revisions"; export { default as erase } from "./services/erase"; export { default as getSharedBootstrapItems } from "./services/bootstrap_utils"; export { default as branches } from "./services/branches"; +export { default as bulk_actions } from "./services/bulk_actions"; export { default as attribute_formatter} from "./services/attribute_formatter"; export { default as BUILTIN_ATTRIBUTES } from "./services/builtin_attributes"; diff --git a/packages/trilium-core/src/services/bulk_actions.ts b/packages/trilium-core/src/services/bulk_actions.ts new file mode 100644 index 0000000000..87a681b7f3 --- /dev/null +++ b/packages/trilium-core/src/services/bulk_actions.ts @@ -0,0 +1,178 @@ +import { ActionHandlers, BulkAction, BulkActionData } from "@triliumnext/commons"; +import branchService from "./branches"; +import eraseService from "./erase.js"; + +import becca from "../becca/becca.js"; +import type BNote from "../becca/entities/bnote.js"; +import cloningService from "./cloning.js"; +import { randomString } from "./utils/index.js"; +import { getLog } from "./log"; + +type ActionHandler = (action: T, note: BNote) => void; + +type ActionHandlerMap = { + [K in keyof ActionHandlers]: ActionHandler>; +}; + +const ACTION_HANDLERS: ActionHandlerMap = { + addLabel: (action, note) => { + note.addLabel(action.labelName, action.labelValue); + }, + addRelation: (action, note) => { + note.addRelation(action.relationName, action.targetNoteId); + }, + deleteNote: (action, note) => { + const deleteId = `searchbulkaction-${randomString(10)}`; + + note.deleteNote(deleteId); + }, + deleteRevisions: (action, note) => { + const revisionIds = note + .getRevisions() + .map((rev) => rev.revisionId) + .filter((rev) => !!rev) as string[]; + eraseService.eraseRevisions(revisionIds); + }, + deleteLabel: (action, note) => { + for (const label of note.getOwnedLabels(action.labelName)) { + label.markAsDeleted(); + } + }, + deleteRelation: (action, note) => { + for (const relation of note.getOwnedRelations(action.relationName)) { + relation.markAsDeleted(); + } + }, + renameNote: (action, note) => { + // "officially" injected value: + // - note + + const newTitle = eval(`\`${action.newTitle}\``); + + if (note.title !== newTitle) { + note.title = newTitle; + note.save(); + } + }, + renameLabel: (action, note) => { + for (const label of note.getOwnedLabels(action.oldLabelName)) { + // attribute name is immutable, renaming means delete old + create new + const newLabel = label.createClone("label", action.newLabelName, label.value); + + newLabel.save(); + label.markAsDeleted(); + } + }, + renameRelation: (action, note) => { + for (const relation of note.getOwnedRelations(action.oldRelationName)) { + // attribute name is immutable, renaming means delete old + create new + const newRelation = relation.createClone("relation", action.newRelationName, relation.value); + + newRelation.save(); + relation.markAsDeleted(); + } + }, + updateLabelValue: (action, note) => { + for (const label of note.getOwnedLabels(action.labelName)) { + label.value = action.labelValue; + label.save(); + } + }, + updateRelationTarget: (action, note) => { + for (const relation of note.getOwnedRelations(action.relationName)) { + relation.value = action.targetNoteId; + relation.save(); + } + }, + moveNote: (action, note) => { + const targetParentNote = becca.getNote(action.targetParentNoteId); + + if (!targetParentNote) { + getLog().info(`Cannot execute moveNote because note ${action.targetParentNoteId} doesn't exist.`); + + return; + } + + let res; + + if (note.getParentBranches().length > 1) { + res = cloningService.cloneNoteToParentNote(note.noteId, action.targetParentNoteId); + } else { + res = branchService.moveBranchToNote(note.getParentBranches()[0], action.targetParentNoteId); + } + + if ("success" in res && !res.success) { + getLog().info(`Moving/cloning note ${note.noteId} to ${action.targetParentNoteId} failed with error ${JSON.stringify(res)}`); + } + }, + executeScript: (action, note) => { + if (!action.script || !action.script.trim()) { + getLog().info("Ignoring executeScript since the script is empty."); + return; + } + + const scriptFunc = new Function("note", action.script); + scriptFunc(note); + + note.save(); + } +} as const; + +function getActions(note: BNote) { + return note + .getLabels("action") + .map((actionLabel) => { + let action; + + try { + action = JSON.parse(actionLabel.value); + } catch (e) { + getLog().error(`Cannot parse '${actionLabel.value}' into search action, skipping.`); + return null; + } + + if (!(action.name in ACTION_HANDLERS)) { + getLog().error(`Cannot find '${action.name}' search action handler, skipping.`); + return null; + } + + return action as BulkAction; + }) + .filter((a) => !!a); +} + +/** + * Executes the bulk actions defined in the note against the provided search result note IDs. + * @param note the note containing the bulk actions, read from the `action` label. + * @param noteIds the IDs of the notes to apply the actions to. + */ +function executeActionsFromNote(note: BNote, noteIds: string[] | Set) { + const actions = getActions(note); + return executeActions(actions, noteIds); +} + +function executeActions(actions: BulkAction[], noteIds: string[] | Set) { + for (const resultNoteId of noteIds) { + const resultNote = becca.getNote(resultNoteId); + + if (!resultNote) { + continue; + } + + for (const action of actions) { + try { + getLog().info(`Applying action handler to note ${resultNote.noteId}: ${JSON.stringify(action)}`); + + const handler = ACTION_HANDLERS[action.name] as (a: typeof action, n: BNote) => void; + handler(action, resultNote); + } catch (e: any) { + getLog().error(`ExecuteScript search action failed with ${e.message}`); + } + } + } +} + +export default { + executeActions, + executeActionsFromNote +};