becca_entities_note.js.html 37 KB

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