Notes/src/services/note_cache.js

903 lines
25 KiB
JavaScript
Raw Normal View History

2018-04-18 00:26:42 -04:00
const sql = require('./sql');
const sqlInit = require('./sql_init');
const eventService = require('./events');
const protectedSessionService = require('./protected_session');
const utils = require('./utils');
const hoistedNoteService = require('./hoisted_note');
const stringSimilarity = require('string-similarity');
2018-04-18 00:26:42 -04:00
2020-05-09 23:42:26 +02:00
/** @type {Object.<String, Note>} */
2020-05-03 09:18:57 +02:00
let notes;
2020-05-09 23:42:26 +02:00
/** @type {Object.<String, Branch>} */
2020-05-03 09:18:57 +02:00
let branches
2020-05-09 23:42:26 +02:00
/** @type {Object.<String, Attribute>} */
2020-05-03 09:18:57 +02:00
let attributes;
let childParentToBranch = {};
class Note {
constructor(row) {
/** @param {string} */
this.noteId = row.noteId;
/** @param {string} */
this.title = row.title;
/** @param {boolean} */
this.isProtected = !!row.isProtected;
/** @param {boolean} */
2020-05-13 23:06:13 +02:00
this.isDecrypted = !row.isProtected || !!row.isContentAvailable;
2020-05-03 09:18:57 +02:00
/** @param {Note[]} */
this.parents = [];
/** @param {Note[]} */
this.children = [];
2020-05-03 09:18:57 +02:00
/** @param {Attribute[]} */
this.ownedAttributes = [];
2020-05-13 14:42:16 +02:00
/** @param {Attribute[]|null} */
this.attributeCache = null;
/** @param {Attribute[]|null} */
this.templateAttributeCache = null;
/** @param {Attribute[]|null} */
this.inheritableAttributeCache = null;
/** @param {string|null} */
this.fulltextCache = null;
2020-05-13 23:06:13 +02:00
if (protectedSessionService.isProtectedSessionAvailable()) {
decryptProtectedNote(this);
}
2020-05-03 09:18:57 +02:00
}
/** @return {Attribute[]} */
get attributes() {
2020-05-13 14:42:16 +02:00
if (!this.attributeCache) {
const parentAttributes = this.ownedAttributes.slice();
2020-05-03 09:18:57 +02:00
2020-05-13 14:42:16 +02:00
if (this.noteId !== 'root') {
for (const parentNote of this.parents) {
parentAttributes.push(...parentNote.inheritableAttributes);
2020-05-03 09:18:57 +02:00
}
}
2020-05-13 14:42:16 +02:00
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);
}
2020-05-03 09:18:57 +02:00
}
}
2020-05-13 14:42:16 +02:00
this.attributeCache = parentAttributes.concat(templateAttributes);
this.inheritableAttributeCache = [];
this.templateAttributeCache = [];
for (const attr of this.attributeCache) {
if (attr.isInheritable) {
this.inheritableAttributeCache.push(attr);
}
if (attr.type === 'relation' && attr.name === 'template') {
this.templateAttributeCache.push(attr);
}
}
2020-05-03 09:18:57 +02:00
}
2020-05-13 14:42:16 +02:00
return this.attributeCache;
2020-05-03 09:18:57 +02:00
}
2020-05-13 14:42:16 +02:00
/** @return {Attribute[]} */
get inheritableAttributes() {
if (!this.inheritableAttributeCache) {
this.attributes; // will refresh also this.inheritableAttributeCache
}
2020-05-13 14:42:16 +02:00
return this.inheritableAttributeCache;
}
2020-05-03 09:18:57 +02:00
/** @return {Attribute[]} */
2020-05-13 14:42:16 +02:00
get templateAttributes() {
if (!this.templateAttributeCache) {
this.attributes; // will refresh also this.templateAttributeCache
}
return this.templateAttributeCache;
2020-05-03 09:18:57 +02:00
}
hasAttribute(type, name) {
return this.attributes.find(attr => attr.type === type && attr.name === name);
}
get isArchived() {
return this.hasAttribute('label', 'archived');
}
2020-05-13 23:06:13 +02:00
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);
}
2020-05-13 14:42:16 +02:00
get fulltext() {
if (!this.fulltextCache) {
2020-05-13 23:06:13 +02:00
if (this.isHideInAutocompleteOrArchived) {
this.fulltextCache = " "; // can't be empty
return this.fulltextCache;
}
2020-05-13 14:42:16 +02:00
this.fulltextCache = this.title.toLowerCase();
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.fulltextCache += ' ' + attr.name.toLowerCase();
if (attr.value) {
this.fulltextCache += ' ' + attr.value.toLowerCase();
}
}
}
return this.fulltextCache;
}
invalidateThisCache() {
this.fulltextCache = null;
this.attributeCache = null;
this.templateAttributeCache = null;
this.inheritableAttributeCache = null;
}
invalidateSubtreeCaches() {
this.invalidateThisCache();
for (const childNote of this.children) {
childNote.invalidateSubtreeCaches();
}
for (const templateAttr of this.templateAttributes) {
const targetNote = templateAttr.targetNote;
if (targetNote) {
targetNote.invalidateSubtreeCaches();
}
}
}
invalidateSubtreeFulltext() {
this.fulltextCache = null;
for (const childNote of this.children) {
childNote.invalidateSubtreeFulltext();
}
for (const templateAttr of this.templateAttributes) {
const targetNote = templateAttr.targetNote;
if (targetNote) {
targetNote.invalidateSubtreeFulltext();
}
}
}
2020-05-03 09:18:57 +02:00
}
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;
2020-05-13 23:06:13 +02:00
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);
parentNote.children.push(childNote);
childParentToBranch[`${this.noteId}-${this.parentNoteId}`] = this;
2020-05-03 09:18:57 +02:00
}
/** @return {Note} */
get parentNote() {
2020-05-10 23:27:53 +02:00
const note = notes[this.parentNoteId];
if (!note) {
console.log(`Cannot find note ${this.parentNoteId}`);
}
return note;
2020-05-03 09:18:57 +02:00
}
}
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;
2020-05-13 23:06:13 +02:00
notes[this.noteId].ownedAttributes.push(this);
2020-05-03 09:18:57 +02:00
}
2020-05-09 23:42:26 +02:00
2020-05-13 14:42:16 +02:00
get isAffectingSubtree() {
return this.isInheritable
|| (this.type === 'relation' && this.name === 'template');
2020-05-09 23:42:26 +02:00
}
2020-05-13 14:42:16 +02:00
get targetNote() {
if (this.type === 'relation') {
return notes[this.value];
2020-05-09 23:42:26 +02:00
}
}
}
let loaded = false;
2019-10-25 21:57:08 +02:00
let loadedPromiseResolve;
/** Is resolved after the initial load */
let loadedPromise = new Promise(res => loadedPromiseResolve = res);
2019-10-25 21:57:08 +02:00
2020-05-03 09:18:57 +02:00
async function 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;
}
2018-04-18 00:26:42 -04:00
async function load() {
2020-05-03 09:18:57 +02:00
notes = await getMappedRows(`SELECT noteId, title, isProtected FROM notes WHERE isDeleted = 0`,
row => new Note(row));
2018-04-18 00:26:42 -04:00
2020-05-03 09:18:57 +02:00
branches = await getMappedRows(`SELECT branchId, noteId, parentNoteId, prefix FROM branches WHERE isDeleted = 0`,
row => new Branch(row));
2018-04-19 20:59:44 -04:00
2020-05-03 09:18:57 +02:00
attributes = await getMappedRows(`SELECT attributeId, noteId, type, name, value, isInheritable FROM attributes WHERE isDeleted = 0`,
row => new Attribute(row));
2018-04-18 00:26:42 -04:00
2020-05-13 23:06:13 +02:00
loaded = true;
loadedPromiseResolve();
}
2020-05-13 23:06:13 +02:00
function decryptProtectedNote(note) {
if (note.isProtected && !note.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) {
note.title = protectedSessionService.decryptString(note.title);
2020-05-13 23:06:13 +02:00
note.isDecrypted = true;
}
2018-04-18 00:26:42 -04:00
}
2020-05-03 09:18:57 +02:00
async function decryptProtectedNotes() {
2020-05-13 23:06:13 +02:00
for (const note of Object.values(notes)) {
decryptProtectedNote(note);
}
}
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;
}
}
function highlightResults(results, allTokens) {
// 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)
allTokens = allTokens.map(token => token.replace('/[<\{\}]/g', ''));
// sort by the longest so we first highlight longest matches
allTokens.sort((a, b) => a.length > b.length ? -1 : 1);
2018-11-07 17:16:33 +01:00
for (const result of results) {
2020-05-10 23:27:53 +02:00
const note = notes[result.noteId];
for (const attr of note.attributes) {
if (allTokens.find(token => attr.name.includes(token) || attr.value.includes(token))) {
result.pathTitle += ` <small>${formatAttribute(attr)}</small>`;
2020-05-10 23:27:53 +02:00
}
}
result.highlightedTitle = result.pathTitle;
2018-11-07 17:16:33 +01:00
}
for (const token of allTokens) {
const tokenRegex = new RegExp("(" + utils.escapeRegExp(token) + ")", "gi");
for (const result of results) {
result.highlightedTitle = result.highlightedTitle.replace(tokenRegex, "{$1}");
}
}
for (const result of results) {
result.highlightedTitle = result.highlightedTitle
.replace(/{/g, "<b>")
.replace(/}/g, "</b>");
}
}
/**
* Returns noteIds which have at least one matching tokens
*
* @param tokens
2020-05-13 14:42:16 +02:00
* @return {String[]}
*/
function getCandidateNotes(tokens) {
2020-05-13 14:42:16 +02:00
const candidateNotes = [];
2020-05-10 23:27:53 +02:00
2020-05-13 14:42:16 +02:00
for (const note of Object.values(notes)) {
for (const token of tokens) {
if (note.fulltext.includes(token)) {
candidateNotes.push(note);
break;
2020-05-10 23:27:53 +02:00
}
}
}
2020-05-13 14:42:16 +02:00
return candidateNotes;
}
async function findNotes(query) {
if (!query.length) {
return [];
}
const allTokens = query
.trim() // necessary because even with .split() trailing spaces are tokens which causes havoc
.toLowerCase()
.split(/[ -]/)
.filter(token => token !== '/'); // '/' is used as separator
2020-05-13 14:42:16 +02:00
const candidateNotes = getCandidateNotes(allTokens);
2020-05-10 23:27:53 +02:00
// now we have set of noteIds which match at least one token
let results = [];
const tokens = allTokens.slice();
2018-04-18 00:26:42 -04:00
2020-05-13 14:42:16 +02:00
for (const note of candidateNotes) {
// autocomplete should be able to find notes by their noteIds as well (only leafs)
2020-05-13 14:42:16 +02:00
if (note.noteId === query) {
2020-05-10 23:27:53 +02:00
search(note, [], [], results);
continue;
}
// for leaf note it doesn't matter if "archived" label is inheritable or not
2020-05-03 09:18:57 +02:00
if (note.isArchived) {
2018-04-19 20:59:44 -04:00
continue;
}
2018-04-18 00:26:42 -04:00
2020-05-10 23:27:53 +02:00
const foundAttrTokens = [];
for (const attribute of note.ownedAttributes) {
2020-05-10 23:27:53 +02:00
for (const token of tokens) {
if (attribute.name.toLowerCase().includes(token)
|| attribute.value.toLowerCase().includes(token)) {
2020-05-10 23:27:53 +02:00
foundAttrTokens.push(token);
}
}
}
2020-05-03 09:18:57 +02:00
for (const parentNote of note.parents) {
2020-05-10 23:27:53 +02:00
const title = getNoteTitle(note.noteId, parentNote.noteId).toLowerCase();
const foundTokens = foundAttrTokens.slice();
2018-04-19 20:59:44 -04:00
for (const token of tokens) {
if (title.includes(token)) {
foundTokens.push(token);
}
2018-04-18 00:26:42 -04:00
}
2018-04-19 20:59:44 -04:00
if (foundTokens.length > 0) {
const remainingTokens = tokens.filter(token => !foundTokens.includes(token));
2018-04-18 00:26:42 -04:00
2020-05-13 14:42:16 +02:00
search(parentNote, remainingTokens, [note.noteId], results);
2018-04-19 20:59:44 -04:00
}
2018-04-18 00:26:42 -04:00
}
}
if (hoistedNoteService.getHoistedNoteId() !== 'root') {
results = results.filter(res => res.pathArray.includes(hoistedNoteService.getHoistedNoteId()));
}
// sort results by depth of the note. This is based on the assumption that more important results
// are closer to the note root.
results.sort((a, b) => {
if (a.pathArray.length === b.pathArray.length) {
return a.title < b.title ? -1 : 1;
}
return a.pathArray.length < b.pathArray.length ? -1 : 1;
});
const apiResults = results.slice(0, 200).map(res => {
const notePath = res.pathArray.join('/');
return {
noteId: res.noteId,
branchId: res.branchId,
path: notePath,
pathTitle: res.titleArray.join(' / '),
noteTitle: getNoteTitleFromPath(notePath)
};
});
highlightResults(apiResults, allTokens);
return apiResults;
2018-04-18 00:26:42 -04:00
}
2020-05-03 09:18:57 +02:00
function getBranch(childNoteId, parentNoteId) {
return childParentToBranch[`${childNoteId}-${parentNoteId}`];
}
function search(note, tokens, path, results) {
if (tokens.length === 0) {
2020-05-03 09:18:57 +02:00
const retPath = getSomePath(note, path);
2018-04-18 00:26:42 -04:00
2020-05-03 09:18:57 +02:00
if (retPath) {
2018-06-04 23:21:45 -04:00
const thisNoteId = retPath[retPath.length - 1];
const thisParentNoteId = retPath[retPath.length - 2];
2018-04-18 00:26:42 -04:00
results.push({
2018-06-04 23:21:45 -04:00
noteId: thisNoteId,
2020-05-03 09:18:57 +02:00
branchId: getBranch(thisNoteId, thisParentNoteId),
pathArray: retPath,
titleArray: getNoteTitleArrayForPath(retPath)
});
}
return;
}
2020-05-10 23:27:53 +02:00
if (!note.parents.length === 0 || note.noteId === 'root') {
2018-04-19 20:59:44 -04:00
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);
}
}
}
2020-05-03 09:18:57 +02:00
for (const parentNote of note.parents) {
2020-05-10 23:27:53 +02:00
const title = getNoteTitle(note.noteId, parentNote.noteId).toLowerCase();
const foundTokens = foundAttrTokens.slice();
2018-04-18 00:26:42 -04:00
for (const token of tokens) {
if (title.includes(token)) {
foundTokens.push(token);
}
}
if (foundTokens.length > 0) {
const remainingTokens = tokens.filter(token => !foundTokens.includes(token));
2020-05-10 23:27:53 +02:00
search(parentNote, remainingTokens, path.concat([note.noteId]), results);
2018-04-18 00:26:42 -04:00
}
else {
2020-05-10 23:27:53 +02:00
search(parentNote, tokens, path.concat([note.noteId]), results);
2018-04-18 00:26:42 -04:00
}
}
}
function isNotePathArchived(notePath) {
2020-05-03 09:18:57 +02:00
const noteId = notePath[notePath.length - 1];
const note = notes[noteId];
2020-05-03 09:18:57 +02:00
if (note.isArchived) {
return true;
}
for (let i = 0; i < notePath.length - 1; i++) {
const note = 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;
}
2020-05-03 09:18:57 +02:00
const note = notes[noteId];
2020-05-10 23:27:53 +02:00
for (const parentNote of note.parents) {
2020-05-03 09:18:57 +02:00
if (isInAncestor(parentNote.noteId, ancestorNoteId)) {
return true;
}
}
return false;
}
function getNoteTitleFromPath(notePath) {
const pathArr = notePath.split("/");
if (pathArr.length === 1) {
return getNoteTitle(pathArr[0], 'root');
}
else {
return getNoteTitle(pathArr[pathArr.length - 1], pathArr[pathArr.length - 2]);
}
}
2020-05-10 23:27:53 +02:00
function getNoteTitle(childNoteId, parentNoteId) {
const childNote = notes[childNoteId];
const parentNote = notes[parentNoteId];
2020-05-03 09:18:57 +02:00
let title;
2020-05-03 09:18:57 +02:00
if (childNote.isProtected) {
title = protectedSessionService.isProtectedSessionAvailable() ? childNote.title : '[protected]';
}
2020-05-03 09:18:57 +02:00
else {
title = childNote.title;
}
2020-05-10 23:27:53 +02:00
const branch = parentNote ? getBranch(childNote.noteId, parentNote.noteId) : null;
return ((branch && branch.prefix) ? `${branch.prefix} - ` : '') + title;
}
function getNoteTitleArrayForPath(path) {
2018-04-19 20:59:44 -04:00
const titles = [];
2018-04-18 00:26:42 -04:00
2018-12-13 21:43:13 +01:00
if (path[0] === hoistedNoteService.getHoistedNoteId() && path.length === 1) {
return [ getNoteTitle(hoistedNoteService.getHoistedNoteId()) ];
2018-06-06 22:38:36 -04:00
}
2018-04-19 20:59:44 -04:00
let parentNoteId = 'root';
let hoistedNotePassed = false;
2018-04-18 00:26:42 -04:00
2018-04-19 20:59:44 -04:00
for (const noteId of path) {
// 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;
}
2018-04-19 20:59:44 -04:00
parentNoteId = noteId;
}
return titles;
}
function getNoteTitleForPath(path) {
const titles = getNoteTitleArrayForPath(path);
2018-04-19 20:59:44 -04:00
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()
*/
2020-05-10 23:27:53 +02:00
function getSomePath(note, path = []) {
if (note.noteId === 'root') {
path.push(note.noteId);
2018-12-13 21:43:13 +01:00
path.reverse();
if (!path.includes(hoistedNoteService.getHoistedNoteId())) {
return false;
}
2018-04-19 20:59:44 -04:00
return path;
}
2020-05-10 23:27:53 +02:00
const parents = note.parents;
if (parents.length === 0) {
2018-04-19 20:59:44 -04:00
return false;
}
2020-05-10 23:27:53 +02:00
for (const parentNote of parents) {
const retPath = getSomePath(parentNote, path.concat([note.noteId]));
if (retPath) {
return retPath;
}
}
return false;
}
function getNotePath(noteId) {
const note = 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,
2019-09-07 10:11:59 +02:00
notePath: retPath,
path: retPath.join('/')
};
}
}
2020-05-13 14:42:16 +02:00
function evaluateSimilarity(sourceNote, candidateNote, results) {
let coeff = stringSimilarity.compareTwoStrings(sourceNote.fulltext, candidateNote.fulltext);
if (coeff > 0.4) {
2020-05-13 14:42:16 +02:00
const notePath = getSomePath(candidateNote);
// this takes care of note hoisting
if (!notePath) {
return;
}
if (isNotePathArchived(notePath)) {
coeff -= 0.2; // archived penalization
}
2020-05-13 14:42:16 +02:00
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-05-13 14:42:16 +02:00
async function findSimilarNotes(noteId) {
const results = [];
let i = 0;
2020-05-13 14:42:16 +02:00
const origNote = notes[noteId];
if (!origNote) {
return [];
}
for (const note of Object.values(notes)) {
if (note.isProtected && !note.isDecrypted) {
continue;
}
2020-05-13 14:42:16 +02:00
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;
}
eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED, eventService.ENTITY_SYNCED], async ({entityName, entity}) => {
2019-01-03 23:27:10 +01:00
// note that entity can also be just POJO without methods if coming from sync
if (!loaded) {
return;
}
if (entityName === 'notes') {
const {noteId} = entity;
if (entity.isDeleted) {
delete notes[noteId];
}
else if (noteId in notes) {
2020-05-13 14:42:16 +02:00
const note = notes[noteId];
// we can assume we have protected session since we managed to update
2020-05-13 14:42:16 +02:00
note.title = entity.title;
2020-05-13 23:06:13 +02:00
note.isProtected = entity.isProtected;
note.isDecrypted = !entity.isProtected || !!entity.isContentAvailable;
2020-05-13 14:42:16 +02:00
note.fulltextCache = null;
2020-05-13 23:06:13 +02:00
decryptProtectedNote(note);
}
else {
2020-05-13 23:06:13 +02:00
const note = new Note(entity);
notes[noteId] = note;
decryptProtectedNote(note);
}
}
2018-04-19 20:59:44 -04:00
else if (entityName === 'branches') {
const {branchId, noteId, parentNoteId} = entity;
2018-04-19 20:59:44 -04:00
if (entity.isDeleted) {
const childNote = notes[noteId];
2018-04-19 20:59:44 -04:00
if (childNote) {
childNote.parents = childNote.parents.filter(parent => parent.noteId !== parentNoteId);
2020-05-13 14:42:16 +02:00
if (childNote.parents.length > 0) {
childNote.invalidateSubtreeCaches();
}
2018-04-19 20:59:44 -04:00
}
const parentNote = notes[parentNoteId];
if (parentNote) {
childNote.children = childNote.children.filter(child => child.noteId !== noteId);
}
delete childParentToBranch[`${noteId}-${parentNoteId}`];
delete branches[branchId];
}
else if (branchId in branches) {
// only relevant thing which can change in a branch is prefix
branches[branchId].prefix = entity.prefix;
}
else {
branches[branchId] = new Branch(entity);
const note = notes[entity.noteId];
if (note) {
note.resortParents();
}
2018-04-19 20:59:44 -04:00
}
}
else if (entityName === 'attributes') {
const {attributeId, noteId} = entity;
2020-05-13 14:42:16 +02:00
const note = notes[noteId];
const attr = attributes[attributeId];
if (entity.isDeleted) {
2020-05-13 14:42:16 +02:00
if (note && attr) {
note.ownedAttributes = note.ownedAttributes.filter(attr => attr.attributeId !== attributeId);
2020-05-13 14:42:16 +02:00
if (attr.isAffectingSubtree) {
note.invalidateSubtreeCaches();
}
}
2020-05-13 14:42:16 +02:00
delete attributes[attributeId];
}
else if (attributeId in attributes) {
const attr = attributes[attributeId];
2020-05-13 14:42:16 +02:00
// attr name and isInheritable are immutable
attr.value = entity.value;
2020-05-13 14:42:16 +02:00
if (attr.isAffectingSubtree) {
note.invalidateSubtreeFulltext();
}
else {
note.fulltextCache = null;
}
}
else {
2020-05-13 14:42:16 +02:00
const attr = new Attribute(entity);
attributes[attributeId] = attr;
if (note) {
2020-05-13 14:42:16 +02:00
if (attr.isAffectingSubtree) {
note.invalidateSubtreeCaches();
}
else {
this.invalidateThisCache();
}
}
}
}
});
2019-04-22 18:08:33 +02:00
/**
* @param noteId
* @returns {boolean} - true if note exists (is not deleted) and is available in current note hoisting
2019-04-22 18:08:33 +02:00
*/
function isAvailable(noteId) {
const notePath = getNotePath(noteId);
return !!notePath;
}
eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, () => {
2020-05-03 09:18:57 +02:00
loadedPromise.then(() => decryptProtectedNotes());
});
2019-10-25 21:57:08 +02:00
sqlInit.dbReady.then(() => utils.stopWatch("Note cache load", load));
2018-04-18 00:26:42 -04:00
module.exports = {
2019-10-25 21:57:08 +02:00
loadedPromise,
findNotes,
2018-06-06 22:38:36 -04:00
getNotePath,
getNoteTitleForPath,
getNoteTitleFromPath,
2019-04-22 18:08:33 +02:00
isAvailable,
isArchived,
isInAncestor,
load,
findSimilarNotes
2020-05-09 23:42:26 +02:00
};