mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	note cache breakup into classes, WIP
This commit is contained in:
		| @@ -1,6 +1,6 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const noteCacheService = require('../../services/note_cache'); | const noteCacheService = require('../../services/note_cache/note_cache.js'); | ||||||
| const repository = require('../../services/repository'); | const repository = require('../../services/repository'); | ||||||
| const log = require('../../services/log'); | const log = require('../../services/log'); | ||||||
| const utils = require('../../services/utils'); | const utils = require('../../services/utils'); | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ const zipImportService = require('../../services/import/zip'); | |||||||
| const singleImportService = require('../../services/import/single'); | const singleImportService = require('../../services/import/single'); | ||||||
| const cls = require('../../services/cls'); | const cls = require('../../services/cls'); | ||||||
| const path = require('path'); | 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 log = require('../../services/log'); | ||||||
| const TaskContext = require('../../services/task_context.js'); | const TaskContext = require('../../services/task_context.js'); | ||||||
|  |  | ||||||
| @@ -85,4 +85,4 @@ async function importToBranch(req) { | |||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     importToBranch |     importToBranch | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const repository = require('../../services/repository'); | 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 protectedSessionService = require('../../services/protected_session'); | ||||||
| const noteRevisionService = require('../../services/note_revisions'); | const noteRevisionService = require('../../services/note_revisions'); | ||||||
| const utils = require('../../services/utils'); | const utils = require('../../services/utils'); | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ | |||||||
| const sql = require('../../services/sql'); | const sql = require('../../services/sql'); | ||||||
| const protectedSessionService = require('../../services/protected_session'); | const protectedSessionService = require('../../services/protected_session'); | ||||||
| const noteService = require('../../services/notes'); | const noteService = require('../../services/notes'); | ||||||
| const noteCacheService = require('../../services/note_cache'); | const noteCacheService = require('../../services/note_cache/note_cache.js'); | ||||||
|  |  | ||||||
| async function getRecentChanges(req) { | async function getRecentChanges(req) { | ||||||
|     const {ancestorNoteId} = req.params; |     const {ancestorNoteId} = req.params; | ||||||
| @@ -102,4 +102,4 @@ async function getRecentChanges(req) { | |||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     getRecentChanges |     getRecentChanges | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const repository = require('../../services/repository'); | 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 log = require('../../services/log'); | ||||||
| const scriptService = require('../../services/script'); | const scriptService = require('../../services/script'); | ||||||
| const searchService = require('../../services/search'); | const searchService = require('../../services/search'); | ||||||
| @@ -110,4 +110,4 @@ async function searchFromRelation(note, relationName) { | |||||||
| module.exports = { | module.exports = { | ||||||
|     searchNotes, |     searchNotes, | ||||||
|     searchFromNote |     searchFromNote | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const noteCacheService = require('../../services/note_cache'); | const noteCacheService = require('../../services/note_cache/note_cache.js'); | ||||||
| const repository = require('../../services/repository'); | const repository = require('../../services/repository'); | ||||||
|  |  | ||||||
| async function getSimilarNotes(req) { | 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 log = require('./log'); | ||||||
| const parseFilters = require('./parse_filters'); | const parseFilters = require('./parse_filters'); | ||||||
| const buildSearchQuery = require('./build_search_query'); | const buildSearchQuery = require('./build_search_query'); | ||||||
| const noteCacheService = require('./note_cache'); | const noteCacheService = require('./note_cache/note_cache.js'); | ||||||
|  |  | ||||||
| async function searchForNotes(searchString) { | async function searchForNotes(searchString) { | ||||||
|     const noteIds = await searchForNoteIds(searchString); |     const noteIds = await searchForNoteIds(searchString); | ||||||
| @@ -71,4 +71,4 @@ async function searchForNoteIds(searchString) { | |||||||
| module.exports = { | module.exports = { | ||||||
|     searchForNotes, |     searchForNotes, | ||||||
|     searchForNoteIds |     searchForNoteIds | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ const repository = require('./repository'); | |||||||
| const Branch = require('../entities/branch'); | const Branch = require('../entities/branch'); | ||||||
| const syncTableService = require('./sync_table'); | const syncTableService = require('./sync_table'); | ||||||
| const protectedSessionService = require('./protected_session'); | const protectedSessionService = require('./protected_session'); | ||||||
| const noteCacheService = require('./note_cache'); | const noteCacheService = require('./note_cache/note_cache.js'); | ||||||
|  |  | ||||||
| async function getNotes(noteIds) { | async function getNotes(noteIds) { | ||||||
|     // we return also deleted notes which have been specifically asked for |     // we return also deleted notes which have been specifically asked for | ||||||
| @@ -197,4 +197,4 @@ module.exports = { | |||||||
|     validateParentChild, |     validateParentChild, | ||||||
|     sortNotesAlphabetically, |     sortNotesAlphabetically, | ||||||
|     setNoteToParent |     setNoteToParent | ||||||
| }; | }; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user