diff --git a/db/demo.zip b/db/demo.zip index 7f106b568..a2327ff33 100644 Binary files a/db/demo.zip and b/db/demo.zip differ diff --git a/docs/backend_api/AbstractEntity.html b/docs/backend_api/AbstractEntity.html index e017cf1c9..ab08501ce 100644 --- a/docs/backend_api/AbstractEntity.html +++ b/docs/backend_api/AbstractEntity.html @@ -158,6 +158,8 @@
Mark the entity as (soft) deleted. It will be completely erased later. + +This is a low level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead.
@@ -265,7 +267,7 @@
Source:
diff --git a/docs/backend_api/Attribute.html b/docs/backend_api/Attribute.html index 59b518036..8633da77b 100644 --- a/docs/backend_api/Attribute.html +++ b/docs/backend_api/Attribute.html @@ -1030,6 +1030,8 @@ and relation (representing named relationship between source and target note) Mark the entity as (soft) deleted. It will be completely erased later. + +This is a low level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead. @@ -1142,7 +1144,7 @@ and relation (representing named relationship between source and target note)Source:
diff --git a/docs/backend_api/Branch.html b/docs/backend_api/Branch.html index 4be4b47f5..f89ff45ad 100644 --- a/docs/backend_api/Branch.html +++ b/docs/backend_api/Branch.html @@ -94,7 +94,7 @@ parents.
Source:
@@ -205,7 +205,7 @@ parents.
Source:
@@ -263,7 +263,7 @@ parents.
Source:
@@ -331,7 +331,7 @@ parents.
Source:
@@ -399,7 +399,7 @@ parents.
Source:
@@ -467,7 +467,7 @@ parents.
Source:
@@ -525,7 +525,7 @@ parents.
Source:
@@ -593,7 +593,7 @@ parents.
Source:
@@ -661,7 +661,7 @@ parents.
Source:
@@ -729,7 +729,7 @@ parents.
Source:
@@ -757,6 +757,210 @@ parents. +

deleteBranch(deleteIdopt, taskContextopt) → {boolean}

+ + + + + + +
+ Delete a branch. If this is a last note's branch, delete the note as well. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
deleteId + + +string + + + + + + <optional>
+ + + + + +
optional delete identified
taskContext + + +TaskContext + + + + + + <optional>
+ + + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ - true if note has been deleted, false otherwise +
+ + + +
+
+ Type +
+
+ +boolean + + +
+
+ + + + + + + + + + + + +

markAsDeleted(deleteIdopt)

@@ -766,6 +970,8 @@ parents.
Mark the entity as (soft) deleted. It will be completely erased later. + +This is a low level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead.
@@ -878,7 +1084,7 @@ parents.
Source:
diff --git a/docs/backend_api/EtapiToken.html b/docs/backend_api/EtapiToken.html index bffcb3658..b675d8cdc 100644 --- a/docs/backend_api/EtapiToken.html +++ b/docs/backend_api/EtapiToken.html @@ -587,6 +587,8 @@ from tokenHash and token.
Mark the entity as (soft) deleted. It will be completely erased later. + +This is a low level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead.
@@ -699,7 +701,7 @@ from tokenHash and token.
Source:
diff --git a/docs/backend_api/Note.html b/docs/backend_api/Note.html index 5a24d464a..c5eea4f2f 100644 --- a/docs/backend_api/Note.html +++ b/docs/backend_api/Note.html @@ -93,7 +93,7 @@
Source:
@@ -204,7 +204,7 @@
Source:
@@ -279,7 +279,7 @@
Source:
@@ -347,7 +347,7 @@
Source:
@@ -415,7 +415,7 @@
Source:
@@ -486,7 +486,7 @@
Source:
@@ -554,7 +554,7 @@
Source:
@@ -622,7 +622,7 @@
Source:
@@ -690,7 +690,7 @@
Source:
@@ -758,7 +758,7 @@
Source:
@@ -833,7 +833,7 @@
Source:
@@ -901,7 +901,7 @@
Source:
@@ -969,7 +969,7 @@
Source:
@@ -1037,7 +1037,7 @@
Source:
@@ -1112,7 +1112,7 @@
Source:
@@ -1180,7 +1180,7 @@
Source:
@@ -1248,7 +1248,7 @@
Source:
@@ -1316,7 +1316,7 @@
Source:
@@ -1384,7 +1384,7 @@
Source:
@@ -1452,7 +1452,7 @@
Source:
@@ -1528,7 +1528,7 @@
Source:
@@ -1630,7 +1630,7 @@
Source:
@@ -1678,6 +1678,188 @@ + + + + + + +

deleteNote(deleteIdopt, taskContextopt)

+ + + + + + +
+ (Soft) delete a note and all its descendants. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
deleteId + + +string + + + + + + <optional>
+ + + + + +
optional delete identified
taskContext + + +TaskContext + + + + + + <optional>
+ + + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + @@ -1732,7 +1914,7 @@
Source:
@@ -1838,7 +2020,7 @@
Source:
@@ -2012,7 +2194,7 @@
Source:
@@ -2212,7 +2394,7 @@
Source:
@@ -2390,7 +2572,7 @@
Source:
@@ -2501,7 +2683,7 @@
Source:
@@ -2603,7 +2785,7 @@
Source:
@@ -2705,7 +2887,7 @@
Source:
@@ -2807,7 +2989,7 @@
Source:
@@ -2909,7 +3091,7 @@
Source:
@@ -3017,7 +3199,7 @@
Source:
@@ -3123,7 +3305,7 @@
Source:
@@ -3274,7 +3456,7 @@
Source:
@@ -3444,7 +3626,7 @@
Source:
@@ -3599,7 +3781,7 @@
Source:
@@ -3769,7 +3951,7 @@
Source:
@@ -3875,7 +4057,7 @@
Source:
@@ -4077,7 +4259,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -4255,7 +4437,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -4413,7 +4595,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -4583,7 +4765,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -4738,7 +4920,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -4908,7 +5090,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -5063,7 +5245,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -5233,7 +5415,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -5388,7 +5570,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -5497,7 +5679,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -5599,7 +5781,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -5750,7 +5932,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -5920,7 +6102,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -6075,7 +6257,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -6184,7 +6366,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -6293,7 +6475,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -6395,7 +6577,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -6497,7 +6679,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -6599,7 +6781,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -6706,7 +6888,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -6808,7 +6990,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -6959,7 +7141,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -7137,7 +7319,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -7292,7 +7474,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -7447,7 +7629,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -7602,7 +7784,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -7752,7 +7934,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -7858,7 +8040,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -7964,7 +8146,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -8070,7 +8252,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -8176,7 +8358,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -8282,7 +8464,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -8349,6 +8531,8 @@ This method can be significantly faster than the getAttribute()
Mark the entity as (soft) deleted. It will be completely erased later. + +This is a low level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead.
@@ -8461,7 +8645,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -8672,7 +8856,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -8852,7 +9036,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -9032,7 +9216,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -9354,7 +9538,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -9534,7 +9718,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -9694,7 +9878,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -9936,7 +10120,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -10147,7 +10331,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -10358,7 +10542,7 @@ This method can be significantly faster than the getAttribute()
Source:
diff --git a/docs/backend_api/NoteRevision.html b/docs/backend_api/NoteRevision.html index f9575d7df..ad8b80285 100644 --- a/docs/backend_api/NoteRevision.html +++ b/docs/backend_api/NoteRevision.html @@ -1300,6 +1300,8 @@ It's used for seamless note versioning.
Mark the entity as (soft) deleted. It will be completely erased later. + +This is a low level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead.
@@ -1412,7 +1414,7 @@ It's used for seamless note versioning.
Source:
diff --git a/docs/backend_api/Option.html b/docs/backend_api/Option.html index 2c43460c7..c812d8ea6 100644 --- a/docs/backend_api/Option.html +++ b/docs/backend_api/Option.html @@ -445,6 +445,8 @@
Mark the entity as (soft) deleted. It will be completely erased later. + +This is a low level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead.
@@ -557,7 +559,7 @@
Source:
diff --git a/docs/backend_api/RecentNote.html b/docs/backend_api/RecentNote.html index 264580c72..621598ed2 100644 --- a/docs/backend_api/RecentNote.html +++ b/docs/backend_api/RecentNote.html @@ -377,6 +377,8 @@
Mark the entity as (soft) deleted. It will be completely erased later. + +This is a low level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead.
@@ -489,7 +491,7 @@
Source:
diff --git a/docs/backend_api/becca_entities_abstract_entity.js.html b/docs/backend_api/becca_entities_abstract_entity.js.html index 43b0899a8..a66b5654a 100644 --- a/docs/backend_api/becca_entities_abstract_entity.js.html +++ b/docs/backend_api/becca_entities_abstract_entity.js.html @@ -139,6 +139,8 @@ class AbstractEntity { /** * Mark the entity as (soft) deleted. It will be completely erased later. * + * This is a low level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead. + * * @param [deleteId=null] */ markAsDeleted(deleteId = null) { diff --git a/docs/backend_api/becca_entities_branch.js.html b/docs/backend_api/becca_entities_branch.js.html index 94ffed9dc..73fd62a3b 100644 --- a/docs/backend_api/becca_entities_branch.js.html +++ b/docs/backend_api/becca_entities_branch.js.html @@ -32,6 +32,10 @@ const Note = require('./note'); const AbstractEntity = require("./abstract_entity"); const sql = require("../../services/sql"); const dateUtils = require("../../services/date_utils"); +const utils = require("../../services/utils.js"); +const TaskContext = require("../../services/task_context.js"); +const cls = require("../../services/cls.js"); +const log = require("../../services/log.js"); /** * Branch represents a relationship between a child note and its parent note. Trilium allows a note to have multiple @@ -142,6 +146,63 @@ class Branch extends AbstractEntity { return !(this.branchId in this.becca.branches); } + /** + * Delete a branch. If this is a last note's branch, delete the note as well. + * + * @param {string} [deleteId] - optional delete identified + * @param {TaskContext} [taskContext] + * + * @return {boolean} - true if note has been deleted, false otherwise + */ + deleteBranch(deleteId, taskContext) { + if (!deleteId) { + deleteId = utils.randomString(10); + } + + if (!taskContext) { + taskContext = new TaskContext('no-progress-reporting'); + } + + taskContext.increaseProgressCount(); + + if (this.branchId === 'root' + || this.noteId === 'root' + || this.noteId === cls.getHoistedNoteId()) { + + throw new Error("Can't delete root or hoisted branch/note"); + } + + this.markAsDeleted(deleteId); + + const note = this.getNote(); + const notDeletedBranches = note.getParentBranches(); + + if (notDeletedBranches.length === 0) { + for (const childBranch of note.getChildBranches()) { + childBranch.deleteBranch(deleteId, taskContext); + } + + // first delete children and then parent - this will show up better in recent changes + + log.info("Deleting note " + note.noteId); + + for (const attribute of note.getOwnedAttributes()) { + attribute.markAsDeleted(deleteId); + } + + for (const relation of note.getTargetRelations()) { + relation.markAsDeleted(deleteId); + } + + note.markAsDeleted(deleteId); + + return true; + } + else { + return false; + } + } + beforeSaving() { if (this.notePosition === undefined || this.notePosition === null) { // TODO finding new position can be refactored into becca diff --git a/docs/backend_api/becca_entities_note.js.html b/docs/backend_api/becca_entities_note.js.html index 2aae3ae51..ad2571123 100644 --- a/docs/backend_api/becca_entities_note.js.html +++ b/docs/backend_api/becca_entities_note.js.html @@ -36,6 +36,7 @@ const dateUtils = require('../../services/date_utils'); const entityChangesService = require('../../services/entity_changes'); const AbstractEntity = require("./abstract_entity"); const NoteRevision = require("./note_revision"); +const TaskContext = require("../../services/task_context.js"); const LABEL = 'label'; const RELATION = 'relation'; @@ -1153,6 +1154,26 @@ class Note extends AbstractEntity { return cloningService.cloneNoteToBranch(this.noteId, branch.branchId); } + /** + * (Soft) delete a note and all its descendants. + * + * @param {string} [deleteId] - optional delete identified + * @param {TaskContext} [taskContext] + */ + deleteNote(deleteId, taskContext) { + if (!deleteId) { + deleteId = utils.randomString(10); + } + + if (!taskContext) { + taskContext = new TaskContext('no-progress-reporting'); + } + + for (const branch of this.getParentBranches()) { + branch.deleteBranch(deleteId, taskContext); + } + } + decrypt() { if (this.isProtected && !this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) { try { diff --git a/package-lock.json b/package-lock.json index 50926f6e1..0ea3bd1ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "express-partial-content": "1.0.2", "express-rate-limit": "6.3.0", "express-session": "1.17.2", - "fs-extra": "10.0.1", + "fs-extra": "10.1.0", "helmet": "5.0.2", "html": "1.0.0", "html2plaintext": "2.1.4", @@ -45,7 +45,7 @@ "jsdom": "19.0.0", "mime-types": "2.1.35", "multer": "1.4.4", - "node-abi": "3.8.0", + "node-abi": "3.15.0", "normalize-strings": "1.1.1", "open": "8.4.0", "portscanner": "2.2.0", @@ -75,7 +75,7 @@ "cross-env": "7.0.3", "electron": "16.2.1", "electron-builder": "23.0.3", - "electron-packager": "15.4.0", + "electron-packager": "15.5.0", "electron-rebuild": "3.2.7", "esm": "3.2.25", "jasmine": "4.1.0", @@ -4211,12 +4211,13 @@ } }, "node_modules/electron-packager": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-15.4.0.tgz", - "integrity": "sha512-JrrLcBP15KGrPj0cZ/ALKGmaQ4gJkn3mocf0E3bRKdR3kxKWYcDRpCvdhksYDXw/r3I6tMEcZ7XzyApWFXdVpw==", + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-15.5.0.tgz", + "integrity": "sha512-8mITLQgTm9xdrO8XL/PsK0EZGU7zK/ay7TI8M1C9pc1UZ++HlaWQJBRJHlOXf4TL/7FsiF4OciEhiqhMn+LKQQ==", "dev": true, "dependencies": { "@electron/get": "^1.6.0", + "@electron/universal": "^1.2.1", "asar": "^3.1.0", "cross-spawn-windows-exe": "^1.2.0", "debug": "^4.0.1", @@ -4245,25 +4246,22 @@ "url": "https://github.com/electron/electron-packager?sponsor=1" } }, - "node_modules/electron-packager/node_modules/asar": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/asar/-/asar-3.1.0.tgz", - "integrity": "sha512-vyxPxP5arcAqN4F/ebHd/HhwnAiZtwhglvdmc7BR2f0ywbVNTOpSeyhLDbGXtE/y58hv1oC75TaNIXutnsOZsQ==", + "node_modules/electron-packager/node_modules/@electron/universal": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.2.1.tgz", + "integrity": "sha512-7323HyMh7KBAl/nPDppdLsC87G6RwRU02dy5FPeGB1eS7rUePh55+WNWiDPLhFQqqVPHzh77M69uhmoT8XnwMQ==", "dev": true, "dependencies": { - "chromium-pickle-js": "^0.2.0", - "commander": "^5.0.0", - "glob": "^7.1.6", - "minimatch": "^3.0.4" - }, - "bin": { - "asar": "bin/asar.js" + "@malept/cross-spawn-promise": "^1.1.0", + "asar": "^3.1.0", + "debug": "^4.3.1", + "dir-compare": "^2.4.0", + "fs-extra": "^9.0.1", + "minimatch": "^3.0.4", + "plist": "^3.0.4" }, "engines": { - "node": ">=10.12.0" - }, - "optionalDependencies": { - "@types/glob": "^7.1.1" + "node": ">=8.6" } }, "node_modules/electron-packager/node_modules/cross-spawn-windows-exe": { @@ -5676,9 +5674,9 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, "node_modules/fs-extra": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz", - "integrity": "sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -7785,9 +7783,9 @@ "dev": true }, "node_modules/node-abi": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.8.0.tgz", - "integrity": "sha512-tzua9qWWi7iW4I42vUPKM+SfaF0vQSLAm4yO5J83mSwB7GeoWrDKC/K+8YCnYNwqP5duwazbw2X9l4m8SC2cUw==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.15.0.tgz", + "integrity": "sha512-Ic6z/j6I9RLm4ov7npo1I48UQr2BEyFCqh6p7S1dhEx9jPO0GPGq/e2Rb7x7DroQrmiVMz/Bw1vJm9sPAl2nxA==", "dependencies": { "semver": "^7.3.5" }, @@ -14795,12 +14793,13 @@ } }, "electron-packager": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-15.4.0.tgz", - "integrity": "sha512-JrrLcBP15KGrPj0cZ/ALKGmaQ4gJkn3mocf0E3bRKdR3kxKWYcDRpCvdhksYDXw/r3I6tMEcZ7XzyApWFXdVpw==", + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-15.5.0.tgz", + "integrity": "sha512-8mITLQgTm9xdrO8XL/PsK0EZGU7zK/ay7TI8M1C9pc1UZ++HlaWQJBRJHlOXf4TL/7FsiF4OciEhiqhMn+LKQQ==", "dev": true, "requires": { "@electron/get": "^1.6.0", + "@electron/universal": "^1.2.1", "asar": "^3.1.0", "cross-spawn-windows-exe": "^1.2.0", "debug": "^4.0.1", @@ -14820,17 +14819,19 @@ "yargs-parser": "^20.0.0" }, "dependencies": { - "asar": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/asar/-/asar-3.1.0.tgz", - "integrity": "sha512-vyxPxP5arcAqN4F/ebHd/HhwnAiZtwhglvdmc7BR2f0ywbVNTOpSeyhLDbGXtE/y58hv1oC75TaNIXutnsOZsQ==", + "@electron/universal": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.2.1.tgz", + "integrity": "sha512-7323HyMh7KBAl/nPDppdLsC87G6RwRU02dy5FPeGB1eS7rUePh55+WNWiDPLhFQqqVPHzh77M69uhmoT8XnwMQ==", "dev": true, "requires": { - "@types/glob": "^7.1.1", - "chromium-pickle-js": "^0.2.0", - "commander": "^5.0.0", - "glob": "^7.1.6", - "minimatch": "^3.0.4" + "@malept/cross-spawn-promise": "^1.1.0", + "asar": "^3.1.0", + "debug": "^4.3.1", + "dir-compare": "^2.4.0", + "fs-extra": "^9.0.1", + "minimatch": "^3.0.4", + "plist": "^3.0.4" } }, "cross-spawn-windows-exe": { @@ -15757,9 +15758,9 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, "fs-extra": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz", - "integrity": "sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "requires": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -17411,9 +17412,9 @@ "dev": true }, "node-abi": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.8.0.tgz", - "integrity": "sha512-tzua9qWWi7iW4I42vUPKM+SfaF0vQSLAm4yO5J83mSwB7GeoWrDKC/K+8YCnYNwqP5duwazbw2X9l4m8SC2cUw==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.15.0.tgz", + "integrity": "sha512-Ic6z/j6I9RLm4ov7npo1I48UQr2BEyFCqh6p7S1dhEx9jPO0GPGq/e2Rb7x7DroQrmiVMz/Bw1vJm9sPAl2nxA==", "requires": { "semver": "^7.3.5" } diff --git a/package.json b/package.json index 35bb0f668..8e4220b73 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "trilium", "productName": "Trilium Notes", "description": "Trilium Notes", - "version": "0.51.0-beta", + "version": "0.51.2", "license": "AGPL-3.0-only", "main": "electron.js", "bin": { @@ -45,7 +45,7 @@ "express-partial-content": "1.0.2", "express-rate-limit": "6.3.0", "express-session": "1.17.2", - "fs-extra": "10.0.1", + "fs-extra": "10.1.0", "helmet": "5.0.2", "html": "1.0.0", "html2plaintext": "2.1.4", @@ -60,7 +60,7 @@ "jsdom": "19.0.0", "mime-types": "2.1.35", "multer": "1.4.4", - "node-abi": "3.8.0", + "node-abi": "3.15.0", "normalize-strings": "1.1.1", "open": "8.4.0", "portscanner": "2.2.0", @@ -87,7 +87,7 @@ "cross-env": "7.0.3", "electron": "16.2.1", "electron-builder": "23.0.3", - "electron-packager": "15.4.0", + "electron-packager": "15.5.0", "electron-rebuild": "3.2.7", "esm": "3.2.25", "jasmine": "4.1.0", diff --git a/src/becca/becca_service.js b/src/becca/becca_service.js index b27e985f5..f2cebbaae 100644 --- a/src/becca/becca_service.js +++ b/src/becca/becca_service.js @@ -67,7 +67,7 @@ function getNoteTitle(childNoteId, parentNoteId) { const parentNote = becca.notes[parentNoteId]; if (!childNote) { - log.info(`Cannot find note in cache for noteId ${childNoteId}`); + log.info(`Cannot find note in cache for noteId '${childNoteId}'`); return "[error fetching title]"; } @@ -162,7 +162,7 @@ function getNotePath(noteId) { const note = becca.notes[noteId]; if (!note) { - console.trace(`Cannot find note ${noteId} in cache.`); + console.trace(`Cannot find note '${noteId}' in cache.`); return; } diff --git a/src/becca/entities/abstract_entity.js b/src/becca/entities/abstract_entity.js index 9981373c3..574c5bae5 100644 --- a/src/becca/entities/abstract_entity.js +++ b/src/becca/entities/abstract_entity.js @@ -111,6 +111,8 @@ class AbstractEntity { /** * Mark the entity as (soft) deleted. It will be completely erased later. * + * This is a low level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead. + * * @param [deleteId=null] */ markAsDeleted(deleteId = null) { diff --git a/src/becca/entities/branch.js b/src/becca/entities/branch.js index 10ce46a54..8df3febb8 100644 --- a/src/becca/entities/branch.js +++ b/src/becca/entities/branch.js @@ -4,6 +4,10 @@ const Note = require('./note'); const AbstractEntity = require("./abstract_entity"); const sql = require("../../services/sql"); const dateUtils = require("../../services/date_utils"); +const utils = require("../../services/utils.js"); +const TaskContext = require("../../services/task_context.js"); +const cls = require("../../services/cls.js"); +const log = require("../../services/log.js"); /** * Branch represents a relationship between a child note and its parent note. Trilium allows a note to have multiple @@ -114,6 +118,63 @@ class Branch extends AbstractEntity { return !(this.branchId in this.becca.branches); } + /** + * Delete a branch. If this is a last note's branch, delete the note as well. + * + * @param {string} [deleteId] - optional delete identified + * @param {TaskContext} [taskContext] + * + * @return {boolean} - true if note has been deleted, false otherwise + */ + deleteBranch(deleteId, taskContext) { + if (!deleteId) { + deleteId = utils.randomString(10); + } + + if (!taskContext) { + taskContext = new TaskContext('no-progress-reporting'); + } + + taskContext.increaseProgressCount(); + + if (this.branchId === 'root' + || this.noteId === 'root' + || this.noteId === cls.getHoistedNoteId()) { + + throw new Error("Can't delete root or hoisted branch/note"); + } + + this.markAsDeleted(deleteId); + + const note = this.getNote(); + const notDeletedBranches = note.getParentBranches(); + + if (notDeletedBranches.length === 0) { + for (const childBranch of note.getChildBranches()) { + childBranch.deleteBranch(deleteId, taskContext); + } + + // first delete children and then parent - this will show up better in recent changes + + log.info("Deleting note " + note.noteId); + + for (const attribute of note.getOwnedAttributes()) { + attribute.markAsDeleted(deleteId); + } + + for (const relation of note.getTargetRelations()) { + relation.markAsDeleted(deleteId); + } + + note.markAsDeleted(deleteId); + + return true; + } + else { + return false; + } + } + beforeSaving() { if (this.notePosition === undefined || this.notePosition === null) { // TODO finding new position can be refactored into becca diff --git a/src/becca/entities/note.js b/src/becca/entities/note.js index 5f23e2d15..595e41052 100644 --- a/src/becca/entities/note.js +++ b/src/becca/entities/note.js @@ -8,6 +8,7 @@ const dateUtils = require('../../services/date_utils'); const entityChangesService = require('../../services/entity_changes'); const AbstractEntity = require("./abstract_entity"); const NoteRevision = require("./note_revision"); +const TaskContext = require("../../services/task_context.js"); const LABEL = 'label'; const RELATION = 'relation'; @@ -237,7 +238,7 @@ class Note extends AbstractEntity { setContent(content, ignoreMissingProtectedSession = false) { if (content === null || content === undefined) { - throw new Error(`Cannot set null content to note ${this.noteId}`); + throw new Error(`Cannot set null content to note '${this.noteId}'`); } if (this.isStringNote()) { @@ -259,7 +260,7 @@ class Note extends AbstractEntity { 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.`); + throw new Error(`Cannot update content of noteId '${this.noteId}' since we're out of protected session.`); } } @@ -1125,6 +1126,26 @@ class Note extends AbstractEntity { return cloningService.cloneNoteToBranch(this.noteId, branch.branchId); } + /** + * (Soft) delete a note and all its descendants. + * + * @param {string} [deleteId] - optional delete identified + * @param {TaskContext} [taskContext] + */ + deleteNote(deleteId, taskContext) { + if (!deleteId) { + deleteId = utils.randomString(10); + } + + if (!taskContext) { + taskContext = new TaskContext('no-progress-reporting'); + } + + for (const branch of this.getParentBranches()) { + branch.deleteBranch(deleteId, taskContext); + } + } + decrypt() { if (this.isProtected && !this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) { try { diff --git a/src/etapi/branches.js b/src/etapi/branches.js index 71117e339..be6d9f516 100644 --- a/src/etapi/branches.js +++ b/src/etapi/branches.js @@ -71,7 +71,7 @@ function register(router) { return res.sendStatus(204); } - noteService.deleteBranch(branch, null, new TaskContext('no-progress-reporting')); + branch.deleteBranch(); res.sendStatus(204); }); diff --git a/src/etapi/notes.js b/src/etapi/notes.js index b35c296ff..788b0f4ff 100644 --- a/src/etapi/notes.js +++ b/src/etapi/notes.js @@ -98,7 +98,7 @@ function register(router) { return res.sendStatus(204); } - noteService.deleteNote(note, null, new TaskContext('no-progress-reporting')); + note.deleteNote(null, new TaskContext('no-progress-reporting')); res.sendStatus(204); }); diff --git a/src/public/app/entities/note_short.js b/src/public/app/entities/note_short.js index e8bb1de42..3ea507266 100644 --- a/src/public/app/entities/note_short.js +++ b/src/public/app/entities/note_short.js @@ -125,7 +125,7 @@ class NoteShort { return JSON.parse(content); } catch (e) { - console.log(`Cannot parse content of note ${this.noteId}: `, e.message); + console.log(`Cannot parse content of note '${this.noteId}': `, e.message); return null; } diff --git a/src/public/app/services/froca.js b/src/public/app/services/froca.js index abc59d7e0..91af4f1b2 100644 --- a/src/public/app/services/froca.js +++ b/src/public/app/services/froca.js @@ -179,7 +179,7 @@ class Froca { const searchResultNoteIds = await server.get('search-note/' + note.noteId); if (!Array.isArray(searchResultNoteIds)) { - throw new Error(`Search note ${note.noteId} failed: ${searchResultNoteIds}`); + throw new Error(`Search note '${note.noteId}' failed: ${searchResultNoteIds}`); } // reset all the virtual branches from old search results @@ -254,7 +254,7 @@ class Froca { return null; } else if (!noteId) { - console.trace(`Falsy noteId ${noteId}, returning null.`); + console.trace(`Falsy noteId '${noteId}', returning null.`); return null; } @@ -312,7 +312,7 @@ class Froca { if (!this.noteComplementPromises[noteId]) { this.noteComplementPromises[noteId] = server.get('notes/' + noteId) .then(row => new NoteComplement(row)) - .catch(e => console.error(`Cannot get note complement for note ${noteId}`)); + .catch(e => console.error(`Cannot get note complement for note '${noteId}'`)); // we don't want to keep large payloads forever in memory so we clean that up quite quickly // this cache is more meant to share the data between different components within one business transaction (e.g. loading of the note into the tab context and all the components) diff --git a/src/public/app/widgets/buttons/button_widget.js b/src/public/app/widgets/buttons/button_widget.js index d925f2357..92f4d701d 100644 --- a/src/public/app/widgets/buttons/button_widget.js +++ b/src/public/app/widgets/buttons/button_widget.js @@ -71,7 +71,6 @@ export default class ButtonWidget extends NoteContextAwareWidget { } this.$widget - .attr("title", this.settings.title) .addClass(this.settings.icon); } diff --git a/src/public/app/widgets/buttons/edit_button.js b/src/public/app/widgets/buttons/edit_button.js index af70cc6aa..9b458fcca 100644 --- a/src/public/app/widgets/buttons/edit_button.js +++ b/src/public/app/widgets/buttons/edit_button.js @@ -1,5 +1,7 @@ import ButtonWidget from "./button_widget.js"; import appContext from "../../services/app_context.js"; +import attributeService from "../../services/attributes.js"; +import protectedSessionHolder from "../../services/protected_session_holder.js"; export default class EditButton extends ButtonWidget { isEnabled() { @@ -22,9 +24,29 @@ export default class EditButton extends ButtonWidget { } async refreshWithNote(note) { - // can't do this in isEnabled() since isReadOnly is async - this.toggleInt(await this.noteContext.isReadOnly()); + if (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) { + this.toggleInt(false); + } + else { + // prevent flickering by assuming hidden before async operation + this.toggleInt(false); + + // can't do this in isEnabled() since isReadOnly is async + this.toggleInt(await this.noteContext.isReadOnly()); + } await super.refreshWithNote(note); } + + entitiesReloadedEvent({loadResults}) { + if (loadResults.getAttributes().find( + attr => attr.type === 'label' + && attr.name.toLowerCase().includes("readonly") + && attributeService.isAffecting(attr, this.note) + )) { + this.noteContext.readOnlyTemporarilyDisabled = false; + + this.refresh(); + } + } } diff --git a/src/public/app/widgets/note_detail.js b/src/public/app/widgets/note_detail.js index 4c3ddd3a4..f6de7bda5 100644 --- a/src/public/app/widgets/note_detail.js +++ b/src/public/app/widgets/note_detail.js @@ -154,7 +154,8 @@ export default class NoteDetailWidget extends NoteContextAwareWidget { // https://github.com/zadam/trilium/issues/2522 this.$widget.toggleClass("full-height", !this.noteContext.hasNoteList() - && ['editable-text', 'editable-code', 'canvas-note'].includes(this.type)); + && ['editable-text', 'editable-code', 'canvas-note'].includes(this.type) + && this.mime !== 'text/x-sqlite;schema=trilium'); } getTypeWidget() { diff --git a/src/public/app/widgets/type_widgets/editable_text.js b/src/public/app/widgets/type_widgets/editable_text.js index e82ba6d68..d497ecbbe 100644 --- a/src/public/app/widgets/type_widgets/editable_text.js +++ b/src/public/app/widgets/type_widgets/editable_text.js @@ -191,7 +191,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { await this.initialized; this.textEditor.model.change(writer => { - const insertPosition = this.textEditor.model.document.selection.getFirstPosition(); + const insertPosition = this.textEditor.model.document.selection.getLastPosition(); writer.insertText(text, insertPosition); }); } diff --git a/src/public/app/widgets/type_widgets/type_widget.js b/src/public/app/widgets/type_widgets/type_widget.js index 7123b51db..faa1ff98a 100644 --- a/src/public/app/widgets/type_widgets/type_widget.js +++ b/src/public/app/widgets/type_widgets/type_widget.js @@ -1,4 +1,5 @@ import NoteContextAwareWidget from "../note_context_aware_widget.js"; +import appContext from "../../services/app_context.js"; export default class TypeWidget extends NoteContextAwareWidget { // for overriding @@ -34,7 +35,7 @@ export default class TypeWidget extends NoteContextAwareWidget { } isActive() { - return this.$widget.is(":visible"); + return this.$widget.is(":visible") && this.noteContext?.ntxId === appContext.tabManager.activeNtxId; } /** diff --git a/src/routes/api/branches.js b/src/routes/api/branches.js index f05a6e0c3..3af5fe8ed 100644 --- a/src/routes/api/branches.js +++ b/src/routes/api/branches.js @@ -194,7 +194,7 @@ function deleteBranch(req) { const taskContext = TaskContext.getInstance(req.query.taskId, 'delete-notes'); const deleteId = utils.randomString(10); - const noteDeleted = noteService.deleteBranch(branch, deleteId, taskContext); + const noteDeleted = branch.deleteBranch(deleteId, taskContext); if (eraseNotes) { noteService.eraseNotesWithDeleteId(deleteId); diff --git a/src/routes/api/clipper.js b/src/routes/api/clipper.js index d777b9bb9..b8bb158e7 100644 --- a/src/routes/api/clipper.js +++ b/src/routes/api/clipper.js @@ -138,7 +138,7 @@ function processContent(images, note, content) { value: imageNote.noteId }).save(); - log.info(`Replacing ${imageId} with ${url} in note ${note.noteId}`); + log.info(`Replacing '${imageId}' with '${url}' in note '${note.noteId}'`); rewrittenContent = utils.replaceAll(rewrittenContent, imageId, url); } diff --git a/src/routes/api/notes.js b/src/routes/api/notes.js index f602189e2..5f334edc8 100644 --- a/src/routes/api/notes.js +++ b/src/routes/api/notes.js @@ -73,7 +73,7 @@ function deleteNote(req) { const taskContext = TaskContext.getInstance(taskId, 'delete-notes'); - noteService.deleteNote(note, deleteId, taskContext); + note.deleteNote(deleteId, taskContext); if (eraseNotes) { noteService.eraseNotesWithDeleteId(deleteId); @@ -96,7 +96,7 @@ function sortChildNotes(req) { const noteId = req.params.noteId; const {sortBy, sortDirection} = req.body; - log.info(`Sorting ${noteId} children with ${sortBy} ${sortDirection}`); + log.info(`Sorting '${noteId}' children with ${sortBy} ${sortDirection}`); const reverse = sortDirection === 'desc'; @@ -196,11 +196,11 @@ function changeTitle(req) { const note = becca.getNote(noteId); if (!note) { - return [404, `Note ${noteId} has not been found`]; + return [404, `Note '${noteId}' has not been found`]; } if (!note.isContentAvailable()) { - return [400, `Note ${noteId} is not available for change`]; + return [400, `Note '${noteId}' is not available for change`]; } const noteTitleChanged = note.title !== title; @@ -289,10 +289,10 @@ function uploadModifiedFile(req) { const note = becca.getNote(noteId); if (!note) { - return [404, `Note ${noteId} has not been found`]; + return [404, `Note '${noteId}' has not been found`]; } - log.info(`Updating note ${noteId} with content from ${filePath}`); + log.info(`Updating note '${noteId}' with content from ${filePath}`); noteRevisionService.createNoteRevision(note); diff --git a/src/services/build.js b/src/services/build.js index f5dc88301..6717d2293 100644 --- a/src/services/build.js +++ b/src/services/build.js @@ -1 +1 @@ -module.exports = { buildDate:"2022-04-10T14:13:51+02:00", buildRevision: "a04becc4ec653e21c2c80aa9d9ef5b7c9a8e1aa8" }; +module.exports = { buildDate:"2022-05-01T23:18:35+02:00", buildRevision: "b3763eed610fa3f2aabbcbdbd21efca704a5dd08" }; diff --git a/src/services/cloning.js b/src/services/cloning.js index 6fbd3adf1..d929a1cbf 100644 --- a/src/services/cloning.js +++ b/src/services/cloning.js @@ -89,8 +89,7 @@ function ensureNoteIsAbsentFromParent(noteId, parentNoteId) { throw new Error(`Cannot remove branch ${branch.branchId} between child ${noteId} and parent ${parentNoteId} because this would delete the note as well.`); } - const deleteId = utils.randomString(10); - noteService.deleteBranch(branch, deleteId, new TaskContext()); + branch.deleteBranch(); log.info(`Ensured note ${noteId} is NOT in parent note ${parentNoteId}`); } diff --git a/src/services/consistency_checks.js b/src/services/consistency_checks.js index 267c9fec3..dbf4b4478 100644 --- a/src/services/consistency_checks.js +++ b/src/services/consistency_checks.js @@ -56,41 +56,66 @@ class ConsistencyChecks { childToParents[childNoteId].push(parentNoteId); } + /** @returns {boolean} true if cycle was found and we should try again */ const checkTreeCycle = (noteId, path) => { if (noteId === 'root') { - return; - } - - if (!childToParents[noteId] || childToParents[noteId].length === 0) { - logError(`No parents found for note ${noteId}`); - - this.unrecoveredConsistencyErrors = true; - return; + return false; } for (const parentNoteId of childToParents[noteId]) { if (path.includes(parentNoteId)) { - logError(`Tree cycle detected at parent-child relationship: ${parentNoteId} - ${noteId}, whole path: ${path}`); + if (this.autoFix) { + const branch = becca.getBranchFromChildAndParent(noteId, parentNoteId); + branch.markAsDeleted('cycle-autofix'); + logFix(`Branch '${branch.branchId}' between child '${noteId}' and parent '${parentNoteId}' has been deleted since it was causing a tree cycle.`); - this.unrecoveredConsistencyErrors = true; + return true; + } + else { + logError(`Tree cycle detected at parent-child relationship: ${parentNoteId} - ${noteId}, whole path: ${path}`); + + this.unrecoveredConsistencyErrors = true; + } } else { const newPath = path.slice(); newPath.push(noteId); - checkTreeCycle(parentNoteId, newPath); + const retryNeeded = checkTreeCycle(parentNoteId, newPath); + + if (retryNeeded) { + return true; + } } } + + return false; }; const noteIds = Object.keys(childToParents); for (const noteId of noteIds) { - checkTreeCycle(noteId, []); + const retryNeeded = checkTreeCycle(noteId, []); + + if (retryNeeded) { + return true; + } } - if (childToParents['root'].length !== 1 || childToParents['root'][0] !== 'none') { - logError('Incorrect root parent: ' + JSON.stringify(childToParents['root'])); - this.unrecoveredConsistencyErrors = true; + return false; + } + + checkAndRepairTreeCycles() { + let treeFixed = false; + + while (this.checkTreeCycles()) { + // fixing cycle means deleting branches, we might need to create a new branch to recover the note + this.findExistencyIssues(); + + treeFixed = true; + } + + if (treeFixed) { + this.reloadNeeded = true; } } @@ -646,7 +671,7 @@ class ConsistencyChecks { if (!this.unrecoveredConsistencyErrors) { // we run this only if basic checks passed since this assumes basic data consistency - this.checkTreeCycles(); + this.checkAndRepairTreeCycles(); } if (this.reloadNeeded) { diff --git a/src/services/notes.js b/src/services/notes.js index 4d62ae274..41ad60d09 100644 --- a/src/services/notes.js +++ b/src/services/notes.js @@ -17,6 +17,7 @@ const becca = require('../becca/becca'); const Branch = require('../becca/entities/branch'); const Note = require('../becca/entities/note'); const Attribute = require('../becca/entities/attribute'); +const TaskContext = require("./task_context.js"); function getNewNotePosition(parentNoteId) { const note = becca.notes[parentNoteId]; @@ -138,7 +139,7 @@ function createNewNote(params) { triggerNoteTitleChanged(note); triggerChildNoteCreated(note, parentNote); - log.info(`Created new note ${note.noteId}, branch ${branch.branchId} of type ${note.type}, mime ${note.mime}`); + log.info(`Created new note '${note.noteId}', branch '${branch.branchId}' of type '${note.type}', mime '${note.mime}'`); return { note, @@ -284,10 +285,10 @@ async function downloadImage(noteId, imageUrl) { imageUrlToNoteIdMapping[imageUrl] = note.noteId; - log.info(`Download of ${imageUrl} succeeded and was saved as image note ${note.noteId}`); + log.info(`Download of '${imageUrl}' succeeded and was saved as image note '${note.noteId}'`); } catch (e) { - log.error(`Download of ${imageUrl} for note ${noteId} failed with error: ${e.message} ${e.stack}`); + log.error(`Download of '${imageUrl}' for note '${noteId}' failed with error: ${e.message} ${e.stack}`); } } @@ -372,7 +373,7 @@ function downloadImages(noteId, content) { const origNote = becca.getNote(noteId); if (!origNote) { - log.error(`Cannot find note ${noteId} to replace image link.`); + log.error(`Cannot find note '${noteId}' to replace image link.`); return; } @@ -393,7 +394,7 @@ function downloadImages(noteId, content) { scanForLinks(origNote); - console.log(`Fixed the image links for note ${noteId} to the offline saved.`); + console.log(`Fixed the image links for note '${noteId}' to the offline saved.`); } }); }, 5000); @@ -490,7 +491,7 @@ function updateNote(noteId, noteUpdates) { const note = becca.getNote(noteId); if (!note.isContentAvailable()) { - throw new Error(`Note ${noteId} is not available for change!`); + throw new Error(`Note '${noteId}' is not available for change!`); } saveNoteRevision(note); @@ -524,69 +525,6 @@ function updateNote(noteId, noteUpdates) { }; } -/** - * @param {Branch} branch - * @param {string|null} deleteId - * @param {TaskContext} taskContext - * - * @return {boolean} - true if note has been deleted, false otherwise - */ -function deleteBranch(branch, deleteId, taskContext) { - taskContext.increaseProgressCount(); - - if (!branch) { - return false; - } - - if (branch.branchId === 'root' - || branch.noteId === 'root' - || branch.noteId === cls.getHoistedNoteId()) { - - throw new Error("Can't delete root or hoisted branch/note"); - } - - branch.markAsDeleted(deleteId); - - const note = branch.getNote(); - const notDeletedBranches = note.getParentBranches(); - - if (notDeletedBranches.length === 0) { - for (const childBranch of note.getChildBranches()) { - deleteBranch(childBranch, deleteId, taskContext); - } - - // first delete children and then parent - this will show up better in recent changes - - log.info("Deleting note " + note.noteId); - - for (const attribute of note.getOwnedAttributes()) { - attribute.markAsDeleted(deleteId); - } - - for (const relation of note.getTargetRelations()) { - relation.markAsDeleted(deleteId); - } - - note.markAsDeleted(deleteId); - - return true; - } - else { - return false; - } -} - -/** - * @param {Note} note - * @param {string|null} deleteId - * @param {TaskContext} taskContext - */ -function deleteNote(note, deleteId, taskContext) { - for (const branch of note.getParentBranches()) { - deleteBranch(branch, deleteId, taskContext); - } -} - /** * @param {string} noteId * @param {TaskContext} taskContext @@ -595,7 +533,7 @@ function undeleteNote(noteId, taskContext) { const note = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); if (!note.isDeleted) { - log.error(`Note ${noteId} is not deleted and thus cannot be undeleted.`); + log.error(`Note '${noteId}' is not deleted and thus cannot be undeleted.`); return; } @@ -938,8 +876,6 @@ module.exports = { createNewNote, createNewNoteWithTarget, updateNote, - deleteBranch, - deleteNote, undeleteNote, protectNoteRecursively, scanForLinks, diff --git a/src/services/search/expressions/note_content_protected_fulltext.js b/src/services/search/expressions/note_content_protected_fulltext.js index b24d3801a..05130e087 100644 --- a/src/services/search/expressions/note_content_protected_fulltext.js +++ b/src/services/search/expressions/note_content_protected_fulltext.js @@ -10,7 +10,7 @@ const utils = require("../../utils"); // FIXME: create common subclass with NoteContentUnprotectedFulltextExp to avoid duplication class NoteContentProtectedFulltextExp extends Expression { - constructor(operator, tokens, raw) { + constructor(operator, {tokens, raw, flatText}) { super(); if (operator !== '*=*') { @@ -19,6 +19,7 @@ class NoteContentProtectedFulltextExp extends Expression { this.tokens = tokens; this.raw = !!raw; + this.flatText = !!flatText; } execute(inputNoteSet) { @@ -33,7 +34,7 @@ class NoteContentProtectedFulltextExp extends Expression { for (let {noteId, type, mime, content} of sql.iterateRows(` SELECT noteId, type, mime, content FROM notes JOIN note_contents USING (noteId) - WHERE type IN ('text', 'code') AND isDeleted = 0 AND isProtected = 1`)) { + WHERE type IN ('text', 'code', 'mermaid') AND isDeleted = 0 AND isProtected = 1`)) { if (!inputNoteSet.hasNoteId(noteId) || !(noteId in becca.notes)) { continue; @@ -49,7 +50,17 @@ class NoteContentProtectedFulltextExp extends Expression { content = this.preprocessContent(content, type, mime); - if (!this.tokens.find(token => !content.includes(token))) { + const nonMatchingToken = this.tokens.find(token => + !content.includes(token) && + ( + // in case of default fulltext search we should consider both title, attrs and content + // so e.g. "hello world" should match when "hello" is in title and "world" in content + !this.flatText + || !becca.notes[noteId].getFlatText().includes(token) + ) + ); + + if (!nonMatchingToken) { resultNoteSet.add(becca.notes[noteId]); } } diff --git a/src/services/search/expressions/note_content_unprotected_fulltext.js b/src/services/search/expressions/note_content_unprotected_fulltext.js index 60df92570..7abbd0d78 100644 --- a/src/services/search/expressions/note_content_unprotected_fulltext.js +++ b/src/services/search/expressions/note_content_unprotected_fulltext.js @@ -8,7 +8,7 @@ const utils = require("../../utils"); // FIXME: create common subclass with NoteContentProtectedFulltextExp to avoid duplication class NoteContentUnprotectedFulltextExp extends Expression { - constructor(operator, tokens, raw) { + constructor(operator, {tokens, raw, flatText}) { super(); if (operator !== '*=*') { @@ -17,6 +17,7 @@ class NoteContentUnprotectedFulltextExp extends Expression { this.tokens = tokens; this.raw = !!raw; + this.flatText = !!flatText; } execute(inputNoteSet) { @@ -27,7 +28,7 @@ class NoteContentUnprotectedFulltextExp extends Expression { for (let {noteId, type, mime, content} of sql.iterateRows(` SELECT noteId, type, mime, content FROM notes JOIN note_contents USING (noteId) - WHERE type IN ('text', 'code') AND isDeleted = 0 AND isProtected = 0`)) { + WHERE type IN ('text', 'code', 'mermaid') AND isDeleted = 0 AND isProtected = 0`)) { if (!inputNoteSet.hasNoteId(noteId) || !(noteId in becca.notes)) { continue; @@ -35,7 +36,17 @@ class NoteContentUnprotectedFulltextExp extends Expression { content = this.preprocessContent(content, type, mime); - if (!this.tokens.find(token => !content.includes(token))) { + const nonMatchingToken = this.tokens.find(token => + !content.includes(token) && + ( + // in case of default fulltext search we should consider both title, attrs and content + // so e.g. "hello world" should match when "hello" is in title and "world" in content + !this.flatText + || !becca.notes[noteId].getFlatText().includes(token) + ) + ); + + if (!nonMatchingToken) { resultNoteSet.add(becca.notes[noteId]); } } diff --git a/src/services/search/services/parse.js b/src/services/search/services/parse.js index 02d2cc3bb..9ba5ce506 100644 --- a/src/services/search/services/parse.js +++ b/src/services/search/services/parse.js @@ -32,8 +32,8 @@ function getFulltext(tokens, searchContext) { if (!searchContext.fastSearch) { return new OrExp([ new NoteFlatTextExp(tokens), - new NoteContentProtectedFulltextExp('*=*', tokens), - new NoteContentUnprotectedFulltextExp('*=*', tokens) + new NoteContentProtectedFulltextExp('*=*', {tokens, flatText: true}), + new NoteContentUnprotectedFulltextExp('*=*', {tokens, flatText: true}) ]); } else { @@ -141,8 +141,8 @@ function getExpression(tokens, searchContext, level = 0) { i++; return new OrExp([ - new NoteContentUnprotectedFulltextExp(operator, [tokens[i].token], raw), - new NoteContentProtectedFulltextExp(operator, [tokens[i].token], raw) + new NoteContentUnprotectedFulltextExp(operator, {tokens: [tokens[i].token], raw }), + new NoteContentProtectedFulltextExp(operator, {tokens: [tokens[i].token], raw }) ]); } @@ -196,8 +196,8 @@ function getExpression(tokens, searchContext, level = 0) { return new OrExp([ new PropertyComparisonExp(searchContext, 'title', '*=*', tokens[i].token), - new NoteContentProtectedFulltextExp('*=*', [tokens[i].token]), - new NoteContentUnprotectedFulltextExp('*=*', [tokens[i].token]) + new NoteContentProtectedFulltextExp('*=*', {tokens: [tokens[i].token]}), + new NoteContentUnprotectedFulltextExp('*=*', {tokens: [tokens[i].token]}) ]); } diff --git a/src/share/routes.js b/src/share/routes.js index b8678c053..44d1cd26d 100644 --- a/src/share/routes.js +++ b/src/share/routes.js @@ -62,21 +62,23 @@ function register(router) { }); router.get('/share/:shareId', (req, res, next) => { - const {shareId} = req.params; - shacaLoader.ensureLoad(); + const {shareId} = req.params; + const note = shaca.aliasToNote[shareId] || shaca.notes[shareId]; renderNote(note, res); }); router.get('/share/api/notes/:noteId', (req, res, next) => { + shacaLoader.ensureLoad(); + const {noteId} = req.params; const note = shaca.getNote(noteId); if (!note) { - return res.status(404).send(`Note ${noteId} not found`); + return res.status(404).send(`Note '${noteId}' not found`); } addNoIndexHeader(note, res); @@ -85,11 +87,13 @@ function register(router) { }); router.get('/share/api/notes/:noteId/download', (req, res, next) => { + shacaLoader.ensureLoad(); + const {noteId} = req.params; const note = shaca.getNote(noteId); if (!note) { - return res.status(404).send(`Note ${noteId} not found`); + return res.status(404).send(`Note '${noteId}' not found`); } addNoIndexHeader(note, res); @@ -106,11 +110,13 @@ function register(router) { res.send(note.getContent()); }); - router.get(['/share/api/images/:noteId/:filename', '/share/api/images/:noteId'], (req, res, next) => { + router.get('/share/api/images/:noteId/:filename', (req, res, next) => { + shacaLoader.ensureLoad(); + const image = shaca.getNote(req.params.noteId); if (!image) { - return res.status(404).send(`Note ${noteId} not found`); + return res.status(404).send(`Note '${req.params.noteId}' not found`); } else if (!["image", "canvas-note"].includes(image.type)) { return res.status(400).send("Requested note is not a shareable image"); @@ -123,6 +129,7 @@ function register(router) { try { const data = JSON.parse(content) const svg = data.svg || '' + addNoIndexHeader(image, res); res.set('Content-Type', "image/svg+xml"); res.set("Cache-Control", "no-cache, no-store, must-revalidate"); res.send(svg); @@ -132,19 +139,20 @@ function register(router) { } else { // normal image res.set('Content-Type', image.mime); - + addNoIndexHeader(image, res); res.send(image.getContent()); } - }); // used for PDF viewing router.get('/share/api/notes/:noteId/view', (req, res, next) => { + shacaLoader.ensureLoad(); + const {noteId} = req.params; const note = shaca.getNote(noteId); if (!note) { - return res.status(404).send(`Note ${noteId} not found`); + return res.status(404).send(`Note '${noteId}' not found`); } addNoIndexHeader(note, res);