diff --git a/migrations/0021__sync_add_source_id.sql b/migrations/0021__sync_add_source_id.sql new file mode 100644 index 000000000..59315fbf4 --- /dev/null +++ b/migrations/0021__sync_add_source_id.sql @@ -0,0 +1 @@ +ALTER TABLE sync ADD COLUMN source_id TEXT \ No newline at end of file diff --git a/migrations/0022__add_note_history_id.sql b/migrations/0022__add_note_history_id.sql new file mode 100644 index 000000000..f47295552 --- /dev/null +++ b/migrations/0022__add_note_history_id.sql @@ -0,0 +1,7 @@ +ALTER TABLE notes_history ADD COLUMN note_history_id TEXT; + +UPDATE notes_history SET note_history_id = id; + +CREATE UNIQUE INDEX `IDX_note_history_note_history_id` ON `notes_history` ( + `note_history_id` +); \ No newline at end of file diff --git a/routes/api/notes.js b/routes/api/notes.js index ea7b399d1..9035e53a3 100644 --- a/routes/api/notes.js +++ b/routes/api/notes.js @@ -14,8 +14,8 @@ router.get('/:noteId', auth.checkApiAuth, async (req, res, next) => { let detail = await sql.getSingleResult("select * from notes where note_id = ?", [noteId]); - if (detail['note_clone_id']) { - noteId = detail['note_clone_id']; + if (detail.note_clone_id) { + noteId = detail.note_clone_id; detail = sql.getSingleResult("select * from notes where note_id = ?", [noteId]); } @@ -30,8 +30,8 @@ router.put('/:noteId', async (req, res, next) => { const detail = await sql.getSingleResult("select * from notes where note_id = ?", [noteId]); - if (detail['note_clone_id']) { - noteId = detail['note_clone_id']; + if (detail.note_clone_id) { + noteId = detail.note_clone_id; } const note = req.body; @@ -42,63 +42,71 @@ router.put('/:noteId', async (req, res, next) => { const historyCutoff = now - historySnapshotTimeInterval; - const history = await sql.getSingleResult("select id from notes_history where note_id = ? and date_modified_from >= ?", [noteId, historyCutoff]); + let noteHistoryId = await sql.getSingleValue("select note_history_id from notes_history where note_id = ? and date_modified_from >= ?", [noteId, historyCutoff]); await sql.doInTransaction(async () => { - if (history) { - await sql.execute("update notes_history set note_title = ?, note_text = ?, encryption = ?, date_modified_to = ? where id = ?", [ - note['detail']['note_title'], - note['detail']['note_text'], - note['detail']['encryption'], + if (noteHistoryId) { + await sql.execute("update notes_history set note_title = ?, note_text = ?, encryption = ?, date_modified_to = ? where note_history_id = ?", [ + note.detail.note_title, + note.detail.note_text, + note.detail.encryption, now, - history['id'] + noteHistoryId ]); } else { - await sql.execute("insert into notes_history (note_id, note_title, note_text, encryption, date_modified_from, date_modified_to) values (?, ?, ?, ?, ?, ?)", [ + noteHistoryId = utils.randomString(16); + + await sql.execute("insert into notes_history (note_history_id, note_id, note_title, note_text, encryption, date_modified_from, date_modified_to) " + + "values (?, ?, ?, ?, ?, ?, ?)", [ + noteHistoryId, noteId, - note['detail']['note_title'], - note['detail']['note_text'], - note['detail']['encryption'], + note.detail.note_title, + note.detail.note_text, + note.detail.encryption, now, now ]); } - if (note['detail']['note_title'] !== detail['note_title']) { + await sql.addNoteHistorySync(noteHistoryId); + + if (note.detail.note_title !== detail.note_title) { await sql.deleteRecentAudits(audit_category.UPDATE_TITLE, req, noteId); await sql.addAudit(audit_category.UPDATE_TITLE, req, noteId); } - if (note['detail']['note_text'] !== detail['note_text']) { + if (note.detail.note_text !== detail.note_text) { await sql.deleteRecentAudits(audit_category.UPDATE_CONTENT, req, noteId); await sql.addAudit(audit_category.UPDATE_CONTENT, req, noteId); } - if (note['detail']['encryption'] !== detail['encryption']) { - await sql.addAudit(audit_category.ENCRYPTION, req, noteId, detail['encryption'], note['detail']['encryption']); + if (note.detail.encryption !== detail.encryption) { + await sql.addAudit(audit_category.ENCRYPTION, req, noteId, detail.encryption, note.detail.encryption); } await sql.execute("update notes set note_title = ?, note_text = ?, encryption = ?, date_modified = ? where note_id = ?", [ - note['detail']['note_title'], - note['detail']['note_text'], - note['detail']['encryption'], + note.detail.note_title, + note.detail.note_text, + note.detail.encryption, now, noteId]); await sql.remove("images", noteId); - for (const img of note['images']) { - img['image_data'] = atob(img['image_data']); + for (const img of note.images) { + img.image_data = atob(img.image_data); await sql.insert("images", img); } await sql.remove("links", noteId); - for (const link in note['links']) { + for (const link in note.links) { await sql.insert("links", link); } + + await sql.addNoteSync(noteId); }); res.send({}); @@ -118,7 +126,7 @@ async function deleteNote(noteId, req) { const children = await sql.getResults("select note_id from notes_tree where note_pid = ? and is_deleted = 0", [noteId]); for (const child of children) { - await deleteNote(child['note_id']); + await deleteNote(child.note_id); } await sql.execute("update notes_tree set is_deleted = 1, date_modified = ? where note_id = ?", [now, noteId]); @@ -140,7 +148,7 @@ router.post('/:parentNoteId/children', async (req, res, next) => { let newNotePos = 0; - if (note['target'] === 'into') { + if (note.target === 'into') { const res = await sql.getSingleResult('select max(note_pos) as max_note_pos from notes_tree where note_pid = ? and is_deleted = 0', [parentNoteId]); const maxNotePos = res['max_note_pos']; @@ -149,17 +157,17 @@ router.post('/:parentNoteId/children', async (req, res, next) => { else newNotePos = maxNotePos + 1 } - else if (note['target'] === 'after') { - const afterNote = await sql.getSingleResult('select note_pos from notes_tree where note_id = ?', [note['target_note_id']]); + else if (note.target === 'after') { + const afterNote = await sql.getSingleResult('select note_pos from notes_tree where note_id = ?', [note.target_note_id]); - newNotePos = afterNote['note_pos'] + 1; + newNotePos = afterNote.note_pos + 1; const now = utils.nowTimestamp(); await sql.execute('update notes_tree set note_pos = note_pos + 1, date_modified = ? where note_pid = ? and note_pos > ? and is_deleted = 0', [now, parentNoteId, afterNote['note_pos']]); } else { - throw new ('Unknown target: ' + note['target']); + throw new Error('Unknown target: ' + note.target); } await sql.doInTransaction(async () => { @@ -169,12 +177,12 @@ router.post('/:parentNoteId/children', async (req, res, next) => { await sql.insert("notes", { 'note_id': noteId, - 'note_title': note['note_title'], + 'note_title': note.note_title, 'note_text': '', 'note_clone_id': '', 'date_created': now, 'date_modified': now, - 'encryption': note['encryption'] + 'encryption': note.encryption }); await sql.insert("notes_tree", { @@ -200,7 +208,7 @@ router.get('/', async (req, res, next) => { const noteIdList = []; for (const res of result) { - noteIdList.push(res['note_id']); + noteIdList.push(res.note_id); } res.send(noteIdList); diff --git a/routes/api/notes_move.js b/routes/api/notes_move.js index 8659f1f3b..295bbc15a 100644 --- a/routes/api/notes_move.js +++ b/routes/api/notes_move.js @@ -26,6 +26,7 @@ router.put('/:noteId/moveTo/:parentId', auth.checkApiAuth, async (req, res, next await sql.execute("update notes_tree set note_pid = ?, note_pos = ?, date_modified = ? where note_id = ?", [parentId, newNotePos, now, noteId]); + await sql.addNoteTreeSync(noteId); await sql.addAudit(audit_category.CHANGE_PARENT, req, noteId, null, parentId); }); @@ -47,6 +48,7 @@ router.put('/:noteId/moveBefore/:beforeNoteId', async (req, res, next) => { await sql.execute("update notes_tree set note_pid = ?, note_pos = ?, date_modified = ? where note_id = ?", [beforeNote['note_pid'], beforeNote['note_pos'], now, noteId]); + await sql.addNoteTreeSync(noteId); await sql.addAudit(audit_category.CHANGE_POSITION, req, noteId); }); } @@ -70,6 +72,7 @@ router.put('/:noteId/moveAfter/:afterNoteId', async (req, res, next) => { await sql.execute("update notes_tree set note_pid = ?, note_pos = ?, date_modified = ? where note_id = ?", [afterNote['note_pid'], afterNote['note_pos'] + 1, now, noteId]); + await sql.addNoteTreeSync(noteId); await sql.addAudit(audit_category.CHANGE_POSITION, req, noteId); }); } @@ -85,6 +88,7 @@ router.put('/:noteId/expanded/:expanded', async (req, res, next) => { await sql.doInTransaction(async () => { await sql.execute("update notes_tree set is_expanded = ?, date_modified = ? where note_id = ?", [expanded, now, noteId]); + await sql.addNoteTreeSync(noteId); await sql.addAudit(audit_category.CHANGE_EXPANDED, req, noteId, null, expanded); }); diff --git a/services/migration.js b/services/migration.js index 22682dccd..25db3109a 100644 --- a/services/migration.js +++ b/services/migration.js @@ -3,7 +3,7 @@ const sql = require('./sql'); const fs = require('fs-extra'); const log = require('./log'); -const APP_DB_VERSION = 20; +const APP_DB_VERSION = 22; const MIGRATIONS_DIR = "./migrations"; async function migrate() { diff --git a/services/source_id.js b/services/source_id.js new file mode 100644 index 000000000..9721d99fb --- /dev/null +++ b/services/source_id.js @@ -0,0 +1,3 @@ +const utils = require('./utils'); + +module.exports = utils.randomString(16); \ No newline at end of file diff --git a/services/sql.js b/services/sql.js index 03dbfc358..34a2f705a 100644 --- a/services/sql.js +++ b/services/sql.js @@ -3,6 +3,7 @@ const db = require('sqlite'); const utils = require('./utils'); const log = require('./log'); +const SOURCE_ID = require('./source_id'); async function insert(table_name, rec, replace = false) { const keys = Object.keys(rec); @@ -123,8 +124,32 @@ async function deleteRecentAudits(category, req, noteId) { [category, browserId, noteId, deleteCutoff]) } +async function addNoteSync(noteId, sourceId) { + await addEntitySync("notes", noteId, sourceId) +} + +async function addNoteTreeSync(noteId, sourceId) { + await addEntitySync("notes_tree", noteId, sourceId) +} + +async function addNoteHistorySync(noteHistoryId, sourceId) { + await addEntitySync("notes_history", noteHistoryId, sourceId); +} + +async function addEntitySync(entityName, entityId, sourceId) { + await replace("sync", { + entity_name: entityName, + entity_id: entityId, + sync_date: utils.nowTimestamp(), + source_id: sourceId || SOURCE_ID + }); +} + async function doInTransaction(func) { + const error = new Error(); // to capture correct stack trace in case of exception + try { + await beginTransaction(); await func(); @@ -132,9 +157,11 @@ async function doInTransaction(func) { await commit(); } catch (e) { - log.error("Error executing transaction, executing rollback. Inner exception: " + e.stack); + log.error("Error executing transaction, executing rollback. Inner exception: " + e.stack + error.stack); await rollback(); + + throw e; } } @@ -153,5 +180,8 @@ module.exports = { addAudit, deleteRecentAudits, remove, - doInTransaction + doInTransaction, + addNoteSync, + addNoteTreeSync, + addNoteHistorySync }; \ No newline at end of file diff --git a/services/sync.js b/services/sync.js index b3b7b3ee1..a4b6ae411 100644 --- a/services/sync.js +++ b/services/sync.js @@ -8,6 +8,7 @@ const utils = require('./utils'); const config = require('./config'); const audit_category = require('./audit_category'); const crypto = require('crypto'); +const SOURCE_ID = require('./source_id'); const SYNC_SERVER = config['Sync']['syncServerHost']; @@ -72,6 +73,7 @@ async function pullSync(cookieJar, syncLog) { async function syncEntity(entity, entityName, cookieJar, syncLog) { try { const payload = { + sourceId: SOURCE_ID, entity: entity }; @@ -269,6 +271,8 @@ async function updateNote(body, syncLog) { await sql.insert('link', link); } + + await sql.addNoteSync(entity.note_id, body.source_id); }); logSync("Update/sync note " + entity.note_id, syncLog); @@ -284,7 +288,11 @@ async function updateNoteTree(body, syncLog) { const orig = await sql.getSingleResultOrNull("select * from notes_tree where note_id = ?", [entity.note_id]); if (orig === null || orig.date_modified < entity.date_modified) { - await sql.replace('notes_tree', entity); + await sql.doInTransaction(async () => { + await sql.replace('notes_tree', entity); + + await sql.addNoteTreeSync(entity.note_id, body.source_id); + }); logSync("Update/sync note tree " + entity.note_id, syncLog); } @@ -296,14 +304,18 @@ async function updateNoteTree(body, syncLog) { async function updateNoteHistory(body, syncLog) { const entity = body.entity; - const orig = await sql.getSingleResultOrNull("select * from notes_history where note_id = ? and date_modified_from = ?", [entity.note_id, entity.date_modified_from]); + const orig = await sql.getSingleResultOrNull("select * from notes_history where note_history_id", [entity.note_history_id]); if (orig === null || orig.date_modified_to < entity.date_modified_to) { - await sql.execute("delete from notes_history where note_id = ? and date_modified_from = ?", [entity.note_id, entity.date_modified_from]); + await sql.doInTransaction(async () => { + await sql.execute("delete from notes_history where note_history_id", [entity.note_history_id]); - delete entity['id']; + delete entity['id']; - await sql.insert('notes_history', entity); + await sql.insert('notes_history', entity); + + await sql.addNoteHistorySync(entity.note_history_id, body.source_id); + }); logSync("Update/sync note history " + entity.note_id, syncLog); }