2020-05-17 09:48:24 +02:00
"use strict" ;
2024-07-18 21:35:17 +03:00
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" ;
2024-07-18 21:35:17 +03:00
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" ;
2024-07-18 21:37:45 +03:00
import dayjs from "dayjs" ;
2024-07-24 20:35:19 +03:00
import utc from "dayjs/plugin/utc.js" ;
2024-07-18 21:35:17 +03:00
import eventService from "../../services/events.js" ;
2025-04-18 12:33:50 +03:00
import type { AttachmentRow , AttributeType , NoteRow , NoteType , RevisionRow } from "@triliumnext/commons" ;
2025-01-13 23:18:10 +02:00
import type BBranch from "./bbranch.js" ;
2024-07-18 21:35:17 +03:00
import BAttribute from "./battribute.js" ;
2025-01-09 18:36:24 +02:00
import type { NotePojo } from "../becca-interface.js" ;
2024-07-18 22:58:12 +03:00
import searchService from "../../services/search/services/search.js" ;
2025-01-09 18:36:24 +02:00
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" ;
2023-01-22 23:36:05 +01:00
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
2025-03-29 22:18:42 +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
2021-11-10 21:30:54 +01:00
/ * *
2023-06-30 11:18:34 +02:00
* Trilium ' s main entity , which can represent text note , image , code note , file attachment etc .
2021-11-10 21:30:54 +01:00
* /
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" ] ;
}
2021-04-25 20:00:42 +02:00
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
2024-02-17 11:13:53 +02: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 */
2024-02-18 11:16:30 +02:00
contentSize ! : number | null ;
2024-02-17 10:56:27 +02:00
/** size of the note content, attachment contents in bytes */
2024-02-18 11:16:30 +02:00
contentAndAttachmentsSize ! : number | null ;
2024-02-17 10:56:27 +02:00
/** size of the note content, attachment contents and revision contents in bytes */
2024-02-18 11:16:30 +02:00
contentAndAttachmentsAndRevisionsSize ! : number | null ;
2024-02-17 10:56:27 +02:00
/** number of note revisions for this note */
2024-02-18 11:16:30 +02:00
revisionCount ! : number | null ;
2024-02-17 10:56:27 +02:00
2024-02-17 20:45:31 +02:00
constructor ( row? : Partial < NoteRow > ) {
2021-04-25 20:00:42 +02:00
super ( ) ;
2021-07-24 21:10:16 +02:00
if ( ! row ) {
return ;
}
2021-08-07 21:21:30 +02:00
this . updateFromRow ( row ) ;
this . init ( ) ;
}
2024-02-17 11:13:53 +02:00
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 ] ) ;
2021-07-24 21:10:16 +02:00
}
2024-02-17 10:56:27 +02:00
update ( [ noteId , title , type , mime , isProtected , blobId , dateCreated , dateModified , utcDateCreated , utcDateModified ] : any ) {
2021-07-24 21:10:16 +02:00
// ------ 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 ;
2021-07-24 21:10:16 +02:00
this . dateCreated = dateCreated || dateUtils . localNowDateTime ( ) ;
this . dateModified = dateModified ;
this . utcDateCreated = utcDateCreated || dateUtils . utcNowDateTime ( ) ;
this . utcDateModified = utcDateModified ;
2022-12-06 16:11:43 +01:00
this . isBeingDeleted = false ;
2021-07-24 21:10:16 +02:00
// ------ Derived attributes ------
2021-10-28 22:02:18 +02:00
this . isDecrypted = ! this . noteId || ! this . isProtected ;
2021-07-24 21:10:16 +02:00
this . decrypt ( ) ;
2023-06-01 00:07:57 +02:00
this . __flatTextCache = null ;
2021-07-24 21:10:16 +02:00
return this ;
}
2020-09-03 21:37:06 +02:00
2021-07-24 21:10:16 +02:00
init() {
2020-09-03 21:37:06 +02:00
this . parentBranches = [ ] ;
this . parents = [ ] ;
this . children = [ ] ;
this . ownedAttributes = [ ] ;
2021-04-25 21:19:18 +02:00
this . __attributeCache = null ;
2023-05-31 23:21:06 +02:00
this . __inheritableAttributeCache = null ;
2020-09-03 21:37:06 +02:00
this . targetRelations = [ ] ;
2021-10-06 19:59:29 +02:00
this . becca . addNote ( this . noteId , this ) ;
2023-06-01 00:07:57 +02:00
this . __ancestorCache = null ;
2021-01-22 22:20:17 +01:00
this . contentSize = null ;
2023-10-02 22:02:25 +02:00
this . contentAndAttachmentsSize = null ;
this . contentAndAttachmentsAndRevisionsSize = null ;
2021-01-22 22:20:17 +01:00
this . revisionCount = null ;
2020-09-03 21:37:06 +02:00
}
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
}
2022-01-08 21:50:16 +01:00
getTitleOrProtected() {
2025-01-09 18:07:02 +02:00
return this . isContentAvailable ( ) ? this . title : "[protected]" ;
2022-01-08 21:50:16 +01:00
}
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 ;
}
2021-09-03 22:33:40 +02:00
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 )
* /
2024-02-17 22:58:54 +02:00
getContent() {
return this . _getContent ( ) ;
2021-04-25 20:00:42 +02:00
}
2023-09-08 21:53:57 +02:00
/ * *
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 {
2024-02-17 11:13:53 +02:00
const content = this . getContent ( ) ;
2021-04-25 20:00:42 +02:00
2024-02-17 22:58:54 +02:00
if ( typeof content !== "string" || ! content || ! content . trim ( ) ) {
2021-04-25 20:00:42 +02:00
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 */
2023-09-08 21:53:57 +02:00
getJsonContentSafely() {
try {
return this . getJsonContent ( ) ;
2025-01-09 18:07:02 +02:00
} catch ( e ) {
2023-09-08 21:53:57 +02:00
return null ;
}
}
2024-02-17 22:58:54 +02:00
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 } ) ;
2023-03-16 15:19:26 +01:00
}
2021-04-25 20:00:42 +02:00
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" ) ) ;
2023-03-16 15:19:26 +01:00
}
2021-04-25 20:00:42 +02:00
2023-03-16 15:19:26 +01:00
get dateCreatedObj() {
return this . dateCreated === null ? null : dayjs ( this . dateCreated ) ;
}
2021-04-25 20:00:42 +02:00
2023-03-16 15:19:26 +01:00
get utcDateCreatedObj() {
return this . utcDateCreated === null ? null : dayjs . utc ( this . utcDateCreated ) ;
}
2023-03-15 22:44:08 +01:00
2023-03-16 15:19:26 +01:00
get dateModifiedObj() {
return this . dateModified === null ? null : dayjs ( this . dateModified ) ;
2021-04-25 20:00:42 +02:00
}
2023-03-16 15:19:26 +01:00
get utcDateModifiedObj() {
return this . utcDateModified === null ? null : dayjs . utc ( this . utcDateModified ) ;
2021-04-25 20:00:42 +02:00
}
2024-04-17 22:35:38 +03:00
/** @returns true if this note is the root of the note tree. Root note has "root" noteId */
2021-04-25 20:00:42 +02:00
isRoot() {
2025-01-09 18:07:02 +02:00
return this . noteId === "root" ;
2021-04-25 20:00:42 +02:00
}
2024-04-17 22:35:38 +03:00
/** @returns true if this note is of application/json content type */
2021-04-25 20:00:42 +02:00
isJson() {
return this . mime === "application/json" ;
}
2024-04-17 22:35:38 +03:00
/** @returns true if this note is JavaScript (code or attachment) */
2021-04-25 20:00:42 +02:00
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" )
) ;
2021-04-25 20:00:42 +02:00
}
2024-04-17 22:35:38 +03:00
/** @returns true if this note is HTML */
2021-04-25 20:00:42 +02:00
isHtml() {
2025-01-09 18:07:02 +02:00
return [ "code" , "file" , "render" ] . includes ( this . type ) && this . mime === "text/html" ;
2021-04-25 20:00:42 +02:00
}
2024-04-17 22:35:38 +03:00
/** @returns true if this note is an image */
2023-01-26 20:32:27 +01:00
isImage() {
2025-01-09 18:07:02 +02:00
return this . type === "image" || ( this . type === "file" && this . mime ? . startsWith ( "image/" ) ) ;
2023-01-26 20:32:27 +01:00
}
2023-05-05 16:37:39 +02:00
/** @deprecated use hasStringContent() instead */
2021-04-25 20:00:42 +02:00
isStringNote() {
2023-05-05 16:37:39 +02:00
return this . hasStringContent ( ) ;
}
2024-04-17 22:35:38 +03:00
/** @returns true if the note has string content (not binary) */
2023-05-05 16:37:39 +02:00
hasStringContent() {
2021-04-25 20:00:42 +02:00
return utils . isStringNote ( this . type , this . mime ) ;
}
2024-04-17 22:35:38 +03:00
/** @returns JS script environment - either "frontend" or "backend" */
2021-04-25 20:00:42 +02:00
getScriptEnv() {
2025-01-09 18:07:02 +02:00
if ( this . isHtml ( ) || ( this . isJavaScript ( ) && this . mime . endsWith ( "env=frontend" ) ) ) {
2021-04-25 20:00:42 +02:00
return "frontend" ;
}
2025-01-09 18:07:02 +02:00
if ( this . type === "render" ) {
2021-04-25 20:00:42 +02:00
return "frontend" ;
}
2025-01-09 18:07:02 +02:00
if ( this . isJavaScript ( ) && this . mime . endsWith ( "env=backend" ) ) {
2021-04-25 20:00:42 +02:00
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 [ ] {
2022-12-17 21:46:51 +01:00
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 {
2022-12-14 23:44:26 +01:00
return this . __attributeCache ;
2021-04-25 21:19:18 +02:00
}
2020-06-04 23:58:31 +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 [ ] ) {
2020-06-04 23:58:31 +02:00
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 ( ) ;
2020-06-04 23:58:31 +02:00
const newPath = [ . . . path , this . noteId ] ;
2020-05-16 23:12:29 +02:00
2023-01-17 22:14:53 +01: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 ) {
2020-06-04 23:58:31 +02:00
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 ) {
2022-06-19 11:36:29 +02:00
templateAttributes . push (
2025-01-09 18:07:02 +02:00
. . . templateNote
. __getAttributes ( newPath )
2022-06-19 11:36:29 +02:00
// 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" ) ) )
2022-06-19 11:36:29 +02:00
) ;
2020-05-16 23:12:29 +02:00
}
}
}
2021-04-25 21:19:18 +02:00
this . __attributeCache = [ ] ;
2020-09-30 22:34:18 +02:00
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 ) ;
2020-09-30 22:34:18 +02:00
}
}
2023-05-31 23:21:06 +02:00
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 ) {
2023-05-31 23:21:06 +02:00
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 [ ] {
2020-06-04 23:58:31 +02:00
if ( path . includes ( this . noteId ) ) {
return [ ] ;
}
2023-05-31 23:21:06 +02:00
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" ) {
2022-12-17 21:46:51 +01:00
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 === "~" ) {
2022-12-17 21:46:51 +01:00
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
}
2024-02-18 00:34:30 +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-01-23 21:00:59 +01:00
}
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
2023-04-03 21:08:32 +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 {
2023-04-03 21:08:32 +02:00
const label = this . getLabel ( name ) ;
if ( ! label ) {
return false ;
}
2025-01-09 18:07:02 +02:00
return label && label . value !== "false" ;
2023-04-03 21:08:32 +02:00
}
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 {
2022-06-19 14:06:00 +02:00
return ! ! this . getOwnedAttribute ( type , name , value ) ;
2021-01-23 21:00:59 +01: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 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
* /
2024-02-17 22:58:54 +02:00
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 ) {
2022-12-17 21:46:51 +01:00
this . __validateTypeName ( type , name ) ;
2021-09-02 22:14:22 +02:00
2022-06-19 14:06:00 +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 {
2023-06-27 23:03:17 +02:00
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 ) {
2022-06-19 14:06:00 +02:00
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
2021-10-24 14:37:41 +02:00
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
}
} ) ;
2021-02-24 22:38:26 +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
}
2023-01-06 15:07:18 +01: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 ;
2023-01-06 15:07:18 +01:00
} ) ;
}
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
}
2020-09-07 00:05:01 +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 ;
2023-05-31 23:21:06 +02:00
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 ) {
2023-01-17 22:14:53 +01:00
// _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" ) {
2022-11-07 23:56:53 +01:00
return [ ] ;
}
try {
2024-07-18 22:58:12 +03:00
const result = searchService . searchFromNote ( this ) ;
2022-11-07 23:56:53 +01:00
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 ) {
2022-11-07 23:56:53 +01:00
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
2022-11-05 22:32:50 +01:00
2024-02-17 10:56:27 +02:00
function resolveSearchNote ( searchNote : BNote ) {
2022-11-05 22:32:50 +01:00
try {
2022-11-07 23:56:53 +01:00
for ( const resultNote of searchNote . getSearchResultNotes ( ) ) {
addSubtreeNotesInner ( resultNote , searchNote ) ;
2022-11-05 22:32:50 +01:00
}
2025-01-09 18:07:02 +02:00
} catch ( e : any ) {
2022-11-05 22:32:50 +01:00
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 ;
}
2022-11-05 22:32:50 +01:00
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" ) {
2022-11-05 22:32:50 +01:00
if ( resolveSearch ) {
resolveSearchNote ( note ) ;
}
2025-01-09 18:07:02 +02:00
} else {
2022-11-05 22:32:50 +01:00
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 ) ;
2022-11-05 22:32:50 +01:00
return {
notes : Array.from ( noteSet ) ,
relationships
} ;
2020-05-16 23:12:29 +02:00
}
2024-04-17 22:35:38 +03: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 ) ;
2021-01-23 21:00:59 +01:00
}
2022-12-27 14:44:28 +01:00
/** @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
}
2021-02-19 20:42:34 +01:00
get ownedLabelCount() {
2025-01-09 18:07:02 +02:00
return this . ownedAttributes . filter ( ( attr ) = > attr . type === "label" ) . length ;
2021-02-19 20:42:34 +01:00
}
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 ;
2021-02-19 20:42:34 +01:00
}
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
}
2021-02-19 20:42:34 +01:00
get ownedRelationCount() {
2025-01-09 18:07:02 +02:00
return this . ownedAttributes . filter ( ( attr ) = > attr . type === "relation" && ! attr . isAutoLink ( ) ) . length ;
2021-02-19 20:42:34 +01:00
}
get ownedRelationCountIncludingLinks() {
2025-01-09 18:07:02 +02:00
return this . ownedAttributes . filter ( ( attr ) = > attr . type === "relation" ) . length ;
2021-02-19 20:42:34 +01:00
}
get targetRelationCount() {
2025-01-09 18:07:02 +02:00
return this . targetRelations . filter ( ( attr ) = > ! attr . isAutoLink ( ) ) . length ;
2021-02-19 20:42:34 +01:00
}
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
}
2021-02-19 20:42:34 +01:00
get ownedAttributeCount() {
2023-01-27 08:46:04 +01:00
return this . getOwnedAttributes ( ) . length ;
2021-02-19 20:42:34 +01:00
}
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 ) {
2022-01-13 21:28:51 +01:00
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 ) ;
2022-01-13 21:28:51 +01:00
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 {
2021-10-24 14:53:45 +02:00
for ( const ancestorNote of this . getAncestors ( ) ) {
if ( ancestorNote . noteId === ancestorNoteId ) {
return true ;
}
}
return false ;
}
2022-12-23 15:32:11 +01:00
isInHiddenSubtree() {
2025-01-09 18:07:02 +02:00
return this . noteId === "_hidden" || this . hasAncestor ( "_hidden" ) ;
2022-12-23 15:32:11 +01:00
}
2021-05-08 23:31:20 +02:00
getTargetRelations() {
return this . targetRelations ;
}
2024-02-17 10:56:27 +02:00
/ * * @ r e t u r n s r e t u r n s o n l y n o t e s w h i c h a r e t e m p l a t e d , d o e s n o t i n c l u d e t h e i r s u b t r e e s
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 ;
}
2021-03-15 20:31:12 +01:00
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 ) ) ;
2021-04-26 22:00:55 +02:00
}
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
2025-04-01 23:30:21 +03:00
? /*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 `
2025-04-01 23:30:21 +03:00
: /*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 ) ) ;
2023-03-08 09:01:23 +01:00
}
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
2025-04-01 23:30:21 +03:00
? /*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 `
2025-04-01 23:30:21 +03:00
: /*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 ] ;
2023-03-08 09:01:23 +01:00
}
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
}
2024-07-18 19:29:34 +03:00
getAttachmentByTitle ( title : string ) : BAttachment | undefined {
2023-11-12 23:43:04 +01:00
// 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 ] ;
2023-09-08 21:53:57 +02:00
}
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 ( "/" ) ;
}
2023-01-13 10:09:41 +01:00
/ * *
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
* /
2023-01-13 10:09:41 +01:00
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 ;
2023-01-13 10:09:41 +01:00
}
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-08-12 20:42:35 +02:00
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 {
2023-01-03 13:52:37 +01:00
new BAttribute ( {
2021-05-11 22:00:16 +02:00
noteId : this.noteId ,
type : type ,
name : name ,
2021-08-12 20:42:35 +02:00
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 ( ) ;
}
2022-08-27 13:40:01 -07:00
/ * *
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 ) ;
}
2022-08-27 13:40:01 -07:00
/ * *
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 )
* /
2024-04-04 22:00:20 +03:00
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 {
2024-02-17 11:39:29 +02:00
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
}
2023-05-02 22:46:39 +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 ) {
2023-05-02 22:46:39 +02:00
return false ;
}
2025-01-09 18:07:02 +02:00
const targetRelations = this . getTargetRelations ( ) . filter ( ( relation ) = > relation . name === "imageLink" ) ;
2023-05-02 22:46:39 +02:00
2023-07-14 18:24:15 +02:00
if ( opts . autoConversion && targetRelations . length === 0 ) {
return false ;
} else if ( targetRelations . length > 1 ) {
2023-05-02 22:46:39 +02:00
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-02 22:46:39 +02:00
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 ( ) ) {
2023-05-02 22:46:39 +02:00
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 {
2023-07-14 18:24:15 +02:00
if ( ! this . isEligibleForConversionToAttachment ( opts ) ) {
2023-03-24 09:13:35 +01:00
return null ;
}
2024-02-17 11:13:53 +02:00
const content = this . getContent ( ) ;
2023-03-24 09:13:35 +01:00
2023-05-02 22:46:39 +02: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
} ) ;
2024-02-17 11:13:53 +02:00
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
2024-02-17 22:58:54 +02: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
2022-04-19 23:06:46 +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 ;
}
2022-04-19 23:06:46 +02:00
if ( ! deleteId ) {
deleteId = utils . randomString ( 10 ) ;
}
if ( ! taskContext ) {
2025-01-09 18:07:02 +02:00
taskContext = new TaskContext ( "no-progress-reporting" ) ;
2022-04-19 23:06:46 +02:00
}
2022-06-05 14:58:19 +02:00
// 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 ) ;
2022-06-05 14:58:19 +02:00
taskContext . noteDeletionHandlerTriggered = true ;
2022-04-19 23:06:46 +02:00
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 ( ) ) {
2021-04-03 21:48:16 +02:00
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
2021-04-03 21:48:16 +02:00
this . isDecrypted = true ;
2025-01-09 18:07:02 +02:00
} catch ( e : any ) {
2021-04-03 21:48:16 +02:00
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() {
2025-01-18 18:49:36 +02:00
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
}
2021-10-31 21:03:26 +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)
2022-12-06 16:11:43 +01:00
return ! ( this . noteId in this . becca . notes ) || this . isBeingDeleted ;
2021-10-31 21:03:26 +01:00
}
2024-04-17 22:35:38 +03:00
saveRevision ( ) : BRevision {
2023-04-11 23:24:39 +02:00
return sql . transactional ( ( ) = > {
2024-02-17 11:13:53 +02:00
let noteContent = this . getContent ( ) ;
2022-06-02 17:25:58 +02:00
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-04-11 23:24:39 +02:00
2023-06-29 12:19:01 +02:00
revision . save ( ) ; // to generate revisionId, which is then used to save attachments
2023-04-11 23:24:39 +02:00
2023-10-02 15:24:40 +02:00
for ( const noteAttachment of this . getAttachments ( ) ) {
const revisionAttachment = noteAttachment . copy ( ) ;
2024-02-17 11:39:29 +02:00
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 } ` ) ;
2023-07-10 20:30:04 +02:00
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
2023-08-17 16:39:57 +02:00
revision . setContent ( noteContent ) ;
2025-01-09 18:07:02 +02:00
this . eraseExcessRevisionSnapshots ( ) ;
2023-06-04 23:01:40 +02:00
return revision ;
2023-04-11 23:24:39 +02:00
} ) ;
2022-06-02 17:25:58 +02:00
}
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 ) ;
}
}
}
2023-03-08 09:01:23 +01:00
/ * *
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'
* /
2025-03-06 22:44:54 +01:00
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 ) ) {
2023-10-21 00:23:16 +02:00
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 ;
2023-03-08 09:01:23 +01:00
2025-01-09 18:07:02 +02:00
if ( matchBy === "title" && title ) {
2023-10-21 00:23:16 +02:00
attachment = this . getAttachmentByTitle ( title ) ;
2025-01-09 18:07:02 +02:00
} else if ( matchBy === "attachmentId" && attachmentId ) {
2023-05-08 00:02:08 +02:00
attachment = this . becca . getAttachmentOrThrow ( attachmentId ) ;
2023-03-16 16:37:31 +01:00
}
2023-03-08 09:01:23 +01:00
2025-01-09 18:07:02 +02:00
attachment =
attachment ||
new BAttachment ( {
ownerId : this.noteId ,
title ,
role ,
mime ,
isProtected : this.isProtected ,
position
} ) ;
2023-10-21 00:23:16 +02:00
2024-07-14 09:52:01 +03:00
content = content || "" ;
2025-01-09 18:07:02 +02:00
attachment . setContent ( content , { forceSave : true } ) ;
2023-03-08 09:01:23 +01:00
2023-03-16 12:17:55 +01:00
return attachment ;
2023-03-08 09:01:23 +01:00
}
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 ( ) ;
2021-10-06 19:59:29 +02:00
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 ( ) ;
}
2024-04-06 22:32:03 +03:00
getPojo ( ) : NotePojo {
2021-09-08 22:35:32 +02:00
return {
2021-04-25 20:00:42 +02:00
noteId : this.noteId ,
2024-07-14 20:50:42 +03:00
title : this.title ,
2021-04-25 20:00:42 +02:00
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 ,
2021-04-25 20:00:42 +02:00
dateCreated : this.dateCreated ,
dateModified : this.dateModified ,
utcDateCreated : this.utcDateCreated ,
utcDateModified : this.utcDateModified
} ;
2021-09-08 22:35:32 +02:00
}
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 ;
2021-04-25 20:00:42 +02:00
}
2025-03-29 22:18:42 +02:00
// 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
2024-07-18 21:50:12 +03:00
export default BNote ;