chore(core): integrate date_utils

This commit is contained in:
Elian Doran
2026-01-06 11:03:33 +02:00
parent c7f0d541c2
commit 14e2e85da7
8 changed files with 199 additions and 186 deletions

View File

@@ -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<BBranch> {
note.markAsDeleted(deleteId);
return true;
} else {
return false;
}
}
return false;
}
override beforeSaving() {
@@ -268,15 +269,15 @@ class BBranch extends AbstractBeccaEntity<BBranch> {
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() {

View File

@@ -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,

View File

@@ -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;

View File

@@ -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) {

View File

@@ -5,5 +5,8 @@
"main": "src/index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"@triliumnext/commons": "workspace:*"
}
}

View File

@@ -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 }: {

View File

@@ -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
};

8
pnpm-lock.yaml generated
View File

@@ -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: