| 
									
										
										
										
											2020-05-17 09:48:24 +02:00
										 |  |  | "use strict"; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const noteCache = require('./note_cache'); | 
					
						
							|  |  |  | const hoistedNoteService = require('../hoisted_note'); | 
					
						
							| 
									
										
										
										
											2020-06-04 00:04:57 +02:00
										 |  |  | const protectedSessionService = require('../protected_session'); | 
					
						
							| 
									
										
										
										
											2020-05-17 10:11:19 +02:00
										 |  |  | const stringSimilarity = require('string-similarity'); | 
					
						
							| 
									
										
										
										
											2020-08-17 23:54:18 +02:00
										 |  |  | const log = require('../log'); | 
					
						
							| 
									
										
										
										
											2020-05-17 09:48:24 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-16 23:12:29 +02:00
										 |  |  | 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]; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-17 23:54:18 +02:00
										 |  |  |     if (!childNote) { | 
					
						
							|  |  |  |         log.info(`Cannot find note in cache for noteId ${childNoteId}`); | 
					
						
							|  |  |  |         return "[error fetching title]"; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-16 23:12:29 +02:00
										 |  |  |     let title; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (childNote.isProtected) { | 
					
						
							|  |  |  |         title = protectedSessionService.isProtectedSessionAvailable() ? childNote.title : '[protected]'; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     else { | 
					
						
							|  |  |  |         title = childNote.title; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-17 10:11:19 +02:00
										 |  |  |     const branch = parentNote ? noteCache.getBranch(childNote.noteId, parentNote.noteId) : null; | 
					
						
							| 
									
										
										
										
											2020-05-16 23:12:29 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     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]; | 
					
						
							| 
									
										
										
										
											2020-08-28 23:20:22 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     if (!note) { | 
					
						
							|  |  |  |         console.trace(`Cannot find note ${noteId} in cache.`); | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-16 23:12:29 +02:00
										 |  |  |     const retPath = getSomePath(note); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (retPath) { | 
					
						
							|  |  |  |         const noteTitle = getNoteTitleForPath(retPath); | 
					
						
							|  |  |  |         const parentNote = note.parents[0]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return { | 
					
						
							|  |  |  |             noteId: noteId, | 
					
						
							| 
									
										
										
										
											2020-06-20 21:42:41 +02:00
										 |  |  |             branchId: noteCache.getBranch(noteId, parentNote.noteId).branchId, | 
					
						
							| 
									
										
										
										
											2020-05-16 23:12:29 +02:00
										 |  |  |             title: noteTitle, | 
					
						
							|  |  |  |             notePath: retPath, | 
					
						
							|  |  |  |             path: retPath.join('/') | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function evaluateSimilarity(sourceNote, candidateNote, results) { | 
					
						
							|  |  |  |     let coeff = stringSimilarity.compareTwoStrings(sourceNote.flatText, candidateNote.flatText); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-07 00:05:01 +02:00
										 |  |  |     if (coeff > 0.5) { | 
					
						
							| 
									
										
										
										
											2020-05-16 23:12:29 +02:00
										 |  |  |         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); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-20 12:31:38 +02:00
										 |  |  | function findSimilarNotes(noteId) { | 
					
						
							| 
									
										
										
										
											2020-05-16 23:12:29 +02:00
										 |  |  |     const results = []; | 
					
						
							|  |  |  |     let i = 0; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const origNote = noteCache.notes[noteId]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (!origNote) { | 
					
						
							|  |  |  |         return []; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-17 10:11:19 +02:00
										 |  |  |     for (const note of Object.values(noteCache.notes)) { | 
					
						
							| 
									
										
										
										
											2020-09-07 00:05:01 +02:00
										 |  |  |         if (note.noteId === origNote.noteId) { | 
					
						
							| 
									
										
										
										
											2020-05-16 23:12:29 +02:00
										 |  |  |             continue; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         evaluateSimilarity(origNote, note, results); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         i++; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (i % 200 === 0) { | 
					
						
							| 
									
										
										
										
											2020-06-20 12:31:38 +02:00
										 |  |  |             setImmediatePromise(); | 
					
						
							| 
									
										
										
										
											2020-05-16 23:12:29 +02:00
										 |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     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 = { | 
					
						
							| 
									
										
										
										
											2020-05-17 10:11:19 +02:00
										 |  |  |     getSomePath, | 
					
						
							| 
									
										
										
										
											2020-05-16 23:12:29 +02:00
										 |  |  |     getNotePath, | 
					
						
							| 
									
										
										
										
											2020-05-17 10:11:19 +02:00
										 |  |  |     getNoteTitle, | 
					
						
							| 
									
										
										
										
											2020-05-16 23:12:29 +02:00
										 |  |  |     getNoteTitleForPath, | 
					
						
							|  |  |  |     isAvailable, | 
					
						
							|  |  |  |     isArchived, | 
					
						
							|  |  |  |     isInAncestor, | 
					
						
							|  |  |  |     findSimilarNotes | 
					
						
							|  |  |  | }; |