From 29c60581a69486e20878f1f4b343b6b6dbf36136 Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 26 Mar 2019 22:24:04 +0100 Subject: [PATCH] yet another refactoring of working with note's payload/content --- db/migrations/0130__cleanup_note_contents.sql | 13 +++ src/entities/entity.js | 8 +- src/entities/entity_constructor.js | 5 - src/entities/note.js | 69 +++++++----- src/entities/note_content.js | 101 ------------------ src/public/javascripts/dialogs/note_source.js | 2 +- src/public/javascripts/entities/note_full.js | 2 +- .../javascripts/services/note_detail.js | 5 +- .../javascripts/services/note_detail_code.js | 2 +- .../javascripts/services/note_detail_file.js | 4 +- .../services/note_detail_relation_map.js | 4 +- .../services/note_detail_search.js | 2 +- .../javascripts/services/note_detail_text.js | 2 +- .../javascripts/services/note_tooltip.js | 4 +- src/routes/api/file_upload.js | 2 +- src/routes/api/notes.js | 4 +- src/routes/api/search.js | 4 +- src/services/app_info.js | 4 +- src/services/content_hash.js | 25 +++-- src/services/export/single.js | 14 +-- src/services/import/enex.js | 12 +-- src/services/import/tar.js | 5 +- src/services/note_fulltext.js | 6 +- src/services/notes.js | 15 +-- src/services/protected_session.js | 16 ++- src/services/repository.js | 15 --- src/services/script.js | 4 +- src/services/sql_init.js | 6 +- src/services/sync.js | 2 +- src/services/sync_table.js | 4 +- src/services/sync_update.js | 4 +- 31 files changed, 126 insertions(+), 239 deletions(-) create mode 100644 db/migrations/0130__cleanup_note_contents.sql delete mode 100644 src/entities/note_content.js diff --git a/db/migrations/0130__cleanup_note_contents.sql b/db/migrations/0130__cleanup_note_contents.sql new file mode 100644 index 000000000..7663b181c --- /dev/null +++ b/db/migrations/0130__cleanup_note_contents.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS "note_contents_mig" ( + `noteId` TEXT NOT NULL, + `content` TEXT NULL DEFAULT NULL, + `hash` TEXT DEFAULT "" NOT NULL, + `utcDateModified` TEXT NOT NULL, + PRIMARY KEY(`noteId`) +); + +INSERT INTO note_contents_mig (noteId, content, hash, utcDateModified) +SELECT noteId, content, hash, utcDateModified FROM note_contents; + +DROP TABLE note_contents; +ALTER TABLE note_contents_mig RENAME TO note_contents; diff --git a/src/entities/entity.js b/src/entities/entity.js index e88f5ca50..293aec9ae 100644 --- a/src/entities/entity.js +++ b/src/entities/entity.js @@ -26,7 +26,13 @@ class Entity { this.hash = this.generateHash(); - this.isChanged = origHash !== this.hash; + if (this.forcedChange) { + this.isChanged = true; + delete this.forcedChange; + } + else { + this.isChanged = origHash !== this.hash; + } } generateIdIfNecessary() { diff --git a/src/entities/entity_constructor.js b/src/entities/entity_constructor.js index 906ae5b99..b7ee4b313 100644 --- a/src/entities/entity_constructor.js +++ b/src/entities/entity_constructor.js @@ -1,5 +1,4 @@ const Note = require('../entities/note'); -const NoteContent = require('../entities/note_content'); const NoteRevision = require('../entities/note_revision'); const Link = require('../entities/link'); const Branch = require('../entities/branch'); @@ -13,7 +12,6 @@ const ENTITY_NAME_TO_ENTITY = { "attributes": Attribute, "branches": Branch, "notes": Note, - "note_contents": NoteContent, "note_revisions": NoteRevision, "recent_notes": RecentNote, "options": Option, @@ -50,9 +48,6 @@ function createEntityFromRow(row) { else if (row.branchId) { entity = new Branch(row); } - else if (row.noteContentId) { - entity = new NoteContent(row); - } else if (row.noteId) { entity = new Note(row); } diff --git a/src/entities/note.js b/src/entities/note.js index 24d86b0d8..997db0dfb 100644 --- a/src/entities/note.js +++ b/src/entities/note.js @@ -2,12 +2,13 @@ const Entity = require('./entity'); const Attribute = require('./attribute'); -const NoteContent = require('./note_content'); const protectedSessionService = require('../services/protected_session'); const repository = require('../services/repository'); const sql = require('../services/sql'); +const utils = require('../services/utils'); const dateUtils = require('../services/date_utils'); const noteFulltextService = require('../services/note_fulltext'); +const syncTableService = require('../services/sync_table'); const LABEL = 'label'; const LABEL_DEFINITION = 'label-definition'; @@ -56,37 +57,33 @@ class Note extends Entity { protectedSessionService.decryptNote(this); } else { - // saving ciphertexts in case we do want to update protected note outside of protected session - // (which is allowed) - this.titleCipherText = this.title; this.title = "[protected]"; } } } - /** @returns {Promise} */ - async getNoteContent() { - if (!this.noteContent) { - this.noteContent = await repository.getEntity(`SELECT * FROM note_contents WHERE noteId = ?`, [this.noteId]); + /** @returns {Promise<*>} */ + async getContent() { + if (this.content === undefined) { + this.content = await sql.getValue(`SELECT content FROM note_contents WHERE noteId = ?`, [this.noteId]); - if (!this.noteContent) { - throw new Error("Note content not found for noteId=" + this.noteId); + if (this.isProtected) { + if (this.isContentAvailable) { + protectedSessionService.decryptNoteContent(this); + } + else { + this.content = ""; + } } if (this.isStringNote()) { - this.noteContent.content = this.noteContent.content === null - ? "" : this.noteContent.content.toString("UTF-8"); + this.content = this.content === null + ? "" + : this.content.toString("UTF-8"); } } - return this.noteContent; - } - - /** @returns {Promise<*>} */ - async getContent() { - const noteContent = await this.getNoteContent(); - - return noteContent.content; + return this.content; } /** @returns {Promise<*>} */ @@ -98,14 +95,31 @@ class Note extends Entity { /** @returns {Promise} */ async setContent(content) { - if (!this.noteContent) { - // make sure it is loaded - await this.getNoteContent(); + this.content = content; + + const pojo = { + noteId: this.noteId, + content: content, + utcDateModified: dateUtils.utcNowDateTime(), + hash: utils.hash(this.noteId + "|" + content) + }; + + if (this.isProtected) { + if (this.isContentAvailable) { + protectedSessionService.encryptNoteContent(pojo); + } + else { + throw new Error(`Cannot update content of noteId=${this.noteId} since we're out of protected session.`); + } } - this.noteContent.content = content; + await sql.upsert("note_contents", "noteId", pojo); - await this.noteContent.save(); + await syncTableService.addNoteContentSync(this.noteId); + + this.forcedChange = true; + + await this.save(); } /** @returns {Promise} */ @@ -687,14 +701,13 @@ class Note extends Entity { } else { // updating protected note outside of protected session means we will keep original ciphertexts - pojo.title = pojo.titleCipherText; + delete pojo.title; } } delete pojo.isContentAvailable; delete pojo.__attributeCache; - delete pojo.titleCipherText; - delete pojo.noteContent; + delete pojo.content; } async afterSaving() { diff --git a/src/entities/note_content.js b/src/entities/note_content.js deleted file mode 100644 index b2aab8625..000000000 --- a/src/entities/note_content.js +++ /dev/null @@ -1,101 +0,0 @@ -"use strict"; - -const Entity = require('./entity'); -const protectedSessionService = require('../services/protected_session'); -const repository = require('../services/repository'); -const dateUtils = require('../services/date_utils'); -const noteFulltextService = require('../services/note_fulltext'); - -/** - * This represents a Note which is a central object in the Trilium Notes project. - * - * @property {string} noteContentId - primary key - * @property {string} noteId - reference to owning note - * @property {boolean} isProtected - true if note content is protected - * @property {blob} content - note content - e.g. HTML text for text notes, file payload for files - * @property {string} utcDateCreated - * @property {string} utcDateModified - * - * @extends Entity - */ -class NoteContent extends Entity { - static get entityName() { - return "note_contents"; - } - - static get primaryKeyName() { - return "noteContentId"; - } - - static get hashedProperties() { - return ["noteContentId", "noteId", "isProtected", "content"]; - } - - /** - * @param row - object containing database row from "note_contents" table - */ - constructor(row) { - super(row); - - this.isProtected = !!this.isProtected; - /* true if content (meaning any kind of potentially encrypted content) is either not encrypted - * or encrypted, but with available protected session (so effectively decrypted) */ - this.isContentAvailable = true; - - // check if there's noteContentId, otherwise this is a new entity which wasn't encrypted yet - if (this.isProtected && this.noteContentId) { - this.isContentAvailable = protectedSessionService.isProtectedSessionAvailable(); - - if (this.isContentAvailable) { - protectedSessionService.decryptNoteContent(this); - } - else { - // saving ciphertexts in case we do want to update protected note outside of protected session - // (which is allowed) - this.contentCipherText = this.content; - this.content = ""; - } - } - } - - /** - * @returns {Promise} - */ - async getNote() { - return await repository.getNote(this.noteId); - } - - beforeSaving() { - if (!this.utcDateCreated) { - this.utcDateCreated = dateUtils.utcNowDateTime(); - } - - super.beforeSaving(); - - if (this.isChanged) { - this.utcDateModified = dateUtils.utcNowDateTime(); - } - } - - // cannot be static! - updatePojo(pojo) { - if (pojo.isProtected) { - if (this.isContentAvailable) { - protectedSessionService.encryptNoteContent(pojo); - } - else { - // updating protected note outside of protected session means we will keep original ciphertext - pojo.content = pojo.contentCipherText; - } - } - - delete pojo.isContentAvailable; - delete pojo.contentCipherText; - } - - async afterSaving() { - noteFulltextService.triggerNoteFulltextUpdate(this.noteId); - } -} - -module.exports = NoteContent; \ No newline at end of file diff --git a/src/public/javascripts/dialogs/note_source.js b/src/public/javascripts/dialogs/note_source.js index c7db2b586..528bedb4b 100644 --- a/src/public/javascripts/dialogs/note_source.js +++ b/src/public/javascripts/dialogs/note_source.js @@ -8,7 +8,7 @@ function showDialog() { $dialog.modal(); - const noteText = noteDetailService.getActiveNote().noteContent.content; + const noteText = noteDetailService.getActiveNote().content; $noteSource.text(formatHtml(noteText)); } diff --git a/src/public/javascripts/entities/note_full.js b/src/public/javascripts/entities/note_full.js index f8ec6d068..6f104e018 100644 --- a/src/public/javascripts/entities/note_full.js +++ b/src/public/javascripts/entities/note_full.js @@ -8,7 +8,7 @@ class NoteFull extends NoteShort { super(treeCache, row); /** @param {string} */ - this.noteContent = row.noteContent; + this.content = row.content; /** @param {string} */ this.utcDateCreated = row.utcDateCreated; diff --git a/src/public/javascripts/services/note_detail.js b/src/public/javascripts/services/note_detail.js index 4faddef5a..f779e1d2a 100644 --- a/src/public/javascripts/services/note_detail.js +++ b/src/public/javascripts/services/note_detail.js @@ -117,10 +117,7 @@ async function saveNote() { } note.title = $noteTitle.val(); - - if (note.noteContent != null) { // might be null for file/image - note.noteContent.content = getActiveNoteContent(note); - } + note.content = getActiveNoteContent(note); // it's important to set the flag back to false immediatelly after retrieving title and content // otherwise we might overwrite another change (especially async code) diff --git a/src/public/javascripts/services/note_detail_code.js b/src/public/javascripts/services/note_detail_code.js index a250f9010..e4949d859 100644 --- a/src/public/javascripts/services/note_detail_code.js +++ b/src/public/javascripts/services/note_detail_code.js @@ -49,7 +49,7 @@ async function show() { // this needs to happen after the element is shown, otherwise the editor won't be refreshed // CodeMirror breaks pretty badly on null so even though it shouldn't happen (guarded by consistency check) // we provide fallback - codeEditor.setValue(activeNote.noteContent.content || ""); + codeEditor.setValue(activeNote.content || ""); const info = CodeMirror.findModeByMIME(activeNote.mime); diff --git a/src/public/javascripts/services/note_detail_file.js b/src/public/javascripts/services/note_detail_file.js index d8f044900..109f1abcd 100644 --- a/src/public/javascripts/services/note_detail_file.js +++ b/src/public/javascripts/services/note_detail_file.js @@ -27,9 +27,9 @@ async function show() { $fileSize.text((attributeMap.fileSize || "?") + " bytes"); $fileType.text(activeNote.mime); - if (activeNote.noteContent && activeNote.noteContent.content) { + if (activeNote.content) { $previewRow.show(); - $previewContent.text(activeNote.noteContent.content); + $previewContent.text(activeNote.content); } else { $previewRow.hide(); diff --git a/src/public/javascripts/services/note_detail_relation_map.js b/src/public/javascripts/services/note_detail_relation_map.js index 2fe9f5d36..ee3c1ddff 100644 --- a/src/public/javascripts/services/note_detail_relation_map.js +++ b/src/public/javascripts/services/note_detail_relation_map.js @@ -92,9 +92,9 @@ function loadMapData() { } }; - if (activeNote.noteContent.content) { + if (activeNote.content) { try { - mapData = JSON.parse(activeNote.noteContent.content); + mapData = JSON.parse(activeNote.content); } catch (e) { console.log("Could not parse content: ", e); } diff --git a/src/public/javascripts/services/note_detail_search.js b/src/public/javascripts/services/note_detail_search.js index 9973a26a1..c6e27ee0b 100644 --- a/src/public/javascripts/services/note_detail_search.js +++ b/src/public/javascripts/services/note_detail_search.js @@ -10,7 +10,7 @@ function show() { $component.show(); try { - const json = JSON.parse(noteDetailService.getActiveNote().noteContent.content); + const json = JSON.parse(noteDetailService.getActiveNote().content); $searchString.val(json.searchString); } diff --git a/src/public/javascripts/services/note_detail_text.js b/src/public/javascripts/services/note_detail_text.js index 3adb34064..d4da96df9 100644 --- a/src/public/javascripts/services/note_detail_text.js +++ b/src/public/javascripts/services/note_detail_text.js @@ -31,7 +31,7 @@ async function show() { $component.show(); - textEditor.setData(noteDetailService.getActiveNote().noteContent.content); + textEditor.setData(noteDetailService.getActiveNote().content); } function getContent() { diff --git a/src/public/javascripts/services/note_tooltip.js b/src/public/javascripts/services/note_tooltip.js index c546bdd77..5e87a853f 100644 --- a/src/public/javascripts/services/note_tooltip.js +++ b/src/public/javascripts/services/note_tooltip.js @@ -113,11 +113,11 @@ async function renderTooltip(note, attributes) { if (note.type === 'text') { // surround with
for a case when note's content is pure text (e.g. "[protected]") which // then fails the jquery non-empty text test - content += '
' + note.noteContent.content + '
'; + content += '
' + note.content + '
'; } else if (note.type === 'code') { content += $("
")
-            .text(note.noteContent.content)
+            .text(note.content)
             .prop('outerHTML');
     }
     else if (note.type === 'image') {
diff --git a/src/routes/api/file_upload.js b/src/routes/api/file_upload.js
index 663432b2f..6d2053973 100644
--- a/src/routes/api/file_upload.js
+++ b/src/routes/api/file_upload.js
@@ -51,7 +51,7 @@ async function downloadNoteFile(noteId, res) {
     res.setHeader('Content-Disposition', utils.getContentDisposition(fileName));
     res.setHeader('Content-Type', note.mime);
 
-    res.send((await note.getNoteContent()).content);
+    res.send(await note.getContent());
 }
 
 async function downloadFile(req, res) {
diff --git a/src/routes/api/notes.js b/src/routes/api/notes.js
index cdb9c078f..f16e5f34f 100644
--- a/src/routes/api/notes.js
+++ b/src/routes/api/notes.js
@@ -13,10 +13,10 @@ async function getNote(req) {
     }
 
     if (note.isStringNote()) {
-        const noteContent = await note.getNoteContent();
+        await note.getContent();
 
         if (note.type === 'file') {
-            noteContent.content = noteContent.content.substr(0, 10000);
+            note.content = note.content.substr(0, 10000);
         }
     }
 
diff --git a/src/routes/api/search.js b/src/routes/api/search.js
index c1076caf5..4a5a179a4 100644
--- a/src/routes/api/search.js
+++ b/src/routes/api/search.js
@@ -14,11 +14,11 @@ async function searchNotes(req) {
 }
 
 async function saveSearchToNote(req) {
-    const noteContent = {
+    const content = {
         searchString: req.params.searchString
     };
 
-    const {note} = await noteService.createNote('root', req.params.searchString, noteContent, {
+    const {note} = await noteService.createNote('root', req.params.searchString, content, {
         json: true,
         type: 'search',
         mime: "application/json"
diff --git a/src/services/app_info.js b/src/services/app_info.js
index 1863e5135..6eaa01d9f 100644
--- a/src/services/app_info.js
+++ b/src/services/app_info.js
@@ -4,8 +4,8 @@ const build = require('./build');
 const packageJson = require('../../package');
 const {TRILIUM_DATA_DIR} = require('./data_dir');
 
-const APP_DB_VERSION = 129;
-const SYNC_VERSION = 7;
+const APP_DB_VERSION = 130;
+const SYNC_VERSION = 8;
 
 module.exports = {
     appVersion: packageJson.version,
diff --git a/src/services/content_hash.js b/src/services/content_hash.js
index 2b3de34c5..69b0d9745 100644
--- a/src/services/content_hash.js
+++ b/src/services/content_hash.js
@@ -8,17 +8,16 @@ const messagingService = require('./messaging');
 const ApiToken = require('../entities/api_token');
 const Branch = require('../entities/branch');
 const Note = require('../entities/note');
-const NoteContent = require('../entities/note_content');
 const Attribute = require('../entities/attribute');
 const NoteRevision = require('../entities/note_revision');
 const RecentNote = require('../entities/recent_note');
 const Option = require('../entities/option');
 const Link = require('../entities/link');
 
-async function getHash(entityConstructor, whereBranch) {
+async function getHash(tableName, primaryKeyName, whereBranch) {
     // subselect is necessary to have correct ordering in GROUP_CONCAT
-    const query = `SELECT GROUP_CONCAT(hash) FROM (SELECT hash FROM ${entityConstructor.entityName} `
-        + (whereBranch ? `WHERE ${whereBranch} ` : '') + `ORDER BY ${entityConstructor.primaryKeyName})`;
+    const query = `SELECT GROUP_CONCAT(hash) FROM (SELECT hash FROM ${tableName} `
+        + (whereBranch ? `WHERE ${whereBranch} ` : '') + `ORDER BY ${primaryKeyName})`;
 
     let contentToHash = await sql.getValue(query);
 
@@ -33,15 +32,15 @@ async function getHashes() {
     const startTime = new Date();
 
     const hashes = {
-        notes: await getHash(Note),
-        note_contents: await getHash(NoteContent),
-        branches: await getHash(Branch),
-        note_revisions: await getHash(NoteRevision),
-        recent_notes: await getHash(RecentNote),
-        options: await getHash(Option, "isSynced = 1"),
-        attributes: await getHash(Attribute),
-        api_tokens: await getHash(ApiToken),
-        links: await getHash(Link)
+        notes: await getHash(Note.entityName, Note.primaryKeyName),
+        note_contents: await getHash("note_contents", "noteId"),
+        branches: await getHash(Branch.entityName, Branch.primaryKeyName),
+        note_revisions: await getHash(NoteRevision.entityName, NoteRevision.primaryKeyName),
+        recent_notes: await getHash(RecentNote.entityName, RecentNote.primaryKeyName),
+        options: await getHash(Option.entityName, Option.primaryKeyName, "isSynced = 1"),
+        attributes: await getHash(Attribute.entityName, Attribute.primaryKeyName),
+        api_tokens: await getHash(ApiToken.entityName, ApiToken.primaryKeyName),
+        links: await getHash(Link.entityName, Link.primaryKeyName)
     };
 
     const elapseTimeMs = Date.now() - startTime.getTime();
diff --git a/src/services/export/single.js b/src/services/export/single.js
index 3e14e485c..dce90e1aa 100644
--- a/src/services/export/single.js
+++ b/src/services/export/single.js
@@ -18,32 +18,32 @@ async function exportSingleNote(exportContext, branch, format, res) {
 
     let payload, extension, mime;
 
-    const noteContent = await note.getNoteContent();
+    let content = await note.getContent();
 
     if (note.type === 'text') {
         if (format === 'html') {
-            if (!noteContent.content.toLowerCase().includes("';
+            if (!content.toLowerCase().includes("';
             }
 
-            payload = html.prettyPrint(noteContent.content, {indent_size: 2});
+            payload = html.prettyPrint(content, {indent_size: 2});
             extension = 'html';
             mime = 'text/html';
         }
         else if (format === 'markdown') {
             const turndownService = new TurndownService();
-            payload = turndownService.turndown(noteContent.content);
+            payload = turndownService.turndown(content);
             extension = 'md';
             mime = 'text/x-markdown'
         }
     }
     else if (note.type === 'code') {
-        payload = noteContent.content;
+        payload = content;
         extension = mimeTypes.extension(note.mime) || 'code';
         mime = note.mime;
     }
     else if (note.type === 'relation-map' || note.type === 'search') {
-        payload = noteContent.content;
+        payload = content;
         extension = 'json';
         mime = 'application/json';
     }
diff --git a/src/services/import/enex.js b/src/services/import/enex.js
index be93aaa68..6ec07fb1a 100644
--- a/src/services/import/enex.js
+++ b/src/services/import/enex.js
@@ -223,7 +223,7 @@ async function importEnex(importContext, file, parentNote) {
 
         importContext.increaseProgressCount();
 
-        const noteContent = await noteEntity.getNoteContent();
+        let noteContent = await noteEntity.getContent();
 
         for (const resource of resources) {
             const hash = utils.md5(resource.content);
@@ -248,7 +248,7 @@ async function importEnex(importContext, file, parentNote) {
 
                 const resourceLink = `${utils.escapeHtml(resource.title)}`;
 
-                noteContent.content = noteContent.content.replace(mediaRegex, resourceLink);
+                noteContent = noteContent.replace(mediaRegex, resourceLink);
             };
 
             if (["image/jpeg", "image/png", "image/gif"].includes(resource.mime)) {
@@ -259,12 +259,12 @@ async function importEnex(importContext, file, parentNote) {
 
                     const imageLink = ``;
 
-                    noteContent.content = noteContent.content.replace(mediaRegex, imageLink);
+                    noteContent = noteContent.replace(mediaRegex, imageLink);
 
-                    if (!noteContent.content.includes(imageLink)) {
+                    if (!noteContent.includes(imageLink)) {
                         // if there wasn't any match for the reference, we'll add the image anyway
                         // otherwise image would be removed since no note would include it
-                        noteContent.content += imageLink;
+                        noteContent += imageLink;
                     }
                 } catch (e) {
                     log.error("error when saving image from ENEX file: " + e);
@@ -276,7 +276,7 @@ async function importEnex(importContext, file, parentNote) {
         }
 
         // save updated content with links to files/images
-        await noteContent.save();
+        await noteEntity.setContent(noteContent);
     }
 
     saxStream.on("closetag", async tag => {
diff --git a/src/services/import/tar.js b/src/services/import/tar.js
index 3b1eb1353..08e321a09 100644
--- a/src/services/import/tar.js
+++ b/src/services/import/tar.js
@@ -259,10 +259,7 @@ async function importTar(importContext, fileBuffer, importRootNote) {
         let note = await repository.getNote(noteId);
 
         if (note) {
-            const noteContent = await note.getNoteContent();
-
-            noteContent.content = content;
-            await noteContent.save();
+            await note.setContent(content);
         }
         else {
             const noteTitle = getNoteTitle(filePath, noteMeta);
diff --git a/src/services/note_fulltext.js b/src/services/note_fulltext.js
index 44155d010..f1338baa1 100644
--- a/src/services/note_fulltext.js
+++ b/src/services/note_fulltext.js
@@ -14,14 +14,14 @@ async function updateNoteFulltext(note) {
         let contentHash = null;
 
         if (['text', 'code'].includes(note.type)) {
-            const noteContent = await note.getNoteContent();
-            content = noteContent.content;
+            content = await note.getContent();
 
             if (note.type === 'text' && note.mime === 'text/html') {
                 content = html2plaintext(content);
             }
 
-            contentHash = noteContent.hash;
+            // FIXME
+            //contentHash = noteContent.hash;
         }
 
         // optimistically try to update first ...
diff --git a/src/services/notes.js b/src/services/notes.js
index 49db80439..48666ef82 100644
--- a/src/services/notes.js
+++ b/src/services/notes.js
@@ -8,7 +8,6 @@ const eventService = require('./events');
 const repository = require('./repository');
 const cls = require('../services/cls');
 const Note = require('../entities/note');
-const NoteContent = require('../entities/note_content');
 const Link = require('../entities/link');
 const NoteRevision = require('../entities/note_revision');
 const Branch = require('../entities/branch');
@@ -93,10 +92,7 @@ async function createNewNote(parentNoteId, noteData) {
         noteData.content = noteData.content || "";
     }
 
-    note.noteContent = await new NoteContent({
-        noteId: note.noteId,
-        content: noteData.content
-    }).save();
+    await note.setContent(noteData.content);
 
     const branch = await new Branch({
         noteId: note.noteId,
@@ -338,16 +334,11 @@ async function updateNote(noteId, noteUpdates) {
     note.isProtected = noteUpdates.isProtected;
     await note.save();
 
-    const noteContent = await note.getNoteContent();
-
     if (!['file', 'image'].includes(note.type)) {
-        noteUpdates.noteContent.content = await saveLinks(note, noteUpdates.noteContent.content);
-
-        noteContent.content = noteUpdates.noteContent.content;
+        noteUpdates.content = await saveLinks(note, noteUpdates.content);
     }
 
-    noteContent.isProtected = noteUpdates.isProtected;
-    await noteContent.save();
+    await note.setContent(noteUpdates.content);
 
     if (noteTitleChanged) {
         await triggerNoteTitleChanged(note);
diff --git a/src/services/protected_session.js b/src/services/protected_session.js
index 5a24d5ef9..b18815d38 100644
--- a/src/services/protected_session.js
+++ b/src/services/protected_session.js
@@ -56,18 +56,14 @@ function decryptNote(note) {
     }
 }
 
-function decryptNoteContent(noteContent) {
-    if (!noteContent.isProtected) {
-        return;
-    }
-
+function decryptNoteContent(note) {
     try {
-        if (noteContent.content != null) {
-            noteContent.content = dataEncryptionService.decrypt(getDataKey(), noteContent.content.toString());
+        if (note.content != null) {
+            note.content = dataEncryptionService.decrypt(getDataKey(), note.content.toString());
         }
     }
     catch (e) {
-        e.message = `Cannot decrypt note content for noteContentId=${noteContent.noteContentId}: ` + e.message;
+        e.message = `Cannot decrypt content for noteId=${note.noteId}: ` + e.message;
         throw e;
     }
 }
@@ -98,8 +94,8 @@ function encryptNote(note) {
     note.title = dataEncryptionService.encrypt(getDataKey(), note.title);
 }
 
-function encryptNoteContent(noteContent) {
-    noteContent.content = dataEncryptionService.encrypt(getDataKey(), noteContent.content);
+function encryptNoteContent(note) {
+    note.content = dataEncryptionService.encrypt(getDataKey(), note.content);
 }
 
 function encryptNoteRevision(revision) {
diff --git a/src/services/repository.js b/src/services/repository.js
index c96ffb794..45c004ee4 100644
--- a/src/services/repository.js
+++ b/src/services/repository.js
@@ -42,19 +42,6 @@ async function getNote(noteId) {
     return await getEntity("SELECT * FROM notes WHERE noteId = ?", [noteId]);
 }
 
-/** @returns {Promise} */
-async function getNoteWithContent(noteId) {
-    const note = await getEntity("SELECT * FROM notes WHERE noteId = ?", [noteId]);
-    await note.getNoteContent();
-
-    return note;
-}
-
-/** @returns {Promise} */
-async function getNoteContent(noteContentId) {
-    return await getEntity("SELECT * FROM note_contents WHERE noteContentId = ?", [noteContentId]);
-}
-
 /** @returns {Promise} */
 async function getBranch(branchId) {
     return await getEntity("SELECT * FROM branches WHERE branchId = ?", [branchId]);
@@ -138,8 +125,6 @@ module.exports = {
     getEntities,
     getEntity,
     getNote,
-    getNoteWithContent,
-    getNoteContent,
     getBranch,
     getAttribute,
     getOption,
diff --git a/src/services/script.js b/src/services/script.js
index d667e76b2..f3e10166e 100644
--- a/src/services/script.js
+++ b/src/services/script.js
@@ -58,10 +58,10 @@ async function executeBundle(bundle, apiParams = {}) {
  */
 async function executeScript(script, params, startNoteId, currentNoteId, originEntityName, originEntityId) {
     const startNote = await repository.getNote(startNoteId);
-    const currentNote = await repository.getNoteWithContent(currentNoteId);
+    const currentNote = await repository.getNote(currentNoteId);
     const originEntity = await repository.getEntityFromName(originEntityName, originEntityId);
 
-    currentNote.noteContent.content = `return await (${script}\r\n)(${getParams(params)})`;
+    currentNote.content = `return await (${script}\r\n)(${getParams(params)})`;
     currentNote.type = 'code';
     currentNote.mime = 'application/javascript;env=backend';
 
diff --git a/src/services/sql_init.js b/src/services/sql_init.js
index b744d18df..60ad22163 100644
--- a/src/services/sql_init.js
+++ b/src/services/sql_init.js
@@ -78,7 +78,6 @@ async function createInitialDatabase(username, password) {
         await sql.executeScript(schema);
 
         const Note = require("../entities/note");
-        const NoteContent = require("../entities/note_content");
         const Branch = require("../entities/branch");
 
         const rootNote = await new Note({
@@ -88,10 +87,7 @@ async function createInitialDatabase(username, password) {
             mime: 'text/html'
         }).save();
 
-        const rootNoteContent = await new NoteContent({
-            noteId: rootNote.noteId,
-            content: ''
-        }).save();
+        await rootNote.setContent('');
 
         await new Branch({
             branchId: 'root',
diff --git a/src/services/sync.js b/src/services/sync.js
index a7b51e2a4..a216f97c9 100644
--- a/src/services/sync.js
+++ b/src/services/sync.js
@@ -239,7 +239,7 @@ async function syncRequest(syncContext, method, requestPath, body) {
 
 const primaryKeys = {
     "notes": "noteId",
-    "note_contents": "noteContentId",
+    "note_contents": "noteId",
     "branches": "branchId",
     "note_revisions": "noteRevisionId",
     "recent_notes": "branchId",
diff --git a/src/services/sync_table.js b/src/services/sync_table.js
index 7cd4ae923..1aa2c2c7f 100644
--- a/src/services/sync_table.js
+++ b/src/services/sync_table.js
@@ -8,8 +8,8 @@ async function addNoteSync(noteId, sourceId) {
     await addEntitySync("notes", noteId, sourceId)
 }
 
-async function addNoteContentSync(noteContentId, sourceId) {
-    await addEntitySync("note_contents", noteContentId, sourceId)
+async function addNoteContentSync(noteId, sourceId) {
+    await addEntitySync("note_contents", noteId, sourceId)
 }
 
 async function addBranchSync(branchId, sourceId) {
diff --git a/src/services/sync_update.js b/src/services/sync_update.js
index 21538c9c3..28636927a 100644
--- a/src/services/sync_update.js
+++ b/src/services/sync_update.js
@@ -77,12 +77,12 @@ async function updateNoteContent(entity, sourceId) {
         await sql.transactional(async () => {
             await sql.replace("note_contents", entity);
 
-            await syncTableService.addNoteContentSync(entity.noteContentId, sourceId);
+            await syncTableService.addNoteContentSync(entity.noteId, sourceId);
 
             noteFulltextService.triggerNoteFulltextUpdate(entity.noteId);
         });
 
-        log.info("Update/sync note content " + entity.noteContentId);
+        log.info("Update/sync note content for noteId=" + entity.noteId);
     }
 }