525 lines
16 KiB
TypeScript
Raw Normal View History

2021-10-16 22:13:34 +02:00
"use strict";
2024-04-09 22:13:01 +03:00
import sql = require('../../sql');
import utils = require('../../../services/utils');
import AbstractShacaEntity = require('./abstract_shaca_entity');
import escape = require('escape-html');
import { Blob } from '../../../services/blob-interface';
import SAttachment = require('./sattachment');
2024-04-09 22:25:32 +03:00
import SAttribute = require('./sattribute');
2024-04-09 22:32:06 +03:00
import SBranch = require('./sbranch');
2021-10-16 22:13:34 +02:00
const LABEL = 'label';
const RELATION = 'relation';
const CREDENTIALS = 'shareCredentials';
2024-04-09 22:25:32 +03:00
const isCredentials = (attr: SAttribute) => attr.type === 'label' && attr.name === CREDENTIALS;
2024-04-09 22:13:01 +03:00
class SNote extends AbstractShacaEntity {
2024-04-09 22:25:32 +03:00
noteId: string;
2024-04-10 19:04:38 +03:00
title: string;
type: string;
mime: string;
2024-04-09 22:13:01 +03:00
private blobId: string;
utcDateModified: string;
isProtected: boolean;
2024-04-09 22:32:06 +03:00
parentBranches: SBranch[];
parents: SNote[];
children: SNote[];
2024-04-09 22:25:32 +03:00
private ownedAttributes: SAttribute[];
private __attributeCache: SAttribute[] | null;
private __inheritableAttributeCache: SAttribute[] | null;
targetRelations: SAttribute[];
2024-04-09 22:39:43 +03:00
attachments: SAttachment[];
2024-04-09 22:13:01 +03:00
constructor([noteId, title, type, mime, blobId, utcDateModified, isProtected]: SNoteRow) {
2021-10-17 14:44:59 +02:00
super();
2021-10-16 22:13:34 +02:00
this.noteId = noteId;
this.title = isProtected ? "[protected]" : title;
2021-10-16 22:13:34 +02:00
this.type = type;
this.mime = mime;
2023-03-16 11:02:07 +01:00
this.blobId = blobId;
2021-12-06 22:53:17 +01:00
this.utcDateModified = utcDateModified; // used for caching of images
this.isProtected = isProtected;
2021-10-16 22:13:34 +02:00
this.parentBranches = [];
this.parents = [];
this.children = [];
this.ownedAttributes = [];
this.__attributeCache = null;
this.__inheritableAttributeCache = null;
2021-10-16 22:13:34 +02:00
this.targetRelations = [];
2023-06-05 23:05:05 +02:00
this.attachments = [];
2021-10-17 14:44:59 +02:00
this.shaca.notes[this.noteId] = this;
2021-10-16 22:13:34 +02:00
}
getParentBranches() {
return this.parentBranches;
}
getBranches() {
return this.parentBranches;
}
2024-04-09 22:32:06 +03:00
getChildBranches(): SBranch[] {
return this.children.map(childNote => this.shaca.getBranchFromChildAndParent(childNote.noteId, this.noteId));
}
getVisibleChildBranches() {
return this.getChildBranches()
.filter(branch => !branch.isHidden
&& !branch.getNote().isLabelTruthy('shareHiddenFromTree'));
}
2021-10-16 22:13:34 +02:00
getParentNotes() {
return this.parents;
}
getChildNotes() {
return this.children;
}
getVisibleChildNotes() {
return this.getVisibleChildBranches()
.map(branch => branch.getNote());
}
2021-10-16 22:13:34 +02:00
hasChildren() {
return this.children && this.children.length > 0;
}
hasVisibleChildren() {
return this.getVisibleChildNotes().length > 0;
}
2021-10-16 22:13:34 +02:00
getContent(silentNotFoundError = false) {
2024-04-09 22:13:01 +03:00
const row = sql.getRow<Pick<Blob, "content">>(`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]);
2021-10-16 22:13:34 +02:00
if (!row) {
if (silentNotFoundError) {
return undefined;
}
else {
2023-06-05 23:05:05 +02:00
throw new Error(`Cannot find note content for note '${this.noteId}', blob '${this.blobId}'`);
2021-10-16 22:13:34 +02:00
}
}
let content = row.content;
2023-05-05 16:37:39 +02:00
if (this.hasStringContent()) {
return content === null
? ""
: content.toString("utf-8");
2021-10-16 22:13:34 +02:00
}
else {
return content;
}
}
2024-04-09 22:13:01 +03:00
/** @returns true if the note has string content (not binary) */
2023-05-05 16:37:39 +02:00
hasStringContent() {
2021-10-16 22:13:34 +02:00
return utils.isStringNote(this.type, this.mime);
}
/**
2024-04-09 22:13:01 +03:00
* @param type - (optional) attribute type to filter
* @param name - (optional) attribute name to filter
* @returns all note's attributes, including inherited ones
2021-10-16 22:13:34 +02:00
*/
2024-04-09 22:13:01 +03:00
getAttributes(type?: string, name?: string) {
let attributeCache = this.__attributeCache;
if (!attributeCache) {
attributeCache = this.__getAttributes([]);
2023-01-27 08:46:04 +01:00
}
2021-10-16 22:13:34 +02:00
if (type && name) {
2024-04-09 22:13:01 +03:00
return attributeCache.filter(attr => attr.type === type && attr.name === name && !isCredentials(attr));
2021-10-16 22:13:34 +02:00
}
else if (type) {
2024-04-09 22:13:01 +03:00
return attributeCache.filter(attr => attr.type === type && !isCredentials(attr));
2021-10-16 22:13:34 +02:00
}
else if (name) {
2024-04-09 22:13:01 +03:00
return attributeCache.filter(attr => attr.name === name && !isCredentials(attr));
2021-10-16 22:13:34 +02:00
}
else {
2024-04-09 22:13:01 +03:00
return attributeCache.filter(attr => !isCredentials(attr));
2021-10-16 22:13:34 +02:00
}
}
getCredentials() {
2024-04-09 22:13:01 +03:00
return this.__getAttributes([]).filter(isCredentials);
}
2024-04-09 22:13:01 +03:00
__getAttributes(path: string[]) {
2021-10-16 22:13:34 +02:00
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: SAttribute[] = [];
2021-10-16 22:13:34 +02:00
for (const ownedAttr of parentAttributes) { // parentAttributes so we process also inherited templates
2023-01-06 20:31:55 +01:00
if (ownedAttr.type === 'relation' && ['template', 'inherit'].includes(ownedAttr.name)) {
2021-10-17 14:44:59 +02:00
const templateNote = this.shaca.notes[ownedAttr.value];
2021-10-16 22:13:34 +02:00
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 = [];
2021-10-16 22:13:34 +02:00
for (const attr of this.__attributeCache) {
if (attr.isInheritable) {
this.__inheritableAttributeCache.push(attr);
2021-10-16 22:13:34 +02:00
}
}
}
return this.__attributeCache;
}
2024-04-09 22:13:01 +03:00
__getInheritableAttributes(path: string[]) {
2021-10-16 22:13:34 +02:00
if (path.includes(this.noteId)) {
return [];
}
if (!this.__inheritableAttributeCache) {
2024-04-17 22:49:41 +03:00
this.__getAttributes(path); // will refresh also this.__inheritableAttributeCache
2021-10-16 22:13:34 +02:00
}
2024-04-17 22:49:41 +03:00
return this.__inheritableAttributeCache || [];
2021-10-16 22:13:34 +02:00
}
2024-04-10 19:04:38 +03:00
/**
* @throws Error in case of invalid JSON
*/
getJsonContent(): any | null {
const content = this.getContent();
if (typeof content !== "string" || !content || !content.trim()) {
return null;
}
return JSON.parse(content);
}
/** @returns valid object or null if the content cannot be parsed as JSON */
getJsonContentSafely() {
try {
return this.getJsonContent();
}
catch (e) {
return null;
}
}
2024-04-09 22:13:01 +03:00
hasAttribute(type: string, name: string) {
2021-10-16 22:13:34 +02:00
return !!this.getAttributes().find(attr => attr.type === type && attr.name === name);
}
2024-04-09 22:13:01 +03:00
getRelationTarget(name: string) {
2021-10-16 22:13:34 +02:00
const relation = this.getAttributes().find(attr => attr.type === 'relation' && attr.name === name);
return relation ? relation.targetNote : null;
}
/**
2024-04-09 22:13:01 +03:00
* @param name - label name
* @returns true if label exists (including inherited)
2021-10-16 22:13:34 +02:00
*/
2024-04-09 22:13:01 +03:00
hasLabel(name: string) { return this.hasAttribute(LABEL, name); }
2021-10-16 22:13:34 +02:00
/**
2024-04-09 22:13:01 +03:00
* @param name - label name
* @returns true if label exists (including inherited) and does not have "false" value.
*/
2024-04-09 22:13:01 +03:00
isLabelTruthy(name: string) {
const label = this.getLabel(name);
if (!label) {
return false;
}
2024-04-09 22:13:01 +03:00
return !!label && label.value !== 'false';
}
/**
2024-04-09 22:13:01 +03:00
* @param name - label name
* @returns true if label exists (excluding inherited)
2021-10-16 22:13:34 +02:00
*/
2024-04-09 22:13:01 +03:00
hasOwnedLabel(name: string) { return this.hasOwnedAttribute(LABEL, name); }
2021-10-16 22:13:34 +02:00
/**
2024-04-09 22:13:01 +03:00
* @param name - relation name
* @returns true if relation exists (including inherited)
2021-10-16 22:13:34 +02:00
*/
2024-04-09 22:13:01 +03:00
hasRelation(name: string) { return this.hasAttribute(RELATION, name); }
2021-10-16 22:13:34 +02:00
/**
2024-04-09 22:13:01 +03:00
* @param name - relation name
* @returns true if relation exists (excluding inherited)
2021-10-16 22:13:34 +02:00
*/
2024-04-09 22:13:01 +03:00
hasOwnedRelation(name: string) { return this.hasOwnedAttribute(RELATION, name); }
2021-10-16 22:13:34 +02:00
/**
2024-04-09 22:13:01 +03:00
* @param name - label name
2024-04-09 22:25:32 +03:00
* @returns label if it exists, null otherwise
2021-10-16 22:13:34 +02:00
*/
2024-04-09 22:13:01 +03:00
getLabel(name: string) { return this.getAttribute(LABEL, name); }
2021-10-16 22:13:34 +02:00
/**
2024-04-09 22:13:01 +03:00
* @param name - label name
2024-04-09 22:25:32 +03:00
* @returns label if it exists, null otherwise
2021-10-16 22:13:34 +02:00
*/
2024-04-09 22:13:01 +03:00
getOwnedLabel(name: string) { return this.getOwnedAttribute(LABEL, name); }
2021-10-16 22:13:34 +02:00
/**
2024-04-09 22:13:01 +03:00
* @param name - relation name
2024-04-09 22:25:32 +03:00
* @returns relation if it exists, null otherwise
2021-10-16 22:13:34 +02:00
*/
2024-04-09 22:13:01 +03:00
getRelation(name: string) { return this.getAttribute(RELATION, name); }
2021-10-16 22:13:34 +02:00
/**
2024-04-09 22:13:01 +03:00
* @param name - relation name
2024-04-09 22:25:32 +03:00
* @returns relation if it exists, null otherwise
2021-10-16 22:13:34 +02:00
*/
2024-04-09 22:13:01 +03:00
getOwnedRelation(name: string) { return this.getOwnedAttribute(RELATION, name); }
2021-10-16 22:13:34 +02:00
/**
2024-04-09 22:13:01 +03:00
* @param name - label name
* @returns label value if label exists, null otherwise
2021-10-16 22:13:34 +02:00
*/
2024-04-09 22:13:01 +03:00
getLabelValue(name: string) { return this.getAttributeValue(LABEL, name); }
2021-10-16 22:13:34 +02:00
/**
2024-04-09 22:13:01 +03:00
* @param name - label name
* @returns label value if label exists, null otherwise
2021-10-16 22:13:34 +02:00
*/
2024-04-09 22:13:01 +03:00
getOwnedLabelValue(name: string) { return this.getOwnedAttributeValue(LABEL, name); }
2021-10-16 22:13:34 +02:00
/**
2024-04-09 22:13:01 +03:00
* @param name - relation name
* @returns relation value if relation exists, null otherwise
2021-10-16 22:13:34 +02:00
*/
2024-04-09 22:13:01 +03:00
getRelationValue(name: string) { return this.getAttributeValue(RELATION, name); }
2021-10-16 22:13:34 +02:00
/**
2024-04-09 22:13:01 +03:00
* @param name - relation name
* @returns relation value if relation exists, null otherwise
2021-10-16 22:13:34 +02:00
*/
2024-04-09 22:13:01 +03:00
getOwnedRelationValue(name: string) { return this.getOwnedAttributeValue(RELATION, name); }
2021-10-16 22:13:34 +02:00
/**
2024-04-09 22:13:01 +03:00
* @param type - attribute type (label, relation, etc.)
* @param name - attribute name
* @returns true if note has an attribute with given type and name (excluding inherited)
2021-10-16 22:13:34 +02:00
*/
2024-04-09 22:13:01 +03:00
hasOwnedAttribute(type: string, name: string) {
2021-10-16 22:13:34 +02:00
return !!this.getOwnedAttribute(type, name);
}
/**
2024-04-09 22:13:01 +03:00
* @param type - attribute type (label, relation, etc.)
* @param name - attribute name
2024-04-09 22:25:32 +03:00
* @returns attribute of the given type and name. If there are more such attributes, first is returned.
* Returns null if there's no such attribute belonging to this note.
2021-10-16 22:13:34 +02:00
*/
2024-04-09 22:13:01 +03:00
getAttribute(type: string, name: string) {
2021-10-16 22:13:34 +02:00
const attributes = this.getAttributes();
return attributes.find(attr => attr.type === type && attr.name === name);
}
/**
2024-04-09 22:13:01 +03:00
* @param type - attribute type (label, relation, etc.)
* @param name - attribute name
* @returns attribute value of the given type and name or null if no such attribute exists.
2021-10-16 22:13:34 +02:00
*/
2024-04-09 22:13:01 +03:00
getAttributeValue(type: string, name: string) {
2021-10-16 22:13:34 +02:00
const attr = this.getAttribute(type, name);
return attr ? attr.value : null;
}
/**
2024-04-09 22:13:01 +03:00
* @param type - attribute type (label, relation, etc.)
* @param name - attribute name
* @returns attribute value of the given type and name or null if no such attribute exists.
2021-10-16 22:13:34 +02:00
*/
2024-04-09 22:13:01 +03:00
getOwnedAttributeValue(type: string, name: string) {
2021-10-16 22:13:34 +02:00
const attr = this.getOwnedAttribute(type, name);
2024-04-09 22:13:01 +03:00
return attr ? attr.value as string : null; // FIXME
2021-10-16 22:13:34 +02:00
}
/**
2024-04-09 22:13:01 +03:00
* @param name - label name to filter
2024-04-09 22:25:32 +03:00
* @returns all note's labels (attributes with type label), including inherited ones
2021-10-16 22:13:34 +02:00
*/
2024-04-09 22:13:01 +03:00
getLabels(name: string) {
2021-10-16 22:13:34 +02:00
return this.getAttributes(LABEL, name);
}
/**
2024-04-09 22:13:01 +03:00
* @param name - label name to filter
* @returns all note's label values, including inherited ones
2021-10-16 22:13:34 +02:00
*/
2024-04-09 22:13:01 +03:00
getLabelValues(name: string) {
return this.getLabels(name).map(l => l.value) as string[]; // FIXME
2021-10-16 22:13:34 +02:00
}
/**
2024-04-09 22:13:01 +03:00
* @param name - label name to filter
2024-04-09 22:25:32 +03:00
* @returns all note's labels (attributes with type label), excluding inherited ones
2021-10-16 22:13:34 +02:00
*/
2024-04-09 22:13:01 +03:00
getOwnedLabels(name: string) {
2021-10-16 22:13:34 +02:00
return this.getOwnedAttributes(LABEL, name);
}
/**
2024-04-09 22:13:01 +03:00
* @param name - label name to filter
* @returns all note's label values, excluding inherited ones
2021-10-16 22:13:34 +02:00
*/
2024-04-09 22:13:01 +03:00
getOwnedLabelValues(name: string) {
2021-10-16 22:13:34 +02:00
return this.getOwnedAttributes(LABEL, name).map(l => l.value);
}
/**
2024-04-09 22:13:01 +03:00
* @param name - relation name to filter
2024-04-09 22:25:32 +03:00
* @returns all note's relations (attributes with type relation), including inherited ones
2021-10-16 22:13:34 +02:00
*/
2024-04-09 22:13:01 +03:00
getRelations(name: string) {
2021-10-16 22:13:34 +02:00
return this.getAttributes(RELATION, name);
}
/**
2024-04-09 22:25:32 +03:00
* @param name - relation name to filter
* @returns all note's relations (attributes with type relation), excluding inherited ones
2021-10-16 22:13:34 +02:00
*/
2024-04-09 22:13:01 +03:00
getOwnedRelations(name: string) {
2021-10-16 22:13:34 +02:00
return this.getOwnedAttributes(RELATION, name);
}
/**
2024-04-09 22:25:32 +03:00
* @param type - (optional) attribute type to filter
* @param name - (optional) attribute name to filter
* @returns note's "owned" attributes - excluding inherited ones
2021-10-16 22:13:34 +02:00
*/
2024-04-09 22:13:01 +03:00
getOwnedAttributes(type: string, name: string) {
2021-10-16 22:13:34 +02:00
// 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();
}
}
/**
2024-04-09 22:25:32 +03:00
* @returns attribute belonging to this specific note (excludes inherited attributes)
2021-10-16 22:13:34 +02:00
*
* This method can be significantly faster than the getAttribute()
*/
2024-04-09 22:13:01 +03:00
getOwnedAttribute(type: string, name: string) {
2021-10-16 22:13:34 +02:00
const attrs = this.getOwnedAttributes(type, name);
return attrs.length > 0 ? attrs[0] : null;
}
get isArchived() {
return this.hasAttribute('label', 'archived');
}
2023-01-06 20:31:55 +01:00
isInherited() {
return !!this.targetRelations.find(rel => rel.name === 'template' || rel.name === 'inherit');
2021-10-16 22:13:34 +02:00
}
getTargetRelations() {
return this.targetRelations;
}
2023-06-05 23:05:05 +02:00
getAttachments() {
return this.attachments;
}
2024-04-09 22:13:01 +03:00
getAttachmentByTitle(title: string) {
return this.attachments.find(attachment => attachment.title === title);
}
2021-12-22 10:57:02 +01:00
get shareId() {
if (this.hasOwnedLabel('shareRoot')) {
return "";
}
2021-12-22 10:57:02 +01:00
const sharedAlias = this.getOwnedLabelValue("shareAlias");
return sharedAlias || this.noteId;
}
get escapedTitle() {
return escape(this.title);
}
get encodedTitle() {
return encodeURIComponent(this.title);
}
2023-06-05 23:05:05 +02:00
getPojo() {
return {
noteId: this.noteId,
title: this.title,
type: this.type,
mime: this.mime,
utcDateModified: this.utcDateModified,
attributes: this.getAttributes()
// relations could link across shared subtrees which might leak them
// individual relations might be whitelisted based on needs #3434
.filter(attr => attr.type === 'label')
.map(attr => attr.getPojo()),
2023-06-05 23:05:05 +02:00
attachments: this.getAttachments()
.map(attachment => attachment.getPojo()),
parentNoteIds: this.parents.map(parentNote => parentNote.noteId),
childNoteIds: this.children.map(child => child.noteId)
};
}
2021-10-16 22:13:34 +02:00
}
export = SNote;