mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-11-04 15:11:31 +08:00 
			
		
		
		
	note cache breakup into classes, WIP
This commit is contained in:
		
							parent
							
								
									e3071e630a
								
							
						
					
					
						commit
						dcd371b5b1
					
				@ -1,6 +1,6 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const noteCacheService = require('../../services/note_cache');
 | 
			
		||||
const noteCacheService = require('../../services/note_cache/note_cache.js');
 | 
			
		||||
const repository = require('../../services/repository');
 | 
			
		||||
const log = require('../../services/log');
 | 
			
		||||
const utils = require('../../services/utils');
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@ const zipImportService = require('../../services/import/zip');
 | 
			
		||||
const singleImportService = require('../../services/import/single');
 | 
			
		||||
const cls = require('../../services/cls');
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const noteCacheService = require('../../services/note_cache');
 | 
			
		||||
const noteCacheService = require('../../services/note_cache/note_cache.js');
 | 
			
		||||
const log = require('../../services/log');
 | 
			
		||||
const TaskContext = require('../../services/task_context.js');
 | 
			
		||||
 | 
			
		||||
@ -85,4 +85,4 @@ async function importToBranch(req) {
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    importToBranch
 | 
			
		||||
};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const repository = require('../../services/repository');
 | 
			
		||||
const noteCacheService = require('../../services/note_cache');
 | 
			
		||||
const noteCacheService = require('../../services/note_cache/note_cache.js');
 | 
			
		||||
const protectedSessionService = require('../../services/protected_session');
 | 
			
		||||
const noteRevisionService = require('../../services/note_revisions');
 | 
			
		||||
const utils = require('../../services/utils');
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@
 | 
			
		||||
const sql = require('../../services/sql');
 | 
			
		||||
const protectedSessionService = require('../../services/protected_session');
 | 
			
		||||
const noteService = require('../../services/notes');
 | 
			
		||||
const noteCacheService = require('../../services/note_cache');
 | 
			
		||||
const noteCacheService = require('../../services/note_cache/note_cache.js');
 | 
			
		||||
 | 
			
		||||
async function getRecentChanges(req) {
 | 
			
		||||
    const {ancestorNoteId} = req.params;
 | 
			
		||||
@ -102,4 +102,4 @@ async function getRecentChanges(req) {
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    getRecentChanges
 | 
			
		||||
};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const repository = require('../../services/repository');
 | 
			
		||||
const noteCacheService = require('../../services/note_cache');
 | 
			
		||||
const noteCacheService = require('../../services/note_cache/note_cache.js');
 | 
			
		||||
const log = require('../../services/log');
 | 
			
		||||
const scriptService = require('../../services/script');
 | 
			
		||||
const searchService = require('../../services/search');
 | 
			
		||||
@ -110,4 +110,4 @@ async function searchFromRelation(note, relationName) {
 | 
			
		||||
module.exports = {
 | 
			
		||||
    searchNotes,
 | 
			
		||||
    searchFromNote
 | 
			
		||||
};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const noteCacheService = require('../../services/note_cache');
 | 
			
		||||
const noteCacheService = require('../../services/note_cache/note_cache.js');
 | 
			
		||||
const repository = require('../../services/repository');
 | 
			
		||||
 | 
			
		||||
async function getSimilarNotes(req) {
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										43
									
								
								src/services/note_cache/entities/attribute.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/services/note_cache/entities/attribute.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,43 @@
 | 
			
		||||
class Attribute {
 | 
			
		||||
    constructor(row) {
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        this.attributeId = row.attributeId;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        this.noteId = row.noteId;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        this.type = row.type;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        this.name = row.name;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        this.value = row.value;
 | 
			
		||||
        /** @param {boolean} */
 | 
			
		||||
        this.isInheritable = !!row.isInheritable;
 | 
			
		||||
 | 
			
		||||
        notes[this.noteId].ownedAttributes.push(this);
 | 
			
		||||
 | 
			
		||||
        const key = `${this.type-this.name}`;
 | 
			
		||||
        attributeIndex[key] = attributeIndex[key] || [];
 | 
			
		||||
        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 note() {
 | 
			
		||||
        return notes[this.noteId];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get targetNote() {
 | 
			
		||||
        if (this.type === 'relation') {
 | 
			
		||||
            return notes[this.value];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										42
									
								
								src/services/note_cache/entities/branch.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/services/note_cache/entities/branch.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,42 @@
 | 
			
		||||
export default class Branch {
 | 
			
		||||
    constructor(row) {
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        this.branchId = row.branchId;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        this.noteId = row.noteId;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        this.parentNoteId = row.parentNoteId;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        this.prefix = row.prefix;
 | 
			
		||||
 | 
			
		||||
        if (this.branchId === 'root') {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const childNote = notes[this.noteId];
 | 
			
		||||
        const parentNote = this.parentNote;
 | 
			
		||||
 | 
			
		||||
        if (!childNote) {
 | 
			
		||||
            console.log(`Cannot find child note ${this.noteId} of a branch ${this.branchId}`);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        childNote.parents.push(parentNote);
 | 
			
		||||
        childNote.parentBranches.push(this);
 | 
			
		||||
 | 
			
		||||
        parentNote.children.push(childNote);
 | 
			
		||||
 | 
			
		||||
        childParentToBranch[`${this.noteId}-${this.parentNoteId}`] = this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Note} */
 | 
			
		||||
    get parentNote() {
 | 
			
		||||
        const note = notes[this.parentNoteId];
 | 
			
		||||
 | 
			
		||||
        if (!note) {
 | 
			
		||||
            console.log(`Cannot find note ${this.parentNoteId}`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return note;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										236
									
								
								src/services/note_cache/entities/note.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								src/services/note_cache/entities/note.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,236 @@
 | 
			
		||||
export default class Note {
 | 
			
		||||
    constructor(row) {
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        this.noteId = row.noteId;
 | 
			
		||||
        /** @param {string} */
 | 
			
		||||
        this.title = row.title;
 | 
			
		||||
        /** @param {boolean} */
 | 
			
		||||
        this.isProtected = !!row.isProtected;
 | 
			
		||||
        /** @param {boolean} */
 | 
			
		||||
        this.isDecrypted = !row.isProtected || !!row.isContentAvailable;
 | 
			
		||||
        /** @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 = [];
 | 
			
		||||
 | 
			
		||||
        /** @param {string|null} */
 | 
			
		||||
        this.flatTextCache = null;
 | 
			
		||||
 | 
			
		||||
        if (protectedSessionService.isProtectedSessionAvailable()) {
 | 
			
		||||
            decryptProtectedNote(this);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Attribute[]} */
 | 
			
		||||
    get attributes() {
 | 
			
		||||
        if (!this.attributeCache) {
 | 
			
		||||
            const parentAttributes = this.ownedAttributes.slice();
 | 
			
		||||
 | 
			
		||||
            if (this.noteId !== 'root') {
 | 
			
		||||
                for (const parentNote of this.parents) {
 | 
			
		||||
                    parentAttributes.push(...parentNote.inheritableAttributes);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const templateAttributes = [];
 | 
			
		||||
 | 
			
		||||
            for (const ownedAttr of parentAttributes) { // parentAttributes so we process also inherited templates
 | 
			
		||||
                if (ownedAttr.type === 'relation' && ownedAttr.name === 'template') {
 | 
			
		||||
                    const templateNote = notes[ownedAttr.value];
 | 
			
		||||
 | 
			
		||||
                    if (templateNote) {
 | 
			
		||||
                        templateAttributes.push(...templateNote.attributes);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.attributeCache = parentAttributes.concat(templateAttributes);
 | 
			
		||||
            this.inheritableAttributeCache = [];
 | 
			
		||||
 | 
			
		||||
            for (const attr of this.attributeCache) {
 | 
			
		||||
                if (attr.isInheritable) {
 | 
			
		||||
                    this.inheritableAttributeCache.push(attr);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.attributeCache;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Attribute[]} */
 | 
			
		||||
    get inheritableAttributes() {
 | 
			
		||||
        if (!this.inheritableAttributeCache) {
 | 
			
		||||
            this.attributes; // will refresh also this.inheritableAttributeCache
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.inheritableAttributeCache;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    hasAttribute(type, name) {
 | 
			
		||||
        return this.attributes.find(attr => attr.type === type && attr.name === name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get isArchived() {
 | 
			
		||||
        return this.hasAttribute('label', 'archived');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get isHideInAutocompleteOrArchived() {
 | 
			
		||||
        return this.attributes.find(attr =>
 | 
			
		||||
            attr.type === 'label'
 | 
			
		||||
            && ["archived", "hideInAutocomplete"].includes(attr.name));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get hasInheritableOwnedArchivedLabel() {
 | 
			
		||||
        return !!this.ownedAttributes.find(attr => attr.type === 'label' && attr.name === 'archived' && attr.isInheritable);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // will sort the parents so that non-archived are first and archived at the end
 | 
			
		||||
    // this is done so that non-archived paths are always explored as first when searching for note path
 | 
			
		||||
    resortParents() {
 | 
			
		||||
        this.parents.sort((a, b) => a.hasInheritableOwnedArchivedLabel ? 1 : -1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return {string} - returns flattened textual representation of note, prefixes and attributes usable for searching
 | 
			
		||||
     */
 | 
			
		||||
    get flatText() {
 | 
			
		||||
        if (!this.flatTextCache) {
 | 
			
		||||
            if (this.isHideInAutocompleteOrArchived) {
 | 
			
		||||
                this.flatTextCache = " "; // can't be empty
 | 
			
		||||
                return this.flatTextCache;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.flatTextCache = '';
 | 
			
		||||
 | 
			
		||||
            for (const branch of this.parentBranches) {
 | 
			
		||||
                if (branch.prefix) {
 | 
			
		||||
                    this.flatTextCache += branch.prefix + ' - ';
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.flatTextCache += this.title;
 | 
			
		||||
 | 
			
		||||
            for (const attr of this.attributes) {
 | 
			
		||||
                // it's best to use space as separator since spaces are filtered from the search string by the tokenization into words
 | 
			
		||||
                this.flatTextCache += (attr.type === 'label' ? '#' : '@') + attr.name;
 | 
			
		||||
 | 
			
		||||
                if (attr.value) {
 | 
			
		||||
                    this.flatTextCache += '=' + attr.value;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.flatTextCache = this.flatTextCache.toLowerCase();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.flatTextCache;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    invalidateThisCache() {
 | 
			
		||||
        this.flatTextCache = null;
 | 
			
		||||
 | 
			
		||||
        this.attributeCache = null;
 | 
			
		||||
        this.inheritableAttributeCache = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    invalidateSubtreeCaches() {
 | 
			
		||||
        this.invalidateThisCache();
 | 
			
		||||
 | 
			
		||||
        for (const childNote of this.children) {
 | 
			
		||||
            childNote.invalidateSubtreeCaches();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (const targetRelation of this.targetRelations) {
 | 
			
		||||
            if (targetRelation.name === 'template') {
 | 
			
		||||
                const note = targetRelation.note;
 | 
			
		||||
 | 
			
		||||
                if (note) {
 | 
			
		||||
                    note.invalidateSubtreeCaches();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    invalidateSubtreeFlatText() {
 | 
			
		||||
        this.flatTextCache = null;
 | 
			
		||||
 | 
			
		||||
        for (const childNote of this.children) {
 | 
			
		||||
            childNote.invalidateSubtreeFlatText();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (const targetRelation of this.targetRelations) {
 | 
			
		||||
            if (targetRelation.name === 'template') {
 | 
			
		||||
                const note = targetRelation.note;
 | 
			
		||||
 | 
			
		||||
                if (note) {
 | 
			
		||||
                    note.invalidateSubtreeFlatText();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get isTemplate() {
 | 
			
		||||
        return !!this.targetRelations.find(rel => rel.name === 'template');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Note[]} */
 | 
			
		||||
    get subtreeNotesIncludingTemplated() {
 | 
			
		||||
        const arr = [[this]];
 | 
			
		||||
 | 
			
		||||
        for (const childNote of this.children) {
 | 
			
		||||
            arr.push(childNote.subtreeNotesIncludingTemplated);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (const targetRelation of this.targetRelations) {
 | 
			
		||||
            if (targetRelation.name === 'template') {
 | 
			
		||||
                const note = targetRelation.note;
 | 
			
		||||
 | 
			
		||||
                if (note) {
 | 
			
		||||
                    arr.push(note.subtreeNotesIncludingTemplated);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return arr.flat();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Note[]} */
 | 
			
		||||
    get subtreeNotes() {
 | 
			
		||||
        const arr = [[this]];
 | 
			
		||||
 | 
			
		||||
        for (const childNote of this.children) {
 | 
			
		||||
            arr.push(childNote.subtreeNotes);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return arr.flat();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @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 */
 | 
			
		||||
    get templatedNotes() {
 | 
			
		||||
        const arr = [this];
 | 
			
		||||
 | 
			
		||||
        for (const targetRelation of this.targetRelations) {
 | 
			
		||||
            if (targetRelation.name === 'template') {
 | 
			
		||||
                const note = targetRelation.note;
 | 
			
		||||
 | 
			
		||||
                if (note) {
 | 
			
		||||
                    arr.push(note);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return arr;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								src/services/note_cache/expressions/and.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/services/note_cache/expressions/and.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
export default class AndExp {
 | 
			
		||||
    constructor(subExpressions) {
 | 
			
		||||
        this.subExpressions = subExpressions;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    execute(noteSet, searchContext) {
 | 
			
		||||
        for (const subExpression of this.subExpressions) {
 | 
			
		||||
            noteSet = subExpression.execute(noteSet, searchContext);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return noteSet;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										28
									
								
								src/services/note_cache/expressions/equals.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/services/note_cache/expressions/equals.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
			
		||||
export default class EqualsExp {
 | 
			
		||||
    constructor(attributeType, attributeName, attributeValue) {
 | 
			
		||||
        this.attributeType = attributeType;
 | 
			
		||||
        this.attributeName = attributeName;
 | 
			
		||||
        this.attributeValue = attributeValue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    execute(noteSet) {
 | 
			
		||||
        const attrs = findAttributes(this.attributeType, this.attributeName);
 | 
			
		||||
        const resultNoteSet = new NoteSet();
 | 
			
		||||
 | 
			
		||||
        for (const attr of attrs) {
 | 
			
		||||
            const note = attr.note;
 | 
			
		||||
 | 
			
		||||
            if (noteSet.hasNoteId(note.noteId) && attr.value === this.attributeValue) {
 | 
			
		||||
                if (attr.isInheritable) {
 | 
			
		||||
                    resultNoteSet.addAll(note.subtreeNotesIncludingTemplated);
 | 
			
		||||
                }
 | 
			
		||||
                else if (note.isTemplate) {
 | 
			
		||||
                    resultNoteSet.addAll(note.templatedNotes);
 | 
			
		||||
                }
 | 
			
		||||
                else {
 | 
			
		||||
                    resultNoteSet.add(note);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								src/services/note_cache/expressions/exists.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/services/note_cache/expressions/exists.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
export default class ExistsExp {
 | 
			
		||||
    constructor(attributeType, attributeName) {
 | 
			
		||||
        this.attributeType = attributeType;
 | 
			
		||||
        this.attributeName = attributeName;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    execute(noteSet) {
 | 
			
		||||
        const attrs = findAttributes(this.attributeType, this.attributeName);
 | 
			
		||||
        const resultNoteSet = new NoteSet();
 | 
			
		||||
 | 
			
		||||
        for (const attr of attrs) {
 | 
			
		||||
            const note = attr.note;
 | 
			
		||||
 | 
			
		||||
            if (noteSet.hasNoteId(note.noteId)) {
 | 
			
		||||
                if (attr.isInheritable) {
 | 
			
		||||
                    resultNoteSet.addAll(note.subtreeNotesIncludingTemplated);
 | 
			
		||||
                }
 | 
			
		||||
                else if (note.isTemplate) {
 | 
			
		||||
                    resultNoteSet.addAll(note.templatedNotes);
 | 
			
		||||
                }
 | 
			
		||||
                else {
 | 
			
		||||
                    resultNoteSet.add(note);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										125
									
								
								src/services/note_cache/expressions/note_cache_fulltext.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								src/services/note_cache/expressions/note_cache_fulltext.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,125 @@
 | 
			
		||||
export default class NoteCacheFulltextExp {
 | 
			
		||||
    constructor(tokens) {
 | 
			
		||||
        this.tokens = tokens;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    execute(noteSet, searchContext) {
 | 
			
		||||
        const resultNoteSet = new NoteSet();
 | 
			
		||||
 | 
			
		||||
        const candidateNotes = this.getCandidateNotes(noteSet);
 | 
			
		||||
 | 
			
		||||
        for (const note of candidateNotes) {
 | 
			
		||||
            // autocomplete should be able to find notes by their noteIds as well (only leafs)
 | 
			
		||||
            if (this.tokens.length === 1 && note.noteId === this.tokens[0]) {
 | 
			
		||||
                this.searchDownThePath(note, [], [], resultNoteSet, searchContext);
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // for leaf note it doesn't matter if "archived" label is inheritable or not
 | 
			
		||||
            if (note.isArchived) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const foundAttrTokens = [];
 | 
			
		||||
 | 
			
		||||
            for (const attribute of note.ownedAttributes) {
 | 
			
		||||
                for (const token of this.tokens) {
 | 
			
		||||
                    if (attribute.name.toLowerCase().includes(token)
 | 
			
		||||
                        || attribute.value.toLowerCase().includes(token)) {
 | 
			
		||||
                        foundAttrTokens.push(token);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            for (const parentNote of note.parents) {
 | 
			
		||||
                const title = getNoteTitle(note.noteId, parentNote.noteId).toLowerCase();
 | 
			
		||||
                const foundTokens = foundAttrTokens.slice();
 | 
			
		||||
 | 
			
		||||
                for (const token of this.tokens) {
 | 
			
		||||
                    if (title.includes(token)) {
 | 
			
		||||
                        foundTokens.push(token);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (foundTokens.length > 0) {
 | 
			
		||||
                    const remainingTokens = this.tokens.filter(token => !foundTokens.includes(token));
 | 
			
		||||
 | 
			
		||||
                    this.searchDownThePath(parentNote, remainingTokens, [note.noteId], resultNoteSet, searchContext);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return resultNoteSet;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns noteIds which have at least one matching tokens
 | 
			
		||||
     *
 | 
			
		||||
     * @param {NoteSet} noteSet
 | 
			
		||||
     * @return {String[]}
 | 
			
		||||
     */
 | 
			
		||||
    getCandidateNotes(noteSet) {
 | 
			
		||||
        const candidateNotes = [];
 | 
			
		||||
 | 
			
		||||
        for (const note of noteSet.notes) {
 | 
			
		||||
            for (const token of this.tokens) {
 | 
			
		||||
                if (note.flatText.includes(token)) {
 | 
			
		||||
                    candidateNotes.push(note);
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return candidateNotes;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    searchDownThePath(note, tokens, path, resultNoteSet, searchContext) {
 | 
			
		||||
        if (tokens.length === 0) {
 | 
			
		||||
            const retPath = getSomePath(note, path);
 | 
			
		||||
 | 
			
		||||
            if (retPath) {
 | 
			
		||||
                const noteId = retPath[retPath.length - 1];
 | 
			
		||||
                searchContext.noteIdToNotePath[noteId] = retPath;
 | 
			
		||||
 | 
			
		||||
                resultNoteSet.add(notes[noteId]);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!note.parents.length === 0 || note.noteId === 'root') {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const foundAttrTokens = [];
 | 
			
		||||
 | 
			
		||||
        for (const attribute of note.ownedAttributes) {
 | 
			
		||||
            for (const token of tokens) {
 | 
			
		||||
                if (attribute.name.toLowerCase().includes(token)
 | 
			
		||||
                    || attribute.value.toLowerCase().includes(token)) {
 | 
			
		||||
                    foundAttrTokens.push(token);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (const parentNote of note.parents) {
 | 
			
		||||
            const title = getNoteTitle(note.noteId, parentNote.noteId).toLowerCase();
 | 
			
		||||
            const foundTokens = foundAttrTokens.slice();
 | 
			
		||||
 | 
			
		||||
            for (const token of tokens) {
 | 
			
		||||
                if (title.includes(token)) {
 | 
			
		||||
                    foundTokens.push(token);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (foundTokens.length > 0) {
 | 
			
		||||
                const remainingTokens = tokens.filter(token => !foundTokens.includes(token));
 | 
			
		||||
 | 
			
		||||
                this.searchDownThePath(parentNote, remainingTokens, path.concat([note.noteId]), resultNoteSet, searchContext);
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                this.searchDownThePath(parentNote, tokens, path.concat([note.noteId]), resultNoteSet, searchContext);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										26
									
								
								src/services/note_cache/expressions/note_content_fulltext.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/services/note_cache/expressions/note_content_fulltext.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
			
		||||
export default class NoteContentFulltextExp {
 | 
			
		||||
    constructor(tokens) {
 | 
			
		||||
        this.tokens = tokens;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async execute(noteSet) {
 | 
			
		||||
        const resultNoteSet = new NoteSet();
 | 
			
		||||
        const wheres = this.tokens.map(token => "note_contents.content LIKE " + utils.prepareSqlForLike('%', token, '%'));
 | 
			
		||||
 | 
			
		||||
        const noteIds = await sql.getColumn(`
 | 
			
		||||
            SELECT notes.noteId 
 | 
			
		||||
            FROM notes
 | 
			
		||||
            JOIN note_contents ON notes.noteId = note_contents.noteId
 | 
			
		||||
            WHERE isDeleted = 0 AND isProtected = 0 AND ${wheres.join(' AND ')}`);
 | 
			
		||||
 | 
			
		||||
        const results = [];
 | 
			
		||||
 | 
			
		||||
        for (const noteId of noteIds) {
 | 
			
		||||
            if (noteSet.hasNoteId(noteId) && noteId in notes) {
 | 
			
		||||
                resultNoteSet.add(notes[noteId]);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return results;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								src/services/note_cache/expressions/or.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/services/note_cache/expressions/or.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
export default class OrExp {
 | 
			
		||||
    constructor(subExpressions) {
 | 
			
		||||
        this.subExpressions = subExpressions;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    execute(noteSet, searchContext) {
 | 
			
		||||
        const resultNoteSet = new NoteSet();
 | 
			
		||||
 | 
			
		||||
        for (const subExpression of this.subExpressions) {
 | 
			
		||||
            resultNoteSet.mergeIn(subExpression.execute(noteSet, searchContext));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return resultNoteSet;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										224
									
								
								src/services/note_cache/note_cache.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										224
									
								
								src/services/note_cache/note_cache.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,224 @@
 | 
			
		||||
import treeCache from "../../public/app/services/tree_cache.js";
 | 
			
		||||
 | 
			
		||||
const sql = require('../sql.js');
 | 
			
		||||
const sqlInit = require('../sql_init.js');
 | 
			
		||||
const eventService = require('../events.js');
 | 
			
		||||
const protectedSessionService = require('../protected_session.js');
 | 
			
		||||
const utils = require('../utils.js');
 | 
			
		||||
const hoistedNoteService = require('../hoisted_note.js');
 | 
			
		||||
const stringSimilarity = require('string-similarity');
 | 
			
		||||
 | 
			
		||||
class NoteCache {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        /** @type {Object.<String, Note>} */
 | 
			
		||||
        this.notes = null;
 | 
			
		||||
        /** @type {Object.<String, Branch>} */
 | 
			
		||||
        this.branches = null;
 | 
			
		||||
        /** @type {Object.<String, Branch>} */
 | 
			
		||||
        this.childParentToBranch = {};
 | 
			
		||||
        /** @type {Object.<String, Attribute>} */
 | 
			
		||||
        this.attributes = null;
 | 
			
		||||
        /** @type {Object.<String, Attribute[]>} Points from attribute type-name to list of attributes them */
 | 
			
		||||
        this.attributeIndex = null;
 | 
			
		||||
 | 
			
		||||
        this.loaded = false;
 | 
			
		||||
        this.loadedPromiseResolve;
 | 
			
		||||
        /** Is resolved after the initial load */
 | 
			
		||||
        this.loadedPromise = new Promise(res => this.loadedPromiseResolve = res);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return {Attribute[]} */
 | 
			
		||||
    findAttributes(type, name) {
 | 
			
		||||
        return this.attributeIndex[`${type}-${name}`] || [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async load() {
 | 
			
		||||
        this.notes = await this.getMappedRows(`SELECT noteId, title, isProtected FROM notes WHERE isDeleted = 0`,
 | 
			
		||||
            row => new Note(row));
 | 
			
		||||
 | 
			
		||||
        this.branches = await this.getMappedRows(`SELECT branchId, noteId, parentNoteId, prefix FROM branches WHERE isDeleted = 0`,
 | 
			
		||||
            row => new Branch(row));
 | 
			
		||||
 | 
			
		||||
        this.attributeIndex = [];
 | 
			
		||||
 | 
			
		||||
        this.attributes = await this.getMappedRows(`SELECT attributeId, noteId, type, name, value, isInheritable FROM attributes WHERE isDeleted = 0`,
 | 
			
		||||
            row => new Attribute(row));
 | 
			
		||||
 | 
			
		||||
        this.loaded = true;
 | 
			
		||||
        this.loadedPromiseResolve();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getMappedRows(query, cb) {
 | 
			
		||||
        const map = {};
 | 
			
		||||
        const results = await sql.getRows(query, []);
 | 
			
		||||
 | 
			
		||||
        for (const row of results) {
 | 
			
		||||
            const keys = Object.keys(row);
 | 
			
		||||
 | 
			
		||||
            map[row[keys[0]]] = cb(row);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return map;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    decryptProtectedNote(note) {
 | 
			
		||||
        if (note.isProtected && !note.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) {
 | 
			
		||||
            note.title = protectedSessionService.decryptString(note.title);
 | 
			
		||||
 | 
			
		||||
            note.isDecrypted = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    decryptProtectedNotes() {
 | 
			
		||||
        for (const note of Object.values(this.notes)) {
 | 
			
		||||
            decryptProtectedNote(note);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const noteCache = new NoteCache();
 | 
			
		||||
 | 
			
		||||
eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED, eventService.ENTITY_SYNCED],  async ({entityName, entity}) => {
 | 
			
		||||
    // note that entity can also be just POJO without methods if coming from sync
 | 
			
		||||
 | 
			
		||||
    if (!noteCache.loaded) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (entityName === 'notes') {
 | 
			
		||||
        const {noteId} = entity;
 | 
			
		||||
 | 
			
		||||
        if (entity.isDeleted) {
 | 
			
		||||
            delete noteCache.notes[noteId];
 | 
			
		||||
        }
 | 
			
		||||
        else if (noteId in noteCache.notes) {
 | 
			
		||||
            const note = noteCache.notes[noteId];
 | 
			
		||||
 | 
			
		||||
            // we can assume we have protected session since we managed to update
 | 
			
		||||
            note.title = entity.title;
 | 
			
		||||
            note.isProtected = entity.isProtected;
 | 
			
		||||
            note.isDecrypted = !entity.isProtected || !!entity.isContentAvailable;
 | 
			
		||||
            note.flatTextCache = null;
 | 
			
		||||
 | 
			
		||||
            decryptProtectedNote(note);
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            const note = new Note(entity);
 | 
			
		||||
            noteCache.notes[noteId] = note;
 | 
			
		||||
 | 
			
		||||
            decryptProtectedNote(note);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    else if (entityName === 'branches') {
 | 
			
		||||
        const {branchId, noteId, parentNoteId} = entity;
 | 
			
		||||
        const childNote = noteCache.notes[noteId];
 | 
			
		||||
 | 
			
		||||
        if (entity.isDeleted) {
 | 
			
		||||
            if (childNote) {
 | 
			
		||||
                childNote.parents = childNote.parents.filter(parent => parent.noteId !== parentNoteId);
 | 
			
		||||
                childNote.parentBranches = childNote.parentBranches.filter(branch => branch.branchId !== branchId);
 | 
			
		||||
 | 
			
		||||
                if (childNote.parents.length > 0) {
 | 
			
		||||
                    childNote.invalidateSubtreeCaches();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const parentNote = noteCache.notes[parentNoteId];
 | 
			
		||||
 | 
			
		||||
            if (parentNote) {
 | 
			
		||||
                parentNote.children = parentNote.children.filter(child => child.noteId !== noteId);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            delete noteCache.childParentToBranch[`${noteId}-${parentNoteId}`];
 | 
			
		||||
            delete noteCache.branches[branchId];
 | 
			
		||||
        }
 | 
			
		||||
        else if (branchId in noteCache.branches) {
 | 
			
		||||
            // only relevant thing which can change in a branch is prefix
 | 
			
		||||
            noteCache.branches[branchId].prefix = entity.prefix;
 | 
			
		||||
 | 
			
		||||
            if (childNote) {
 | 
			
		||||
                childNote.flatTextCache = null;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            noteCache.branches[branchId] = new Branch(entity);
 | 
			
		||||
 | 
			
		||||
            if (childNote) {
 | 
			
		||||
                childNote.resortParents();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    else if (entityName === 'attributes') {
 | 
			
		||||
        const {attributeId, noteId} = entity;
 | 
			
		||||
        const note = noteCache.notes[noteId];
 | 
			
		||||
        const attr = noteCache.attributes[attributeId];
 | 
			
		||||
 | 
			
		||||
        if (entity.isDeleted) {
 | 
			
		||||
            if (note && attr) {
 | 
			
		||||
                // first invalidate and only then remove the attribute (otherwise invalidation wouldn't be complete)
 | 
			
		||||
                if (attr.isAffectingSubtree || note.isTemplate) {
 | 
			
		||||
                    note.invalidateSubtreeCaches();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                note.ownedAttributes = note.ownedAttributes.filter(attr => attr.attributeId !== attributeId);
 | 
			
		||||
 | 
			
		||||
                const targetNote = attr.targetNote;
 | 
			
		||||
 | 
			
		||||
                if (targetNote) {
 | 
			
		||||
                    targetNote.targetRelations = targetNote.targetRelations.filter(rel => rel.attributeId !== attributeId);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            delete noteCache.attributes[attributeId];
 | 
			
		||||
            delete noteCache.attributeIndex[`${attr.type}-${attr.name}`];
 | 
			
		||||
        }
 | 
			
		||||
        else if (attributeId in noteCache.attributes) {
 | 
			
		||||
            const attr = noteCache.attributes[attributeId];
 | 
			
		||||
 | 
			
		||||
            // attr name and isInheritable are immutable
 | 
			
		||||
            attr.value = entity.value;
 | 
			
		||||
 | 
			
		||||
            if (attr.isAffectingSubtree || note.isTemplate) {
 | 
			
		||||
                note.invalidateSubtreeFlatText();
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                note.flatTextCache = null;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            const attr = new Attribute(entity);
 | 
			
		||||
            noteCache.attributes[attributeId] = attr;
 | 
			
		||||
 | 
			
		||||
            if (note) {
 | 
			
		||||
                if (attr.isAffectingSubtree || note.isTemplate) {
 | 
			
		||||
                    note.invalidateSubtreeCaches();
 | 
			
		||||
                }
 | 
			
		||||
                else {
 | 
			
		||||
                    this.invalidateThisCache();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function getBranch(childNoteId, parentNoteId) {
 | 
			
		||||
    return noteCache.childParentToBranch[`${childNoteId}-${parentNoteId}`];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, () => {
 | 
			
		||||
    noteCache.loadedPromise.then(() => noteCache.decryptProtectedNotes());
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
sqlInit.dbReady.then(() => utils.stopWatch("Note cache load", () => treeCache.load()));
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    loadedPromise,
 | 
			
		||||
    findNotesForAutocomplete,
 | 
			
		||||
    getNotePath,
 | 
			
		||||
    getNoteTitleForPath,
 | 
			
		||||
    isAvailable,
 | 
			
		||||
    isArchived,
 | 
			
		||||
    isInAncestor,
 | 
			
		||||
    load,
 | 
			
		||||
    findSimilarNotes
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										233
									
								
								src/services/note_cache/note_cache_service.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										233
									
								
								src/services/note_cache/note_cache_service.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,233 @@
 | 
			
		||||
function isNotePathArchived(notePath) {
 | 
			
		||||
    const noteId = notePath[notePath.length - 1];
 | 
			
		||||
    const note = noteCache.notes[noteId];
 | 
			
		||||
 | 
			
		||||
    if (note.isArchived) {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < notePath.length - 1; i++) {
 | 
			
		||||
        const note = noteCache.notes[notePath[i]];
 | 
			
		||||
 | 
			
		||||
        // this is going through parents so archived must be inheritable
 | 
			
		||||
        if (note.hasInheritableOwnedArchivedLabel) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This assumes that note is available. "archived" note means that there isn't a single non-archived note-path
 | 
			
		||||
 * leading to this note.
 | 
			
		||||
 *
 | 
			
		||||
 * @param noteId
 | 
			
		||||
 */
 | 
			
		||||
function isArchived(noteId) {
 | 
			
		||||
    const notePath = getSomePath(noteId);
 | 
			
		||||
 | 
			
		||||
    return isNotePathArchived(notePath);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {string} noteId
 | 
			
		||||
 * @param {string} ancestorNoteId
 | 
			
		||||
 * @return {boolean} - true if given noteId has ancestorNoteId in any of its paths (even archived)
 | 
			
		||||
 */
 | 
			
		||||
function isInAncestor(noteId, ancestorNoteId) {
 | 
			
		||||
    if (ancestorNoteId === 'root' || ancestorNoteId === noteId) {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const note = noteCache.notes[noteId];
 | 
			
		||||
 | 
			
		||||
    for (const parentNote of note.parents) {
 | 
			
		||||
        if (isInAncestor(parentNote.noteId, ancestorNoteId)) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getNoteTitle(childNoteId, parentNoteId) {
 | 
			
		||||
    const childNote = noteCache.notes[childNoteId];
 | 
			
		||||
    const parentNote = noteCache.notes[parentNoteId];
 | 
			
		||||
 | 
			
		||||
    let title;
 | 
			
		||||
 | 
			
		||||
    if (childNote.isProtected) {
 | 
			
		||||
        title = protectedSessionService.isProtectedSessionAvailable() ? childNote.title : '[protected]';
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        title = childNote.title;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const branch = parentNote ? getBranch(childNote.noteId, parentNote.noteId) : null;
 | 
			
		||||
 | 
			
		||||
    return ((branch && branch.prefix) ? `${branch.prefix} - ` : '') + title;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getNoteTitleArrayForPath(notePathArray) {
 | 
			
		||||
    const titles = [];
 | 
			
		||||
 | 
			
		||||
    if (notePathArray[0] === hoistedNoteService.getHoistedNoteId() && notePathArray.length === 1) {
 | 
			
		||||
        return [ getNoteTitle(hoistedNoteService.getHoistedNoteId()) ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let parentNoteId = 'root';
 | 
			
		||||
    let hoistedNotePassed = false;
 | 
			
		||||
 | 
			
		||||
    for (const noteId of notePathArray) {
 | 
			
		||||
        // start collecting path segment titles only after hoisted note
 | 
			
		||||
        if (hoistedNotePassed) {
 | 
			
		||||
            const title = getNoteTitle(noteId, parentNoteId);
 | 
			
		||||
 | 
			
		||||
            titles.push(title);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (noteId === hoistedNoteService.getHoistedNoteId()) {
 | 
			
		||||
            hoistedNotePassed = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        parentNoteId = noteId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return titles;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getNoteTitleForPath(notePathArray) {
 | 
			
		||||
    const titles = getNoteTitleArrayForPath(notePathArray);
 | 
			
		||||
 | 
			
		||||
    return titles.join(' / ');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Returns notePath for noteId from cache. Note hoisting is respected.
 | 
			
		||||
 * Archived notes are also returned, but non-archived paths are preferred if available
 | 
			
		||||
 * - this means that archived paths is returned only if there's no non-archived path
 | 
			
		||||
 * - you can check whether returned path is archived using isArchived()
 | 
			
		||||
 */
 | 
			
		||||
function getSomePath(note, path = []) {
 | 
			
		||||
    if (note.noteId === 'root') {
 | 
			
		||||
        path.push(note.noteId);
 | 
			
		||||
        path.reverse();
 | 
			
		||||
 | 
			
		||||
        if (!path.includes(hoistedNoteService.getHoistedNoteId())) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return path;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const parents = note.parents;
 | 
			
		||||
    if (parents.length === 0) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const parentNote of parents) {
 | 
			
		||||
        const retPath = getSomePath(parentNote, path.concat([note.noteId]));
 | 
			
		||||
 | 
			
		||||
        if (retPath) {
 | 
			
		||||
            return retPath;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getNotePath(noteId) {
 | 
			
		||||
    const note = noteCache.notes[noteId];
 | 
			
		||||
    const retPath = getSomePath(note);
 | 
			
		||||
 | 
			
		||||
    if (retPath) {
 | 
			
		||||
        const noteTitle = getNoteTitleForPath(retPath);
 | 
			
		||||
        const parentNote = note.parents[0];
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            noteId: noteId,
 | 
			
		||||
            branchId: getBranch(noteId, parentNote.noteId).branchId,
 | 
			
		||||
            title: noteTitle,
 | 
			
		||||
            notePath: retPath,
 | 
			
		||||
            path: retPath.join('/')
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function evaluateSimilarity(sourceNote, candidateNote, results) {
 | 
			
		||||
    let coeff = stringSimilarity.compareTwoStrings(sourceNote.flatText, candidateNote.flatText);
 | 
			
		||||
 | 
			
		||||
    if (coeff > 0.4) {
 | 
			
		||||
        const notePath = getSomePath(candidateNote);
 | 
			
		||||
 | 
			
		||||
        // this takes care of note hoisting
 | 
			
		||||
        if (!notePath) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (isNotePathArchived(notePath)) {
 | 
			
		||||
            coeff -= 0.2; // archived penalization
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        results.push({coeff, notePath, noteId: candidateNote.noteId});
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Point of this is to break up long running sync process to avoid blocking
 | 
			
		||||
 * see https://snyk.io/blog/nodejs-how-even-quick-async-functions-can-block-the-event-loop-starve-io/
 | 
			
		||||
 */
 | 
			
		||||
function setImmediatePromise() {
 | 
			
		||||
    return new Promise((resolve) => {
 | 
			
		||||
        setTimeout(() => resolve(), 0);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function findSimilarNotes(noteId) {
 | 
			
		||||
    const results = [];
 | 
			
		||||
    let i = 0;
 | 
			
		||||
 | 
			
		||||
    const origNote = noteCache.notes[noteId];
 | 
			
		||||
 | 
			
		||||
    if (!origNote) {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const note of Object.values(notes)) {
 | 
			
		||||
        if (note.isProtected && !note.isDecrypted) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        evaluateSimilarity(origNote, note, results);
 | 
			
		||||
 | 
			
		||||
        i++;
 | 
			
		||||
 | 
			
		||||
        if (i % 200 === 0) {
 | 
			
		||||
            await setImmediatePromise();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    results.sort((a, b) => a.coeff > b.coeff ? -1 : 1);
 | 
			
		||||
 | 
			
		||||
    return results.length > 50 ? results.slice(0, 50) : results;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param noteId
 | 
			
		||||
 * @returns {boolean} - true if note exists (is not deleted) and is available in current note hoisting
 | 
			
		||||
 */
 | 
			
		||||
function isAvailable(noteId) {
 | 
			
		||||
    const notePath = getNotePath(noteId);
 | 
			
		||||
 | 
			
		||||
    return !!notePath;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    getNotePath,
 | 
			
		||||
    getNoteTitleForPath,
 | 
			
		||||
    isAvailable,
 | 
			
		||||
    isArchived,
 | 
			
		||||
    isInAncestor,
 | 
			
		||||
    findSimilarNotes
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										22
									
								
								src/services/note_cache/note_set.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/services/note_cache/note_set.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
export default class NoteSet {
 | 
			
		||||
    constructor(notes = []) {
 | 
			
		||||
        this.notes = notes;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    add(note) {
 | 
			
		||||
        this.notes.push(note);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    addAll(notes) {
 | 
			
		||||
        this.notes.push(...notes);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    hasNoteId(noteId) {
 | 
			
		||||
        // TODO: optimize
 | 
			
		||||
        return !!this.notes.find(note => note.noteId === noteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    mergeIn(anotherNoteSet) {
 | 
			
		||||
        this.notes = this.notes.concat(anotherNoteSet.arr);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										113
									
								
								src/services/note_cache/search.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								src/services/note_cache/search.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,113 @@
 | 
			
		||||
async function findNotesWithExpression(expression) {
 | 
			
		||||
 | 
			
		||||
    const hoistedNote = notes[hoistedNoteService.getHoistedNoteId()];
 | 
			
		||||
    const allNotes = (hoistedNote && hoistedNote.noteId !== 'root')
 | 
			
		||||
        ? hoistedNote.subtreeNotes
 | 
			
		||||
        : Object.values(notes);
 | 
			
		||||
 | 
			
		||||
    const allNoteSet = new NoteSet(allNotes);
 | 
			
		||||
 | 
			
		||||
    const searchContext = {
 | 
			
		||||
        noteIdToNotePath: {}
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const noteSet = await expression.execute(allNoteSet, searchContext);
 | 
			
		||||
 | 
			
		||||
    let searchResults = noteSet.notes
 | 
			
		||||
        .map(note => searchContext.noteIdToNotePath[note.noteId] || getSomePath(note))
 | 
			
		||||
        .filter(notePathArray => notePathArray.includes(hoistedNoteService.getHoistedNoteId()))
 | 
			
		||||
        .map(notePathArray => new SearchResult(notePathArray));
 | 
			
		||||
 | 
			
		||||
    // sort results by depth of the note. This is based on the assumption that more important results
 | 
			
		||||
    // are closer to the note root.
 | 
			
		||||
    searchResults.sort((a, b) => {
 | 
			
		||||
        if (a.notePathArray.length === b.notePathArray.length) {
 | 
			
		||||
            return a.notePathTitle < b.notePathTitle ? -1 : 1;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return a.notePathArray.length < b.notePathArray.length ? -1 : 1;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return searchResults;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function findNotesForAutocomplete(query) {
 | 
			
		||||
    if (!query.trim().length) {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const tokens = query
 | 
			
		||||
        .trim() // necessary because even with .split() trailing spaces are tokens which causes havoc
 | 
			
		||||
        .toLowerCase()
 | 
			
		||||
        .split(/[ -]/)
 | 
			
		||||
        .filter(token => token !== '/'); // '/' is used as separator
 | 
			
		||||
 | 
			
		||||
    const expression = new NoteCacheFulltextExp(tokens);
 | 
			
		||||
 | 
			
		||||
    let searchResults = await findNotesWithExpression(expression);
 | 
			
		||||
 | 
			
		||||
    searchResults = searchResults.slice(0, 200);
 | 
			
		||||
 | 
			
		||||
    highlightSearchResults(searchResults, tokens);
 | 
			
		||||
 | 
			
		||||
    return searchResults.map(result => {
 | 
			
		||||
        return {
 | 
			
		||||
            notePath: result.notePath,
 | 
			
		||||
            notePathTitle: result.notePathTitle,
 | 
			
		||||
            highlightedNotePathTitle: result.highlightedNotePathTitle
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function highlightSearchResults(searchResults, tokens) {
 | 
			
		||||
    // we remove < signs because they can cause trouble in matching and overwriting existing highlighted chunks
 | 
			
		||||
    // which would make the resulting HTML string invalid.
 | 
			
		||||
    // { and } are used for marking <b> and </b> tag (to avoid matches on single 'b' character)
 | 
			
		||||
    tokens = tokens.map(token => token.replace('/[<\{\}]/g', ''));
 | 
			
		||||
 | 
			
		||||
    // sort by the longest so we first highlight longest matches
 | 
			
		||||
    tokens.sort((a, b) => a.length > b.length ? -1 : 1);
 | 
			
		||||
 | 
			
		||||
    for (const result of searchResults) {
 | 
			
		||||
        const note = notes[result.noteId];
 | 
			
		||||
 | 
			
		||||
        result.highlightedNotePathTitle = result.notePathTitle;
 | 
			
		||||
 | 
			
		||||
        for (const attr of note.attributes) {
 | 
			
		||||
            if (tokens.find(token => attr.name.includes(token) || attr.value.includes(token))) {
 | 
			
		||||
                result.highlightedNotePathTitle += ` <small>${formatAttribute(attr)}</small>`;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const token of tokens) {
 | 
			
		||||
        const tokenRegex = new RegExp("(" + utils.escapeRegExp(token) + ")", "gi");
 | 
			
		||||
 | 
			
		||||
        for (const result of searchResults) {
 | 
			
		||||
            result.highlightedNotePathTitle = result.highlightedNotePathTitle.replace(tokenRegex, "{$1}");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const result of searchResults) {
 | 
			
		||||
        result.highlightedNotePathTitle = result.highlightedNotePathTitle
 | 
			
		||||
            .replace(/{/g, "<b>")
 | 
			
		||||
            .replace(/}/g, "</b>");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function formatAttribute(attr) {
 | 
			
		||||
    if (attr.type === 'relation') {
 | 
			
		||||
        return '@' + utils.escapeHtml(attr.name) + "=…";
 | 
			
		||||
    }
 | 
			
		||||
    else if (attr.type === 'label') {
 | 
			
		||||
        let label = '#' + utils.escapeHtml(attr.name);
 | 
			
		||||
 | 
			
		||||
        if (attr.value) {
 | 
			
		||||
            const val = /[^\w_-]/.test(attr.value) ? '"' + attr.value + '"' : attr.value;
 | 
			
		||||
 | 
			
		||||
            label += '=' + utils.escapeHtml(val);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return label;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								src/services/note_cache/search_result.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/services/note_cache/search_result.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
			
		||||
export default class SearchResult {
 | 
			
		||||
    constructor(notePathArray) {
 | 
			
		||||
        this.notePathArray = notePathArray;
 | 
			
		||||
        this.notePathTitle = getNoteTitleForPath(notePathArray);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get notePath() {
 | 
			
		||||
        return this.notePathArray.join('/');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get noteId() {
 | 
			
		||||
        return this.notePathArray[this.notePathArray.length - 1];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -3,7 +3,7 @@ const sql = require('./sql');
 | 
			
		||||
const log = require('./log');
 | 
			
		||||
const parseFilters = require('./parse_filters');
 | 
			
		||||
const buildSearchQuery = require('./build_search_query');
 | 
			
		||||
const noteCacheService = require('./note_cache');
 | 
			
		||||
const noteCacheService = require('./note_cache/note_cache.js');
 | 
			
		||||
 | 
			
		||||
async function searchForNotes(searchString) {
 | 
			
		||||
    const noteIds = await searchForNoteIds(searchString);
 | 
			
		||||
@ -71,4 +71,4 @@ async function searchForNoteIds(searchString) {
 | 
			
		||||
module.exports = {
 | 
			
		||||
    searchForNotes,
 | 
			
		||||
    searchForNoteIds
 | 
			
		||||
};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ const repository = require('./repository');
 | 
			
		||||
const Branch = require('../entities/branch');
 | 
			
		||||
const syncTableService = require('./sync_table');
 | 
			
		||||
const protectedSessionService = require('./protected_session');
 | 
			
		||||
const noteCacheService = require('./note_cache');
 | 
			
		||||
const noteCacheService = require('./note_cache/note_cache.js');
 | 
			
		||||
 | 
			
		||||
async function getNotes(noteIds) {
 | 
			
		||||
    // we return also deleted notes which have been specifically asked for
 | 
			
		||||
@ -197,4 +197,4 @@ module.exports = {
 | 
			
		||||
    validateParentChild,
 | 
			
		||||
    sortNotesAlphabetically,
 | 
			
		||||
    setNoteToParent
 | 
			
		||||
};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user