becca_entities_note.js.html 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <title>JSDoc: Source: becca/entities/note.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/note.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 entityChangesService = require('../../services/entity_changes');
  26. const AbstractEntity = require("./abstract_entity");
  27. const NoteRevision = require("./note_revision");
  28. const LABEL = 'label';
  29. const RELATION = 'relation';
  30. /**
  31. * Trilium's main entity which can represent text note, image, code note, file attachment etc.
  32. */
  33. class Note extends AbstractEntity {
  34. static get entityName() { return "notes"; }
  35. static get primaryKeyName() { return "noteId"; }
  36. static get hashedProperties() { return ["noteId", "title", "isProtected", "type", "mime"]; }
  37. constructor(row) {
  38. super();
  39. if (!row) {
  40. return;
  41. }
  42. this.updateFromRow(row);
  43. this.init();
  44. }
  45. updateFromRow(row) {
  46. this.update([
  47. row.noteId,
  48. row.title,
  49. row.type,
  50. row.mime,
  51. row.isProtected,
  52. row.dateCreated,
  53. row.dateModified,
  54. row.utcDateCreated,
  55. row.utcDateModified
  56. ]);
  57. }
  58. update([noteId, title, type, mime, isProtected, dateCreated, dateModified, utcDateCreated, utcDateModified]) {
  59. // ------ Database persisted attributes ------
  60. /** @type {string} */
  61. this.noteId = noteId;
  62. /** @type {string} */
  63. this.title = title;
  64. /** @type {boolean} */
  65. this.isProtected = !!isProtected;
  66. /** @type {string} */
  67. this.type = type;
  68. /** @type {string} */
  69. this.mime = mime;
  70. /** @type {string} */
  71. this.dateCreated = dateCreated || dateUtils.localNowDateTime();
  72. /** @type {string} */
  73. this.dateModified = dateModified;
  74. /** @type {string} */
  75. this.utcDateCreated = utcDateCreated || dateUtils.utcNowDateTime();
  76. /** @type {string} */
  77. this.utcDateModified = utcDateModified;
  78. // ------ Derived attributes ------
  79. /** @type {boolean} */
  80. this.isDecrypted = !this.noteId || !this.isProtected;
  81. this.decrypt();
  82. /** @type {string|null} */
  83. this.flatTextCache = null;
  84. return this;
  85. }
  86. init() {
  87. /** @type {Branch[]} */
  88. this.parentBranches = [];
  89. /** @type {Note[]} */
  90. this.parents = [];
  91. /** @type {Note[]} */
  92. this.children = [];
  93. /** @type {Attribute[]} */
  94. this.ownedAttributes = [];
  95. /** @type {Attribute[]|null}
  96. * @private */
  97. this.__attributeCache = null;
  98. /** @type {Attribute[]|null}
  99. * @private*/
  100. this.inheritableAttributeCache = null;
  101. /** @type {Attribute[]} */
  102. this.targetRelations = [];
  103. this.becca.addNote(this.noteId, this);
  104. /** @type {Note[]|null}
  105. * @private */
  106. this.ancestorCache = null;
  107. // following attributes are filled during searching from database
  108. /**
  109. * size of the content in bytes
  110. * @type {int|null}
  111. */
  112. this.contentSize = null;
  113. /**
  114. * size of the content and note revision contents in bytes
  115. * @type {int|null}
  116. */
  117. this.noteSize = null;
  118. /**
  119. * number of note revisions for this note
  120. * @type {int|null}
  121. */
  122. this.revisionCount = null;
  123. }
  124. isContentAvailable() {
  125. return !this.noteId // new note which was not encrypted yet
  126. || !this.isProtected
  127. || protectedSessionService.isProtectedSessionAvailable()
  128. }
  129. getTitleOrProtected() {
  130. return this.isContentAvailable() ? this.title : '[protected]';
  131. }
  132. /** @returns {Branch[]} */
  133. getParentBranches() {
  134. return this.parentBranches;
  135. }
  136. /**
  137. * @returns {Branch[]}
  138. * @deprecated use getParentBranches() instead
  139. */
  140. getBranches() {
  141. return this.parentBranches;
  142. }
  143. /** @returns {Note[]} */
  144. getParentNotes() {
  145. return this.parents;
  146. }
  147. /** @returns {Note[]} */
  148. getChildNotes() {
  149. return this.children;
  150. }
  151. /** @returns {boolean} */
  152. hasChildren() {
  153. return this.children &amp;&amp; this.children.length > 0;
  154. }
  155. /** @returns {Branch[]} */
  156. getChildBranches() {
  157. return this.children.map(childNote => this.becca.getBranchFromChildAndParent(childNote.noteId, this.noteId));
  158. }
  159. /*
  160. * Note content has quite special handling - it's not a separate entity, but a lazily loaded
  161. * part of Note entity with it's own sync. Reasons behind this hybrid design has been:
  162. *
  163. * - 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
  164. * - changes in the note metadata or title should not trigger note content sync (so we keep separate utcDateModified and entity changes records)
  165. * - 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)
  166. */
  167. /** @returns {*} */
  168. getContent(silentNotFoundError = false) {
  169. const row = sql.getRow(`SELECT content FROM note_contents WHERE noteId = ?`, [this.noteId]);
  170. if (!row) {
  171. if (silentNotFoundError) {
  172. return undefined;
  173. }
  174. else {
  175. throw new Error("Cannot find note content for noteId=" + this.noteId);
  176. }
  177. }
  178. let content = row.content;
  179. if (this.isProtected) {
  180. if (protectedSessionService.isProtectedSessionAvailable()) {
  181. content = content === null ? null : protectedSessionService.decrypt(content);
  182. }
  183. else {
  184. content = "";
  185. }
  186. }
  187. if (this.isStringNote()) {
  188. return content === null
  189. ? ""
  190. : content.toString("UTF-8");
  191. }
  192. else {
  193. return content;
  194. }
  195. }
  196. /** @returns {{contentLength, dateModified, utcDateModified}} */
  197. getContentMetadata() {
  198. return sql.getRow(`
  199. SELECT
  200. LENGTH(content) AS contentLength,
  201. dateModified,
  202. utcDateModified
  203. FROM note_contents
  204. WHERE noteId = ?`, [this.noteId]);
  205. }
  206. /** @returns {*} */
  207. getJsonContent() {
  208. const content = this.getContent();
  209. if (!content || !content.trim()) {
  210. return null;
  211. }
  212. return JSON.parse(content);
  213. }
  214. setContent(content, ignoreMissingProtectedSession = false) {
  215. if (content === null || content === undefined) {
  216. throw new Error(`Cannot set null content to note ${this.noteId}`);
  217. }
  218. if (this.isStringNote()) {
  219. content = content.toString();
  220. }
  221. else {
  222. content = Buffer.isBuffer(content) ? content : Buffer.from(content);
  223. }
  224. const pojo = {
  225. noteId: this.noteId,
  226. content: content,
  227. dateModified: dateUtils.localNowDateTime(),
  228. utcDateModified: dateUtils.utcNowDateTime()
  229. };
  230. if (this.isProtected) {
  231. if (protectedSessionService.isProtectedSessionAvailable()) {
  232. pojo.content = protectedSessionService.encrypt(pojo.content);
  233. }
  234. else if (!ignoreMissingProtectedSession) {
  235. throw new Error(`Cannot update content of noteId=${this.noteId} since we're out of protected session.`);
  236. }
  237. }
  238. sql.upsert("note_contents", "noteId", pojo);
  239. const hash = utils.hash(this.noteId + "|" + pojo.content.toString());
  240. entityChangesService.addEntityChange({
  241. entityName: 'note_contents',
  242. entityId: this.noteId,
  243. hash: hash,
  244. isErased: false,
  245. utcDateChanged: pojo.utcDateModified,
  246. isSynced: true
  247. });
  248. }
  249. setJsonContent(content) {
  250. this.setContent(JSON.stringify(content, null, '\t'));
  251. }
  252. /** @returns {boolean} true if this note is the root of the note tree. Root note has "root" noteId */
  253. isRoot() {
  254. return this.noteId === 'root';
  255. }
  256. /** @returns {boolean} true if this note is of application/json content type */
  257. isJson() {
  258. return this.mime === "application/json";
  259. }
  260. /** @returns {boolean} true if this note is JavaScript (code or attachment) */
  261. isJavaScript() {
  262. return (this.type === "code" || this.type === "file")
  263. &amp;&amp; (this.mime.startsWith("application/javascript")
  264. || this.mime === "application/x-javascript"
  265. || this.mime === "text/javascript");
  266. }
  267. /** @returns {boolean} true if this note is HTML */
  268. isHtml() {
  269. return ["code", "file", "render"].includes(this.type)
  270. &amp;&amp; this.mime === "text/html";
  271. }
  272. /** @returns {boolean} true if the note has string content (not binary) */
  273. isStringNote() {
  274. return utils.isStringNote(this.type, this.mime);
  275. }
  276. /** @returns {string|null} JS script environment - either "frontend" or "backend" */
  277. getScriptEnv() {
  278. if (this.isHtml() || (this.isJavaScript() &amp;&amp; this.mime.endsWith('env=frontend'))) {
  279. return "frontend";
  280. }
  281. if (this.type === 'render') {
  282. return "frontend";
  283. }
  284. if (this.isJavaScript() &amp;&amp; this.mime.endsWith('env=backend')) {
  285. return "backend";
  286. }
  287. return null;
  288. }
  289. /**
  290. * @param {string} [type] - (optional) attribute type to filter
  291. * @param {string} [name] - (optional) attribute name to filter
  292. * @returns {Attribute[]} all note's attributes, including inherited ones
  293. */
  294. getAttributes(type, name) {
  295. this.__getAttributes([]);
  296. if (type &amp;&amp; name) {
  297. return this.__attributeCache.filter(attr => attr.type === type &amp;&amp; attr.name === name);
  298. }
  299. else if (type) {
  300. return this.__attributeCache.filter(attr => attr.type === type);
  301. }
  302. else if (name) {
  303. return this.__attributeCache.filter(attr => attr.name === name);
  304. }
  305. else {
  306. return this.__attributeCache.slice();
  307. }
  308. }
  309. __getAttributes(path) {
  310. if (path.includes(this.noteId)) {
  311. return [];
  312. }
  313. if (!this.__attributeCache) {
  314. const parentAttributes = this.ownedAttributes.slice();
  315. const newPath = [...path, this.noteId];
  316. if (this.noteId !== 'root') {
  317. for (const parentNote of this.parents) {
  318. parentAttributes.push(...parentNote.__getInheritableAttributes(newPath));
  319. }
  320. }
  321. const templateAttributes = [];
  322. for (const ownedAttr of parentAttributes) { // parentAttributes so we process also inherited templates
  323. if (ownedAttr.type === 'relation' &amp;&amp; ownedAttr.name === 'template') {
  324. const templateNote = this.becca.notes[ownedAttr.value];
  325. if (templateNote) {
  326. templateAttributes.push(...templateNote.__getAttributes(newPath));
  327. }
  328. }
  329. }
  330. this.__attributeCache = [];
  331. const addedAttributeIds = new Set();
  332. for (const attr of parentAttributes.concat(templateAttributes)) {
  333. if (!addedAttributeIds.has(attr.attributeId)) {
  334. addedAttributeIds.add(attr.attributeId);
  335. this.__attributeCache.push(attr);
  336. }
  337. }
  338. this.inheritableAttributeCache = [];
  339. for (const attr of this.__attributeCache) {
  340. if (attr.isInheritable) {
  341. this.inheritableAttributeCache.push(attr);
  342. }
  343. }
  344. }
  345. return this.__attributeCache;
  346. }
  347. /** @returns {Attribute[]} */
  348. __getInheritableAttributes(path) {
  349. if (path.includes(this.noteId)) {
  350. return [];
  351. }
  352. if (!this.inheritableAttributeCache) {
  353. this.__getAttributes(path); // will refresh also this.inheritableAttributeCache
  354. }
  355. return this.inheritableAttributeCache;
  356. }
  357. hasAttribute(type, name) {
  358. return !!this.getAttributes().find(attr => attr.type === type &amp;&amp; attr.name === name);
  359. }
  360. getAttributeCaseInsensitive(type, name, value) {
  361. name = name.toLowerCase();
  362. value = value ? value.toLowerCase() : null;
  363. return this.getAttributes().find(
  364. attr => attr.type === type
  365. &amp;&amp; attr.name.toLowerCase() === name
  366. &amp;&amp; (!value || attr.value.toLowerCase() === value));
  367. }
  368. getRelationTarget(name) {
  369. const relation = this.getAttributes().find(attr => attr.type === 'relation' &amp;&amp; attr.name === name);
  370. return relation ? relation.targetNote : null;
  371. }
  372. /**
  373. * @param {string} name - label name
  374. * @returns {boolean} true if label exists (including inherited)
  375. */
  376. hasLabel(name) { return this.hasAttribute(LABEL, name); }
  377. /**
  378. * @param {string} name - label name
  379. * @returns {boolean} true if label exists (excluding inherited)
  380. */
  381. hasOwnedLabel(name) { return this.hasOwnedAttribute(LABEL, name); }
  382. /**
  383. * @param {string} name - relation name
  384. * @returns {boolean} true if relation exists (including inherited)
  385. */
  386. hasRelation(name) { return this.hasAttribute(RELATION, name); }
  387. /**
  388. * @param {string} name - relation name
  389. * @returns {boolean} true if relation exists (excluding inherited)
  390. */
  391. hasOwnedRelation(name) { return this.hasOwnedAttribute(RELATION, name); }
  392. /**
  393. * @param {string} name - label name
  394. * @returns {Attribute|null} label if it exists, null otherwise
  395. */
  396. getLabel(name) { return this.getAttribute(LABEL, name); }
  397. /**
  398. * @param {string} name - label name
  399. * @returns {Attribute|null} label if it exists, null otherwise
  400. */
  401. getOwnedLabel(name) { return this.getOwnedAttribute(LABEL, name); }
  402. /**
  403. * @param {string} name - relation name
  404. * @returns {Attribute|null} relation if it exists, null otherwise
  405. */
  406. getRelation(name) { return this.getAttribute(RELATION, name); }
  407. /**
  408. * @param {string} name - relation name
  409. * @returns {Attribute|null} relation if it exists, null otherwise
  410. */
  411. getOwnedRelation(name) { return this.getOwnedAttribute(RELATION, name); }
  412. /**
  413. * @param {string} name - label name
  414. * @returns {string|null} label value if label exists, null otherwise
  415. */
  416. getLabelValue(name) { return this.getAttributeValue(LABEL, name); }
  417. /**
  418. * @param {string} name - label name
  419. * @returns {string|null} label value if label exists, null otherwise
  420. */
  421. getOwnedLabelValue(name) { return this.getOwnedAttributeValue(LABEL, name); }
  422. /**
  423. * @param {string} name - relation name
  424. * @returns {string|null} relation value if relation exists, null otherwise
  425. */
  426. getRelationValue(name) { return this.getAttributeValue(RELATION, name); }
  427. /**
  428. * @param {string} name - relation name
  429. * @returns {string|null} relation value if relation exists, null otherwise
  430. */
  431. getOwnedRelationValue(name) { return this.getOwnedAttributeValue(RELATION, name); }
  432. /**
  433. * @param {string} type - attribute type (label, relation, etc.)
  434. * @param {string} name - attribute name
  435. * @returns {boolean} true if note has an attribute with given type and name (excluding inherited)
  436. */
  437. hasOwnedAttribute(type, name) {
  438. return !!this.getOwnedAttribute(type, name);
  439. }
  440. /**
  441. * @param {string} type - attribute type (label, relation, etc.)
  442. * @param {string} name - attribute name
  443. * @returns {Attribute} attribute of given type and name. If there's more such attributes, first is returned. Returns null if there's no such attribute belonging to this note.
  444. */
  445. getAttribute(type, name) {
  446. const attributes = this.getAttributes();
  447. return attributes.find(attr => attr.type === type &amp;&amp; attr.name === name);
  448. }
  449. /**
  450. * @param {string} type - attribute type (label, relation, etc.)
  451. * @param {string} name - attribute name
  452. * @returns {string|null} attribute value of given type and name or null if no such attribute exists.
  453. */
  454. getAttributeValue(type, name) {
  455. const attr = this.getAttribute(type, name);
  456. return attr ? attr.value : null;
  457. }
  458. /**
  459. * @param {string} type - attribute type (label, relation, etc.)
  460. * @param {string} name - attribute name
  461. * @returns {string|null} attribute value of given type and name or null if no such attribute exists.
  462. */
  463. getOwnedAttributeValue(type, name) {
  464. const attr = this.getOwnedAttribute(type, name);
  465. return attr ? attr.value : null;
  466. }
  467. /**
  468. * @param {string} [name] - label name to filter
  469. * @returns {Attribute[]} all note's labels (attributes with type label), including inherited ones
  470. */
  471. getLabels(name) {
  472. return this.getAttributes(LABEL, name);
  473. }
  474. /**
  475. * @param {string} [name] - label name to filter
  476. * @returns {string[]} all note's label values, including inherited ones
  477. */
  478. getLabelValues(name) {
  479. return this.getLabels(name).map(l => l.value);
  480. }
  481. /**
  482. * @param {string} [name] - label name to filter
  483. * @returns {Attribute[]} all note's labels (attributes with type label), excluding inherited ones
  484. */
  485. getOwnedLabels(name) {
  486. return this.getOwnedAttributes(LABEL, name);
  487. }
  488. /**
  489. * @param {string} [name] - label name to filter
  490. * @returns {string[]} all note's label values, excluding inherited ones
  491. */
  492. getOwnedLabelValues(name) {
  493. return this.getOwnedAttributes(LABEL, name).map(l => l.value);
  494. }
  495. /**
  496. * @param {string} [name] - relation name to filter
  497. * @returns {Attribute[]} all note's relations (attributes with type relation), including inherited ones
  498. */
  499. getRelations(name) {
  500. return this.getAttributes(RELATION, name);
  501. }
  502. /**
  503. * @param {string} [name] - relation name to filter
  504. * @returns {Attribute[]} all note's relations (attributes with type relation), excluding inherited ones
  505. */
  506. getOwnedRelations(name) {
  507. return this.getOwnedAttributes(RELATION, name);
  508. }
  509. /**
  510. * @param {string} [type] - (optional) attribute type to filter
  511. * @param {string} [name] - (optional) attribute name to filter
  512. * @returns {Attribute[]} note's "owned" attributes - excluding inherited ones
  513. */
  514. getOwnedAttributes(type, name) {
  515. // it's a common mistake to include # or ~ into attribute name
  516. if (name &amp;&amp; ["#", "~"].includes(name[0])) {
  517. name = name.substr(1);
  518. }
  519. if (type &amp;&amp; name) {
  520. return this.ownedAttributes.filter(attr => attr.type === type &amp;&amp; attr.name === name);
  521. }
  522. else if (type) {
  523. return this.ownedAttributes.filter(attr => attr.type === type);
  524. }
  525. else if (name) {
  526. return this.ownedAttributes.filter(attr => attr.name === name);
  527. }
  528. else {
  529. return this.ownedAttributes.slice();
  530. }
  531. }
  532. /**
  533. * @returns {Attribute} attribute belonging to this specific note (excludes inherited attributes)
  534. *
  535. * This method can be significantly faster than the getAttribute()
  536. */
  537. getOwnedAttribute(type, name) {
  538. const attrs = this.getOwnedAttributes(type, name);
  539. return attrs.length > 0 ? attrs[0] : null;
  540. }
  541. get isArchived() {
  542. return this.hasAttribute('label', 'archived');
  543. }
  544. hasInheritableOwnedArchivedLabel() {
  545. return !!this.ownedAttributes.find(attr => attr.type === 'label' &amp;&amp; attr.name === 'archived' &amp;&amp; attr.isInheritable);
  546. }
  547. // will sort the parents so that non-search &amp; non-archived are first and archived at the end
  548. // this is done so that non-search &amp; non-archived paths are always explored as first when looking for note path
  549. sortParents() {
  550. this.parentBranches.sort((a, b) =>
  551. a.branchId.startsWith('virt-')
  552. || a.parentNote.hasInheritableOwnedArchivedLabel() ? 1 : -1);
  553. this.parents = this.parentBranches.map(branch => branch.parentNote);
  554. }
  555. /**
  556. * This is used for:
  557. * - fast searching
  558. * - note similarity evaluation
  559. *
  560. * @return {string} - returns flattened textual representation of note, prefixes and attributes
  561. */
  562. getFlatText() {
  563. if (!this.flatTextCache) {
  564. this.flatTextCache = this.noteId + ' ' + this.type + ' ' + this.mime + ' ';
  565. for (const branch of this.parentBranches) {
  566. if (branch.prefix) {
  567. this.flatTextCache += branch.prefix + ' ';
  568. }
  569. }
  570. this.flatTextCache += this.title + ' ';
  571. for (const attr of this.getAttributes()) {
  572. // it's best to use space as separator since spaces are filtered from the search string by the tokenization into words
  573. this.flatTextCache += (attr.type === 'label' ? '#' : '~') + attr.name;
  574. if (attr.value) {
  575. this.flatTextCache += '=' + attr.value;
  576. }
  577. this.flatTextCache += ' ';
  578. }
  579. this.flatTextCache = utils.normalize(this.flatTextCache);
  580. }
  581. return this.flatTextCache;
  582. }
  583. invalidateThisCache() {
  584. this.flatTextCache = null;
  585. this.__attributeCache = null;
  586. this.inheritableAttributeCache = null;
  587. this.ancestorCache = null;
  588. }
  589. invalidateSubTree(path = []) {
  590. if (path.includes(this.noteId)) {
  591. return;
  592. }
  593. this.invalidateThisCache();
  594. if (this.children.length || this.targetRelations.length) {
  595. path = [...path, this.noteId];
  596. }
  597. for (const childNote of this.children) {
  598. childNote.invalidateSubTree(path);
  599. }
  600. for (const targetRelation of this.targetRelations) {
  601. if (targetRelation.name === 'template') {
  602. const note = targetRelation.note;
  603. if (note) {
  604. note.invalidateSubTree(path);
  605. }
  606. }
  607. }
  608. }
  609. invalidateSubtreeFlatText() {
  610. this.flatTextCache = null;
  611. for (const childNote of this.children) {
  612. childNote.invalidateSubtreeFlatText();
  613. }
  614. for (const targetRelation of this.targetRelations) {
  615. if (targetRelation.name === 'template') {
  616. const note = targetRelation.note;
  617. if (note) {
  618. note.invalidateSubtreeFlatText();
  619. }
  620. }
  621. }
  622. }
  623. getRelationDefinitions() {
  624. return this.getLabels()
  625. .filter(l => l.name.startsWith("relation:"));
  626. }
  627. getLabelDefinitions() {
  628. return this.getLabels()
  629. .filter(l => l.name.startsWith("relation:"));
  630. }
  631. isTemplate() {
  632. return !!this.targetRelations.find(rel => rel.name === 'template');
  633. }
  634. /** @returns {Note[]} */
  635. getSubtreeNotesIncludingTemplated() {
  636. const set = new Set();
  637. function inner(note) {
  638. if (set.has(note)) {
  639. return;
  640. }
  641. set.add(note);
  642. for (const childNote of note.children) {
  643. inner(childNote);
  644. }
  645. for (const targetRelation of note.targetRelations) {
  646. if (targetRelation.name === 'template') {
  647. const targetNote = targetRelation.note;
  648. if (targetNote) {
  649. inner(targetNote);
  650. }
  651. }
  652. }
  653. }
  654. inner(this);
  655. return Array.from(set);
  656. }
  657. /** @returns {Note[]} */
  658. getSubtreeNotes(includeArchived = true) {
  659. const noteSet = new Set();
  660. function addSubtreeNotesInner(note) {
  661. if (!includeArchived &amp;&amp; note.isArchived) {
  662. return;
  663. }
  664. noteSet.add(note);
  665. for (const childNote of note.children) {
  666. addSubtreeNotesInner(childNote);
  667. }
  668. }
  669. addSubtreeNotesInner(this);
  670. return Array.from(noteSet);
  671. }
  672. /** @returns {String[]} */
  673. getSubtreeNoteIds(includeArchived = true) {
  674. return this.getSubtreeNotes(includeArchived).map(note => note.noteId);
  675. }
  676. getDescendantNoteIds() {
  677. return this.getSubtreeNoteIds();
  678. }
  679. get parentCount() {
  680. return this.parents.length;
  681. }
  682. get childrenCount() {
  683. return this.children.length;
  684. }
  685. get labelCount() {
  686. return this.getAttributes().filter(attr => attr.type === 'label').length;
  687. }
  688. get ownedLabelCount() {
  689. return this.ownedAttributes.filter(attr => attr.type === 'label').length;
  690. }
  691. get relationCount() {
  692. return this.getAttributes().filter(attr => attr.type === 'relation' &amp;&amp; !attr.isAutoLink()).length;
  693. }
  694. get relationCountIncludingLinks() {
  695. return this.getAttributes().filter(attr => attr.type === 'relation').length;
  696. }
  697. get ownedRelationCount() {
  698. return this.ownedAttributes.filter(attr => attr.type === 'relation' &amp;&amp; !attr.isAutoLink()).length;
  699. }
  700. get ownedRelationCountIncludingLinks() {
  701. return this.ownedAttributes.filter(attr => attr.type === 'relation').length;
  702. }
  703. get targetRelationCount() {
  704. return this.targetRelations.filter(attr => !attr.isAutoLink()).length;
  705. }
  706. get targetRelationCountIncludingLinks() {
  707. return this.targetRelations.length;
  708. }
  709. get attributeCount() {
  710. return this.getAttributes().length;
  711. }
  712. get ownedAttributeCount() {
  713. return this.getAttributes().length;
  714. }
  715. /** @returns {Note[]} */
  716. getAncestors() {
  717. if (!this.ancestorCache) {
  718. const noteIds = new Set();
  719. this.ancestorCache = [];
  720. for (const parent of this.parents) {
  721. if (noteIds.has(parent.noteId)) {
  722. continue;
  723. }
  724. this.ancestorCache.push(parent);
  725. noteIds.add(parent.noteId);
  726. for (const ancestorNote of parent.getAncestors()) {
  727. if (!noteIds.has(ancestorNote.noteId)) {
  728. this.ancestorCache.push(ancestorNote);
  729. noteIds.add(ancestorNote.noteId);
  730. }
  731. }
  732. }
  733. }
  734. return this.ancestorCache;
  735. }
  736. /** @returns {boolean} */
  737. hasAncestor(ancestorNoteId) {
  738. for (const ancestorNote of this.getAncestors()) {
  739. if (ancestorNote.noteId === ancestorNoteId) {
  740. return true;
  741. }
  742. }
  743. return false;
  744. }
  745. getTargetRelations() {
  746. return this.targetRelations;
  747. }
  748. /** @returns {Note[]} - returns only notes which are templated, does not include their subtrees
  749. * in effect returns notes which are influenced by note's non-inheritable attributes */
  750. getTemplatedNotes() {
  751. const arr = [this];
  752. for (const targetRelation of this.targetRelations) {
  753. if (targetRelation.name === 'template') {
  754. const note = targetRelation.note;
  755. if (note) {
  756. arr.push(note);
  757. }
  758. }
  759. }
  760. return arr;
  761. }
  762. getDistanceToAncestor(ancestorNoteId) {
  763. if (this.noteId === ancestorNoteId) {
  764. return 0;
  765. }
  766. let minDistance = 999999;
  767. for (const parent of this.parents) {
  768. minDistance = Math.min(minDistance, parent.getDistanceToAncestor(ancestorNoteId) + 1);
  769. }
  770. return minDistance;
  771. }
  772. getNoteRevisions() {
  773. return sql.getRows("SELECT * FROM note_revisions WHERE noteId = ?", [this.noteId])
  774. .map(row => new NoteRevision(row));
  775. }
  776. /**
  777. * @return {string[][]} - array of notePaths (each represented by array of noteIds constituting the particular note path)
  778. */
  779. getAllNotePaths() {
  780. if (this.noteId === 'root') {
  781. return [['root']];
  782. }
  783. const notePaths = [];
  784. for (const parentNote of this.getParentNotes()) {
  785. for (const parentPath of parentNote.getAllNotePaths()) {
  786. parentPath.push(this.noteId);
  787. notePaths.push(parentPath);
  788. }
  789. }
  790. return notePaths;
  791. }
  792. /**
  793. * @param ancestorNoteId
  794. * @return {boolean} - true if ancestorNoteId occurs in at least one of the note's paths
  795. */
  796. isDescendantOfNote(ancestorNoteId) {
  797. const notePaths = this.getAllNotePaths();
  798. return notePaths.some(path => path.includes(ancestorNoteId));
  799. }
  800. /**
  801. * Update's given attribute's value or creates it if it doesn't exist
  802. *
  803. * @param {string} type - attribute type (label, relation, etc.)
  804. * @param {string} name - attribute name
  805. * @param {string} [value] - attribute value (optional)
  806. */
  807. setAttribute(type, name, value) {
  808. const attributes = this.getOwnedAttributes();
  809. const attr = attributes.find(attr => attr.type === type &amp;&amp; attr.name === name);
  810. value = value !== null &amp;&amp; value !== undefined ? value.toString() : "";
  811. if (attr) {
  812. if (attr.value !== value) {
  813. attr.value = value;
  814. attr.save();
  815. }
  816. }
  817. else {
  818. const Attribute = require("./attribute");
  819. new Attribute({
  820. noteId: this.noteId,
  821. type: type,
  822. name: name,
  823. value: value
  824. }).save();
  825. }
  826. }
  827. /**
  828. * Removes given attribute name-value pair if it exists.
  829. *
  830. * @param {string} type - attribute type (label, relation, etc.)
  831. * @param {string} name - attribute name
  832. * @param {string} [value] - attribute value (optional)
  833. */
  834. removeAttribute(type, name, value) {
  835. const attributes = this.getOwnedAttributes();
  836. for (const attribute of attributes) {
  837. if (attribute.type === type &amp;&amp; attribute.name === name &amp;&amp; (value === undefined || value === attribute.value)) {
  838. attribute.markAsDeleted();
  839. }
  840. }
  841. }
  842. /**
  843. * @return {Attribute}
  844. */
  845. addAttribute(type, name, value = "", isInheritable = false, position = 1000) {
  846. const Attribute = require("./attribute");
  847. return new Attribute({
  848. noteId: this.noteId,
  849. type: type,
  850. name: name,
  851. value: value,
  852. isInheritable: isInheritable,
  853. position: position
  854. }).save();
  855. }
  856. addLabel(name, value = "", isInheritable = false) {
  857. return this.addAttribute(LABEL, name, value, isInheritable);
  858. }
  859. addRelation(name, targetNoteId, isInheritable = false) {
  860. return this.addAttribute(RELATION, name, targetNoteId, isInheritable);
  861. }
  862. /**
  863. * Based on enabled, attribute is either set or removed.
  864. *
  865. * @param {string} type - attribute type ('relation', 'label' etc.)
  866. * @param {boolean} enabled - toggle On or Off
  867. * @param {string} name - attribute name
  868. * @param {string} [value] - attribute value (optional)
  869. */
  870. toggleAttribute(type, enabled, name, value) {
  871. if (enabled) {
  872. this.setAttribute(type, name, value);
  873. }
  874. else {
  875. this.removeAttribute(type, name, value);
  876. }
  877. }
  878. /**
  879. * Based on enabled, label is either set or removed.
  880. *
  881. * @param {boolean} enabled - toggle On or Off
  882. * @param {string} name - label name
  883. * @param {string} [value] - label value (optional)
  884. */
  885. toggleLabel(enabled, name, value) { return this.toggleAttribute(LABEL, enabled, name, value); }
  886. /**
  887. * Based on enabled, relation is either set or removed.
  888. *
  889. * @param {boolean} enabled - toggle On or Off
  890. * @param {string} name - relation name
  891. * @param {string} [value] - relation value (noteId)
  892. */
  893. toggleRelation(enabled, name, value) { return this.toggleAttribute(RELATION, enabled, name, value); }
  894. /**
  895. * Update's given label's value or creates it if it doesn't exist
  896. *
  897. * @param {string} name - label name
  898. * @param {string} [value] - label value
  899. */
  900. setLabel(name, value) { return this.setAttribute(LABEL, name, value); }
  901. /**
  902. * Update's given relation's value or creates it if it doesn't exist
  903. *
  904. * @param {string} name - relation name
  905. * @param {string} value - relation value (noteId)
  906. */
  907. setRelation(name, value) { return this.setAttribute(RELATION, name, value); }
  908. /**
  909. * Remove label name-value pair, if it exists.
  910. *
  911. * @param {string} name - label name
  912. * @param {string} [value] - label value
  913. */
  914. removeLabel(name, value) { return this.removeAttribute(LABEL, name, value); }
  915. /**
  916. * Remove relation name-value pair, if it exists.
  917. *
  918. * @param {string} name - relation name
  919. * @param {string} [value] - relation value (noteId)
  920. */
  921. removeRelation(name, value) { return this.removeAttribute(RELATION, name, value); }
  922. searchNotesInSubtree(searchString) {
  923. const searchService = require("../../services/search/services/search");
  924. return searchService.searchNotes(searchString);
  925. }
  926. searchNoteInSubtree(searchString) {
  927. return this.searchNotesInSubtree(searchString)[0];
  928. }
  929. cloneTo(parentNoteId) {
  930. const cloningService = require("../../services/cloning");
  931. const branch = this.becca.getNote(parentNoteId).getParentBranches()[0];
  932. return cloningService.cloneNoteToBranch(this.noteId, branch.branchId);
  933. }
  934. decrypt() {
  935. if (this.isProtected &amp;&amp; !this.isDecrypted &amp;&amp; protectedSessionService.isProtectedSessionAvailable()) {
  936. try {
  937. this.title = protectedSessionService.decryptString(this.title);
  938. this.isDecrypted = true;
  939. }
  940. catch (e) {
  941. log.error(`Could not decrypt note ${this.noteId}: ${e.message} ${e.stack}`);
  942. }
  943. }
  944. }
  945. get isDeleted() {
  946. return !(this.noteId in this.becca.notes);
  947. }
  948. beforeSaving() {
  949. super.beforeSaving();
  950. this.becca.addNote(this.noteId, this);
  951. this.dateModified = dateUtils.localNowDateTime();
  952. this.utcDateModified = dateUtils.utcNowDateTime();
  953. }
  954. getPojo() {
  955. return {
  956. noteId: this.noteId,
  957. title: this.title,
  958. isProtected: this.isProtected,
  959. type: this.type,
  960. mime: this.mime,
  961. isDeleted: false,
  962. dateCreated: this.dateCreated,
  963. dateModified: this.dateModified,
  964. utcDateCreated: this.utcDateCreated,
  965. utcDateModified: this.utcDateModified
  966. };
  967. }
  968. getPojoToSave() {
  969. const pojo = this.getPojo();
  970. if (pojo.isProtected) {
  971. if (this.isDecrypted) {
  972. pojo.title = protectedSessionService.encrypt(pojo.title);
  973. }
  974. else {
  975. // updating protected note outside of protected session means we will keep original ciphertexts
  976. delete pojo.title;
  977. }
  978. }
  979. return pojo;
  980. }
  981. }
  982. module.exports = Note;
  983. </code></pre>
  984. </article>
  985. </section>
  986. </div>
  987. <nav>
  988. <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="Attribute.html">Attribute</a></li><li><a href="BackendScriptApi.html">BackendScriptApi</a></li><li><a href="Branch.html">Branch</a></li><li><a href="EtapiToken.html">EtapiToken</a></li><li><a href="Note.html">Note</a></li><li><a href="NoteRevision.html">NoteRevision</a></li><li><a href="Option.html">Option</a></li><li><a href="RecentNote.html">RecentNote</a></li></ul><h3><a href="global.html">Global</a></h3>
  989. </nav>
  990. <br class="clear">
  991. <footer>
  992. Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.7</a>
  993. </footer>
  994. <script> prettyPrint(); </script>
  995. <script src="scripts/linenumber.js"> </script>
  996. </body>
  997. </html>