2018-01-29 12:16:50 +08:00
"use strict" ;
2018-01-30 07:34:59 +08:00
const Entity = require ( './entity' ) ;
2018-08-13 16:59:31 +08:00
const Attribute = require ( './attribute' ) ;
2018-04-20 12:12:01 +08:00
const protectedSessionService = require ( '../services/protected_session' ) ;
2018-03-31 22:51:37 +08:00
const repository = require ( '../services/repository' ) ;
2018-11-26 05:09:52 +08:00
const sql = require ( '../services/sql' ) ;
2019-03-27 05:24:04 +08:00
const utils = require ( '../services/utils' ) ;
2018-04-03 08:46:46 +08:00
const dateUtils = require ( '../services/date_utils' ) ;
2019-03-27 05:24:04 +08:00
const syncTableService = require ( '../services/sync_table' ) ;
2018-01-29 12:16:50 +08:00
2018-08-16 04:06:49 +08:00
const LABEL = 'label' ;
2018-11-13 19:50:08 +08:00
const LABEL _DEFINITION = 'label-definition' ;
2018-08-16 04:06:49 +08:00
const RELATION = 'relation' ;
2018-11-13 19:50:08 +08:00
const RELATION _DEFINITION = 'relation-definition' ;
2018-08-16 04:06:49 +08:00
2019-02-24 03:33:27 +08:00
const STRING _MIME _TYPES = [ "application/x-javascript" ] ;
2018-08-23 05:37:06 +08:00
/ * *
* This represents a Note which is a central object in the Trilium Notes project .
*
* @ property { string } noteId - primary key
* @ property { string } type - one of "text" , "code" , "file" or "render"
* @ property { string } mime - MIME type , e . g . "text/html"
* @ property { string } title - note title
* @ property { boolean } isProtected - true if note is protected
* @ property { boolean } isDeleted - true if note is deleted
2019-03-14 05:43:59 +08:00
* @ property { string } dateCreated - local date time ( with offset )
* @ property { string } dateModified - local date time ( with offset )
2019-03-13 03:58:31 +08:00
* @ property { string } utcDateCreated
* @ property { string } utcDateModified
2018-08-23 05:37:06 +08:00
*
* @ extends Entity
* /
2018-01-30 07:34:59 +08:00
class Note extends Entity {
2018-08-17 05:00:04 +08:00
static get entityName ( ) { return "notes" ; }
2018-01-31 09:12:19 +08:00
static get primaryKeyName ( ) { return "noteId" ; }
2019-02-07 03:19:25 +08:00
static get hashedProperties ( ) { return [ "noteId" , "title" , "type" , "isProtected" , "isDeleted" ] ; }
2018-01-30 12:35:36 +08:00
2018-08-23 05:37:06 +08:00
/ * *
* @ param row - object containing database row from "notes" table
* /
2018-03-31 22:51:37 +08:00
constructor ( row ) {
super ( row ) ;
2018-01-30 09:57:55 +08:00
2018-08-07 17:38:00 +08:00
this . isProtected = ! ! this . isProtected ;
2018-10-31 05:18:20 +08:00
/ * t r u e i f c o n t e n t ( m e a n i n g a n y k i n d o f p o t e n t i a l l y e n c r y p t e d c o n t e n t ) i s e i t h e r n o t e n c r y p t e d
* or encrypted , but with available protected session ( so effectively decrypted ) * /
this . isContentAvailable = true ;
2018-08-07 17:38:00 +08:00
2018-04-09 00:27:10 +08:00
// check if there's noteId, otherwise this is a new entity which wasn't encrypted yet
if ( this . isProtected && this . noteId ) {
2018-10-31 05:18:20 +08:00
this . isContentAvailable = protectedSessionService . isProtectedSessionAvailable ( ) ;
2019-01-13 07:24:51 +08:00
if ( this . isContentAvailable ) {
protectedSessionService . decryptNote ( this ) ;
}
else {
this . title = "[protected]" ;
}
2018-01-30 12:35:36 +08:00
}
2018-01-30 09:57:55 +08:00
}
2019-03-28 04:16:13 +08: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 sync rows )
* - 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 )
* /
2019-03-27 05:24:04 +08:00
/** @returns {Promise<*>} */
2019-04-14 01:34:19 +08:00
async getContent ( silentNotFoundError = false ) {
2019-03-27 05:24:04 +08:00
if ( this . content === undefined ) {
2019-03-28 04:27:29 +08:00
const res = await sql . getRow ( ` SELECT content, hash FROM note_contents WHERE noteId = ? ` , [ this . noteId ] ) ;
2019-04-14 01:34:19 +08:00
if ( ! res ) {
if ( silentNotFoundError ) {
return undefined ;
}
else {
throw new Error ( "Cannot find note content for noteId=" + this . noteId ) ;
}
}
2019-03-28 04:27:29 +08:00
this . content = res . content ;
2019-02-09 04:01:26 +08:00
2019-03-27 05:24:04 +08:00
if ( this . isProtected ) {
if ( this . isContentAvailable ) {
protectedSessionService . decryptNoteContent ( this ) ;
}
else {
this . content = "" ;
}
2019-02-09 04:01:26 +08:00
}
2019-02-11 05:45:44 +08:00
if ( this . isStringNote ( ) ) {
2019-03-27 05:24:04 +08:00
this . content = this . content === null
? ""
: this . content . toString ( "UTF-8" ) ;
2019-02-11 05:45:44 +08:00
}
2018-04-08 20:31:19 +08:00
}
2019-02-07 03:19:25 +08:00
2019-03-27 05:24:04 +08:00
return this . content ;
2019-02-07 04:29:23 +08:00
}
2019-02-08 05:16:40 +08:00
/** @returns {Promise<*>} */
async getJsonContent ( ) {
const content = await this . getContent ( ) ;
return JSON . parse ( content ) ;
}
2019-02-09 04:01:26 +08:00
/** @returns {Promise} */
async setContent ( content ) {
2019-04-12 03:44:35 +08:00
// force updating note itself so that dateChanged is represented correctly even for the content
this . forcedChange = true ;
await this . save ( ) ;
2019-03-27 05:24:04 +08:00
this . content = content ;
const pojo = {
noteId : this . noteId ,
content : content ,
utcDateModified : dateUtils . utcNowDateTime ( ) ,
hash : utils . hash ( this . noteId + "|" + content )
} ;
if ( this . isProtected ) {
if ( this . isContentAvailable ) {
protectedSessionService . encryptNoteContent ( pojo ) ;
}
else {
throw new Error ( ` Cannot update content of noteId= ${ this . noteId } since we're out of protected session. ` ) ;
}
2019-02-09 04:01:26 +08:00
}
2019-03-27 05:24:04 +08:00
await sql . upsert ( "note_contents" , "noteId" , pojo ) ;
await syncTableService . addNoteContentSync ( this . noteId ) ;
2019-02-09 04:01:26 +08:00
}
/** @returns {Promise} */
async setJsonContent ( content ) {
2019-03-06 03:44:50 +08:00
await this . setContent ( JSON . stringify ( content , null , '\t' ) ) ;
2019-02-09 04:01:26 +08:00
}
2018-08-23 05:37:06 +08:00
/** @returns {boolean} true if this note is the root of the note tree. Root note has "root" noteId */
2018-08-01 15:26:02 +08:00
isRoot ( ) {
return this . noteId === 'root' ;
}
2018-08-23 05:37:06 +08:00
/** @returns {boolean} true if this note is of application/json content type */
2018-01-30 12:17:44 +08:00
isJson ( ) {
2018-03-26 11:25:17 +08:00
return this . mime === "application/json" ;
2018-01-30 12:17:44 +08:00
}
2018-08-23 05:37:06 +08:00
/** @returns {boolean} true if this note is JavaScript (code or attachment) */
2018-02-18 23:47:02 +08:00
isJavaScript ( ) {
2018-03-02 11:30:06 +08:00
return ( this . type === "code" || this . type === "file" )
2018-12-01 05:28:30 +08:00
&& ( this . mime . startsWith ( "application/javascript" )
|| this . mime === "application/x-javascript"
|| this . mime === "text/javascript" ) ;
2018-02-18 23:47:02 +08:00
}
2018-08-23 05:37:06 +08:00
/** @returns {boolean} true if this note is HTML */
2018-03-05 10:05:14 +08:00
isHtml ( ) {
2018-05-27 07:27:47 +08:00
return ( this . type === "code" || this . type === "file" || this . type === "render" ) && this . mime === "text/html" ;
2018-03-05 10:05:14 +08:00
}
2019-02-11 05:45:44 +08:00
/** @returns {boolean} true if the note has string content (not binary) */
isStringNote ( ) {
2019-02-24 03:33:27 +08:00
return [ "text" , "code" , "relation-map" , "search" ] . includes ( this . type )
|| this . mime . startsWith ( 'text/' )
|| STRING _MIME _TYPES . includes ( this . mime ) ;
2019-02-11 05:45:44 +08:00
}
2018-08-23 05:37:06 +08:00
/** @returns {string} JS script environment - either "frontend" or "backend" */
2018-03-06 12:09:36 +08:00
getScriptEnv ( ) {
if ( this . isHtml ( ) || ( this . isJavaScript ( ) && this . mime . endsWith ( 'env=frontend' ) ) ) {
return "frontend" ;
}
2018-03-07 13:17:18 +08:00
if ( this . type === 'render' ) {
return "frontend" ;
}
2018-03-06 12:09:36 +08:00
if ( this . isJavaScript ( ) && this . mime . endsWith ( 'env=backend' ) ) {
return "backend" ;
}
return null ;
}
2018-08-23 05:37:06 +08:00
/ * *
2018-08-23 21:33:19 +08:00
* @ returns { Promise < Attribute [ ] > } attributes belonging to this specific note ( excludes inherited attributes )
2019-09-08 03:40:18 +08:00
*
* This method can be significantly faster than the getAttributes ( )
2018-08-23 05:37:06 +08:00
* /
2019-09-08 02:43:59 +08:00
async getOwnedAttributes ( type , name ) {
let query = ` SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ? ` ;
const params = [ this . noteId ] ;
if ( type ) {
query += ` AND type = ? ` ;
params . push ( type ) ;
}
if ( name ) {
query += ` AND name = ? ` ;
params . push ( name ) ;
}
return await repository . getEntities ( query , params ) ;
}
/ * *
* @ returns { Promise < Attribute > } attribute belonging to this specific note ( excludes inherited attributes )
2019-09-08 03:40:18 +08:00
*
* This method can be significantly faster than the getAttribute ( )
2019-09-08 02:43:59 +08:00
* /
async getOwnedAttribute ( type , name ) {
const attrs = await this . getOwnedAttributes ( type , name ) ;
return attrs . length > 0 ? attrs [ 0 ] : null ;
2018-01-29 12:16:50 +08:00
}
2018-11-01 17:23:21 +08:00
/ * *
* @ returns { Promise < Attribute [ ] > } relations targetting this specific note
* /
async getTargetRelations ( ) {
return await repository . getEntities ( "SELECT * FROM attributes WHERE type = 'relation' AND isDeleted = 0 AND value = ?" , [ this . noteId ] ) ;
}
2018-09-01 19:18:55 +08:00
/ * *
* @ param { string } [ name ] - attribute name to filter
* @ returns { Promise < Attribute [ ] > } all note ' s attributes , including inherited ones
* /
async getAttributes ( name ) {
2018-08-13 23:16:06 +08:00
if ( ! this . _ _attributeCache ) {
await this . loadAttributesToCache ( ) ;
}
2018-09-01 19:18:55 +08:00
if ( name ) {
return this . _ _attributeCache . filter ( attr => attr . name === name ) ;
}
else {
return this . _ _attributeCache ;
}
2018-08-13 23:16:06 +08:00
}
2018-09-01 19:18:55 +08:00
/ * *
* @ param { string } [ name ] - label name to filter
* @ returns { Promise < Attribute [ ] > } all note ' s labels ( attributes with type label ) , including inherited ones
* /
async getLabels ( name ) {
return ( await this . getAttributes ( name ) ) . filter ( attr => attr . type === LABEL ) ;
2018-08-16 04:06:49 +08:00
}
2018-11-13 19:50:08 +08:00
/ * *
* @ param { string } [ name ] - label name to filter
* @ returns { Promise < Attribute [ ] > } all note ' s label definitions , including inherited ones
* /
async getLabelDefinitions ( name ) {
return ( await this . getAttributes ( name ) ) . filter ( attr => attr . type === LABEL _DEFINITION ) ;
}
2018-09-01 19:18:55 +08:00
/ * *
* @ param { string } [ name ] - relation name to filter
* @ returns { Promise < Attribute [ ] > } all note ' s relations ( attributes with type relation ) , including inherited ones
* /
async getRelations ( name ) {
return ( await this . getAttributes ( name ) ) . filter ( attr => attr . type === RELATION ) ;
2018-08-16 04:06:49 +08:00
}
2019-08-17 17:28:36 +08:00
/ * *
* @ param { string } [ name ] - relation name to filter
* @ returns { Promise < Note [ ] > }
* /
async getRelationTargets ( name ) {
const relations = await this . getRelations ( name ) ;
const targets = [ ] ;
for ( const relation of relations ) {
targets . push ( await relation . getTargetNote ( ) ) ;
}
return targets ;
}
2018-11-13 19:50:08 +08:00
/ * *
* @ param { string } [ name ] - relation name to filter
* @ returns { Promise < Attribute [ ] > } all note ' s relation definitions including inherited ones
* /
async getRelationDefinitions ( name ) {
return ( await this . getAttributes ( name ) ) . filter ( attr => attr . type === RELATION _DEFINITION ) ;
}
2018-08-23 05:37:06 +08:00
/ * *
* Clear note ' s attributes cache to force fresh reload for next attribute request .
* Cache is note instance scoped .
* /
2018-08-13 23:16:06 +08:00
invalidateAttributeCache ( ) {
this . _ _attributeCache = null ;
}
2018-08-23 05:37:06 +08:00
/** @returns {Promise<void>} */
2018-08-13 23:16:06 +08:00
async loadAttributesToCache ( ) {
2018-08-07 19:33:10 +08:00
const attributes = await repository . getEntities ( `
2018-08-13 23:05:16 +08:00
WITH RECURSIVE
tree ( noteId , level ) AS (
SELECT ? , 0
UNION
SELECT branches . parentNoteId , tree . level + 1 FROM branches
JOIN tree ON branches . noteId = tree . noteId
JOIN notes ON notes . noteId = branches . parentNoteId
WHERE notes . isDeleted = 0
AND branches . isDeleted = 0
) ,
treeWithAttrs ( noteId , level ) AS (
SELECT * FROM tree
UNION
2019-09-07 17:00:15 +08:00
SELECT attributes . value , treeWithAttrs . level FROM attributes
2018-08-13 23:05:16 +08:00
JOIN treeWithAttrs ON treeWithAttrs . noteId = attributes . noteId
WHERE attributes . isDeleted = 0
AND attributes . type = 'relation'
2018-08-21 18:52:11 +08:00
AND attributes . name = 'template'
2019-09-07 17:00:15 +08:00
AND ( treeWithAttrs . level = 0 OR attributes . isInheritable = 1 )
2018-08-13 23:05:16 +08:00
)
SELECT attributes . * FROM attributes JOIN treeWithAttrs ON attributes . noteId = treeWithAttrs . noteId
2019-09-07 17:00:15 +08:00
WHERE attributes . isDeleted = 0 AND ( attributes . isInheritable = 1 OR treeWithAttrs . level = 0 )
ORDER BY level , noteId , position ` , [this.noteId]);
2018-08-07 19:33:10 +08:00
// attributes are ordered so that "closest" attributes are first
// we order by noteId so that attributes from same note stay together. Actual noteId ordering doesn't matter.
const filteredAttributes = attributes . filter ( ( attr , index ) => {
if ( attr . isDefinition ( ) ) {
const firstDefinitionIndex = attributes . findIndex ( el => el . type === attr . type && el . name === attr . name ) ;
// keep only if this element is the first definition for this type & name
return firstDefinitionIndex === index ;
}
else {
const definitionAttr = attributes . find ( el => el . type === attr . type + '-definition' && el . name === attr . name ) ;
if ( ! definitionAttr ) {
return true ;
}
const definition = definitionAttr . value ;
if ( definition . multiplicityType === 'multivalue' ) {
return true ;
}
else {
const firstAttrIndex = attributes . findIndex ( el => el . type === attr . type && el . name === attr . name ) ;
// in case of single-valued attribute we'll keep it only if it's first (closest)
return firstAttrIndex === index ;
}
}
} ) ;
for ( const attr of filteredAttributes ) {
attr . isOwned = attr . noteId === this . noteId ;
2018-03-03 22:11:41 +08:00
}
2018-08-13 23:16:06 +08:00
this . _ _attributeCache = filteredAttributes ;
2018-03-03 22:11:41 +08:00
}
2018-08-23 05:37:06 +08:00
/ * *
* @ param { string } type - attribute type ( label , relation , etc . )
* @ param { string } name - attribute name
* @ returns { Promise < boolean > } true if note has an attribute with given type and name ( including inherited )
* /
2018-08-16 04:06:49 +08:00
async hasAttribute ( type , name ) {
return ! ! await this . getAttribute ( type , name ) ;
2018-03-05 11:09:51 +08:00
}
2018-08-23 05:37:06 +08:00
/ * *
* @ param { string } type - attribute type ( label , relation , etc . )
* @ param { string } name - attribute name
* @ returns { Promise < 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 .
* /
2018-08-16 04:06:49 +08:00
async getAttribute ( type , name ) {
2018-08-07 19:33:10 +08:00
const attributes = await this . getAttributes ( ) ;
2018-08-16 04:06:49 +08:00
return attributes . find ( attr => attr . type === type && attr . name === name ) ;
2018-01-30 06:41:59 +08:00
}
2018-08-23 05:37:06 +08:00
/ * *
* @ param { string } type - attribute type ( label , relation , etc . )
* @ param { string } name - attribute name
* @ returns { Promise < string > } attribute value of given type and name or null if no such attribute exists .
* /
2018-08-16 04:06:49 +08:00
async getAttributeValue ( type , name ) {
const attr = await this . getAttribute ( type , name ) ;
2018-08-13 16:59:31 +08:00
2018-08-16 04:06:49 +08:00
return attr ? attr . value : null ;
2018-08-13 16:59:31 +08:00
}
2018-08-23 05:37:06 +08:00
/ * *
* 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 )
* @ returns { Promise < void > }
* /
async toggleAttribute ( type , enabled , name , value ) {
2018-08-13 19:53:08 +08:00
if ( enabled ) {
2018-08-16 04:06:49 +08:00
await this . setAttribute ( type , name , value ) ;
2018-08-13 19:53:08 +08:00
}
else {
2018-08-16 04:06:49 +08:00
await this . removeAttribute ( type , name , value ) ;
2018-08-13 19:53:08 +08:00
}
}
2018-08-23 05:37:06 +08:00
/ * *
* Creates given attribute name - value pair if it doesn ' t exist .
*
* @ param { string } type - attribute type ( label , relation , etc . )
* @ param { string } name - attribute name
* @ param { string } [ value ] - attribute value ( optional )
* @ returns { Promise < void > }
* /
async setAttribute ( type , name , value ) {
2018-08-13 19:53:08 +08:00
const attributes = await this . getOwnedAttributes ( ) ;
2018-08-23 05:37:06 +08:00
let attr = attributes . find ( attr => attr . type === type && ( value === undefined || attr . value === value ) ) ;
2018-08-13 16:59:31 +08:00
2018-08-16 04:06:49 +08:00
if ( ! attr ) {
attr = new Attribute ( {
2018-08-13 16:59:31 +08:00
noteId : this . noteId ,
2018-08-16 04:06:49 +08:00
type : type ,
2018-08-13 19:53:08 +08:00
name : name ,
2018-08-23 05:37:06 +08:00
value : value !== undefined ? value : ""
2018-08-13 16:59:31 +08:00
} ) ;
2018-08-16 04:06:49 +08:00
await attr . save ( ) ;
2018-08-13 23:16:06 +08:00
this . invalidateAttributeCache ( ) ;
2018-08-13 19:53:08 +08:00
}
2018-08-13 16:59:31 +08:00
}
2018-08-23 05:37:06 +08:00
/ * *
* 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 )
* @ returns { Promise < void > }
* /
async removeAttribute ( type , name , value ) {
2018-08-13 19:53:08 +08:00
const attributes = await this . getOwnedAttributes ( ) ;
2018-08-13 16:59:31 +08:00
2018-08-13 19:53:08 +08:00
for ( const attribute of attributes ) {
2018-08-23 05:37:06 +08:00
if ( attribute . type === type && ( value === undefined || value === attribute . value ) ) {
2018-08-13 19:53:08 +08:00
attribute . isDeleted = true ;
await attribute . save ( ) ;
2018-08-13 23:16:06 +08:00
this . invalidateAttributeCache ( ) ;
2018-08-13 19:53:08 +08:00
}
2018-08-13 16:59:31 +08:00
}
}
2018-08-23 05:37:06 +08:00
/ * *
* @ param { string } name - label name
* @ returns { Promise < boolean > } true if label exists ( including inherited )
* /
2018-08-16 04:06:49 +08:00
async hasLabel ( name ) { return await this . hasAttribute ( LABEL , name ) ; }
2018-08-23 05:37:06 +08:00
/ * *
* @ param { string } name - relation name
* @ returns { Promise < boolean > } true if relation exists ( including inherited )
* /
2018-08-16 04:06:49 +08:00
async hasRelation ( name ) { return await this . hasAttribute ( RELATION , name ) ; }
2018-08-23 05:37:06 +08:00
/ * *
* @ param { string } name - label name
* @ returns { Promise < Attribute > } label if it exists , null otherwise
* /
2018-08-16 04:06:49 +08:00
async getLabel ( name ) { return await this . getAttribute ( LABEL , name ) ; }
2018-08-23 05:37:06 +08:00
/ * *
* @ param { string } name - relation name
* @ returns { Promise < Attribute > } relation if it exists , null otherwise
* /
2018-08-16 04:06:49 +08:00
async getRelation ( name ) { return await this . getAttribute ( RELATION , name ) ; }
2018-08-23 05:37:06 +08:00
/ * *
* @ param { string } name - label name
* @ returns { Promise < string > } label value if label exists , null otherwise
* /
2018-08-16 04:06:49 +08:00
async getLabelValue ( name ) { return await this . getAttributeValue ( LABEL , name ) ; }
2018-08-23 05:37:06 +08:00
/ * *
* @ param { string } name - relation name
* @ returns { Promise < string > } relation value if relation exists , null otherwise
* /
async getRelationValue ( name ) { return await this . getAttributeValue ( RELATION , name ) ; }
2018-08-16 04:06:49 +08:00
2018-12-23 05:16:32 +08:00
/ * *
* @ param { string } name
* @ returns { Promise < Note > | null } target note of the relation or null ( if target is empty or note was not found )
* /
async getRelationTarget ( name ) {
const relation = await this . getRelation ( name ) ;
return relation ? await repository . getNote ( relation . value ) : null ;
}
2018-08-23 05:37:06 +08:00
/ * *
* 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 )
* @ returns { Promise < void > }
* /
async toggleLabel ( enabled , name , value ) { return await 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 )
* @ returns { Promise < void > }
* /
async toggleRelation ( enabled , name , value ) { return await this . toggleAttribute ( RELATION , enabled , name , value ) ; }
/ * *
* Create label name - value pair if it doesn ' t exist yet .
*
* @ param { string } name - label name
* @ param { string } [ value ] - label value
* @ returns { Promise < void > }
* /
async setLabel ( name , value ) { return await this . setAttribute ( LABEL , name , value ) ; }
/ * *
* Create relation name - value pair if it doesn ' t exist yet .
*
* @ param { string } name - relation name
* @ param { string } [ value ] - relation value ( noteId )
* @ returns { Promise < void > }
* /
async setRelation ( name , value ) { return await this . setAttribute ( RELATION , name , value ) ; }
/ * *
* Remove label name - value pair , if it exists .
*
* @ param { string } name - label name
* @ param { string } [ value ] - label value
* @ returns { Promise < void > }
* /
async removeLabel ( name , value ) { return await this . removeAttribute ( LABEL , name , value ) ; }
/ * *
* Remove relation name - value pair , if it exists .
*
* @ param { string } name - relation name
* @ param { string } [ value ] - relation value ( noteId )
* @ returns { Promise < void > }
* /
async removeRelation ( name , value ) { return await this . removeAttribute ( RELATION , name , value ) ; }
/ * *
2018-11-26 05:09:52 +08:00
* @ return { Promise < string [ ] > } return list of all descendant noteIds of this note . Returning just noteIds because number of notes can be huge . Includes also this note ' s noteId
* /
async getDescendantNoteIds ( ) {
return await sql . getColumn ( `
WITH RECURSIVE
tree ( noteId ) AS (
SELECT ?
UNION
SELECT branches . noteId FROM branches
JOIN tree ON branches . parentNoteId = tree . noteId
JOIN notes ON notes . noteId = branches . noteId
WHERE notes . isDeleted = 0
AND branches . isDeleted = 0
)
SELECT noteId FROM tree ` , [this.noteId]);
}
/ * *
* Finds descendant notes with given attribute name and value . Only own attributes are considered , not inherited ones
2018-08-23 05:37:06 +08:00
*
* @ param { string } type - attribute type ( label , relation , etc . )
* @ param { string } name - attribute name
* @ param { string } [ value ] - attribute value
2018-08-23 21:33:19 +08:00
* @ returns { Promise < Note [ ] > }
2018-08-23 05:37:06 +08:00
* /
2018-11-26 05:09:52 +08:00
async getDescendantNotesWithAttribute ( type , name , value ) {
2018-08-21 18:50:43 +08:00
const params = [ this . noteId , name ] ;
let valueCondition = "" ;
if ( value !== undefined ) {
params . push ( value ) ;
valueCondition = " AND attributes.value = ?" ;
}
const notes = await repository . getEntities ( `
WITH RECURSIVE
tree ( noteId ) AS (
SELECT ?
UNION
SELECT branches . noteId FROM branches
JOIN tree ON branches . parentNoteId = tree . noteId
JOIN notes ON notes . noteId = branches . noteId
WHERE notes . isDeleted = 0
AND branches . isDeleted = 0
)
SELECT notes . * FROM notes
JOIN tree ON tree . noteId = notes . noteId
JOIN attributes ON attributes . noteId = notes . noteId
WHERE attributes . isDeleted = 0
AND attributes . name = ?
$ { valueCondition }
ORDER BY noteId , position ` , params);
return notes ;
}
2018-08-23 05:37:06 +08:00
/ * *
2018-11-26 05:09:52 +08:00
* Finds descendant notes with given label name and value . Only own labels are considered , not inherited ones
2018-08-23 05:37:06 +08:00
*
* @ param { string } name - label name
* @ param { string } [ value ] - label value
2018-08-23 21:33:19 +08:00
* @ returns { Promise < Note [ ] > }
2018-08-23 05:37:06 +08:00
* /
2018-11-26 05:09:52 +08:00
async getDescendantNotesWithLabel ( name , value ) { return await this . getDescendantNotesWithAttribute ( LABEL , name , value ) ; }
2018-08-23 05:37:06 +08:00
/ * *
2018-11-26 05:09:52 +08:00
* Finds descendant notes with given relation name and value . Only own relations are considered , not inherited ones
2018-08-23 05:37:06 +08:00
*
* @ param { string } name - relation name
* @ param { string } [ value ] - relation value
2018-08-23 21:33:19 +08:00
* @ returns { Promise < Note [ ] > }
2018-08-23 05:37:06 +08:00
* /
2018-11-26 05:09:52 +08:00
async getDescendantNotesWithRelation ( name , value ) { return await this . getDescendantNotesWithAttribute ( RELATION , name , value ) ; }
2018-08-21 18:50:43 +08:00
2018-08-23 05:37:06 +08:00
/ * *
* Returns note revisions of this note .
*
2018-08-23 21:33:19 +08:00
* @ returns { Promise < NoteRevision [ ] > }
2018-08-23 05:37:06 +08:00
* /
2018-01-30 07:34:59 +08:00
async getRevisions ( ) {
2018-03-31 22:51:37 +08:00
return await repository . getEntities ( "SELECT * FROM note_revisions WHERE noteId = ?" , [ this . noteId ] ) ;
2018-01-30 07:34:59 +08:00
}
2018-08-23 05:37:06 +08:00
/ * *
2018-11-15 20:58:14 +08:00
* Get list of links coming out of this note .
*
2019-08-20 02:12:00 +08:00
* @ deprecated - not intended for general use
* @ returns { Promise < Attribute [ ] > }
2018-08-23 05:37:06 +08:00
* /
2018-11-08 18:08:16 +08:00
async getLinks ( ) {
2019-08-20 02:12:00 +08:00
return await repository . getEntities ( `
SELECT *
FROM attributes
WHERE noteId = ? AND
isDeleted = 0 AND
type = 'relation' AND
name IN ( 'internal-link' , 'image-link' , 'relation-map-link' ) ` , [this.noteId]);
2018-04-01 10:15:06 +08:00
}
2018-08-23 05:37:06 +08:00
/ * *
2018-08-23 21:33:19 +08:00
* @ returns { Promise < Branch [ ] > }
2018-08-23 05:37:06 +08:00
* /
2018-04-01 11:08:22 +08:00
async getBranches ( ) {
2018-03-31 22:51:37 +08:00
return await repository . getEntities ( "SELECT * FROM branches WHERE isDeleted = 0 AND noteId = ?" , [ this . noteId ] ) ;
2018-01-29 12:16:50 +08:00
}
2018-01-30 12:35:36 +08:00
2018-09-03 15:40:22 +08:00
/ * *
* @ returns { boolean } - true if note has children
* /
async hasChildren ( ) {
return ( await this . getChildNotes ( ) ) . length > 0 ;
}
2018-08-23 05:37:06 +08:00
/ * *
2018-08-23 21:33:19 +08:00
* @ returns { Promise < Note [ ] > } child notes of this note
2018-08-23 05:37:06 +08:00
* /
2018-04-01 11:08:22 +08:00
async getChildNotes ( ) {
2018-03-31 22:51:37 +08:00
return await repository . getEntities ( `
2018-02-25 03:42:52 +08:00
SELECT notes . *
2018-03-25 09:39:15 +08:00
FROM branches
2018-02-25 03:42:52 +08:00
JOIN notes USING ( noteId )
WHERE notes . isDeleted = 0
2018-03-25 09:39:15 +08:00
AND branches . isDeleted = 0
AND branches . parentNoteId = ?
ORDER BY branches . notePosition ` , [this.noteId]);
2018-02-25 03:42:52 +08:00
}
2018-08-23 05:37:06 +08:00
/ * *
2018-08-23 21:33:19 +08:00
* @ returns { Promise < Branch [ ] > } child branches of this note
2018-08-23 05:37:06 +08:00
* /
2018-04-01 11:08:22 +08:00
async getChildBranches ( ) {
return await repository . getEntities ( `
SELECT branches . *
FROM branches
WHERE branches . isDeleted = 0
AND branches . parentNoteId = ?
ORDER BY branches . notePosition ` , [this.noteId]);
}
2018-08-23 05:37:06 +08:00
/ * *
2018-08-23 21:33:19 +08:00
* @ returns { Promise < Note [ ] > } parent notes of this note ( note can have multiple parents because of cloning )
2018-08-23 05:37:06 +08:00
* /
2018-04-01 11:08:22 +08:00
async getParentNotes ( ) {
2018-03-31 22:51:37 +08:00
return await repository . getEntities ( `
2018-02-25 03:42:52 +08:00
SELECT parent _notes . *
FROM
2018-03-25 09:39:15 +08:00
branches AS child _tree
2018-02-25 03:42:52 +08:00
JOIN notes AS parent _notes ON parent _notes . noteId = child _tree . parentNoteId
WHERE child _tree . noteId = ?
AND child _tree . isDeleted = 0
AND parent _notes . isDeleted = 0 ` , [this.noteId]);
}
2018-01-30 12:35:36 +08:00
beforeSaving ( ) {
2018-04-02 05:38:24 +08:00
if ( ! this . isDeleted ) {
this . isDeleted = false ;
}
2019-03-14 05:43:59 +08:00
if ( ! this . dateCreated ) {
this . dateCreated = dateUtils . localNowDateTime ( ) ;
}
2019-03-13 03:58:31 +08:00
if ( ! this . utcDateCreated ) {
2019-03-14 05:43:59 +08:00
this . utcDateCreated = dateUtils . utcNowDateTime ( ) ;
2018-04-01 10:15:06 +08:00
}
2018-08-13 02:04:48 +08:00
super . beforeSaving ( ) ;
if ( this . isChanged ) {
2019-03-14 05:43:59 +08:00
this . dateModified = dateUtils . localNowDateTime ( ) ;
this . utcDateModified = dateUtils . utcNowDateTime ( ) ;
2018-08-13 02:04:48 +08:00
}
2018-01-30 12:35:36 +08:00
}
2018-11-30 17:20:03 +08:00
// cannot be static!
updatePojo ( pojo ) {
if ( pojo . isProtected ) {
2019-01-13 07:24:51 +08:00
if ( this . isContentAvailable ) {
protectedSessionService . encryptNote ( pojo ) ;
}
else {
// updating protected note outside of protected session means we will keep original ciphertexts
2019-03-27 05:24:04 +08:00
delete pojo . title ;
2019-01-13 07:24:51 +08:00
}
2018-11-30 17:20:03 +08:00
}
delete pojo . isContentAvailable ;
delete pojo . _ _attributeCache ;
2019-03-27 05:24:04 +08:00
delete pojo . content ;
2019-03-29 04:17:40 +08:00
delete pojo . contentHash ;
2018-11-30 17:20:03 +08:00
}
2018-01-29 12:16:50 +08:00
}
module . exports = Note ;