diff --git a/docs/backend_api/ApiToken.html b/docs/backend_api/ApiToken.html index 8f0026765..2d4a1d17c 100644 --- a/docs/backend_api/ApiToken.html +++ b/docs/backend_api/ApiToken.html @@ -64,127 +64,6 @@ -
Properties:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
apiTokenId - - -string - - - - primary key
token - - -string - - - -
isDeleted - - -boolean - - - - true if API token is deleted
utcDateCreated - - -string - - - -
- - - -
@@ -214,7 +93,7 @@
Source:
@@ -248,17 +127,6 @@ -

Extends

- - - - - - - - @@ -287,13 +155,13 @@
diff --git a/docs/backend_api/BackendScriptApi.html b/docs/backend_api/BackendScriptApi.html index 09ccf7808..b4c76a6b5 100644 --- a/docs/backend_api/BackendScriptApi.html +++ b/docs/backend_api/BackendScriptApi.html @@ -504,7 +504,7 @@ -Entity +Entity @@ -663,7 +663,7 @@
Source:
@@ -1059,7 +1059,7 @@ JSON MIME type. See also createNewNote() for more options.
Source:
@@ -1226,7 +1226,7 @@ JSON MIME type. See also createNewNote() for more options.
Source:
@@ -1512,7 +1512,7 @@ JSON MIME type. See also createNewNote() for more options.
Source:
@@ -1717,7 +1717,7 @@ JSON MIME type. See also createNewNote() for more options.
Source:
@@ -1899,7 +1899,7 @@ JSON MIME type. See also createNewNote() for more options.
Source:
@@ -2100,7 +2100,7 @@ JSON MIME type. See also createNewNote() for more options.
Source:
@@ -2251,7 +2251,7 @@ JSON MIME type. See also createNewNote() for more options.
Source:
@@ -2357,7 +2357,7 @@ JSON MIME type. See also createNewNote() for more options.
Source:
@@ -2827,7 +2827,7 @@ JSON MIME type. See also createNewNote() for more options.
Source:
@@ -2884,361 +2884,6 @@ JSON MIME type. See also createNewNote() for more options. -

getEntities(SQL, array) → {Array.<Entity>}

- - - - - - - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
SQL - - -string - - - - query
array - - -Array.<?> - - - - of params
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - - - -
-
- Type -
-
- -Array.<Entity> - - -
-
- - - - - - - - - - - - - -

getEntity(SQL, array) → {Entity|null}

- - - - - - -
- Retrieves first entity from the SQL's result set. -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
SQL - - -string - - - - query
array - - -Array.<?> - - - - of params
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - - - -
-
- Type -
-
- -Entity -| - -null - - -
-
- - - - - - - - - - - - -

getInstanceName() → {string|null}

@@ -3450,7 +3095,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -3805,7 +3450,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -4003,7 +3648,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -4112,7 +3757,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -4221,7 +3866,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -4402,7 +4047,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -4560,7 +4205,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -4713,7 +4358,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -4850,7 +4495,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -4958,7 +4603,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -4994,7 +4639,7 @@ if some action needs to happen on only one specific instance. -

searchForNote(searchString) → {Note|null}

+

searchForNote(query, searchParamsopt) → {Note|null}

@@ -5027,6 +4672,8 @@ if some action needs to happen on only one specific instance. Type + Attributes + @@ -5039,7 +4686,7 @@ if some action needs to happen on only one specific instance. - searchString + query @@ -5052,6 +4699,47 @@ if some action needs to happen on only one specific instance. + + + + + + + + + + + + + + + + + + + searchParams + + + + + +Object + + + + + + + + + <optional>
+ + + + + + + @@ -5096,7 +4784,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -5298,7 +4986,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -5507,7 +5195,7 @@ This method looks similar to toggleNoteInParent() but differs because we're look
Source:
@@ -5640,7 +5328,7 @@ This method looks similar to toggleNoteInParent() but differs because we're look
Source:
@@ -5846,7 +5534,7 @@ This method looks similar to toggleNoteInParent() but differs because we're look
Source:
@@ -6002,7 +5690,7 @@ exists, then we'll use that transaction.
Source:
@@ -6157,7 +5845,7 @@ exists, then we'll use that transaction.
Source:
@@ -6225,13 +5913,13 @@ exists, then we'll use that transaction.
diff --git a/docs/backend_api/NoteRevision.html b/docs/backend_api/NoteRevision.html index 12ab87c6e..df87d061b 100644 --- a/docs/backend_api/NoteRevision.html +++ b/docs/backend_api/NoteRevision.html @@ -64,288 +64,6 @@ -
Properties:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
noteRevisionId - - -string - - - -
noteId - - -string - - - -
type - - -string - - - -
mime - - -string - - - -
title - - -string - - - -
isProtected - - -boolean - - - -
dateLastEdited - - -string - - - -
dateCreated - - -string - - - -
utcDateLastEdited - - -string - - - -
utcDateCreated - - -string - - - -
utcDateModified - - -string - - - -
- - - -
@@ -375,7 +93,7 @@
Source:
@@ -409,16 +127,35 @@ -

Extends

+ + + + + + + + + + + + +

Members

+ +

contentLength

- + + + + + + +
+ @@ -434,6 +171,676 @@ + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

dateCreated

+ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

dateLastEdited

+ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

isProtected

+ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

mime

+ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

noteId

+ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

noteRevisionId

+ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

title

+ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

type

+ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

utcDateCreated

+ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

utcDateLastEdited

+ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

utcDateModified

+ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +

Methods

@@ -490,7 +897,7 @@
Source:
@@ -544,6 +951,108 @@ +

getContentMetadata() → {Object}

+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Object + + +
+
+ + + + + + + + + + + + +

isStringNote() → {boolean}

@@ -592,7 +1101,7 @@
Source:
@@ -660,13 +1169,13 @@
diff --git a/docs/backend_api/Option.html b/docs/backend_api/Option.html index 2ecb34751..ef481bf80 100644 --- a/docs/backend_api/Option.html +++ b/docs/backend_api/Option.html @@ -64,150 +64,6 @@ -
Properties:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
name - - -string - - - -
value - - -string - - - -
isSynced - - -boolean - - - -
utcDateModified - - -string - - - -
utcDateCreated - - -string - - - -
- - - -
@@ -237,7 +93,7 @@
Source:
@@ -271,17 +127,6 @@ -

Extends

- - - - - - - - @@ -310,13 +155,13 @@
diff --git a/docs/backend_api/RecentNote.html b/docs/backend_api/RecentNote.html index 6831a401d..3aeec055d 100644 --- a/docs/backend_api/RecentNote.html +++ b/docs/backend_api/RecentNote.html @@ -64,104 +64,6 @@ -
Properties:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
noteId - - -string - - - -
notePath - - -string - - - -
utcDateCreated - - -string - - - -
- - - -
@@ -191,7 +93,7 @@
Source:
@@ -225,17 +127,6 @@ -

Extends

- - - - - - - - @@ -264,13 +155,13 @@
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;
+
+
+
+ + + + +
+ + + +
+ + + + + + + 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;
+
+
+
+ + + + +
+ + + +
+ + + + + + + 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;
+
+
+
+ + + + +
+ + + +
+ + + + + + + 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;
+
+
+
+ + + + +
+ + + +
+ + + + + + + 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;
+
+
+
+ + + + +
+ + + +
+ + + + + + + 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;
+
+
+
+ + + + +
+ + + +
+ + + + + + + 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;
+
+
+
+ + + + +
+ + + +
+ + + + + + + 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;
+
+
+
+ + + + +
+ + + +
+ + + + + + + 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 @@
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 @@
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 @@
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;
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 = {
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
+};
+
+
+
+ + + + +
+ + + +
+ + + + + + + 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 @@
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:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDescription
+ + +NoteContextAwareWidget + + + +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

NoteContextCachingWidget

+ + + + + + + + + + +
Properties:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDescription
+ + +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
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 @@
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.
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;
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;
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;
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;
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

- - - - - - -

decorateWidget()

- - - - - - -
- for overriding -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -244,96 +156,7 @@
Source:
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -

widgetCollapsedStateChangedEvent()

- - - - - - -
- 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.
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 @@
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("&nbsp;"); + } + + 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("&nbsp;"); - } - - 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;
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 {
 
 
 
 
 
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"; }