chore(core): integrate branches service and route

This commit is contained in:
Elian Doran 2026-01-12 19:25:45 +02:00
parent f9731d9cfc
commit 0c52b56e02
No known key found for this signature in database
9 changed files with 88 additions and 34 deletions

View File

@ -1,3 +1,4 @@
import { routes } from "@triliumnext/core";
import { createPartialContentHandler } from "@triliumnext/express-partial-content";
import express from "express";
import rateLimit from "express-rate-limit";
@ -21,7 +22,6 @@ import appInfoRoute from "./api/app_info.js";
import attributesRoute from "./api/attributes.js";
import autocompleteApiRoute from "./api/autocomplete.js";
import backendLogRoute from "./api/backend_log.js";
import branchesApiRoute from "./api/branches.js";
import bulkActionRoute from "./api/bulk_action.js";
import clipperRoute from "./api/clipper.js";
import cloningApiRoute from "./api/cloning.js";
@ -62,7 +62,6 @@ 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",
@ -124,15 +123,6 @@ function register(app: express.Application) {
apiRoute(PST, "/api/notes/:noteId/save-to-tmp-dir", filesRoute.saveNoteToTmpDir);
apiRoute(PST, "/api/notes/:noteId/upload-modified-file", filesRoute.uploadModifiedFileToNote);
apiRoute(PUT, "/api/branches/:branchId/move-to/:parentBranchId", branchesApiRoute.moveBranchToParent);
apiRoute(PUT, "/api/branches/:branchId/move-before/:beforeBranchId", branchesApiRoute.moveBranchBeforeNote);
apiRoute(PUT, "/api/branches/:branchId/move-after/:afterBranchId", branchesApiRoute.moveBranchAfterNote);
apiRoute(PUT, "/api/branches/:branchId/expanded/:expanded", branchesApiRoute.setExpanded);
apiRoute(PUT, "/api/branches/:branchId/expanded-subtree/:expanded", branchesApiRoute.setExpandedForSubtree);
apiRoute(DEL, "/api/branches/:branchId", branchesApiRoute.deleteBranch);
apiRoute(PUT, "/api/branches/:branchId/set-prefix", branchesApiRoute.setPrefix);
apiRoute(PUT, "/api/branches/set-prefix-batch", branchesApiRoute.setPrefixBatch);
// TODO: Bring back attachment uploading
// route(PST, "/api/notes/:noteId/attachments/upload", [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], attachmentsApiRoute.uploadAttachment, apiResultHandler);
route(GET, "/api/attachments/:attachmentId/image/:filename", [auth.checkApiAuthOrElectron], imageRoute.returnAttachedImage);

View File

@ -1,5 +1,5 @@
import { type AttributeRow, dayjs, formatLogMessage } from "@triliumnext/commons";
import { type AbstractBeccaEntity, Becca, NoteParams } from "@triliumnext/core";
import { type AbstractBeccaEntity, Becca, branches as branchService, NoteParams } from "@triliumnext/core";
import axios from "axios";
import * as cheerio from "cheerio";
import xml2js from "xml2js";
@ -16,7 +16,6 @@ import appInfo from "./app_info.js";
import attributeService from "./attributes.js";
import type { ApiParams } from "./backend_script_api_interface.js";
import backupService from "./backup.js";
import branchService from "./branches.js";
import cloningService from "./cloning.js";
import config from "./config.js";
import dateNoteService from "./date_notes.js";

View File

@ -1,9 +1,8 @@
import { ActionHandlers, BulkAction, BulkActionData } from "@triliumnext/commons";
import { erase as eraseService } from "@triliumnext/core";
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 branchService from "./branches.js";
import cloningService from "./cloning.js";
import log from "./log.js";
import { randomString } from "./utils.js";

View File

@ -87,11 +87,6 @@ export function constantTimeCompare(a: string | null | undefined, b: string | nu
return crypto.timingSafeEqual(bufA, bufB);
}
export function isEmptyOrWhitespace(str: string | null | undefined) {
if (!str) return true;
return str.match(/^ *$/) !== null;
}
export function sanitizeSqlIdentifier(str: string) {
return str.replace(/[^A-Za-z0-9_]/g, "");
}
@ -457,6 +452,8 @@ export const unescapeHtml = coreUtils.unescapeHtml;
export const randomSecureToken = coreUtils.randomSecureToken;
/** @deprecated */
export const safeExtractMessageAndStackFromError = coreUtils.safeExtractMessageAndStackFromError;
/** @deprecated */
export const isEmptyOrWhitespace = coreUtils.isEmptyOrWhitespace;
export default {
compareVersions,

View File

@ -37,6 +37,7 @@ export { default as TaskContext } from "./services/task_context";
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";
// Messaging system
export * from "./services/messaging/index";

View File

@ -1,14 +1,16 @@
import { erase as eraseService, events as eventService, ValidationError } from "@triliumnext/core";
import branchService from "../../services/branches.js";
import eraseService from "../../services/erase.js";
import eventService from "../../services/events.js";
import type { Request } from "express";
import becca from "../../becca/becca.js";
import branchService from "../../services/branches.js";
import entityChangesService from "../../services/entity_changes.js";
import log from "../../services/log.js";
import sql from "../../services/sql.js";
import { getLog } from "../../services/log.js";
import TaskContext from "../../services/task_context.js";
import treeService from "../../services/tree.js";
import utils from "../../services/utils.js";
import { isEmptyOrWhitespace, randomString } from "../../services/utils/index.js";
import { getSql } from "../../services/sql/index.js";
import { ValidationError } from "../../errors.js";
/**
* Code in this file deals with moving and cloning branches. The relationship between note and parent note is unique
@ -45,7 +47,7 @@ function moveBranchBeforeNote(req: Request) {
// we don't change utcDateModified, so other changes are prioritized in case of conflict
// also we would have to sync all those modified branches otherwise hash checks would fail
sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition >= ? AND isDeleted = 0", [beforeBranch.parentNoteId, originalBeforeNotePosition]);
getSql().execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition >= ? AND isDeleted = 0", [beforeBranch.parentNoteId, originalBeforeNotePosition]);
// also need to update becca positions
const parentNote = becca.getNoteOrThrow(beforeBranch.parentNoteId);
@ -71,7 +73,7 @@ function moveBranchBeforeNote(req: Request) {
// if sorting is not needed, then still the ordering might have changed above manually
entityChangesService.putNoteReorderingEntityChange(parentNote.noteId);
log.info(`Moved note ${branchToMove.noteId}, branch ${branchId} before note ${beforeBranch.noteId}, branch ${beforeBranchId}`);
getLog().info(`Moved note ${branchToMove.noteId}, branch ${branchId} before note ${beforeBranch.noteId}, branch ${beforeBranchId}`);
return { success: true };
}
@ -92,7 +94,7 @@ function moveBranchAfterNote(req: Request) {
// we don't change utcDateModified, so other changes are prioritized in case of conflict
// also we would have to sync all those modified branches otherwise hash checks would fail
sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0", [afterNote.parentNoteId, originalAfterNotePosition]);
getSql().execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0", [afterNote.parentNoteId, originalAfterNotePosition]);
// also need to update becca positions
const parentNote = becca.getNoteOrThrow(afterNote.parentNoteId);
@ -120,7 +122,7 @@ function moveBranchAfterNote(req: Request) {
// if sorting is not needed, then still the ordering might have changed above manually
entityChangesService.putNoteReorderingEntityChange(parentNote.noteId);
log.info(`Moved note ${branchToMove.noteId}, branch ${branchId} after note ${afterNote.noteId}, branch ${afterBranchId}`);
getLog().info(`Moved note ${branchToMove.noteId}, branch ${branchId} after note ${afterNote.noteId}, branch ${afterBranchId}`);
return { success: true };
}
@ -130,7 +132,7 @@ function setExpanded(req: Request) {
const expanded = parseInt(req.params.expanded);
if (branchId !== "none_root") {
sql.execute("UPDATE branches SET isExpanded = ? WHERE branchId = ?", [expanded, branchId]);
getSql().execute("UPDATE branches SET isExpanded = ? WHERE branchId = ?", [expanded, branchId]);
// we don't sync expanded label
// also this does not trigger updates to the frontend, this would trigger too many reloads
@ -150,6 +152,7 @@ function setExpanded(req: Request) {
function setExpandedForSubtree(req: Request) {
const { branchId } = req.params;
const expanded = parseInt(req.params.expanded);
const sql = getSql();
let branchIds = sql.getColumn<string>(
`
@ -236,7 +239,7 @@ function deleteBranch(req: Request) {
const taskContext = TaskContext.getInstance(req.query.taskId as string, "deleteNotes", null);
const deleteId = utils.randomString(10);
const deleteId = randomString(10);
let noteDeleted;
if (eraseNotes) {
@ -260,7 +263,7 @@ function deleteBranch(req: Request) {
function setPrefix(req: Request) {
const branchId = req.params.branchId;
//TriliumNextTODO: req.body arrives as string, so req.body.prefix will be undefined did the code below ever even work?
const prefix = utils.isEmptyOrWhitespace(req.body.prefix) ? null : req.body.prefix;
const prefix = isEmptyOrWhitespace(req.body.prefix) ? null : req.body.prefix;
const branch = becca.getBranchOrThrow(branchId);
branch.prefix = prefix;
@ -279,7 +282,7 @@ function setPrefixBatch(req: Request) {
throw new ValidationError("prefix must be a string or null");
}
const normalizedPrefix = utils.isEmptyOrWhitespace(prefix) ? null : prefix;
const normalizedPrefix = isEmptyOrWhitespace(prefix) ? null : prefix;
let updatedCount = 0;
for (const branchId of branchIds) {
@ -289,7 +292,7 @@ function setPrefixBatch(req: Request) {
branch.save();
updatedCount++;
} else {
log.info(`Branch ${branchId} not found, skipping prefix update`);
getLog().info(`Branch ${branchId} not found, skipping prefix update`);
}
}

View File

@ -6,6 +6,7 @@ import attachmentsApiRoute from "./api/attachments";
import noteMapRoute from "./api/note_map";
import recentNotesRoute from "./api/recent_notes";
import otherRoute from "./api/others";
import branchesApiRoute from "./api/branches";
import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity";
// TODO: Deduplicate with routes.ts
@ -53,6 +54,15 @@ export function buildSharedApiRoutes(apiRoute: any) {
apiRoute(PUT, "/api/attachments/:attachmentId/rename", attachmentsApiRoute.renameAttachment);
apiRoute(GET, "/api/attachments/:attachmentId/blob", attachmentsApiRoute.getAttachmentBlob);
apiRoute(PUT, "/api/branches/:branchId/move-to/:parentBranchId", branchesApiRoute.moveBranchToParent);
apiRoute(PUT, "/api/branches/:branchId/move-before/:beforeBranchId", branchesApiRoute.moveBranchBeforeNote);
apiRoute(PUT, "/api/branches/:branchId/move-after/:afterBranchId", branchesApiRoute.moveBranchAfterNote);
apiRoute(PUT, "/api/branches/:branchId/expanded/:expanded", branchesApiRoute.setExpanded);
apiRoute(PUT, "/api/branches/:branchId/expanded-subtree/:expanded", branchesApiRoute.setExpandedForSubtree);
apiRoute(DEL, "/api/branches/:branchId", branchesApiRoute.deleteBranch);
apiRoute(PUT, "/api/branches/:branchId/set-prefix", branchesApiRoute.setPrefix);
apiRoute(PUT, "/api/branches/set-prefix-batch", branchesApiRoute.setPrefixBatch);
apiRoute(GET, "/api/note-map/:noteId/backlink-count", noteMapRoute.getBacklinkCount);
apiRoute(PST, "/api/recent-notes", recentNotesRoute.addRecentNote);

View File

@ -0,0 +1,50 @@
import treeService from "./tree.js";
import type BBranch from "../becca/entities/bbranch.js";
import { getSql } from "./sql/index.js";
function moveBranchToNote(branchToMove: BBranch, targetParentNoteId: string) {
if (branchToMove.parentNoteId === targetParentNoteId) {
return { success: true }; // no-op
}
const validationResult = treeService.validateParentChild(targetParentNoteId, branchToMove.noteId, branchToMove.branchId);
if (!validationResult.success) {
return [200, validationResult];
}
const maxNotePos = getSql().getValue<number | null>("SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0", [targetParentNoteId]);
const newNotePos = !maxNotePos ? 0 : maxNotePos + 10;
const newBranch = branchToMove.createClone(targetParentNoteId, newNotePos);
newBranch.save();
branchToMove.markAsDeleted();
return {
success: true,
branch: newBranch
};
}
function moveBranchToBranch(branchToMove: BBranch, targetParentBranch: BBranch, branchId: string) {
// TODO: Unused branch ID argument.
const res = moveBranchToNote(branchToMove, targetParentBranch.noteId);
if (!("success" in res) || !res.success) {
return res;
}
// expanding so that the new placement of the branch is immediately visible
if (!targetParentBranch.isExpanded) {
targetParentBranch.isExpanded = true;
targetParentBranch.save();
}
return res;
}
export default {
moveBranchToBranch,
moveBranchToNote
};

View File

@ -130,3 +130,8 @@ export function randomSecureToken(bytes = 32) {
export function safeExtractMessageAndStackFromError(err: unknown): [errMessage: string, errStack: string | undefined] {
return (err instanceof Error) ? [err.message, err.stack] as const : ["Unknown Error", undefined] as const;
}
export function isEmptyOrWhitespace(str: string | null | undefined) {
if (!str) return true;
return str.match(/^ *$/) !== null;
}