@@ -191,7 +93,7 @@
Source:
@@ -225,17 +127,6 @@
- Extends
-
-
-
-
-
-
-
-
@@ -264,13 +155,13 @@
- Modules Classes
+ Modules Classes
diff --git a/docs/backend_api/backend_script_api.js.html b/docs/backend_api/backend_script_api.js.html
new file mode 100644
index 000000000..b47696c4b
--- /dev/null
+++ b/docs/backend_api/backend_script_api.js.html
@@ -0,0 +1,472 @@
+
+
+
+
+ JSDoc: Source: backend_script_api.js
+
+
+
+
+
+
+
+
+
+
+
+
+
Source: backend_script_api.js
+
+
+
+
+
+
+
+
+ const log = require('./log');
+const noteService = require('./notes');
+const sql = require('./sql');
+const utils = require('./utils');
+const attributeService = require('./attributes');
+const dateNoteService = require('./date_notes');
+const treeService = require('./tree');
+const config = require('./config');
+const axios = require('axios');
+const dayjs = require('dayjs');
+const xml2js = require('xml2js');
+const cloningService = require('./cloning');
+const appInfo = require('./app_info');
+const searchService = require('./search/services/search');
+const SearchContext = require("./search/search_context");
+const becca = require("../becca/becca");
+
+/**
+ * This is the main backend API interface for scripts. It's published in the local "api" object.
+ *
+ * @constructor
+ * @hideconstructor
+ */
+function BackendScriptApi(currentNote, apiParams) {
+ /** @property {Note} note where script started executing */
+ this.startNote = apiParams.startNote;
+ /** @property {Note} note where script is currently executing. Don't mix this up with concept of active note */
+ this.currentNote = currentNote;
+ /** @property {Entity} entity whose event triggered this executions */
+ this.originEntity = apiParams.originEntity;
+
+ for (const key in apiParams) {
+ this[key] = apiParams[key];
+ }
+
+ /** @property {axios} Axios library for HTTP requests. See https://axios-http.com/ for documentation */
+ this.axios = axios;
+ /** @property {dayjs} day.js library for date manipulation. See https://day.js.org/ for documentation */
+ this.dayjs = dayjs;
+ /** @property {axios} xml2js library for XML parsing. See https://github.com/Leonidas-from-XIV/node-xml2js for documentation */
+ this.xml2js = xml2js;
+
+ // DEPRECATED - use direct api.unescapeHtml
+ this.utils = {
+ unescapeHtml: utils.unescapeHtml
+ };
+
+ /**
+ * Instance name identifies particular Trilium instance. It can be useful for scripts
+ * if some action needs to happen on only one specific instance.
+ *
+ * @returns {string|null}
+ */
+ this.getInstanceName = () => config.General ? config.General.instanceName : null;
+
+ /**
+ * @method
+ * @param {string} noteId
+ * @returns {Note|null}
+ */
+ this.getNote = becca.getNote;
+
+ /**
+ * @method
+ * @param {string} branchId
+ * @returns {Branch|null}
+ */
+ this.getBranch = becca.getBranch;
+
+ /**
+ * @method
+ * @param {string} attributeId
+ * @returns {Attribute|null}
+ */
+ this.getAttribute = becca.getAttribute;
+
+ /**
+ * This is a powerful search method - you can search by attributes and their values, e.g.:
+ * "#dateModified =* MONTH AND #log". See full documentation for all options at: https://github.com/zadam/trilium/wiki/Search
+ *
+ * @method
+ * @param {string} query
+ * @param {Object} [searchParams]
+ * @returns {Note[]}
+ */
+ this.searchForNotes = (query, searchParams = {}) => {
+ if (searchParams.includeArchivedNotes === undefined) {
+ searchParams.includeArchivedNotes = true;
+ }
+
+ if (searchParams.ignoreHoistedNote === undefined) {
+ searchParams.ignoreHoistedNote = true;
+ }
+
+ const noteIds = searchService.findResultsWithQuery(query, new SearchContext(searchParams))
+ .map(sr => sr.noteId);
+
+ return becca.getNotes(noteIds);
+ };
+
+ /**
+ * This is a powerful search method - you can search by attributes and their values, e.g.:
+ * "#dateModified =* MONTH AND #log". See full documentation for all options at: https://github.com/zadam/trilium/wiki/Search
+ *
+ * @method
+ * @param {string} query
+ * @param {Object} [searchParams]
+ * @returns {Note|null}
+ */
+ this.searchForNote = (query, searchParams = {}) => {
+ const notes = this.searchForNotes(query, searchParams);
+
+ return notes.length > 0 ? notes[0] : null;
+ };
+
+ /**
+ * Retrieves notes with given label name & value
+ *
+ * @method
+ * @param {string} name - attribute name
+ * @param {string} [value] - attribute value
+ * @returns {Note[]}
+ */
+ this.getNotesWithLabel = attributeService.getNotesWithLabel;
+
+ /**
+ * Retrieves first note with given label name & value
+ *
+ * @method
+ * @param {string} name - attribute name
+ * @param {string} [value] - attribute value
+ * @returns {Note|null}
+ */
+ this.getNoteWithLabel = attributeService.getNoteWithLabel;
+
+ /**
+ * If there's no branch between note and parent note, create one. Otherwise do nothing.
+ *
+ * @method
+ * @param {string} noteId
+ * @param {string} parentNoteId
+ * @param {string} prefix - if branch will be create between note and parent note, set this prefix
+ * @returns {void}
+ */
+ this.ensureNoteIsPresentInParent = cloningService.ensureNoteIsPresentInParent;
+
+ /**
+ * If there's a branch between note and parent note, remove it. Otherwise do nothing.
+ *
+ * @method
+ * @param {string} noteId
+ * @param {string} parentNoteId
+ * @returns {void}
+ */
+ this.ensureNoteIsAbsentFromParent = cloningService.ensureNoteIsAbsentFromParent;
+
+ /**
+ * Based on the value, either create or remove branch between note and parent note.
+ *
+ * @method
+ * @param {boolean} present - true if we want the branch to exist, false if we want it gone
+ * @param {string} noteId
+ * @param {string} parentNoteId
+ * @param {string} prefix - if branch will be create between note and parent note, set this prefix
+ * @returns {void}
+ */
+ this.toggleNoteInParent = cloningService.toggleNoteInParent;
+
+ /**
+ * @typedef {object} CreateNoteAttribute
+ * @property {string} type - attribute type - label, relation etc.
+ * @property {string} name - attribute name
+ * @property {string} [value] - attribute value
+ */
+
+ /**
+ * Create text note. See also createNewNote() for more options.
+ *
+ * @param {string} parentNoteId
+ * @param {string} title
+ * @param {string} content
+ * @return {{note: Note, branch: Branch}} - object having "note" and "branch" keys representing respective objects
+ */
+ this.createTextNote = (parentNoteId, title, content = '') => noteService.createNewNote({
+ parentNoteId,
+ title,
+ content,
+ type: 'text'
+ });
+
+ /**
+ * Create data note - data in this context means object serializable to JSON. Created note will be of type 'code' and
+ * JSON MIME type. See also createNewNote() for more options.
+ *
+ * @param {string} parentNoteId
+ * @param {string} title
+ * @param {object} content
+ * @return {{note: Note, branch: Branch}} object having "note" and "branch" keys representing respective objects
+ */
+ this.createDataNote = (parentNoteId, title, content = {}) => noteService.createNewNote({
+ parentNoteId,
+ title,
+ content: JSON.stringify(content, null, '\t'),
+ type: 'code',
+ mime: 'application/json'
+ });
+
+ /**
+ * @typedef {object} CreateNewNoteParams
+ * @property {string} parentNoteId - MANDATORY
+ * @property {string} title - MANDATORY
+ * @property {string|buffer} content - MANDATORY
+ * @property {string} type - text, code, file, image, search, book, relation-map - MANDATORY
+ * @property {string} mime - value is derived from default mimes for type
+ * @property {boolean} isProtected - default is false
+ * @property {boolean} isExpanded - default is false
+ * @property {string} prefix - default is empty string
+ * @property {int} notePosition - default is last existing notePosition in a parent + 10
+ */
+
+ /**
+ * @method
+ *
+ * @param {CreateNewNoteParams} [params]
+ * @returns {{note: Note, branch: Branch}} object contains newly created entities note and branch
+ */
+ this.createNewNote = noteService.createNewNote;
+
+ /**
+ * @typedef {object} CreateNoteAttribute
+ * @property {string} type - attribute type - label, relation etc.
+ * @property {string} name - attribute name
+ * @property {string} [value] - attribute value
+ */
+
+ /**
+ * @typedef {object} CreateNoteExtraOptions
+ * @property {boolean} [json=false] - should the note be JSON
+ * @property {boolean} [isProtected=false] - should the note be protected
+ * @property {string} [type='text'] - note type
+ * @property {string} [mime='text/html'] - MIME type of the note
+ * @property {CreateNoteAttribute[]} [attributes=[]] - attributes to be created for this note
+ */
+
+ /**
+ * @method
+ * @deprecated please use createTextNote() with similar API for simpler use cases or createNewNote() for more complex needs
+ *
+ * @param {string} parentNoteId - create new note under this parent
+ * @param {string} title
+ * @param {string} [content=""]
+ * @param {CreateNoteExtraOptions} [extraOptions={}]
+ * @returns {{note: Note, branch: Branch}} object contains newly created entities note and branch
+ */
+ this.createNote = (parentNoteId, title, content = "", extraOptions= {}) => {
+ extraOptions.parentNoteId = parentNoteId;
+ extraOptions.title = title;
+
+ const parentNote = becca.getNote(parentNoteId);
+
+ // code note type can be inherited, otherwise text is default
+ extraOptions.type = parentNote.type === 'code' ? 'code' : 'text';
+ extraOptions.mime = parentNote.type === 'code' ? parentNote.mime : 'text/html';
+
+ if (extraOptions.json) {
+ extraOptions.content = JSON.stringify(content || {}, null, '\t');
+ extraOptions.type = 'code';
+ extraOptions.mime = 'application/json';
+ }
+ else {
+ extraOptions.content = content;
+ }
+
+ return sql.transactional(() => {
+ const {note, branch} = noteService.createNewNote(extraOptions);
+
+ for (const attr of extraOptions.attributes || []) {
+ attributeService.createAttribute({
+ noteId: note.noteId,
+ type: attr.type,
+ name: attr.name,
+ value: attr.value,
+ isInheritable: !!attr.isInheritable
+ });
+ }
+
+ return {note, branch};
+ });
+ };
+
+ /**
+ * Log given message to trilium logs.
+ *
+ * @param message
+ */
+ this.log = message => log.info(`Script "${currentNote.title}" (${currentNote.noteId}): ${message}`);
+
+ /**
+ * Returns root note of the calendar.
+ *
+ * @method
+ * @returns {Note|null}
+ */
+ this.getRootCalendarNote = dateNoteService.getRootCalendarNote;
+
+ /**
+ * Returns day note for given date. If such note doesn't exist, it is created.
+ *
+ * @method
+ * @param {string} date in YYYY-MM-DD format
+ * @returns {Note|null}
+ */
+ this.getDateNote = dateNoteService.getDateNote;
+
+ /**
+ * Returns today's day note. If such note doesn't exist, it is created.
+ *
+ * @method
+ * @returns {Note|null}
+ */
+ this.getTodayNote = dateNoteService.getTodayNote;
+
+ /**
+ * Returns note for the first date of the week of the given date.
+ *
+ * @method
+ * @param {string} date in YYYY-MM-DD format
+ * @param {object} options - "startOfTheWeek" - either "monday" (default) or "sunday"
+ * @returns {Note|null}
+ */
+ this.getWeekNote = dateNoteService.getWeekNote;
+
+ /**
+ * Returns month note for given date. If such note doesn't exist, it is created.
+ *
+ * @method
+ * @param {string} date in YYYY-MM format
+ * @returns {Note|null}
+ */
+ this.getMonthNote = dateNoteService.getMonthNote;
+
+ /**
+ * Returns year note for given year. If such note doesn't exist, it is created.
+ *
+ * @method
+ * @param {string} year in YYYY format
+ * @returns {Note|null}
+ */
+ this.getYearNote = dateNoteService.getYearNote;
+
+ /**
+ * @method
+ * @param {string} parentNoteId - this note's child notes will be sorted
+ */
+ this.sortNotesByTitle = parentNoteId => treeService.sortNotes(parentNoteId);
+
+ /**
+ * This method finds note by its noteId and prefix and either sets it to the given parentNoteId
+ * or removes the branch (if parentNoteId is not given).
+ *
+ * This method looks similar to toggleNoteInParent() but differs because we're looking up branch by prefix.
+ *
+ * @method
+ * @deprecated - this method is pretty confusing and serves specialized purpose only
+ * @param {string} noteId
+ * @param {string} prefix
+ * @param {string|null} parentNoteId
+ */
+ this.setNoteToParent = treeService.setNoteToParent;
+
+ /**
+ * This functions wraps code which is supposed to be running in transaction. If transaction already
+ * exists, then we'll use that transaction.
+ *
+ * @method
+ * @param {function} func
+ * @returns {?} result of func callback
+ */
+ this.transactional = sql.transactional;
+
+ /**
+ * Return randomly generated string of given length. This random string generation is NOT cryptographically secure.
+ *
+ * @method
+ * @param {number} length of the string
+ * @returns {string} random string
+ */
+ this.randomString = utils.randomString;
+
+ /**
+ * @method
+ * @param {string} string to escape
+ * @returns {string} escaped string
+ */
+ this.escapeHtml = utils.escapeHtml;
+
+ /**
+ * @method
+ * @param {string} string to unescape
+ * @returns {string} unescaped string
+ */
+ this.unescapeHtml = utils.unescapeHtml;
+
+ /**
+ * @property {module:sql} sql
+ */
+ this.sql = sql;
+
+ /**
+ * @method
+ * @deprecated - this is now no-op since all the changes should be gracefully handled per widget
+ */
+ this.refreshTree = () => {};
+
+ /**
+ * @return {{syncVersion, appVersion, buildRevision, dbVersion, dataDirectory, buildDate}|*} - object representing basic info about running Trilium version
+ */
+ this.getAppInfo = () => appInfo
+}
+
+module.exports = BackendScriptApi;
+
+
+
+
+
+
+
+
+
+
+ Modules Classes
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/backend_api/becca_entities_api_token.js.html b/docs/backend_api/becca_entities_api_token.js.html
new file mode 100644
index 000000000..ec881d8ed
--- /dev/null
+++ b/docs/backend_api/becca_entities_api_token.js.html
@@ -0,0 +1,82 @@
+
+
+
+
+ JSDoc: Source: becca/entities/api_token.js
+
+
+
+
+
+
+
+
+
+
+
+
+
Source: becca/entities/api_token.js
+
+
+
+
+
+
+
+
+ "use strict";
+
+const dateUtils = require('../../services/date_utils.js');
+const AbstractEntity = require("./abstract_entity.js");
+
+/**
+ * ApiToken is an entity representing token used to authenticate against Trilium API from client applications. Currently used only by Trilium Sender.
+ */
+class ApiToken extends AbstractEntity {
+ static get entityName() { return "api_tokens"; }
+ static get primaryKeyName() { return "apiTokenId"; }
+ static get hashedProperties() { return ["apiTokenId", "token", "utcDateCreated"]; }
+
+ constructor(row) {
+ super();
+
+ this.apiTokenId = row.apiTokenId;
+ this.token = row.token;
+ this.utcDateCreated = row.utcDateCreated || dateUtils.utcNowDateTime();
+ }
+
+ getPojo() {
+ return {
+ apiTokenId: this.apiTokenId,
+ token: this.token,
+ utcDateCreated: this.utcDateCreated
+ }
+ }
+}
+
+module.exports = ApiToken;
+
+
+
+
+
+
+
+
+
+
+ Modules Classes
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/backend_api/becca_entities_attribute.js.html b/docs/backend_api/becca_entities_attribute.js.html
new file mode 100644
index 000000000..933968145
--- /dev/null
+++ b/docs/backend_api/becca_entities_attribute.js.html
@@ -0,0 +1,252 @@
+
+
+
+
+ JSDoc: Source: becca/entities/attribute.js
+
+
+
+
+
+
+
+
+
+
+
+
+
Source: becca/entities/attribute.js
+
+
+
+
+
+
+
+
+ "use strict";
+
+const Note = require('./note.js');
+const AbstractEntity = require("./abstract_entity.js");
+const sql = require("../../services/sql.js");
+const dateUtils = require("../../services/date_utils.js");
+const promotedAttributeDefinitionParser = require("../../services/promoted_attribute_definition_parser");
+
+class Attribute extends AbstractEntity {
+ static get entityName() { return "attributes"; }
+ static get primaryKeyName() { return "attributeId"; }
+ static get hashedProperties() { return ["attributeId", "noteId", "type", "name", "value", "isInheritable"]; }
+
+ constructor(row) {
+ super();
+
+ if (!row) {
+ return;
+ }
+
+ this.updateFromRow(row);
+ this.init();
+ }
+
+ updateFromRow(row) {
+ this.update([
+ row.attributeId,
+ row.noteId,
+ row.type,
+ row.name,
+ row.value,
+ row.isInheritable,
+ row.position,
+ row.utcDateModified
+ ]);
+ }
+
+ update([attributeId, noteId, type, name, value, isInheritable, position, utcDateModified]) {
+ /** @param {string} */
+ this.attributeId = attributeId;
+ /** @param {string} */
+ this.noteId = noteId;
+ /** @param {string} */
+ this.type = type;
+ /** @param {string} */
+ this.name = name;
+ /** @param {int} */
+ this.position = position;
+ /** @param {string} */
+ this.value = value;
+ /** @param {boolean} */
+ this.isInheritable = !!isInheritable;
+ /** @param {string} */
+ this.utcDateModified = utcDateModified;
+
+ return this;
+ }
+
+ init() {
+ if (this.attributeId) {
+ this.becca.attributes[this.attributeId] = this;
+ }
+
+ if (!(this.noteId in this.becca.notes)) {
+ // entities can come out of order in sync, create skeleton which will be filled later
+ this.becca.notes[this.noteId] = new Note({noteId: this.noteId});
+ }
+
+ this.becca.notes[this.noteId].ownedAttributes.push(this);
+
+ const key = `${this.type}-${this.name.toLowerCase()}`;
+ this.becca.attributeIndex[key] = this.becca.attributeIndex[key] || [];
+ this.becca.attributeIndex[key].push(this);
+
+ const targetNote = this.targetNote;
+
+ if (targetNote) {
+ targetNote.targetRelations.push(this);
+ }
+ }
+
+ get isAffectingSubtree() {
+ return this.isInheritable
+ || (this.type === 'relation' && this.name === 'template');
+ }
+
+ get targetNoteId() { // alias
+ return this.type === 'relation' ? this.value : undefined;
+ }
+
+ isAutoLink() {
+ return this.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(this.name);
+ }
+
+ get note() {
+ return this.becca.notes[this.noteId];
+ }
+
+ get targetNote() {
+ if (this.type === 'relation') {
+ return this.becca.notes[this.value];
+ }
+ }
+
+ /**
+ * @returns {Note|null}
+ */
+ getNote() {
+ return this.becca.getNote(this.noteId);
+ }
+
+ /**
+ * @returns {Note|null}
+ */
+ getTargetNote() {
+ if (this.type !== 'relation') {
+ throw new Error(`Attribute ${this.attributeId} is not relation`);
+ }
+
+ if (!this.value) {
+ return null;
+ }
+
+ return this.becca.getNote(this.value);
+ }
+
+ /**
+ * @return {boolean}
+ */
+ isDefinition() {
+ return this.type === 'label' && (this.name.startsWith('label:') || this.name.startsWith('relation:'));
+ }
+
+ getDefinition() {
+ return promotedAttributeDefinitionParser.parse(this.value);
+ }
+
+ getDefinedName() {
+ if (this.type === 'label' && this.name.startsWith('label:')) {
+ return this.name.substr(6);
+ } else if (this.type === 'label' && this.name.startsWith('relation:')) {
+ return this.name.substr(9);
+ } else {
+ return this.name;
+ }
+ }
+
+ beforeSaving() {
+ if (!this.value) {
+ if (this.type === 'relation') {
+ throw new Error(`Cannot save relation ${this.name} since it does not target any note.`);
+ }
+
+ // null value isn't allowed
+ this.value = "";
+ }
+
+ if (this.position === undefined) {
+ // TODO: can be calculated from becca
+ this.position = 1 + sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM attributes WHERE noteId = ?`, [this.noteId]);
+ }
+
+ if (!this.isInheritable) {
+ this.isInheritable = false;
+ }
+
+ this.utcDateModified = dateUtils.utcNowDateTime();
+
+ super.beforeSaving();
+
+ this.becca.attributes[this.attributeId] = this;
+ }
+
+ getPojo() {
+ return {
+ attributeId: this.attributeId,
+ noteId: this.noteId,
+ type: this.type,
+ name: this.name,
+ position: this.position,
+ value: this.value,
+ isInheritable: this.isInheritable,
+ utcDateModified: this.utcDateModified,
+ isDeleted: false
+ };
+ }
+
+ createClone(type, name, value, isInheritable) {
+ return new Attribute({
+ noteId: this.noteId,
+ type: type,
+ name: name,
+ value: value,
+ position: this.position,
+ isInheritable: isInheritable,
+ utcDateModified: this.utcDateModified
+ });
+ }
+}
+
+module.exports = Attribute;
+
+
+
+
+
+
+
+
+
+
+ Modules Classes
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/backend_api/becca_entities_branch.js.html b/docs/backend_api/becca_entities_branch.js.html
new file mode 100644
index 000000000..f5b83e478
--- /dev/null
+++ b/docs/backend_api/becca_entities_branch.js.html
@@ -0,0 +1,192 @@
+
+
+
+
+ JSDoc: Source: becca/entities/branch.js
+
+
+
+
+
+
+
+
+
+
+
+
+
Source: becca/entities/branch.js
+
+
+
+
+
+
+
+
+ "use strict";
+
+const Note = require('./note.js');
+const AbstractEntity = require("./abstract_entity.js");
+const sql = require("../../services/sql.js");
+const dateUtils = require("../../services/date_utils.js");
+
+class Branch extends AbstractEntity {
+ static get entityName() { return "branches"; }
+ static get primaryKeyName() { return "branchId"; }
+ // notePosition is not part of hash because it would produce a lot of updates in case of reordering
+ static get hashedProperties() { return ["branchId", "noteId", "parentNoteId", "prefix"]; }
+
+ constructor(row) {
+ super();
+
+ if (!row) {
+ return;
+ }
+
+ this.updateFromRow(row);
+ this.init();
+ }
+
+ updateFromRow(row) {
+ this.update([
+ row.branchId,
+ row.noteId,
+ row.parentNoteId,
+ row.prefix,
+ row.notePosition,
+ row.isExpanded,
+ row.utcDateModified
+ ]);
+ }
+
+ update([branchId, noteId, parentNoteId, prefix, notePosition, isExpanded, utcDateModified]) {
+ /** @param {string} */
+ this.branchId = branchId;
+ /** @param {string} */
+ this.noteId = noteId;
+ /** @param {string} */
+ this.parentNoteId = parentNoteId;
+ /** @param {string} */
+ this.prefix = prefix;
+ /** @param {int} */
+ this.notePosition = notePosition;
+ /** @param {boolean} */
+ this.isExpanded = !!isExpanded;
+ /** @param {string} */
+ this.utcDateModified = utcDateModified;
+
+ return this;
+ }
+
+ init() {
+ if (this.branchId === 'root') {
+ return;
+ }
+
+ const childNote = this.childNote;
+ const parentNote = this.parentNote;
+
+ childNote.parents.push(parentNote);
+ childNote.parentBranches.push(this);
+
+ parentNote.children.push(childNote);
+
+ this.becca.branches[this.branchId] = this;
+ this.becca.childParentToBranch[`${this.noteId}-${this.parentNoteId}`] = this;
+ }
+
+ /** @return {Note} */
+ get childNote() {
+ if (!(this.noteId in this.becca.notes)) {
+ // entities can come out of order in sync, create skeleton which will be filled later
+ this.becca.notes[this.noteId] = new Note({noteId: this.noteId});
+ }
+
+ return this.becca.notes[this.noteId];
+ }
+
+ getNote() {
+ return this.childNote;
+ }
+
+ /** @return {Note} */
+ get parentNote() {
+ if (!(this.parentNoteId in this.becca.notes)) {
+ // entities can come out of order in sync, create skeleton which will be filled later
+ this.becca.notes[this.parentNoteId] = new Note({noteId: this.parentNoteId});
+ }
+
+ return this.becca.notes[this.parentNoteId];
+ }
+
+ beforeSaving() {
+ if (this.notePosition === undefined || this.notePosition === null) {
+ // TODO finding new position can be refactored into becca
+ const maxNotePos = sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [this.parentNoteId]);
+ this.notePosition = maxNotePos === null ? 0 : maxNotePos + 10;
+ }
+
+ if (!this.isExpanded) {
+ this.isExpanded = false;
+ }
+
+ this.utcDateModified = dateUtils.utcNowDateTime();
+
+ super.beforeSaving();
+
+ this.becca.branches[this.branchId] = this;
+ }
+
+ getPojo() {
+ return {
+ branchId: this.branchId,
+ noteId: this.noteId,
+ parentNoteId: this.parentNoteId,
+ prefix: this.prefix,
+ notePosition: this.notePosition,
+ isExpanded: this.isExpanded,
+ isDeleted: false,
+ utcDateModified: this.utcDateModified,
+ // not used for anything, will be later dropped
+ utcDateCreated: dateUtils.utcNowDateTime()
+ };
+ }
+
+ createClone(parentNoteId, notePosition) {
+ return new Branch({
+ noteId: this.noteId,
+ parentNoteId: parentNoteId,
+ notePosition: notePosition,
+ prefix: this.prefix,
+ isExpanded: this.isExpanded
+ });
+ }
+}
+
+module.exports = Branch;
+
+
+
+
+
+
+
+
+
+
+ Modules Classes
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/backend_api/becca_entities_note.js.html b/docs/backend_api/becca_entities_note.js.html
new file mode 100644
index 000000000..888251975
--- /dev/null
+++ b/docs/backend_api/becca_entities_note.js.html
@@ -0,0 +1,1179 @@
+
+
+
+
+ JSDoc: Source: becca/entities/note.js
+
+
+
+
+
+
+
+
+
+
+
+
+
Source: becca/entities/note.js
+
+
+
+
+
+
+
+
+ "use strict";
+
+const protectedSessionService = require('../../services/protected_session');
+const log = require('../../services/log');
+const sql = require('../../services/sql');
+const utils = require('../../services/utils');
+const dateUtils = require('../../services/date_utils');
+const entityChangesService = require('../../services/entity_changes');
+const AbstractEntity = require("./abstract_entity.js");
+const NoteRevision = require("./note_revision.js");
+
+const LABEL = 'label';
+const RELATION = 'relation';
+
+class Note extends AbstractEntity {
+ static get entityName() { return "notes"; }
+ static get primaryKeyName() { return "noteId"; }
+ static get hashedProperties() { return ["noteId", "title", "isProtected", "type", "mime"]; }
+
+ constructor(row) {
+ super();
+
+ if (!row) {
+ return;
+ }
+
+ this.updateFromRow(row);
+ this.init();
+ }
+
+ updateFromRow(row) {
+ this.update([
+ row.noteId,
+ row.title,
+ row.type,
+ row.mime,
+ row.isProtected,
+ row.dateCreated,
+ row.dateModified,
+ row.utcDateCreated,
+ row.utcDateModified
+ ]);
+ }
+
+ update([noteId, title, type, mime, isProtected, dateCreated, dateModified, utcDateCreated, utcDateModified]) {
+ // ------ Database persisted attributes ------
+
+ /** @param {string} */
+ this.noteId = noteId;
+ /** @param {string} */
+ this.title = title;
+ /** @param {boolean} */
+ this.isProtected = !!isProtected;
+ /** @param {string} */
+ this.type = type;
+ /** @param {string} */
+ this.mime = mime;
+ /** @param {string} */
+ this.dateCreated = dateCreated || dateUtils.localNowDateTime();
+ /** @param {string} */
+ this.dateModified = dateModified;
+ /** @param {string} */
+ this.utcDateCreated = utcDateCreated || dateUtils.utcNowDateTime();
+ /** @param {string} */
+ this.utcDateModified = utcDateModified;
+
+ // ------ Derived attributes ------
+
+ /** @param {boolean} */
+ this.isDecrypted = !this.isProtected;
+
+ this.decrypt();
+
+ /** @param {string|null} */
+ this.flatTextCache = null;
+
+ return this;
+ }
+
+ init() {
+ /** @param {Branch[]} */
+ this.parentBranches = [];
+ /** @param {Note[]} */
+ this.parents = [];
+ /** @param {Note[]} */
+ this.children = [];
+ /** @param {Attribute[]} */
+ this.ownedAttributes = [];
+
+ /** @param {Attribute[]|null} */
+ this.__attributeCache = null;
+ /** @param {Attribute[]|null} */
+ this.inheritableAttributeCache = null;
+
+ /** @param {Attribute[]} */
+ this.targetRelations = [];
+
+ this.becca.notes[this.noteId] = this;
+
+ /** @param {Note[]|null} */
+ this.ancestorCache = null;
+
+ // following attributes are filled during searching from database
+
+ /** @param {int} size of the content in bytes */
+ this.contentSize = null;
+ /** @param {int} size of the content and note revision contents in bytes */
+ this.noteSize = null;
+ /** @param {int} number of note revisions for this note */
+ this.revisionCount = null;
+ }
+
+ isContentAvailable() {
+ return !this.noteId // new note which was not encrypted yet
+ || !this.isProtected
+ || protectedSessionService.isProtectedSessionAvailable()
+ }
+
+ getParentBranches() {
+ return this.parentBranches;
+ }
+
+ getBranches() {
+ return this.parentBranches;
+ }
+
+ getParentNotes() {
+ return this.parents;
+ }
+
+ getChildNotes() {
+ return this.children;
+ }
+
+ hasChildren() {
+ return this.children && this.children.length > 0;
+ }
+
+ getChildBranches() {
+ return this.children.map(childNote => this.becca.getBranchFromChildAndParent(childNote.noteId, this.noteId));
+ }
+
+ /*
+ * Note content has quite special handling - it's not a separate entity, but a lazily loaded
+ * part of Note entity with it's own sync. Reasons behind this hybrid design has been:
+ *
+ * - content can be quite large and it's not necessary to load it / fill memory for any note access even if we don't need a content, especially for bulk operations like search
+ * - changes in the note metadata or title should not trigger note content sync (so we keep separate utcDateModified and entity changes records)
+ * - but to the user note content and title changes are one and the same - single dateModified (so all changes must go through Note and content is not a separate entity)
+ */
+
+ /** @returns {*} */
+ getContent(silentNotFoundError = false) {
+ const row = sql.getRow(`SELECT content FROM note_contents WHERE noteId = ?`, [this.noteId]);
+
+ if (!row) {
+ if (silentNotFoundError) {
+ return undefined;
+ }
+ else {
+ throw new Error("Cannot find note content for noteId=" + this.noteId);
+ }
+ }
+
+ let content = row.content;
+
+ if (this.isProtected) {
+ if (protectedSessionService.isProtectedSessionAvailable()) {
+ content = content === null ? null : protectedSessionService.decrypt(content);
+ }
+ else {
+ content = "";
+ }
+ }
+
+ if (this.isStringNote()) {
+ return content === null
+ ? ""
+ : content.toString("UTF-8");
+ }
+ else {
+ return content;
+ }
+ }
+
+ /** @returns {{contentLength, dateModified, utcDateModified}} */
+ getContentMetadata() {
+ return sql.getRow(`
+ SELECT
+ LENGTH(content) AS contentLength,
+ dateModified,
+ utcDateModified
+ FROM note_contents
+ WHERE noteId = ?`, [this.noteId]);
+ }
+
+ /** @returns {*} */
+ getJsonContent() {
+ const content = this.getContent();
+
+ if (!content || !content.trim()) {
+ return null;
+ }
+
+ return JSON.parse(content);
+ }
+
+ setContent(content, ignoreMissingProtectedSession = false) {
+ if (content === null || content === undefined) {
+ throw new Error(`Cannot set null content to note ${this.noteId}`);
+ }
+
+ if (this.isStringNote()) {
+ content = content.toString();
+ }
+ else {
+ content = Buffer.isBuffer(content) ? content : Buffer.from(content);
+ }
+
+ const pojo = {
+ noteId: this.noteId,
+ content: content,
+ dateModified: dateUtils.localNowDateTime(),
+ utcDateModified: dateUtils.utcNowDateTime()
+ };
+
+ if (this.isProtected) {
+ if (protectedSessionService.isProtectedSessionAvailable()) {
+ pojo.content = protectedSessionService.encrypt(pojo.content);
+ }
+ else if (!ignoreMissingProtectedSession) {
+ throw new Error(`Cannot update content of noteId=${this.noteId} since we're out of protected session.`);
+ }
+ }
+
+ sql.upsert("note_contents", "noteId", pojo);
+
+ const hash = utils.hash(this.noteId + "|" + pojo.content.toString());
+
+ entityChangesService.addEntityChange({
+ entityName: 'note_contents',
+ entityId: this.noteId,
+ hash: hash,
+ isErased: false,
+ utcDateChanged: pojo.utcDateModified,
+ isSynced: true
+ });
+ }
+
+ setJsonContent(content) {
+ this.setContent(JSON.stringify(content, null, '\t'));
+ }
+
+ /** @returns {boolean} true if this note is the root of the note tree. Root note has "root" noteId */
+ isRoot() {
+ return this.noteId === 'root';
+ }
+
+ /** @returns {boolean} true if this note is of application/json content type */
+ isJson() {
+ return this.mime === "application/json";
+ }
+
+ /** @returns {boolean} true if this note is JavaScript (code or attachment) */
+ isJavaScript() {
+ return (this.type === "code" || this.type === "file")
+ && (this.mime.startsWith("application/javascript")
+ || this.mime === "application/x-javascript"
+ || this.mime === "text/javascript");
+ }
+
+ /** @returns {boolean} true if this note is HTML */
+ isHtml() {
+ return ["code", "file", "render"].includes(this.type)
+ && this.mime === "text/html";
+ }
+
+ /** @returns {boolean} true if the note has string content (not binary) */
+ isStringNote() {
+ return utils.isStringNote(this.type, this.mime);
+ }
+
+ /** @returns {string|null} JS script environment - either "frontend" or "backend" */
+ getScriptEnv() {
+ if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) {
+ return "frontend";
+ }
+
+ if (this.type === 'render') {
+ return "frontend";
+ }
+
+ if (this.isJavaScript() && this.mime.endsWith('env=backend')) {
+ return "backend";
+ }
+
+ return null;
+ }
+
+ /**
+ * @param {string} [type] - (optional) attribute type to filter
+ * @param {string} [name] - (optional) attribute name to filter
+ * @returns {Attribute[]} all note's attributes, including inherited ones
+ */
+ getAttributes(type, name) {
+ this.__getAttributes([]);
+
+ if (type && name) {
+ return this.__attributeCache.filter(attr => attr.type === type && attr.name === name);
+ }
+ else if (type) {
+ return this.__attributeCache.filter(attr => attr.type === type);
+ }
+ else if (name) {
+ return this.__attributeCache.filter(attr => attr.name === name);
+ }
+ else {
+ return this.__attributeCache.slice();
+ }
+ }
+
+ __getAttributes(path) {
+ if (path.includes(this.noteId)) {
+ return [];
+ }
+
+ if (!this.__attributeCache) {
+ const parentAttributes = this.ownedAttributes.slice();
+ const newPath = [...path, this.noteId];
+
+ if (this.noteId !== 'root') {
+ for (const parentNote of this.parents) {
+ parentAttributes.push(...parentNote.__getInheritableAttributes(newPath));
+ }
+ }
+
+ const templateAttributes = [];
+
+ for (const ownedAttr of parentAttributes) { // parentAttributes so we process also inherited templates
+ if (ownedAttr.type === 'relation' && ownedAttr.name === 'template') {
+ const templateNote = this.becca.notes[ownedAttr.value];
+
+ if (templateNote) {
+ templateAttributes.push(...templateNote.__getAttributes(newPath));
+ }
+ }
+ }
+
+ this.__attributeCache = [];
+
+ const addedAttributeIds = new Set();
+
+ for (const attr of parentAttributes.concat(templateAttributes)) {
+ if (!addedAttributeIds.has(attr.attributeId)) {
+ addedAttributeIds.add(attr.attributeId);
+
+ this.__attributeCache.push(attr);
+ }
+ }
+
+ this.inheritableAttributeCache = [];
+
+ for (const attr of this.__attributeCache) {
+ if (attr.isInheritable) {
+ this.inheritableAttributeCache.push(attr);
+ }
+ }
+ }
+
+ return this.__attributeCache;
+ }
+
+ /** @return {Attribute[]} */
+ __getInheritableAttributes(path) {
+ if (path.includes(this.noteId)) {
+ return [];
+ }
+
+ if (!this.inheritableAttributeCache) {
+ this.__getAttributes(path); // will refresh also this.inheritableAttributeCache
+ }
+
+ return this.inheritableAttributeCache;
+ }
+
+ hasAttribute(type, name) {
+ return !!this.getAttributes().find(attr => attr.type === type && attr.name === name);
+ }
+
+ getAttributeCaseInsensitive(type, name, value) {
+ name = name.toLowerCase();
+ value = value ? value.toLowerCase() : null;
+
+ return this.getAttributes().find(
+ attr => attr.type === type
+ && attr.name.toLowerCase() === name
+ && (!value || attr.value.toLowerCase() === value));
+ }
+
+ getRelationTarget(name) {
+ const relation = this.getAttributes().find(attr => attr.type === 'relation' && attr.name === name);
+
+ return relation ? relation.targetNote : null;
+ }
+
+ /**
+ * @param {string} name - label name
+ * @returns {boolean} true if label exists (including inherited)
+ */
+ hasLabel(name) { return this.hasAttribute(LABEL, name); }
+
+ /**
+ * @param {string} name - label name
+ * @returns {boolean} true if label exists (excluding inherited)
+ */
+ hasOwnedLabel(name) { return this.hasOwnedAttribute(LABEL, name); }
+
+ /**
+ * @param {string} name - relation name
+ * @returns {boolean} true if relation exists (including inherited)
+ */
+ hasRelation(name) { return this.hasAttribute(RELATION, name); }
+
+ /**
+ * @param {string} name - relation name
+ * @returns {boolean} true if relation exists (excluding inherited)
+ */
+ hasOwnedRelation(name) { return this.hasOwnedAttribute(RELATION, name); }
+
+ /**
+ * @param {string} name - label name
+ * @returns {Attribute|null} label if it exists, null otherwise
+ */
+ getLabel(name) { return this.getAttribute(LABEL, name); }
+
+ /**
+ * @param {string} name - label name
+ * @returns {Attribute|null} label if it exists, null otherwise
+ */
+ getOwnedLabel(name) { return this.getOwnedAttribute(LABEL, name); }
+
+ /**
+ * @param {string} name - relation name
+ * @returns {Attribute|null} relation if it exists, null otherwise
+ */
+ getRelation(name) { return this.getAttribute(RELATION, name); }
+
+ /**
+ * @param {string} name - relation name
+ * @returns {Attribute|null} relation if it exists, null otherwise
+ */
+ getOwnedRelation(name) { return this.getOwnedAttribute(RELATION, name); }
+
+ /**
+ * @param {string} name - label name
+ * @returns {string|null} label value if label exists, null otherwise
+ */
+ getLabelValue(name) { return this.getAttributeValue(LABEL, name); }
+
+ /**
+ * @param {string} name - label name
+ * @returns {string|null} label value if label exists, null otherwise
+ */
+ getOwnedLabelValue(name) { return this.getOwnedAttributeValue(LABEL, name); }
+
+ /**
+ * @param {string} name - relation name
+ * @returns {string|null} relation value if relation exists, null otherwise
+ */
+ getRelationValue(name) { return this.getAttributeValue(RELATION, name); }
+
+ /**
+ * @param {string} name - relation name
+ * @returns {string|null} relation value if relation exists, null otherwise
+ */
+ getOwnedRelationValue(name) { return this.getOwnedAttributeValue(RELATION, name); }
+
+ /**
+ * @param {string} type - attribute type (label, relation, etc.)
+ * @param {string} name - attribute name
+ * @returns {boolean} true if note has an attribute with given type and name (excluding inherited)
+ */
+ hasOwnedAttribute(type, name) {
+ return !!this.getOwnedAttribute(type, name);
+ }
+
+ /**
+ * @param {string} type - attribute type (label, relation, etc.)
+ * @param {string} name - attribute name
+ * @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.
+ */
+ getAttribute(type, name) {
+ const attributes = this.getAttributes();
+
+ return attributes.find(attr => attr.type === type && attr.name === name);
+ }
+
+ /**
+ * @param {string} type - attribute type (label, relation, etc.)
+ * @param {string} name - attribute name
+ * @returns {string|null} attribute value of given type and name or null if no such attribute exists.
+ */
+ getAttributeValue(type, name) {
+ const attr = this.getAttribute(type, name);
+
+ return attr ? attr.value : null;
+ }
+
+ /**
+ * @param {string} type - attribute type (label, relation, etc.)
+ * @param {string} name - attribute name
+ * @returns {string|null} attribute value of given type and name or null if no such attribute exists.
+ */
+ getOwnedAttributeValue(type, name) {
+ const attr = this.getOwnedAttribute(type, name);
+
+ return attr ? attr.value : null;
+ }
+
+ /**
+ * @param {string} [name] - label name to filter
+ * @returns {Attribute[]} all note's labels (attributes with type label), including inherited ones
+ */
+ getLabels(name) {
+ return this.getAttributes(LABEL, name);
+ }
+
+ /**
+ * @param {string} [name] - label name to filter
+ * @returns {string[]} all note's label values, including inherited ones
+ */
+ getLabelValues(name) {
+ return this.getLabels(name).map(l => l.value);
+ }
+
+ /**
+ * @param {string} [name] - label name to filter
+ * @returns {Attribute[]} all note's labels (attributes with type label), excluding inherited ones
+ */
+ getOwnedLabels(name) {
+ return this.getOwnedAttributes(LABEL, name);
+ }
+
+ /**
+ * @param {string} [name] - label name to filter
+ * @returns {string[]} all note's label values, excluding inherited ones
+ */
+ getOwnedLabelValues(name) {
+ return this.getOwnedAttributes(LABEL, name).map(l => l.value);
+ }
+
+ /**
+ * @param {string} [name] - relation name to filter
+ * @returns {Attribute[]} all note's relations (attributes with type relation), including inherited ones
+ */
+ getRelations(name) {
+ return this.getAttributes(RELATION, name);
+ }
+
+ /**
+ * @param {string} [name] - relation name to filter
+ * @returns {Attribute[]} all note's relations (attributes with type relation), excluding inherited ones
+ */
+ getOwnedRelations(name) {
+ return this.getOwnedAttributes(RELATION, name);
+ }
+
+ /**
+ * @param {string} [type] - (optional) attribute type to filter
+ * @param {string} [name] - (optional) attribute name to filter
+ * @returns {Attribute[]} note's "owned" attributes - excluding inherited ones
+ */
+ getOwnedAttributes(type, name) {
+ // it's a common mistake to include # or ~ into attribute name
+ if (name && ["#", "~"].includes(name[0])) {
+ name = name.substr(1);
+ }
+
+ if (type && name) {
+ return this.ownedAttributes.filter(attr => attr.type === type && attr.name === name);
+ }
+ else if (type) {
+ return this.ownedAttributes.filter(attr => attr.type === type);
+ }
+ else if (name) {
+ return this.ownedAttributes.filter(attr => attr.name === name);
+ }
+ else {
+ return this.ownedAttributes.slice();
+ }
+ }
+
+ /**
+ * @returns {Attribute} attribute belonging to this specific note (excludes inherited attributes)
+ *
+ * This method can be significantly faster than the getAttribute()
+ */
+ getOwnedAttribute(type, name) {
+ const attrs = this.getOwnedAttributes(type, name);
+
+ return attrs.length > 0 ? attrs[0] : null;
+ }
+
+ get isArchived() {
+ return this.hasAttribute('label', 'archived');
+ }
+
+ hasInheritableOwnedArchivedLabel() {
+ return !!this.ownedAttributes.find(attr => attr.type === 'label' && attr.name === 'archived' && attr.isInheritable);
+ }
+
+ // will sort the parents so that non-search & non-archived are first and archived at the end
+ // this is done so that non-search & non-archived paths are always explored as first when looking for note path
+ resortParents() {
+ this.parentBranches.sort((a, b) =>
+ a.branchId.startsWith('virt-')
+ || a.parentNote.hasInheritableOwnedArchivedLabel() ? 1 : -1);
+
+ this.parents = this.parentBranches.map(branch => branch.parentNote);
+ }
+
+ /**
+ * This is used for:
+ * - fast searching
+ * - note similarity evaluation
+ *
+ * @return {string} - returns flattened textual representation of note, prefixes and attributes
+ */
+ getFlatText() {
+ if (!this.flatTextCache) {
+ this.flatTextCache = this.noteId + ' ' + this.type + ' ' + this.mime + ' ';
+
+ for (const branch of this.parentBranches) {
+ if (branch.prefix) {
+ this.flatTextCache += branch.prefix + ' ';
+ }
+ }
+
+ this.flatTextCache += this.title + ' ';
+
+ for (const attr of this.getAttributes()) {
+ // it's best to use space as separator since spaces are filtered from the search string by the tokenization into words
+ this.flatTextCache += (attr.type === 'label' ? '#' : '~') + attr.name;
+
+ if (attr.value) {
+ this.flatTextCache += '=' + attr.value;
+ }
+
+ this.flatTextCache += ' ';
+ }
+
+ this.flatTextCache = utils.normalize(this.flatTextCache);
+ }
+
+ return this.flatTextCache;
+ }
+
+ invalidateThisCache() {
+ this.flatTextCache = null;
+
+ this.__attributeCache = null;
+ this.inheritableAttributeCache = null;
+ this.ancestorCache = null;
+ }
+
+ invalidateSubTree(path = []) {
+ if (path.includes(this.noteId)) {
+ return;
+ }
+
+ this.invalidateThisCache();
+
+ if (this.children.length || this.targetRelations.length) {
+ path = [...path, this.noteId];
+ }
+
+ for (const childNote of this.children) {
+ childNote.invalidateSubTree(path);
+ }
+
+ for (const targetRelation of this.targetRelations) {
+ if (targetRelation.name === 'template') {
+ const note = targetRelation.note;
+
+ if (note) {
+ note.invalidateSubTree(path);
+ }
+ }
+ }
+ }
+
+ invalidateSubtreeFlatText() {
+ this.flatTextCache = null;
+
+ for (const childNote of this.children) {
+ childNote.invalidateSubtreeFlatText();
+ }
+
+ for (const targetRelation of this.targetRelations) {
+ if (targetRelation.name === 'template') {
+ const note = targetRelation.note;
+
+ if (note) {
+ note.invalidateSubtreeFlatText();
+ }
+ }
+ }
+ }
+
+ getRelationDefinitions() {
+ return this.getLabels()
+ .filter(l => l.name.startsWith("relation:"));
+ }
+
+ getLabelDefinitions() {
+ return this.getLabels()
+ .filter(l => l.name.startsWith("relation:"));
+ }
+
+ isTemplate() {
+ return !!this.targetRelations.find(rel => rel.name === 'template');
+ }
+
+ /** @return {Note[]} */
+ getSubtreeNotesIncludingTemplated() {
+ const arr = [[this]];
+
+ for (const childNote of this.children) {
+ arr.push(childNote.getSubtreeNotesIncludingTemplated());
+ }
+
+ for (const targetRelation of this.targetRelations) {
+ if (targetRelation.name === 'template') {
+ const note = targetRelation.note;
+
+ if (note) {
+ arr.push(note.getSubtreeNotesIncludingTemplated());
+ }
+ }
+ }
+
+ return arr.flat();
+ }
+
+ /** @return {Note[]} */
+ getSubtreeNotes(includeArchived = true) {
+ const noteSet = new Set();
+
+ function addSubtreeNotesInner(note) {
+ if (!includeArchived && note.isArchived) {
+ return;
+ }
+
+ noteSet.add(note);
+
+ for (const childNote of note.children) {
+ addSubtreeNotesInner(childNote);
+ }
+ }
+
+ addSubtreeNotesInner(this);
+
+ return Array.from(noteSet);
+ }
+
+ /** @return {String[]} */
+ getSubtreeNoteIds() {
+ return this.getSubtreeNotes().map(note => note.noteId);
+ }
+
+ getDescendantNoteIds() {
+ return this.getSubtreeNoteIds();
+ }
+
+ get parentCount() {
+ return this.parents.length;
+ }
+
+ get childrenCount() {
+ return this.children.length;
+ }
+
+ get labelCount() {
+ return this.getAttributes().filter(attr => attr.type === 'label').length;
+ }
+
+ get ownedLabelCount() {
+ return this.ownedAttributes.filter(attr => attr.type === 'label').length;
+ }
+
+ get relationCount() {
+ return this.getAttributes().filter(attr => attr.type === 'relation' && !attr.isAutoLink()).length;
+ }
+
+ get relationCountIncludingLinks() {
+ return this.getAttributes().filter(attr => attr.type === 'relation').length;
+ }
+
+ get ownedRelationCount() {
+ return this.ownedAttributes.filter(attr => attr.type === 'relation' && !attr.isAutoLink()).length;
+ }
+
+ get ownedRelationCountIncludingLinks() {
+ return this.ownedAttributes.filter(attr => attr.type === 'relation').length;
+ }
+
+ get targetRelationCount() {
+ return this.targetRelations.filter(attr => !attr.isAutoLink()).length;
+ }
+
+ get targetRelationCountIncludingLinks() {
+ return this.targetRelations.length;
+ }
+
+ get attributeCount() {
+ return this.getAttributes().length;
+ }
+
+ get ownedAttributeCount() {
+ return this.getAttributes().length;
+ }
+
+ getAncestors() {
+ if (!this.ancestorCache) {
+ const noteIds = new Set();
+ this.ancestorCache = [];
+
+ for (const parent of this.parents) {
+ if (!noteIds.has(parent.noteId)) {
+ this.ancestorCache.push(parent);
+ noteIds.add(parent.noteId);
+ }
+
+ for (const ancestorNote of parent.getAncestors()) {
+ if (!noteIds.has(ancestorNote.noteId)) {
+ this.ancestorCache.push(ancestorNote);
+ noteIds.add(ancestorNote.noteId);
+ }
+ }
+ }
+ }
+
+ return this.ancestorCache;
+ }
+
+ getTargetRelations() {
+ return this.targetRelations;
+ }
+
+ /** @return {Note[]} - returns only notes which are templated, does not include their subtrees
+ * in effect returns notes which are influenced by note's non-inheritable attributes */
+ getTemplatedNotes() {
+ const arr = [this];
+
+ for (const targetRelation of this.targetRelations) {
+ if (targetRelation.name === 'template') {
+ const note = targetRelation.note;
+
+ if (note) {
+ arr.push(note);
+ }
+ }
+ }
+
+ return arr;
+ }
+
+ getDistanceToAncestor(ancestorNoteId) {
+ if (this.noteId === ancestorNoteId) {
+ return 0;
+ }
+
+ let minDistance = 999999;
+
+ for (const parent of this.parents) {
+ minDistance = Math.min(minDistance, parent.getDistanceToAncestor(ancestorNoteId) + 1);
+ }
+
+ return minDistance;
+ }
+
+ getNoteRevisions() {
+ return sql.getRows("SELECT * FROM note_revisions WHERE noteId = ?", [this.noteId])
+ .map(row => new NoteRevision(row));
+ }
+
+ /**
+ * @return {string[][]} - array of notePaths (each represented by array of noteIds constituting the particular note path)
+ */
+ getAllNotePaths() {
+ if (this.noteId === 'root') {
+ return [['root']];
+ }
+
+ const notePaths = [];
+
+ for (const parentNote of this.getParentNotes()) {
+ for (const parentPath of parentNote.getAllNotePaths()) {
+ parentPath.push(this.noteId);
+ notePaths.push(parentPath);
+ }
+ }
+
+ return notePaths;
+ }
+
+ /**
+ * @param ancestorNoteId
+ * @return {boolean} - true if ancestorNoteId occurs in at least one of the note's paths
+ */
+ isDescendantOfNote(ancestorNoteId) {
+ const notePaths = this.getAllNotePaths();
+
+ return notePaths.some(path => path.includes(ancestorNoteId));
+ }
+
+ /**
+ * Update's given attribute's value or creates it if it doesn't exist
+ *
+ * @param {string} type - attribute type (label, relation, etc.)
+ * @param {string} name - attribute name
+ * @param {string} [value] - attribute value (optional)
+ */
+ setAttribute(type, name, value) {
+ const attributes = this.getOwnedAttributes();
+ const attr = attributes.find(attr => attr.type === type && attr.name === name);
+
+ value = value !== null && value !== undefined ? value.toString() : "";
+
+ if (attr) {
+ if (attr.value !== value) {
+ attr.value = value;
+ attr.save();
+ }
+ }
+ else {
+ const Attribute = require("./attribute.js");
+
+ new Attribute({
+ noteId: this.noteId,
+ type: type,
+ name: name,
+ value: value
+ }).save();
+ }
+ }
+
+ /**
+ * Removes given attribute name-value pair if it exists.
+ *
+ * @param {string} type - attribute type (label, relation, etc.)
+ * @param {string} name - attribute name
+ * @param {string} [value] - attribute value (optional)
+ */
+ removeAttribute(type, name, value) {
+ const attributes = this.getOwnedAttributes();
+
+ for (const attribute of attributes) {
+ if (attribute.type === type && attribute.name === name && (value === undefined || value === attribute.value)) {
+ attribute.markAsDeleted();
+ }
+ }
+ }
+
+ /**
+ * @return {Attribute}
+ */
+ addAttribute(type, name, value = "", isInheritable = false, position = 1000) {
+ const Attribute = require("./attribute.js");
+
+ return new Attribute({
+ noteId: this.noteId,
+ type: type,
+ name: name,
+ value: value,
+ isInheritable: isInheritable,
+ position: position
+ }).save();
+ }
+
+ addLabel(name, value = "", isInheritable = false) {
+ return this.addAttribute(LABEL, name, value, isInheritable);
+ }
+
+ addRelation(name, targetNoteId, isInheritable = false) {
+ return this.addAttribute(RELATION, name, targetNoteId, isInheritable);
+ }
+
+ /**
+ * Based on enabled, attribute is either set or removed.
+ *
+ * @param {string} type - attribute type ('relation', 'label' etc.)
+ * @param {boolean} enabled - toggle On or Off
+ * @param {string} name - attribute name
+ * @param {string} [value] - attribute value (optional)
+ */
+ toggleAttribute(type, enabled, name, value) {
+ if (enabled) {
+ this.setAttribute(type, name, value);
+ }
+ else {
+ this.removeAttribute(type, name, value);
+ }
+ }
+
+ /**
+ * Based on enabled, label is either set or removed.
+ *
+ * @param {boolean} enabled - toggle On or Off
+ * @param {string} name - label name
+ * @param {string} [value] - label value (optional)
+ */
+ toggleLabel(enabled, name, value) { return this.toggleAttribute(LABEL, enabled, name, value); }
+
+ /**
+ * Based on enabled, relation is either set or removed.
+ *
+ * @param {boolean} enabled - toggle On or Off
+ * @param {string} name - relation name
+ * @param {string} [value] - relation value (noteId)
+ */
+ toggleRelation(enabled, name, value) { return this.toggleAttribute(RELATION, enabled, name, value); }
+
+ /**
+ * Update's given label's value or creates it if it doesn't exist
+ *
+ * @param {string} name - label name
+ * @param {string} [value] - label value
+ */
+ setLabel(name, value) { return this.setAttribute(LABEL, name, value); }
+
+ /**
+ * Update's given relation's value or creates it if it doesn't exist
+ *
+ * @param {string} name - relation name
+ * @param {string} value - relation value (noteId)
+ */
+ setRelation(name, value) { return this.setAttribute(RELATION, name, value); }
+
+ /**
+ * Remove label name-value pair, if it exists.
+ *
+ * @param {string} name - label name
+ * @param {string} [value] - label value
+ */
+ removeLabel(name, value) { return this.removeAttribute(LABEL, name, value); }
+
+ /**
+ * Remove relation name-value pair, if it exists.
+ *
+ * @param {string} name - relation name
+ * @param {string} [value] - relation value (noteId)
+ */
+ removeRelation(name, value) { return this.removeAttribute(RELATION, name, value); }
+
+ searchNotesInSubtree(searchString) {
+ const searchService = require("../../services/search/services/search");
+
+ return searchService.searchNotes(searchString);
+ }
+
+ searchNoteInSubtree(searchString) {
+ return this.searchNotesInSubtree(searchString)[0];
+ }
+
+ cloneTo(parentNoteId) {
+ const cloningService = require("../../services/cloning");
+
+ const branch = this.becca.getNote(parentNoteId).getParentBranches()[0];
+
+ return cloningService.cloneNoteToParent(this.noteId, branch.branchId);
+ }
+
+ decrypt() {
+ if (this.isProtected && !this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) {
+ try {
+ this.title = protectedSessionService.decryptString(this.title);
+
+ this.isDecrypted = true;
+ }
+ catch (e) {
+ log.error(`Could not decrypt note ${this.noteId}: ${e.message} ${e.stack}`);
+ }
+ }
+ }
+
+ beforeSaving() {
+ super.beforeSaving();
+
+ this.becca.notes[this.noteId] = this;
+
+ this.dateModified = dateUtils.localNowDateTime();
+ this.utcDateModified = dateUtils.utcNowDateTime();
+ }
+
+ getPojo() {
+ return {
+ noteId: this.noteId,
+ title: this.title,
+ isProtected: this.isProtected,
+ type: this.type,
+ mime: this.mime,
+ isDeleted: false,
+ dateCreated: this.dateCreated,
+ dateModified: this.dateModified,
+ utcDateCreated: this.utcDateCreated,
+ utcDateModified: this.utcDateModified
+ };
+ }
+
+ getPojoToSave() {
+ const pojo = this.getPojo();
+
+ if (pojo.isProtected) {
+ if (this.isDecrypted) {
+ pojo.title = protectedSessionService.encrypt(pojo.title);
+ }
+ else {
+ // updating protected note outside of protected session means we will keep original ciphertexts
+ delete pojo.title;
+ }
+ }
+
+ return pojo;
+ }
+}
+
+module.exports = Note;
+
+
+
+
+
+
+
+
+
+
+ Modules Classes
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/backend_api/becca_entities_note_revision.js.html b/docs/backend_api/becca_entities_note_revision.js.html
new file mode 100644
index 000000000..63a014b97
--- /dev/null
+++ b/docs/backend_api/becca_entities_note_revision.js.html
@@ -0,0 +1,242 @@
+
+
+
+
+ JSDoc: Source: becca/entities/note_revision.js
+
+
+
+
+
+
+
+
+
+
+
+
+
Source: becca/entities/note_revision.js
+
+
+
+
+
+
+
+
+ "use strict";
+
+const protectedSessionService = require('../../services/protected_session');
+const utils = require('../../services/utils');
+const sql = require('../../services/sql');
+const dateUtils = require('../../services/date_utils');
+const becca = require('../becca.js');
+const entityChangesService = require('../../services/entity_changes');
+const AbstractEntity = require("./abstract_entity.js");
+
+/**
+ * NoteRevision represents snapshot of note's title and content at some point in the past. It's used for seamless note versioning.
+ */
+class NoteRevision extends AbstractEntity {
+ static get entityName() { return "note_revisions"; }
+ static get primaryKeyName() { return "noteRevisionId"; }
+ static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "isProtected", "dateLastEdited", "dateCreated", "utcDateLastEdited", "utcDateCreated", "utcDateModified"]; }
+
+ constructor(row) {
+ super();
+
+ /** @param {string} */
+ this.noteRevisionId = row.noteRevisionId;
+ /** @param {string} */
+ this.noteId = row.noteId;
+ /** @param {string} */
+ this.type = row.type;
+ /** @param {string} */
+ this.mime = row.mime;
+ /** @param {boolean} */
+ this.isProtected = !!row.isProtected;
+ /** @param {string} */
+ this.title = row.title;
+ /** @param {string} */
+ this.dateLastEdited = row.dateLastEdited;
+ /** @param {string} */
+ this.dateCreated = row.dateCreated;
+ /** @param {string} */
+ this.utcDateLastEdited = row.utcDateLastEdited;
+ /** @param {string} */
+ this.utcDateCreated = row.utcDateCreated;
+ /** @param {string} */
+ this.utcDateModified = row.utcDateModified;
+ /** @param {number} */
+ this.contentLength = row.contentLength;
+
+ if (this.isProtected) {
+ if (protectedSessionService.isProtectedSessionAvailable()) {
+ this.title = protectedSessionService.decryptString(this.title);
+ }
+ else {
+ this.title = "[Protected]";
+ }
+ }
+ }
+
+ getNote() {
+ return becca.notes[this.noteId];
+ }
+
+ /** @returns {boolean} true if the note has string content (not binary) */
+ isStringNote() {
+ return utils.isStringNote(this.type, this.mime);
+ }
+
+ /*
+ * Note revision content has quite special handling - it's not a separate entity, but a lazily loaded
+ * part of NoteRevision entity with it's own sync. Reason behind this hybrid design is that
+ * 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.
+ *
+ * This is the same approach as is used for Note's content.
+ */
+
+ /** @returns {*} */
+ getContent(silentNotFoundError = false) {
+ const res = sql.getRow(`SELECT content FROM note_revision_contents WHERE noteRevisionId = ?`, [this.noteRevisionId]);
+
+ if (!res) {
+ if (silentNotFoundError) {
+ return undefined;
+ }
+ else {
+ throw new Error("Cannot find note revision content for noteRevisionId=" + this.noteRevisionId);
+ }
+ }
+
+ let content = res.content;
+
+ if (this.isProtected) {
+ if (protectedSessionService.isProtectedSessionAvailable()) {
+ content = protectedSessionService.decrypt(content);
+ }
+ else {
+ content = "";
+ }
+ }
+
+ if (this.isStringNote()) {
+ return content === null
+ ? ""
+ : content.toString("UTF-8");
+ }
+ else {
+ return content;
+ }
+ }
+
+ setContent(content, ignoreMissingProtectedSession = false) {
+ const pojo = {
+ noteRevisionId: this.noteRevisionId,
+ content: content,
+ utcDateModified: dateUtils.utcNowDateTime()
+ };
+
+ if (this.isProtected) {
+ if (protectedSessionService.isProtectedSessionAvailable()) {
+ pojo.content = protectedSessionService.encrypt(pojo.content);
+ }
+ else if (!ignoreMissingProtectedSession) {
+ throw new Error(`Cannot update content of noteRevisionId=${this.noteRevisionId} since we're out of protected session.`);
+ }
+ }
+
+ sql.upsert("note_revision_contents", "noteRevisionId", pojo);
+
+ const hash = utils.hash(this.noteRevisionId + "|" + pojo.content.toString());
+
+ entityChangesService.addEntityChange({
+ entityName: 'note_revision_contents',
+ entityId: this.noteRevisionId,
+ hash: hash,
+ isErased: false,
+ utcDateChanged: this.getUtcDateChanged(),
+ isSynced: true
+ });
+ }
+
+ /** @returns {{contentLength, dateModified, utcDateModified}} */
+ getContentMetadata() {
+ return sql.getRow(`
+ SELECT
+ LENGTH(content) AS contentLength,
+ dateModified,
+ utcDateModified
+ FROM note_revision_contents
+ WHERE noteRevisionId = ?`, [this.noteRevisionId]);
+ }
+
+ beforeSaving() {
+ super.beforeSaving();
+
+ this.utcDateModified = dateUtils.utcNowDateTime();
+ }
+
+ getPojo() {
+ return {
+ noteRevisionId: this.noteRevisionId,
+ noteId: this.noteId,
+ type: this.type,
+ mime: this.mime,
+ isProtected: this.isProtected,
+ title: this.title,
+ dateLastEdited: this.dateLastEdited,
+ dateCreated: this.dateCreated,
+ utcDateLastEdited: this.utcDateLastEdited,
+ utcDateCreated: this.utcDateCreated,
+ utcDateModified: this.utcDateModified,
+ contentLength: this.contentLength
+ };
+ }
+
+ getPojoToSave() {
+ const pojo = this.getPojo();
+ delete pojo.contentLength; // not getting persisted
+
+ if (pojo.isProtected) {
+ if (protectedSessionService.isProtectedSessionAvailable()) {
+ pojo.title = protectedSessionService.encrypt(this.title);
+ }
+ else {
+ // updating protected note outside of protected session means we will keep original ciphertexts
+ delete pojo.title;
+ }
+ }
+
+ return pojo;
+ }
+}
+
+module.exports = NoteRevision;
+
+
+
+
+
+
+
+
+
+
+ Modules Classes
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/backend_api/becca_entities_option.js.html b/docs/backend_api/becca_entities_option.js.html
new file mode 100644
index 000000000..1a5c269d8
--- /dev/null
+++ b/docs/backend_api/becca_entities_option.js.html
@@ -0,0 +1,94 @@
+
+
+
+
+ JSDoc: Source: becca/entities/option.js
+
+
+
+
+
+
+
+
+
+
+
+
+
Source: becca/entities/option.js
+
+
+
+
+
+
+
+
+ "use strict";
+
+const dateUtils = require('../../services/date_utils.js');
+const AbstractEntity = require("./abstract_entity.js");
+
+/**
+ * Option represents name-value pair, either directly configurable by the user or some system property.
+ */
+class Option extends AbstractEntity {
+ static get entityName() { return "options"; }
+ static get primaryKeyName() { return "name"; }
+ static get hashedProperties() { return ["name", "value"]; }
+
+ constructor(row) {
+ super();
+
+ this.name = row.name;
+ this.value = row.value;
+ this.isSynced = !!row.isSynced;
+ this.utcDateModified = row.utcDateModified;
+
+ this.becca.options[this.name] = this;
+ }
+
+ beforeSaving() {
+ super.beforeSaving();
+
+ this.utcDateModified = dateUtils.utcNowDateTime();
+ }
+
+ getPojo() {
+ return {
+ name: this.name,
+ value: this.value,
+ isSynced: this.isSynced,
+ utcDateModified: this.utcDateModified,
+ // utcDateCreated is scheduled for removal so the value does not matter
+ utcDateCreated: dateUtils.utcNowDateTime()
+ }
+ }
+}
+
+module.exports = Option;
+
+
+
+
+
+
+
+
+
+
+ Modules Classes
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/backend_api/becca_entities_recent_note.js.html b/docs/backend_api/becca_entities_recent_note.js.html
new file mode 100644
index 000000000..ea9a1fae3
--- /dev/null
+++ b/docs/backend_api/becca_entities_recent_note.js.html
@@ -0,0 +1,81 @@
+
+
+
+
+ JSDoc: Source: becca/entities/recent_note.js
+
+
+
+
+
+
+
+
+
+
+
+
+
Source: becca/entities/recent_note.js
+
+
+
+
+
+
+
+
+ "use strict";
+
+const dateUtils = require('../../services/date_utils.js');
+const AbstractEntity = require("./abstract_entity.js");
+
+/**
+ * RecentNote represents recently visited note.
+ */
+class RecentNote extends AbstractEntity {
+ static get entityName() { return "recent_notes"; }
+ static get primaryKeyName() { return "noteId"; }
+
+ constructor(row) {
+ super();
+
+ this.noteId = row.noteId;
+ this.notePath = row.notePath;
+ this.utcDateCreated = row.utcDateCreated || dateUtils.utcNowDateTime();
+ }
+
+ getPojo() {
+ return {
+ noteId: this.noteId,
+ notePath: this.notePath,
+ utcDateCreated: this.utcDateCreated
+ }
+ }
+}
+
+module.exports = RecentNote;
+
+
+
+
+
+
+
+
+
+
+ Modules Classes
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/backend_api/global.html b/docs/backend_api/global.html
index 7bf9e2d7a..19416cf68 100644
--- a/docs/backend_api/global.html
+++ b/docs/backend_api/global.html
@@ -391,7 +391,7 @@
Source:
@@ -579,7 +579,7 @@
Source:
@@ -767,7 +767,7 @@
Source:
@@ -1053,7 +1053,7 @@
Source:
@@ -1083,13 +1083,13 @@
- Modules Classes
+ Modules Classes
diff --git a/docs/backend_api/index.html b/docs/backend_api/index.html
index 16d0a14a7..db1dc7202 100644
--- a/docs/backend_api/index.html
+++ b/docs/backend_api/index.html
@@ -50,13 +50,13 @@
- Modules Classes
+ Modules Classes
diff --git a/docs/backend_api/module-sql.html b/docs/backend_api/module-sql.html
index 290599ca1..e4ea35b11 100644
--- a/docs/backend_api/module-sql.html
+++ b/docs/backend_api/module-sql.html
@@ -208,7 +208,7 @@
Source:
@@ -388,7 +388,7 @@
Source:
@@ -590,7 +590,7 @@
Source:
@@ -792,7 +792,7 @@
Source:
@@ -994,7 +994,7 @@
Source:
@@ -1196,7 +1196,7 @@
Source:
@@ -1252,13 +1252,13 @@
- Modules Classes
+ Modules Classes
diff --git a/docs/backend_api/services_backend_script_api.js.html b/docs/backend_api/services_backend_script_api.js.html
index 8f44ab816..6bdededcb 100644
--- a/docs/backend_api/services_backend_script_api.js.html
+++ b/docs/backend_api/services_backend_script_api.js.html
@@ -34,14 +34,14 @@ const attributeService = require('./attributes');
const dateNoteService = require('./date_notes');
const treeService = require('./tree');
const config = require('./config');
-const repository = require('./repository');
const axios = require('axios');
const dayjs = require('dayjs');
const xml2js = require('xml2js');
const cloningService = require('./cloning');
const appInfo = require('./app_info');
const searchService = require('./search/services/search');
-const SearchContext = require("./search/search_context.js");
+const SearchContext = require("./search/search_context");
+const becca = require("../becca/becca");
/**
* This is the main backend API interface for scripts. It's published in the local "api" object.
@@ -86,39 +86,21 @@ function BackendScriptApi(currentNote, apiParams) {
* @param {string} noteId
* @returns {Note|null}
*/
- this.getNote = repository.getNote;
+ this.getNote = becca.getNote;
/**
* @method
* @param {string} branchId
* @returns {Branch|null}
*/
- this.getBranch = repository.getBranch;
+ this.getBranch = becca.getBranch;
/**
* @method
* @param {string} attributeId
* @returns {Attribute|null}
*/
- this.getAttribute = repository.getAttribute;
-
- /**
- * Retrieves first entity from the SQL's result set.
- *
- * @method
- * @param {string} SQL query
- * @param {Array.<?>} array of params
- * @returns {Entity|null}
- */
- this.getEntity = repository.getEntity;
-
- /**
- * @method
- * @param {string} SQL query
- * @param {Array.<?>} array of params
- * @returns {Entity[]}
- */
- this.getEntities = repository.getEntities;
+ this.getAttribute = becca.getAttribute;
/**
* This is a powerful search method - you can search by attributes and their values, e.g.:
@@ -141,7 +123,7 @@ function BackendScriptApi(currentNote, apiParams) {
const noteIds = searchService.findResultsWithQuery(query, new SearchContext(searchParams))
.map(sr => sr.noteId);
- return repository.getNotes(noteIds);
+ return becca.getNotes(noteIds);
};
/**
@@ -149,11 +131,12 @@ function BackendScriptApi(currentNote, apiParams) {
* "#dateModified =* MONTH AND #log". See full documentation for all options at: https://github.com/zadam/trilium/wiki/Search
*
* @method
- * @param {string} searchString
+ * @param {string} query
+ * @param {Object} [searchParams]
* @returns {Note|null}
*/
- this.searchForNote = searchString => {
- const notes = searchService.searchNoteEntities(searchString);
+ this.searchForNote = (query, searchParams = {}) => {
+ const notes = this.searchForNotes(query, searchParams);
return notes.length > 0 ? notes[0] : null;
};
@@ -301,7 +284,7 @@ function BackendScriptApi(currentNote, apiParams) {
extraOptions.parentNoteId = parentNoteId;
extraOptions.title = title;
- const parentNote = repository.getNote(parentNoteId);
+ const parentNote = becca.getNote(parentNoteId);
// code note type can be inherited, otherwise text is default
extraOptions.type = parentNote.type === 'code' ? 'code' : 'text';
@@ -397,7 +380,7 @@ function BackendScriptApi(currentNote, apiParams) {
* @method
* @param {string} parentNoteId - this note's child notes will be sorted
*/
- this.sortNotesByTitle = treeService.sortNotesByTitle;
+ this.sortNotesByTitle = parentNoteId => treeService.sortNotes(parentNoteId);
/**
* This method finds note by its noteId and prefix and either sets it to the given parentNoteId
@@ -474,13 +457,13 @@ module.exports = BackendScriptApi;
- Modules Classes
+ Modules Classes
diff --git a/docs/backend_api/services_sql.js.html b/docs/backend_api/services_sql.js.html
index 6ad8e618a..6f39b4b0e 100644
--- a/docs/backend_api/services_sql.js.html
+++ b/docs/backend_api/services_sql.js.html
@@ -162,6 +162,10 @@ function getRows(query, params = []) {
return wrap(query, s => s.all(params));
}
+function getRawRows(query, params = []) {
+ return wrap(query, s => s.raw().all(params));
+}
+
function iterateRows(query, params = []) {
return stmt(query).iterate(params);
}
@@ -266,13 +270,19 @@ function transactional(func) {
const ret = dbConnection.transaction(func).deferred();
if (!dbConnection.inTransaction) { // i.e. transaction was really committed (and not just savepoint released)
- require('./ws.js').sendTransactionEntityChangesToAllClients();
+ require('./ws').sendTransactionEntityChangesToAllClients();
}
return ret;
}
catch (e) {
- cls.clearEntityChanges();
+ const entityChanges = cls.getAndClearEntityChangeIds();
+
+ if (entityChanges.length > 0) {
+ log.info("Transaction rollback dirtied the becca, forcing reload.");
+
+ require('../becca/becca_loader').load();
+ }
throw e;
}
@@ -336,6 +346,7 @@ module.exports = {
* @return {object[]} - array of all rows, each row is a map of column name to column value
*/
getRows,
+ getRawRows,
iterateRows,
getManyRows,
@@ -384,13 +395,13 @@ module.exports = {
- Modules Classes
+ Modules Classes
diff --git a/docs/backend_api/sql.js.html b/docs/backend_api/sql.js.html
new file mode 100644
index 000000000..6acf22d50
--- /dev/null
+++ b/docs/backend_api/sql.js.html
@@ -0,0 +1,410 @@
+
+
+
+
+ JSDoc: Source: sql.js
+
+
+
+
+
+
+
+
+
+
+
+
+
Source: sql.js
+
+
+
+
+
+
+
+
+ "use strict";
+
+/**
+ * @module sql
+ */
+
+const log = require('./log');
+const Database = require('better-sqlite3');
+const dataDir = require('./data_dir');
+const cls = require('./cls');
+
+const dbConnection = new Database(dataDir.DOCUMENT_PATH);
+dbConnection.pragma('journal_mode = WAL');
+
+[`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach(eventType => {
+ process.on(eventType, () => {
+ if (dbConnection) {
+ // closing connection is especially important to fold -wal file into the main DB file
+ // (see https://sqlite.org/tempfiles.html for details)
+ dbConnection.close();
+ }
+ });
+});
+
+function insert(tableName, rec, replace = false) {
+ const keys = Object.keys(rec);
+ if (keys.length === 0) {
+ log.error("Can't insert empty object into table " + tableName);
+ return;
+ }
+
+ const columns = keys.join(", ");
+ const questionMarks = keys.map(p => "?").join(", ");
+
+ const query = "INSERT " + (replace ? "OR REPLACE" : "") + " INTO " + tableName + "(" + columns + ") VALUES (" + questionMarks + ")";
+
+ const res = execute(query, Object.values(rec));
+
+ return res ? res.lastInsertRowid : null;
+}
+
+function replace(tableName, rec) {
+ return insert(tableName, rec, true);
+}
+
+function upsert(tableName, primaryKey, rec) {
+ const keys = Object.keys(rec);
+ if (keys.length === 0) {
+ log.error("Can't upsert empty object into table " + tableName);
+ return;
+ }
+
+ const columns = keys.join(", ");
+
+ const questionMarks = keys.map(colName => "@" + colName).join(", ");
+
+ const updateMarks = keys.map(colName => `${colName} = @${colName}`).join(", ");
+
+ const query = `INSERT INTO ${tableName} (${columns}) VALUES (${questionMarks})
+ ON CONFLICT (${primaryKey}) DO UPDATE SET ${updateMarks}`;
+
+ for (const idx in rec) {
+ if (rec[idx] === true || rec[idx] === false) {
+ rec[idx] = rec[idx] ? 1 : 0;
+ }
+ }
+
+ execute(query, rec);
+}
+
+const statementCache = {};
+
+function stmt(sql) {
+ if (!(sql in statementCache)) {
+ statementCache[sql] = dbConnection.prepare(sql);
+ }
+
+ return statementCache[sql];
+}
+
+function getRow(query, params = []) {
+ return wrap(query, s => s.get(params));
+}
+
+function getRowOrNull(query, params = []) {
+ const all = getRows(query, params);
+
+ return all.length > 0 ? all[0] : null;
+}
+
+function getValue(query, params = []) {
+ const row = getRowOrNull(query, params);
+
+ if (!row) {
+ return null;
+ }
+
+ return row[Object.keys(row)[0]];
+}
+
+// smaller values can result in better performance due to better usage of statement cache
+const PARAM_LIMIT = 100;
+
+function getManyRows(query, params) {
+ let results = [];
+
+ while (params.length > 0) {
+ const curParams = params.slice(0, Math.min(params.length, PARAM_LIMIT));
+ params = params.slice(curParams.length);
+
+ const curParamsObj = {};
+
+ let j = 1;
+ for (const param of curParams) {
+ curParamsObj['param' + j++] = param;
+ }
+
+ let i = 1;
+ const questionMarks = curParams.map(() => ":param" + i++).join(",");
+ const curQuery = query.replace(/\?\?\?/g, questionMarks);
+
+ const statement = curParams.length === PARAM_LIMIT
+ ? stmt(curQuery)
+ : dbConnection.prepare(curQuery);
+
+ const subResults = statement.all(curParamsObj);
+ results = results.concat(subResults);
+ }
+
+ return results;
+}
+
+function getRows(query, params = []) {
+ return wrap(query, s => s.all(params));
+}
+
+function getRawRows(query, params = []) {
+ return wrap(query, s => s.raw().all(params));
+}
+
+function iterateRows(query, params = []) {
+ return stmt(query).iterate(params);
+}
+
+function getMap(query, params = []) {
+ const map = {};
+ const results = getRows(query, params);
+
+ for (const row of results) {
+ const keys = Object.keys(row);
+
+ map[row[keys[0]]] = row[keys[1]];
+ }
+
+ return map;
+}
+
+function getColumn(query, params = []) {
+ const list = [];
+ const result = getRows(query, params);
+
+ if (result.length === 0) {
+ return list;
+ }
+
+ const key = Object.keys(result[0])[0];
+
+ for (const row of result) {
+ list.push(row[key]);
+ }
+
+ return list;
+}
+
+function execute(query, params = []) {
+ return wrap(query, s => s.run(params));
+}
+
+function executeWithoutTransaction(query, params = []) {
+ dbConnection.run(query, params);
+}
+
+function executeMany(query, params) {
+ while (params.length > 0) {
+ const curParams = params.slice(0, Math.min(params.length, PARAM_LIMIT));
+ params = params.slice(curParams.length);
+
+ const curParamsObj = {};
+
+ let j = 1;
+ for (const param of curParams) {
+ curParamsObj['param' + j++] = param;
+ }
+
+ let i = 1;
+ const questionMarks = curParams.map(() => ":param" + i++).join(",");
+ const curQuery = query.replace(/\?\?\?/g, questionMarks);
+
+ dbConnection.prepare(curQuery).run(curParamsObj);
+ }
+}
+
+function executeScript(query) {
+ return dbConnection.exec(query);
+}
+
+function wrap(query, func) {
+ const startTimestamp = Date.now();
+ let result;
+
+ try {
+ result = func(stmt(query));
+ }
+ catch (e) {
+ if (e.message.includes("The database connection is not open")) {
+ // this often happens on killing the app which puts these alerts in front of user
+ // in these cases error should be simply ignored.
+ console.log(e.message);
+
+ return null
+ }
+
+ throw e;
+ }
+
+ const milliseconds = Date.now() - startTimestamp;
+
+ if (milliseconds >= 20) {
+ if (query.includes("WITH RECURSIVE")) {
+ log.info(`Slow recursive query took ${milliseconds}ms.`);
+ }
+ else {
+ log.info(`Slow query took ${milliseconds}ms: ${query.trim().replace(/\s+/g, " ")}`);
+ }
+ }
+
+ return result;
+}
+
+function transactional(func) {
+ try {
+ const ret = dbConnection.transaction(func).deferred();
+
+ if (!dbConnection.inTransaction) { // i.e. transaction was really committed (and not just savepoint released)
+ require('./ws').sendTransactionEntityChangesToAllClients();
+ }
+
+ return ret;
+ }
+ catch (e) {
+ const entityChanges = cls.getAndClearEntityChangeIds();
+
+ if (entityChanges.length > 0) {
+ log.info("Transaction rollback dirtied the becca, forcing reload.");
+
+ require('../becca/becca_loader').load();
+ }
+
+ throw e;
+ }
+}
+
+function fillParamList(paramIds, truncate = true) {
+ if (paramIds.length === 0) {
+ return;
+ }
+
+ if (truncate) {
+ execute("DELETE FROM param_list");
+ }
+
+ paramIds = Array.from(new Set(paramIds));
+
+ if (paramIds.length > 30000) {
+ fillParamList(paramIds.slice(30000), false);
+
+ paramIds = paramIds.slice(0, 30000);
+ }
+
+ // doing it manually to avoid this showing up on the sloq query list
+ const s = stmt(`INSERT INTO param_list VALUES ` + paramIds.map(paramId => `(?)`).join(','), paramIds);
+
+ s.run(paramIds);
+}
+
+module.exports = {
+ dbConnection,
+ insert,
+ replace,
+
+ /**
+ * Get single value from the given query - first column from first returned row.
+ *
+ * @method
+ * @param {string} query - SQL query with ? used as parameter placeholder
+ * @param {object[]} [params] - array of params if needed
+ * @return [object] - single value
+ */
+ getValue,
+
+ /**
+ * Get first returned row.
+ *
+ * @method
+ * @param {string} query - SQL query with ? used as parameter placeholder
+ * @param {object[]} [params] - array of params if needed
+ * @return {object} - map of column name to column value
+ */
+ getRow,
+ getRowOrNull,
+
+ /**
+ * Get all returned rows.
+ *
+ * @method
+ * @param {string} query - SQL query with ? used as parameter placeholder
+ * @param {object[]} [params] - array of params if needed
+ * @return {object[]} - array of all rows, each row is a map of column name to column value
+ */
+ getRows,
+ getRawRows,
+ iterateRows,
+ getManyRows,
+
+ /**
+ * Get a map of first column mapping to second column.
+ *
+ * @method
+ * @param {string} query - SQL query with ? used as parameter placeholder
+ * @param {object[]} [params] - array of params if needed
+ * @return {object} - map of first column to second column
+ */
+ getMap,
+
+ /**
+ * Get a first column in an array.
+ *
+ * @method
+ * @param {string} query - SQL query with ? used as parameter placeholder
+ * @param {object[]} [params] - array of params if needed
+ * @return {object[]} - array of first column of all returned rows
+ */
+ getColumn,
+
+ /**
+ * Execute SQL
+ *
+ * @method
+ * @param {string} query - SQL query with ? used as parameter placeholder
+ * @param {object[]} [params] - array of params if needed
+ */
+ execute,
+ executeWithoutTransaction,
+ executeMany,
+ executeScript,
+ transactional,
+ upsert,
+ fillParamList
+};
+
+
+
+
+
+
+
+
+
+
+ Modules Classes
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/frontend_api/Branch.html b/docs/frontend_api/Branch.html
index 1670c9962..67d43ea40 100644
--- a/docs/frontend_api/Branch.html
+++ b/docs/frontend_api/Branch.html
@@ -259,64 +259,6 @@
- isDeleted
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Source:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
isExpanded
@@ -665,7 +607,7 @@
Source:
@@ -767,7 +709,7 @@
Source:
@@ -869,7 +811,7 @@
Source:
@@ -971,7 +913,7 @@
Source:
@@ -1039,13 +981,13 @@
- Classes Global
+ Classes Global
diff --git a/docs/frontend_api/FrontendScriptApi.html b/docs/frontend_api/FrontendScriptApi.html
index 7dabc35d2..77182b3f3 100644
--- a/docs/frontend_api/FrontendScriptApi.html
+++ b/docs/frontend_api/FrontendScriptApi.html
@@ -329,7 +329,7 @@
Source:
@@ -563,6 +563,218 @@
+ NoteContextAwareWidget
+
+
+
+
+
+
+
+
+
+
+ Properties:
+
+
+
+
+
+
+
+
+ Type
+
+
+
+
+
+ Description
+
+
+
+
+
+
+
+
+
+
+
+
+NoteContextAwareWidget
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Source:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ NoteContextCachingWidget
+
+
+
+
+
+
+
+
+
+
+ Properties:
+
+
+
+
+
+
+
+
+ Type
+
+
+
+
+
+ Description
+
+
+
+
+
+
+
+
+
+
+
+
+NoteContextAwareWidget
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Source:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
originEntity
@@ -825,7 +1037,7 @@
-TabAwareWidget
+NoteContextAwareWidget
@@ -862,6 +1074,8 @@
+ Deprecated: use NoteContextAwareWidget instead
+
@@ -874,7 +1088,7 @@
Source:
@@ -931,7 +1145,7 @@
-TabCachingWidget
+NoteContextCachingWidget
@@ -968,6 +1182,8 @@
+ Deprecated: use NoteContextCachingWidget instead
+
@@ -980,7 +1196,7 @@
Source:
@@ -1109,7 +1325,7 @@
Source:
@@ -1264,7 +1480,7 @@
Source:
@@ -1419,7 +1635,7 @@
Source:
@@ -1556,7 +1772,7 @@
Source:
@@ -1712,7 +1928,7 @@
Source:
@@ -2033,7 +2249,7 @@
Source:
@@ -2166,7 +2382,7 @@
Source:
@@ -2272,7 +2488,7 @@
Source:
@@ -2378,7 +2594,7 @@
Source:
@@ -2532,7 +2748,7 @@
Source:
@@ -2669,7 +2885,7 @@
Source:
@@ -2776,7 +2992,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -2931,7 +3147,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -3087,7 +3303,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -3288,7 +3504,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -3394,7 +3610,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -3549,7 +3765,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -3727,7 +3943,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -3878,7 +4094,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -3986,7 +4202,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -4142,7 +4358,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -4298,7 +4514,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -4435,7 +4651,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -4589,7 +4805,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -4675,7 +4891,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -4812,7 +5028,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -4973,7 +5189,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -5081,7 +5297,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -5219,7 +5435,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -5375,7 +5591,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -5530,7 +5746,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -5681,7 +5897,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -5818,7 +6034,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -5955,7 +6171,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -6047,7 +6263,7 @@ Typical use case is when new note has been created, we should wait until it is s
Source:
@@ -6093,13 +6309,13 @@ Typical use case is when new note has been created, we should wait until it is s
- Classes Global
+ Classes Global
diff --git a/docs/frontend_api/NoteComplement.html b/docs/frontend_api/NoteComplement.html
index 9475f6efa..7833bdd4a 100644
--- a/docs/frontend_api/NoteComplement.html
+++ b/docs/frontend_api/NoteComplement.html
@@ -681,13 +681,13 @@
- Classes Global
+ Classes Global
diff --git a/docs/frontend_api/NoteShort.html b/docs/frontend_api/NoteShort.html
index 00d441ae3..4bd75c9e0 100644
--- a/docs/frontend_api/NoteShort.html
+++ b/docs/frontend_api/NoteShort.html
@@ -167,7 +167,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -267,7 +267,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -335,7 +335,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -403,65 +403,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- isDeleted
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Source:
-
@@ -519,7 +461,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -577,7 +519,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -635,7 +577,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -703,7 +645,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -771,7 +713,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -839,7 +781,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -897,7 +839,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -955,7 +897,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -1103,7 +1045,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -1481,7 +1423,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -2146,7 +2088,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -2313,7 +2255,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -2468,7 +2410,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -2578,7 +2520,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -2752,7 +2694,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -3130,7 +3072,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -3285,7 +3227,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -3452,7 +3394,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -3607,7 +3549,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -3762,7 +3704,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -3929,7 +3871,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -4084,7 +4026,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -4443,7 +4385,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -4610,7 +4552,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -4765,7 +4707,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -4935,7 +4877,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -5086,7 +5028,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -5144,6 +5086,115 @@ This note's representation is used in note tree and is kept in Froca.
+ getScriptEnv() → {string|null}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Source:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Returns:
+
+
+
+ JS script environment - either "frontend" or "backend"
+
+
+
+
+
+
+ Type
+
+
+
+string
+|
+
+null
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
getTargetRelations() → {Array.<Attribute >}
@@ -5196,7 +5247,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -5302,7 +5353,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -5404,7 +5455,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -5578,7 +5629,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -5835,7 +5886,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -6013,7 +6064,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -6168,7 +6219,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -6323,7 +6374,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -6478,7 +6529,7 @@ This note's representation is used in note tree and is kept in Froca.
Source:
@@ -6589,7 +6640,7 @@ Cache is note instance scoped.
Source:
@@ -6619,6 +6670,218 @@ Cache is note instance scoped.
+
+
+
+
+
+
+ isHtml() → {boolean}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Source:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Returns:
+
+
+
+ true if this note is HTML
+
+
+
+
+
+
+ Type
+
+
+
+boolean
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ isJavaScript() → {boolean}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Source:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Returns:
+
+
+
+ true if this note is JavaScript (code or attachment)
+
+
+
+
+
+
+ Type
+
+
+
+boolean
+
+
+
+
+
+
+
+
+
+
+
@@ -6737,13 +7000,13 @@ Cache is note instance scoped.
- Classes Global
+ Classes Global
diff --git a/docs/frontend_api/entities_attribute.js.html b/docs/frontend_api/entities_attribute.js.html
index 59872f59b..4c99cf6ae 100644
--- a/docs/frontend_api/entities_attribute.js.html
+++ b/docs/frontend_api/entities_attribute.js.html
@@ -50,8 +50,6 @@ class Attribute {
this.position = row.position;
/** @param {boolean} isInheritable */
this.isInheritable = !!row.isInheritable;
- /** @param {boolean} */
- this.isDeleted = !!row.isDeleted;
}
/** @returns {NoteShort} */
@@ -71,44 +69,6 @@ class Attribute {
return `Attribute(attributeId=${this.attributeId}, type=${this.type}, name=${this.name}, value=${this.value})`;
}
- /**
- * @return {boolean} - returns true if this attribute has the potential to influence the note in the argument.
- * That can happen in multiple ways:
- * 1. attribute is owned by the note
- * 2. attribute is owned by the template of the note
- * 3. attribute is owned by some note's ancestor and is inheritable
- */
- isAffecting(affectedNote) {
- if (!affectedNote) {
- return false;
- }
-
- const attrNote = this.getNote();
-
- if (!attrNote) {
- // the note (owner of the attribute) is not even loaded into the cache so it should not affect anything else
- return false;
- }
-
- const owningNotes = [affectedNote, ...affectedNote.getTemplateNotes()];
-
- for (const owningNote of owningNotes) {
- if (owningNote.noteId === attrNote.noteId) {
- return true;
- }
- }
-
- if (this.isInheritable) {
- for (const owningNote of owningNotes) {
- if (owningNote.hasAncestor(attrNote)) {
- return true;
- }
- }
- }
-
- return false;
- }
-
isDefinition() {
return this.type === 'label' && (this.name.startsWith('label:') || this.name.startsWith('relation:'));
}
@@ -140,13 +100,13 @@ export default Attribute;
- Classes Global
+ Classes Global
diff --git a/docs/frontend_api/entities_branch.js.html b/docs/frontend_api/entities_branch.js.html
index 129c2c2d6..aa3f353c5 100644
--- a/docs/frontend_api/entities_branch.js.html
+++ b/docs/frontend_api/entities_branch.js.html
@@ -49,8 +49,6 @@ class Branch {
this.isExpanded = !!row.isExpanded;
/** @param {boolean} */
this.fromSearchNote = !!row.fromSearchNote;
- /** @param {boolean} */
- this.isDeleted = !!row.isDeleted;
}
/** @returns {NoteShort} */
@@ -89,13 +87,13 @@ export default Branch;
- Classes Global
+ Classes Global
diff --git a/docs/frontend_api/entities_note_complement.js.html b/docs/frontend_api/entities_note_complement.js.html
index c2253b930..838b3f857 100644
--- a/docs/frontend_api/entities_note_complement.js.html
+++ b/docs/frontend_api/entities_note_complement.js.html
@@ -75,13 +75,13 @@ export default NoteComplement;
- Classes Global
+ Classes Global
diff --git a/docs/frontend_api/entities_note_short.js.html b/docs/frontend_api/entities_note_short.js.html
index 56e5f0558..407282a76 100644
--- a/docs/frontend_api/entities_note_short.js.html
+++ b/docs/frontend_api/entities_note_short.js.html
@@ -42,7 +42,9 @@ const NOTE_TYPE_ICONS = {
"render": "bx bx-extension",
"search": "bx bx-file-find",
"relation-map": "bx bx-map-alt",
- "book": "bx bx-book"
+ "book": "bx bx-book",
+ "note-map": "bx bx-map-alt",
+ "mermaid": "bx bx-selection"
};
/**
@@ -89,8 +91,6 @@ class NoteShort {
this.type = row.type;
/** @param {string} content-type, e.g. "application/json" */
this.mime = row.mime;
- /** @param {boolean} */
- this.isDeleted = !!row.isDeleted;
}
addParent(parentNoteId, branchId) {
@@ -252,7 +252,7 @@ class NoteShort {
if (this.noteId !== 'root') {
for (const parentNote of this.getParentNotes()) {
- // these virtual parent-child relationships are also loaded into frontend tree cache
+ // these virtual parent-child relationships are also loaded into froca
if (parentNote.type !== 'search') {
attrArrs.push(parentNote.__getInheritableAttributes(newPath));
}
@@ -282,6 +282,10 @@ class NoteShort {
return noteAttributeCache.attributes[this.noteId];
}
+ isRoot() {
+ return this.noted
+ }
+
getAllNotePaths(encounteredNoteIds = null) {
if (this.noteId === 'root') {
return [['root']];
@@ -330,7 +334,8 @@ class NoteShort {
notePath: path,
isInHoistedSubTree: path.includes(hoistedNotePath),
isArchived: path.find(noteId => froca.notes[noteId].hasLabel('archived')),
- isSearch: path.find(noteId => froca.notes[noteId].type === 'search')
+ isSearch: path.find(noteId => froca.notes[noteId].type === 'search'),
+ isHidden: path.includes("hidden")
}));
notePaths.sort((a, b) => {
@@ -729,6 +734,55 @@ class NoteShort {
const labels = this.getLabels('workspaceTabBackgroundColor');
return labels.length > 0 ? labels[0].value : "";
}
+
+ /** @returns {boolean} true if this note is JavaScript (code or attachment) */
+ isJavaScript() {
+ return (this.type === "code" || this.type === "file")
+ && (this.mime.startsWith("application/javascript")
+ || this.mime === "application/x-javascript"
+ || this.mime === "text/javascript");
+ }
+
+ /** @returns {boolean} true if this note is HTML */
+ isHtml() {
+ return (this.type === "code" || this.type === "file" || this.type === "render") && this.mime === "text/html";
+ }
+
+ /** @returns {string|null} JS script environment - either "frontend" or "backend" */
+ getScriptEnv() {
+ if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) {
+ return "frontend";
+ }
+
+ if (this.type === 'render') {
+ return "frontend";
+ }
+
+ if (this.isJavaScript() && this.mime.endsWith('env=backend')) {
+ return "backend";
+ }
+
+ return null;
+ }
+
+ async executeScript() {
+ if (!this.isJavaScript()) {
+ throw new Error(`Note ${this.noteId} is of type ${this.type} and mime ${this.mime} and thus cannot be executed`);
+ }
+
+ const env = this.getScriptEnv();
+
+ if (env === "frontend") {
+ const bundleService = (await import("../services/bundle.js")).default;
+ await bundleService.getAndExecuteBundle(this.noteId);
+ }
+ else if (env === "backend") {
+ await server.post('script/run/' + this.noteId);
+ }
+ else {
+ throw new Error(`Unrecognized env type ${env} for note ${this.noteId}`);
+ }
+ }
}
export default NoteShort;
@@ -742,13 +796,13 @@ export default NoteShort;
- Classes Global
+ Classes Global
diff --git a/docs/frontend_api/global.html b/docs/frontend_api/global.html
index 7fe7edd24..f7bdbe3ca 100644
--- a/docs/frontend_api/global.html
+++ b/docs/frontend_api/global.html
@@ -98,94 +98,6 @@
Methods
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- for overriding
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Source:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -244,96 +156,7 @@
Source:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- This event is used to synchronize collapsed state of all the tab-cached widgets since they are all rendered
-separately but should behave uniformly for the user.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Source:
-
@@ -572,7 +395,7 @@ separately but should behave uniformly for the user.
Source:
@@ -602,13 +425,13 @@ separately but should behave uniformly for the user.
- Classes Global
+ Classes Global
diff --git a/docs/frontend_api/index.html b/docs/frontend_api/index.html
index d72750442..68f6692d0 100644
--- a/docs/frontend_api/index.html
+++ b/docs/frontend_api/index.html
@@ -50,13 +50,13 @@
- Classes Global
+ Classes Global
diff --git a/docs/frontend_api/services_frontend_script_api.js.html b/docs/frontend_api/services_frontend_script_api.js.html
index 857da64d7..18b9328b0 100644
--- a/docs/frontend_api/services_frontend_script_api.js.html
+++ b/docs/frontend_api/services_frontend_script_api.js.html
@@ -38,8 +38,8 @@ import searchService from './search.js';
import CollapsibleWidget from '../widgets/collapsible_widget.js';
import ws from "./ws.js";
import appContext from "./app_context.js";
-import TabAwareWidget from "../widgets/tab_aware_widget.js";
-import TabCachingWidget from "../widgets/tab_caching_widget.js";
+import NoteContextAwareWidget from "../widgets/note_context_aware_widget.js";
+import NoteContextCachingWidget from "../widgets/note_context_caching_widget.js";
import BasicWidget from "../widgets/basic_widget.js";
/**
@@ -67,11 +67,23 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
/** @property {CollapsibleWidget} */
this.CollapsibleWidget = CollapsibleWidget;
- /** @property {TabAwareWidget} */
- this.TabAwareWidget = TabAwareWidget;
+ /**
+ * @property {NoteContextAwareWidget}
+ * @deprecated use NoteContextAwareWidget instead
+ */
+ this.TabAwareWidget = NoteContextAwareWidget;
- /** @property {TabCachingWidget} */
- this.TabCachingWidget = TabCachingWidget;
+ /** @property {NoteContextAwareWidget} */
+ this.NoteContextAwareWidget = NoteContextAwareWidget;
+
+ /**
+ * @property {NoteContextCachingWidget}
+ * @deprecated use NoteContextCachingWidget instead
+ */
+ this.TabCachingWidget = NoteContextCachingWidget;
+
+ /** @property {NoteContextAwareWidget} */
+ this.NoteContextCachingWidget = NoteContextCachingWidget;
/** @property {BasicWidget} */
this.BasicWidget = BasicWidget;
@@ -84,7 +96,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
* @returns {Promise<void>}
*/
this.activateNote = async notePath => {
- await appContext.tabManager.getActiveTabContext().setNote(notePath);
+ await appContext.tabManager.getActiveContext().setNote(notePath);
};
/**
@@ -96,7 +108,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
this.activateNewNote = async notePath => {
await ws.waitForMaxKnownEntityChangeId();
- await appContext.tabManager.getActiveTabContext().setNote(notePath);
+ await appContext.tabManager.getActiveContext().setNote(notePath);
appContext.triggerEvent('focusAndSelectTitle');
};
@@ -110,7 +122,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
this.openTabWithNote = async (notePath, activate) => {
await ws.waitForMaxKnownEntityChangeId();
- await appContext.tabManager.openTabWithNote(notePath, activate);
+ await appContext.tabManager.openContextWithNote(notePath, activate);
if (activate) {
appContext.triggerEvent('focusAndSelectTitle');
@@ -140,19 +152,23 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
.on('click', () => {
setTimeout(() => $pluginButtons.dropdown('hide'), 0);
});
+
+ if (opts.icon) {
+ button.append($("<span>").addClass("bx bx-" + opts.icon))
+ .append(" ");
+ }
+
+ button.append($("<span>").text(opts.title));
} else {
- button = $('<button class="noborder">')
- .addClass("btn btn-sm");
+ button = $('<span class="button-widget icon-action bx" data-toggle="tooltip" title="" data-placement="right"></span>')
+ .addClass("bx bx-" + (opts.icon || "question-mark"));
+
+ button.attr("title", opts.title);
+ button.tooltip({html: true});
}
+
button = button.on('click', opts.action);
- if (opts.icon) {
- button.append($("<span>").addClass("bx bx-" + opts.icon))
- .append(" ");
- }
-
- button.append($("<span>").text(opts.title));
-
button.attr('id', buttonId);
if ($("#" + buttonId).replaceWith(button).length === 0) {
@@ -341,7 +357,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
* @method
* @returns {NoteShort} active note (loaded into right pane)
*/
- this.getActiveTabNote = () => appContext.tabManager.getActiveTabNote();
+ this.getActiveTabNote = () => appContext.tabManager.getActiveContextNote();
/**
* See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html for a documentation on the returned instance.
@@ -355,7 +371,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
* @method
* @returns {Promise<string|null>} returns note path of active note or null if there isn't active note
*/
- this.getActiveTabNotePath = () => appContext.tabManager.getActiveTabNotePath();
+ this.getActiveTabNotePath = () => appContext.tabManager.getActiveContextNotePath();
/**
* @method
@@ -368,7 +384,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
* @method
*/
this.protectActiveNote = async () => {
- const activeNote = appContext.tabManager.getActiveTabNote();
+ const activeNote = appContext.tabManager.getActiveContextNote();
await protectedSessionService.protectNote(activeNote.noteId, true, false);
};
@@ -434,10 +450,10 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
* @return {Promise}
*/
this.setHoistedNoteId = (noteId) => {
- const activeTabContext = appContext.tabManager.getActiveTabContext();
+ const activeNoteContext = appContext.tabManager.getActiveContext();
- if (activeTabContext) {
- activeTabContext.setHoistedNoteId(noteId);
+ if (activeNoteContext) {
+ activeNoteContext.setHoistedNoteId(noteId);
}
};
@@ -487,13 +503,13 @@ export default FrontendScriptApi;
- Classes Global
+ Classes Global
diff --git a/docs/frontend_api/widgets_collapsible_widget.js.html b/docs/frontend_api/widgets_collapsible_widget.js.html
index 9b90ba367..a2bdb927b 100644
--- a/docs/frontend_api/widgets_collapsible_widget.js.html
+++ b/docs/frontend_api/widgets_collapsible_widget.js.html
@@ -26,42 +26,20 @@
- import TabAwareWidget from "./tab_aware_widget.js";
-import options from "../services/options.js";
+ import NoteContextAwareWidget from "./note_context_aware_widget.js";
const WIDGET_TPL = `
<div class="card widget">
- <div class="card-header">
- <div>
- <a class="widget-toggle-button no-arrow"
- title="Minimize/maximize widget"
- data-toggle="collapse" data-target="#[to be set]">
-
- <span class="widget-toggle-icon bx"></span>
-
- <span class="widget-title">
- Collapsible Group Item
- </span>
- </a>
-
- <span class="widget-header-actions"></span>
- </div>
-
- <div>
- <a class="widget-help external no-arrow bx bx-info-circle"></a>
- </div>
- </div>
+ <div class="card-header"></div>
- <div id="[to be set]" class="collapse body-wrapper" style="transition: none; ">
+ <div id="[to be set]" class="body-wrapper">
<div class="card-body"></div>
</div>
</div>`;
-export default class CollapsibleWidget extends TabAwareWidget {
+export default class CollapsibleWidget extends NoteContextAwareWidget {
get widgetTitle() { return "Untitled widget"; }
- get headerActions() { return []; }
-
get help() { return {}; }
doRender() {
@@ -72,85 +50,14 @@ export default class CollapsibleWidget extends TabAwareWidget {
this.$bodyWrapper = this.$widget.find('.body-wrapper');
this.$bodyWrapper.attr('id', this.componentId); // for toggle to work we need id
- // not using constructor name because of webpack mangling class names ...
- this.widgetName = this.widgetTitle.replace(/[^[a-zA-Z0-9]/g, "_");
-
- this.$toggleButton = this.$widget.find('.widget-toggle-button');
- this.$toggleIcon = this.$widget.find('.widget-toggle-icon');
-
- const collapsed = options.is(this.widgetName + 'Collapsed');
- if (!collapsed) {
- this.$bodyWrapper.collapse("show");
- }
-
- this.updateToggleIcon(collapsed);
-
- // using immediate variants of the event so that the previous collapse is not caught
- this.$bodyWrapper.on('hide.bs.collapse', () => this.toggleCollapsed(true));
- this.$bodyWrapper.on('show.bs.collapse', () => this.toggleCollapsed(false));
-
this.$body = this.$bodyWrapper.find('.card-body');
- this.$title = this.$widget.find('.widget-title');
+ this.$title = this.$widget.find('.card-header');
this.$title.text(this.widgetTitle);
- this.$help = this.$widget.find('.widget-help');
-
- if (this.help.title) {
- this.$help.attr("title", this.help.title);
- this.$help.attr("href", this.help.url || "javascript:");
-
- if (!this.help.url) {
- this.$help.addClass('no-link');
- }
- }
- else {
- this.$help.hide();
- }
-
- this.$headerActions = this.$widget.find('.widget-header-actions');
- this.$headerActions.append(this.headerActions);
-
this.initialized = this.doRenderBody();
-
- this.decorateWidget();
}
- toggleCollapsed(collapse) {
- this.updateToggleIcon(collapse);
-
- options.save(this.widgetName + 'Collapsed', collapse.toString());
-
- this.triggerEvent(`widgetCollapsedStateChanged`, {widgetName: this.widgetName, collapse});
- }
-
- updateToggleIcon(collapse) {
- if (collapse) {
- this.$toggleIcon
- .addClass("bx-chevron-right")
- .removeClass("bx-chevron-down")
- .attr("title", "Show");
- } else {
- this.$toggleIcon
- .addClass("bx-chevron-down")
- .removeClass("bx-chevron-right")
- .attr("title", "Hide");
- }
- }
-
- /**
- * This event is used to synchronize collapsed state of all the tab-cached widgets since they are all rendered
- * separately but should behave uniformly for the user.
- */
- widgetCollapsedStateChangedEvent({widgetName, collapse}) {
- if (widgetName === this.widgetName) {
- this.$bodyWrapper.toggleClass('show', !collapse);
- }
- }
-
- /** for overriding */
- decorateWidget() {}
-
/** for overriding */
async doRenderBody() {}
@@ -168,13 +75,13 @@ export default class CollapsibleWidget extends TabAwareWidget {
- Classes Global
+ Classes Global
diff --git a/package.json b/package.json
index de498e199..9b400567f 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,7 @@
"scripts": {
"start-server": "cross-env TRILIUM_ENV=dev node ./src/www",
"start-electron": "cross-env TRILIUM_ENV=dev electron --inspect=5858 .",
- "build-backend-docs": "./node_modules/.bin/jsdoc -c jsdoc-conf.json -d ./docs/backend_api src/entities/*.js src/services/backend_script_api.js src/services/sql.js",
+ "build-backend-docs": "./node_modules/.bin/jsdoc -c jsdoc-conf.json -d ./docs/backend_api src/becca/entities/*.js src/services/backend_script_api.js src/services/sql.js",
"build-frontend-docs": "./node_modules/.bin/jsdoc -c jsdoc-conf.json -d ./docs/frontend_api src/public/app/entities/*.js src/public/app/services/frontend_script_api.js src/public/app/widgets/collapsible_widget.js",
"build-docs": "npm run build-backend-docs && npm run build-frontend-docs",
"webpack": "npx webpack -c webpack-desktop.config.js && npx webpack -c webpack-mobile.config.js && npx webpack -c webpack-setup.config.js",
diff --git a/src/becca/entities/note_revision.js b/src/becca/entities/note_revision.js
index af51687a5..08fb710b4 100644
--- a/src/becca/entities/note_revision.js
+++ b/src/becca/entities/note_revision.js
@@ -10,8 +10,6 @@ const AbstractEntity = require("./abstract_entity.js");
/**
* NoteRevision represents snapshot of note's title and content at some point in the past. It's used for seamless note versioning.
- *
- * @extends Entity
*/
class NoteRevision extends AbstractEntity {
static get entityName() { return "note_revisions"; }