2018-04-18 00:26:42 -04:00
|
|
|
const sql = require('./sql');
|
|
|
|
const sqlInit = require('./sql_init');
|
2018-04-20 00:12:01 -04:00
|
|
|
const eventService = require('./events');
|
|
|
|
const protectedSessionService = require('./protected_session');
|
2018-05-26 10:24:33 -04:00
|
|
|
const utils = require('./utils');
|
2018-12-13 21:18:35 +01:00
|
|
|
const hoistedNoteService = require('./hoisted_note');
|
2019-09-01 11:33:45 +02:00
|
|
|
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;
|
2020-05-13 10:47:22 +02:00
|
|
|
/** @param {boolean} */
|
2020-05-13 23:06:13 +02:00
|
|
|
this.isDecrypted = !row.isProtected || !!row.isContentAvailable;
|
2020-05-14 21:30:36 +02:00
|
|
|
/** @param {Branch[]} */
|
|
|
|
this.parentBranches = [];
|
2020-05-03 09:18:57 +02:00
|
|
|
/** @param {Note[]} */
|
|
|
|
this.parents = [];
|
2020-05-13 00:01:10 +02:00
|
|
|
/** @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 00:01:10 +02:00
|
|
|
}
|
2020-05-13 14:42:16 +02:00
|
|
|
|
|
|
|
return this.inheritableAttributeCache;
|
2020-05-13 00:01:10 +02:00
|
|
|
}
|
|
|
|
|
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 10:47:22 +02:00
|
|
|
|
2020-05-13 23:06:13 +02:00
|
|
|
get isHideInAutocompleteOrArchived() {
|
|
|
|
return this.attributes.find(attr =>
|
|
|
|
attr.type === 'label'
|
|
|
|
&& ["archived", "hideInAutocomplete"].includes(attr.name));
|
|
|
|
}
|
|
|
|
|
2020-05-13 10:47:22 +02:00
|
|
|
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();
|
|
|
|
|
2020-05-14 21:30:36 +02:00
|
|
|
for (const branch of this.parentBranches) {
|
|
|
|
if (branch.prefix) {
|
|
|
|
this.fulltextCache += ' ' + branch.prefix;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-13 14:42:16 +02:00
|
|
|
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 10:47:22 +02:00
|
|
|
|
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);
|
2020-05-14 21:30:36 +02:00
|
|
|
childNote.parentBranches.push(this);
|
|
|
|
|
2020-05-13 23:06:13 +02:00
|
|
|
parentNote.children.push(childNote);
|
|
|
|
|
2020-05-13 10:47:22 +02:00
|
|
|
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} */
|
2020-05-13 00:01:10 +02:00
|
|
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-23 21:15:32 +02:00
|
|
|
let loaded = false;
|
2019-10-25 21:57:08 +02:00
|
|
|
let loadedPromiseResolve;
|
|
|
|
/** Is resolved after the initial load */
|
2019-10-29 20:54:08 +01:00
|
|
|
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();
|
|
|
|
}
|
2018-07-23 21:15:32 +02:00
|
|
|
|
2020-05-13 23:06:13 +02:00
|
|
|
function decryptProtectedNote(note) {
|
|
|
|
if (note.isProtected && !note.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) {
|
|
|
|
note.title = protectedSessionService.decryptString(note.title);
|
2019-01-08 20:19:41 +01:00
|
|
|
|
2020-05-13 23:06:13 +02:00
|
|
|
note.isDecrypted = true;
|
2019-09-01 11:33:45 +02:00
|
|
|
}
|
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);
|
2019-01-08 20:19:41 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-13 10:47:22 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-07 09:35:29 +01:00
|
|
|
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.
|
2018-11-26 22:35:19 +01:00
|
|
|
// { and } are used for marking <b> and </b> tag (to avoid matches on single 'b' character)
|
|
|
|
allTokens = allTokens.map(token => token.replace('/[<\{\}]/g', ''));
|
2018-11-07 09:35:29 +01:00
|
|
|
|
|
|
|
// 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))) {
|
2020-05-13 10:47:22 +02:00
|
|
|
result.pathTitle += ` <small>${formatAttribute(attr)}</small>`;
|
2020-05-10 23:27:53 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-07 22:43:01 +01:00
|
|
|
result.highlightedTitle = result.pathTitle;
|
2018-11-07 17:16:33 +01:00
|
|
|
}
|
|
|
|
|
2018-11-07 09:35:29 +01:00
|
|
|
for (const token of allTokens) {
|
|
|
|
const tokenRegex = new RegExp("(" + utils.escapeRegExp(token) + ")", "gi");
|
|
|
|
|
|
|
|
for (const result of results) {
|
2019-11-07 22:43:01 +01:00
|
|
|
result.highlightedTitle = result.highlightedTitle.replace(tokenRegex, "{$1}");
|
2018-11-07 09:35:29 +01:00
|
|
|
}
|
|
|
|
}
|
2018-11-26 22:35:19 +01:00
|
|
|
|
|
|
|
for (const result of results) {
|
2019-11-07 22:43:01 +01:00
|
|
|
result.highlightedTitle = result.highlightedTitle
|
2018-11-26 22:35:19 +01:00
|
|
|
.replace(/{/g, "<b>")
|
|
|
|
.replace(/}/g, "</b>");
|
|
|
|
}
|
2018-11-07 09:35:29 +01:00
|
|
|
}
|
|
|
|
|
2020-05-13 10:47:22 +02:00
|
|
|
/**
|
|
|
|
* Returns noteIds which have at least one matching tokens
|
|
|
|
*
|
|
|
|
* @param tokens
|
2020-05-13 14:42:16 +02:00
|
|
|
* @return {String[]}
|
2020-05-13 10:47:22 +02:00
|
|
|
*/
|
|
|
|
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;
|
2020-05-13 10:47:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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-13 10:47:22 +02:00
|
|
|
|
2020-05-10 23:27:53 +02:00
|
|
|
// now we have set of noteIds which match at least one token
|
|
|
|
|
2018-12-12 21:28:38 +01:00
|
|
|
let results = [];
|
2020-05-13 10:47:22 +02:00
|
|
|
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) {
|
2018-08-17 10:06:52 +02:00
|
|
|
// 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);
|
2018-08-17 10:06:52 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-11-20 21:22:20 +01:00
|
|
|
// 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 = [];
|
|
|
|
|
2020-05-13 00:01:10 +02:00
|
|
|
for (const attribute of note.ownedAttributes) {
|
2020-05-10 23:27:53 +02:00
|
|
|
for (const token of tokens) {
|
2020-05-13 00:01:10 +02:00
|
|
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-13 21:18:35 +01:00
|
|
|
if (hoistedNoteService.getHoistedNoteId() !== 'root') {
|
|
|
|
results = results.filter(res => res.pathArray.includes(hoistedNoteService.getHoistedNoteId()));
|
2018-12-12 21:28:38 +01:00
|
|
|
}
|
|
|
|
|
2018-11-20 22:22:26 +01:00
|
|
|
// sort results by depth of the note. This is based on the assumption that more important results
|
|
|
|
// are closer to the note root.
|
2018-11-20 21:22:20 +01:00
|
|
|
results.sort((a, b) => {
|
2018-11-20 22:22:26 +01:00
|
|
|
if (a.pathArray.length === b.pathArray.length) {
|
2018-11-20 21:22:20 +01:00
|
|
|
return a.title < b.title ? -1 : 1;
|
|
|
|
}
|
|
|
|
|
2018-11-20 22:22:26 +01:00
|
|
|
return a.pathArray.length < b.pathArray.length ? -1 : 1;
|
2018-11-20 21:22:20 +01:00
|
|
|
});
|
2018-04-18 20:56:23 -04:00
|
|
|
|
2018-11-20 21:22:20 +01:00
|
|
|
const apiResults = results.slice(0, 200).map(res => {
|
2019-11-07 22:43:01 +01:00
|
|
|
const notePath = res.pathArray.join('/');
|
|
|
|
|
2018-11-20 21:22:20 +01:00
|
|
|
return {
|
|
|
|
noteId: res.noteId,
|
|
|
|
branchId: res.branchId,
|
2019-11-07 22:43:01 +01:00
|
|
|
path: notePath,
|
|
|
|
pathTitle: res.titleArray.join(' / '),
|
|
|
|
noteTitle: getNoteTitleFromPath(notePath)
|
2018-11-20 21:22:20 +01:00
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
highlightResults(apiResults, allTokens);
|
2018-11-07 09:35:29 +01:00
|
|
|
|
2018-11-20 21:22:20 +01:00
|
|
|
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) {
|
2018-04-18 20:56:23 -04:00
|
|
|
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
|
|
|
|
2018-04-19 00:13:55 -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),
|
2018-11-20 21:22:20 +01:00
|
|
|
pathArray: retPath,
|
|
|
|
titleArray: getNoteTitleArrayForPath(retPath)
|
2018-04-19 00:13:55 -04:00
|
|
|
});
|
2018-04-18 20:56:23 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-05-13 00:01:10 +02:00
|
|
|
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();
|
2020-05-13 00:01:10 +02:00
|
|
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-29 20:54:08 +01:00
|
|
|
function isNotePathArchived(notePath) {
|
2020-05-03 09:18:57 +02:00
|
|
|
const noteId = notePath[notePath.length - 1];
|
2020-05-13 10:47:22 +02:00
|
|
|
const note = notes[noteId];
|
2020-05-03 09:18:57 +02:00
|
|
|
|
2020-05-13 10:47:22 +02:00
|
|
|
if (note.isArchived) {
|
2019-10-29 20:54:08 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = 0; i < notePath.length - 1; i++) {
|
2020-05-13 10:47:22 +02:00
|
|
|
const note = notes[notePath[i]];
|
|
|
|
|
2019-10-29 20:54:08 +01:00
|
|
|
// this is going through parents so archived must be inheritable
|
2020-05-13 10:47:22 +02:00
|
|
|
if (note.hasInheritableOwnedArchivedLabel) {
|
2019-10-29 20:54:08 +01:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2019-11-17 09:07:35 +01:00
|
|
|
/**
|
|
|
|
* @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) {
|
2020-04-29 23:25:34 +02:00
|
|
|
if (ancestorNoteId === 'root' || ancestorNoteId === noteId) {
|
2019-11-17 09:07:35 +01:00
|
|
|
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)) {
|
2019-11-17 09:07:35 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-11-07 22:43:01 +01:00
|
|
|
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;
|
2018-04-20 00:12:01 -04:00
|
|
|
|
2020-05-03 09:18:57 +02:00
|
|
|
if (childNote.isProtected) {
|
|
|
|
title = protectedSessionService.isProtectedSessionAvailable() ? childNote.title : '[protected]';
|
2018-04-20 00:12:01 -04:00
|
|
|
}
|
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;
|
2018-04-20 00:12:01 -04:00
|
|
|
|
2020-05-13 10:47:22 +02:00
|
|
|
return ((branch && branch.prefix) ? `${branch.prefix} - ` : '') + title;
|
2018-04-20 00:12:01 -04:00
|
|
|
}
|
|
|
|
|
2018-11-20 21:22:20 +01:00
|
|
|
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';
|
2018-12-13 21:18:35 +01:00
|
|
|
let hoistedNotePassed = false;
|
2018-04-18 00:26:42 -04:00
|
|
|
|
2018-04-19 20:59:44 -04:00
|
|
|
for (const noteId of path) {
|
2018-12-13 21:18:35 +01:00
|
|
|
// 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 00:13:55 -04:00
|
|
|
|
2018-04-19 20:59:44 -04:00
|
|
|
parentNoteId = noteId;
|
|
|
|
}
|
2018-04-19 00:13:55 -04:00
|
|
|
|
2018-11-20 21:22:20 +01:00
|
|
|
return titles;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getNoteTitleForPath(path) {
|
|
|
|
const titles = getNoteTitleArrayForPath(path);
|
|
|
|
|
2018-04-19 20:59:44 -04:00
|
|
|
return titles.join(' / ');
|
|
|
|
}
|
2018-04-19 00:13:55 -04:00
|
|
|
|
2019-09-01 11:33:45 +02:00
|
|
|
/**
|
|
|
|
* 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();
|
|
|
|
|
2018-12-13 21:18:35 +01:00
|
|
|
if (!path.includes(hoistedNoteService.getHoistedNoteId())) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-04-19 20:59:44 -04:00
|
|
|
return path;
|
|
|
|
}
|
2018-04-19 00:13:55 -04:00
|
|
|
|
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]));
|
2018-04-19 00:13:55 -04:00
|
|
|
|
|
|
|
if (retPath) {
|
|
|
|
return retPath;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-06-04 19:48:02 -04:00
|
|
|
function getNotePath(noteId) {
|
2020-05-13 00:01:10 +02:00
|
|
|
const note = notes[noteId];
|
|
|
|
const retPath = getSomePath(note);
|
2018-06-03 20:42:25 -04:00
|
|
|
|
2019-09-03 20:16:38 +02:00
|
|
|
if (retPath) {
|
2018-06-03 20:42:25 -04:00
|
|
|
const noteTitle = getNoteTitleForPath(retPath);
|
2020-05-13 00:01:10 +02:00
|
|
|
const parentNote = note.parents[0];
|
2018-06-03 20:42:25 -04:00
|
|
|
|
|
|
|
return {
|
|
|
|
noteId: noteId,
|
2020-05-13 00:01:10 +02:00
|
|
|
branchId: getBranch(noteId, parentNote.noteId).branchId,
|
2018-06-03 20:42:25 -04:00
|
|
|
title: noteTitle,
|
2019-09-07 10:11:59 +02:00
|
|
|
notePath: retPath,
|
2018-06-03 20:42:25 -04:00
|
|
|
path: retPath.join('/')
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-13 14:42:16 +02:00
|
|
|
function evaluateSimilarity(sourceNote, candidateNote, results) {
|
|
|
|
let coeff = stringSimilarity.compareTwoStrings(sourceNote.fulltext, candidateNote.fulltext);
|
2019-09-01 11:33:45 +02:00
|
|
|
|
|
|
|
if (coeff > 0.4) {
|
2020-05-13 14:42:16 +02:00
|
|
|
const notePath = getSomePath(candidateNote);
|
2019-09-01 11:33:45 +02:00
|
|
|
|
|
|
|
// this takes care of note hoisting
|
|
|
|
if (!notePath) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-09-03 20:16:38 +02:00
|
|
|
if (isNotePathArchived(notePath)) {
|
2019-09-01 11:33:45 +02:00
|
|
|
coeff -= 0.2; // archived penalization
|
|
|
|
}
|
|
|
|
|
2020-05-13 14:42:16 +02:00
|
|
|
results.push({coeff, notePath, noteId: candidateNote.noteId});
|
2019-09-01 11:33:45 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-03 21:16:14 +02:00
|
|
|
/**
|
|
|
|
* 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) => {
|
2020-02-03 21:16:33 +01:00
|
|
|
setTimeout(() => resolve(), 0);
|
2019-09-03 21:16:14 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-05-13 14:42:16 +02:00
|
|
|
async function findSimilarNotes(noteId) {
|
2020-05-13 10:47:22 +02:00
|
|
|
const results = [];
|
2019-09-03 21:16:14 +02:00
|
|
|
let i = 0;
|
|
|
|
|
2020-05-13 14:42:16 +02:00
|
|
|
const origNote = notes[noteId];
|
|
|
|
|
|
|
|
if (!origNote) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
2020-05-13 10:47:22 +02:00
|
|
|
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);
|
2019-09-01 11:33:45 +02:00
|
|
|
|
2019-09-03 21:16:14 +02:00
|
|
|
i++;
|
|
|
|
|
|
|
|
if (i % 200 === 0) {
|
|
|
|
await setImmediatePromise();
|
|
|
|
}
|
2019-09-01 11:33:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
results.sort((a, b) => a.coeff > b.coeff ? -1 : 1);
|
|
|
|
|
|
|
|
return results.length > 50 ? results.slice(0, 50) : results;
|
|
|
|
}
|
|
|
|
|
2020-04-04 14:57:19 +02:00
|
|
|
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
|
|
|
|
|
2018-07-23 21:15:32 +02:00
|
|
|
if (!loaded) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-04-18 23:11:30 -04:00
|
|
|
if (entityName === 'notes') {
|
2020-05-13 10:47:22 +02:00
|
|
|
const {noteId} = entity;
|
2018-04-18 23:11:30 -04:00
|
|
|
|
2020-05-13 10:47:22 +02:00
|
|
|
if (entity.isDeleted) {
|
|
|
|
delete notes[noteId];
|
|
|
|
}
|
|
|
|
else if (noteId in notes) {
|
2020-05-13 14:42:16 +02:00
|
|
|
const note = notes[noteId];
|
|
|
|
|
2020-05-13 10:47:22 +02:00
|
|
|
// 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);
|
2018-04-18 23:11:30 -04:00
|
|
|
}
|
|
|
|
else {
|
2020-05-13 23:06:13 +02:00
|
|
|
const note = new Note(entity);
|
|
|
|
notes[noteId] = note;
|
|
|
|
|
|
|
|
decryptProtectedNote(note);
|
2018-04-18 23:11:30 -04:00
|
|
|
}
|
|
|
|
}
|
2018-04-19 20:59:44 -04:00
|
|
|
else if (entityName === 'branches') {
|
2020-05-13 10:47:22 +02:00
|
|
|
const {branchId, noteId, parentNoteId} = entity;
|
2020-05-14 21:30:36 +02:00
|
|
|
const childNote = notes[noteId];
|
2018-04-19 20:59:44 -04:00
|
|
|
|
2020-05-13 10:47:22 +02:00
|
|
|
if (entity.isDeleted) {
|
|
|
|
if (childNote) {
|
|
|
|
childNote.parents = childNote.parents.filter(parent => parent.noteId !== parentNoteId);
|
2020-05-14 21:30:36 +02:00
|
|
|
childNote.parentBranches = childNote.parentBranches.filter(branch => branch.branchId !== branchId);
|
2020-05-13 14:42:16 +02:00
|
|
|
|
|
|
|
if (childNote.parents.length > 0) {
|
|
|
|
childNote.invalidateSubtreeCaches();
|
|
|
|
}
|
2018-04-19 20:59:44 -04:00
|
|
|
}
|
|
|
|
|
2020-05-13 10:47:22 +02:00
|
|
|
const parentNote = notes[parentNoteId];
|
2020-02-03 20:07:34 +01:00
|
|
|
|
2020-05-13 10:47:22 +02:00
|
|
|
if (parentNote) {
|
2020-05-14 21:30:36 +02:00
|
|
|
parentNote.children = parentNote.children.filter(child => child.noteId !== noteId);
|
2019-10-29 20:54:08 +01:00
|
|
|
}
|
|
|
|
|
2020-05-13 10:47:22 +02:00
|
|
|
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;
|
2020-05-14 21:30:36 +02:00
|
|
|
|
|
|
|
if (childNote) {
|
|
|
|
childNote.fulltextCache = null;
|
|
|
|
}
|
2020-05-13 10:47:22 +02:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
branches[branchId] = new Branch(entity);
|
2019-09-01 11:33:45 +02:00
|
|
|
|
2020-05-14 21:30:36 +02:00
|
|
|
if (childNote) {
|
|
|
|
childNote.resortParents();
|
2020-05-13 10:47:22 +02:00
|
|
|
}
|
2018-04-19 20:59:44 -04:00
|
|
|
}
|
|
|
|
}
|
2018-08-07 13:44:51 +02:00
|
|
|
else if (entityName === 'attributes') {
|
2020-05-13 10:47:22 +02:00
|
|
|
const {attributeId, noteId} = entity;
|
2020-05-13 14:42:16 +02:00
|
|
|
const note = notes[noteId];
|
|
|
|
const attr = attributes[attributeId];
|
2018-04-19 00:13:55 -04:00
|
|
|
|
2020-05-13 10:47:22 +02:00
|
|
|
if (entity.isDeleted) {
|
2020-05-13 14:42:16 +02:00
|
|
|
if (note && attr) {
|
2020-05-13 10:47:22 +02:00
|
|
|
note.ownedAttributes = note.ownedAttributes.filter(attr => attr.attributeId !== attributeId);
|
2020-05-13 14:42:16 +02:00
|
|
|
|
|
|
|
if (attr.isAffectingSubtree) {
|
|
|
|
note.invalidateSubtreeCaches();
|
|
|
|
}
|
2018-04-19 00:13:55 -04:00
|
|
|
}
|
2020-05-13 10:47:22 +02:00
|
|
|
|
2020-05-13 14:42:16 +02:00
|
|
|
delete attributes[attributeId];
|
2020-05-13 10:47:22 +02:00
|
|
|
}
|
|
|
|
else if (attributeId in attributes) {
|
|
|
|
const attr = attributes[attributeId];
|
|
|
|
|
2020-05-13 14:42:16 +02:00
|
|
|
// attr name and isInheritable are immutable
|
2020-05-13 10:47:22 +02:00
|
|
|
attr.value = entity.value;
|
2020-05-13 14:42:16 +02:00
|
|
|
|
|
|
|
if (attr.isAffectingSubtree) {
|
|
|
|
note.invalidateSubtreeFulltext();
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
note.fulltextCache = null;
|
|
|
|
}
|
2020-05-13 10:47:22 +02:00
|
|
|
}
|
|
|
|
else {
|
2020-05-13 14:42:16 +02:00
|
|
|
const attr = new Attribute(entity);
|
|
|
|
attributes[attributeId] = attr;
|
2020-05-13 10:47:22 +02:00
|
|
|
|
|
|
|
if (note) {
|
2020-05-13 14:42:16 +02:00
|
|
|
if (attr.isAffectingSubtree) {
|
|
|
|
note.invalidateSubtreeCaches();
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
this.invalidateThisCache();
|
|
|
|
}
|
2018-04-19 00:13:55 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-04-18 23:11:30 -04:00
|
|
|
});
|
|
|
|
|
2019-04-22 18:08:33 +02:00
|
|
|
/**
|
|
|
|
* @param noteId
|
2019-09-03 20:16:38 +02:00
|
|
|
* @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;
|
|
|
|
}
|
|
|
|
|
2019-01-08 20:19:41 +01:00
|
|
|
eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, () => {
|
2020-05-03 09:18:57 +02:00
|
|
|
loadedPromise.then(() => decryptProtectedNotes());
|
2018-04-20 00:12:01 -04:00
|
|
|
});
|
|
|
|
|
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,
|
2018-06-04 19:48:02 -04:00
|
|
|
findNotes,
|
2018-06-06 22:38:36 -04:00
|
|
|
getNotePath,
|
2019-01-08 20:19:41 +01:00
|
|
|
getNoteTitleForPath,
|
2019-11-07 22:43:01 +01:00
|
|
|
getNoteTitleFromPath,
|
2019-04-22 18:08:33 +02:00
|
|
|
isAvailable,
|
2019-09-03 20:16:38 +02:00
|
|
|
isArchived,
|
2019-11-17 09:07:35 +01:00
|
|
|
isInAncestor,
|
2019-09-01 11:33:45 +02:00
|
|
|
load,
|
|
|
|
findSimilarNotes
|
2020-05-09 23:42:26 +02:00
|
|
|
};
|