mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	sharing WIP
This commit is contained in:
		| @@ -3,6 +3,9 @@ | |||||||
| const sql = require("../services/sql.js"); | const sql = require("../services/sql.js"); | ||||||
| const NoteSet = require("../services/search/note_set"); | const NoteSet = require("../services/search/note_set"); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Becca is a backend cache of all notes, branches and attributes. There's a similar frontend cache Froca. | ||||||
|  |  */ | ||||||
| class Becca { | class Becca { | ||||||
|     constructor() { |     constructor() { | ||||||
|         this.reset(); |         this.reset(); | ||||||
|   | |||||||
| @@ -6,12 +6,14 @@ import appContext from "./app_context.js"; | |||||||
| import NoteComplement from "../entities/note_complement.js"; | import NoteComplement from "../entities/note_complement.js"; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Froca keeps a read only cache of note tree structure in frontend's memory. |  * Froca (FROntend CAche) keeps a read only cache of note tree structure in frontend's memory. | ||||||
|  * - notes are loaded lazily when unknown noteId is requested |  * - notes are loaded lazily when unknown noteId is requested | ||||||
|  * - when note is loaded, all its parent and child branches are loaded as well. For a branch to be used, it's not must be loaded before |  * - when note is loaded, all its parent and child branches are loaded as well. For a branch to be used, it's not must be loaded before | ||||||
|  * - deleted notes are present in the cache as well, but they don't have any branches. As a result check for deleted branch is done by presence check - if the branch is not there even though the corresponding note has been loaded, we can infer it is deleted. |  * - deleted notes are present in the cache as well, but they don't have any branches. As a result check for deleted branch is done by presence check - if the branch is not there even though the corresponding note has been loaded, we can infer it is deleted. | ||||||
|  * |  * | ||||||
|  * Note and branch deletions are corner cases and usually not needed. |  * Note and branch deletions are corner cases and usually not needed. | ||||||
|  |  * | ||||||
|  |  * Backend has a similar cache called Becca | ||||||
|  */ |  */ | ||||||
| class Froca { | class Froca { | ||||||
|     constructor() { |     constructor() { | ||||||
|   | |||||||
							
								
								
									
										112
									
								
								src/share/entities/attribute.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								src/share/entities/attribute.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | const Note = require('./note.js'); | ||||||
|  | const sql = require("../sql.js"); | ||||||
|  |  | ||||||
|  | class Attribute { | ||||||
|  |     constructor(row) { | ||||||
|  |         this.updateFromRow(row); | ||||||
|  |         this.init(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     updateFromRow(row) { | ||||||
|  |         this.update([ | ||||||
|  |             row.attributeId, | ||||||
|  |             row.noteId, | ||||||
|  |             row.type, | ||||||
|  |             row.name, | ||||||
|  |             row.value, | ||||||
|  |             row.isInheritable, | ||||||
|  |             row.position | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     update([attributeId, noteId, type, name, value, isInheritable, position]) { | ||||||
|  |         /** @param {string} */ | ||||||
|  |         this.attributeId = attributeId; | ||||||
|  |         /** @param {string} */ | ||||||
|  |         this.noteId = noteId; | ||||||
|  |         /** @param {string} */ | ||||||
|  |         this.type = type; | ||||||
|  |         /** @param {string} */ | ||||||
|  |         this.name = name; | ||||||
|  |         /** @param {int} */ | ||||||
|  |         this.position = position; | ||||||
|  |         /** @param {string} */ | ||||||
|  |         this.value = value; | ||||||
|  |         /** @param {boolean} */ | ||||||
|  |         this.isInheritable = !!isInheritable; | ||||||
|  |  | ||||||
|  |         return this; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     init() { | ||||||
|  |         if (this.attributeId) { | ||||||
|  |             this.becca.attributes[this.attributeId] = this; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!(this.noteId in this.becca.notes)) { | ||||||
|  |             // entities can come out of order in sync, create skeleton which will be filled later | ||||||
|  |             this.becca.addNote(this.noteId, new Note({noteId: this.noteId})); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         this.becca.notes[this.noteId].ownedAttributes.push(this); | ||||||
|  |  | ||||||
|  |         const key = `${this.type}-${this.name.toLowerCase()}`; | ||||||
|  |         this.becca.attributeIndex[key] = this.becca.attributeIndex[key] || []; | ||||||
|  |         this.becca.attributeIndex[key].push(this); | ||||||
|  |  | ||||||
|  |         const targetNote = this.targetNote; | ||||||
|  |  | ||||||
|  |         if (targetNote) { | ||||||
|  |             targetNote.targetRelations.push(this); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     get isAffectingSubtree() { | ||||||
|  |         return this.isInheritable | ||||||
|  |             || (this.type === 'relation' && this.name === 'template'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     get targetNoteId() { // alias | ||||||
|  |         return this.type === 'relation' ? this.value : undefined; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     isAutoLink() { | ||||||
|  |         return this.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(this.name); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     get note() { | ||||||
|  |         return this.becca.notes[this.noteId]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     get targetNote() { | ||||||
|  |         if (this.type === 'relation') { | ||||||
|  |             return this.becca.notes[this.value]; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @returns {Note|null} | ||||||
|  |      */ | ||||||
|  |     getNote() { | ||||||
|  |         return this.becca.getNote(this.noteId); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @returns {Note|null} | ||||||
|  |      */ | ||||||
|  |     getTargetNote() { | ||||||
|  |         if (this.type !== 'relation') { | ||||||
|  |             throw new Error(`Attribute ${this.attributeId} is not relation`); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!this.value) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return this.becca.getNote(this.value); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = Attribute; | ||||||
							
								
								
									
										90
									
								
								src/share/entities/branch.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/share/entities/branch.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | const Note = require('./note.js'); | ||||||
|  | const sql = require("../sql.js"); | ||||||
|  |  | ||||||
|  | class Branch { | ||||||
|  |     constructor(row) { | ||||||
|  |         this.updateFromRow(row); | ||||||
|  |         this.init(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     updateFromRow(row) { | ||||||
|  |         this.update([ | ||||||
|  |             row.branchId, | ||||||
|  |             row.noteId, | ||||||
|  |             row.parentNoteId, | ||||||
|  |             row.prefix, | ||||||
|  |             row.notePosition, | ||||||
|  |             row.isExpanded, | ||||||
|  |             row.utcDateModified | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     update([branchId, noteId, parentNoteId, prefix, notePosition, isExpanded]) { | ||||||
|  |         /** @param {string} */ | ||||||
|  |         this.branchId = branchId; | ||||||
|  |         /** @param {string} */ | ||||||
|  |         this.noteId = noteId; | ||||||
|  |         /** @param {string} */ | ||||||
|  |         this.parentNoteId = parentNoteId; | ||||||
|  |         /** @param {string} */ | ||||||
|  |         this.prefix = prefix; | ||||||
|  |         /** @param {int} */ | ||||||
|  |         this.notePosition = notePosition; | ||||||
|  |         /** @param {boolean} */ | ||||||
|  |         this.isExpanded = !!isExpanded; | ||||||
|  |  | ||||||
|  |         return this; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     init() { | ||||||
|  |         if (this.branchId === 'root') { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const childNote = this.childNote; | ||||||
|  |         const parentNote = this.parentNote; | ||||||
|  |  | ||||||
|  |         if (!childNote.parents.includes(parentNote)) { | ||||||
|  |             childNote.parents.push(parentNote); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!childNote.parentBranches.includes(this)) { | ||||||
|  |             childNote.parentBranches.push(this); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!parentNote.children.includes(childNote)) { | ||||||
|  |             parentNote.children.push(childNote); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         this.becca.branches[this.branchId] = this; | ||||||
|  |         this.becca.childParentToBranch[`${this.noteId}-${this.parentNoteId}`] = this; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** @return {Note} */ | ||||||
|  |     get childNote() { | ||||||
|  |         if (!(this.noteId in this.becca.notes)) { | ||||||
|  |             // entities can come out of order in sync, create skeleton which will be filled later | ||||||
|  |             this.becca.addNote(this.noteId, new Note({noteId: this.noteId})); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return this.becca.notes[this.noteId]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     getNote() { | ||||||
|  |         return this.childNote; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** @return {Note} */ | ||||||
|  |     get parentNote() { | ||||||
|  |         if (!(this.parentNoteId in this.becca.notes)) { | ||||||
|  |             // entities can come out of order in sync, create skeleton which will be filled later | ||||||
|  |             this.becca.addNote(this.parentNoteId, new Note({noteId: this.parentNoteId})); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return this.becca.notes[this.parentNoteId]; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = Branch; | ||||||
							
								
								
									
										577
									
								
								src/share/entities/note.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										577
									
								
								src/share/entities/note.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,577 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | const sql = require('../sql'); | ||||||
|  | const utils = require('../../services/utils'); | ||||||
|  |  | ||||||
|  | const LABEL = 'label'; | ||||||
|  | const RELATION = 'relation'; | ||||||
|  |  | ||||||
|  | class Note { | ||||||
|  |     constructor(row) { | ||||||
|  |         this.updateFromRow(row); | ||||||
|  |         this.init(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     updateFromRow(row) { | ||||||
|  |         this.update([ | ||||||
|  |             row.noteId, | ||||||
|  |             row.title, | ||||||
|  |             row.type, | ||||||
|  |             row.mime | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     update([noteId, title, type, mime]) { | ||||||
|  |         /** @param {string} */ | ||||||
|  |         this.noteId = noteId; | ||||||
|  |         /** @param {string} */ | ||||||
|  |         this.title = title; | ||||||
|  |         /** @param {string} */ | ||||||
|  |         this.type = type; | ||||||
|  |         /** @param {string} */ | ||||||
|  |         this.mime = mime; | ||||||
|  |  | ||||||
|  |         return this; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     init() { | ||||||
|  |         /** @param {Branch[]} */ | ||||||
|  |         this.parentBranches = []; | ||||||
|  |         /** @param {Note[]} */ | ||||||
|  |         this.parents = []; | ||||||
|  |         /** @param {Note[]} */ | ||||||
|  |         this.children = []; | ||||||
|  |         /** @param {Attribute[]} */ | ||||||
|  |         this.ownedAttributes = []; | ||||||
|  |  | ||||||
|  |         /** @param {Attribute[]|null} */ | ||||||
|  |         this.__attributeCache = null; | ||||||
|  |         /** @param {Attribute[]|null} */ | ||||||
|  |         this.inheritableAttributeCache = null; | ||||||
|  |  | ||||||
|  |         /** @param {Attribute[]} */ | ||||||
|  |         this.targetRelations = []; | ||||||
|  |  | ||||||
|  |         this.becca.addNote(this.noteId, this); | ||||||
|  |  | ||||||
|  |         /** @param {Note[]|null} */ | ||||||
|  |         this.ancestorCache = null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     getParentBranches() { | ||||||
|  |         return this.parentBranches; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     getBranches() { | ||||||
|  |         return this.parentBranches; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     getParentNotes() { | ||||||
|  |         return this.parents; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     getChildNotes() { | ||||||
|  |         return this.children; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     hasChildren() { | ||||||
|  |         return this.children && this.children.length > 0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     getChildBranches() { | ||||||
|  |         return this.children.map(childNote => this.becca.getBranchFromChildAndParent(childNote.noteId, this.noteId)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     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.isStringNote()) { | ||||||
|  |             return content === null | ||||||
|  |                 ? "" | ||||||
|  |                 : content.toString("UTF-8"); | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             return content; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** @returns {*} */ | ||||||
|  |     getJsonContent() { | ||||||
|  |         const content = this.getContent(); | ||||||
|  |  | ||||||
|  |         if (!content || !content.trim()) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return JSON.parse(content); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** @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); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @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(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     __getAttributes(path) { | ||||||
|  |         if (path.includes(this.noteId)) { | ||||||
|  |             return []; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!this.__attributeCache) { | ||||||
|  |             const parentAttributes = this.ownedAttributes.slice(); | ||||||
|  |             const newPath = [...path, this.noteId]; | ||||||
|  |  | ||||||
|  |             if (this.noteId !== 'root') { | ||||||
|  |                 for (const parentNote of this.parents) { | ||||||
|  |                     parentAttributes.push(...parentNote.__getInheritableAttributes(newPath)); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const templateAttributes = []; | ||||||
|  |  | ||||||
|  |             for (const ownedAttr of parentAttributes) { // parentAttributes so we process also inherited templates | ||||||
|  |                 if (ownedAttr.type === 'relation' && ownedAttr.name === 'template') { | ||||||
|  |                     const templateNote = this.becca.notes[ownedAttr.value]; | ||||||
|  |  | ||||||
|  |                     if (templateNote) { | ||||||
|  |                         templateAttributes.push(...templateNote.__getAttributes(newPath)); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             this.__attributeCache = []; | ||||||
|  |  | ||||||
|  |             const addedAttributeIds = new Set(); | ||||||
|  |  | ||||||
|  |             for (const attr of parentAttributes.concat(templateAttributes)) { | ||||||
|  |                 if (!addedAttributeIds.has(attr.attributeId)) { | ||||||
|  |                     addedAttributeIds.add(attr.attributeId); | ||||||
|  |  | ||||||
|  |                     this.__attributeCache.push(attr); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             this.inheritableAttributeCache = []; | ||||||
|  |  | ||||||
|  |             for (const attr of this.__attributeCache) { | ||||||
|  |                 if (attr.isInheritable) { | ||||||
|  |                     this.inheritableAttributeCache.push(attr); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return this.__attributeCache; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** @return {Attribute[]} */ | ||||||
|  |     __getInheritableAttributes(path) { | ||||||
|  |         if (path.includes(this.noteId)) { | ||||||
|  |             return []; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!this.inheritableAttributeCache) { | ||||||
|  |             this.__getAttributes(path); // will refresh also this.inheritableAttributeCache | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return this.inheritableAttributeCache; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     hasAttribute(type, name) { | ||||||
|  |         return !!this.getAttributes().find(attr => attr.type === type && attr.name === name); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     getAttributeCaseInsensitive(type, name, value) { | ||||||
|  |         name = name.toLowerCase(); | ||||||
|  |         value = value ? value.toLowerCase() : null; | ||||||
|  |  | ||||||
|  |         return this.getAttributes().find( | ||||||
|  |             attr => attr.type === type | ||||||
|  |             && attr.name.toLowerCase() === name | ||||||
|  |             && (!value || attr.value.toLowerCase() === value)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     getRelationTarget(name) { | ||||||
|  |         const relation = this.getAttributes().find(attr => attr.type === 'relation' && attr.name === name); | ||||||
|  |  | ||||||
|  |         return relation ? relation.targetNote : null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @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); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @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(); | ||||||
|  |  | ||||||
|  |         return attributes.find(attr => attr.type === type && attr.name === name); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @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); | ||||||
|  |  | ||||||
|  |         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; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @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); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @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) { | ||||||
|  |         // it's a common mistake to include # or ~ into attribute name | ||||||
|  |         if (name && ["#", "~"].includes(name[0])) { | ||||||
|  |             name = name.substr(1); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         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(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @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; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     get isArchived() { | ||||||
|  |         return this.hasAttribute('label', 'archived'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     hasInheritableOwnedArchivedLabel() { | ||||||
|  |         return !!this.ownedAttributes.find(attr => attr.type === 'label' && attr.name === 'archived' && attr.isInheritable); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // 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 | ||||||
|  |     resortParents() { | ||||||
|  |         this.parentBranches.sort((a, b) => | ||||||
|  |             a.branchId.startsWith('virt-') | ||||||
|  |             || a.parentNote.hasInheritableOwnedArchivedLabel() ? 1 : -1); | ||||||
|  |  | ||||||
|  |         this.parents = this.parentBranches.map(branch => branch.parentNote); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     isTemplate() { | ||||||
|  |         return !!this.targetRelations.find(rel => rel.name === 'template'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** @return {Note[]} */ | ||||||
|  |     getSubtreeNotesIncludingTemplated() { | ||||||
|  |         const arr = [[this]]; | ||||||
|  |  | ||||||
|  |         for (const childNote of this.children) { | ||||||
|  |             arr.push(childNote.getSubtreeNotesIncludingTemplated()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         for (const targetRelation of this.targetRelations) { | ||||||
|  |             if (targetRelation.name === 'template') { | ||||||
|  |                 const note = targetRelation.note; | ||||||
|  |  | ||||||
|  |                 if (note) { | ||||||
|  |                     arr.push(note.getSubtreeNotesIncludingTemplated()); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return arr.flat(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** @return {Note[]} */ | ||||||
|  |     getSubtreeNotes(includeArchived = true) { | ||||||
|  |         const noteSet = new Set(); | ||||||
|  |  | ||||||
|  |         function addSubtreeNotesInner(note) { | ||||||
|  |             if (!includeArchived && note.isArchived) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             noteSet.add(note); | ||||||
|  |  | ||||||
|  |             for (const childNote of note.children) { | ||||||
|  |                 addSubtreeNotesInner(childNote); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         addSubtreeNotesInner(this); | ||||||
|  |  | ||||||
|  |         return Array.from(noteSet); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** @return {String[]} */ | ||||||
|  |     getSubtreeNoteIds() { | ||||||
|  |         return this.getSubtreeNotes().map(note => note.noteId); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     getDescendantNoteIds() { | ||||||
|  |         return this.getSubtreeNoteIds(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     getAncestors() { | ||||||
|  |         if (!this.ancestorCache) { | ||||||
|  |             const noteIds = new Set(); | ||||||
|  |             this.ancestorCache = []; | ||||||
|  |  | ||||||
|  |             for (const parent of this.parents) { | ||||||
|  |                 if (!noteIds.has(parent.noteId)) { | ||||||
|  |                     this.ancestorCache.push(parent); | ||||||
|  |                     noteIds.add(parent.noteId); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 for (const ancestorNote of parent.getAncestors()) { | ||||||
|  |                     if (!noteIds.has(ancestorNote.noteId)) { | ||||||
|  |                         this.ancestorCache.push(ancestorNote); | ||||||
|  |                         noteIds.add(ancestorNote.noteId); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return this.ancestorCache; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     getTargetRelations() { | ||||||
|  |         return this.targetRelations; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** @return {Note[]} - returns only notes which are templated, does not include their subtrees | ||||||
|  |      *                     in effect returns notes which are influenced by note's non-inheritable attributes */ | ||||||
|  |     getTemplatedNotes() { | ||||||
|  |         const arr = [this]; | ||||||
|  |  | ||||||
|  |         for (const targetRelation of this.targetRelations) { | ||||||
|  |             if (targetRelation.name === 'template') { | ||||||
|  |                 const note = targetRelation.note; | ||||||
|  |  | ||||||
|  |                 if (note) { | ||||||
|  |                     arr.push(note); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return arr; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @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)); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = Note; | ||||||
							
								
								
									
										75
									
								
								src/share/shaca/shaca.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/share/shaca/shaca.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | class Shaca { | ||||||
|  |     constructor() { | ||||||
|  |         this.reset(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     reset() { | ||||||
|  |         /** @type {Object.<String, Note>} */ | ||||||
|  |         this.notes = {}; | ||||||
|  |         /** @type {Object.<String, Branch>} */ | ||||||
|  |         this.branches = {}; | ||||||
|  |         /** @type {Object.<String, Branch>} */ | ||||||
|  |         this.childParentToBranch = {}; | ||||||
|  |         /** @type {Object.<String, Attribute>} */ | ||||||
|  |         this.attributes = {}; | ||||||
|  |  | ||||||
|  |         this.loaded = false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     getNote(noteId) { | ||||||
|  |         return this.notes[noteId]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     getNotes(noteIds, ignoreMissing = false) { | ||||||
|  |         const filteredNotes = []; | ||||||
|  |  | ||||||
|  |         for (const noteId of noteIds) { | ||||||
|  |             const note = this.notes[noteId]; | ||||||
|  |  | ||||||
|  |             if (!note) { | ||||||
|  |                 if (ignoreMissing) { | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 throw new Error(`Note '${noteId}' was not found in becca.`); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             filteredNotes.push(note); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return filteredNotes; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     getBranch(branchId) { | ||||||
|  |         return this.branches[branchId]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     getAttribute(attributeId) { | ||||||
|  |         return this.attributes[attributeId]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     getBranchFromChildAndParent(childNoteId, parentNoteId) { | ||||||
|  |         return this.childParentToBranch[`${childNoteId}-${parentNoteId}`]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     getEntity(entityName, entityId) { | ||||||
|  |         if (!entityName || !entityId) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const camelCaseEntityName = entityName.toLowerCase().replace(/(_[a-z])/g, | ||||||
|  |             group => | ||||||
|  |                 group | ||||||
|  |                     .toUpperCase() | ||||||
|  |                     .replace('_', '') | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         return this[camelCaseEntityName][entityId]; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const shaca = new Shaca(); | ||||||
|  |  | ||||||
|  | module.exports = shaca; | ||||||
							
								
								
									
										207
									
								
								src/share/shaca/shaca_loader.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								src/share/shaca/shaca_loader.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,207 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | const sql = require('../services/sql'); | ||||||
|  | const eventService = require('../services/events'); | ||||||
|  | const shaca = require('./shaca.js'); | ||||||
|  | const sqlInit = require('../services/sql_init'); | ||||||
|  | const log = require('../services/log'); | ||||||
|  | const Note = require('./entities/note'); | ||||||
|  | const Branch = require('./entities/branch'); | ||||||
|  | const Attribute = require('./entities/attribute'); | ||||||
|  | const Option = require('./entities/option'); | ||||||
|  | const entityConstructor = require("../becca/entity_constructor"); | ||||||
|  |  | ||||||
|  | function load() { | ||||||
|  |     const start = Date.now(); | ||||||
|  |     shaca.reset(); | ||||||
|  |  | ||||||
|  |     // using raw query and passing arrays to avoid allocating new objects | ||||||
|  |     // this is worth it for becca load since it happens every run and blocks the app until finished | ||||||
|  |  | ||||||
|  |     for (const row of sql.getRawRows(`SELECT noteId, title, type, mime, isProtected, dateCreated, dateModified, utcDateCreated, utcDateModified FROM notes WHERE isDeleted = 0`, [])) { | ||||||
|  |         new Note().update(row).init(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     for (const row of sql.getRawRows(`SELECT branchId, noteId, parentNoteId, prefix, notePosition, isExpanded, utcDateModified FROM branches WHERE isDeleted = 0`, [])) { | ||||||
|  |         new Branch().update(row).init(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     for (const row of sql.getRawRows(`SELECT attributeId, noteId, type, name, value, isInheritable, position, utcDateModified FROM attributes WHERE isDeleted = 0`, [])) { | ||||||
|  |         new Attribute().update(row).init(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     for (const row of sql.getRows(`SELECT name, value, isSynced, utcDateModified FROM options`)) { | ||||||
|  |         new Option(row); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     shaca.loaded = true; | ||||||
|  |  | ||||||
|  |     log.info(`Shaca load took ${Date.now() - start}ms`); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | eventService.subscribe([eventService.ENTITY_CHANGE_SYNCED],  ({entityName, entityRow}) => { | ||||||
|  |     if (!becca.loaded) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (["notes", "branches", "attributes"].includes(entityName)) { | ||||||
|  |         const EntityClass = entityConstructor.getEntityFromEntityName(entityName); | ||||||
|  |         const primaryKeyName = EntityClass.primaryKeyName; | ||||||
|  |  | ||||||
|  |         let beccaEntity = becca.getEntity(entityName, entityRow[primaryKeyName]); | ||||||
|  |  | ||||||
|  |         if (beccaEntity) { | ||||||
|  |             beccaEntity.updateFromRow(entityRow); | ||||||
|  |         } else { | ||||||
|  |             beccaEntity = new EntityClass(); | ||||||
|  |             beccaEntity.updateFromRow(entityRow); | ||||||
|  |             beccaEntity.init(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     postProcessEntityUpdate(entityName, entityRow); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | eventService.subscribe(eventService.ENTITY_CHANGED,  ({entityName, entity}) => { | ||||||
|  |     if (!becca.loaded) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     postProcessEntityUpdate(entityName, entity); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | eventService.subscribe([eventService.ENTITY_DELETED, eventService.ENTITY_DELETE_SYNCED],  ({entityName, entityId}) => { | ||||||
|  |     if (!becca.loaded) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (entityName === 'notes') { | ||||||
|  |         noteDeleted(entityId); | ||||||
|  |     } else if (entityName === 'branches') { | ||||||
|  |         branchDeleted(entityId); | ||||||
|  |     } else if (entityName === 'attributes') { | ||||||
|  |         attributeDeleted(entityId); | ||||||
|  |     } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | function noteDeleted(noteId) { | ||||||
|  |     delete becca.notes[noteId]; | ||||||
|  |  | ||||||
|  |     becca.dirtyNoteSetCache(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function branchDeleted(branchId) { | ||||||
|  |     const branch = becca.branches[branchId]; | ||||||
|  |  | ||||||
|  |     if (!branch) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const childNote = becca.notes[branch.noteId]; | ||||||
|  |  | ||||||
|  |     if (childNote) { | ||||||
|  |         childNote.parents = childNote.parents.filter(parent => parent.noteId !== branch.parentNoteId); | ||||||
|  |         childNote.parentBranches = childNote.parentBranches | ||||||
|  |             .filter(parentBranch => parentBranch.branchId !== branch.branchId); | ||||||
|  |  | ||||||
|  |         if (childNote.parents.length > 0) { | ||||||
|  |             childNote.invalidateSubTree(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const parentNote = becca.notes[branch.parentNoteId]; | ||||||
|  |  | ||||||
|  |     if (parentNote) { | ||||||
|  |         parentNote.children = parentNote.children.filter(child => child.noteId !== branch.noteId); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     delete becca.childParentToBranch[`${branch.noteId}-${branch.parentNoteId}`]; | ||||||
|  |     delete becca.branches[branch.branchId]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function branchUpdated(branch) { | ||||||
|  |     const childNote = becca.notes[branch.noteId]; | ||||||
|  |  | ||||||
|  |     if (childNote) { | ||||||
|  |         childNote.flatTextCache = null; | ||||||
|  |         childNote.resortParents(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function attributeDeleted(attributeId) { | ||||||
|  |     const attribute = becca.attributes[attributeId]; | ||||||
|  |  | ||||||
|  |     if (!attribute) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const note = becca.notes[attribute.noteId]; | ||||||
|  |  | ||||||
|  |     if (note) { | ||||||
|  |         // first invalidate and only then remove the attribute (otherwise invalidation wouldn't be complete) | ||||||
|  |         if (attribute.isAffectingSubtree || note.isTemplate()) { | ||||||
|  |             note.invalidateSubTree(); | ||||||
|  |         } else { | ||||||
|  |             note.invalidateThisCache(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         note.ownedAttributes = note.ownedAttributes.filter(attr => attr.attributeId !== attribute.attributeId); | ||||||
|  |  | ||||||
|  |         const targetNote = attribute.targetNote; | ||||||
|  |  | ||||||
|  |         if (targetNote) { | ||||||
|  |             targetNote.targetRelations = targetNote.targetRelations.filter(rel => rel.attributeId !== attribute.attributeId); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     delete becca.attributes[attribute.attributeId]; | ||||||
|  |  | ||||||
|  |     const key = `${attribute.type}-${attribute.name.toLowerCase()}`; | ||||||
|  |  | ||||||
|  |     if (key in becca.attributeIndex) { | ||||||
|  |         becca.attributeIndex[key] = becca.attributeIndex[key].filter(attr => attr.attributeId !== attribute.attributeId); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function attributeUpdated(attribute) { | ||||||
|  |     const note = becca.notes[attribute.noteId]; | ||||||
|  |  | ||||||
|  |     if (note) { | ||||||
|  |         if (attribute.isAffectingSubtree || note.isTemplate()) { | ||||||
|  |             note.invalidateSubTree(); | ||||||
|  |         } else { | ||||||
|  |             note.invalidateThisCache(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function noteReorderingUpdated(branchIdList) { | ||||||
|  |     const parentNoteIds = new Set(); | ||||||
|  |  | ||||||
|  |     for (const branchId in branchIdList) { | ||||||
|  |         const branch = becca.branches[branchId]; | ||||||
|  |  | ||||||
|  |         if (branch) { | ||||||
|  |             branch.notePosition = branchIdList[branchId]; | ||||||
|  |  | ||||||
|  |             parentNoteIds.add(branch.parentNoteId); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, () => { | ||||||
|  |     try { | ||||||
|  |         becca.decryptProtectedNotes(); | ||||||
|  |     } | ||||||
|  |     catch (e) { | ||||||
|  |         log.error(`Could not decrypt protected notes: ${e.message} ${e.stack}`); | ||||||
|  |     } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | eventService.subscribe(eventService.LEAVE_PROTECTED_SESSION, load); | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |     load, | ||||||
|  |     reload, | ||||||
|  |     beccaLoaded | ||||||
|  | }; | ||||||
							
								
								
									
										167
									
								
								src/share/sql.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								src/share/sql.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,167 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | const log = require('../services/log'); | ||||||
|  | const Database = require('better-sqlite3'); | ||||||
|  | const dataDir = require('../services/data_dir'); | ||||||
|  |  | ||||||
|  | const dbConnection = new Database(dataDir.DOCUMENT_PATH, { readonly: true }); | ||||||
|  |  | ||||||
|  | [`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach(eventType => { | ||||||
|  |     process.on(eventType, () => { | ||||||
|  |         if (dbConnection) { | ||||||
|  |             // closing connection is especially important to fold -wal file into the main DB file | ||||||
|  |             // (see https://sqlite.org/tempfiles.html for details) | ||||||
|  |             dbConnection.close(); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const statementCache = {}; | ||||||
|  |  | ||||||
|  | function stmt(sql) { | ||||||
|  |     if (!(sql in statementCache)) { | ||||||
|  |         statementCache[sql] = dbConnection.prepare(sql); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return statementCache[sql]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getRow(query, params = []) { | ||||||
|  |     return wrap(query, s => s.get(params)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getRowOrNull(query, params = []) { | ||||||
|  |     const all = getRows(query, params); | ||||||
|  |  | ||||||
|  |     return all.length > 0 ? all[0] : null; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getValue(query, params = []) { | ||||||
|  |     const row = getRowOrNull(query, params); | ||||||
|  |  | ||||||
|  |     if (!row) { | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return row[Object.keys(row)[0]]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // smaller values can result in better performance due to better usage of statement cache | ||||||
|  | const PARAM_LIMIT = 100; | ||||||
|  |  | ||||||
|  | function getManyRows(query, params) { | ||||||
|  |     let results = []; | ||||||
|  |  | ||||||
|  |     while (params.length > 0) { | ||||||
|  |         const curParams = params.slice(0, Math.min(params.length, PARAM_LIMIT)); | ||||||
|  |         params = params.slice(curParams.length); | ||||||
|  |  | ||||||
|  |         const curParamsObj = {}; | ||||||
|  |  | ||||||
|  |         let j = 1; | ||||||
|  |         for (const param of curParams) { | ||||||
|  |             curParamsObj['param' + j++] = param; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         let i = 1; | ||||||
|  |         const questionMarks = curParams.map(() => ":param" + i++).join(","); | ||||||
|  |         const curQuery = query.replace(/\?\?\?/g, questionMarks); | ||||||
|  |  | ||||||
|  |         const statement = curParams.length === PARAM_LIMIT | ||||||
|  |             ? stmt(curQuery) | ||||||
|  |             : dbConnection.prepare(curQuery); | ||||||
|  |  | ||||||
|  |         const subResults = statement.all(curParamsObj); | ||||||
|  |         results = results.concat(subResults); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return results; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getRows(query, params = []) { | ||||||
|  |     return wrap(query, s => s.all(params)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getRawRows(query, params = []) { | ||||||
|  |     return wrap(query, s => s.raw().all(params)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function iterateRows(query, params = []) { | ||||||
|  |     return stmt(query).iterate(params); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getMap(query, params = []) { | ||||||
|  |     const map = {}; | ||||||
|  |     const results = getRows(query, params); | ||||||
|  |  | ||||||
|  |     for (const row of results) { | ||||||
|  |         const keys = Object.keys(row); | ||||||
|  |  | ||||||
|  |         map[row[keys[0]]] = row[keys[1]]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return map; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getColumn(query, params = []) { | ||||||
|  |     const list = []; | ||||||
|  |     const result = getRows(query, params); | ||||||
|  |  | ||||||
|  |     if (result.length === 0) { | ||||||
|  |         return list; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const key = Object.keys(result[0])[0]; | ||||||
|  |  | ||||||
|  |     for (const row of result) { | ||||||
|  |         list.push(row[key]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return list; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function wrap(query, func) { | ||||||
|  |     const startTimestamp = Date.now(); | ||||||
|  |     let result; | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |         result = func(stmt(query)); | ||||||
|  |     } | ||||||
|  |     catch (e) { | ||||||
|  |         if (e.message.includes("The database connection is not open")) { | ||||||
|  |             // this often happens on killing the app which puts these alerts in front of user | ||||||
|  |             // in these cases error should be simply ignored. | ||||||
|  |             console.log(e.message); | ||||||
|  |  | ||||||
|  |             return null | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         throw e; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const milliseconds = Date.now() - startTimestamp; | ||||||
|  |  | ||||||
|  |     if (milliseconds >= 20) { | ||||||
|  |         if (query.includes("WITH RECURSIVE")) { | ||||||
|  |             log.info(`Slow recursive query took ${milliseconds}ms.`); | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             log.info(`Slow query took ${milliseconds}ms: ${query.trim().replace(/\s+/g, " ")}`); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return result; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |     dbConnection, | ||||||
|  |     getValue, | ||||||
|  |     getRow, | ||||||
|  |     getRowOrNull, | ||||||
|  |     getRows, | ||||||
|  |     getRawRows, | ||||||
|  |     iterateRows, | ||||||
|  |     getManyRows, | ||||||
|  |     getMap, | ||||||
|  |     getColumn | ||||||
|  | }; | ||||||
		Reference in New Issue
	
	Block a user