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 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 { | ||||
|     constructor() { | ||||
|         this.reset(); | ||||
|   | ||||
| @@ -6,12 +6,14 @@ import appContext from "./app_context.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 | ||||
|  * - 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. | ||||
|  * | ||||
|  * Note and branch deletions are corner cases and usually not needed. | ||||
|  * | ||||
|  * Backend has a similar cache called Becca | ||||
|  */ | ||||
| class Froca { | ||||
|     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