From 81c85d712ea42e571f3ab28630d001e67f05cb63 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 23 Jan 2026 12:11:06 +0200 Subject: [PATCH] chore(calendar): create note with attributes atomically --- .../src/widgets/collections/calendar/api.ts | 43 +++++--- apps/server/src/services/note-interface.ts | 4 +- apps/server/src/services/notes.ts | 98 ++++++++++--------- 3 files changed, 85 insertions(+), 60 deletions(-) diff --git a/apps/client/src/widgets/collections/calendar/api.ts b/apps/client/src/widgets/collections/calendar/api.ts index 65f8905e4c..fde9a26501 100644 --- a/apps/client/src/widgets/collections/calendar/api.ts +++ b/apps/client/src/widgets/collections/calendar/api.ts @@ -1,4 +1,4 @@ -import { CreateChildrenResponse } from "@triliumnext/commons"; +import { AttributeRow, CreateChildrenResponse } from "@triliumnext/commons"; import FNote from "../../../entities/fnote"; import { setAttribute, setLabel } from "../../../services/attributes"; @@ -21,24 +21,41 @@ interface ChangeEventOpts { } export async function newEvent(parentNote: FNote, { title, startDate, endDate, startTime, endTime, componentId }: NewEventOpts) { - // Create the note. - const { note } = await server.post(`notes/${parentNote.noteId}/children?target=into`, { - title, - content: "", - type: "text" - }, componentId); - - // Set the attributes. - setLabel(note.noteId, "startDate", startDate, false, componentId); + const attributes: Omit[] = []; + attributes.push({ + type: "label", + name: "startDate", + value: startDate + }); if (endDate) { - setLabel(note.noteId, "endDate", endDate, false, componentId); + attributes.push({ + type: "label", + name: "endDate", + value: endDate + }); } if (startTime) { - setLabel(note.noteId, "startTime", startTime, false, componentId); + attributes.push({ + type: "label", + name: "startTime", + value: startTime + }); } if (endTime) { - setLabel(note.noteId, "endTime", endTime, false, componentId); + attributes.push({ + type: "label", + name: "endTime", + value: endTime + }); } + + // Create the note. + await server.post(`notes/${parentNote.noteId}/children?target=into`, { + title, + content: "", + type: "text", + attributes + }, componentId); } export async function changeEvent(note: FNote, { startDate, endDate, startTime, endTime }: ChangeEventOpts) { diff --git a/apps/server/src/services/note-interface.ts b/apps/server/src/services/note-interface.ts index 5ebaa6dfa7..d7b7c9b4fa 100644 --- a/apps/server/src/services/note-interface.ts +++ b/apps/server/src/services/note-interface.ts @@ -1,4 +1,4 @@ -import type { NoteType } from "@triliumnext/commons"; +import type { AttributeRow, NoteType } from "@triliumnext/commons"; export interface NoteParams { /** optionally can force specific noteId */ @@ -24,4 +24,6 @@ export interface NoteParams { utcDateCreated?: string; ignoreForbiddenParents?: boolean; target?: "into"; + /** Attributes to be set on the note. These are set atomically on note creation, so entity changes are not sent for attributes defined here. */ + attributes?: Omit[]; } diff --git a/apps/server/src/services/notes.ts b/apps/server/src/services/notes.ts index 4964a57976..a54f9e6340 100644 --- a/apps/server/src/services/notes.ts +++ b/apps/server/src/services/notes.ts @@ -1,33 +1,34 @@ -import sql from "./sql.js"; -import optionService from "./options.js"; +import type { AttachmentRow, AttributeRow, BranchRow, NoteRow } from "@triliumnext/commons"; +import { dayjs } from "@triliumnext/commons"; +import fs from "fs"; +import html2plaintext from "html2plaintext"; +import { t } from "i18next"; +import path from "path"; +import url from "url"; + +import becca from "../becca/becca.js"; +import BAttachment from "../becca/entities/battachment.js"; +import BAttribute from "../becca/entities/battribute.js"; +import BBranch from "../becca/entities/bbranch.js"; +import BNote from "../becca/entities/bnote.js"; +import ValidationError from "../errors/validation_error.js"; +import cls from "../services/cls.js"; +import log from "../services/log.js"; +import protectedSessionService from "../services/protected_session.js"; +import { newEntityId, quoteRegex, toMap,unescapeHtml } from "../services/utils.js"; import dateUtils from "./date_utils.js"; import entityChangesService from "./entity_changes.js"; import eventService from "./events.js"; -import cls from "../services/cls.js"; -import protectedSessionService from "../services/protected_session.js"; -import log from "../services/log.js"; -import { newEntityId, unescapeHtml, quoteRegex, toMap } from "../services/utils.js"; -import revisionService from "./revisions.js"; -import request from "./request.js"; -import path from "path"; -import url from "url"; -import becca from "../becca/becca.js"; -import BBranch from "../becca/entities/bbranch.js"; -import BNote from "../becca/entities/bnote.js"; -import BAttribute from "../becca/entities/battribute.js"; -import BAttachment from "../becca/entities/battachment.js"; -import { dayjs } from "@triliumnext/commons"; import htmlSanitizer from "./html_sanitizer.js"; -import ValidationError from "../errors/validation_error.js"; -import noteTypesService from "./note_types.js"; -import fs from "fs"; -import ws from "./ws.js"; -import html2plaintext from "html2plaintext"; -import type { AttachmentRow, AttributeRow, BranchRow, NoteRow } from "@triliumnext/commons"; -import type TaskContext from "./task_context.js"; -import type { NoteParams } from "./note-interface.js"; import imageService from "./image.js"; -import { t } from "i18next"; +import noteTypesService from "./note_types.js"; +import type { NoteParams } from "./note-interface.js"; +import optionService from "./options.js"; +import request from "./request.js"; +import revisionService from "./revisions.js"; +import sql from "./sql.js"; +import type TaskContext from "./task_context.js"; +import ws from "./ws.js"; interface FoundLink { name: "imageLink" | "internalLink" | "includeNoteLink" | "relationMapLink"; @@ -47,14 +48,13 @@ function getNewNotePosition(parentNote: BNote) { .reduce((min, note) => Math.min(min, note?.notePosition || 0), 0); return minNotePos - 10; - } else { - const maxNotePos = parentNote - .getChildBranches() - .filter((branch) => branch?.noteId !== "_hidden") // has "always last" note position - .reduce((max, note) => Math.max(max, note?.notePosition || 0), 0); - - return maxNotePos + 10; } + const maxNotePos = parentNote + .getChildBranches() + .filter((branch) => branch?.noteId !== "_hidden") // has "always last" note position + .reduce((max, note) => Math.max(max, note?.notePosition || 0), 0); + + return maxNotePos + 10; } function triggerNoteTitleChanged(note: BNote) { @@ -88,7 +88,7 @@ function copyChildAttributes(parentNote: BNote, childNote: BNote) { new BAttribute({ noteId: childNote.noteId, type: attr.type, - name: name, + name, value: attr.value, position: attr.position, isInheritable: attr.isInheritable @@ -222,6 +222,14 @@ function createNewNote(params: NoteParams): { utcDateCreated: params.utcDateCreated }).save(); + // Create attributes atomically. + for (const attribute of params.attributes || []) { + new BAttribute({ + ...attribute, + noteId: note.noteId + }).save(); + } + note.setContent(params.content); branch = new BBranch({ @@ -260,7 +268,7 @@ function createNewNote(params: NoteParams): { eventService.emit(eventService.ENTITY_CHANGED, { entityName: "blobs", entity: note }); eventService.emit(eventService.ENTITY_CREATED, { entityName: "branches", entity: branch }); eventService.emit(eventService.ENTITY_CHANGED, { entityName: "branches", entity: branch }); - eventService.emit(eventService.CHILD_NOTE_CREATED, { childNote: note, parentNote: parentNote }); + eventService.emit(eventService.CHILD_NOTE_CREATED, { childNote: note, parentNote }); log.info(`Created new note '${note.noteId}', branch '${branch.branchId}' of type '${note.type}', mime '${note.mime}'`); @@ -308,9 +316,8 @@ function createNewNoteWithTarget(target: "into" | "after" | "before", targetBran entityChangesService.putNoteReorderingEntityChange(params.parentNoteId); return retObject; - } else { - throw new Error(`Unknown target '${target}'`); } + throw new Error(`Unknown target '${target}'`); } function protectNoteRecursively(note: BNote, protect: boolean, includingSubTree: boolean, taskContext: TaskContext<"protectNotes">) { @@ -488,7 +495,7 @@ function findRelationMapLinks(content: string, foundLinks: FoundLink[]) { }); } } catch (e: any) { - log.error("Could not scan for relation map links: " + e.message); + log.error(`Could not scan for relation map links: ${e.message}`); } } @@ -656,8 +663,8 @@ function saveAttachments(note: BNote, content: string) { const attachment = note.saveAttachment({ role: "file", - mime: mime, - title: title, + mime, + title, content: buffer }); @@ -953,7 +960,7 @@ function duplicateSubtree(origNoteId: string, newParentNoteId: string) { const duplicateNoteSuffix = t("notes.duplicate-note-suffix"); if (!res.note.title.endsWith(duplicateNoteSuffix) && !res.note.title.startsWith(duplicateNoteSuffix)) { - res.note.title = t("notes.duplicate-note-title", { noteTitle: res.note.title, duplicateNoteSuffix: duplicateNoteSuffix }); + res.note.title = t("notes.duplicate-note-title", { noteTitle: res.note.title, duplicateNoteSuffix }); } res.note.save(); @@ -1050,13 +1057,12 @@ function duplicateSubtreeInner(origNote: BNote, origBranch: BBranch | null | und note: existingNote, branch: createDuplicatedBranch() }; - } else { - return { - // order here is important, note needs to be created first to not mess up the becca - note: createDuplicatedNote(), - branch: createDuplicatedBranch() - }; } + return { + // order here is important, note needs to be created first to not mess up the becca + note: createDuplicatedNote(), + branch: createDuplicatedBranch() + }; } function getNoteIdMapping(origNote: BNote) {