"use strict"; const sql = require('../sql'); const utils = require('../../services/utils'); const LABEL = 'label'; const RELATION = 'relation'; class Note { constructor(row) { this.updateFromRow(row); this.init(); } updateFromRow(row) { this.update([ row.noteId, row.title, row.type, row.mime ]); } update([noteId, title, type, mime]) { /** @param {string} */ this.noteId = noteId; /** @param {string} */ this.title = title; /** @param {string} */ this.type = type; /** @param {string} */ this.mime = mime; return this; } init() { /** @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.becca.addNote(this.noteId, this); /** @param {Note[]|null} */ this.ancestorCache = null; } getParentBranches() { return this.parentBranches; } getBranches() { return this.parentBranches; } getParentNotes() { return this.parents; } getChildNotes() { return this.children; } hasChildren() { return this.children && this.children.length > 0; } getChildBranches() { return this.children.map(childNote => this.becca.getBranchFromChildAndParent(childNote.noteId, this.noteId)); } getContent(silentNotFoundError = false) { const row = sql.getRow(`SELECT content FROM note_contents WHERE noteId = ?`, [this.noteId]); if (!row) { if (silentNotFoundError) { return undefined; } else { throw new Error("Cannot find note content for noteId=" + this.noteId); } } let content = row.content; if (this.isStringNote()) { return content === null ? "" : content.toString("UTF-8"); } else { return content; } } /** @returns {*} */ getJsonContent() { const content = this.getContent(); if (!content || !content.trim()) { return null; } return JSON.parse(content); } /** @returns {boolean} true if this note is of application/json content type */ isJson() { return this.mime === "application/json"; } /** @returns {boolean} true if this note is JavaScript (code or attachment) */ isJavaScript() { return (this.type === "code" || this.type === "file") && (this.mime.startsWith("application/javascript") || this.mime === "application/x-javascript" || this.mime === "text/javascript"); } /** @returns {boolean} true if this note is HTML */ isHtml() { return ["code", "file", "render"].includes(this.type) && this.mime === "text/html"; } /** @returns {boolean} true if the note has string content (not binary) */ isStringNote() { return utils.isStringNote(this.type, this.mime); } /** * @param {string} [type] - (optional) attribute type to filter * @param {string} [name] - (optional) attribute name to filter * @returns {Attribute[]} all note's attributes, including inherited ones */ getAttributes(type, name) { this.__getAttributes([]); if (type && name) { return this.__attributeCache.filter(attr => attr.type === type && attr.name === name); } else if (type) { return this.__attributeCache.filter(attr => attr.type === type); } else if (name) { return this.__attributeCache.filter(attr => attr.name === name); } else { return this.__attributeCache.slice(); } } __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.becca.notes[ownedAttr.value]; if (templateNote) { templateAttributes.push(...templateNote.__getAttributes(newPath)); } } } this.__attributeCache = []; const addedAttributeIds = new Set(); for (const attr of parentAttributes.concat(templateAttributes)) { if (!addedAttributeIds.has(attr.attributeId)) { addedAttributeIds.add(attr.attributeId); this.__attributeCache.push(attr); } } 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.getAttributes().find(attr => attr.type === type && attr.name === name); } getAttributeCaseInsensitive(type, name, value) { name = name.toLowerCase(); value = value ? value.toLowerCase() : null; return this.getAttributes().find( attr => attr.type === type && attr.name.toLowerCase() === name && (!value || attr.value.toLowerCase() === value)); } getRelationTarget(name) { const relation = this.getAttributes().find(attr => attr.type === 'relation' && attr.name === name); return relation ? relation.targetNote : null; } /** * @param {string} name - label name * @returns {boolean} true if label exists (including inherited) */ hasLabel(name) { return this.hasAttribute(LABEL, name); } /** * @param {string} name - label name * @returns {boolean} true if label exists (excluding inherited) */ hasOwnedLabel(name) { return this.hasOwnedAttribute(LABEL, name); } /** * @param {string} name - relation name * @returns {boolean} true if relation exists (including inherited) */ hasRelation(name) { return this.hasAttribute(RELATION, name); } /** * @param {string} name - relation name * @returns {boolean} true if relation exists (excluding inherited) */ hasOwnedRelation(name) { return this.hasOwnedAttribute(RELATION, name); } /** * @param {string} name - label name * @returns {Attribute|null} label if it exists, null otherwise */ getLabel(name) { return this.getAttribute(LABEL, name); } /** * @param {string} name - label name * @returns {Attribute|null} label if it exists, null otherwise */ getOwnedLabel(name) { return this.getOwnedAttribute(LABEL, name); } /** * @param {string} name - relation name * @returns {Attribute|null} relation if it exists, null otherwise */ getRelation(name) { return this.getAttribute(RELATION, name); } /** * @param {string} name - relation name * @returns {Attribute|null} relation if it exists, null otherwise */ getOwnedRelation(name) { return this.getOwnedAttribute(RELATION, name); } /** * @param {string} name - label name * @returns {string|null} label value if label exists, null otherwise */ getLabelValue(name) { return this.getAttributeValue(LABEL, name); } /** * @param {string} name - label name * @returns {string|null} label value if label exists, null otherwise */ getOwnedLabelValue(name) { return this.getOwnedAttributeValue(LABEL, name); } /** * @param {string} name - relation name * @returns {string|null} relation value if relation exists, null otherwise */ getRelationValue(name) { return this.getAttributeValue(RELATION, name); } /** * @param {string} name - relation name * @returns {string|null} relation value if relation exists, null otherwise */ getOwnedRelationValue(name) { return this.getOwnedAttributeValue(RELATION, name); } /** * @param {string} type - attribute type (label, relation, etc.) * @param {string} name - attribute name * @returns {boolean} true if note has an attribute with given type and name (excluding inherited) */ hasOwnedAttribute(type, name) { return !!this.getOwnedAttribute(type, name); } /** * @param {string} type - attribute type (label, relation, etc.) * @param {string} name - attribute name * @returns {Attribute} attribute of given type and name. If there's more such attributes, first is returned. Returns null if there's no such attribute belonging to this note. */ getAttribute(type, name) { const attributes = this.getAttributes(); return attributes.find(attr => attr.type === type && attr.name === name); } /** * @param {string} type - attribute type (label, relation, etc.) * @param {string} name - attribute name * @returns {string|null} attribute value of given type and name or null if no such attribute exists. */ getAttributeValue(type, name) { const attr = this.getAttribute(type, name); return attr ? attr.value : null; } /** * @param {string} type - attribute type (label, relation, etc.) * @param {string} name - attribute name * @returns {string|null} attribute value of given type and name or null if no such attribute exists. */ getOwnedAttributeValue(type, name) { const attr = this.getOwnedAttribute(type, name); return attr ? attr.value : null; } /** * @param {string} [name] - label name to filter * @returns {Attribute[]} all note's labels (attributes with type label), including inherited ones */ getLabels(name) { return this.getAttributes(LABEL, name); } /** * @param {string} [name] - label name to filter * @returns {string[]} all note's label values, including inherited ones */ getLabelValues(name) { return this.getLabels(name).map(l => l.value); } /** * @param {string} [name] - label name to filter * @returns {Attribute[]} all note's labels (attributes with type label), excluding inherited ones */ getOwnedLabels(name) { return this.getOwnedAttributes(LABEL, name); } /** * @param {string} [name] - label name to filter * @returns {string[]} all note's label values, excluding inherited ones */ getOwnedLabelValues(name) { return this.getOwnedAttributes(LABEL, name).map(l => l.value); } /** * @param {string} [name] - relation name to filter * @returns {Attribute[]} all note's relations (attributes with type relation), including inherited ones */ getRelations(name) { return this.getAttributes(RELATION, name); } /** * @param {string} [name] - relation name to filter * @returns {Attribute[]} all note's relations (attributes with type relation), excluding inherited ones */ getOwnedRelations(name) { return this.getOwnedAttributes(RELATION, name); } /** * @param {string} [type] - (optional) attribute type to filter * @param {string} [name] - (optional) attribute name to filter * @returns {Attribute[]} note's "owned" attributes - excluding inherited ones */ getOwnedAttributes(type, name) { // it's a common mistake to include # or ~ into attribute name if (name && ["#", "~"].includes(name[0])) { name = name.substr(1); } if (type && name) { return this.ownedAttributes.filter(attr => attr.type === type && attr.name === name); } else if (type) { return this.ownedAttributes.filter(attr => attr.type === type); } else if (name) { return this.ownedAttributes.filter(attr => attr.name === name); } else { return this.ownedAttributes.slice(); } } /** * @returns {Attribute} attribute belonging to this specific note (excludes inherited attributes) * * This method can be significantly faster than the getAttribute() */ getOwnedAttribute(type, name) { const attrs = this.getOwnedAttributes(type, name); return attrs.length > 0 ? attrs[0] : null; } get isArchived() { return this.hasAttribute('label', 'archived'); } hasInheritableOwnedArchivedLabel() { return !!this.ownedAttributes.find(attr => attr.type === 'label' && attr.name === 'archived' && attr.isInheritable); } // will sort the parents so that non-search & non-archived are first and archived at the end // this is done so that non-search & non-archived paths are always explored as first when looking for note path resortParents() { this.parentBranches.sort((a, b) => a.branchId.startsWith('virt-') || a.parentNote.hasInheritableOwnedArchivedLabel() ? 1 : -1); this.parents = this.parentBranches.map(branch => branch.parentNote); } isTemplate() { return !!this.targetRelations.find(rel => rel.name === 'template'); } /** @return {Note[]} */ getSubtreeNotesIncludingTemplated() { const arr = [[this]]; for (const childNote of this.children) { arr.push(childNote.getSubtreeNotesIncludingTemplated()); } for (const targetRelation of this.targetRelations) { if (targetRelation.name === 'template') { const note = targetRelation.note; if (note) { arr.push(note.getSubtreeNotesIncludingTemplated()); } } } return arr.flat(); } /** @return {Note[]} */ getSubtreeNotes(includeArchived = true) { const noteSet = new Set(); function addSubtreeNotesInner(note) { if (!includeArchived && note.isArchived) { return; } noteSet.add(note); for (const childNote of note.children) { addSubtreeNotesInner(childNote); } } addSubtreeNotesInner(this); return Array.from(noteSet); } /** @return {String[]} */ getSubtreeNoteIds() { return this.getSubtreeNotes().map(note => note.noteId); } getDescendantNoteIds() { return this.getSubtreeNoteIds(); } getAncestors() { 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.getAncestors()) { if (!noteIds.has(ancestorNote.noteId)) { this.ancestorCache.push(ancestorNote); noteIds.add(ancestorNote.noteId); } } } } return this.ancestorCache; } getTargetRelations() { return this.targetRelations; } /** @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 */ getTemplatedNotes() { const arr = [this]; for (const targetRelation of this.targetRelations) { if (targetRelation.name === 'template') { const note = targetRelation.note; if (note) { arr.push(note); } } } return arr; } /** * @param ancestorNoteId * @return {boolean} - true if ancestorNoteId occurs in at least one of the note's paths */ isDescendantOfNote(ancestorNoteId) { const notePaths = this.getAllNotePaths(); return notePaths.some(path => path.includes(ancestorNoteId)); } } module.exports = Note;