353 lines
9.9 KiB
JavaScript

"use strict";
const protectedSessionService = require('../../protected_session');
class Note {
constructor(noteCache, row) {
/** @param {NoteCache} */
this.noteCache = noteCache;
this.update(row);
/** @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 = [];
this.noteCache.notes[this.noteId] = this;
/** @param {Note[]|null} */
this.ancestorCache = null;
}
update(row) {
/** @param {string} */
this.noteId = row.noteId;
/** @param {string} */
this.title = row.title;
/** @param {string} */
this.type = row.type;
/** @param {string} */
this.mime = row.mime;
/** @param {string} */
this.dateCreated = row.dateCreated;
/** @param {string} */
this.dateModified = row.dateModified;
/** @param {string} */
this.utcDateCreated = row.utcDateCreated;
/** @param {string} */
this.utcDateModified = row.utcDateModified;
/** @param {boolean} */
this.isProtected = !!row.isProtected;
/** @param {boolean} */
this.isDecrypted = !row.isProtected || !!row.isContentAvailable;
this.decrypt();
/** @param {string|null} */
this.flatTextCache = null;
}
/** @return {Attribute[]} */
get attributes() {
return this.__getAttributes([]);
}
__getAttributes(path) {
if (path.includes(this.noteId)) {
return [];
}
if (!this.attributeCache) {
const parentAttributes = this.ownedAttributes.slice();
const newPath = [...path, this.noteId];
if (this.noteId !== 'root') {
for (const parentNote of this.parents) {
parentAttributes.push(...parentNote.__getInheritableAttributes(newPath));
}
}
const templateAttributes = [];
for (const ownedAttr of parentAttributes) { // parentAttributes so we process also inherited templates
if (ownedAttr.type === 'relation' && ownedAttr.name === 'template') {
const templateNote = this.noteCache.notes[ownedAttr.value];
if (templateNote) {
templateAttributes.push(...templateNote.__getAttributes(newPath));
}
}
}
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[]} */
__getInheritableAttributes(path) {
if (path.includes(this.noteId)) {
return [];
}
if (!this.inheritableAttributeCache) {
this.__getAttributes(path); // will refresh also this.inheritableAttributeCache
}
return this.inheritableAttributeCache;
}
hasAttribute(type, name) {
return !!this.attributes.find(attr => attr.type === type && attr.name === name);
}
getLabelValue(name) {
const label = this.attributes.find(attr => attr.type === 'label' && attr.name === name);
return label ? label.value : null;
}
getRelationTarget(name) {
const relation = this.attributes.find(attr => attr.type === 'relation' && attr.name === name);
return relation ? relation.targetNote : null;
}
get isArchived() {
return this.hasAttribute('label', 'archived');
}
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);
}
/**
* This is used for:
* - fast searching
* - note similarity evaluation
*
* @return {string} - returns flattened textual representation of note, prefixes and attributes
*/
get flatText() {
if (!this.flatTextCache) {
this.flatTextCache = this.noteId + ' ' + this.type + ' ' + this.mime + ' ';
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 = this.flatTextCache.toLowerCase();
}
return this.flatTextCache;
}
invalidateThisCache() {
this.flatTextCache = null;
this.attributeCache = null;
this.inheritableAttributeCache = null;
this.ancestorCache = 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();
}
get parentCount() {
return this.parents.length;
}
get childrenCount() {
return this.children.length;
}
get labelCount() {
return this.attributes.filter(attr => attr.type === 'label').length;
}
get relationCount() {
return this.attributes.filter(attr => attr.type === 'relation').length;
}
get attributeCount() {
return this.attributes.length;
}
get ancestors() {
if (!this.ancestorCache) {
const noteIds = new Set();
this.ancestorCache = [];
for (const parent of this.parents) {
if (!noteIds.has(parent.noteId)) {
this.ancestorCache.push(parent);
noteIds.add(parent.noteId);
}
for (const ancestorNote of parent.ancestors) {
if (!noteIds.has(ancestorNote.noteId)) {
this.ancestorCache.push(ancestorNote);
noteIds.add(ancestorNote.noteId);
}
}
}
}
return this.ancestorCache;
}
/** @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;
}
decrypt() {
if (this.isProtected && !this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) {
this.title = protectedSessionService.decryptString(this.title);
this.isDecrypted = true;
}
}
// for logging etc
get pojo() {
const pojo = {...this};
delete pojo.noteCache;
delete pojo.ancestorCache;
delete pojo.attributeCache;
delete pojo.flatTextCache;
delete pojo.children;
delete pojo.parents;
delete pojo.parentBranches;
return pojo;
}
}
module.exports = Note;