2020-05-17 09:48:24 +02:00
"use strict" ;
2021-05-17 22:09:49 +02:00
const protectedSessionService = require ( '../../services/protected_session' ) ;
const log = require ( '../../services/log' ) ;
const sql = require ( '../../services/sql' ) ;
const utils = require ( '../../services/utils' ) ;
const dateUtils = require ( '../../services/date_utils' ) ;
const entityChangesService = require ( '../../services/entity_changes' ) ;
2022-01-10 17:09:20 +01:00
const AbstractEntity = require ( "./abstract_entity" ) ;
const NoteRevision = require ( "./note_revision" ) ;
2022-06-05 14:58:19 +02:00
const TaskContext = require ( "../../services/task_context" ) ;
2020-05-17 09:48:24 +02:00
2021-04-17 20:52:46 +02:00
const LABEL = 'label' ;
const RELATION = 'relation' ;
2021-11-10 21:30:54 +01:00
/ * *
* Trilium ' s main entity which can represent text note , image , code note , file attachment etc .
2022-04-16 00:17:32 +02:00
*
* @ extends AbstractEntity
2021-11-10 21:30:54 +01:00
* /
2021-04-25 20:00:42 +02:00
class Note extends AbstractEntity {
static get entityName ( ) { return "notes" ; }
static get primaryKeyName ( ) { return "noteId" ; }
static get hashedProperties ( ) { return [ "noteId" , "title" , "isProtected" , "type" , "mime" ] ; }
2021-04-30 22:13:13 +02:00
constructor ( row ) {
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 ( ) ;
}
updateFromRow ( row ) {
2021-07-24 21:10:16 +02:00
this . update ( [
row . noteId ,
row . title ,
row . type ,
row . mime ,
row . isProtected ,
row . dateCreated ,
row . dateModified ,
row . utcDateCreated ,
row . utcDateModified
] ) ;
}
update ( [ noteId , title , type , mime , isProtected , dateCreated , dateModified , utcDateCreated , utcDateModified ] ) {
// ------ Database persisted attributes ------
2021-10-29 21:36:23 +02:00
/** @type {string} */
2021-07-24 21:10:16 +02:00
this . noteId = noteId ;
2021-10-29 21:36:23 +02:00
/** @type {string} */
2021-07-24 21:10:16 +02:00
this . title = title ;
2021-10-29 21:36:23 +02:00
/** @type {boolean} */
2021-07-24 21:10:16 +02:00
this . isProtected = ! ! isProtected ;
2021-10-29 21:36:23 +02:00
/** @type {string} */
2021-07-24 21:10:16 +02:00
this . type = type ;
2021-10-29 21:36:23 +02:00
/** @type {string} */
2021-07-24 21:10:16 +02:00
this . mime = mime ;
2021-10-29 21:36:23 +02:00
/** @type {string} */
2021-07-24 21:10:16 +02:00
this . dateCreated = dateCreated || dateUtils . localNowDateTime ( ) ;
2021-10-29 21:36:23 +02:00
/** @type {string} */
2021-07-24 21:10:16 +02:00
this . dateModified = dateModified ;
2021-10-29 21:36:23 +02:00
/** @type {string} */
2021-07-24 21:10:16 +02:00
this . utcDateCreated = utcDateCreated || dateUtils . utcNowDateTime ( ) ;
2021-10-29 21:36:23 +02:00
/** @type {string} */
2021-07-24 21:10:16 +02:00
this . utcDateModified = utcDateModified ;
// ------ Derived attributes ------
2021-10-29 21:36:23 +02:00
/** @type {boolean} */
2021-10-28 22:02:18 +02:00
this . isDecrypted = ! this . noteId || ! this . isProtected ;
2021-07-24 21:10:16 +02:00
this . decrypt ( ) ;
2021-10-29 21:36:23 +02:00
/** @type {string|null} */
2021-07-24 21:10:16 +02:00
this . flatTextCache = null ;
return this ;
}
2020-09-03 21:37:06 +02:00
2021-07-24 21:10:16 +02:00
init ( ) {
2021-11-10 21:30:54 +01:00
/** @type {Branch[]} */
2020-09-03 21:37:06 +02:00
this . parentBranches = [ ] ;
2021-11-10 21:30:54 +01:00
/** @type {Note[]} */
2020-09-03 21:37:06 +02:00
this . parents = [ ] ;
2021-11-10 21:30:54 +01:00
/** @type {Note[]} */
2020-09-03 21:37:06 +02:00
this . children = [ ] ;
2021-11-10 21:30:54 +01:00
/** @type {Attribute[]} */
2020-09-03 21:37:06 +02:00
this . ownedAttributes = [ ] ;
2021-11-10 21:30:54 +01:00
/ * * @ t y p e { A t t r i b u t e [ ] | n u l l }
* @ private * /
2021-04-25 21:19:18 +02:00
this . _ _attributeCache = null ;
2021-11-10 21:30:54 +01:00
/ * * @ t y p e { A t t r i b u t e [ ] | n u l l }
* @ private * /
2020-09-03 21:37:06 +02:00
this . inheritableAttributeCache = null ;
2021-11-10 21:30:54 +01:00
/** @type {Attribute[]} */
2020-09-03 21:37:06 +02:00
this . targetRelations = [ ] ;
2021-10-06 19:59:29 +02:00
this . becca . addNote ( this . noteId , this ) ;
2020-09-03 21:37:06 +02:00
2021-11-10 21:30:54 +01:00
/ * * @ t y p e { N o t e [ ] | n u l l }
* @ private * /
2020-09-03 21:37:06 +02:00
this . ancestorCache = null ;
2021-01-22 22:20:17 +01:00
// following attributes are filled during searching from database
2021-11-10 21:30:54 +01:00
/ * *
* size of the content in bytes
* @ type { int | null }
* /
2021-01-22 22:20:17 +01:00
this . contentSize = null ;
2021-11-10 21:30:54 +01:00
/ * *
* size of the content and note revision contents in bytes
* @ type { int | null }
* /
2021-01-22 22:20:17 +01:00
this . noteSize = null ;
2021-11-10 21:30:54 +01:00
/ * *
* number of note revisions for this note
* @ type { int | 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 ( ) {
2021-05-09 11:12:53 +02:00
return ! this . noteId // new note which was not encrypted yet
|| ! this . isProtected
|| protectedSessionService . isProtectedSessionAvailable ( )
}
2022-01-08 21:50:16 +01:00
getTitleOrProtected ( ) {
return this . isContentAvailable ( ) ? this . title : '[protected]' ;
}
2021-10-24 14:53:45 +02:00
/** @returns {Branch[]} */
2021-04-25 21:19:18 +02:00
getParentBranches ( ) {
return this . parentBranches ;
}
2021-12-20 17:30:47 +01:00
/ * *
* @ returns { Branch [ ] }
* @ deprecated use getParentBranches ( ) instead
* /
2021-04-25 22:02:32 +02:00
getBranches ( ) {
return this . parentBranches ;
}
2021-10-24 14:53:45 +02:00
/** @returns {Note[]} */
2021-04-25 21:19:18 +02:00
getParentNotes ( ) {
return this . parents ;
}
2021-10-24 14:53:45 +02:00
/** @returns {Note[]} */
2021-04-25 22:02:32 +02:00
getChildNotes ( ) {
2021-04-25 21:19:18 +02:00
return this . children ;
}
2021-10-24 14:53:45 +02:00
/** @returns {boolean} */
2021-09-03 22:33:40 +02:00
hasChildren ( ) {
return this . children && this . children . length > 0 ;
}
2021-10-24 14:53:45 +02:00
/** @returns {Branch[]} */
2021-04-25 22:02:32 +02:00
getChildBranches ( ) {
2021-04-26 22:18:14 +02:00
return this . children . map ( childNote => this . becca . getBranchFromChildAndParent ( childNote . noteId , this . noteId ) ) ;
2021-04-25 22:02:32 +02:00
}
2021-04-25 20:00:42 +02:00
/ *
* Note content has quite special handling - it ' s not a separate entity , but a lazily loaded
* part of Note entity with it ' s 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 )
* /
/** @returns {*} */
getContent ( silentNotFoundError = false ) {
const row = sql . getRow ( ` SELECT content FROM note_contents WHERE noteId = ? ` , [ this . noteId ] ) ;
if ( ! row ) {
if ( silentNotFoundError ) {
return undefined ;
}
else {
throw new Error ( "Cannot find note content for noteId=" + this . noteId ) ;
}
}
let content = row . content ;
if ( this . isProtected ) {
if ( protectedSessionService . isProtectedSessionAvailable ( ) ) {
content = content === null ? null : protectedSessionService . decrypt ( content ) ;
}
else {
content = "" ;
}
}
if ( this . isStringNote ( ) ) {
return content === null
? ""
: content . toString ( "UTF-8" ) ;
}
else {
return content ;
}
}
/** @returns {{contentLength, dateModified, utcDateModified}} */
getContentMetadata ( ) {
return sql . getRow ( `
SELECT
LENGTH ( content ) AS contentLength ,
dateModified ,
utcDateModified
FROM note _contents
WHERE noteId = ? ` , [this.noteId]);
}
/** @returns {*} */
getJsonContent ( ) {
const content = this . getContent ( ) ;
if ( ! content || ! content . trim ( ) ) {
return null ;
}
return JSON . parse ( content ) ;
}
2021-08-07 21:21:30 +02:00
setContent ( content , ignoreMissingProtectedSession = false ) {
2021-04-25 20:00:42 +02:00
if ( content === null || content === undefined ) {
2022-04-19 23:36:21 +02:00
throw new Error ( ` Cannot set null content to note ' ${ this . noteId } ' ` ) ;
2021-04-25 20:00:42 +02:00
}
if ( this . isStringNote ( ) ) {
content = content . toString ( ) ;
}
else {
content = Buffer . isBuffer ( content ) ? content : Buffer . from ( content ) ;
}
const pojo = {
noteId : this . noteId ,
content : content ,
dateModified : dateUtils . localNowDateTime ( ) ,
utcDateModified : dateUtils . utcNowDateTime ( )
} ;
if ( this . isProtected ) {
if ( protectedSessionService . isProtectedSessionAvailable ( ) ) {
pojo . content = protectedSessionService . encrypt ( pojo . content ) ;
}
2021-08-07 21:21:30 +02:00
else if ( ! ignoreMissingProtectedSession ) {
2022-04-19 23:36:21 +02:00
throw new Error ( ` Cannot update content of noteId ' ${ this . noteId } ' since we're out of protected session. ` ) ;
2021-04-25 20:00:42 +02:00
}
}
sql . upsert ( "note_contents" , "noteId" , pojo ) ;
const hash = utils . hash ( this . noteId + "|" + pojo . content . toString ( ) ) ;
entityChangesService . addEntityChange ( {
entityName : 'note_contents' ,
entityId : this . noteId ,
hash : hash ,
isErased : false ,
2021-06-29 23:45:45 +02:00
utcDateChanged : pojo . utcDateModified ,
isSynced : true
2021-06-30 20:54:15 +02:00
} ) ;
2021-04-25 20:00:42 +02:00
}
setJsonContent ( content ) {
this . setContent ( JSON . stringify ( content , null , '\t' ) ) ;
}
/** @returns {boolean} true if this note is the root of the note tree. Root note has "root" noteId */
isRoot ( ) {
return this . noteId === 'root' ;
}
/** @returns {boolean} true if this note is of application/json content type */
isJson ( ) {
return this . mime === "application/json" ;
}
/** @returns {boolean} true if this note is JavaScript (code or attachment) */
isJavaScript ( ) {
return ( this . type === "code" || this . type === "file" )
&& ( this . mime . startsWith ( "application/javascript" )
|| this . mime === "application/x-javascript"
|| this . mime === "text/javascript" ) ;
}
/** @returns {boolean} true if this note is HTML */
isHtml ( ) {
return [ "code" , "file" , "render" ] . includes ( this . type )
&& this . mime === "text/html" ;
}
/** @returns {boolean} true if the note has string content (not binary) */
isStringNote ( ) {
return utils . isStringNote ( this . type , this . mime ) ;
}
/** @returns {string|null} JS script environment - either "frontend" or "backend" */
getScriptEnv ( ) {
if ( this . isHtml ( ) || ( this . isJavaScript ( ) && this . mime . endsWith ( 'env=frontend' ) ) ) {
return "frontend" ;
}
if ( this . type === 'render' ) {
return "frontend" ;
}
if ( this . isJavaScript ( ) && this . mime . endsWith ( 'env=backend' ) ) {
return "backend" ;
}
return null ;
}
2021-04-25 21:19:18 +02:00
/ * *
* @ param { string } [ type ] - ( optional ) attribute type to filter
* @ param { string } [ name ] - ( optional ) attribute name to filter
* @ returns { Attribute [ ] } all note ' s attributes , including inherited ones
* /
getAttributes ( type , name ) {
this . _ _getAttributes ( [ ] ) ;
if ( type && name ) {
return this . _ _attributeCache . filter ( attr => attr . type === type && attr . name === name ) ;
}
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 . slice ( ) ;
}
2020-06-04 23:58:31 +02:00
}
_ _getAttributes ( path ) {
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
if ( this . noteId !== 'root' ) {
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 = [ ] ;
for ( const ownedAttr of parentAttributes ) { // parentAttributes so we process also inherited templates
if ( ownedAttr . type === 'relation' && ownedAttr . name === 'template' ) {
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 ) {
2020-06-04 23:58:31 +02:00
templateAttributes . push ( ... templateNote . _ _getAttributes ( newPath ) ) ;
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
}
}
2020-05-16 23:12:29 +02:00
this . inheritableAttributeCache = [ ] ;
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 ) ;
}
}
}
2021-04-25 21:19:18 +02:00
return this . _ _attributeCache ;
2020-05-16 23:12:29 +02:00
}
2021-10-24 14:53:45 +02:00
/** @returns {Attribute[]} */
2020-06-04 23:58:31 +02:00
_ _getInheritableAttributes ( path ) {
if ( path . includes ( this . noteId ) ) {
return [ ] ;
}
2020-05-16 23:12:29 +02:00
if ( ! this . inheritableAttributeCache ) {
2020-06-04 23:58:31 +02:00
this . _ _getAttributes ( path ) ; // will refresh also this.inheritableAttributeCache
2020-05-16 23:12:29 +02:00
}
return this . inheritableAttributeCache ;
}
hasAttribute ( type , name ) {
2021-04-25 21:19:18 +02:00
return ! ! this . getAttributes ( ) . find ( attr => attr . type === type && attr . name === name ) ;
2020-05-16 23:12:29 +02:00
}
2020-12-15 15:09:00 +01:00
getAttributeCaseInsensitive ( type , name , value ) {
name = name . toLowerCase ( ) ;
value = value ? value . toLowerCase ( ) : null ;
2021-04-25 21:19:18 +02:00
return this . getAttributes ( ) . find (
2020-12-15 15:09:00 +01:00
attr => attr . type === type
&& attr . name . toLowerCase ( ) === name
&& ( ! value || attr . value . toLowerCase ( ) === value ) ) ;
}
2021-04-17 20:52:46 +02:00
getRelationTarget ( name ) {
2021-04-25 21:19:18 +02:00
const relation = this . getAttributes ( ) . find ( attr => attr . type === 'relation' && attr . name === name ) ;
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
/ * *
* @ param { string } name - label name
* @ returns { boolean } true if label exists ( including inherited )
* /
hasLabel ( name ) { return this . hasAttribute ( LABEL , name ) ; }
/ * *
* @ param { string } name - label name
* @ returns { boolean } true if label exists ( excluding inherited )
* /
hasOwnedLabel ( name ) { return this . hasOwnedAttribute ( LABEL , name ) ; }
/ * *
* @ param { string } name - relation name
* @ returns { boolean } true if relation exists ( including inherited )
* /
hasRelation ( name ) { return this . hasAttribute ( RELATION , name ) ; }
/ * *
* @ param { string } name - relation name
* @ returns { boolean } true if relation exists ( excluding inherited )
* /
hasOwnedRelation ( name ) { return this . hasOwnedAttribute ( RELATION , name ) ; }
/ * *
* @ param { string } name - label name
* @ returns { Attribute | null } label if it exists , null otherwise
* /
getLabel ( name ) { return this . getAttribute ( LABEL , name ) ; }
/ * *
* @ param { string } name - label name
* @ returns { Attribute | null } label if it exists , null otherwise
* /
getOwnedLabel ( name ) { return this . getOwnedAttribute ( LABEL , name ) ; }
/ * *
* @ param { string } name - relation name
* @ returns { Attribute | null } relation if it exists , null otherwise
* /
getRelation ( name ) { return this . getAttribute ( RELATION , name ) ; }
/ * *
* @ param { string } name - relation name
* @ returns { Attribute | null } relation if it exists , null otherwise
* /
getOwnedRelation ( name ) { return this . getOwnedAttribute ( RELATION , name ) ; }
/ * *
* @ param { string } name - label name
* @ returns { string | null } label value if label exists , null otherwise
* /
getLabelValue ( name ) { return this . getAttributeValue ( LABEL , name ) ; }
/ * *
* @ param { string } name - label name
* @ returns { string | null } label value if label exists , null otherwise
* /
getOwnedLabelValue ( name ) { return this . getOwnedAttributeValue ( LABEL , name ) ; }
/ * *
* @ param { string } name - relation name
* @ returns { string | null } relation value if relation exists , null otherwise
* /
getRelationValue ( name ) { return this . getAttributeValue ( RELATION , name ) ; }
/ * *
* @ param { string } name - relation name
* @ returns { string | null } relation value if relation exists , null otherwise
* /
getOwnedRelationValue ( name ) { return this . getOwnedAttributeValue ( RELATION , name ) ; }
/ * *
* @ param { string } type - attribute type ( label , relation , etc . )
* @ param { string } name - attribute name
* @ returns { boolean } true if note has an attribute with given type and name ( excluding inherited )
* /
hasOwnedAttribute ( type , name ) {
return ! ! this . getOwnedAttribute ( type , name ) ;
2021-01-23 21:00:59 +01:00
}
2021-04-17 20:52:46 +02:00
/ * *
* @ param { string } type - attribute type ( label , relation , etc . )
* @ param { string } name - attribute name
* @ returns { Attribute } attribute of given type and name . If there 's more such attributes, first is returned. Returns null if there' s no such attribute belonging to this note .
* /
getAttribute ( type , name ) {
const attributes = this . getAttributes ( ) ;
2020-05-25 00:25:47 +02:00
2021-04-17 20:52:46 +02:00
return attributes . find ( attr => attr . type === type && attr . name === name ) ;
2020-05-25 00:25:47 +02:00
}
2021-04-17 20:52:46 +02:00
/ * *
* @ param { string } type - attribute type ( label , relation , etc . )
* @ param { string } name - attribute name
* @ returns { string | null } attribute value of given type and name or null if no such attribute exists .
* /
getAttributeValue ( type , name ) {
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 ;
}
/ * *
* @ param { string } type - attribute type ( label , relation , etc . )
* @ param { string } name - attribute name
* @ returns { string | null } attribute value of given type and name or null if no such attribute exists .
* /
getOwnedAttributeValue ( type , name ) {
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
/ * *
* @ param { string } [ name ] - label name to filter
* @ returns { Attribute [ ] } all note ' s labels ( attributes with type label ) , including inherited ones
* /
getLabels ( name ) {
return this . getAttributes ( LABEL , name ) ;
}
/ * *
* @ param { string } [ name ] - label name to filter
* @ returns { string [ ] } all note ' s label values , including inherited ones
* /
getLabelValues ( name ) {
return this . getLabels ( name ) . map ( l => l . value ) ;
}
/ * *
* @ param { string } [ name ] - label name to filter
* @ returns { Attribute [ ] } all note ' s labels ( attributes with type label ) , excluding inherited ones
* /
getOwnedLabels ( name ) {
return this . getOwnedAttributes ( LABEL , name ) ;
}
/ * *
* @ param { string } [ name ] - label name to filter
* @ returns { string [ ] } all note ' s label values , excluding inherited ones
* /
getOwnedLabelValues ( name ) {
return this . getOwnedAttributes ( LABEL , name ) . map ( l => l . value ) ;
}
/ * *
* @ param { string } [ name ] - relation name to filter
* @ returns { Attribute [ ] } all note ' s relations ( attributes with type relation ) , including inherited ones
* /
getRelations ( name ) {
return this . getAttributes ( RELATION , name ) ;
}
/ * *
* @ param { string } [ name ] - relation name to filter
* @ returns { Attribute [ ] } all note ' s relations ( attributes with type relation ) , excluding inherited ones
* /
getOwnedRelations ( name ) {
return this . getOwnedAttributes ( RELATION , name ) ;
}
2021-04-25 22:02:32 +02:00
/ * *
* @ param { string } [ type ] - ( optional ) attribute type to filter
* @ param { string } [ name ] - ( optional ) attribute name to filter
* @ returns { Attribute [ ] } note ' s "owned" attributes - excluding inherited ones
* /
getOwnedAttributes ( type , name ) {
2021-09-02 22:27:35 +02:00
// it's a common mistake to include # or ~ into attribute name
2021-09-02 22:14:22 +02:00
if ( name && [ "#" , "~" ] . includes ( name [ 0 ] ) ) {
name = name . substr ( 1 ) ;
}
2021-04-25 22:02:32 +02:00
if ( type && name ) {
return this . ownedAttributes . filter ( attr => attr . type === type && attr . name === name ) ;
}
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 . slice ( ) ;
}
}
2021-05-08 23:31:20 +02:00
/ * *
* @ returns { Attribute } attribute belonging to this specific note ( excludes inherited attributes )
*
* This method can be significantly faster than the getAttribute ( )
* /
getOwnedAttribute ( type , name ) {
const attrs = this . getOwnedAttributes ( type , name ) ;
return attrs . length > 0 ? attrs [ 0 ] : null ;
}
2021-05-18 20:56:49 +02:00
get isArchived ( ) {
2020-05-16 23:12:29 +02:00
return this . hasAttribute ( 'label' , 'archived' ) ;
}
2021-05-17 22:35:36 +02:00
hasInheritableOwnedArchivedLabel ( ) {
2020-05-16 23:12:29 +02:00
return ! ! this . ownedAttributes . find ( attr => attr . type === 'label' && attr . name === 'archived' && attr . isInheritable ) ;
}
2021-02-24 22:38:26 +01:00
// will sort the parents so that non-search & non-archived are first and archived at the end
// this is done so that non-search & non-archived paths are always explored as first when looking for note path
2021-10-24 14:37:41 +02:00
sortParents ( ) {
2021-02-24 22:38:26 +01:00
this . parentBranches . sort ( ( a , b ) =>
a . branchId . startsWith ( 'virt-' )
2021-05-17 22:35:36 +02:00
|| a . parentNote . hasInheritableOwnedArchivedLabel ( ) ? 1 : - 1 ) ;
2021-02-24 22:38:26 +01:00
this . parents = this . parentBranches . map ( branch => branch . parentNote ) ;
2020-05-16 23:12:29 +02:00
}
/ * *
2020-09-07 00:05:01 +02:00
* This is used for :
* - fast searching
* - note similarity evaluation
*
* @ return { string } - returns flattened textual representation of note , prefixes and attributes
2020-05-16 23:12:29 +02:00
* /
2021-05-17 22:35:36 +02:00
getFlatText ( ) {
2020-05-16 23:12:29 +02:00
if ( ! this . flatTextCache ) {
2020-09-10 21:01:46 +02:00
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 ) {
2020-09-07 00:05:01 +02:00
this . flatTextCache += branch . prefix + ' ' ;
2020-05-16 23:12:29 +02:00
}
}
2020-08-06 23:55:17 +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
2020-09-07 00:05:01 +02:00
this . flatTextCache += ( attr . type === 'label' ? '#' : '~' ) + attr . name ;
2020-05-16 23:12:29 +02:00
if ( attr . value ) {
this . flatTextCache += '=' + attr . value ;
}
2020-09-07 00:05:01 +02:00
this . flatTextCache += ' ' ;
2020-05-16 23:12:29 +02:00
}
2021-09-28 10:02:25 +02:00
this . flatTextCache = utils . normalize ( this . flatTextCache ) ;
2020-05-16 23:12:29 +02:00
}
return this . flatTextCache ;
}
invalidateThisCache ( ) {
this . flatTextCache = null ;
2021-04-25 21:19:18 +02:00
this . _ _attributeCache = null ;
2020-05-16 23:12:29 +02:00
this . inheritableAttributeCache = null ;
2020-05-23 23:44:55 +02:00
this . ancestorCache = null ;
2020-05-16 23:12:29 +02:00
}
2021-04-17 20:52:46 +02:00
invalidateSubTree ( path = [ ] ) {
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 ) {
if ( targetRelation . name === 'template' ) {
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
}
}
}
}
invalidateSubtreeFlatText ( ) {
this . flatTextCache = null ;
for ( const childNote of this . children ) {
childNote . invalidateSubtreeFlatText ( ) ;
}
for ( const targetRelation of this . targetRelations ) {
if ( targetRelation . name === 'template' ) {
const note = targetRelation . note ;
if ( note ) {
note . invalidateSubtreeFlatText ( ) ;
}
}
}
}
2021-06-03 12:32:48 +02:00
getRelationDefinitions ( ) {
return this . getLabels ( )
. filter ( l => l . name . startsWith ( "relation:" ) ) ;
}
getLabelDefinitions ( ) {
return this . getLabels ( )
. filter ( l => l . name . startsWith ( "relation:" ) ) ;
}
2021-05-17 22:35:36 +02:00
isTemplate ( ) {
2020-05-16 23:12:29 +02:00
return ! ! this . targetRelations . find ( rel => rel . name === 'template' ) ;
}
2021-10-24 14:53:45 +02:00
/** @returns {Note[]} */
2021-05-17 22:35:36 +02:00
getSubtreeNotesIncludingTemplated ( ) {
2021-10-29 21:36:23 +02:00
const set = new Set ( ) ;
2020-05-16 23:12:29 +02:00
2021-10-29 21:36:23 +02:00
function inner ( note ) {
if ( set . has ( note ) ) {
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 ) {
if ( targetRelation . name === 'template' ) {
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
}
2021-10-24 14:53:45 +02:00
/** @returns {Note[]} */
2021-09-20 23:04:41 +02:00
getSubtreeNotes ( includeArchived = true ) {
2021-09-21 22:45:06 +02:00
const noteSet = new Set ( ) ;
2021-09-20 23:04:41 +02:00
2021-09-21 22:45:06 +02:00
function addSubtreeNotesInner ( note ) {
if ( ! includeArchived && note . isArchived ) {
return ;
}
2020-05-16 23:12:29 +02:00
2021-09-21 22:45:06 +02:00
noteSet . add ( note ) ;
for ( const childNote of note . children ) {
addSubtreeNotesInner ( childNote ) ;
}
2020-05-16 23:12:29 +02:00
}
2021-09-21 22:45:06 +02:00
addSubtreeNotesInner ( this ) ;
return Array . from ( noteSet ) ;
2020-05-16 23:12:29 +02:00
}
2021-10-24 14:53:45 +02:00
/** @returns {String[]} */
2021-10-21 22:52:52 +02:00
getSubtreeNoteIds ( includeArchived = true ) {
return this . getSubtreeNotes ( includeArchived ) . map ( note => note . noteId ) ;
2021-01-23 21:00:59 +01:00
}
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 ( ) {
2021-04-25 21:19:18 +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 ( ) {
return this . ownedAttributes . filter ( attr => attr . type === 'label' ) . length ;
}
2020-05-23 20:52:55 +02:00
get relationCount ( ) {
2021-04-25 21:19:18 +02:00
return this . getAttributes ( ) . filter ( attr => attr . type === 'relation' && ! attr . isAutoLink ( ) ) . length ;
2021-02-19 20:42:34 +01:00
}
get relationCountIncludingLinks ( ) {
2021-04-25 21:19:18 +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 ( ) {
return this . ownedAttributes . filter ( attr => attr . type === 'relation' && ! attr . isAutoLink ( ) ) . length ;
}
get ownedRelationCountIncludingLinks ( ) {
return this . ownedAttributes . filter ( attr => attr . type === 'relation' ) . length ;
}
get targetRelationCount ( ) {
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
}
2021-02-19 20:42:34 +01:00
get ownedAttributeCount ( ) {
2021-04-25 21:19:18 +02:00
return this . getAttributes ( ) . length ;
2021-02-19 20:42:34 +01:00
}
2021-10-24 14:53:45 +02:00
/** @returns {Note[]} */
2021-05-17 22:35:36 +02:00
getAncestors ( ) {
2020-05-23 23:44:55 +02:00
if ( ! this . ancestorCache ) {
const noteIds = new Set ( ) ;
this . ancestorCache = [ ] ;
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
}
2022-01-13 21:28:51 +01: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 ) ) {
this . ancestorCache . push ( ancestorNote ) ;
noteIds . add ( ancestorNote . noteId ) ;
}
}
}
}
return this . ancestorCache ;
}
2021-10-24 14:53:45 +02:00
/** @returns {boolean} */
hasAncestor ( ancestorNoteId ) {
for ( const ancestorNote of this . getAncestors ( ) ) {
if ( ancestorNote . noteId === ancestorNoteId ) {
return true ;
}
}
return false ;
}
2021-05-08 23:31:20 +02:00
getTargetRelations ( ) {
return this . targetRelations ;
}
2021-10-24 14:53:45 +02:00
/ * * @ r e t u r n s { N o t e [ ] } - 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
2020-05-16 23:12:29 +02:00
* in effect returns notes which are influenced by note ' s non - inheritable attributes * /
2021-05-17 22:35:36 +02:00
getTemplatedNotes ( ) {
2020-05-16 23:12:29 +02:00
const arr = [ this ] ;
for ( const targetRelation of this . targetRelations ) {
if ( targetRelation . name === 'template' ) {
const note = targetRelation . note ;
if ( note ) {
arr . push ( note ) ;
}
}
}
return arr ;
}
2020-05-17 09:48:24 +02:00
2021-01-26 23:25:18 +01:00
getDistanceToAncestor ( ancestorNoteId ) {
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 ;
}
2021-04-26 22:00:55 +02:00
getNoteRevisions ( ) {
return sql . getRows ( "SELECT * FROM note_revisions WHERE noteId = ?" , [ this . noteId ] )
. map ( row => new NoteRevision ( row ) ) ;
}
2021-05-09 20:46:32 +02:00
/ * *
* @ return { string [ ] [ ] } - array of notePaths ( each represented by array of noteIds constituting the particular note path )
* /
getAllNotePaths ( ) {
if ( this . noteId === 'root' ) {
return [ [ 'root' ] ] ;
}
const notePaths = [ ] ;
for ( const parentNote of this . getParentNotes ( ) ) {
for ( const parentPath of parentNote . getAllNotePaths ( ) ) {
parentPath . push ( this . noteId ) ;
notePaths . push ( parentPath ) ;
}
}
return notePaths ;
}
/ * *
* @ param ancestorNoteId
* @ return { boolean } - true if ancestorNoteId occurs in at least one of the note ' s paths
* /
isDescendantOfNote ( ancestorNoteId ) {
const notePaths = this . getAllNotePaths ( ) ;
return notePaths . some ( path => path . includes ( ancestorNoteId ) ) ;
}
2021-05-11 22:00:16 +02:00
/ * *
* Update 's given attribute' s value or creates it if it doesn ' t exist
*
* @ param { string } type - attribute type ( label , relation , etc . )
* @ param { string } name - attribute name
* @ param { string } [ value ] - attribute value ( optional )
* /
setAttribute ( type , name , value ) {
const attributes = this . getOwnedAttributes ( ) ;
const attr = attributes . find ( attr => attr . type === type && attr . name === name ) ;
2021-08-12 20:42:35 +02:00
value = value !== null && value !== undefined ? value . toString ( ) : "" ;
2021-05-11 22:00:16 +02:00
if ( attr ) {
if ( attr . value !== value ) {
attr . value = value ;
attr . save ( ) ;
}
}
else {
2022-01-10 17:09:20 +01:00
const Attribute = require ( "./attribute" ) ;
2021-05-11 22:00:16 +02:00
new Attribute ( {
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 ( ) ;
}
}
/ * *
* Removes given attribute name - value pair if it exists .
*
* @ param { string } type - attribute type ( label , relation , etc . )
* @ param { string } name - attribute name
* @ param { string } [ value ] - attribute value ( optional )
* /
removeAttribute ( type , name , value ) {
const attributes = this . getOwnedAttributes ( ) ;
for ( const attribute of attributes ) {
if ( attribute . type === type && attribute . name === name && ( value === undefined || value === attribute . value ) ) {
attribute . markAsDeleted ( ) ;
}
}
}
/ * *
* @ return { Attribute }
* /
addAttribute ( type , name , value = "" , isInheritable = false , position = 1000 ) {
2022-01-10 17:09:20 +01:00
const Attribute = require ( "./attribute" ) ;
2021-05-11 22:00:16 +02:00
return new Attribute ( {
noteId : this . noteId ,
type : type ,
name : name ,
value : value ,
isInheritable : isInheritable ,
position : position
} ) . save ( ) ;
}
addLabel ( name , value = "" , isInheritable = false ) {
return this . addAttribute ( LABEL , name , value , isInheritable ) ;
}
addRelation ( name , targetNoteId , isInheritable = false ) {
return this . addAttribute ( RELATION , name , targetNoteId , isInheritable ) ;
}
/ * *
* Based on enabled , attribute is either set or removed .
*
* @ param { string } type - attribute type ( 'relation' , 'label' etc . )
* @ param { boolean } enabled - toggle On or Off
* @ param { string } name - attribute name
* @ param { string } [ value ] - attribute value ( optional )
* /
toggleAttribute ( type , enabled , name , value ) {
if ( enabled ) {
this . setAttribute ( type , name , value ) ;
}
else {
this . removeAttribute ( type , name , value ) ;
}
}
/ * *
* Based on enabled , label is either set or removed .
*
* @ param { boolean } enabled - toggle On or Off
* @ param { string } name - label name
* @ param { string } [ value ] - label value ( optional )
* /
toggleLabel ( enabled , name , value ) { return this . toggleAttribute ( LABEL , enabled , name , value ) ; }
/ * *
* Based on enabled , relation is either set or removed .
*
* @ param { boolean } enabled - toggle On or Off
* @ param { string } name - relation name
* @ param { string } [ value ] - relation value ( noteId )
* /
toggleRelation ( enabled , name , value ) { return this . toggleAttribute ( RELATION , enabled , name , value ) ; }
/ * *
* Update 's given label' s value or creates it if it doesn ' t exist
*
* @ param { string } name - label name
* @ param { string } [ value ] - label value
* /
setLabel ( name , value ) { return this . setAttribute ( LABEL , name , value ) ; }
/ * *
* Update 's given relation' s value or creates it if it doesn ' t exist
*
* @ param { string } name - relation name
* @ param { string } value - relation value ( noteId )
* /
setRelation ( name , value ) { return this . setAttribute ( RELATION , name , value ) ; }
/ * *
* Remove label name - value pair , if it exists .
*
* @ param { string } name - label name
* @ param { string } [ value ] - label value
* /
removeLabel ( name , value ) { return this . removeAttribute ( LABEL , name , value ) ; }
/ * *
* Remove relation name - value pair , if it exists .
*
* @ param { string } name - relation name
* @ param { string } [ value ] - relation value ( noteId )
* /
removeRelation ( name , value ) { return this . removeAttribute ( RELATION , name , value ) ; }
2021-06-06 11:01:10 +02:00
searchNotesInSubtree ( searchString ) {
const searchService = require ( "../../services/search/services/search" ) ;
return searchService . searchNotes ( searchString ) ;
}
searchNoteInSubtree ( searchString ) {
return this . searchNotesInSubtree ( searchString ) [ 0 ] ;
}
cloneTo ( parentNoteId ) {
const cloningService = require ( "../../services/cloning" ) ;
const branch = this . becca . getNote ( parentNoteId ) . getParentBranches ( ) [ 0 ] ;
2021-12-27 23:39:46 +01:00
return cloningService . cloneNoteToBranch ( this . noteId , branch . branchId ) ;
2021-06-06 11:01:10 +02:00
}
2022-04-19 23:06:46 +02:00
/ * *
* ( Soft ) delete a note and all its descendants .
*
* @ param { string } [ deleteId ] - optional delete identified
* @ param { TaskContext } [ taskContext ]
* /
deleteNote ( deleteId , taskContext ) {
if ( ! deleteId ) {
deleteId = utils . randomString ( 10 ) ;
}
if ( ! taskContext ) {
taskContext = new TaskContext ( 'no-progress-reporting' ) ;
}
2022-06-05 14:58:19 +02:00
// needs to be run before branches and attributes are deleted and thus attached relations disappear
2022-06-06 21:59:44 +02:00
const handlers = require ( "../../services/handlers" ) ;
2022-06-05 14:58:19 +02:00
handlers . runAttachedRelations ( this , 'runOnNoteDeletion' , this ) ;
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 {
this . title = protectedSessionService . decryptString ( this . title ) ;
2022-03-20 22:23:48 +01:00
this . flatTextCache = null ;
2020-05-17 09:48:24 +02:00
2021-04-03 21:48:16 +02:00
this . isDecrypted = true ;
}
catch ( e ) {
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
2021-10-31 21:03:26 +01:00
get isDeleted ( ) {
return ! ( this . noteId in this . becca . notes ) ;
}
2022-06-02 17:25:58 +02:00
/ * *
* @ return { NoteRevision | null }
* /
saveNoteRevision ( ) {
const content = this . getContent ( ) ;
if ( ! content || ( Buffer . isBuffer ( content ) && content . byteLength === 0 ) ) {
return null ;
}
const contentMetadata = this . getContentMetadata ( ) ;
const noteRevision = new NoteRevision ( {
noteId : this . noteId ,
// title and text should be decrypted now
title : this . title ,
type : this . type ,
mime : this . mime ,
isProtected : false , // will be fixed in the protectNoteRevisions() call
utcDateLastEdited : this . utcDateModified > contentMetadata . utcDateModified
? this . utcDateModified
: contentMetadata . utcDateModified ,
utcDateCreated : dateUtils . utcNowDateTime ( ) ,
utcDateModified : dateUtils . utcNowDateTime ( ) ,
dateLastEdited : this . dateModified > contentMetadata . dateModified
? this . dateModified
: contentMetadata . dateModified ,
dateCreated : dateUtils . localNowDateTime ( )
} ) . save ( ) ;
noteRevision . setContent ( content ) ;
return noteRevision ;
}
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 ( ) ;
}
2021-04-25 21:19:18 +02:00
getPojo ( ) {
2021-09-08 22:35:32 +02:00
return {
2021-04-25 20:00:42 +02:00
noteId : this . noteId ,
title : this . title ,
isProtected : this . isProtected ,
type : this . type ,
mime : this . mime ,
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 ) {
if ( this . isDecrypted ) {
pojo . title = protectedSessionService . encrypt ( pojo . title ) ;
}
else {
// 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
}
2020-05-16 23:12:29 +02:00
}
2020-05-17 09:48:24 +02:00
module . exports = Note ;