123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779 |
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="utf-8">
- <title>JSDoc: Source: becca/entities/bnote.js</title>
- <script src="scripts/prettify/prettify.js"> </script>
- <script src="scripts/prettify/lang-css.js"> </script>
- <!--[if lt IE 9]>
- <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
- <![endif]-->
- <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
- <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
- </head>
- <body>
- <div id="main">
- <h1 class="page-title">Source: becca/entities/bnote.js</h1>
-
-
- <section>
- <article>
- <pre class="prettyprint source linenums"><code>"use strict";
- const protectedSessionService = require('../../services/protected_session');
- const log = require('../../services/log');
- const sql = require('../../services/sql');
- const utils = require('../../services/utils');
- const dateUtils = require('../../services/date_utils');
- const AbstractBeccaEntity = require("./abstract_becca_entity");
- const BRevision = require("./brevision");
- const BAttachment = require("./battachment");
- const TaskContext = require("../../services/task_context");
- const dayjs = require("dayjs");
- const utc = require('dayjs/plugin/utc');
- const eventService = require("../../services/events");
- dayjs.extend(utc);
- const LABEL = 'label';
- const RELATION = 'relation';
- /**
- * There are many different Note types, some of which are entirely opaque to the
- * end user. Those types should be used only for checking against, they are
- * not for direct use.
- * @typedef {"file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code"} NoteType
- */
- /**
- * @typedef {Object} NotePathRecord
- * @property {boolean} isArchived
- * @property {boolean} isInHoistedSubTree
- * @property {Array<string>} notePath
- * @property {boolean} isHidden
- */
- /**
- * Trilium's main entity, which can represent text note, image, code note, file attachment etc.
- *
- * @extends AbstractBeccaEntity
- */
- class BNote extends AbstractBeccaEntity {
- static get entityName() { return "notes"; }
- static get primaryKeyName() { return "noteId"; }
- static get hashedProperties() { return ["noteId", "title", "isProtected", "type", "mime", "blobId"]; }
- constructor(row) {
- super();
- if (!row) {
- return;
- }
- this.updateFromRow(row);
- this.init();
- }
- updateFromRow(row) {
- this.update([
- row.noteId,
- row.title,
- row.type,
- row.mime,
- row.isProtected,
- row.blobId,
- row.dateCreated,
- row.dateModified,
- row.utcDateCreated,
- row.utcDateModified
- ]);
- }
- update([noteId, title, type, mime, isProtected, blobId, dateCreated, dateModified, utcDateCreated, utcDateModified]) {
- // ------ Database persisted attributes ------
- /** @type {string} */
- this.noteId = noteId;
- /** @type {string} */
- this.title = title;
- /** @type {NoteType} */
- this.type = type;
- /** @type {string} */
- this.mime = mime;
- /** @type {boolean} */
- this.isProtected = !!isProtected;
- /** @type {string} */
- this.blobId = blobId;
- /** @type {string} */
- this.dateCreated = dateCreated || dateUtils.localNowDateTime();
- /** @type {string} */
- this.dateModified = dateModified;
- /** @type {string} */
- this.utcDateCreated = utcDateCreated || dateUtils.utcNowDateTime();
- /** @type {string} */
- this.utcDateModified = utcDateModified;
- /**
- * set during the deletion operation, before it is completed (removed from becca completely)
- * @type {boolean}
- */
- this.isBeingDeleted = false;
- // ------ Derived attributes ------
- /** @type {boolean} */
- this.isDecrypted = !this.noteId || !this.isProtected;
- this.decrypt();
- /** @type {string|null} */
- this.__flatTextCache = null;
- return this;
- }
- init() {
- /** @type {BBranch[]}
- * @private */
- this.parentBranches = [];
- /** @type {BNote[]}
- * @private */
- this.parents = [];
- /** @type {BNote[]}
- * @private */
- this.children = [];
- /** @type {BAttribute[]}
- * @private */
- this.ownedAttributes = [];
- /** @type {BAttribute[]|null}
- * @private */
- this.__attributeCache = null;
- /** @type {BAttribute[]|null}
- * @private */
- this.__inheritableAttributeCache = null;
- /** @type {BAttribute[]}
- * @private */
- this.targetRelations = [];
- this.becca.addNote(this.noteId, this);
- /** @type {BNote[]|null}
- * @private */
- this.__ancestorCache = null;
- // following attributes are filled during searching in the database
- /**
- * size of the content in bytes
- * @type {int|null}
- * @private
- */
- this.contentSize = null;
- /**
- * size of the note content, attachment contents in bytes
- * @type {int|null}
- * @private
- */
- this.contentAndAttachmentsSize = null;
- /**
- * size of the note content, attachment contents and revision contents in bytes
- * @type {int|null}
- * @private
- */
- this.contentAndAttachmentsAndRevisionsSize = null;
- /**
- * number of note revisions for this note
- * @type {int|null}
- * @private
- */
- this.revisionCount = null;
- }
- isContentAvailable() {
- return !this.noteId // new note which was not encrypted yet
- || !this.isProtected
- || protectedSessionService.isProtectedSessionAvailable()
- }
- getTitleOrProtected() {
- return this.isContentAvailable() ? this.title : '[protected]';
- }
- /** @returns {BBranch[]} */
- getParentBranches() {
- return this.parentBranches;
- }
- /**
- * Returns <i>strong</i> (as opposed to <i>weak</i>) parent branches. See isWeak for details.
- *
- * @returns {BBranch[]}
- */
- getStrongParentBranches() {
- return this.getParentBranches().filter(branch => !branch.isWeak);
- }
- /**
- * @returns {BBranch[]}
- * @deprecated use getParentBranches() instead
- */
- getBranches() {
- return this.parentBranches;
- }
- /** @returns {BNote[]} */
- getParentNotes() {
- return this.parents;
- }
- /** @returns {BNote[]} */
- getChildNotes() {
- return this.children;
- }
- /** @returns {boolean} */
- hasChildren() {
- return this.children && this.children.length > 0;
- }
- /** @returns {BBranch[]} */
- getChildBranches() {
- return this.children.map(childNote => this.becca.getBranchFromChildAndParent(childNote.noteId, this.noteId));
- }
- /*
- * Note content has quite special handling - it's not a separate entity, but a lazily loaded
- * part of Note entity with its own sync. Reasons behind this hybrid design has been:
- *
- * - content can be quite large, and it's not necessary to load it / fill memory for any note access even if we don't need a content, especially for bulk operations like search
- * - changes in the note metadata or title should not trigger note content sync (so we keep separate utcDateModified and entity changes records)
- * - but to the user note content and title changes are one and the same - single dateModified (so all changes must go through Note and content is not a separate entity)
- */
- /** @returns {string|Buffer} */
- getContent() {
- return this._getContent();
- }
- /**
- * @returns {*}
- * @throws Error in case of invalid JSON */
- getJsonContent() {
- const content = this.getContent();
- if (!content || !content.trim()) {
- return null;
- }
- return JSON.parse(content);
- }
- /** @returns {*|null} valid object or null if the content cannot be parsed as JSON */
- getJsonContentSafely() {
- try {
- return this.getJsonContent();
- }
- catch (e) {
- return null;
- }
- }
- /**
- * @param content
- * @param {object} [opts]
- * @param {object} [opts.forceSave=false] - will also save this BNote entity
- * @param {object} [opts.forceFrontendReload=false] - override frontend heuristics on when to reload, instruct to reload
- */
- setContent(content, opts) {
- this._setContent(content, opts);
- eventService.emit(eventService.NOTE_CONTENT_CHANGE, { entity: this });
- }
- setJsonContent(content) {
- this.setContent(JSON.stringify(content, null, '\t'));
- }
- get dateCreatedObj() {
- return this.dateCreated === null ? null : dayjs(this.dateCreated);
- }
- get utcDateCreatedObj() {
- return this.utcDateCreated === null ? null : dayjs.utc(this.utcDateCreated);
- }
- get dateModifiedObj() {
- return this.dateModified === null ? null : dayjs(this.dateModified);
- }
- get utcDateModifiedObj() {
- return this.utcDateModified === null ? null : dayjs.utc(this.utcDateModified);
- }
- /** @returns {boolean} true if this note is the root of the note tree. Root note has "root" noteId */
- isRoot() {
- return this.noteId === 'root';
- }
- /** @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.type === 'launcher')
- && (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 this note is an image */
- isImage() {
- return this.type === 'image'
- || (this.type === 'file' && this.mime?.startsWith('image/'));
- }
- /** @deprecated use hasStringContent() instead */
- isStringNote() {
- return this.hasStringContent();
- }
- /** @returns {boolean} true if the note has string content (not binary) */
- hasStringContent() {
- return utils.isStringNote(this.type, this.mime);
- }
- /** @returns {string|null} JS script environment - either "frontend" or "backend" */
- getScriptEnv() {
- if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) {
- return "frontend";
- }
- if (this.type === 'render') {
- return "frontend";
- }
- if (this.isJavaScript() && this.mime.endsWith('env=backend')) {
- return "backend";
- }
- return null;
- }
- /**
- * Beware that the method must not create a copy of the array, but actually returns its internal array
- * (for performance reasons)
- *
- * @param {string} [type] - (optional) attribute type to filter
- * @param {string} [name] - (optional) attribute name to filter
- * @returns {BAttribute[]} all note's attributes, including inherited ones
- */
- getAttributes(type, name) {
- this.__validateTypeName(type, name);
- this.__ensureAttributeCacheIsAvailable();
- if (type && name) {
- return this.__attributeCache.filter(attr => attr.name === name && attr.type === type);
- }
- 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;
- }
- }
- /** @private */
- __ensureAttributeCacheIsAvailable() {
- if (!this.__attributeCache) {
- this.__getAttributes([]);
- }
- }
- /** @private */
- __getAttributes(path) {
- if (path.includes(this.noteId)) {
- return [];
- }
- if (!this.__attributeCache) {
- const parentAttributes = this.ownedAttributes.slice();
- const newPath = [...path, this.noteId];
- // inheritable attrs on root are typically not intended to be applied to hidden subtree #3537
- if (this.noteId !== 'root' && this.noteId !== '_hidden') {
- 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' && ['template', 'inherit'].includes(ownedAttr.name)) {
- const templateNote = this.becca.notes[ownedAttr.value];
- if (templateNote) {
- templateAttributes.push(
- ...templateNote.__getAttributes(newPath)
- // template attr is used as a marker for templates, but it's not meant to be inherited
- .filter(attr => !(attr.type === 'label' && (attr.name === 'template' || attr.name === 'workspacetemplate')))
- );
- }
- }
- }
- 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;
- }
- /**
- * @private
- * @returns {BAttribute[]}
- */
- __getInheritableAttributes(path) {
- if (path.includes(this.noteId)) {
- return [];
- }
- if (!this.__inheritableAttributeCache) {
- this.__getAttributes(path); // will refresh also this.__inheritableAttributeCache
- }
- return this.__inheritableAttributeCache;
- }
- __validateTypeName(type, name) {
- if (type && type !== 'label' && type !== 'relation') {
- throw new Error(`Unrecognized attribute type '${type}'. Only 'label' and 'relation' are possible values.`);
- }
- if (name) {
- const firstLetter = name.charAt(0);
- if (firstLetter === '#' || firstLetter === '~') {
- throw new Error(`Detect '#' or '~' in the attribute's name. In the API, attribute names should be set without these characters.`);
- }
- }
- }
- /**
- * @param type
- * @param name
- * @param [value]
- * @returns {boolean}
- */
- hasAttribute(type, name, value = null) {
- return !!this.getAttributes().find(attr =>
- attr.name === name
- && (value === undefined || value === null || attr.value === value)
- && attr.type === type
- );
- }
- getAttributeCaseInsensitive(type, name, value) {
- name = name.toLowerCase();
- value = value ? value.toLowerCase() : null;
- return this.getAttributes().find(
- attr => attr.name.toLowerCase() === name
- && (!value || attr.value.toLowerCase() === value)
- && attr.type === type);
- }
- getRelationTarget(name) {
- const relation = this.getAttributes().find(attr => attr.name === name && attr.type === 'relation');
- return relation ? relation.targetNote : null;
- }
- /**
- * @param {string} name - label name
- * @param {string} [value] - label value
- * @returns {boolean} true if label exists (including inherited)
- */
- hasLabel(name, value) { return this.hasAttribute(LABEL, name, value); }
- /**
- * @param {string} name - label name
- * @returns {boolean} true if label exists (including inherited) and does not have "false" value.
- */
- isLabelTruthy(name) {
- const label = this.getLabel(name);
- if (!label) {
- return false;
- }
- return label && label.value !== 'false';
- }
- /**
- * @param {string} name - label name
- * @param {string} [value] - label value
- * @returns {boolean} true if label exists (excluding inherited)
- */
- hasOwnedLabel(name, value) { return this.hasOwnedAttribute(LABEL, name, value); }
- /**
- * @param {string} name - relation name
- * @param {string} [value] - relation value
- * @returns {boolean} true if relation exists (including inherited)
- */
- hasRelation(name, value) { return this.hasAttribute(RELATION, name, value); }
- /**
- * @param {string} name - relation name
- * @param {string} [value] - relation value
- * @returns {boolean} true if relation exists (excluding inherited)
- */
- hasOwnedRelation(name, value) { return this.hasOwnedAttribute(RELATION, name, value); }
- /**
- * @param {string} name - label name
- * @returns {BAttribute|null} label if it exists, null otherwise
- */
- getLabel(name) { return this.getAttribute(LABEL, name); }
- /**
- * @param {string} name - label name
- * @returns {BAttribute|null} label if it exists, null otherwise
- */
- getOwnedLabel(name) { return this.getOwnedAttribute(LABEL, name); }
- /**
- * @param {string} name - relation name
- * @returns {BAttribute|null} relation if it exists, null otherwise
- */
- getRelation(name) { return this.getAttribute(RELATION, name); }
- /**
- * @param {string} name - relation name
- * @returns {BAttribute|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
- * @param {string} [value] - attribute value
- * @returns {boolean} true if note has an attribute with given type and name (excluding inherited)
- */
- hasOwnedAttribute(type, name, value) {
- return !!this.getOwnedAttribute(type, name, value);
- }
- /**
- * @param {string} type - attribute type (label, relation, etc.)
- * @param {string} name - attribute name
- * @returns {BAttribute} 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.
- */
- getAttribute(type, name) {
- const attributes = this.getAttributes();
- return attributes.find(attr => attr.name === name && attr.type === type);
- }
- /**
- * @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 {BAttribute[]} 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 {BAttribute[]} 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 {BAttribute[]} 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 {BAttribute[]} all note's relations (attributes with type relation), excluding inherited ones
- */
- getOwnedRelations(name) {
- return this.getOwnedAttributes(RELATION, name);
- }
- /**
- * Beware that the method must not create a copy of the array, but actually returns its internal array
- * (for performance reasons)
- *
- * @param {string|null} [type] - (optional) attribute type to filter
- * @param {string|null} [name] - (optional) attribute name to filter
- * @param {string|null} [value] - (optional) attribute value to filter
- * @returns {BAttribute[]} note's "owned" attributes - excluding inherited ones
- */
- getOwnedAttributes(type = null, name = null, value = null) {
- this.__validateTypeName(type, name);
- if (type && name && value !== undefined && value !== null) {
- return this.ownedAttributes.filter(attr => attr.name === name && attr.value === value && attr.type === type);
- }
- else if (type && name) {
- return this.ownedAttributes.filter(attr => attr.name === name && attr.type === type);
- }
- 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;
- }
- }
- /**
- * @returns {BAttribute} attribute belonging to this specific note (excludes inherited attributes)
- *
- * This method can be significantly faster than the getAttribute()
- */
- getOwnedAttribute(type, name, value = null) {
- const attrs = this.getOwnedAttributes(type, name, value);
- return attrs.length > 0 ? attrs[0] : null;
- }
- get isArchived() {
- return this.hasAttribute('label', 'archived');
- }
- areAllNotePathsArchived() {
- // there's a slight difference between note being itself archived and all its note paths being archived
- // - note is archived when it itself has an archived label or inherits it
- // - note does not have or inherit archived label, but each note path contains a note with (non-inheritable)
- // archived label
- const bestNotePathRecord = this.getSortedNotePathRecords()[0];
- if (!bestNotePathRecord) {
- throw new Error(`No note path available for note '${this.noteId}'`);
- }
- return bestNotePathRecord.isArchived;
- }
- hasInheritableArchivedLabel() {
- for (const attr of this.getAttributes()) {
- if (attr.name === 'archived' && attr.type === LABEL && attr.isInheritable) {
- return true;
- }
- }
- return false;
- }
- // will sort the parents so that the non-archived are first and archived at the end
- // this is done so that the non-archived paths are always explored as first when looking for note path
- sortParents() {
- this.parentBranches.sort((a, b) => {
- if (a.parentNote?.isArchived) {
- return 1;
- } else if (a.parentNote?.isHiddenCompletely()) {
- return 1;
- } else {
- return 0;
- }
- });
- this.parents = this.parentBranches
- .map(branch => branch.parentNote)
- .filter(note => !!note);
- }
- sortChildren() {
- if (this.children.length === 0) {
- return;
- }
- const becca = this.becca;
- this.children.sort((a, b) => {
- const aBranch = becca.getBranchFromChildAndParent(a.noteId, this.noteId);
- const bBranch = becca.getBranchFromChildAndParent(b.noteId, this.noteId);
- return (aBranch?.notePosition - bBranch?.notePosition) || 0;
- });
- }
- /**
- * This is used for:
- * - fast searching
- * - note similarity evaluation
- *
- * @returns {string} - returns flattened textual representation of note, prefixes and attributes
- */
- getFlatText() {
- 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.getAttributes()) {
- // 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 = utils.normalize(this.__flatTextCache);
- }
- return this.__flatTextCache;
- }
- invalidateThisCache() {
- this.__flatTextCache = null;
- this.__attributeCache = null;
- this.__inheritableAttributeCache = null;
- this.__ancestorCache = null;
- }
- invalidateSubTree(path = []) {
- if (path.includes(this.noteId)) {
- return;
- }
- this.invalidateThisCache();
- if (this.children.length || this.targetRelations.length) {
- path = [...path, this.noteId];
- }
- for (const childNote of this.children) {
- childNote.invalidateSubTree(path);
- }
- for (const targetRelation of this.targetRelations) {
- if (targetRelation.name === 'template' || targetRelation.name === 'inherit') {
- const note = targetRelation.note;
- if (note) {
- note.invalidateSubTree(path);
- }
- }
- }
- }
- getRelationDefinitions() {
- return this.getLabels()
- .filter(l => l.name.startsWith("relation:"));
- }
- getLabelDefinitions() {
- return this.getLabels()
- .filter(l => l.name.startsWith("relation:"));
- }
- isInherited() {
- return !!this.targetRelations.find(rel => rel.name === 'template' || rel.name === 'inherit');
- }
- /** @returns {BNote[]} */
- getSubtreeNotesIncludingTemplated() {
- const set = new Set();
- function inner(note) {
- // _hidden is not counted as subtree for the purpose of inheritance
- if (set.has(note) || note.noteId === '_hidden') {
- return;
- }
- set.add(note);
- for (const childNote of note.children) {
- inner(childNote);
- }
- for (const targetRelation of note.targetRelations) {
- if (targetRelation.name === 'template' || targetRelation.name === 'inherit') {
- const targetNote = targetRelation.note;
- if (targetNote) {
- inner(targetNote);
- }
- }
- }
- }
- inner(this);
- return Array.from(set);
- }
- /** @returns {BNote[]} */
- getSearchResultNotes() {
- if (this.type !== 'search') {
- return [];
- }
- try {
- const searchService = require("../../services/search/services/search");
- const {searchResultNoteIds} = searchService.searchFromNote(this);
- const becca = this.becca;
- return searchResultNoteIds
- .map(resultNoteId => becca.notes[resultNoteId])
- .filter(note => !!note);
- }
- catch (e) {
- log.error(`Could not resolve search note ${this.noteId}: ${e.message}`);
- return [];
- }
- }
- /**
- * @returns {{notes: BNote[], relationships: Array.<{parentNoteId: string, childNoteId: string}>}}
- */
- getSubtree({includeArchived = true, includeHidden = false, resolveSearch = false} = {}) {
- const noteSet = new Set();
- const relationships = []; // list of tuples parentNoteId -> childNoteId
- function resolveSearchNote(searchNote) {
- try {
- for (const resultNote of searchNote.getSearchResultNotes()) {
- addSubtreeNotesInner(resultNote, searchNote);
- }
- }
- catch (e) {
- log.error(`Could not resolve search note ${searchNote?.noteId}: ${e.message}`);
- }
- }
- function addSubtreeNotesInner(note, parentNote = null) {
- if (note.noteId === '_hidden' && !includeHidden) {
- return;
- }
- if (parentNote) {
- // this needs to happen first before noteSet check to include all clone relationships
- relationships.push({
- parentNoteId: parentNote.noteId,
- childNoteId: note.noteId
- });
- }
- if (noteSet.has(note)) {
- return;
- }
- if (!includeArchived && note.isArchived) {
- return;
- }
- noteSet.add(note);
- if (note.type === 'search') {
- if (resolveSearch) {
- resolveSearchNote(note);
- }
- }
- else {
- for (const childNote of note.children) {
- addSubtreeNotesInner(childNote, note);
- }
- }
- }
- addSubtreeNotesInner(this);
- return {
- notes: Array.from(noteSet),
- relationships
- };
- }
- /** @returns {string[]} - includes the subtree root note as well */
- getSubtreeNoteIds({includeArchived = true, includeHidden = false, resolveSearch = false} = {}) {
- return this.getSubtree({includeArchived, includeHidden, resolveSearch})
- .notes
- .map(note => note.noteId);
- }
- /** @deprecated use getSubtreeNoteIds() instead */
- getDescendantNoteIds() {
- return this.getSubtreeNoteIds();
- }
- get parentCount() {
- return this.parents.length;
- }
- get childrenCount() {
- return this.children.length;
- }
- get labelCount() {
- return this.getAttributes().filter(attr => attr.type === 'label').length;
- }
- get ownedLabelCount() {
- return this.ownedAttributes.filter(attr => attr.type === 'label').length;
- }
- get relationCount() {
- return this.getAttributes().filter(attr => attr.type === 'relation' && !attr.isAutoLink()).length;
- }
- get relationCountIncludingLinks() {
- return this.getAttributes().filter(attr => attr.type === 'relation').length;
- }
- get ownedRelationCount() {
- return this.ownedAttributes.filter(attr => attr.type === 'relation' && !attr.isAutoLink()).length;
- }
- get ownedRelationCountIncludingLinks() {
- return this.ownedAttributes.filter(attr => attr.type === 'relation').length;
- }
- get targetRelationCount() {
- return this.targetRelations.filter(attr => !attr.isAutoLink()).length;
- }
- get targetRelationCountIncludingLinks() {
- return this.targetRelations.length;
- }
- get attributeCount() {
- return this.getAttributes().length;
- }
- get ownedAttributeCount() {
- return this.getOwnedAttributes().length;
- }
- /** @returns {BNote[]} */
- getAncestors() {
- if (!this.__ancestorCache) {
- const noteIds = new Set();
- this.__ancestorCache = [];
- for (const parent of this.parents) {
- if (noteIds.has(parent.noteId)) {
- continue;
- }
- 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;
- }
- /** @returns {string[]} */
- getAncestorNoteIds() {
- return this.getAncestors().map(note => note.noteId);
- }
- /** @returns {boolean} */
- hasAncestor(ancestorNoteId) {
- for (const ancestorNote of this.getAncestors()) {
- if (ancestorNote.noteId === ancestorNoteId) {
- return true;
- }
- }
- return false;
- }
- isInHiddenSubtree() {
- return this.noteId === '_hidden' || this.hasAncestor('_hidden');
- }
- /** @returns {BAttribute[]} */
- getTargetRelations() {
- return this.targetRelations;
- }
- /** @returns {BNote[]} - returns only notes which are templated, does not include their subtrees
- * in effect returns notes which are influenced by note's non-inheritable attributes */
- getInheritingNotes() {
- const arr = [this];
- for (const targetRelation of this.targetRelations) {
- if (targetRelation.name === 'template' || targetRelation.name === 'inherit') {
- const note = targetRelation.note;
- if (note) {
- arr.push(note);
- }
- }
- }
- return arr;
- }
- getDistanceToAncestor(ancestorNoteId) {
- if (this.noteId === ancestorNoteId) {
- return 0;
- }
- let minDistance = 999999;
- for (const parent of this.parents) {
- minDistance = Math.min(minDistance, parent.getDistanceToAncestor(ancestorNoteId) + 1);
- }
- return minDistance;
- }
- /** @returns {BRevision[]} */
- getRevisions() {
- return sql.getRows("SELECT * FROM revisions WHERE noteId = ?", [this.noteId])
- .map(row => new BRevision(row));
- }
- /** @returns {BAttachment[]} */
- getAttachments(opts = {}) {
- opts.includeContentLength = !!opts.includeContentLength;
- // from testing, it looks like calculating length does not make a difference in performance even on large-ish DB
- // given that we're always fetching attachments only for a specific note, we might just do it always
- const query = opts.includeContentLength
- ? `SELECT attachments.*, LENGTH(blobs.content) AS contentLength
- FROM attachments
- JOIN blobs USING (blobId)
- WHERE ownerId = ? AND isDeleted = 0
- ORDER BY position`
- : `SELECT * FROM attachments WHERE ownerId = ? AND isDeleted = 0 ORDER BY position`;
- return sql.getRows(query, [this.noteId])
- .map(row => new BAttachment(row));
- }
- /** @returns {BAttachment|null} */
- getAttachmentById(attachmentId, opts = {}) {
- opts.includeContentLength = !!opts.includeContentLength;
- const query = opts.includeContentLength
- ? `SELECT attachments.*, LENGTH(blobs.content) AS contentLength
- FROM attachments
- JOIN blobs USING (blobId)
- WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`
- : `SELECT * FROM attachments WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`;
- return sql.getRows(query, [this.noteId, attachmentId])
- .map(row => new BAttachment(row))[0];
- }
- /** @returns {BAttachment[]} */
- getAttachmentsByRole(role) {
- return sql.getRows(`
- SELECT attachments.*
- FROM attachments
- WHERE ownerId = ?
- AND role = ?
- AND isDeleted = 0
- ORDER BY position`, [this.noteId, role])
- .map(row => new BAttachment(row));
- }
- /** @returns {BAttachment} */
- getAttachmentByTitle(title) {
- // cannot use SQL to filter by title since it can be encrypted
- return this.getAttachments().filter(attachment => attachment.title === title)[0];
- }
- /**
- * Gives all possible note paths leading to this note. Paths containing search note are ignored (could form cycles)
- *
- * @returns {string[][]} - array of notePaths (each represented by array of noteIds constituting the particular note path)
- */
- getAllNotePaths() {
- if (this.noteId === 'root') {
- return [['root']];
- }
- const parentNotes = this.getParentNotes();
- const notePaths = parentNotes.length === 1
- ? parentNotes[0].getAllNotePaths() // optimization for the most common case
- : parentNotes.flatMap(parentNote => parentNote.getAllNotePaths());
- for (const notePath of notePaths) {
- notePath.push(this.noteId);
- }
- return notePaths;
- }
- /**
- * @param {string} [hoistedNoteId='root']
- * @return {Array<NotePathRecord>}
- */
- getSortedNotePathRecords(hoistedNoteId = 'root') {
- const isHoistedRoot = hoistedNoteId === 'root';
- const notePaths = this.getAllNotePaths().map(path => ({
- notePath: path,
- isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId),
- isArchived: path.some(noteId => this.becca.notes[noteId].isArchived),
- isHidden: path.includes('_hidden')
- }));
- notePaths.sort((a, b) => {
- if (a.isInHoistedSubTree !== b.isInHoistedSubTree) {
- return a.isInHoistedSubTree ? -1 : 1;
- } else if (a.isArchived !== b.isArchived) {
- return a.isArchived ? 1 : -1;
- } else if (a.isHidden !== b.isHidden) {
- return a.isHidden ? 1 : -1;
- } else {
- return a.notePath.length - b.notePath.length;
- }
- });
- return notePaths;
- }
- /**
- * Returns a note path considered to be the "best"
- *
- * @param {string} [hoistedNoteId='root']
- * @return {string[]} array of noteIds constituting the particular note path
- */
- getBestNotePath(hoistedNoteId = 'root') {
- return this.getSortedNotePathRecords(hoistedNoteId)[0]?.notePath;
- }
- /**
- * Returns a note path considered to be the "best"
- *
- * @param {string} [hoistedNoteId='root']
- * @return {string} serialized note path (e.g. 'root/a1h315/js725h')
- */
- getBestNotePathString(hoistedNoteId = 'root') {
- const notePath = this.getBestNotePath(hoistedNoteId);
- return notePath?.join("/");
- }
- /**
- * @return boolean - true if there's no non-hidden path, note is not cloned to the visible tree
- */
- isHiddenCompletely() {
- if (this.noteId === 'root') {
- return false;
- }
- for (const parentNote of this.parents) {
- if (parentNote.noteId === 'root') {
- return false;
- } else if (parentNote.noteId === '_hidden') {
- continue;
- } else if (!parentNote.isHiddenCompletely()) {
- return false;
- }
- }
- return true;
- }
- /**
- * @param ancestorNoteId
- * @returns {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));
- }
- /**
- * Update's given attribute's value or creates it if it doesn't exist
- *
- * @param {string} type - attribute type (label, relation, etc.)
- * @param {string} name - attribute name
- * @param {string} [value] - attribute value (optional)
- */
- setAttribute(type, name, value) {
- const attributes = this.getOwnedAttributes();
- const attr = attributes.find(attr => attr.type === type && attr.name === name);
- value = value?.toString() || "";
- if (attr) {
- if (attr.value !== value) {
- attr.value = value;
- attr.save();
- }
- }
- else {
- const BAttribute = require("./battribute");
- new BAttribute({
- noteId: this.noteId,
- type: type,
- name: name,
- value: value
- }).save();
- }
- }
- /**
- * Removes given attribute name-value pair if it exists.
- *
- * @param {string} type - attribute type (label, relation, etc.)
- * @param {string} name - attribute name
- * @param {string} [value] - attribute value (optional)
- */
- removeAttribute(type, name, value) {
- const attributes = this.getOwnedAttributes();
- for (const attribute of attributes) {
- if (attribute.type === type && attribute.name === name && (value === undefined || value === attribute.value)) {
- attribute.markAsDeleted();
- }
- }
- }
- /**
- * Adds a new attribute to this note. The attribute is saved and returned.
- * See addLabel, addRelation for more specific methods.
- *
- * @param {string} type - attribute type (label / relation)
- * @param {string} name - name of the attribute, not including the leading ~/#
- * @param {string} [value] - value of the attribute - text for labels, target note ID for relations; optional.
- * @param {boolean} [isInheritable=false]
- * @param {int|null} [position]
- * @returns {BAttribute}
- */
- addAttribute(type, name, value = "", isInheritable = false, position = null) {
- const BAttribute = require("./battribute");
- return new BAttribute({
- noteId: this.noteId,
- type: type,
- name: name,
- value: value,
- isInheritable: isInheritable,
- position: position
- }).save();
- }
- /**
- * Adds a new label to this note. The label attribute is saved and returned.
- *
- * @param {string} name - name of the label, not including the leading #
- * @param {string} [value] - text value of the label; optional
- * @param {boolean} [isInheritable=false]
- * @returns {BAttribute}
- */
- addLabel(name, value = "", isInheritable = false) {
- return this.addAttribute(LABEL, name, value, isInheritable);
- }
- /**
- * Adds a new relation to this note. The relation attribute is saved and
- * returned.
- *
- * @param {string} name - name of the relation, not including the leading ~
- * @param {string} targetNoteId
- * @param {boolean} [isInheritable=false]
- * @returns {BAttribute}
- */
- addRelation(name, targetNoteId, isInheritable = false) {
- return this.addAttribute(RELATION, name, targetNoteId, isInheritable);
- }
- /**
- * Based on enabled, the attribute is either set or removed.
- *
- * @param {string} type - attribute type ('relation', 'label' etc.)
- * @param {boolean} enabled - toggle On or Off
- * @param {string} name - attribute name
- * @param {string} [value] - attribute value (optional)
- */
- toggleAttribute(type, enabled, name, value) {
- if (enabled) {
- this.setAttribute(type, name, value);
- }
- else {
- this.removeAttribute(type, name, value);
- }
- }
- /**
- * Based on enabled, label is either set or removed.
- *
- * @param {boolean} enabled - toggle On or Off
- * @param {string} name - label name
- * @param {string} [value] - label value (optional)
- */
- toggleLabel(enabled, name, value) { return this.toggleAttribute(LABEL, enabled, name, value); }
- /**
- * Based on enabled, relation is either set or removed.
- *
- * @param {boolean} enabled - toggle On or Off
- * @param {string} name - relation name
- * @param {string} [value] - relation value (noteId)
- */
- toggleRelation(enabled, name, value) { return this.toggleAttribute(RELATION, enabled, name, value); }
- /**
- * Update's given label's value or creates it if it doesn't exist
- *
- * @param {string} name - label name
- * @param {string} [value] - label value
- */
- setLabel(name, value) { return this.setAttribute(LABEL, name, value); }
- /**
- * Update's given relation's value or creates it if it doesn't exist
- *
- * @param {string} name - relation name
- * @param {string} value - relation value (noteId)
- */
- setRelation(name, value) { return this.setAttribute(RELATION, name, value); }
- /**
- * Remove label name-value pair, if it exists.
- *
- * @param {string} name - label name
- * @param {string} [value] - label value
- */
- removeLabel(name, value) { return this.removeAttribute(LABEL, name, value); }
- /**
- * Remove the relation name-value pair, if it exists.
- *
- * @param {string} name - relation name
- * @param {string} [value] - relation value (noteId)
- */
- removeRelation(name, value) { return this.removeAttribute(RELATION, name, value); }
- searchNotesInSubtree(searchString) {
- const searchService = require("../../services/search/services/search");
- return searchService.searchNotes(searchString);
- }
- searchNoteInSubtree(searchString) {
- return this.searchNotesInSubtree(searchString)[0];
- }
- /**
- * @param parentNoteId
- * @returns {{success: boolean, message: string, branchId: string, notePath: string}}
- */
- cloneTo(parentNoteId) {
- const cloningService = require("../../services/cloning");
- const branch = this.becca.getNote(parentNoteId).getParentBranches()[0];
- return cloningService.cloneNoteToBranch(this.noteId, branch.branchId);
- }
- isEligibleForConversionToAttachment(opts = {autoConversion: false}) {
- if (this.type !== 'image' || !this.isContentAvailable() || this.hasChildren() || this.getParentBranches().length !== 1) {
- return false;
- }
- const targetRelations = this.getTargetRelations().filter(relation => relation.name === 'imageLink');
- if (opts.autoConversion && targetRelations.length === 0) {
- return false;
- } else if (targetRelations.length > 1) {
- return false;
- }
- const parentNote = this.getParentNotes()[0]; // at this point note can have only one parent
- const referencingNote = targetRelations[0]?.getNote();
- if (referencingNote && parentNote !== referencingNote) {
- return false;
- } else if (parentNote.type !== 'text' || !parentNote.isContentAvailable()) {
- return false;
- }
- return true;
- }
- /**
- * Some notes are eligible for conversion into an attachment of its parent, note must have these properties:
- * - it has exactly one target relation
- * - it has a relation from its parent note
- * - it has no children
- * - it has no clones
- * - the parent is of type text
- * - both notes are either unprotected or user is in protected session
- *
- * Currently, works only for image notes.
- *
- * In the future, this functionality might get more generic and some of the requirements relaxed.
- *
- * @params {Object} [opts]
- * @params {bolean} [opts.autoConversion=false} if true, the action is not triggered by user, but e.g. by migration,
- * and only perfect candidates will be migrated
- *
- * @returns {BAttachment|null} - null if note is not eligible for conversion
- */
- convertToParentAttachment(opts = {autoConversion: false}) {
- if (!this.isEligibleForConversionToAttachment(opts)) {
- return null;
- }
- const content = this.getContent();
- const parentNote = this.getParentNotes()[0];
- const attachment = parentNote.saveAttachment({
- role: 'image',
- mime: this.mime,
- title: this.title,
- content: content
- });
- let parentContent = parentNote.getContent();
- const oldNoteUrl = `api/images/${this.noteId}/`;
- const newAttachmentUrl = `api/attachments/${attachment.attachmentId}/image/`;
- const fixedContent = utils.replaceAll(parentContent, oldNoteUrl, newAttachmentUrl);
- parentNote.setContent(fixedContent);
- const noteService = require("../../services/notes");
- noteService.asyncPostProcessContent(parentNote, fixedContent); // to mark an unused attachment for deletion
- this.deleteNote();
- return attachment;
- }
- /**
- * (Soft) delete a note and all its descendants.
- *
- * @param {string} [deleteId=null] - optional delete identified
- * @param {TaskContext} [taskContext]
- */
- deleteNote(deleteId = null, taskContext = null) {
- if (this.isDeleted) {
- return;
- }
- if (!deleteId) {
- deleteId = utils.randomString(10);
- }
- if (!taskContext) {
- taskContext = new TaskContext('no-progress-reporting');
- }
- // needs to be run before branches and attributes are deleted and thus attached relations disappear
- const handlers = require("../../services/handlers");
- handlers.runAttachedRelations(this, 'runOnNoteDeletion', this);
- taskContext.noteDeletionHandlerTriggered = true;
- for (const branch of this.getParentBranches()) {
- branch.deleteBranch(deleteId, taskContext);
- }
- }
- decrypt() {
- if (this.isProtected && !this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) {
- try {
- this.title = protectedSessionService.decryptString(this.title);
- this.__flatTextCache = null;
- this.isDecrypted = true;
- }
- catch (e) {
- log.error(`Could not decrypt note ${this.noteId}: ${e.message} ${e.stack}`);
- }
- }
- }
- isLaunchBarConfig() {
- return this.type === 'launcher' || ['_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(this.noteId);
- }
- isOptions() {
- return this.noteId.startsWith("_options");
- }
- get isDeleted() {
- // isBeingDeleted is relevant only in the transition period when the deletion process has begun, but not yet
- // finished (note is still in becca)
- return !(this.noteId in this.becca.notes) || this.isBeingDeleted;
- }
- /**
- * @returns {BRevision|null}
- */
- saveRevision() {
- return sql.transactional(() => {
- let noteContent = this.getContent();
- const revision = new BRevision({
- noteId: this.noteId,
- // title and text should be decrypted now
- title: this.title,
- type: this.type,
- mime: this.mime,
- isProtected: this.isProtected,
- utcDateLastEdited: this.utcDateModified,
- utcDateCreated: dateUtils.utcNowDateTime(),
- utcDateModified: dateUtils.utcNowDateTime(),
- dateLastEdited: this.dateModified,
- dateCreated: dateUtils.localNowDateTime()
- }, true);
- revision.save(); // to generate revisionId, which is then used to save attachments
- for (const noteAttachment of this.getAttachments()) {
- const revisionAttachment = noteAttachment.copy();
- revisionAttachment.ownerId = revision.revisionId;
- revisionAttachment.setContent(noteAttachment.getContent(), {forceSave: true});
- if (this.type === 'text') {
- // content is rewritten to point to the revision attachments
- noteContent = noteContent.replaceAll(`attachments/${noteAttachment.attachmentId}`,
- `attachments/${revisionAttachment.attachmentId}`);
- noteContent = noteContent.replaceAll(new RegExp(`href="[^"]*attachmentId=${noteAttachment.attachmentId}[^"]*"`, 'gi'),
- `href="api/attachments/${revisionAttachment.attachmentId}/download"`);
- }
- }
- revision.setContent(noteContent);
- return revision;
- });
- }
- /**
- * @param {string} matchBy - choose by which property we detect if to update an existing attachment.
- * Supported values are either 'attachmentId' (default) or 'title'
- * @returns {BAttachment}
- */
- saveAttachment({attachmentId, role, mime, title, content, position}, matchBy = 'attachmentId') {
- if (!['attachmentId', 'title'].includes(matchBy)) {
- throw new Error(`Unsupported value '${matchBy}' for matchBy param, has to be either 'attachmentId' or 'title'.`);
- }
- let attachment;
- if (matchBy === 'title') {
- attachment = this.getAttachmentByTitle(title);
- } else if (matchBy === 'attachmentId' && attachmentId) {
- attachment = this.becca.getAttachmentOrThrow(attachmentId);
- }
- attachment = attachment || new BAttachment({
- ownerId: this.noteId,
- title,
- role,
- mime,
- isProtected: this.isProtected,
- position
- });
- content = content || "";
- attachment.setContent(content, {forceSave: true});
- return attachment;
- }
- getFileName() {
- return utils.formatDownloadTitle(this.title, this.type, this.mime);
- }
- beforeSaving() {
- super.beforeSaving();
- this.becca.addNote(this.noteId, this);
- this.dateModified = dateUtils.localNowDateTime();
- this.utcDateModified = dateUtils.utcNowDateTime();
- }
- getPojo() {
- return {
- noteId: this.noteId,
- title: this.title,
- isProtected: this.isProtected,
- type: this.type,
- mime: this.mime,
- blobId: this.blobId,
- isDeleted: false,
- dateCreated: this.dateCreated,
- dateModified: this.dateModified,
- utcDateCreated: this.utcDateCreated,
- utcDateModified: this.utcDateModified
- };
- }
- getPojoToSave() {
- const pojo = this.getPojo();
- if (pojo.isProtected) {
- if (this.isDecrypted) {
- pojo.title = protectedSessionService.encrypt(pojo.title);
- }
- else {
- // updating protected note outside of protected session means we will keep original ciphertexts
- delete pojo.title;
- }
- }
- return pojo;
- }
- }
- module.exports = BNote;
- </code></pre>
- </article>
- </section>
- </div>
- <nav>
- <h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-sql.html">sql</a></li></ul><h3>Classes</h3><ul><li><a href="AbstractBeccaEntity.html">AbstractBeccaEntity</a></li><li><a href="BAttachment.html">BAttachment</a></li><li><a href="BAttribute.html">BAttribute</a></li><li><a href="BBranch.html">BBranch</a></li><li><a href="BEtapiToken.html">BEtapiToken</a></li><li><a href="BNote.html">BNote</a></li><li><a href="BOption.html">BOption</a></li><li><a href="BRecentNote.html">BRecentNote</a></li><li><a href="BRevision.html">BRevision</a></li><li><a href="BackendScriptApi.html">BackendScriptApi</a></li></ul><h3>Global</h3><ul><li><a href="global.html#api">api</a></li></ul>
- </nav>
- <br class="clear">
- <footer>
- Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.2</a>
- </footer>
- <script> prettyPrint(); </script>
- <script src="scripts/linenumber.js"> </script>
- </body>
- </html>
|