1764 lines
57 KiB
TypeScript
Raw Normal View History

2020-05-17 09:48:24 +02:00
"use strict";
import protectedSessionService from "../../services/protected_session.js";
import log from "../../services/log.js";
import sql from "../../services/sql.js";
2024-09-04 08:41:17 +00:00
import optionService from "../../services/options.js";
import eraseService from "../../services/erase.js";
import utils from "../../services/utils.js";
import dateUtils from "../../services/date_utils.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
import BRevision from "./brevision.js";
import BAttachment from "./battachment.js";
import TaskContext from "../../services/task_context.js";
import dayjs from "dayjs";
2024-07-24 20:35:19 +03:00
import utc from "dayjs/plugin/utc.js";
import eventService from "../../services/events.js";
2025-04-18 12:33:50 +03:00
import type { AttachmentRow, AttributeType, NoteRow, NoteType, RevisionRow } from "@triliumnext/commons";
import type BBranch from "./bbranch.js";
import BAttribute from "./battribute.js";
import type { NotePojo } from "../becca-interface.js";
2024-07-18 22:58:12 +03:00
import searchService from "../../services/search/services/search.js";
import cloningService, { type CloneResponse } from "../../services/cloning.js";
2024-07-18 22:58:12 +03:00
import noteService from "../../services/notes.js";
import handlers from "../../services/handlers.js";
dayjs.extend(utc);
2020-05-17 09:48:24 +02:00
2025-01-09 18:07:02 +02:00
const LABEL = "label";
const RELATION = "relation";
2021-04-17 20:52:46 +02:00
// TODO: Deduplicate with fnote
const NOTE_TYPE_ICONS = {
file: "bx bx-file",
image: "bx bx-image",
code: "bx bx-code",
render: "bx bx-extension",
search: "bx bx-file-find",
relationMap: "bx bxs-network-chart",
book: "bx bx-book",
noteMap: "bx bxs-network-chart",
mermaid: "bx bx-selection",
canvas: "bx bx-pen",
webView: "bx bx-globe-alt",
launcher: "bx bx-link",
doc: "bx bxs-file-doc",
contentWidget: "bx bxs-widget",
mindMap: "bx bx-sitemap",
geoMap: "bx bx-map-alt"
};
2024-02-17 10:56:27 +02:00
interface NotePathRecord {
isArchived: boolean;
isInHoistedSubTree: boolean;
notePath: string[];
isHidden: boolean;
}
2023-08-21 04:15:53 -04:00
2024-02-17 10:56:27 +02:00
interface ContentOpts {
/** will also save this BNote entity */
forceSave?: boolean;
/** override frontend heuristics on when to reload, instruct to reload */
forceFrontendReload?: boolean;
}
interface AttachmentOpts {
includeContentLength?: boolean;
}
interface Relationship {
parentNoteId: string;
2025-01-09 18:07:02 +02:00
childNoteId: string;
2024-02-17 10:56:27 +02:00
}
interface ConvertOpts {
/** if true, the action is not triggered by user, but e.g. by migration, and only perfect candidates will be migrated */
autoConversion?: boolean;
}
2023-08-21 04:15:53 -04:00
/**
2023-06-30 11:18:34 +02:00
* Trilium's main entity, which can represent text note, image, code note, file attachment etc.
*/
2024-02-17 10:56:27 +02:00
class BNote extends AbstractBeccaEntity<BNote> {
2025-01-09 18:07:02 +02:00
static get entityName() {
return "notes";
}
static get primaryKeyName() {
return "noteId";
}
static get hashedProperties() {
return ["noteId", "title", "isProtected", "type", "mime", "blobId"];
}
2024-02-17 10:56:27 +02:00
noteId!: string;
title!: string;
type!: NoteType;
mime!: string;
/** set during the deletion operation, before it is completed (removed from becca completely). */
isBeingDeleted!: boolean;
isDecrypted!: boolean;
2024-09-04 08:41:17 +00:00
ownedAttributes!: BAttribute[];
parentBranches!: BBranch[];
parents!: BNote[];
children!: BNote[];
targetRelations!: BAttribute[];
2024-02-17 20:45:31 +02:00
__flatTextCache!: string | null;
2024-02-17 10:56:27 +02:00
private __attributeCache!: BAttribute[] | null;
private __inheritableAttributeCache!: BAttribute[] | null;
private __ancestorCache!: BNote[] | null;
// following attributes are filled during searching in the database
/** size of the content in bytes */
contentSize!: number | null;
2024-02-17 10:56:27 +02:00
/** size of the note content, attachment contents in bytes */
contentAndAttachmentsSize!: number | null;
2024-02-17 10:56:27 +02:00
/** size of the note content, attachment contents and revision contents in bytes */
contentAndAttachmentsAndRevisionsSize!: number | null;
2024-02-17 10:56:27 +02:00
/** number of note revisions for this note */
revisionCount!: number | null;
2024-02-17 10:56:27 +02:00
2024-02-17 20:45:31 +02:00
constructor(row?: Partial<NoteRow>) {
super();
if (!row) {
return;
}
2021-08-07 21:21:30 +02:00
this.updateFromRow(row);
this.init();
}
updateFromRow(row: Partial<NoteRow>) {
2025-01-09 18:07:02 +02:00
this.update([row.noteId, row.title, row.type, row.mime, row.isProtected, row.blobId, row.dateCreated, row.dateModified, row.utcDateCreated, row.utcDateModified]);
}
2024-02-17 10:56:27 +02:00
update([noteId, title, type, mime, isProtected, blobId, dateCreated, dateModified, utcDateCreated, utcDateModified]: any) {
// ------ Database persisted attributes ------
this.noteId = noteId;
this.title = title;
this.type = type;
this.mime = mime;
2023-03-15 22:44:08 +01:00
this.isProtected = !!isProtected;
this.blobId = blobId;
this.dateCreated = dateCreated || dateUtils.localNowDateTime();
this.dateModified = dateModified;
this.utcDateCreated = utcDateCreated || dateUtils.utcNowDateTime();
this.utcDateModified = utcDateModified;
this.isBeingDeleted = false;
// ------ Derived attributes ------
this.isDecrypted = !this.noteId || !this.isProtected;
this.decrypt();
2023-06-01 00:07:57 +02:00
this.__flatTextCache = null;
return this;
}
init() {
this.parentBranches = [];
this.parents = [];
this.children = [];
this.ownedAttributes = [];
2021-04-25 21:19:18 +02:00
this.__attributeCache = null;
this.__inheritableAttributeCache = null;
this.targetRelations = [];
this.becca.addNote(this.noteId, this);
2023-06-01 00:07:57 +02:00
this.__ancestorCache = null;
this.contentSize = null;
this.contentAndAttachmentsSize = null;
this.contentAndAttachmentsAndRevisionsSize = null;
this.revisionCount = null;
}
2021-05-17 22:35:36 +02:00
isContentAvailable() {
2025-01-09 18:07:02 +02:00
return (
!this.noteId || // new note which was not encrypted yet
!this.isProtected ||
protectedSessionService.isProtectedSessionAvailable()
);
2021-05-09 11:12:53 +02:00
}
getTitleOrProtected() {
2025-01-09 18:07:02 +02:00
return this.isContentAvailable() ? this.title : "[protected]";
}
2021-04-25 21:19:18 +02:00
getParentBranches() {
return this.parentBranches;
}
2022-12-04 13:16:05 +01:00
/**
2025-01-09 18:07:02 +02:00
* Returns <i>strong</i> (as opposed to <i>weak</i>) parent branches. See isWeak for details.
*/
2022-12-04 13:16:05 +01:00
getStrongParentBranches() {
2025-01-09 18:07:02 +02:00
return this.getParentBranches().filter((branch) => !branch.isWeak);
2022-12-04 13:16:05 +01:00
}
2021-12-20 17:30:47 +01:00
/**
2025-01-09 18:07:02 +02:00
* @deprecated use getParentBranches() instead
*/
2021-04-25 22:02:32 +02:00
getBranches() {
return this.parentBranches;
}
2021-04-25 21:19:18 +02:00
getParentNotes() {
return this.parents;
}
2021-04-25 22:02:32 +02:00
getChildNotes() {
2021-04-25 21:19:18 +02:00
return this.children;
}
hasChildren() {
return this.children && this.children.length > 0;
}
2024-04-05 20:33:04 +03:00
getChildBranches(): BBranch[] {
2025-01-09 18:07:02 +02:00
return this.children.map((childNote) => this.becca.getBranchFromChildAndParent(childNote.noteId, this.noteId)) as BBranch[];
2021-04-25 22:02:32 +02:00
}
2024-04-10 19:04:38 +03:00
/**
2025-01-09 18:07:02 +02:00
* Note content has quite special handling - it's not a separate entity, but a lazily loaded
* part of Note entity with its own sync. Reasons behind this hybrid design has been:
*
* - content can be quite large, and it's not necessary to load it / fill memory for any note access even if we don't need a content, especially for bulk operations like search
* - changes in the note metadata or title should not trigger note content sync (so we keep separate utcDateModified and entity changes records)
* - but to the user note content and title changes are one and the same - single dateModified (so all changes must go through Note and content is not a separate entity)
*/
getContent() {
return this._getContent();
}
/**
2025-01-09 18:07:02 +02:00
* @throws Error in case of invalid JSON
*/
2024-04-06 21:21:22 +03:00
getJsonContent(): any | null {
const content = this.getContent();
if (typeof content !== "string" || !content || !content.trim()) {
return null;
}
return JSON.parse(content);
}
2024-04-10 19:04:38 +03:00
/** @returns valid object or null if the content cannot be parsed as JSON */
getJsonContentSafely() {
try {
return this.getJsonContent();
2025-01-09 18:07:02 +02:00
} catch (e) {
return null;
}
}
setContent(content: Buffer | string, opts: ContentOpts = {}) {
2023-03-16 16:37:31 +01:00
this._setContent(content, opts);
2023-05-05 15:42:53 +02:00
eventService.emit(eventService.NOTE_CONTENT_CHANGE, { entity: this });
}
2024-02-17 10:56:27 +02:00
setJsonContent(content: {}) {
2025-01-09 18:07:02 +02:00
this.setContent(JSON.stringify(content, null, "\t"));
}
get dateCreatedObj() {
return this.dateCreated === null ? null : dayjs(this.dateCreated);
}
get utcDateCreatedObj() {
return this.utcDateCreated === null ? null : dayjs.utc(this.utcDateCreated);
}
2023-03-15 22:44:08 +01:00
get dateModifiedObj() {
return this.dateModified === null ? null : dayjs(this.dateModified);
}
get utcDateModifiedObj() {
return this.utcDateModified === null ? null : dayjs.utc(this.utcDateModified);
}
/** @returns true if this note is the root of the note tree. Root note has "root" noteId */
isRoot() {
2025-01-09 18:07:02 +02:00
return this.noteId === "root";
}
/** @returns true if this note is of application/json content type */
isJson() {
return this.mime === "application/json";
}
/** @returns true if this note is JavaScript (code or attachment) */
isJavaScript() {
2025-01-09 18:07:02 +02:00
return (
(this.type === "code" || this.type === "file" || this.type === "launcher") &&
(this.mime.startsWith("application/javascript") || this.mime === "application/x-javascript" || this.mime === "text/javascript")
);
}
/** @returns true if this note is HTML */
isHtml() {
2025-01-09 18:07:02 +02:00
return ["code", "file", "render"].includes(this.type) && this.mime === "text/html";
}
/** @returns true if this note is an image */
isImage() {
2025-01-09 18:07:02 +02:00
return this.type === "image" || (this.type === "file" && this.mime?.startsWith("image/"));
}
2023-05-05 16:37:39 +02:00
/** @deprecated use hasStringContent() instead */
isStringNote() {
2023-05-05 16:37:39 +02:00
return this.hasStringContent();
}
/** @returns true if the note has string content (not binary) */
2023-05-05 16:37:39 +02:00
hasStringContent() {
return utils.isStringNote(this.type, this.mime);
}
/** @returns JS script environment - either "frontend" or "backend" */
getScriptEnv() {
2025-01-09 18:07:02 +02:00
if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith("env=frontend"))) {
return "frontend";
}
2025-01-09 18:07:02 +02:00
if (this.type === "render") {
return "frontend";
}
2025-01-09 18:07:02 +02:00
if (this.isJavaScript() && this.mime.endsWith("env=backend")) {
return "backend";
}
return null;
}
2021-04-25 21:19:18 +02:00
/**
2025-01-09 18:07:02 +02:00
* Beware that the method must not create a copy of the array, but actually returns its internal array
* (for performance reasons)
*
* @param type - (optional) attribute type to filter
* @param name - (optional) attribute name to filter
* @returns all note's attributes, including inherited ones
*/
2024-02-17 10:56:27 +02:00
getAttributes(type?: string, name?: string): BAttribute[] {
this.__validateTypeName(type, name);
2023-01-27 08:46:04 +01:00
this.__ensureAttributeCacheIsAvailable();
2021-04-25 21:19:18 +02:00
2024-02-17 10:56:27 +02:00
if (!this.__attributeCache) {
throw new Error("Attribute cache not available.");
}
2021-04-25 21:19:18 +02:00
if (type && name) {
2025-01-09 18:07:02 +02:00
return this.__attributeCache.filter((attr) => attr.name === name && attr.type === type);
} else if (type) {
return this.__attributeCache.filter((attr) => attr.type === type);
} else if (name) {
return this.__attributeCache.filter((attr) => attr.name === name);
} else {
return this.__attributeCache;
2021-04-25 21:19:18 +02:00
}
}
2024-02-17 10:56:27 +02:00
private __ensureAttributeCacheIsAvailable() {
2023-01-27 08:46:04 +01:00
if (!this.__attributeCache) {
this.__getAttributes([]);
}
}
2024-02-17 10:56:27 +02:00
private __getAttributes(path: string[]) {
if (path.includes(this.noteId)) {
return [];
}
2021-04-25 21:19:18 +02:00
if (!this.__attributeCache) {
2020-05-16 23:12:29 +02:00
const parentAttributes = this.ownedAttributes.slice();
const newPath = [...path, this.noteId];
2020-05-16 23:12:29 +02:00
// inheritable attrs on root are typically not intended to be applied to hidden subtree #3537
2025-01-09 18:07:02 +02:00
if (this.noteId !== "root" && this.noteId !== "_hidden") {
2020-05-16 23:12:29 +02:00
for (const parentNote of this.parents) {
parentAttributes.push(...parentNote.__getInheritableAttributes(newPath));
2020-05-16 23:12:29 +02:00
}
}
const templateAttributes = [];
2025-01-09 18:07:02 +02:00
for (const ownedAttr of parentAttributes) {
// parentAttributes so we process also inherited templates
if (ownedAttr.type === "relation" && ["template", "inherit"].includes(ownedAttr.name)) {
2021-04-16 23:00:08 +02:00
const templateNote = this.becca.notes[ownedAttr.value];
2020-05-16 23:12:29 +02:00
if (templateNote) {
templateAttributes.push(
2025-01-09 18:07:02 +02:00
...templateNote
.__getAttributes(newPath)
// template attr is used as a marker for templates, but it's not meant to be inherited
2025-01-09 18:07:02 +02:00
.filter((attr) => !(attr.type === "label" && (attr.name === "template" || attr.name === "workspacetemplate")))
);
2020-05-16 23:12:29 +02:00
}
}
}
2021-04-25 21:19:18 +02:00
this.__attributeCache = [];
const addedAttributeIds = new Set();
for (const attr of parentAttributes.concat(templateAttributes)) {
if (!addedAttributeIds.has(attr.attributeId)) {
addedAttributeIds.add(attr.attributeId);
2021-04-25 21:19:18 +02:00
this.__attributeCache.push(attr);
}
}
this.__inheritableAttributeCache = [];
2020-05-16 23:12:29 +02:00
2021-04-25 21:19:18 +02:00
for (const attr of this.__attributeCache) {
2020-05-16 23:12:29 +02:00
if (attr.isInheritable) {
this.__inheritableAttributeCache.push(attr);
2020-05-16 23:12:29 +02:00
}
}
}
2021-04-25 21:19:18 +02:00
return this.__attributeCache;
2020-05-16 23:12:29 +02:00
}
2024-02-17 10:56:27 +02:00
private __getInheritableAttributes(path: string[]): BAttribute[] {
if (path.includes(this.noteId)) {
return [];
}
if (!this.__inheritableAttributeCache) {
this.__getAttributes(path); // will refresh also this.__inheritableAttributeCache
2020-05-16 23:12:29 +02:00
}
2024-02-17 10:56:27 +02:00
return this.__inheritableAttributeCache || [];
2020-05-16 23:12:29 +02:00
}
2024-02-17 10:56:27 +02:00
__validateTypeName(type?: string | null, name?: string | null) {
2025-01-09 18:07:02 +02:00
if (type && type !== "label" && type !== "relation") {
throw new Error(`Unrecognized attribute type '${type}'. Only 'label' and 'relation' are possible values.`);
}
if (name) {
const firstLetter = name.charAt(0);
2025-01-09 18:07:02 +02:00
if (firstLetter === "#" || firstLetter === "~") {
throw new Error(`Detect '#' or '~' in the attribute's name. In the API, attribute names should be set without these characters.`);
}
}
}
2024-02-17 10:56:27 +02:00
hasAttribute(type: string, name: string, value: string | null = null): boolean {
2025-01-09 18:07:02 +02:00
return !!this.getAttributes().find((attr) => attr.name === name && (value === undefined || value === null || attr.value === value) && attr.type === type);
2020-05-16 23:12:29 +02:00
}
getAttributeCaseInsensitive(type: string, name: string, value?: string | null) {
2020-12-15 15:09:00 +01:00
name = name.toLowerCase();
value = value ? value.toLowerCase() : null;
2025-01-09 18:07:02 +02:00
return this.getAttributes().find((attr) => attr.name.toLowerCase() === name && (!value || attr.value.toLowerCase() === value) && attr.type === type);
2020-12-15 15:09:00 +01:00
}
2024-02-17 10:56:27 +02:00
getRelationTarget(name: string) {
2025-01-09 18:07:02 +02:00
const relation = this.getAttributes().find((attr) => attr.name === name && attr.type === "relation");
2021-04-17 20:52:46 +02:00
return relation ? relation.targetNote : null;
}
2021-04-17 20:52:46 +02:00
/**
2025-01-09 18:07:02 +02:00
* @param name - label name
* @param value - label value
* @returns true if label exists (including inherited)
*/
2024-02-17 10:56:27 +02:00
hasLabel(name: string, value?: string): boolean {
return this.hasAttribute(LABEL, name, value);
}
2021-04-17 20:52:46 +02:00
/**
2025-01-09 18:07:02 +02:00
* @param name - label name
* @returns true if label exists (including inherited) and does not have "false" value.
*/
2024-02-17 10:56:27 +02:00
isLabelTruthy(name: string): boolean {
const label = this.getLabel(name);
if (!label) {
return false;
}
2025-01-09 18:07:02 +02:00
return label && label.value !== "false";
}
2021-04-17 20:52:46 +02:00
/**
2025-01-09 18:07:02 +02:00
* @param name - label name
* @param value - label value
* @returns true if label exists (excluding inherited)
*/
2024-02-17 10:56:27 +02:00
hasOwnedLabel(name: string, value?: string): boolean {
return this.hasOwnedAttribute(LABEL, name, value);
}
2021-04-17 20:52:46 +02:00
/**
2025-01-09 18:07:02 +02:00
* @param name - relation name
* @param value - relation value
* @returns true if relation exists (including inherited)
*/
2024-02-17 10:56:27 +02:00
hasRelation(name: string, value?: string): boolean {
return this.hasAttribute(RELATION, name, value);
}
2021-04-17 20:52:46 +02:00
/**
2025-01-09 18:07:02 +02:00
* @param name - relation name
* @param value - relation value
* @returns true if relation exists (excluding inherited)
*/
2024-02-17 10:56:27 +02:00
hasOwnedRelation(name: string, value?: string): boolean {
return this.hasOwnedAttribute(RELATION, name, value);
}
2021-04-17 20:52:46 +02:00
/**
2025-01-09 18:07:02 +02:00
* @param name - label name
* @returns label if it exists, null otherwise
*/
2024-02-17 10:56:27 +02:00
getLabel(name: string): BAttribute | null {
return this.getAttribute(LABEL, name);
}
2021-04-17 20:52:46 +02:00
/**
2025-01-09 18:07:02 +02:00
* @param name - label name
* @returns label if it exists, null otherwise
*/
2024-02-17 10:56:27 +02:00
getOwnedLabel(name: string): BAttribute | null {
return this.getOwnedAttribute(LABEL, name);
}
2021-04-17 20:52:46 +02:00
/**
2025-01-09 18:07:02 +02:00
* @param name - relation name
* @returns relation if it exists, null otherwise
*/
2024-02-17 10:56:27 +02:00
getRelation(name: string): BAttribute | null {
return this.getAttribute(RELATION, name);
}
2021-04-17 20:52:46 +02:00
/**
2025-01-09 18:07:02 +02:00
* @param name - relation name
* @returns relation if it exists, null otherwise
*/
2024-02-17 10:56:27 +02:00
getOwnedRelation(name: string): BAttribute | null {
return this.getOwnedAttribute(RELATION, name);
}
2021-04-17 20:52:46 +02:00
/**
2025-01-09 18:07:02 +02:00
* @param name - label name
* @returns label value if label exists, null otherwise
*/
2024-02-17 10:56:27 +02:00
getLabelValue(name: string): string | null {
return this.getAttributeValue(LABEL, name);
}
2021-04-17 20:52:46 +02:00
/**
2025-01-09 18:07:02 +02:00
* @param name - label name
* @returns label value if label exists, null otherwise
*/
2024-02-17 10:56:27 +02:00
getOwnedLabelValue(name: string): string | null {
return this.getOwnedAttributeValue(LABEL, name);
}
2021-04-17 20:52:46 +02:00
/**
2025-01-09 18:07:02 +02:00
* @param name - relation name
* @returns relation value if relation exists, null otherwise
*/
2024-02-17 10:56:27 +02:00
getRelationValue(name: string): string | null {
return this.getAttributeValue(RELATION, name);
}
2021-04-17 20:52:46 +02:00
/**
2025-01-09 18:07:02 +02:00
* @param name - relation name
* @returns relation value if relation exists, null otherwise
*/
2024-02-17 10:56:27 +02:00
getOwnedRelationValue(name: string): string | null {
return this.getOwnedAttributeValue(RELATION, name);
}
2021-04-17 20:52:46 +02:00
/**
2025-01-09 18:07:02 +02:00
* @param attribute type (label, relation, etc.)
* @param name - attribute name
* @param value - attribute value
* @returns true if note has an attribute with given type and name (excluding inherited)
*/
2024-02-17 10:56:27 +02:00
hasOwnedAttribute(type: string, name: string, value?: string): boolean {
return !!this.getOwnedAttribute(type, name, value);
}
2021-04-17 20:52:46 +02:00
/**
2025-01-09 18:07:02 +02:00
* @param type - attribute type (label, relation, etc.)
* @param name - attribute name
* @returns attribute of the given type and name. If there are more such attributes, first is returned.
* Returns null if there's no such attribute belonging to this note.
*/
2024-02-17 10:56:27 +02:00
getAttribute(type: string, name: string): BAttribute | null {
2021-04-17 20:52:46 +02:00
const attributes = this.getAttributes();
2020-05-25 00:25:47 +02:00
2025-01-09 18:07:02 +02:00
return attributes.find((attr) => attr.name === name && attr.type === type) || null;
2020-05-25 00:25:47 +02:00
}
2021-04-17 20:52:46 +02:00
/**
2025-01-09 18:07:02 +02:00
* @param type - attribute type (label, relation, etc.)
* @param name - attribute name
* @returns attribute value of given type and name or null if no such attribute exists.
*/
2024-02-17 10:56:27 +02:00
getAttributeValue(type: string, name: string): string | null {
2021-04-17 20:52:46 +02:00
const attr = this.getAttribute(type, name);
2020-05-25 00:25:47 +02:00
2021-04-17 20:52:46 +02:00
return attr ? attr.value : null;
}
/**
2025-01-09 18:07:02 +02:00
* @param type - attribute type (label, relation, etc.)
* @param name - attribute name
* @returns attribute value of given type and name or null if no such attribute exists.
*/
2024-02-17 10:56:27 +02:00
getOwnedAttributeValue(type: string, name: string): string | null {
2021-04-17 20:52:46 +02:00
const attr = this.getOwnedAttribute(type, name);
return attr ? attr.value : null;
2020-05-25 00:25:47 +02:00
}
2021-04-25 21:19:18 +02:00
/**
2025-01-09 18:07:02 +02:00
* @param name - label name to filter
* @returns all note's labels (attributes with type label), including inherited ones
*/
2024-02-17 10:56:27 +02:00
getLabels(name?: string): BAttribute[] {
2021-04-25 21:19:18 +02:00
return this.getAttributes(LABEL, name);
}
/**
2025-01-09 18:07:02 +02:00
* @param name - label name to filter
* @returns all note's label values, including inherited ones
*/
2024-02-17 10:56:27 +02:00
getLabelValues(name: string): string[] {
2025-01-09 18:07:02 +02:00
return this.getLabels(name).map((l) => l.value);
2021-04-25 21:19:18 +02:00
}
/**
2025-01-09 18:07:02 +02:00
* @param name - label name to filter
* @returns all note's labels (attributes with type label), excluding inherited ones
*/
2024-02-17 10:56:27 +02:00
getOwnedLabels(name: string): BAttribute[] {
2021-04-25 21:19:18 +02:00
return this.getOwnedAttributes(LABEL, name);
}
/**
2025-01-09 18:07:02 +02:00
* @param name - label name to filter
* @returns all note's label values, excluding inherited ones
*/
2024-02-17 10:56:27 +02:00
getOwnedLabelValues(name: string): string[] {
2025-01-09 18:07:02 +02:00
return this.getOwnedAttributes(LABEL, name).map((l) => l.value);
2021-04-25 21:19:18 +02:00
}
/**
2025-01-09 18:07:02 +02:00
* @param name - relation name to filter
* @returns all note's relations (attributes with type relation), including inherited ones
*/
getRelations(name?: string): BAttribute[] {
2021-04-25 21:19:18 +02:00
return this.getAttributes(RELATION, name);
}
/**
2025-01-09 18:07:02 +02:00
* @param name - relation name to filter
* @returns all note's relations (attributes with type relation), excluding inherited ones
*/
2024-04-04 23:04:54 +03:00
getOwnedRelations(name?: string | null): BAttribute[] {
2021-04-25 21:19:18 +02:00
return this.getOwnedAttributes(RELATION, name);
}
2021-04-25 22:02:32 +02:00
/**
2025-01-09 18:07:02 +02:00
* Beware that the method must not create a copy of the array, but actually returns its internal array
* (for performance reasons)
*
* @param type - (optional) attribute type to filter
* @param name - (optional) attribute name to filter
* @param value - (optional) attribute value to filter
* @returns note's "owned" attributes - excluding inherited ones
*/
2024-02-17 10:56:27 +02:00
getOwnedAttributes(type: string | null = null, name: string | null = null, value: string | null = null) {
this.__validateTypeName(type, name);
2021-09-02 22:14:22 +02:00
if (type && name && value !== undefined && value !== null) {
2025-01-09 18:07:02 +02:00
return this.ownedAttributes.filter((attr) => attr.name === name && attr.value === value && attr.type === type);
} else if (type && name) {
return this.ownedAttributes.filter((attr) => attr.name === name && attr.type === type);
} else if (type) {
return this.ownedAttributes.filter((attr) => attr.type === type);
} else if (name) {
return this.ownedAttributes.filter((attr) => attr.name === name);
} else {
return this.ownedAttributes;
2021-04-25 22:02:32 +02:00
}
}
2021-05-08 23:31:20 +02:00
/**
2025-01-09 18:07:02 +02:00
* @returns attribute belonging to this specific note (excludes inherited attributes)
*
* This method can be significantly faster than the getAttribute()
*/
2024-02-17 10:56:27 +02:00
getOwnedAttribute(type: string, name: string, value: string | null = null) {
const attrs = this.getOwnedAttributes(type, name, value);
2021-05-08 23:31:20 +02:00
return attrs.length > 0 ? attrs[0] : null;
}
2021-05-18 20:56:49 +02:00
get isArchived() {
2025-01-09 18:07:02 +02:00
return this.hasAttribute("label", "archived");
2020-05-16 23:12:29 +02:00
}
2023-04-16 09:22:24 +02:00
areAllNotePathsArchived() {
// there's a slight difference between note being itself archived and all its note paths being archived
// - note is archived when it itself has an archived label or inherits it
2023-06-30 11:18:34 +02:00
// - note does not have or inherit archived label, but each note path contains a note with (non-inheritable)
2023-04-16 09:22:24 +02:00
// archived label
const bestNotePathRecord = this.getSortedNotePathRecords()[0];
if (!bestNotePathRecord) {
throw new Error(`No note path available for note '${this.noteId}'`);
}
return bestNotePathRecord.isArchived;
}
2023-01-27 08:46:04 +01:00
hasInheritableArchivedLabel() {
for (const attr of this.getAttributes()) {
2025-01-09 18:07:02 +02:00
if (attr.name === "archived" && attr.type === LABEL && attr.isInheritable) {
2023-01-27 08:46:04 +01:00
return true;
}
}
return false;
2020-05-16 23:12:29 +02:00
}
2023-01-27 08:46:04 +01:00
// will sort the parents so that the non-archived are first and archived at the end
// this is done so that the non-archived paths are always explored as first when looking for note path
sortParents() {
2023-01-27 16:57:23 +01:00
this.parentBranches.sort((a, b) => {
if (a.parentNote?.isArchived) {
return 1;
} else if (a.parentNote?.isHiddenCompletely()) {
return 1;
} else {
2023-11-03 01:11:47 +01:00
return 0;
2023-01-27 16:57:23 +01:00
}
});
2025-01-09 18:07:02 +02:00
this.parents = this.parentBranches.map((branch) => branch.parentNote).filter((note) => !!note) as BNote[];
2020-05-16 23:12:29 +02:00
}
sortChildren() {
if (this.children.length === 0) {
return;
}
const becca = this.becca;
this.children.sort((a, b) => {
const aBranch = becca.getBranchFromChildAndParent(a.noteId, this.noteId);
const bBranch = becca.getBranchFromChildAndParent(b.noteId, this.noteId);
2025-01-09 18:07:02 +02:00
return (aBranch?.notePosition || 0) - (bBranch?.notePosition || 0) || 0;
});
}
2020-05-16 23:12:29 +02:00
/**
2025-01-09 18:07:02 +02:00
* This is used for:
* - fast searching
* - note similarity evaluation
*
* @returns - returns flattened textual representation of note, prefixes and attributes
*/
2021-05-17 22:35:36 +02:00
getFlatText() {
2023-06-01 00:07:57 +02:00
if (!this.__flatTextCache) {
this.__flatTextCache = `${this.noteId} ${this.type} ${this.mime} `;
2020-05-16 23:12:29 +02:00
for (const branch of this.parentBranches) {
if (branch.prefix) {
2023-06-01 00:07:57 +02:00
this.__flatTextCache += `${branch.prefix} `;
2020-05-16 23:12:29 +02:00
}
}
2023-06-01 00:07:57 +02:00
this.__flatTextCache += `${this.title} `;
2020-05-16 23:12:29 +02:00
2021-04-25 21:19:18 +02:00
for (const attr of this.getAttributes()) {
2020-05-16 23:12:29 +02:00
// it's best to use space as separator since spaces are filtered from the search string by the tokenization into words
2025-01-09 18:07:02 +02:00
this.__flatTextCache += `${attr.type === "label" ? "#" : "~"}${attr.name}`;
2020-05-16 23:12:29 +02:00
if (attr.value) {
2023-06-01 00:07:57 +02:00
this.__flatTextCache += `=${attr.value}`;
2020-05-16 23:12:29 +02:00
}
2025-01-09 18:07:02 +02:00
this.__flatTextCache += " ";
2020-05-16 23:12:29 +02:00
}
2023-06-01 00:07:57 +02:00
this.__flatTextCache = utils.normalize(this.__flatTextCache);
2020-05-16 23:12:29 +02:00
}
2023-06-01 00:07:57 +02:00
return this.__flatTextCache;
2020-05-16 23:12:29 +02:00
}
invalidateThisCache() {
2023-06-01 00:07:57 +02:00
this.__flatTextCache = null;
2020-05-16 23:12:29 +02:00
2021-04-25 21:19:18 +02:00
this.__attributeCache = null;
this.__inheritableAttributeCache = null;
2023-06-01 00:07:57 +02:00
this.__ancestorCache = null;
2020-05-16 23:12:29 +02:00
}
2024-02-17 10:56:27 +02:00
invalidateSubTree(path: string[] = []) {
2021-03-30 21:39:42 +02:00
if (path.includes(this.noteId)) {
return;
}
2020-05-16 23:12:29 +02:00
this.invalidateThisCache();
2021-03-30 21:39:42 +02:00
if (this.children.length || this.targetRelations.length) {
path = [...path, this.noteId];
}
2020-05-16 23:12:29 +02:00
for (const childNote of this.children) {
2021-04-17 20:52:46 +02:00
childNote.invalidateSubTree(path);
2020-05-16 23:12:29 +02:00
}
for (const targetRelation of this.targetRelations) {
2025-01-09 18:07:02 +02:00
if (targetRelation.name === "template" || targetRelation.name === "inherit") {
2020-05-16 23:12:29 +02:00
const note = targetRelation.note;
if (note) {
2021-04-17 20:52:46 +02:00
note.invalidateSubTree(path);
2020-05-16 23:12:29 +02:00
}
}
}
}
2021-06-03 12:32:48 +02:00
getRelationDefinitions() {
2025-01-09 18:07:02 +02:00
return this.getLabels().filter((l) => l.name.startsWith("relation:"));
2021-06-03 12:32:48 +02:00
}
getLabelDefinitions() {
2025-01-09 18:07:02 +02:00
return this.getLabels().filter((l) => l.name.startsWith("relation:"));
2021-06-03 12:32:48 +02:00
}
2023-01-06 20:31:55 +01:00
isInherited() {
2025-01-09 18:07:02 +02:00
return !!this.targetRelations.find((rel) => rel.name === "template" || rel.name === "inherit");
2020-05-16 23:12:29 +02:00
}
2024-02-17 10:56:27 +02:00
getSubtreeNotesIncludingTemplated(): BNote[] {
const set = new Set<BNote>();
2020-05-16 23:12:29 +02:00
2024-02-17 10:56:27 +02:00
function inner(note: BNote) {
// _hidden is not counted as subtree for the purpose of inheritance
2025-01-09 18:07:02 +02:00
if (set.has(note) || note.noteId === "_hidden") {
2021-10-29 21:36:23 +02:00
return;
}
2020-05-16 23:12:29 +02:00
2021-10-29 21:36:23 +02:00
set.add(note);
2020-05-16 23:12:29 +02:00
2021-10-29 21:36:23 +02:00
for (const childNote of note.children) {
inner(childNote);
}
for (const targetRelation of note.targetRelations) {
2025-01-09 18:07:02 +02:00
if (targetRelation.name === "template" || targetRelation.name === "inherit") {
2021-10-29 21:36:23 +02:00
const targetNote = targetRelation.note;
if (targetNote) {
inner(targetNote);
}
2020-05-16 23:12:29 +02:00
}
}
}
2021-10-29 21:36:23 +02:00
inner(this);
return Array.from(set);
2020-05-16 23:12:29 +02:00
}
2024-02-17 10:56:27 +02:00
getSearchResultNotes(): BNote[] {
2025-01-09 18:07:02 +02:00
if (this.type !== "search") {
return [];
}
try {
2024-07-18 22:58:12 +03:00
const result = searchService.searchFromNote(this);
const becca = this.becca;
2025-01-09 18:07:02 +02:00
return result.searchResultNoteIds.map((resultNoteId) => becca.notes[resultNoteId]).filter((note) => !!note);
} catch (e: any) {
log.error(`Could not resolve search note ${this.noteId}: ${e.message}`);
return [];
}
}
2025-01-09 18:07:02 +02:00
getSubtree({ includeArchived = true, includeHidden = false, resolveSearch = false } = {}): {
notes: BNote[];
relationships: Relationship[];
2024-02-17 10:56:27 +02:00
} {
const noteSet = new Set<BNote>();
const relationships: Relationship[] = []; // list of tuples parentNoteId -> childNoteId
2024-02-17 10:56:27 +02:00
function resolveSearchNote(searchNote: BNote) {
try {
for (const resultNote of searchNote.getSearchResultNotes()) {
addSubtreeNotesInner(resultNote, searchNote);
}
2025-01-09 18:07:02 +02:00
} catch (e: any) {
log.error(`Could not resolve search note ${searchNote?.noteId}: ${e.message}`);
}
}
2024-02-17 10:56:27 +02:00
function addSubtreeNotesInner(note: BNote, parentNote: BNote | null = null) {
2025-01-09 18:07:02 +02:00
if (note.noteId === "_hidden" && !includeHidden) {
2022-11-06 14:38:41 +01:00
return;
}
if (parentNote) {
// this needs to happen first before noteSet check to include all clone relationships
relationships.push({
parentNoteId: parentNote.noteId,
childNoteId: note.noteId
});
}
if (noteSet.has(note)) {
return;
}
2021-09-20 23:04:41 +02:00
2021-09-21 22:45:06 +02:00
if (!includeArchived && note.isArchived) {
return;
}
2020-05-16 23:12:29 +02:00
2021-09-21 22:45:06 +02:00
noteSet.add(note);
2025-01-09 18:07:02 +02:00
if (note.type === "search") {
if (resolveSearch) {
resolveSearchNote(note);
}
2025-01-09 18:07:02 +02:00
} else {
for (const childNote of note.children) {
addSubtreeNotesInner(childNote, note);
}
2021-09-21 22:45:06 +02:00
}
2020-05-16 23:12:29 +02:00
}
2021-09-21 22:45:06 +02:00
addSubtreeNotesInner(this);
return {
notes: Array.from(noteSet),
relationships
};
2020-05-16 23:12:29 +02:00
}
/** @returns includes the subtree root note as well */
2025-01-09 18:07:02 +02:00
getSubtreeNoteIds({ includeArchived = true, includeHidden = false, resolveSearch = false } = {}) {
return this.getSubtree({ includeArchived, includeHidden, resolveSearch }).notes.map((note) => note.noteId);
}
/** @deprecated use getSubtreeNoteIds() instead */
2021-04-25 22:02:32 +02:00
getDescendantNoteIds() {
2021-05-17 22:35:36 +02:00
return this.getSubtreeNoteIds();
2021-04-25 22:02:32 +02:00
}
2020-05-23 20:52:55 +02:00
get parentCount() {
return this.parents.length;
}
get childrenCount() {
return this.children.length;
}
get labelCount() {
2025-01-09 18:07:02 +02:00
return this.getAttributes().filter((attr) => attr.type === "label").length;
2020-05-23 20:52:55 +02:00
}
get ownedLabelCount() {
2025-01-09 18:07:02 +02:00
return this.ownedAttributes.filter((attr) => attr.type === "label").length;
}
2020-05-23 20:52:55 +02:00
get relationCount() {
2025-01-09 18:07:02 +02:00
return this.getAttributes().filter((attr) => attr.type === "relation" && !attr.isAutoLink()).length;
}
get relationCountIncludingLinks() {
2025-01-09 18:07:02 +02:00
return this.getAttributes().filter((attr) => attr.type === "relation").length;
2020-05-23 20:52:55 +02:00
}
get ownedRelationCount() {
2025-01-09 18:07:02 +02:00
return this.ownedAttributes.filter((attr) => attr.type === "relation" && !attr.isAutoLink()).length;
}
get ownedRelationCountIncludingLinks() {
2025-01-09 18:07:02 +02:00
return this.ownedAttributes.filter((attr) => attr.type === "relation").length;
}
get targetRelationCount() {
2025-01-09 18:07:02 +02:00
return this.targetRelations.filter((attr) => !attr.isAutoLink()).length;
}
get targetRelationCountIncludingLinks() {
return this.targetRelations.length;
}
2020-05-23 20:52:55 +02:00
get attributeCount() {
2021-04-25 21:19:18 +02:00
return this.getAttributes().length;
2020-05-23 20:52:55 +02:00
}
get ownedAttributeCount() {
2023-01-27 08:46:04 +01:00
return this.getOwnedAttributes().length;
}
2021-05-17 22:35:36 +02:00
getAncestors() {
2023-06-01 00:07:57 +02:00
if (!this.__ancestorCache) {
2020-05-23 23:44:55 +02:00
const noteIds = new Set();
2023-06-01 00:07:57 +02:00
this.__ancestorCache = [];
2020-05-23 23:44:55 +02:00
for (const parent of this.parents) {
if (noteIds.has(parent.noteId)) {
continue;
2020-05-23 23:44:55 +02:00
}
2023-06-01 00:07:57 +02:00
this.__ancestorCache.push(parent);
noteIds.add(parent.noteId);
2021-05-17 22:35:36 +02:00
for (const ancestorNote of parent.getAncestors()) {
2020-05-23 23:44:55 +02:00
if (!noteIds.has(ancestorNote.noteId)) {
2023-06-01 00:07:57 +02:00
this.__ancestorCache.push(ancestorNote);
2020-05-23 23:44:55 +02:00
noteIds.add(ancestorNote.noteId);
}
}
}
}
2023-06-01 00:07:57 +02:00
return this.__ancestorCache;
2020-05-23 23:44:55 +02:00
}
2024-02-17 10:56:27 +02:00
getAncestorNoteIds(): string[] {
2025-01-09 18:07:02 +02:00
return this.getAncestors().map((note) => note.noteId);
2023-04-14 16:49:06 +02:00
}
2024-02-17 10:56:27 +02:00
hasAncestor(ancestorNoteId: string): boolean {
for (const ancestorNote of this.getAncestors()) {
if (ancestorNote.noteId === ancestorNoteId) {
return true;
}
}
return false;
}
isInHiddenSubtree() {
2025-01-09 18:07:02 +02:00
return this.noteId === "_hidden" || this.hasAncestor("_hidden");
}
2021-05-08 23:31:20 +02:00
getTargetRelations() {
return this.targetRelations;
}
2024-02-17 10:56:27 +02:00
/** @returns returns only notes which are templated, does not include their subtrees
2025-01-09 18:07:02 +02:00
* in effect returns notes which are influenced by note's non-inheritable attributes */
2024-02-17 10:56:27 +02:00
getInheritingNotes(): BNote[] {
const arr: BNote[] = [this];
2020-05-16 23:12:29 +02:00
for (const targetRelation of this.targetRelations) {
2025-01-09 18:07:02 +02:00
if (targetRelation.name === "template" || targetRelation.name === "inherit") {
2020-05-16 23:12:29 +02:00
const note = targetRelation.note;
if (note) {
arr.push(note);
}
}
}
return arr;
}
2020-05-17 09:48:24 +02:00
2024-02-17 10:56:27 +02:00
getDistanceToAncestor(ancestorNoteId: string) {
2021-01-26 23:25:18 +01:00
if (this.noteId === ancestorNoteId) {
return 0;
}
let minDistance = 999999;
2021-01-26 23:25:18 +01:00
for (const parent of this.parents) {
minDistance = Math.min(minDistance, parent.getDistanceToAncestor(ancestorNoteId) + 1);
}
return minDistance;
}
2024-02-17 10:56:27 +02:00
getRevisions(): BRevision[] {
2025-01-09 18:07:02 +02:00
return sql.getRows<RevisionRow>("SELECT * FROM revisions WHERE noteId = ? ORDER BY revisions.utcDateCreated ASC", [this.noteId]).map((row) => new BRevision(row));
}
2024-02-17 10:56:27 +02:00
getAttachments(opts: AttachmentOpts = {}) {
2023-05-21 18:14:17 +02:00
opts.includeContentLength = !!opts.includeContentLength;
2023-06-30 11:18:34 +02:00
// from testing, it looks like calculating length does not make a difference in performance even on large-ish DB
2023-05-29 00:19:54 +02:00
// given that we're always fetching attachments only for a specific note, we might just do it always
2023-05-21 18:14:17 +02:00
const query = opts.includeContentLength
? /*sql*/`SELECT attachments.*, LENGTH(blobs.content) AS contentLength
2024-12-22 15:45:54 +02:00
FROM attachments
JOIN blobs USING (blobId)
WHERE ownerId = ? AND isDeleted = 0
ORDER BY position`
: /*sql*/`SELECT * FROM attachments WHERE ownerId = ? AND isDeleted = 0 ORDER BY position`;
2023-05-21 18:14:17 +02:00
2025-01-09 18:07:02 +02:00
return sql.getRows<AttachmentRow>(query, [this.noteId]).map((row) => new BAttachment(row));
}
2024-02-17 10:56:27 +02:00
getAttachmentById(attachmentId: string, opts: AttachmentOpts = {}) {
2023-05-21 18:14:17 +02:00
opts.includeContentLength = !!opts.includeContentLength;
const query = opts.includeContentLength
? /*sql*/`SELECT attachments.*, LENGTH(blobs.content) AS contentLength
2024-12-22 15:45:54 +02:00
FROM attachments
JOIN blobs USING (blobId)
WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`
: /*sql*/`SELECT * FROM attachments WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`;
2023-05-21 18:14:17 +02:00
2025-01-09 18:07:02 +02:00
return sql.getRows<AttachmentRow>(query, [this.noteId, attachmentId]).map((row) => new BAttachment(row))[0];
}
2024-02-17 10:56:27 +02:00
getAttachmentsByRole(role: string): BAttachment[] {
2025-01-09 18:07:02 +02:00
return sql
.getRows<AttachmentRow>(
`
2023-04-20 00:11:09 +02:00
SELECT attachments.*
2024-12-22 15:45:54 +02:00
FROM attachments
WHERE ownerId = ?
AND role = ?
AND isDeleted = 0
2025-01-09 18:07:02 +02:00
ORDER BY position`,
[this.noteId, role]
)
.map((row) => new BAttachment(row));
2023-04-20 00:11:09 +02:00
}
getAttachmentByTitle(title: string): BAttachment | undefined {
// cannot use SQL to filter by title since it can be encrypted
2025-01-09 18:07:02 +02:00
return this.getAttachments().filter((attachment) => attachment.title === title)[0];
}
2021-05-09 20:46:32 +02:00
/**
2025-01-09 18:07:02 +02:00
* Gives all possible note paths leading to this note. Paths containing search note are ignored (could form cycles)
*
* @returns array of notePaths (each represented by array of noteIds constituting the particular note path)
*/
2024-02-17 10:56:27 +02:00
getAllNotePaths(): string[][] {
2025-01-09 18:07:02 +02:00
if (this.noteId === "root") {
return [["root"]];
2021-05-09 20:46:32 +02:00
}
2023-04-15 00:06:13 +02:00
const parentNotes = this.getParentNotes();
2021-05-09 20:46:32 +02:00
2025-01-09 18:07:02 +02:00
const notePaths =
parentNotes.length === 1
? parentNotes[0].getAllNotePaths() // optimization for the most common case
: parentNotes.flatMap((parentNote) => parentNote.getAllNotePaths());
2021-05-09 20:46:32 +02:00
2023-04-15 00:06:13 +02:00
for (const notePath of notePaths) {
notePath.push(this.noteId);
2021-05-09 20:46:32 +02:00
}
return notePaths;
}
2025-01-09 18:07:02 +02:00
getSortedNotePathRecords(hoistedNoteId: string = "root"): NotePathRecord[] {
const isHoistedRoot = hoistedNoteId === "root";
2023-04-15 00:06:13 +02:00
2025-01-09 18:07:02 +02:00
const notePaths = this.getAllNotePaths().map((path) => ({
2023-04-15 00:06:13 +02:00
notePath: path,
isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId),
2025-01-09 18:07:02 +02:00
isArchived: path.some((noteId) => this.becca.notes[noteId].isArchived),
isHidden: path.includes("_hidden")
2023-04-15 00:06:13 +02:00
}));
notePaths.sort((a, b) => {
if (a.isInHoistedSubTree !== b.isInHoistedSubTree) {
return a.isInHoistedSubTree ? -1 : 1;
} else if (a.isArchived !== b.isArchived) {
return a.isArchived ? 1 : -1;
} else if (a.isHidden !== b.isHidden) {
return a.isHidden ? 1 : -1;
} else {
return a.notePath.length - b.notePath.length;
}
});
2021-05-09 20:46:32 +02:00
return notePaths;
}
2023-04-15 00:06:13 +02:00
/**
2025-01-09 18:07:02 +02:00
* Returns a note path considered to be the "best"
*
* @return array of noteIds constituting the particular note path
*/
getBestNotePath(hoistedNoteId: string = "root"): string[] {
2023-04-15 00:06:13 +02:00
return this.getSortedNotePathRecords(hoistedNoteId)[0]?.notePath;
}
/**
2025-01-09 18:07:02 +02:00
* Returns a note path considered to be the "best"
*
* @return serialized note path (e.g. 'root/a1h315/js725h')
*/
getBestNotePathString(hoistedNoteId: string = "root"): string {
2023-04-15 00:06:13 +02:00
const notePath = this.getBestNotePath(hoistedNoteId);
return notePath?.join("/");
}
/**
2025-01-09 18:07:02 +02:00
* @return boolean - true if there's no non-hidden path, note is not cloned to the visible tree
*/
isHiddenCompletely() {
2025-01-09 18:07:02 +02:00
if (this.noteId === "root") {
2023-01-27 16:57:23 +01:00
return false;
}
for (const parentNote of this.parents) {
2025-01-09 18:07:02 +02:00
if (parentNote.noteId === "root") {
2023-01-27 16:57:23 +01:00
return false;
2025-01-09 18:07:02 +02:00
} else if (parentNote.noteId === "_hidden") {
2023-01-27 16:57:23 +01:00
continue;
2023-02-28 23:23:17 +01:00
} else if (!parentNote.isHiddenCompletely()) {
2023-01-27 16:57:23 +01:00
return false;
}
}
return true;
}
2021-05-09 20:46:32 +02:00
/**
2025-01-09 18:07:02 +02:00
* @returns true if ancestorNoteId occurs in at least one of the note's paths
*/
2024-02-17 10:56:27 +02:00
isDescendantOfNote(ancestorNoteId: string): boolean {
2021-05-09 20:46:32 +02:00
const notePaths = this.getAllNotePaths();
2025-01-09 18:07:02 +02:00
return notePaths.some((path) => path.includes(ancestorNoteId));
2021-05-09 20:46:32 +02:00
}
2021-05-11 22:00:16 +02:00
/**
2025-01-09 18:07:02 +02:00
* Update's given attribute's value or creates it if it doesn't exist
*
* @param type - attribute type (label, relation, etc.)
* @param name - attribute name
* @param value - attribute value (optional)
*/
2024-07-18 22:58:12 +03:00
setAttribute(type: AttributeType, name: string, value?: string) {
2021-05-11 22:00:16 +02:00
const attributes = this.getOwnedAttributes();
2025-01-09 18:07:02 +02:00
const attr = attributes.find((attr) => attr.type === type && attr.name === name);
2021-05-11 22:00:16 +02:00
2022-12-22 14:57:00 +01:00
value = value?.toString() || "";
2021-05-11 22:00:16 +02:00
if (attr) {
if (attr.value !== value) {
attr.value = value;
attr.save();
}
2025-01-09 18:07:02 +02:00
} else {
new BAttribute({
2021-05-11 22:00:16 +02:00
noteId: this.noteId,
type: type,
name: name,
value: value
2021-05-11 22:00:16 +02:00
}).save();
}
}
/**
2025-01-09 18:07:02 +02:00
* Removes given attribute name-value pair if it exists.
*
* @param type - attribute type (label, relation, etc.)
* @param name - attribute name
* @param value - attribute value (optional)
*/
2024-02-17 10:56:27 +02:00
removeAttribute(type: string, name: string, value?: string) {
2021-05-11 22:00:16 +02:00
const attributes = this.getOwnedAttributes();
for (const attribute of attributes) {
if (attribute.type === type && attribute.name === name && (value === undefined || value === attribute.value)) {
attribute.markAsDeleted();
}
}
}
/**
2025-01-09 18:07:02 +02:00
* Adds a new attribute to this note. The attribute is saved and returned.
* See addLabel, addRelation for more specific methods.
*
* @param type - attribute type (label / relation)
* @param name - name of the attribute, not including the leading ~/#
* @param value - value of the attribute - text for labels, target note ID for relations; optional.
*/
2024-07-18 22:58:12 +03:00
addAttribute(type: AttributeType, name: string, value: string = "", isInheritable: boolean = false, position: number | null = null): BAttribute {
2023-01-08 12:46:26 +01:00
return new BAttribute({
2021-05-11 22:00:16 +02:00
noteId: this.noteId,
type: type,
name: name,
value: value,
isInheritable: isInheritable,
position: position
}).save();
}
/**
2025-01-09 18:07:02 +02:00
* Adds a new label to this note. The label attribute is saved and returned.
*
* @param name - name of the label, not including the leading #
* @param value - text value of the label; optional
*/
2024-02-17 10:56:27 +02:00
addLabel(name: string, value: string = "", isInheritable: boolean = false): BAttribute {
2021-05-11 22:00:16 +02:00
return this.addAttribute(LABEL, name, value, isInheritable);
}
/**
2025-01-09 18:07:02 +02:00
* Adds a new relation to this note. The relation attribute is saved and
* returned.
*
* @param name - name of the relation, not including the leading ~
*/
2024-02-17 10:56:27 +02:00
addRelation(name: string, targetNoteId: string, isInheritable: boolean = false): BAttribute {
2021-05-11 22:00:16 +02:00
return this.addAttribute(RELATION, name, targetNoteId, isInheritable);
}
/**
2025-01-09 18:07:02 +02:00
* Based on enabled, the attribute is either set or removed.
*
* @param type - attribute type ('relation', 'label' etc.)
* @param enabled - toggle On or Off
* @param name - attribute name
* @param value - attribute value (optional)
*/
2024-07-18 22:58:12 +03:00
toggleAttribute(type: AttributeType, enabled: boolean, name: string, value?: string) {
2021-05-11 22:00:16 +02:00
if (enabled) {
this.setAttribute(type, name, value);
2025-01-09 18:07:02 +02:00
} else {
2021-05-11 22:00:16 +02:00
this.removeAttribute(type, name, value);
}
}
/**
2025-01-09 18:07:02 +02:00
* Based on enabled, label is either set or removed.
*
* @param enabled - toggle On or Off
* @param name - label name
* @param value - label value (optional)
*/
2024-02-17 10:56:27 +02:00
toggleLabel(enabled: boolean, name: string, value?: string) {
return this.toggleAttribute(LABEL, enabled, name, value);
}
2021-05-11 22:00:16 +02:00
/**
2025-01-09 18:07:02 +02:00
* Based on enabled, relation is either set or removed.
*
* @param enabled - toggle On or Off
* @param name - relation name
* @param value - relation value (noteId)
*/
2024-02-17 10:56:27 +02:00
toggleRelation(enabled: boolean, name: string, value?: string) {
return this.toggleAttribute(RELATION, enabled, name, value);
}
2021-05-11 22:00:16 +02:00
/**
2025-01-09 18:07:02 +02:00
* Update's given label's value or creates it if it doesn't exist
*
* @param name - label name
* @param value label value
*/
2024-02-17 10:56:27 +02:00
setLabel(name: string, value?: string) {
return this.setAttribute(LABEL, name, value);
}
2021-05-11 22:00:16 +02:00
/**
2025-01-09 18:07:02 +02:00
* Update's given relation's value or creates it if it doesn't exist
*
* @param name - relation name
* @param value - relation value (noteId)
*/
setRelation(name: string, value?: string) {
2024-02-17 10:56:27 +02:00
return this.setAttribute(RELATION, name, value);
}
2021-05-11 22:00:16 +02:00
/**
2025-01-09 18:07:02 +02:00
* Remove label name-value pair, if it exists.
*
* @param name - label name
* @param value - label value
*/
2024-02-17 10:56:27 +02:00
removeLabel(name: string, value?: string) {
return this.removeAttribute(LABEL, name, value);
}
2021-05-11 22:00:16 +02:00
/**
2025-01-09 18:07:02 +02:00
* Remove the relation name-value pair, if it exists.
*
* @param name - relation name
* @param value - relation value (noteId)
*/
2024-02-17 10:56:27 +02:00
removeRelation(name: string, value?: string) {
return this.removeAttribute(RELATION, name, value);
}
2021-05-11 22:00:16 +02:00
2024-02-17 10:56:27 +02:00
searchNotesInSubtree(searchString: string) {
2024-04-05 20:45:57 +03:00
return searchService.searchNotes(searchString) as BNote[];
2021-06-06 11:01:10 +02:00
}
2024-02-17 10:56:27 +02:00
searchNoteInSubtree(searchString: string) {
2021-06-06 11:01:10 +02:00
return this.searchNotesInSubtree(searchString)[0];
}
2024-07-18 22:58:12 +03:00
cloneTo(parentNoteId: string): CloneResponse {
const branch = this.becca.getNote(parentNoteId)?.getParentBranches()[0];
2024-07-18 22:58:12 +03:00
if (!branch?.branchId) {
return {
success: false,
message: "Unable to find the branch ID to clone."
};
}
2021-06-06 11:01:10 +02:00
2024-07-18 22:58:12 +03:00
return cloningService.cloneNoteToBranch(this.noteId, branch.branchId);
2021-06-06 11:01:10 +02:00
}
2024-02-17 10:56:27 +02:00
isEligibleForConversionToAttachment(opts: ConvertOpts = { autoConversion: false }) {
2025-01-09 18:07:02 +02:00
if (this.type !== "image" || !this.isContentAvailable() || this.hasChildren() || this.getParentBranches().length !== 1) {
return false;
}
2025-01-09 18:07:02 +02:00
const targetRelations = this.getTargetRelations().filter((relation) => relation.name === "imageLink");
if (opts.autoConversion && targetRelations.length === 0) {
return false;
} else if (targetRelations.length > 1) {
return false;
}
const parentNote = this.getParentNotes()[0]; // at this point note can have only one parent
2023-05-04 22:16:18 +02:00
const referencingNote = targetRelations[0]?.getNote();
2023-05-04 22:16:18 +02:00
if (referencingNote && parentNote !== referencingNote) {
return false;
2025-01-09 18:07:02 +02:00
} else if (parentNote.type !== "text" || !parentNote.isContentAvailable()) {
return false;
}
return true;
}
2023-03-24 09:13:35 +01:00
/**
2025-01-09 18:07:02 +02:00
* Some notes are eligible for conversion into an attachment of its parent, note must have these properties:
* - it has exactly one target relation
* - it has a relation from its parent note
* - it has no children
* - it has no clones
* - the parent is of type text
* - both notes are either unprotected or user is in protected session
*
* Currently, works only for image notes.
*
* In the future, this functionality might get more generic and some of the requirements relaxed.
*
* @returns null if note is not eligible for conversion
*/
2024-02-17 10:56:27 +02:00
convertToParentAttachment(opts: ConvertOpts = { autoConversion: false }): BAttachment | null {
if (!this.isEligibleForConversionToAttachment(opts)) {
2023-03-24 09:13:35 +01:00
return null;
}
const content = this.getContent();
2023-03-24 09:13:35 +01:00
const parentNote = this.getParentNotes()[0];
2023-03-24 09:13:35 +01:00
const attachment = parentNote.saveAttachment({
2025-01-09 18:07:02 +02:00
role: "image",
2023-03-24 09:13:35 +01:00
mime: this.mime,
title: this.title,
content: content
});
let parentContent = parentNote.getContent();
2023-03-24 09:13:35 +01:00
const oldNoteUrl = `api/images/${this.noteId}/`;
2023-04-17 22:40:53 +02:00
const newAttachmentUrl = `api/attachments/${attachment.attachmentId}/image/`;
2023-03-24 09:13:35 +01:00
if (typeof parentContent !== "string") {
throw new Error("Unable to convert image note into attachment because parent note does not have a string content.");
}
2023-03-24 09:13:35 +01:00
const fixedContent = utils.replaceAll(parentContent, oldNoteUrl, newAttachmentUrl);
parentNote.setContent(fixedContent);
2023-07-14 21:28:32 +02:00
noteService.asyncPostProcessContent(parentNote, fixedContent); // to mark an unused attachment for deletion
2023-07-14 21:18:56 +02:00
2023-03-24 09:13:35 +01:00
this.deleteNote();
return attachment;
}
2021-06-06 11:01:10 +02:00
/**
2025-01-09 18:07:02 +02:00
* (Soft) delete a note and all its descendants.
*
* @param deleteId - optional delete identified
*/
2024-02-17 10:56:27 +02:00
deleteNote(deleteId: string | null = null, taskContext: TaskContext | null = null) {
2022-06-08 22:25:00 +02:00
if (this.isDeleted) {
return;
}
if (!deleteId) {
deleteId = utils.randomString(10);
}
if (!taskContext) {
2025-01-09 18:07:02 +02:00
taskContext = new TaskContext("no-progress-reporting");
}
// needs to be run before branches and attributes are deleted and thus attached relations disappear
2025-01-09 18:07:02 +02:00
handlers.runAttachedRelations(this, "runOnNoteDeletion", this);
taskContext.noteDeletionHandlerTriggered = true;
for (const branch of this.getParentBranches()) {
branch.deleteBranch(deleteId, taskContext);
}
}
2020-05-17 09:48:24 +02:00
decrypt() {
if (this.isProtected && !this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) {
try {
2024-02-17 10:56:27 +02:00
this.title = protectedSessionService.decryptString(this.title) || "";
2023-06-01 00:07:57 +02:00
this.__flatTextCache = null;
2020-05-17 09:48:24 +02:00
this.isDecrypted = true;
2025-01-09 18:07:02 +02:00
} catch (e: any) {
log.error(`Could not decrypt note ${this.noteId}: ${e.message} ${e.stack}`);
}
2020-05-17 09:48:24 +02:00
}
}
2020-09-17 14:34:10 +02:00
2022-12-06 23:48:44 +01:00
isLaunchBarConfig() {
return this.type === "launcher"
|| ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers"].includes(this.noteId)
|| ["_lbMobileRoot", "_lbMobileAvailableLaunchers", "_lbMobileVisibleLaunchers"].includes(this.noteId);
2022-12-06 23:48:44 +01:00
}
2022-12-08 15:29:14 +01:00
isOptions() {
2023-01-13 11:34:35 +01:00
return this.noteId.startsWith("_options");
2022-12-08 15:29:14 +01:00
}
get isDeleted() {
2023-06-30 11:18:34 +02:00
// isBeingDeleted is relevant only in the transition period when the deletion process has begun, but not yet
2023-06-05 09:23:42 +02:00
// finished (note is still in becca)
return !(this.noteId in this.becca.notes) || this.isBeingDeleted;
}
saveRevision(): BRevision {
return sql.transactional(() => {
let noteContent = this.getContent();
2025-01-09 18:07:02 +02:00
const revision = new BRevision(
{
noteId: this.noteId,
// title and text should be decrypted now
title: this.title,
type: this.type,
mime: this.mime,
isProtected: this.isProtected,
utcDateLastEdited: this.utcDateModified,
utcDateCreated: dateUtils.utcNowDateTime(),
utcDateModified: dateUtils.utcNowDateTime(),
dateLastEdited: this.dateModified,
dateCreated: dateUtils.localNowDateTime()
},
true
);
2023-06-29 12:19:01 +02:00
revision.save(); // to generate revisionId, which is then used to save attachments
2023-10-02 15:24:40 +02:00
for (const noteAttachment of this.getAttachments()) {
const revisionAttachment = noteAttachment.copy();
if (!revision.revisionId) {
throw new Error("Revision ID is missing.");
}
2023-10-02 15:24:40 +02:00
revisionAttachment.ownerId = revision.revisionId;
2024-02-17 10:56:27 +02:00
revisionAttachment.setContent(noteAttachment.getContent(), { forceSave: true });
2023-04-17 23:21:28 +02:00
2025-01-09 18:07:02 +02:00
if (this.type === "text" && typeof noteContent === "string") {
2023-06-29 12:19:01 +02:00
// content is rewritten to point to the revision attachments
2025-01-09 18:07:02 +02:00
noteContent = noteContent.replaceAll(`attachments/${noteAttachment.attachmentId}`, `attachments/${revisionAttachment.attachmentId}`);
2025-01-09 18:07:02 +02:00
noteContent = noteContent.replaceAll(
new RegExp(`href="[^"]*attachmentId=${noteAttachment.attachmentId}[^"]*"`, "gi"),
`href="api/attachments/${revisionAttachment.attachmentId}/download"`
);
2023-06-29 12:19:01 +02:00
}
}
2023-04-17 23:21:28 +02:00
revision.setContent(noteContent);
2025-01-09 18:07:02 +02:00
this.eraseExcessRevisionSnapshots();
return revision;
});
}
2024-09-04 08:41:17 +00:00
// Limit the number of Snapshots to revisionSnapshotNumberLimit
// Delete older Snapshots that exceed the limit
2024-09-04 09:04:40 +00:00
eraseExcessRevisionSnapshots() {
// lable has a higher priority
let revisionSnapshotNumberLimit = parseInt(this.getLabelValue("versioningLimit") ?? "");
if (!Number.isInteger(revisionSnapshotNumberLimit)) {
2025-01-09 18:07:02 +02:00
revisionSnapshotNumberLimit = parseInt(optionService.getOption("revisionSnapshotNumberLimit"));
2024-09-04 09:04:40 +00:00
}
2024-09-04 08:41:17 +00:00
if (revisionSnapshotNumberLimit >= 0) {
const revisions = this.getRevisions();
if (revisions.length - revisionSnapshotNumberLimit > 0) {
const revisionIds = revisions
.slice(0, revisions.length - revisionSnapshotNumberLimit)
2025-01-09 18:07:02 +02:00
.map((revision) => revision.revisionId)
2024-09-04 08:41:17 +00:00
.filter((id): id is string => id !== undefined);
eraseService.eraseRevisions(revisionIds);
}
}
}
/**
2025-01-09 18:07:02 +02:00
* @param matchBy - choose by which property we detect if to update an existing attachment.
* Supported values are either 'attachmentId' (default) or 'title'
*/
saveAttachment({ attachmentId, role, mime, title, content, position }: AttachmentRow, matchBy: "attachmentId" | "title" | undefined = "attachmentId") {
2025-01-09 18:07:02 +02:00
if (!["attachmentId", "title"].includes(matchBy)) {
throw new Error(`Unsupported value '${matchBy}' for matchBy param, has to be either 'attachmentId' or 'title'.`);
}
2023-03-16 16:37:31 +01:00
let attachment;
2025-01-09 18:07:02 +02:00
if (matchBy === "title" && title) {
attachment = this.getAttachmentByTitle(title);
2025-01-09 18:07:02 +02:00
} else if (matchBy === "attachmentId" && attachmentId) {
attachment = this.becca.getAttachmentOrThrow(attachmentId);
2023-03-16 16:37:31 +01:00
}
2025-01-09 18:07:02 +02:00
attachment =
attachment ||
new BAttachment({
ownerId: this.noteId,
title,
role,
mime,
isProtected: this.isProtected,
position
});
content = content || "";
2025-01-09 18:07:02 +02:00
attachment.setContent(content, { forceSave: true });
2023-03-16 12:17:55 +01:00
return attachment;
}
2023-05-03 10:23:20 +02:00
getFileName() {
return utils.formatDownloadTitle(this.title, this.type, this.mime);
}
2021-05-08 21:10:58 +02:00
beforeSaving() {
super.beforeSaving();
this.becca.addNote(this.noteId, this);
2021-05-09 20:46:32 +02:00
2021-05-08 21:10:58 +02:00
this.dateModified = dateUtils.localNowDateTime();
this.utcDateModified = dateUtils.utcNowDateTime();
}
getPojo(): NotePojo {
return {
noteId: this.noteId,
title: this.title,
isProtected: this.isProtected,
type: this.type,
mime: this.mime,
2023-03-15 22:44:08 +01:00
blobId: this.blobId,
2021-05-09 11:12:53 +02:00
isDeleted: false,
dateCreated: this.dateCreated,
dateModified: this.dateModified,
utcDateCreated: this.utcDateCreated,
utcDateModified: this.utcDateModified
};
}
getPojoToSave() {
const pojo = this.getPojo();
2021-04-25 21:19:18 +02:00
if (pojo.isProtected) {
2024-02-17 10:56:27 +02:00
if (this.isDecrypted && pojo.title) {
pojo.title = protectedSessionService.encrypt(pojo.title) || undefined;
2025-01-09 18:07:02 +02:00
} else {
2021-04-25 21:19:18 +02:00
// updating protected note outside of protected session means we will keep original ciphertexts
delete pojo.title;
}
}
return pojo;
}
// TODO: Deduplicate with fnote
getIcon() {
const iconClassLabels = this.getLabels("iconClass");
if (iconClassLabels && iconClassLabels.length > 0) {
return iconClassLabels[0].value;
} else if (this.noteId === "root") {
return "bx bx-home-alt-2";
}
if (this.noteId === "_share") {
return "bx bx-share-alt";
} else if (this.type === "text") {
if (this.isFolder()) {
return "bx bx-folder";
} else {
return "bx bx-note";
}
} else if (this.type === "code" && this.mime.startsWith("text/x-sql")) {
return "bx bx-data";
} else {
return NOTE_TYPE_ICONS[this.type];
}
}
// TODO: Deduplicate with fnote
isFolder() {
return this.type === "search" || this.getFilteredChildBranches().length > 0;
}
// TODO: Deduplicate with fnote
getFilteredChildBranches() {
let childBranches = this.getChildBranches();
if (!childBranches) {
console.error(`No children for '${this.noteId}'. This shouldn't happen.`);
return [];
}
// we're not checking hideArchivedNotes since that would mean we need to lazy load the child notes
// which would seriously slow down everything.
// we check this flag only once user chooses to expand the parent. This has the negative consequence that
// note may appear as a folder but not contain any children when all of them are archived
return childBranches;
}
2020-05-16 23:12:29 +02:00
}
2020-05-17 09:48:24 +02:00
export default BNote;