becca_entities_bnote.js.html 57 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <title>JSDoc: Source: becca/entities/bnote.js</title>
  6. <script src="scripts/prettify/prettify.js"> </script>
  7. <script src="scripts/prettify/lang-css.js"> </script>
  8. <!--[if lt IE 9]>
  9. <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
  10. <![endif]-->
  11. <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
  12. <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
  13. </head>
  14. <body>
  15. <div id="main">
  16. <h1 class="page-title">Source: becca/entities/bnote.js</h1>
  17. <section>
  18. <article>
  19. <pre class="prettyprint source linenums"><code>"use strict";
  20. const protectedSessionService = require('../../services/protected_session');
  21. const log = require('../../services/log');
  22. const sql = require('../../services/sql');
  23. const utils = require('../../services/utils');
  24. const dateUtils = require('../../services/date_utils');
  25. const AbstractBeccaEntity = require("./abstract_becca_entity");
  26. const BRevision = require("./brevision");
  27. const BAttachment = require("./battachment");
  28. const TaskContext = require("../../services/task_context");
  29. const dayjs = require("dayjs");
  30. const utc = require('dayjs/plugin/utc');
  31. const eventService = require("../../services/events");
  32. dayjs.extend(utc);
  33. const LABEL = 'label';
  34. const RELATION = 'relation';
  35. /**
  36. * There are many different Note types, some of which are entirely opaque to the
  37. * end user. Those types should be used only for checking against, they are
  38. * not for direct use.
  39. * @typedef {"file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code"} NoteType
  40. */
  41. /**
  42. * @typedef {Object} NotePathRecord
  43. * @property {boolean} isArchived
  44. * @property {boolean} isInHoistedSubTree
  45. * @property {Array&lt;string>} notePath
  46. * @property {boolean} isHidden
  47. */
  48. /**
  49. * Trilium's main entity, which can represent text note, image, code note, file attachment etc.
  50. *
  51. * @extends AbstractBeccaEntity
  52. */
  53. class BNote extends AbstractBeccaEntity {
  54. static get entityName() { return "notes"; }
  55. static get primaryKeyName() { return "noteId"; }
  56. static get hashedProperties() { return ["noteId", "title", "isProtected", "type", "mime", "blobId"]; }
  57. constructor(row) {
  58. super();
  59. if (!row) {
  60. return;
  61. }
  62. this.updateFromRow(row);
  63. this.init();
  64. }
  65. updateFromRow(row) {
  66. this.update([
  67. row.noteId,
  68. row.title,
  69. row.type,
  70. row.mime,
  71. row.isProtected,
  72. row.blobId,
  73. row.dateCreated,
  74. row.dateModified,
  75. row.utcDateCreated,
  76. row.utcDateModified
  77. ]);
  78. }
  79. update([noteId, title, type, mime, isProtected, blobId, dateCreated, dateModified, utcDateCreated, utcDateModified]) {
  80. // ------ Database persisted attributes ------
  81. /** @type {string} */
  82. this.noteId = noteId;
  83. /** @type {string} */
  84. this.title = title;
  85. /** @type {NoteType} */
  86. this.type = type;
  87. /** @type {string} */
  88. this.mime = mime;
  89. /** @type {boolean} */
  90. this.isProtected = !!isProtected;
  91. /** @type {string} */
  92. this.blobId = blobId;
  93. /** @type {string} */
  94. this.dateCreated = dateCreated || dateUtils.localNowDateTime();
  95. /** @type {string} */
  96. this.dateModified = dateModified;
  97. /** @type {string} */
  98. this.utcDateCreated = utcDateCreated || dateUtils.utcNowDateTime();
  99. /** @type {string} */
  100. this.utcDateModified = utcDateModified;
  101. /**
  102. * set during the deletion operation, before it is completed (removed from becca completely)
  103. * @type {boolean}
  104. */
  105. this.isBeingDeleted = false;
  106. // ------ Derived attributes ------
  107. /** @type {boolean} */
  108. this.isDecrypted = !this.noteId || !this.isProtected;
  109. this.decrypt();
  110. /** @type {string|null} */
  111. this.__flatTextCache = null;
  112. return this;
  113. }
  114. init() {
  115. /** @type {BBranch[]}
  116. * @private */
  117. this.parentBranches = [];
  118. /** @type {BNote[]}
  119. * @private */
  120. this.parents = [];
  121. /** @type {BNote[]}
  122. * @private */
  123. this.children = [];
  124. /** @type {BAttribute[]}
  125. * @private */
  126. this.ownedAttributes = [];
  127. /** @type {BAttribute[]|null}
  128. * @private */
  129. this.__attributeCache = null;
  130. /** @type {BAttribute[]|null}
  131. * @private */
  132. this.__inheritableAttributeCache = null;
  133. /** @type {BAttribute[]}
  134. * @private */
  135. this.targetRelations = [];
  136. this.becca.addNote(this.noteId, this);
  137. /** @type {BNote[]|null}
  138. * @private */
  139. this.__ancestorCache = null;
  140. // following attributes are filled during searching in the database
  141. /**
  142. * size of the content in bytes
  143. * @type {int|null}
  144. * @private
  145. */
  146. this.contentSize = null;
  147. /**
  148. * size of the note content, attachment contents in bytes
  149. * @type {int|null}
  150. * @private
  151. */
  152. this.contentAndAttachmentsSize = null;
  153. /**
  154. * size of the note content, attachment contents and revision contents in bytes
  155. * @type {int|null}
  156. * @private
  157. */
  158. this.contentAndAttachmentsAndRevisionsSize = null;
  159. /**
  160. * number of note revisions for this note
  161. * @type {int|null}
  162. * @private
  163. */
  164. this.revisionCount = null;
  165. }
  166. isContentAvailable() {
  167. return !this.noteId // new note which was not encrypted yet
  168. || !this.isProtected
  169. || protectedSessionService.isProtectedSessionAvailable()
  170. }
  171. getTitleOrProtected() {
  172. return this.isContentAvailable() ? this.title : '[protected]';
  173. }
  174. /** @returns {BBranch[]} */
  175. getParentBranches() {
  176. return this.parentBranches;
  177. }
  178. /**
  179. * Returns &lt;i>strong&lt;/i> (as opposed to &lt;i>weak&lt;/i>) parent branches. See isWeak for details.
  180. *
  181. * @returns {BBranch[]}
  182. */
  183. getStrongParentBranches() {
  184. return this.getParentBranches().filter(branch => !branch.isWeak);
  185. }
  186. /**
  187. * @returns {BBranch[]}
  188. * @deprecated use getParentBranches() instead
  189. */
  190. getBranches() {
  191. return this.parentBranches;
  192. }
  193. /** @returns {BNote[]} */
  194. getParentNotes() {
  195. return this.parents;
  196. }
  197. /** @returns {BNote[]} */
  198. getChildNotes() {
  199. return this.children;
  200. }
  201. /** @returns {boolean} */
  202. hasChildren() {
  203. return this.children &amp;&amp; this.children.length > 0;
  204. }
  205. /** @returns {BBranch[]} */
  206. getChildBranches() {
  207. return this.children.map(childNote => this.becca.getBranchFromChildAndParent(childNote.noteId, this.noteId));
  208. }
  209. /*
  210. * Note content has quite special handling - it's not a separate entity, but a lazily loaded
  211. * part of Note entity with its own sync. Reasons behind this hybrid design has been:
  212. *
  213. * - 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
  214. * - changes in the note metadata or title should not trigger note content sync (so we keep separate utcDateModified and entity changes records)
  215. * - 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)
  216. */
  217. /** @returns {string|Buffer} */
  218. getContent() {
  219. return this._getContent();
  220. }
  221. /**
  222. * @returns {*}
  223. * @throws Error in case of invalid JSON */
  224. getJsonContent() {
  225. const content = this.getContent();
  226. if (!content || !content.trim()) {
  227. return null;
  228. }
  229. return JSON.parse(content);
  230. }
  231. /** @returns {*|null} valid object or null if the content cannot be parsed as JSON */
  232. getJsonContentSafely() {
  233. try {
  234. return this.getJsonContent();
  235. }
  236. catch (e) {
  237. return null;
  238. }
  239. }
  240. /**
  241. * @param content
  242. * @param {object} [opts]
  243. * @param {object} [opts.forceSave=false] - will also save this BNote entity
  244. * @param {object} [opts.forceFrontendReload=false] - override frontend heuristics on when to reload, instruct to reload
  245. */
  246. setContent(content, opts) {
  247. this._setContent(content, opts);
  248. eventService.emit(eventService.NOTE_CONTENT_CHANGE, { entity: this });
  249. }
  250. setJsonContent(content) {
  251. this.setContent(JSON.stringify(content, null, '\t'));
  252. }
  253. get dateCreatedObj() {
  254. return this.dateCreated === null ? null : dayjs(this.dateCreated);
  255. }
  256. get utcDateCreatedObj() {
  257. return this.utcDateCreated === null ? null : dayjs.utc(this.utcDateCreated);
  258. }
  259. get dateModifiedObj() {
  260. return this.dateModified === null ? null : dayjs(this.dateModified);
  261. }
  262. get utcDateModifiedObj() {
  263. return this.utcDateModified === null ? null : dayjs.utc(this.utcDateModified);
  264. }
  265. /** @returns {boolean} true if this note is the root of the note tree. Root note has "root" noteId */
  266. isRoot() {
  267. return this.noteId === 'root';
  268. }
  269. /** @returns {boolean} true if this note is of application/json content type */
  270. isJson() {
  271. return this.mime === "application/json";
  272. }
  273. /** @returns {boolean} true if this note is JavaScript (code or attachment) */
  274. isJavaScript() {
  275. return (this.type === "code" || this.type === "file" || this.type === 'launcher')
  276. &amp;&amp; (this.mime.startsWith("application/javascript")
  277. || this.mime === "application/x-javascript"
  278. || this.mime === "text/javascript");
  279. }
  280. /** @returns {boolean} true if this note is HTML */
  281. isHtml() {
  282. return ["code", "file", "render"].includes(this.type)
  283. &amp;&amp; this.mime === "text/html";
  284. }
  285. /** @returns {boolean} true if this note is an image */
  286. isImage() {
  287. return this.type === 'image'
  288. || (this.type === 'file' &amp;&amp; this.mime?.startsWith('image/'));
  289. }
  290. /** @deprecated use hasStringContent() instead */
  291. isStringNote() {
  292. return this.hasStringContent();
  293. }
  294. /** @returns {boolean} true if the note has string content (not binary) */
  295. hasStringContent() {
  296. return utils.isStringNote(this.type, this.mime);
  297. }
  298. /** @returns {string|null} JS script environment - either "frontend" or "backend" */
  299. getScriptEnv() {
  300. if (this.isHtml() || (this.isJavaScript() &amp;&amp; this.mime.endsWith('env=frontend'))) {
  301. return "frontend";
  302. }
  303. if (this.type === 'render') {
  304. return "frontend";
  305. }
  306. if (this.isJavaScript() &amp;&amp; this.mime.endsWith('env=backend')) {
  307. return "backend";
  308. }
  309. return null;
  310. }
  311. /**
  312. * Beware that the method must not create a copy of the array, but actually returns its internal array
  313. * (for performance reasons)
  314. *
  315. * @param {string} [type] - (optional) attribute type to filter
  316. * @param {string} [name] - (optional) attribute name to filter
  317. * @returns {BAttribute[]} all note's attributes, including inherited ones
  318. */
  319. getAttributes(type, name) {
  320. this.__validateTypeName(type, name);
  321. this.__ensureAttributeCacheIsAvailable();
  322. if (type &amp;&amp; name) {
  323. return this.__attributeCache.filter(attr => attr.name === name &amp;&amp; attr.type === type);
  324. }
  325. else if (type) {
  326. return this.__attributeCache.filter(attr => attr.type === type);
  327. }
  328. else if (name) {
  329. return this.__attributeCache.filter(attr => attr.name === name);
  330. }
  331. else {
  332. return this.__attributeCache;
  333. }
  334. }
  335. /** @private */
  336. __ensureAttributeCacheIsAvailable() {
  337. if (!this.__attributeCache) {
  338. this.__getAttributes([]);
  339. }
  340. }
  341. /** @private */
  342. __getAttributes(path) {
  343. if (path.includes(this.noteId)) {
  344. return [];
  345. }
  346. if (!this.__attributeCache) {
  347. const parentAttributes = this.ownedAttributes.slice();
  348. const newPath = [...path, this.noteId];
  349. // inheritable attrs on root are typically not intended to be applied to hidden subtree #3537
  350. if (this.noteId !== 'root' &amp;&amp; this.noteId !== '_hidden') {
  351. for (const parentNote of this.parents) {
  352. parentAttributes.push(...parentNote.__getInheritableAttributes(newPath));
  353. }
  354. }
  355. const templateAttributes = [];
  356. for (const ownedAttr of parentAttributes) { // parentAttributes so we process also inherited templates
  357. if (ownedAttr.type === 'relation' &amp;&amp; ['template', 'inherit'].includes(ownedAttr.name)) {
  358. const templateNote = this.becca.notes[ownedAttr.value];
  359. if (templateNote) {
  360. templateAttributes.push(
  361. ...templateNote.__getAttributes(newPath)
  362. // template attr is used as a marker for templates, but it's not meant to be inherited
  363. .filter(attr => !(attr.type === 'label' &amp;&amp; (attr.name === 'template' || attr.name === 'workspacetemplate')))
  364. );
  365. }
  366. }
  367. }
  368. this.__attributeCache = [];
  369. const addedAttributeIds = new Set();
  370. for (const attr of parentAttributes.concat(templateAttributes)) {
  371. if (!addedAttributeIds.has(attr.attributeId)) {
  372. addedAttributeIds.add(attr.attributeId);
  373. this.__attributeCache.push(attr);
  374. }
  375. }
  376. this.__inheritableAttributeCache = [];
  377. for (const attr of this.__attributeCache) {
  378. if (attr.isInheritable) {
  379. this.__inheritableAttributeCache.push(attr);
  380. }
  381. }
  382. }
  383. return this.__attributeCache;
  384. }
  385. /**
  386. * @private
  387. * @returns {BAttribute[]}
  388. */
  389. __getInheritableAttributes(path) {
  390. if (path.includes(this.noteId)) {
  391. return [];
  392. }
  393. if (!this.__inheritableAttributeCache) {
  394. this.__getAttributes(path); // will refresh also this.__inheritableAttributeCache
  395. }
  396. return this.__inheritableAttributeCache;
  397. }
  398. __validateTypeName(type, name) {
  399. if (type &amp;&amp; type !== 'label' &amp;&amp; type !== 'relation') {
  400. throw new Error(`Unrecognized attribute type '${type}'. Only 'label' and 'relation' are possible values.`);
  401. }
  402. if (name) {
  403. const firstLetter = name.charAt(0);
  404. if (firstLetter === '#' || firstLetter === '~') {
  405. throw new Error(`Detect '#' or '~' in the attribute's name. In the API, attribute names should be set without these characters.`);
  406. }
  407. }
  408. }
  409. /**
  410. * @param type
  411. * @param name
  412. * @param [value]
  413. * @returns {boolean}
  414. */
  415. hasAttribute(type, name, value = null) {
  416. return !!this.getAttributes().find(attr =>
  417. attr.name === name
  418. &amp;&amp; (value === undefined || value === null || attr.value === value)
  419. &amp;&amp; attr.type === type
  420. );
  421. }
  422. getAttributeCaseInsensitive(type, name, value) {
  423. name = name.toLowerCase();
  424. value = value ? value.toLowerCase() : null;
  425. return this.getAttributes().find(
  426. attr => attr.name.toLowerCase() === name
  427. &amp;&amp; (!value || attr.value.toLowerCase() === value)
  428. &amp;&amp; attr.type === type);
  429. }
  430. getRelationTarget(name) {
  431. const relation = this.getAttributes().find(attr => attr.name === name &amp;&amp; attr.type === 'relation');
  432. return relation ? relation.targetNote : null;
  433. }
  434. /**
  435. * @param {string} name - label name
  436. * @param {string} [value] - label value
  437. * @returns {boolean} true if label exists (including inherited)
  438. */
  439. hasLabel(name, value) { return this.hasAttribute(LABEL, name, value); }
  440. /**
  441. * @param {string} name - label name
  442. * @returns {boolean} true if label exists (including inherited) and does not have "false" value.
  443. */
  444. isLabelTruthy(name) {
  445. const label = this.getLabel(name);
  446. if (!label) {
  447. return false;
  448. }
  449. return label &amp;&amp; label.value !== 'false';
  450. }
  451. /**
  452. * @param {string} name - label name
  453. * @param {string} [value] - label value
  454. * @returns {boolean} true if label exists (excluding inherited)
  455. */
  456. hasOwnedLabel(name, value) { return this.hasOwnedAttribute(LABEL, name, value); }
  457. /**
  458. * @param {string} name - relation name
  459. * @param {string} [value] - relation value
  460. * @returns {boolean} true if relation exists (including inherited)
  461. */
  462. hasRelation(name, value) { return this.hasAttribute(RELATION, name, value); }
  463. /**
  464. * @param {string} name - relation name
  465. * @param {string} [value] - relation value
  466. * @returns {boolean} true if relation exists (excluding inherited)
  467. */
  468. hasOwnedRelation(name, value) { return this.hasOwnedAttribute(RELATION, name, value); }
  469. /**
  470. * @param {string} name - label name
  471. * @returns {BAttribute|null} label if it exists, null otherwise
  472. */
  473. getLabel(name) { return this.getAttribute(LABEL, name); }
  474. /**
  475. * @param {string} name - label name
  476. * @returns {BAttribute|null} label if it exists, null otherwise
  477. */
  478. getOwnedLabel(name) { return this.getOwnedAttribute(LABEL, name); }
  479. /**
  480. * @param {string} name - relation name
  481. * @returns {BAttribute|null} relation if it exists, null otherwise
  482. */
  483. getRelation(name) { return this.getAttribute(RELATION, name); }
  484. /**
  485. * @param {string} name - relation name
  486. * @returns {BAttribute|null} relation if it exists, null otherwise
  487. */
  488. getOwnedRelation(name) { return this.getOwnedAttribute(RELATION, name); }
  489. /**
  490. * @param {string} name - label name
  491. * @returns {string|null} label value if label exists, null otherwise
  492. */
  493. getLabelValue(name) { return this.getAttributeValue(LABEL, name); }
  494. /**
  495. * @param {string} name - label name
  496. * @returns {string|null} label value if label exists, null otherwise
  497. */
  498. getOwnedLabelValue(name) { return this.getOwnedAttributeValue(LABEL, name); }
  499. /**
  500. * @param {string} name - relation name
  501. * @returns {string|null} relation value if relation exists, null otherwise
  502. */
  503. getRelationValue(name) { return this.getAttributeValue(RELATION, name); }
  504. /**
  505. * @param {string} name - relation name
  506. * @returns {string|null} relation value if relation exists, null otherwise
  507. */
  508. getOwnedRelationValue(name) { return this.getOwnedAttributeValue(RELATION, name); }
  509. /**
  510. * @param {string} type - attribute type (label, relation, etc.)
  511. * @param {string} name - attribute name
  512. * @param {string} [value] - attribute value
  513. * @returns {boolean} true if note has an attribute with given type and name (excluding inherited)
  514. */
  515. hasOwnedAttribute(type, name, value) {
  516. return !!this.getOwnedAttribute(type, name, value);
  517. }
  518. /**
  519. * @param {string} type - attribute type (label, relation, etc.)
  520. * @param {string} name - attribute name
  521. * @returns {BAttribute} attribute of the given type and name. If there are more such attributes, first is returned.
  522. * Returns null if there's no such attribute belonging to this note.
  523. */
  524. getAttribute(type, name) {
  525. const attributes = this.getAttributes();
  526. return attributes.find(attr => attr.name === name &amp;&amp; attr.type === type);
  527. }
  528. /**
  529. * @param {string} type - attribute type (label, relation, etc.)
  530. * @param {string} name - attribute name
  531. * @returns {string|null} attribute value of given type and name or null if no such attribute exists.
  532. */
  533. getAttributeValue(type, name) {
  534. const attr = this.getAttribute(type, name);
  535. return attr ? attr.value : null;
  536. }
  537. /**
  538. * @param {string} type - attribute type (label, relation, etc.)
  539. * @param {string} name - attribute name
  540. * @returns {string|null} attribute value of given type and name or null if no such attribute exists.
  541. */
  542. getOwnedAttributeValue(type, name) {
  543. const attr = this.getOwnedAttribute(type, name);
  544. return attr ? attr.value : null;
  545. }
  546. /**
  547. * @param {string} [name] - label name to filter
  548. * @returns {BAttribute[]} all note's labels (attributes with type label), including inherited ones
  549. */
  550. getLabels(name) {
  551. return this.getAttributes(LABEL, name);
  552. }
  553. /**
  554. * @param {string} [name] - label name to filter
  555. * @returns {string[]} all note's label values, including inherited ones
  556. */
  557. getLabelValues(name) {
  558. return this.getLabels(name).map(l => l.value);
  559. }
  560. /**
  561. * @param {string} [name] - label name to filter
  562. * @returns {BAttribute[]} all note's labels (attributes with type label), excluding inherited ones
  563. */
  564. getOwnedLabels(name) {
  565. return this.getOwnedAttributes(LABEL, name);
  566. }
  567. /**
  568. * @param {string} [name] - label name to filter
  569. * @returns {string[]} all note's label values, excluding inherited ones
  570. */
  571. getOwnedLabelValues(name) {
  572. return this.getOwnedAttributes(LABEL, name).map(l => l.value);
  573. }
  574. /**
  575. * @param {string} [name] - relation name to filter
  576. * @returns {BAttribute[]} all note's relations (attributes with type relation), including inherited ones
  577. */
  578. getRelations(name) {
  579. return this.getAttributes(RELATION, name);
  580. }
  581. /**
  582. * @param {string} [name] - relation name to filter
  583. * @returns {BAttribute[]} all note's relations (attributes with type relation), excluding inherited ones
  584. */
  585. getOwnedRelations(name) {
  586. return this.getOwnedAttributes(RELATION, name);
  587. }
  588. /**
  589. * Beware that the method must not create a copy of the array, but actually returns its internal array
  590. * (for performance reasons)
  591. *
  592. * @param {string|null} [type] - (optional) attribute type to filter
  593. * @param {string|null} [name] - (optional) attribute name to filter
  594. * @param {string|null} [value] - (optional) attribute value to filter
  595. * @returns {BAttribute[]} note's "owned" attributes - excluding inherited ones
  596. */
  597. getOwnedAttributes(type = null, name = null, value = null) {
  598. this.__validateTypeName(type, name);
  599. if (type &amp;&amp; name &amp;&amp; value !== undefined &amp;&amp; value !== null) {
  600. return this.ownedAttributes.filter(attr => attr.name === name &amp;&amp; attr.value === value &amp;&amp; attr.type === type);
  601. }
  602. else if (type &amp;&amp; name) {
  603. return this.ownedAttributes.filter(attr => attr.name === name &amp;&amp; attr.type === type);
  604. }
  605. else if (type) {
  606. return this.ownedAttributes.filter(attr => attr.type === type);
  607. }
  608. else if (name) {
  609. return this.ownedAttributes.filter(attr => attr.name === name);
  610. }
  611. else {
  612. return this.ownedAttributes;
  613. }
  614. }
  615. /**
  616. * @returns {BAttribute} attribute belonging to this specific note (excludes inherited attributes)
  617. *
  618. * This method can be significantly faster than the getAttribute()
  619. */
  620. getOwnedAttribute(type, name, value = null) {
  621. const attrs = this.getOwnedAttributes(type, name, value);
  622. return attrs.length > 0 ? attrs[0] : null;
  623. }
  624. get isArchived() {
  625. return this.hasAttribute('label', 'archived');
  626. }
  627. areAllNotePathsArchived() {
  628. // there's a slight difference between note being itself archived and all its note paths being archived
  629. // - note is archived when it itself has an archived label or inherits it
  630. // - note does not have or inherit archived label, but each note path contains a note with (non-inheritable)
  631. // archived label
  632. const bestNotePathRecord = this.getSortedNotePathRecords()[0];
  633. if (!bestNotePathRecord) {
  634. throw new Error(`No note path available for note '${this.noteId}'`);
  635. }
  636. return bestNotePathRecord.isArchived;
  637. }
  638. hasInheritableArchivedLabel() {
  639. for (const attr of this.getAttributes()) {
  640. if (attr.name === 'archived' &amp;&amp; attr.type === LABEL &amp;&amp; attr.isInheritable) {
  641. return true;
  642. }
  643. }
  644. return false;
  645. }
  646. // will sort the parents so that the non-archived are first and archived at the end
  647. // this is done so that the non-archived paths are always explored as first when looking for note path
  648. sortParents() {
  649. this.parentBranches.sort((a, b) => {
  650. if (a.parentNote?.isArchived) {
  651. return 1;
  652. } else if (a.parentNote?.isHiddenCompletely()) {
  653. return 1;
  654. } else {
  655. return 0;
  656. }
  657. });
  658. this.parents = this.parentBranches
  659. .map(branch => branch.parentNote)
  660. .filter(note => !!note);
  661. }
  662. sortChildren() {
  663. if (this.children.length === 0) {
  664. return;
  665. }
  666. const becca = this.becca;
  667. this.children.sort((a, b) => {
  668. const aBranch = becca.getBranchFromChildAndParent(a.noteId, this.noteId);
  669. const bBranch = becca.getBranchFromChildAndParent(b.noteId, this.noteId);
  670. return (aBranch?.notePosition - bBranch?.notePosition) || 0;
  671. });
  672. }
  673. /**
  674. * This is used for:
  675. * - fast searching
  676. * - note similarity evaluation
  677. *
  678. * @returns {string} - returns flattened textual representation of note, prefixes and attributes
  679. */
  680. getFlatText() {
  681. if (!this.__flatTextCache) {
  682. this.__flatTextCache = `${this.noteId} ${this.type} ${this.mime} `;
  683. for (const branch of this.parentBranches) {
  684. if (branch.prefix) {
  685. this.__flatTextCache += `${branch.prefix} `;
  686. }
  687. }
  688. this.__flatTextCache += `${this.title} `;
  689. for (const attr of this.getAttributes()) {
  690. // it's best to use space as separator since spaces are filtered from the search string by the tokenization into words
  691. this.__flatTextCache += `${attr.type === 'label' ? '#' : '~'}${attr.name}`;
  692. if (attr.value) {
  693. this.__flatTextCache += `=${attr.value}`;
  694. }
  695. this.__flatTextCache += ' ';
  696. }
  697. this.__flatTextCache = utils.normalize(this.__flatTextCache);
  698. }
  699. return this.__flatTextCache;
  700. }
  701. invalidateThisCache() {
  702. this.__flatTextCache = null;
  703. this.__attributeCache = null;
  704. this.__inheritableAttributeCache = null;
  705. this.__ancestorCache = null;
  706. }
  707. invalidateSubTree(path = []) {
  708. if (path.includes(this.noteId)) {
  709. return;
  710. }
  711. this.invalidateThisCache();
  712. if (this.children.length || this.targetRelations.length) {
  713. path = [...path, this.noteId];
  714. }
  715. for (const childNote of this.children) {
  716. childNote.invalidateSubTree(path);
  717. }
  718. for (const targetRelation of this.targetRelations) {
  719. if (targetRelation.name === 'template' || targetRelation.name === 'inherit') {
  720. const note = targetRelation.note;
  721. if (note) {
  722. note.invalidateSubTree(path);
  723. }
  724. }
  725. }
  726. }
  727. getRelationDefinitions() {
  728. return this.getLabels()
  729. .filter(l => l.name.startsWith("relation:"));
  730. }
  731. getLabelDefinitions() {
  732. return this.getLabels()
  733. .filter(l => l.name.startsWith("relation:"));
  734. }
  735. isInherited() {
  736. return !!this.targetRelations.find(rel => rel.name === 'template' || rel.name === 'inherit');
  737. }
  738. /** @returns {BNote[]} */
  739. getSubtreeNotesIncludingTemplated() {
  740. const set = new Set();
  741. function inner(note) {
  742. // _hidden is not counted as subtree for the purpose of inheritance
  743. if (set.has(note) || note.noteId === '_hidden') {
  744. return;
  745. }
  746. set.add(note);
  747. for (const childNote of note.children) {
  748. inner(childNote);
  749. }
  750. for (const targetRelation of note.targetRelations) {
  751. if (targetRelation.name === 'template' || targetRelation.name === 'inherit') {
  752. const targetNote = targetRelation.note;
  753. if (targetNote) {
  754. inner(targetNote);
  755. }
  756. }
  757. }
  758. }
  759. inner(this);
  760. return Array.from(set);
  761. }
  762. /** @returns {BNote[]} */
  763. getSearchResultNotes() {
  764. if (this.type !== 'search') {
  765. return [];
  766. }
  767. try {
  768. const searchService = require("../../services/search/services/search");
  769. const {searchResultNoteIds} = searchService.searchFromNote(this);
  770. const becca = this.becca;
  771. return searchResultNoteIds
  772. .map(resultNoteId => becca.notes[resultNoteId])
  773. .filter(note => !!note);
  774. }
  775. catch (e) {
  776. log.error(`Could not resolve search note ${this.noteId}: ${e.message}`);
  777. return [];
  778. }
  779. }
  780. /**
  781. * @returns {{notes: BNote[], relationships: Array.&lt;{parentNoteId: string, childNoteId: string}>}}
  782. */
  783. getSubtree({includeArchived = true, includeHidden = false, resolveSearch = false} = {}) {
  784. const noteSet = new Set();
  785. const relationships = []; // list of tuples parentNoteId -> childNoteId
  786. function resolveSearchNote(searchNote) {
  787. try {
  788. for (const resultNote of searchNote.getSearchResultNotes()) {
  789. addSubtreeNotesInner(resultNote, searchNote);
  790. }
  791. }
  792. catch (e) {
  793. log.error(`Could not resolve search note ${searchNote?.noteId}: ${e.message}`);
  794. }
  795. }
  796. function addSubtreeNotesInner(note, parentNote = null) {
  797. if (note.noteId === '_hidden' &amp;&amp; !includeHidden) {
  798. return;
  799. }
  800. if (parentNote) {
  801. // this needs to happen first before noteSet check to include all clone relationships
  802. relationships.push({
  803. parentNoteId: parentNote.noteId,
  804. childNoteId: note.noteId
  805. });
  806. }
  807. if (noteSet.has(note)) {
  808. return;
  809. }
  810. if (!includeArchived &amp;&amp; note.isArchived) {
  811. return;
  812. }
  813. noteSet.add(note);
  814. if (note.type === 'search') {
  815. if (resolveSearch) {
  816. resolveSearchNote(note);
  817. }
  818. }
  819. else {
  820. for (const childNote of note.children) {
  821. addSubtreeNotesInner(childNote, note);
  822. }
  823. }
  824. }
  825. addSubtreeNotesInner(this);
  826. return {
  827. notes: Array.from(noteSet),
  828. relationships
  829. };
  830. }
  831. /** @returns {string[]} - includes the subtree root note as well */
  832. getSubtreeNoteIds({includeArchived = true, includeHidden = false, resolveSearch = false} = {}) {
  833. return this.getSubtree({includeArchived, includeHidden, resolveSearch})
  834. .notes
  835. .map(note => note.noteId);
  836. }
  837. /** @deprecated use getSubtreeNoteIds() instead */
  838. getDescendantNoteIds() {
  839. return this.getSubtreeNoteIds();
  840. }
  841. get parentCount() {
  842. return this.parents.length;
  843. }
  844. get childrenCount() {
  845. return this.children.length;
  846. }
  847. get labelCount() {
  848. return this.getAttributes().filter(attr => attr.type === 'label').length;
  849. }
  850. get ownedLabelCount() {
  851. return this.ownedAttributes.filter(attr => attr.type === 'label').length;
  852. }
  853. get relationCount() {
  854. return this.getAttributes().filter(attr => attr.type === 'relation' &amp;&amp; !attr.isAutoLink()).length;
  855. }
  856. get relationCountIncludingLinks() {
  857. return this.getAttributes().filter(attr => attr.type === 'relation').length;
  858. }
  859. get ownedRelationCount() {
  860. return this.ownedAttributes.filter(attr => attr.type === 'relation' &amp;&amp; !attr.isAutoLink()).length;
  861. }
  862. get ownedRelationCountIncludingLinks() {
  863. return this.ownedAttributes.filter(attr => attr.type === 'relation').length;
  864. }
  865. get targetRelationCount() {
  866. return this.targetRelations.filter(attr => !attr.isAutoLink()).length;
  867. }
  868. get targetRelationCountIncludingLinks() {
  869. return this.targetRelations.length;
  870. }
  871. get attributeCount() {
  872. return this.getAttributes().length;
  873. }
  874. get ownedAttributeCount() {
  875. return this.getOwnedAttributes().length;
  876. }
  877. /** @returns {BNote[]} */
  878. getAncestors() {
  879. if (!this.__ancestorCache) {
  880. const noteIds = new Set();
  881. this.__ancestorCache = [];
  882. for (const parent of this.parents) {
  883. if (noteIds.has(parent.noteId)) {
  884. continue;
  885. }
  886. this.__ancestorCache.push(parent);
  887. noteIds.add(parent.noteId);
  888. for (const ancestorNote of parent.getAncestors()) {
  889. if (!noteIds.has(ancestorNote.noteId)) {
  890. this.__ancestorCache.push(ancestorNote);
  891. noteIds.add(ancestorNote.noteId);
  892. }
  893. }
  894. }
  895. }
  896. return this.__ancestorCache;
  897. }
  898. /** @returns {string[]} */
  899. getAncestorNoteIds() {
  900. return this.getAncestors().map(note => note.noteId);
  901. }
  902. /** @returns {boolean} */
  903. hasAncestor(ancestorNoteId) {
  904. for (const ancestorNote of this.getAncestors()) {
  905. if (ancestorNote.noteId === ancestorNoteId) {
  906. return true;
  907. }
  908. }
  909. return false;
  910. }
  911. isInHiddenSubtree() {
  912. return this.noteId === '_hidden' || this.hasAncestor('_hidden');
  913. }
  914. /** @returns {BAttribute[]} */
  915. getTargetRelations() {
  916. return this.targetRelations;
  917. }
  918. /** @returns {BNote[]} - returns only notes which are templated, does not include their subtrees
  919. * in effect returns notes which are influenced by note's non-inheritable attributes */
  920. getInheritingNotes() {
  921. const arr = [this];
  922. for (const targetRelation of this.targetRelations) {
  923. if (targetRelation.name === 'template' || targetRelation.name === 'inherit') {
  924. const note = targetRelation.note;
  925. if (note) {
  926. arr.push(note);
  927. }
  928. }
  929. }
  930. return arr;
  931. }
  932. getDistanceToAncestor(ancestorNoteId) {
  933. if (this.noteId === ancestorNoteId) {
  934. return 0;
  935. }
  936. let minDistance = 999999;
  937. for (const parent of this.parents) {
  938. minDistance = Math.min(minDistance, parent.getDistanceToAncestor(ancestorNoteId) + 1);
  939. }
  940. return minDistance;
  941. }
  942. /** @returns {BRevision[]} */
  943. getRevisions() {
  944. return sql.getRows("SELECT * FROM revisions WHERE noteId = ?", [this.noteId])
  945. .map(row => new BRevision(row));
  946. }
  947. /** @returns {BAttachment[]} */
  948. getAttachments(opts = {}) {
  949. opts.includeContentLength = !!opts.includeContentLength;
  950. // from testing, it looks like calculating length does not make a difference in performance even on large-ish DB
  951. // given that we're always fetching attachments only for a specific note, we might just do it always
  952. const query = opts.includeContentLength
  953. ? `SELECT attachments.*, LENGTH(blobs.content) AS contentLength
  954. FROM attachments
  955. JOIN blobs USING (blobId)
  956. WHERE ownerId = ? AND isDeleted = 0
  957. ORDER BY position`
  958. : `SELECT * FROM attachments WHERE ownerId = ? AND isDeleted = 0 ORDER BY position`;
  959. return sql.getRows(query, [this.noteId])
  960. .map(row => new BAttachment(row));
  961. }
  962. /** @returns {BAttachment|null} */
  963. getAttachmentById(attachmentId, opts = {}) {
  964. opts.includeContentLength = !!opts.includeContentLength;
  965. const query = opts.includeContentLength
  966. ? `SELECT attachments.*, LENGTH(blobs.content) AS contentLength
  967. FROM attachments
  968. JOIN blobs USING (blobId)
  969. WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`
  970. : `SELECT * FROM attachments WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`;
  971. return sql.getRows(query, [this.noteId, attachmentId])
  972. .map(row => new BAttachment(row))[0];
  973. }
  974. /** @returns {BAttachment[]} */
  975. getAttachmentsByRole(role) {
  976. return sql.getRows(`
  977. SELECT attachments.*
  978. FROM attachments
  979. WHERE ownerId = ?
  980. AND role = ?
  981. AND isDeleted = 0
  982. ORDER BY position`, [this.noteId, role])
  983. .map(row => new BAttachment(row));
  984. }
  985. /** @returns {BAttachment} */
  986. getAttachmentByTitle(title) {
  987. // cannot use SQL to filter by title since it can be encrypted
  988. return this.getAttachments().filter(attachment => attachment.title === title)[0];
  989. }
  990. /**
  991. * Gives all possible note paths leading to this note. Paths containing search note are ignored (could form cycles)
  992. *
  993. * @returns {string[][]} - array of notePaths (each represented by array of noteIds constituting the particular note path)
  994. */
  995. getAllNotePaths() {
  996. if (this.noteId === 'root') {
  997. return [['root']];
  998. }
  999. const parentNotes = this.getParentNotes();
  1000. const notePaths = parentNotes.length === 1
  1001. ? parentNotes[0].getAllNotePaths() // optimization for the most common case
  1002. : parentNotes.flatMap(parentNote => parentNote.getAllNotePaths());
  1003. for (const notePath of notePaths) {
  1004. notePath.push(this.noteId);
  1005. }
  1006. return notePaths;
  1007. }
  1008. /**
  1009. * @param {string} [hoistedNoteId='root']
  1010. * @return {Array&lt;NotePathRecord>}
  1011. */
  1012. getSortedNotePathRecords(hoistedNoteId = 'root') {
  1013. const isHoistedRoot = hoistedNoteId === 'root';
  1014. const notePaths = this.getAllNotePaths().map(path => ({
  1015. notePath: path,
  1016. isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId),
  1017. isArchived: path.some(noteId => this.becca.notes[noteId].isArchived),
  1018. isHidden: path.includes('_hidden')
  1019. }));
  1020. notePaths.sort((a, b) => {
  1021. if (a.isInHoistedSubTree !== b.isInHoistedSubTree) {
  1022. return a.isInHoistedSubTree ? -1 : 1;
  1023. } else if (a.isArchived !== b.isArchived) {
  1024. return a.isArchived ? 1 : -1;
  1025. } else if (a.isHidden !== b.isHidden) {
  1026. return a.isHidden ? 1 : -1;
  1027. } else {
  1028. return a.notePath.length - b.notePath.length;
  1029. }
  1030. });
  1031. return notePaths;
  1032. }
  1033. /**
  1034. * Returns a note path considered to be the "best"
  1035. *
  1036. * @param {string} [hoistedNoteId='root']
  1037. * @return {string[]} array of noteIds constituting the particular note path
  1038. */
  1039. getBestNotePath(hoistedNoteId = 'root') {
  1040. return this.getSortedNotePathRecords(hoistedNoteId)[0]?.notePath;
  1041. }
  1042. /**
  1043. * Returns a note path considered to be the "best"
  1044. *
  1045. * @param {string} [hoistedNoteId='root']
  1046. * @return {string} serialized note path (e.g. 'root/a1h315/js725h')
  1047. */
  1048. getBestNotePathString(hoistedNoteId = 'root') {
  1049. const notePath = this.getBestNotePath(hoistedNoteId);
  1050. return notePath?.join("/");
  1051. }
  1052. /**
  1053. * @return boolean - true if there's no non-hidden path, note is not cloned to the visible tree
  1054. */
  1055. isHiddenCompletely() {
  1056. if (this.noteId === 'root') {
  1057. return false;
  1058. }
  1059. for (const parentNote of this.parents) {
  1060. if (parentNote.noteId === 'root') {
  1061. return false;
  1062. } else if (parentNote.noteId === '_hidden') {
  1063. continue;
  1064. } else if (!parentNote.isHiddenCompletely()) {
  1065. return false;
  1066. }
  1067. }
  1068. return true;
  1069. }
  1070. /**
  1071. * @param ancestorNoteId
  1072. * @returns {boolean} - true if ancestorNoteId occurs in at least one of the note's paths
  1073. */
  1074. isDescendantOfNote(ancestorNoteId) {
  1075. const notePaths = this.getAllNotePaths();
  1076. return notePaths.some(path => path.includes(ancestorNoteId));
  1077. }
  1078. /**
  1079. * Update's given attribute's value or creates it if it doesn't exist
  1080. *
  1081. * @param {string} type - attribute type (label, relation, etc.)
  1082. * @param {string} name - attribute name
  1083. * @param {string} [value] - attribute value (optional)
  1084. */
  1085. setAttribute(type, name, value) {
  1086. const attributes = this.getOwnedAttributes();
  1087. const attr = attributes.find(attr => attr.type === type &amp;&amp; attr.name === name);
  1088. value = value?.toString() || "";
  1089. if (attr) {
  1090. if (attr.value !== value) {
  1091. attr.value = value;
  1092. attr.save();
  1093. }
  1094. }
  1095. else {
  1096. const BAttribute = require("./battribute");
  1097. new BAttribute({
  1098. noteId: this.noteId,
  1099. type: type,
  1100. name: name,
  1101. value: value
  1102. }).save();
  1103. }
  1104. }
  1105. /**
  1106. * Removes given attribute name-value pair if it exists.
  1107. *
  1108. * @param {string} type - attribute type (label, relation, etc.)
  1109. * @param {string} name - attribute name
  1110. * @param {string} [value] - attribute value (optional)
  1111. */
  1112. removeAttribute(type, name, value) {
  1113. const attributes = this.getOwnedAttributes();
  1114. for (const attribute of attributes) {
  1115. if (attribute.type === type &amp;&amp; attribute.name === name &amp;&amp; (value === undefined || value === attribute.value)) {
  1116. attribute.markAsDeleted();
  1117. }
  1118. }
  1119. }
  1120. /**
  1121. * Adds a new attribute to this note. The attribute is saved and returned.
  1122. * See addLabel, addRelation for more specific methods.
  1123. *
  1124. * @param {string} type - attribute type (label / relation)
  1125. * @param {string} name - name of the attribute, not including the leading ~/#
  1126. * @param {string} [value] - value of the attribute - text for labels, target note ID for relations; optional.
  1127. * @param {boolean} [isInheritable=false]
  1128. * @param {int|null} [position]
  1129. * @returns {BAttribute}
  1130. */
  1131. addAttribute(type, name, value = "", isInheritable = false, position = null) {
  1132. const BAttribute = require("./battribute");
  1133. return new BAttribute({
  1134. noteId: this.noteId,
  1135. type: type,
  1136. name: name,
  1137. value: value,
  1138. isInheritable: isInheritable,
  1139. position: position
  1140. }).save();
  1141. }
  1142. /**
  1143. * Adds a new label to this note. The label attribute is saved and returned.
  1144. *
  1145. * @param {string} name - name of the label, not including the leading #
  1146. * @param {string} [value] - text value of the label; optional
  1147. * @param {boolean} [isInheritable=false]
  1148. * @returns {BAttribute}
  1149. */
  1150. addLabel(name, value = "", isInheritable = false) {
  1151. return this.addAttribute(LABEL, name, value, isInheritable);
  1152. }
  1153. /**
  1154. * Adds a new relation to this note. The relation attribute is saved and
  1155. * returned.
  1156. *
  1157. * @param {string} name - name of the relation, not including the leading ~
  1158. * @param {string} targetNoteId
  1159. * @param {boolean} [isInheritable=false]
  1160. * @returns {BAttribute}
  1161. */
  1162. addRelation(name, targetNoteId, isInheritable = false) {
  1163. return this.addAttribute(RELATION, name, targetNoteId, isInheritable);
  1164. }
  1165. /**
  1166. * Based on enabled, the attribute is either set or removed.
  1167. *
  1168. * @param {string} type - attribute type ('relation', 'label' etc.)
  1169. * @param {boolean} enabled - toggle On or Off
  1170. * @param {string} name - attribute name
  1171. * @param {string} [value] - attribute value (optional)
  1172. */
  1173. toggleAttribute(type, enabled, name, value) {
  1174. if (enabled) {
  1175. this.setAttribute(type, name, value);
  1176. }
  1177. else {
  1178. this.removeAttribute(type, name, value);
  1179. }
  1180. }
  1181. /**
  1182. * Based on enabled, label is either set or removed.
  1183. *
  1184. * @param {boolean} enabled - toggle On or Off
  1185. * @param {string} name - label name
  1186. * @param {string} [value] - label value (optional)
  1187. */
  1188. toggleLabel(enabled, name, value) { return this.toggleAttribute(LABEL, enabled, name, value); }
  1189. /**
  1190. * Based on enabled, relation is either set or removed.
  1191. *
  1192. * @param {boolean} enabled - toggle On or Off
  1193. * @param {string} name - relation name
  1194. * @param {string} [value] - relation value (noteId)
  1195. */
  1196. toggleRelation(enabled, name, value) { return this.toggleAttribute(RELATION, enabled, name, value); }
  1197. /**
  1198. * Update's given label's value or creates it if it doesn't exist
  1199. *
  1200. * @param {string} name - label name
  1201. * @param {string} [value] - label value
  1202. */
  1203. setLabel(name, value) { return this.setAttribute(LABEL, name, value); }
  1204. /**
  1205. * Update's given relation's value or creates it if it doesn't exist
  1206. *
  1207. * @param {string} name - relation name
  1208. * @param {string} value - relation value (noteId)
  1209. */
  1210. setRelation(name, value) { return this.setAttribute(RELATION, name, value); }
  1211. /**
  1212. * Remove label name-value pair, if it exists.
  1213. *
  1214. * @param {string} name - label name
  1215. * @param {string} [value] - label value
  1216. */
  1217. removeLabel(name, value) { return this.removeAttribute(LABEL, name, value); }
  1218. /**
  1219. * Remove the relation name-value pair, if it exists.
  1220. *
  1221. * @param {string} name - relation name
  1222. * @param {string} [value] - relation value (noteId)
  1223. */
  1224. removeRelation(name, value) { return this.removeAttribute(RELATION, name, value); }
  1225. searchNotesInSubtree(searchString) {
  1226. const searchService = require("../../services/search/services/search");
  1227. return searchService.searchNotes(searchString);
  1228. }
  1229. searchNoteInSubtree(searchString) {
  1230. return this.searchNotesInSubtree(searchString)[0];
  1231. }
  1232. /**
  1233. * @param parentNoteId
  1234. * @returns {{success: boolean, message: string, branchId: string, notePath: string}}
  1235. */
  1236. cloneTo(parentNoteId) {
  1237. const cloningService = require("../../services/cloning");
  1238. const branch = this.becca.getNote(parentNoteId).getParentBranches()[0];
  1239. return cloningService.cloneNoteToBranch(this.noteId, branch.branchId);
  1240. }
  1241. isEligibleForConversionToAttachment(opts = {autoConversion: false}) {
  1242. if (this.type !== 'image' || !this.isContentAvailable() || this.hasChildren() || this.getParentBranches().length !== 1) {
  1243. return false;
  1244. }
  1245. const targetRelations = this.getTargetRelations().filter(relation => relation.name === 'imageLink');
  1246. if (opts.autoConversion &amp;&amp; targetRelations.length === 0) {
  1247. return false;
  1248. } else if (targetRelations.length > 1) {
  1249. return false;
  1250. }
  1251. const parentNote = this.getParentNotes()[0]; // at this point note can have only one parent
  1252. const referencingNote = targetRelations[0]?.getNote();
  1253. if (referencingNote &amp;&amp; parentNote !== referencingNote) {
  1254. return false;
  1255. } else if (parentNote.type !== 'text' || !parentNote.isContentAvailable()) {
  1256. return false;
  1257. }
  1258. return true;
  1259. }
  1260. /**
  1261. * Some notes are eligible for conversion into an attachment of its parent, note must have these properties:
  1262. * - it has exactly one target relation
  1263. * - it has a relation from its parent note
  1264. * - it has no children
  1265. * - it has no clones
  1266. * - the parent is of type text
  1267. * - both notes are either unprotected or user is in protected session
  1268. *
  1269. * Currently, works only for image notes.
  1270. *
  1271. * In the future, this functionality might get more generic and some of the requirements relaxed.
  1272. *
  1273. * @params {Object} [opts]
  1274. * @params {bolean} [opts.autoConversion=false} if true, the action is not triggered by user, but e.g. by migration,
  1275. * and only perfect candidates will be migrated
  1276. *
  1277. * @returns {BAttachment|null} - null if note is not eligible for conversion
  1278. */
  1279. convertToParentAttachment(opts = {autoConversion: false}) {
  1280. if (!this.isEligibleForConversionToAttachment(opts)) {
  1281. return null;
  1282. }
  1283. const content = this.getContent();
  1284. const parentNote = this.getParentNotes()[0];
  1285. const attachment = parentNote.saveAttachment({
  1286. role: 'image',
  1287. mime: this.mime,
  1288. title: this.title,
  1289. content: content
  1290. });
  1291. let parentContent = parentNote.getContent();
  1292. const oldNoteUrl = `api/images/${this.noteId}/`;
  1293. const newAttachmentUrl = `api/attachments/${attachment.attachmentId}/image/`;
  1294. const fixedContent = utils.replaceAll(parentContent, oldNoteUrl, newAttachmentUrl);
  1295. parentNote.setContent(fixedContent);
  1296. const noteService = require("../../services/notes");
  1297. noteService.asyncPostProcessContent(parentNote, fixedContent); // to mark an unused attachment for deletion
  1298. this.deleteNote();
  1299. return attachment;
  1300. }
  1301. /**
  1302. * (Soft) delete a note and all its descendants.
  1303. *
  1304. * @param {string} [deleteId=null] - optional delete identified
  1305. * @param {TaskContext} [taskContext]
  1306. */
  1307. deleteNote(deleteId = null, taskContext = null) {
  1308. if (this.isDeleted) {
  1309. return;
  1310. }
  1311. if (!deleteId) {
  1312. deleteId = utils.randomString(10);
  1313. }
  1314. if (!taskContext) {
  1315. taskContext = new TaskContext('no-progress-reporting');
  1316. }
  1317. // needs to be run before branches and attributes are deleted and thus attached relations disappear
  1318. const handlers = require("../../services/handlers");
  1319. handlers.runAttachedRelations(this, 'runOnNoteDeletion', this);
  1320. taskContext.noteDeletionHandlerTriggered = true;
  1321. for (const branch of this.getParentBranches()) {
  1322. branch.deleteBranch(deleteId, taskContext);
  1323. }
  1324. }
  1325. decrypt() {
  1326. if (this.isProtected &amp;&amp; !this.isDecrypted &amp;&amp; protectedSessionService.isProtectedSessionAvailable()) {
  1327. try {
  1328. this.title = protectedSessionService.decryptString(this.title);
  1329. this.__flatTextCache = null;
  1330. this.isDecrypted = true;
  1331. }
  1332. catch (e) {
  1333. log.error(`Could not decrypt note ${this.noteId}: ${e.message} ${e.stack}`);
  1334. }
  1335. }
  1336. }
  1337. isLaunchBarConfig() {
  1338. return this.type === 'launcher' || ['_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(this.noteId);
  1339. }
  1340. isOptions() {
  1341. return this.noteId.startsWith("_options");
  1342. }
  1343. get isDeleted() {
  1344. // isBeingDeleted is relevant only in the transition period when the deletion process has begun, but not yet
  1345. // finished (note is still in becca)
  1346. return !(this.noteId in this.becca.notes) || this.isBeingDeleted;
  1347. }
  1348. /**
  1349. * @returns {BRevision|null}
  1350. */
  1351. saveRevision() {
  1352. return sql.transactional(() => {
  1353. let noteContent = this.getContent();
  1354. const revision = new BRevision({
  1355. noteId: this.noteId,
  1356. // title and text should be decrypted now
  1357. title: this.title,
  1358. type: this.type,
  1359. mime: this.mime,
  1360. isProtected: this.isProtected,
  1361. utcDateLastEdited: this.utcDateModified,
  1362. utcDateCreated: dateUtils.utcNowDateTime(),
  1363. utcDateModified: dateUtils.utcNowDateTime(),
  1364. dateLastEdited: this.dateModified,
  1365. dateCreated: dateUtils.localNowDateTime()
  1366. }, true);
  1367. revision.save(); // to generate revisionId, which is then used to save attachments
  1368. for (const noteAttachment of this.getAttachments()) {
  1369. const revisionAttachment = noteAttachment.copy();
  1370. revisionAttachment.ownerId = revision.revisionId;
  1371. revisionAttachment.setContent(noteAttachment.getContent(), {forceSave: true});
  1372. if (this.type === 'text') {
  1373. // content is rewritten to point to the revision attachments
  1374. noteContent = noteContent.replaceAll(`attachments/${noteAttachment.attachmentId}`,
  1375. `attachments/${revisionAttachment.attachmentId}`);
  1376. noteContent = noteContent.replaceAll(new RegExp(`href="[^"]*attachmentId=${noteAttachment.attachmentId}[^"]*"`, 'gi'),
  1377. `href="api/attachments/${revisionAttachment.attachmentId}/download"`);
  1378. }
  1379. }
  1380. revision.setContent(noteContent);
  1381. return revision;
  1382. });
  1383. }
  1384. /**
  1385. * @param {string} matchBy - choose by which property we detect if to update an existing attachment.
  1386. * Supported values are either 'attachmentId' (default) or 'title'
  1387. * @returns {BAttachment}
  1388. */
  1389. saveAttachment({attachmentId, role, mime, title, content, position}, matchBy = 'attachmentId') {
  1390. if (!['attachmentId', 'title'].includes(matchBy)) {
  1391. throw new Error(`Unsupported value '${matchBy}' for matchBy param, has to be either 'attachmentId' or 'title'.`);
  1392. }
  1393. let attachment;
  1394. if (matchBy === 'title') {
  1395. attachment = this.getAttachmentByTitle(title);
  1396. } else if (matchBy === 'attachmentId' &amp;&amp; attachmentId) {
  1397. attachment = this.becca.getAttachmentOrThrow(attachmentId);
  1398. }
  1399. attachment = attachment || new BAttachment({
  1400. ownerId: this.noteId,
  1401. title,
  1402. role,
  1403. mime,
  1404. isProtected: this.isProtected,
  1405. position
  1406. });
  1407. content = content || "";
  1408. attachment.setContent(content, {forceSave: true});
  1409. return attachment;
  1410. }
  1411. getFileName() {
  1412. return utils.formatDownloadTitle(this.title, this.type, this.mime);
  1413. }
  1414. beforeSaving() {
  1415. super.beforeSaving();
  1416. this.becca.addNote(this.noteId, this);
  1417. this.dateModified = dateUtils.localNowDateTime();
  1418. this.utcDateModified = dateUtils.utcNowDateTime();
  1419. }
  1420. getPojo() {
  1421. return {
  1422. noteId: this.noteId,
  1423. title: this.title,
  1424. isProtected: this.isProtected,
  1425. type: this.type,
  1426. mime: this.mime,
  1427. blobId: this.blobId,
  1428. isDeleted: false,
  1429. dateCreated: this.dateCreated,
  1430. dateModified: this.dateModified,
  1431. utcDateCreated: this.utcDateCreated,
  1432. utcDateModified: this.utcDateModified
  1433. };
  1434. }
  1435. getPojoToSave() {
  1436. const pojo = this.getPojo();
  1437. if (pojo.isProtected) {
  1438. if (this.isDecrypted) {
  1439. pojo.title = protectedSessionService.encrypt(pojo.title);
  1440. }
  1441. else {
  1442. // updating protected note outside of protected session means we will keep original ciphertexts
  1443. delete pojo.title;
  1444. }
  1445. }
  1446. return pojo;
  1447. }
  1448. }
  1449. module.exports = BNote;
  1450. </code></pre>
  1451. </article>
  1452. </section>
  1453. </div>
  1454. <nav>
  1455. <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>
  1456. </nav>
  1457. <br class="clear">
  1458. <footer>
  1459. Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.2</a>
  1460. </footer>
  1461. <script> prettyPrint(); </script>
  1462. <script src="scripts/linenumber.js"> </script>
  1463. </body>
  1464. </html>