From 14e2e85da70f9f1dbaba78dd7116fd162fe3f4b8 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 6 Jan 2026 11:03:33 +0200 Subject: [PATCH] chore(core): integrate date_utils --- apps/server/src/becca/entities/bbranch.ts | 41 +++---- apps/server/src/services/cls.ts | 5 - apps/server/src/services/date_utils.ts | 109 +---------------- apps/server/src/services/notes.ts | 105 +++++++++-------- packages/trilium-core/package.json | 3 + packages/trilium-core/src/index.ts | 3 +- .../trilium-core/src/services/utils/date.ts | 111 ++++++++++++++++++ pnpm-lock.yaml | 8 +- 8 files changed, 199 insertions(+), 186 deletions(-) create mode 100644 packages/trilium-core/src/services/utils/date.ts diff --git a/apps/server/src/becca/entities/bbranch.ts b/apps/server/src/becca/entities/bbranch.ts index e07443024..d1eb91a9e 100644 --- a/apps/server/src/becca/entities/bbranch.ts +++ b/apps/server/src/becca/entities/bbranch.ts @@ -1,14 +1,15 @@ -"use strict"; -import BNote from "./bnote.js"; -import AbstractBeccaEntity from "./abstract_becca_entity.js"; -import dateUtils from "../../services/date_utils.js"; -import utils from "../../services/utils.js"; -import TaskContext from "../../services/task_context.js"; -import cls from "../../services/cls.js"; -import log from "../../services/log.js"; + import type { BranchRow } from "@triliumnext/commons"; + +import cls from "../../services/cls.js"; +import dateUtils from "../../services/date_utils.js"; import handlers from "../../services/handlers.js"; +import log from "../../services/log.js"; +import TaskContext from "../../services/task_context.js"; +import utils from "../../services/utils.js"; +import AbstractBeccaEntity from "./abstract_becca_entity.js"; +import BNote from "./bnote.js"; /** * Branch represents a relationship between a child note and its parent note. Trilium allows a note to have multiple @@ -199,9 +200,9 @@ class BBranch extends AbstractBeccaEntity { note.markAsDeleted(deleteId); return true; - } else { - return false; - } + } + return false; + } override beforeSaving() { @@ -268,15 +269,15 @@ class BBranch extends AbstractBeccaEntity { existingBranch.notePosition = notePosition; } return existingBranch; - } else { - return new BBranch({ - noteId: this.noteId, - parentNoteId: parentNoteId, - notePosition: notePosition || null, - prefix: this.prefix, - isExpanded: this.isExpanded - }); - } + } + return new BBranch({ + noteId: this.noteId, + parentNoteId, + notePosition: notePosition || null, + prefix: this.prefix, + isExpanded: this.isExpanded + }); + } getParentNote() { diff --git a/apps/server/src/services/cls.ts b/apps/server/src/services/cls.ts index 5835385be..4d75ca67d 100644 --- a/apps/server/src/services/cls.ts +++ b/apps/server/src/services/cls.ts @@ -25,10 +25,6 @@ function getComponentId() { return getContext().get("componentId"); } -function getLocalNowDateTime() { - return getContext().get("localNowDateTime"); -} - function disableEntityEvents() { getContext().set("disableEntityEvents", true); } @@ -93,7 +89,6 @@ export default { set, getHoistedNoteId, getComponentId, - getLocalNowDateTime, disableEntityEvents, enableEntityEvents, isEntityEventsDisabled, diff --git a/apps/server/src/services/date_utils.ts b/apps/server/src/services/date_utils.ts index 0cbe69cfa..0e38fb1ea 100644 --- a/apps/server/src/services/date_utils.ts +++ b/apps/server/src/services/date_utils.ts @@ -1,107 +1,2 @@ -import { dayjs } from "@triliumnext/commons"; -import cls from "./cls.js"; - -const LOCAL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss.SSSZZ"; -const UTC_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ssZ"; - -function utcNowDateTime() { - return utcDateTimeStr(new Date()); -} - -// CLS date time is important in web deployments - server often runs in different time zone than user is located in, -// so we'd prefer client timezone to be used to record local dates. For this reason, requests from clients contain -// "trilium-local-now-datetime" header which is then stored in CLS -function localNowDateTime() { - return cls.getLocalNowDateTime() || dayjs().format(LOCAL_DATETIME_FORMAT); -} - -function localNowDate() { - const clsDateTime = cls.getLocalNowDateTime(); - - if (clsDateTime) { - return clsDateTime.substr(0, 10); - } else { - const date = new Date(); - - return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`; - } -} - -function pad(num: number) { - return num <= 9 ? `0${num}` : `${num}`; -} - -function utcDateStr(date: Date) { - return date.toISOString().split("T")[0]; -} - -function utcDateTimeStr(date: Date) { - return date.toISOString().replace("T", " "); -} - -/** - * @param str - needs to be in the ISO 8601 format "YYYY-MM-DDTHH:MM:SS.sssZ" format as outputted by dateStr(). - * also is assumed to be GMT time (as indicated by the "Z" at the end), *not* local time - */ -function parseDateTime(str: string) { - try { - return new Date(Date.parse(str)); - } catch (e: any) { - throw new Error(`Can't parse date from '${str}': ${e.stack}`); - } -} - -function parseLocalDate(str: string) { - const datePart = str.substr(0, 10); - - // not specifying the timezone and specifying the time means Date.parse() will use the local timezone - return parseDateTime(`${datePart} 12:00:00.000`); -} - -function getDateTimeForFile() { - return new Date().toISOString().substr(0, 19).replace(/:/g, ""); -} - -function validateLocalDateTime(str: string | null | undefined) { - if (!str) { - return; - } - - if (!/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}[+-][0-9]{4}/.test(str)) { - return `Invalid local date time format in '${str}'. Correct format shoud follow example: '2023-08-21 23:38:51.110+0200'`; - } - - if (!dayjs(str, LOCAL_DATETIME_FORMAT)) { - return `Date '${str}' appears to be in the correct format, but cannot be parsed. It likely represents an invalid date.`; - } -} - -function validateUtcDateTime(str: string | undefined) { - if (!str) { - return; - } - - if (!/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z/.test(str)) { - return `Invalid UTC date time format in '${str}'. Correct format shoud follow example: '2023-08-21 23:38:51.110Z'`; - } - - if (!dayjs(str, UTC_DATETIME_FORMAT)) { - return `Date '${str}' appears to be in the correct format, but cannot be parsed. It likely represents an invalid date.`; - } -} - -export default { - LOCAL_DATETIME_FORMAT, - UTC_DATETIME_FORMAT, - utcNowDateTime, - localNowDateTime, - localNowDate, - - utcDateStr, - utcDateTimeStr, - parseDateTime, - parseLocalDate, - getDateTimeForFile, - validateLocalDateTime, - validateUtcDateTime -}; +import { date_utils } from "@triliumnext/core"; +export default date_utils; diff --git a/apps/server/src/services/notes.ts b/apps/server/src/services/notes.ts index 4964a5797..a4e1ed3cf 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 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 type { AttachmentRow, AttributeRow, BranchRow, NoteRow } from "@triliumnext/commons"; +import { dayjs } from "@triliumnext/commons"; +import { date_utils } from "@triliumnext/core"; +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 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 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 entityChangesService from "./entity_changes.js"; +import eventService from "./events.js"; +import htmlSanitizer from "./html_sanitizer.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,14 @@ 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 +89,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 @@ -121,7 +122,7 @@ function getNewNoteTitle(parentNote: BNote) { if (titleTemplate !== null) { try { - const now = dayjs(cls.getLocalNowDateTime() || new Date()); + const now = dayjs(date_utils.localNowDateTime() || new Date()); // "officially" injected values: // - now @@ -189,11 +190,11 @@ function createNewNote(params: NoteParams): { } let error; - if ((error = dateUtils.validateLocalDateTime(params.dateCreated))) { + if ((error = date_utils.validateLocalDateTime(params.dateCreated))) { throw new Error(error); } - if ((error = dateUtils.validateUtcDateTime(params.utcDateCreated))) { + if ((error = date_utils.validateUtcDateTime(params.utcDateCreated))) { throw new Error(error); } @@ -260,7 +261,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 +309,9 @@ 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">) { @@ -384,7 +385,7 @@ function checkImageAttachments(note: BNote, content: string) { attachment.utcDateScheduledForErasureSince = null; attachment.save(); } else if (!attachment.utcDateScheduledForErasureSince && !attachmentInContent) { - attachment.utcDateScheduledForErasureSince = dateUtils.utcNowDateTime(); + attachment.utcDateScheduledForErasureSince = date_utils.utcNowDateTime(); attachment.save(); } } @@ -488,7 +489,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 +657,8 @@ function saveAttachments(note: BNote, content: string) { const attachment = note.saveAttachment({ role: "file", - mime: mime, - title: title, + mime, + title, content: buffer }); @@ -739,11 +740,11 @@ function saveRevisionIfNeeded(note: BNote) { const now = new Date(); const revisionSnapshotTimeInterval = parseInt(optionService.getOption("revisionSnapshotTimeInterval")); - const revisionCutoff = dateUtils.utcDateTimeStr(new Date(now.getTime() - revisionSnapshotTimeInterval * 1000)); + const revisionCutoff = date_utils.utcDateTimeStr(new Date(now.getTime() - revisionSnapshotTimeInterval * 1000)); const existingRevisionId = sql.getValue("SELECT revisionId FROM revisions WHERE noteId = ? AND utcDateCreated >= ?", [note.noteId, revisionCutoff]); - const msSinceDateCreated = now.getTime() - dateUtils.parseDateTime(note.utcDateCreated).getTime(); + const msSinceDateCreated = now.getTime() - date_utils.parseDateTime(note.utcDateCreated).getTime(); if (!existingRevisionId && msSinceDateCreated >= revisionSnapshotTimeInterval * 1000) { note.saveRevision(); @@ -953,7 +954,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(); @@ -999,8 +1000,8 @@ function duplicateSubtreeInner(origNote: BNote, origBranch: BBranch | null | und const newNote = new BNote({ ...origNote, noteId: newNoteId, - dateCreated: dateUtils.localNowDateTime(), - utcDateCreated: dateUtils.utcNowDateTime() + dateCreated: date_utils.localNowDateTime(), + utcDateCreated: date_utils.utcNowDateTime() }).save(); let content = origNote.getContent(); @@ -1050,13 +1051,13 @@ 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) { diff --git a/packages/trilium-core/package.json b/packages/trilium-core/package.json index 7e6df61d3..1bc92db23 100644 --- a/packages/trilium-core/package.json +++ b/packages/trilium-core/package.json @@ -5,5 +5,8 @@ "main": "src/index.ts", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "@triliumnext/commons": "workspace:*" } } diff --git a/packages/trilium-core/src/index.ts b/packages/trilium-core/src/index.ts index eee6b2d65..042bde8b2 100644 --- a/packages/trilium-core/src/index.ts +++ b/packages/trilium-core/src/index.ts @@ -9,7 +9,8 @@ export * from "./services/sql/index"; export * as protected_session from "./services/encryption/protected_session"; export { default as data_encryption } from "./services/encryption/data_encryption" export * as binary_utils from "./services/utils/binary"; -export type { ExecutionContext } from "./services/context"; +export { default as date_utils } from "./services/utils/date"; +export { getContext, type ExecutionContext } from "./services/context"; export type { CryptoProvider } from "./services/encryption/crypto"; export function initializeCore({ dbConfig, executionContext, crypto }: { diff --git a/packages/trilium-core/src/services/utils/date.ts b/packages/trilium-core/src/services/utils/date.ts new file mode 100644 index 000000000..657eea11e --- /dev/null +++ b/packages/trilium-core/src/services/utils/date.ts @@ -0,0 +1,111 @@ +import { dayjs } from "@triliumnext/commons"; +import { getContext } from "../context.js"; + +const LOCAL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss.SSSZZ"; +const UTC_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ssZ"; + +function utcNowDateTime() { + return utcDateTimeStr(new Date()); +} + +// CLS date time is important in web deployments - server often runs in different time zone than user is located in, +// so we'd prefer client timezone to be used to record local dates. For this reason, requests from clients contain +// "trilium-local-now-datetime" header which is then stored in CLS +function localNowDateTime() { + return getLocalNowDateTime() || dayjs().format(LOCAL_DATETIME_FORMAT); +} + +function localNowDate() { + const clsDateTime = getLocalNowDateTime(); + + if (clsDateTime) { + return clsDateTime.substr(0, 10); + } else { + const date = new Date(); + + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`; + } +} + +function pad(num: number) { + return num <= 9 ? `0${num}` : `${num}`; +} + +function utcDateStr(date: Date) { + return date.toISOString().split("T")[0]; +} + +function utcDateTimeStr(date: Date) { + return date.toISOString().replace("T", " "); +} + +/** + * @param str - needs to be in the ISO 8601 format "YYYY-MM-DDTHH:MM:SS.sssZ" format as outputted by dateStr(). + * also is assumed to be GMT time (as indicated by the "Z" at the end), *not* local time + */ +function parseDateTime(str: string) { + try { + return new Date(Date.parse(str)); + } catch (e: any) { + throw new Error(`Can't parse date from '${str}': ${e.stack}`); + } +} + +function parseLocalDate(str: string) { + const datePart = str.substr(0, 10); + + // not specifying the timezone and specifying the time means Date.parse() will use the local timezone + return parseDateTime(`${datePart} 12:00:00.000`); +} + +function getDateTimeForFile() { + return new Date().toISOString().substr(0, 19).replace(/:/g, ""); +} + +function validateLocalDateTime(str: string | null | undefined) { + if (!str) { + return; + } + + if (!/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}[+-][0-9]{4}/.test(str)) { + return `Invalid local date time format in '${str}'. Correct format shoud follow example: '2023-08-21 23:38:51.110+0200'`; + } + + if (!dayjs(str, LOCAL_DATETIME_FORMAT)) { + return `Date '${str}' appears to be in the correct format, but cannot be parsed. It likely represents an invalid date.`; + } +} + +function validateUtcDateTime(str: string | undefined) { + if (!str) { + return; + } + + if (!/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z/.test(str)) { + return `Invalid UTC date time format in '${str}'. Correct format shoud follow example: '2023-08-21 23:38:51.110Z'`; + } + + if (!dayjs(str, UTC_DATETIME_FORMAT)) { + return `Date '${str}' appears to be in the correct format, but cannot be parsed. It likely represents an invalid date.`; + } +} + +function getLocalNowDateTime() { + return getContext().get("localNowDateTime"); +} + +export default { + LOCAL_DATETIME_FORMAT, + UTC_DATETIME_FORMAT, + utcNowDateTime, + localNowDateTime, + localNowDate, + + utcDateStr, + utcDateTimeStr, + parseDateTime, + parseLocalDate, + getDateTimeForFile, + validateLocalDateTime, + validateUtcDateTime +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc5d57faf..2984cdb3b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1444,7 +1444,11 @@ importers: specifier: 5.1.0 version: 5.1.0(karma@6.4.4(bufferutil@4.0.9)(utf-8-validate@6.0.5)) - packages/trilium-core: {} + packages/trilium-core: + dependencies: + '@triliumnext/commons': + specifier: workspace:* + version: link:../commons packages/turndown-plugin-gfm: devDependencies: @@ -15558,6 +15562,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.3.0 ckeditor5: 47.3.0 es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-editor-classic@47.3.0': dependencies: