From eddb3ed58a4408e52f82a972bcefe00bb7091774 Mon Sep 17 00:00:00 2001 From: azivner Date: Tue, 19 Dec 2017 19:31:02 -0500 Subject: [PATCH 1/8] hook on hash change which allows us navigation with alt-left, alt-right --- public/javascripts/note_tree.js | 12 +++++++++++- services/build.js | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/public/javascripts/note_tree.js b/public/javascripts/note_tree.js index 63ebb5a18..b6031678f 100644 --- a/public/javascripts/note_tree.js +++ b/public/javascripts/note_tree.js @@ -505,12 +505,16 @@ const noteTree = (function() { await getTree().reload(notes); } + function getNotePathFromAddress() { + return document.location.hash.substr(1); // strip initial # + } + function loadTree() { return server.get('tree').then(resp => { startNotePath = resp.start_note_path; if (document.location.hash) { - startNotePath = document.location.hash.substr(1); // strip initial # + startNotePath = getNotePathFromAddress(); } return prepareNoteTree(resp.notes); @@ -668,6 +672,12 @@ const noteTree = (function() { $(document).bind('keydown', 'ctrl+.', scrollToCurrentNote); + $(window).bind('hashchange', function() { + const notePath = getNotePathFromAddress(); + + activateNode(notePath); + }); + return { reload, collapseTree, diff --git a/services/build.js b/services/build.js index 13a6403c2..46382d5f5 100644 --- a/services/build.js +++ b/services/build.js @@ -1 +1 @@ -module.exports = { build_date:"2017-12-18T00:01:16-05:00", build_revision: "f96e38fd13152eee4700ead265c5b255b8e6853e" }; +module.exports = { build_date:"2017-12-18T23:45:10-05:00", build_revision: "b0e2d99a7b1073e9ee593b386afa19a62a2651eb" }; From bb2f47deb2d2a60d13247c069201f27ed35b8604 Mon Sep 17 00:00:00 2001 From: azivner Date: Tue, 19 Dec 2017 19:54:55 -0500 Subject: [PATCH 2/8] alt-left and alt-right navigation also for electron --- public/javascripts/note_tree.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/public/javascripts/note_tree.js b/public/javascripts/note_tree.js index b6031678f..25a2f5007 100644 --- a/public/javascripts/note_tree.js +++ b/public/javascripts/note_tree.js @@ -678,6 +678,20 @@ const noteTree = (function() { activateNode(notePath); }); + if (isElectron()) { + $(document).bind('keydown', 'alt+left', e => { + window.history.back(); + + e.preventDefault(); + }); + + $(document).bind('keydown', 'alt+right', e => { + window.history.forward(); + + e.preventDefault(); + }); + } + return { reload, collapseTree, From 72712bc24b299eae1655b97544598a8d35e7dce2 Mon Sep 17 00:00:00 2001 From: azivner Date: Tue, 19 Dec 2017 21:40:48 -0500 Subject: [PATCH 3/8] added foreign keys to relations to guarantee data consistency --- migrations/0057__add_foreign_keys.sql | 101 ++++++++++++++++++++++++++ public/javascripts/context_menu.js | 2 +- public/javascripts/note_tree.js | 14 ++-- public/javascripts/tree_changes.js | 6 +- routes/api/export.js | 4 +- routes/api/import.js | 6 +- routes/api/notes_move.js | 48 ++++++------ routes/api/sync.js | 4 +- routes/api/tree.js | 2 +- schema.sql | 8 +- services/app_info.js | 2 +- services/consistency_checks.js | 6 +- services/content_hash.js | 4 +- services/notes.js | 18 ++--- services/sql.js | 4 +- services/sync.js | 4 +- services/sync_update.js | 4 +- 17 files changed, 171 insertions(+), 66 deletions(-) create mode 100644 migrations/0057__add_foreign_keys.sql diff --git a/migrations/0057__add_foreign_keys.sql b/migrations/0057__add_foreign_keys.sql new file mode 100644 index 000000000..96cf9e4ec --- /dev/null +++ b/migrations/0057__add_foreign_keys.sql @@ -0,0 +1,101 @@ +INSERT INTO notes (note_id, note_title, note_text, date_created, date_modified) + VALUES ('root', 'root', 'root', strftime('%Y-%m-%dT%H:%M:%S.000Z', 'now'), strftime('%Y-%m-%dT%H:%M:%S.000Z', 'now')); + +CREATE TABLE IF NOT EXISTS "notes_mig" ( + `note_id` TEXT NOT NULL, + `note_title` TEXT, + `note_text` TEXT, + `is_protected` INT NOT NULL DEFAULT 0, + `is_deleted` INT NOT NULL DEFAULT 0, + `date_created` TEXT NOT NULL, + `date_modified` TEXT NOT NULL, + PRIMARY KEY(`note_id`) +); + +INSERT INTO notes_mig (note_id, note_title, note_text, is_protected, is_deleted, date_created, date_modified) + SELECT note_id, note_title, note_text, is_protected, is_deleted, date_created, date_modified FROM notes; + +DROP TABLE notes; +ALTER TABLE notes_mig RENAME TO notes; + +CREATE INDEX `IDX_notes_is_deleted` ON `notes` ( + `is_deleted` +); + +CREATE TABLE IF NOT EXISTS "notes_tree_mig" ( + `note_tree_id` TEXT NOT NULL, + `note_id` TEXT NOT NULL, + `parent_note_id` TEXT NOT NULL, + `note_position` INTEGER NOT NULL, + `prefix` TEXT, + `is_expanded` BOOLEAN, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `date_modified` TEXT NOT NULL, + FOREIGN KEY(note_id) REFERENCES notes(note_id), + FOREIGN KEY(parent_note_id) REFERENCES notes(note_id), + PRIMARY KEY(`note_tree_id`) +); + +INSERT INTO notes_tree_mig (note_tree_id, note_id, parent_note_id, note_position, prefix, is_expanded, is_deleted, date_modified) + SELECT note_tree_id, note_id, note_pid, note_pos, prefix, is_expanded, is_deleted, date_modified FROM notes_tree; + +DROP TABLE notes_tree; +ALTER TABLE notes_tree_mig RENAME TO notes_tree; + +CREATE INDEX `IDX_notes_tree_note_tree_id` ON `notes_tree` ( + `note_tree_id` +); +CREATE INDEX `IDX_notes_tree_note_id_parent_note_id` ON `notes_tree` ( + `note_id`, + `parent_note_id` +); +CREATE INDEX `IDX_notes_tree_note_id` ON `notes_tree` ( + `note_id` +); + +CREATE TABLE IF NOT EXISTS "notes_history_mig" ( + `note_history_id` TEXT NOT NULL PRIMARY KEY, + `note_id` TEXT NOT NULL, + `note_title` TEXT, + `note_text` TEXT, + `is_protected` INT NOT NULL DEFAULT 0, + `date_modified_from` TEXT NOT NULL, + `date_modified_to` TEXT NOT NULL, + FOREIGN KEY(note_id) REFERENCES notes(note_id) +); + +INSERT INTO notes_history_mig (note_history_id, note_id, note_title, note_text, is_protected, date_modified_from, date_modified_to) + SELECT note_history_id, note_id, note_title, note_text, is_protected, date_modified_from, date_modified_to FROM notes_history; + +DROP TABLE notes_history; +ALTER TABLE notes_history_mig RENAME TO notes_history; + +CREATE INDEX `IDX_notes_history_note_id` ON `notes_history` ( + `note_id` +); +CREATE INDEX `IDX_notes_history_note_date_modified_from` ON `notes_history` ( + `date_modified_from` +); +CREATE INDEX `IDX_notes_history_note_date_modified_to` ON `notes_history` ( + `date_modified_to` +); + +DROP TABLE recent_notes; + +CREATE TABLE `recent_notes` ( + `note_tree_id` TEXT NOT NULL PRIMARY KEY, + `note_path` TEXT NOT NULL, + `date_accessed` TEXT NOT NULL, + is_deleted INT, + FOREIGN KEY(note_tree_id) REFERENCES notes_tree(note_tree_id) +); + +DROP TABLE event_log; + +CREATE TABLE `event_log` ( + `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + `note_id` TEXT, + `comment` TEXT, + `date_added` TEXT NOT NULL, + FOREIGN KEY(note_id) REFERENCES notes(note_id) +); \ No newline at end of file diff --git a/public/javascripts/context_menu.js b/public/javascripts/context_menu.js index 59e498479..536ca9bc5 100644 --- a/public/javascripts/context_menu.js +++ b/public/javascripts/context_menu.js @@ -88,7 +88,7 @@ const contextMenu = (function() { const node = $.ui.fancytree.getNode(ui.target); if (ui.cmd === "insertNoteHere") { - const parentNoteId = node.data.note_pid; + const parentNoteId = node.data.parent_note_id; const isProtected = treeUtils.getParentProtectedStatus(node); noteTree.createNote(node, parentNoteId, 'after', isProtected); diff --git a/public/javascripts/note_tree.js b/public/javascripts/note_tree.js index 25a2f5007..0fbcf15a7 100644 --- a/public/javascripts/note_tree.js +++ b/public/javascripts/note_tree.js @@ -132,7 +132,7 @@ const noteTree = (function() { delete note.note_title; // this should not be used. Use noteIdToTitle instead - setParentChildRelation(note.note_tree_id, note.note_pid, note.note_id); + setParentChildRelation(note.note_tree_id, note.parent_note_id, note.note_id); } return prepareNoteTreeInner('root'); @@ -171,7 +171,7 @@ const noteTree = (function() { const node = { note_id: noteTree.note_id, - note_pid: noteTree.note_pid, + parent_note_id: noteTree.parent_note_id, note_tree_id: noteTree.note_tree_id, is_protected: noteTree.is_protected, prefix: noteTree.prefix, @@ -207,7 +207,7 @@ const noteTree = (function() { //console.log(now(), "Run path: ", runPath); for (const childNoteId of runPath) { - const node = getNodesByNoteId(childNoteId).find(node => node.data.note_pid === parentNoteId); + const node = getNodesByNoteId(childNoteId).find(node => node.data.parent_note_id === parentNoteId); if (childNoteId === noteId) { await node.setActive(); @@ -334,6 +334,10 @@ const noteTree = (function() { while (cur !== 'root') { path.push(cur); + if (!childToParents[cur]) { + throwError("Can't find parents for " + cur); + } + cur = childToParents[cur][0]; } @@ -614,7 +618,7 @@ const noteTree = (function() { const newNode = { title: newNoteName, note_id: result.note_id, - note_pid: parentNoteId, + parent_note_id: parentNoteId, refKey: result.note_id, note_tree_id: result.note_tree_id, is_protected: isProtected, @@ -646,7 +650,7 @@ const noteTree = (function() { console.log("pressed O"); const node = getCurrentNode(); - const parentNoteId = node.data.note_pid; + const parentNoteId = node.data.parent_note_id; const isProtected = treeUtils.getParentProtectedStatus(node); createNote(node, parentNoteId, 'after', isProtected); diff --git a/public/javascripts/tree_changes.js b/public/javascripts/tree_changes.js index cc2a3cbb9..3a16b8e68 100644 --- a/public/javascripts/tree_changes.js +++ b/public/javascripts/tree_changes.js @@ -96,13 +96,13 @@ const treeChanges = (function() { } function changeNode(node, func) { - noteTree.removeParentChildRelation(node.data.note_pid, node.data.note_id); + noteTree.removeParentChildRelation(node.data.parent_note_id, node.data.note_id); func(node); - node.data.note_pid = node.getParent() === null ? 'root' : node.getParent().data.note_id; + node.data.parent_note_id = node.getParent() === null ? 'root' : node.getParent().data.note_id; - noteTree.setParentChildRelation(node.data.note_tree_id, node.data.note_pid, node.data.note_id); + noteTree.setParentChildRelation(node.data.note_tree_id, node.data.parent_note_id, node.data.note_id); noteTree.setCurrentNotePathToHash(node); } diff --git a/routes/api/export.js b/routes/api/export.js index cfa4d539e..70545859d 100644 --- a/routes/api/export.js +++ b/routes/api/export.js @@ -35,11 +35,11 @@ async function exportNote(noteTreeId, dir) { const noteTree = await sql.getSingleResult("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]); const note = await sql.getSingleResult("SELECT * FROM notes WHERE note_id = ?", [noteTree.note_id]); - const pos = (noteTree.note_pos + '').padStart(4, '0'); + const pos = (noteTree.note_position + '').padStart(4, '0'); fs.writeFileSync(dir + '/' + pos + '-' + note.note_title + '.html', html.prettyPrint(note.note_text, {indent_size: 2})); - const children = await sql.getResults("SELECT * FROM notes_tree WHERE note_pid = ? AND is_deleted = 0", [note.note_id]); + const children = await sql.getResults("SELECT * FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0", [note.note_id]); if (children.length > 0) { const childrenDir = dir + '/' + pos + '-' + note.note_title; diff --git a/routes/api/import.js b/routes/api/import.js index 0b8fe969e..a9b629dd6 100644 --- a/routes/api/import.js +++ b/routes/api/import.js @@ -51,7 +51,7 @@ async function importNotes(dir, parentNoteId) { noteTitle = match[2]; } else { - let maxPos = await sql.getSingleValue("SELECT MAX(note_pos) FROM notes_tree WHERE note_pid = ? AND is_deleted = 0", [parentNoteId]); + let maxPos = await sql.getSingleValue("SELECT MAX(note_position) FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0", [parentNoteId]); if (maxPos) { notePos = maxPos + 1; } @@ -72,8 +72,8 @@ async function importNotes(dir, parentNoteId) { await sql.insert('notes_tree', { note_tree_id: noteTreeId, note_id: noteId, - note_pid: parentNoteId, - note_pos: notePos, + parent_note_id: parentNoteId, + note_position: notePos, is_expanded: 0, is_deleted: 0, date_modified: now diff --git a/routes/api/notes_move.js b/routes/api/notes_move.js index 5bb76d1fc..49b8ab583 100644 --- a/routes/api/notes_move.js +++ b/routes/api/notes_move.js @@ -12,13 +12,13 @@ router.put('/:noteTreeId/move-to/:parentNoteId', auth.checkApiAuth, async (req, const parentNoteId = req.params.parentNoteId; const sourceId = req.headers.source_id; - const maxNotePos = await sql.getSingleValue('SELECT MAX(note_pos) FROM notes_tree WHERE note_pid = ? AND is_deleted = 0', [parentNoteId]); + const maxNotePos = await sql.getSingleValue('SELECT MAX(note_position) FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0', [parentNoteId]); const newNotePos = maxNotePos === null ? 0 : maxNotePos + 1; const now = utils.nowDate(); await sql.doInTransaction(async () => { - await sql.execute("UPDATE notes_tree SET note_pid = ?, note_pos = ?, date_modified = ? WHERE note_tree_id = ?", + await sql.execute("UPDATE notes_tree SET parent_note_id = ?, note_position = ?, date_modified = ? WHERE note_tree_id = ?", [parentNoteId, newNotePos, now, noteTreeId]); await sync_table.addNoteTreeSync(noteTreeId, sourceId); @@ -38,15 +38,15 @@ router.put('/:noteTreeId/move-before/:beforeNoteTreeId', async (req, res, next) await sql.doInTransaction(async () => { // we don't change date_modified so other changes are prioritized in case of conflict // also we would have to sync all those modified note trees otherwise hash checks would fail - await sql.execute("UPDATE notes_tree SET note_pos = note_pos + 1 WHERE note_pid = ? AND note_pos >= ? AND is_deleted = 0", - [beforeNote.note_pid, beforeNote.note_pos]); + await sql.execute("UPDATE notes_tree SET note_position = note_position + 1 WHERE parent_note_id = ? AND note_position >= ? AND is_deleted = 0", + [beforeNote.parent_note_id, beforeNote.note_position]); - await sync_table.addNoteReorderingSync(beforeNote.note_pid, sourceId); + await sync_table.addNoteReorderingSync(beforeNote.parent_note_id, sourceId); const now = utils.nowDate(); - await sql.execute("UPDATE notes_tree SET note_pid = ?, note_pos = ?, date_modified = ? WHERE note_tree_id = ?", - [beforeNote.note_pid, beforeNote.note_pos, now, noteTreeId]); + await sql.execute("UPDATE notes_tree SET parent_note_id = ?, note_position = ?, date_modified = ? WHERE note_tree_id = ?", + [beforeNote.parent_note_id, beforeNote.note_position, now, noteTreeId]); await sync_table.addNoteTreeSync(noteTreeId, sourceId); }); @@ -69,13 +69,13 @@ router.put('/:noteTreeId/move-after/:afterNoteTreeId', async (req, res, next) => await sql.doInTransaction(async () => { // we don't change date_modified so other changes are prioritized in case of conflict // also we would have to sync all those modified note trees otherwise hash checks would fail - await sql.execute("UPDATE notes_tree SET note_pos = note_pos + 1 WHERE note_pid = ? AND note_pos > ? AND is_deleted = 0", - [afterNote.note_pid, afterNote.note_pos]); + await sql.execute("UPDATE notes_tree SET note_position = note_position + 1 WHERE parent_note_id = ? AND note_position > ? AND is_deleted = 0", + [afterNote.parent_note_id, afterNote.note_position]); - await sync_table.addNoteReorderingSync(afterNote.note_pid, sourceId); + await sync_table.addNoteReorderingSync(afterNote.parent_note_id, sourceId); - await sql.execute("UPDATE notes_tree SET note_pid = ?, note_pos = ?, date_modified = ? WHERE note_tree_id = ?", - [afterNote.note_pid, afterNote.note_pos + 1, utils.nowDate(), noteTreeId]); + await sql.execute("UPDATE notes_tree SET parent_note_id = ?, note_position = ?, date_modified = ? WHERE note_tree_id = ?", + [afterNote.parent_note_id, afterNote.note_position + 1, utils.nowDate(), noteTreeId]); await sync_table.addNoteTreeSync(noteTreeId, sourceId); }); @@ -92,7 +92,7 @@ router.put('/:childNoteId/clone-to/:parentNoteId', auth.checkApiAuth, async (req const childNoteId = req.params.childNoteId; const sourceId = req.headers.source_id; - const existing = await sql.getSingleValue('SELECT * FROM notes_tree WHERE note_id = ? AND note_pid = ?', [childNoteId, parentNoteId]); + const existing = await sql.getSingleValue('SELECT * FROM notes_tree WHERE note_id = ? AND parent_note_id = ?', [childNoteId, parentNoteId]); if (existing && !existing.is_deleted) { return res.send({ @@ -108,15 +108,15 @@ router.put('/:childNoteId/clone-to/:parentNoteId', auth.checkApiAuth, async (req }); } - const maxNotePos = await sql.getSingleValue('SELECT MAX(note_pos) FROM notes_tree WHERE note_pid = ? AND is_deleted = 0', [parentNoteId]); + const maxNotePos = await sql.getSingleValue('SELECT MAX(note_position) FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0', [parentNoteId]); const newNotePos = maxNotePos === null ? 0 : maxNotePos + 1; await sql.doInTransaction(async () => { const noteTree = { note_tree_id: utils.newNoteTreeId(), note_id: childNoteId, - note_pid: parentNoteId, - note_pos: newNotePos, + parent_note_id: parentNoteId, + note_position: newNotePos, is_expanded: 0, date_modified: utils.nowDate(), is_deleted: 0 @@ -143,14 +143,14 @@ router.put('/:noteId/clone-after/:afterNoteTreeId', async (req, res, next) => { return res.status(500).send("After note " + afterNoteTreeId + " doesn't exist."); } - if (!await checkCycle(afterNote.note_pid, noteId)) { + if (!await checkCycle(afterNote.parent_note_id, noteId)) { return res.send({ success: false, message: 'Cloning note here would create cycle.' }); } - const existing = await sql.getSingleValue('SELECT * FROM notes_tree WHERE note_id = ? AND note_pid = ?', [noteId, afterNote.note_pid]); + const existing = await sql.getSingleValue('SELECT * FROM notes_tree WHERE note_id = ? AND parent_note_id = ?', [noteId, afterNote.parent_note_id]); if (existing && !existing.is_deleted) { return res.send({ @@ -162,16 +162,16 @@ router.put('/:noteId/clone-after/:afterNoteTreeId', async (req, res, next) => { await sql.doInTransaction(async () => { // we don't change date_modified so other changes are prioritized in case of conflict // also we would have to sync all those modified note trees otherwise hash checks would fail - await sql.execute("UPDATE notes_tree SET note_pos = note_pos + 1 WHERE note_pid = ? AND note_pos > ? AND is_deleted = 0", - [afterNote.note_pid, afterNote.note_pos]); + await sql.execute("UPDATE notes_tree SET note_position = note_position + 1 WHERE parent_note_id = ? AND note_position > ? AND is_deleted = 0", + [afterNote.parent_note_id, afterNote.note_position]); - await sync_table.addNoteReorderingSync(afterNote.note_pid, sourceId); + await sync_table.addNoteReorderingSync(afterNote.parent_note_id, sourceId); const noteTree = { note_tree_id: utils.newNoteTreeId(), note_id: noteId, - note_pid: afterNote.note_pid, - note_pos: afterNote.note_pos + 1, + parent_note_id: afterNote.parent_note_id, + note_position: afterNote.note_position + 1, is_expanded: 0, date_modified: utils.nowDate(), is_deleted: 0 @@ -196,7 +196,7 @@ async function checkCycle(parentNoteId, childNoteId) { return false; } - const parentNoteIds = await sql.getFlattenedResults("SELECT DISTINCT note_pid FROM notes_tree WHERE note_id = ?", [parentNoteId]); + const parentNoteIds = await sql.getFlattenedResults("SELECT DISTINCT parent_note_id FROM notes_tree WHERE note_id = ?", [parentNoteId]); for (const pid of parentNoteIds) { if (!await checkCycle(pid, childNoteId)) { diff --git a/routes/api/sync.js b/routes/api/sync.js index ae1d54d01..b90b8ebd4 100644 --- a/routes/api/sync.js +++ b/routes/api/sync.js @@ -73,8 +73,8 @@ router.get('/notes_reordering/:noteTreeParentId', auth.checkApiAuth, async (req, const noteTreeParentId = req.params.noteTreeParentId; res.send({ - note_pid: noteTreeParentId, - ordering: await sql.getMap("SELECT note_tree_id, note_pos FROM notes_tree WHERE note_pid = ?", [noteTreeParentId]) + parent_note_id: noteTreeParentId, + ordering: await sql.getMap("SELECT note_tree_id, note_position FROM notes_tree WHERE parent_note_id = ?", [noteTreeParentId]) }); }); diff --git a/routes/api/tree.js b/routes/api/tree.js index 14fdb909e..ddef3ff09 100644 --- a/routes/api/tree.js +++ b/routes/api/tree.js @@ -19,7 +19,7 @@ router.get('/', auth.checkApiAuth, async (req, res, next) => { + "FROM notes_tree " + "JOIN notes ON notes.note_id = notes_tree.note_id " + "WHERE notes.is_deleted = 0 AND notes_tree.is_deleted = 0 " - + "ORDER BY note_pos"); + + "ORDER BY note_position"); const dataKey = protected_session.getDataKey(req); diff --git a/schema.sql b/schema.sql index be727bc2d..2527f3df0 100644 --- a/schema.sql +++ b/schema.sql @@ -70,8 +70,8 @@ CREATE TABLE `recent_notes` ( CREATE TABLE IF NOT EXISTS "notes_tree" ( `note_tree_id` TEXT NOT NULL, `note_id` TEXT NOT NULL, - `note_pid` TEXT NOT NULL, - `note_pos` INTEGER NOT NULL, + `parent_note_id` TEXT NOT NULL, + `note_position` INTEGER NOT NULL, `prefix` TEXT, `is_expanded` BOOLEAN, `is_deleted` INTEGER NOT NULL DEFAULT 0, @@ -81,7 +81,7 @@ CREATE TABLE IF NOT EXISTS "notes_tree" ( CREATE INDEX `IDX_notes_tree_note_id` ON `notes_tree` ( `note_id` ); -CREATE INDEX `IDX_notes_tree_note_id_note_pid` ON `notes_tree` ( +CREATE INDEX `IDX_notes_tree_note_id_parent_note_id` ON `notes_tree` ( `note_id`, - `note_pid` + `parent_note_id` ); diff --git a/services/app_info.js b/services/app_info.js index fd2bf3baf..bd51115a9 100644 --- a/services/app_info.js +++ b/services/app_info.js @@ -3,7 +3,7 @@ const build = require('./build'); const packageJson = require('../package'); -const APP_DB_VERSION = 56; +const APP_DB_VERSION = 57; module.exports = { app_version: packageJson.version, diff --git a/services/consistency_checks.js b/services/consistency_checks.js index 10ce17449..aacd5f19d 100644 --- a/services/consistency_checks.js +++ b/services/consistency_checks.js @@ -16,7 +16,7 @@ async function runCheck(query, errorText, errorList) { } async function runSyncRowChecks(table, key, errorList) { - await runCheck(`SELECT ${key} FROM ${table} LEFT JOIN sync ON sync.entity_name = '${table}' AND entity_id = ${key} WHERE sync.id IS NULL`, + await runCheck(`SELECT ${key} FROM ${table} LEFT JOIN sync ON sync.entity_name = '${table}' AND entity_id = ${key} WHERE entity_id != 'root' AND sync.id IS NULL`, `Missing sync records for ${key} in table ${table}`, errorList); await runCheck(`SELECT entity_id FROM sync LEFT JOIN ${table} ON entity_id = ${key} WHERE sync.entity_name = '${table}' AND ${key} IS NULL`, @@ -26,7 +26,7 @@ async function runSyncRowChecks(table, key, errorList) { async function runChecks() { const errorList = []; - await runCheck("SELECT note_id FROM notes LEFT JOIN notes_tree USING(note_id) WHERE notes_tree.note_tree_id IS NULL", + await runCheck("SELECT note_id FROM notes LEFT JOIN notes_tree USING(note_id) WHERE note_id != 'root' AND notes_tree.note_tree_id IS NULL", "Missing notes_tree records for following note IDs", errorList); await runCheck("SELECT note_tree_id || ' > ' || notes_tree.note_id FROM notes_tree LEFT JOIN notes USING(note_id) WHERE notes.note_id IS NULL", @@ -35,7 +35,7 @@ async function runChecks() { await runCheck("SELECT note_tree_id FROM notes_tree JOIN notes USING(note_id) WHERE notes.is_deleted = 1 AND notes_tree.is_deleted = 0", "Note tree is not deleted even though main note is deleted for following note tree IDs", errorList); - await runCheck("SELECT child.note_pid || ' > ' || child.note_id FROM notes_tree AS child LEFT JOIN notes_tree AS parent ON parent.note_id = child.note_pid WHERE parent.note_id IS NULL AND child.note_pid != 'root'", + await runCheck("SELECT child.parent_note_id || ' > ' || child.note_id FROM notes_tree AS child LEFT JOIN notes_tree AS parent ON parent.note_id = child.parent_note_id WHERE parent.note_id IS NULL AND child.parent_note_id != 'root'", "Not existing parent in the following parent > child relations", errorList); await runCheck("SELECT note_history_id || ' > ' || notes_history.note_id FROM notes_history LEFT JOIN notes USING(note_id) WHERE notes.note_id IS NULL", diff --git a/services/content_hash.js b/services/content_hash.js index 43f28f900..3c15099c6 100644 --- a/services/content_hash.js +++ b/services/content_hash.js @@ -29,8 +29,8 @@ async function getHashes() { notes_tree: getHash(await sql.getResults(`SELECT note_tree_id, note_id, - note_pid, - note_pos, + parent_note_id, + note_position, date_modified, is_deleted, prefix diff --git a/services/notes.js b/services/notes.js index 37eef1eda..88c0f3315 100644 --- a/services/notes.js +++ b/services/notes.js @@ -13,18 +13,18 @@ async function createNewNote(parentNoteId, note, sourceId) { await sql.doInTransaction(async () => { if (note.target === 'into') { - const maxNotePos = await sql.getSingleValue('SELECT MAX(note_pos) FROM notes_tree WHERE note_pid = ? AND is_deleted = 0', [parentNoteId]); + const maxNotePos = await sql.getSingleValue('SELECT MAX(note_position) FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0', [parentNoteId]); newNotePos = maxNotePos === null ? 0 : maxNotePos + 1; } else if (note.target === 'after') { - const afterNote = await sql.getSingleResult('SELECT note_pos FROM notes_tree WHERE note_tree_id = ?', [note.target_note_tree_id]); + const afterNote = await sql.getSingleResult('SELECT note_position FROM notes_tree WHERE note_tree_id = ?', [note.target_note_tree_id]); - newNotePos = afterNote.note_pos + 1; + newNotePos = afterNote.note_position + 1; // not updating date_modified to avoig having to sync whole rows - await sql.execute('UPDATE notes_tree SET note_pos = note_pos + 1 WHERE note_pid = ? AND note_pos > ? AND is_deleted = 0', - [parentNoteId, afterNote.note_pos]); + await sql.execute('UPDATE notes_tree SET note_position = note_position + 1 WHERE parent_note_id = ? AND note_position > ? AND is_deleted = 0', + [parentNoteId, afterNote.note_position]); await sync_table.addNoteReorderingSync(parentNoteId, sourceId); } @@ -48,8 +48,8 @@ async function createNewNote(parentNoteId, note, sourceId) { await sql.insert("notes_tree", { note_tree_id: noteTreeId, note_id: noteId, - note_pid: parentNoteId, - note_pos: newNotePos, + parent_note_id: parentNoteId, + note_position: newNotePos, is_expanded: 0, date_modified: now, is_deleted: 0 @@ -74,7 +74,7 @@ async function protectNoteRecursively(noteId, dataKey, protect, sourceId) { await protectNote(note, dataKey, protect, sourceId); - const children = await sql.getFlattenedResults("SELECT note_id FROM notes_tree WHERE note_pid = ?", [noteId]); + const children = await sql.getFlattenedResults("SELECT note_id FROM notes_tree WHERE parent_note_id = ?", [noteId]); for (const childNoteId of children) { await protectNoteRecursively(childNoteId, dataKey, protect, sourceId); @@ -205,7 +205,7 @@ async function deleteNote(noteTreeId, sourceId) { await sql.execute("UPDATE notes SET is_deleted = 1, date_modified = ? WHERE note_id = ?", [now, noteId]); await sync_table.addNoteSync(noteId, sourceId); - const children = await sql.getResults("SELECT note_tree_id FROM notes_tree WHERE note_pid = ? AND is_deleted = 0", [noteId]); + const children = await sql.getResults("SELECT note_tree_id FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0", [noteId]); for (const child of children) { await deleteNote(child.note_tree_id, sourceId); diff --git a/services/sql.js b/services/sql.js index 650afe9ef..ae50a6667 100644 --- a/services/sql.js +++ b/services/sql.js @@ -37,8 +37,8 @@ const dbReady = new Promise((resolve, reject) => { await insert('notes_tree', { note_tree_id: utils.newNoteTreeId(), note_id: noteId, - note_pid: 'root', - note_pos: 1, + parent_note_id: 'root', + note_position: 1, is_deleted: 0, date_modified: now }); diff --git a/services/sync.js b/services/sync.js index e4c20adb7..1a1142c8f 100644 --- a/services/sync.js +++ b/services/sync.js @@ -209,8 +209,8 @@ async function pushEntity(sync, syncContext) { } else if (sync.entity_name === 'notes_reordering') { entity = { - note_pid: sync.entity_id, - ordering: await sql.getMap('SELECT note_tree_id, note_pos FROM notes_tree WHERE note_pid = ?', [sync.entity_id]) + parent_note_id: sync.entity_id, + ordering: await sql.getMap('SELECT note_tree_id, note_position FROM notes_tree WHERE parent_note_id = ?', [sync.entity_id]) }; } else if (sync.entity_name === 'options') { diff --git a/services/sync_update.js b/services/sync_update.js index 2c553d4a2..3306cd87a 100644 --- a/services/sync_update.js +++ b/services/sync_update.js @@ -53,10 +53,10 @@ async function updateNoteHistory(entity, sourceId) { async function updateNoteReordering(entity, sourceId) { await sql.doInTransaction(async () => { Object.keys(entity.ordering).forEach(async key => { - await sql.execute("UPDATE notes_tree SET note_pos = ? WHERE note_tree_id = ?", [entity.ordering[key], key]); + await sql.execute("UPDATE notes_tree SET note_position = ? WHERE note_tree_id = ?", [entity.ordering[key], key]); }); - await sync_table.addNoteReorderingSync(entity.note_pid, sourceId); + await sync_table.addNoteReorderingSync(entity.parent_note_id, sourceId); }); } From 5403f340ecf5d3ae94caf6819f78990d84cf3a70 Mon Sep 17 00:00:00 2001 From: azivner Date: Tue, 19 Dec 2017 22:04:51 -0500 Subject: [PATCH 4/8] functionality to fill up / cleanup sync rows compared to the entity rows --- public/javascripts/dialogs/settings.js | 7 +++++ routes/api/sync.js | 40 ++++++++++++++++++++++++++ services/consistency_checks.js | 2 +- views/index.ejs | 5 ++++ 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/public/javascripts/dialogs/settings.js b/public/javascripts/dialogs/settings.js index 3d9187f8d..2eb385695 100644 --- a/public/javascripts/dialogs/settings.js +++ b/public/javascripts/dialogs/settings.js @@ -154,6 +154,7 @@ settings.addModule((async function () { settings.addModule((async function () { const forceFullSyncButton = $("#force-full-sync-button"); + const fillSyncRowsButton = $("#fill-sync-rows-button"); forceFullSyncButton.click(async () => { await server.post('sync/force-full-sync'); @@ -161,6 +162,12 @@ settings.addModule((async function () { showMessage("Full sync triggered"); }); + fillSyncRowsButton.click(async () => { + await server.post('sync/fill-sync-rows'); + + showMessage("Sync rows filled successfully"); + }); + return {}; })()); diff --git a/routes/api/sync.js b/routes/api/sync.js index b90b8ebd4..c5bbeab58 100644 --- a/routes/api/sync.js +++ b/routes/api/sync.js @@ -8,6 +8,8 @@ const syncUpdate = require('../../services/sync_update'); const sql = require('../../services/sql'); const options = require('../../services/options'); const content_hash = require('../../services/content_hash'); +const utils = require('../../services/utils'); +const log = require('../../services/log'); router.get('/check', auth.checkApiAuth, async (req, res, next) => { res.send({ @@ -20,6 +22,44 @@ router.post('/now', auth.checkApiAuth, async (req, res, next) => { res.send(await sync.sync()); }); +async function fillSyncRows(entityName, entityKey) { + // cleanup sync rows for missing entities + await sql.execute(` + DELETE + FROM sync + WHERE sync.entity_name = '${entityName}' + AND sync.entity_id NOT IN (SELECT ${entityKey} FROM ${entityName})`); + + const entityIds = await sql.getFlattenedResults(`SELECT ${entityKey} FROM ${entityName}`); + + for (const entityId of entityIds) { + const existingRows = await sql.getSingleValue("SELECT COUNT(id) FROM sync WHERE entity_name = ? AND entity_id = ?", [entityName, entityId]); + + // we don't want to replace existing entities (which would effectively cause full resync) + if (existingRows === 0) { + log.info(`Creating missing sync record for ${entityName} ${entityId}`); + + await sql.insert("sync", { + entity_name: entityName, + entity_id: entityId, + source_id: "SYNC_FILL", + sync_date: utils.nowDate() + }); + } + } +} + +router.post('/fill-sync-rows', auth.checkApiAuth, async (req, res, next) => { + await sql.doInTransaction(async () => { + await fillSyncRows("notes", "note_id"); + await fillSyncRows("notes_tree", "note_tree_id"); + await fillSyncRows("notes_history", "note_history_id"); + await fillSyncRows("recent_notes", "note_tree_id"); + }); + + res.send({}); +}); + router.post('/force-full-sync', auth.checkApiAuth, async (req, res, next) => { await sql.doInTransaction(async () => { await options.setOption('last_synced_pull', 0); diff --git a/services/consistency_checks.js b/services/consistency_checks.js index aacd5f19d..5a74a727f 100644 --- a/services/consistency_checks.js +++ b/services/consistency_checks.js @@ -16,7 +16,7 @@ async function runCheck(query, errorText, errorList) { } async function runSyncRowChecks(table, key, errorList) { - await runCheck(`SELECT ${key} FROM ${table} LEFT JOIN sync ON sync.entity_name = '${table}' AND entity_id = ${key} WHERE entity_id != 'root' AND sync.id IS NULL`, + await runCheck(`SELECT ${key} FROM ${table} LEFT JOIN sync ON sync.entity_name = '${table}' AND entity_id = ${key} WHERE sync.id IS NULL`, `Missing sync records for ${key} in table ${table}`, errorList); await runCheck(`SELECT entity_id FROM sync LEFT JOIN ${table} ON entity_id = ${key} WHERE sync.entity_name = '${table}' AND ${key} IS NULL`, diff --git a/views/index.ejs b/views/index.ejs index e249db3f0..c8e0d879f 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -219,6 +219,11 @@
+ +
+
+ +
From 2df81c3a834e3aec0b403e47003b5c7bde53c9a8 Mon Sep 17 00:00:00 2001 From: azivner Date: Tue, 19 Dec 2017 22:24:41 -0500 Subject: [PATCH 5/8] forgot to enable the constraints immediatelly after connecting --- services/sql.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sql.js b/services/sql.js index ae50a6667..f24c17726 100644 --- a/services/sql.js +++ b/services/sql.js @@ -16,6 +16,8 @@ const dbConnected = createConnection(); let dbReadyResolve = null; const dbReady = new Promise((resolve, reject) => { dbConnected.then(async db => { + await execute("PRAGMA foreign_keys = ON"); + dbReadyResolve = () => { log.info("DB ready."); From f54d855f55262f73655e8f903a4935ccba723e06 Mon Sep 17 00:00:00 2001 From: azivner Date: Tue, 19 Dec 2017 22:33:44 -0500 Subject: [PATCH 6/8] sql console now shows error message if any SQL error ocurred --- public/javascripts/dialogs/sql_console.js | 15 +++++++++++---- routes/api/sql.js | 13 ++++++++++++- services/sql.js | 2 ++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/public/javascripts/dialogs/sql_console.js b/public/javascripts/dialogs/sql_console.js index 0f4792bde..171b57d57 100644 --- a/public/javascripts/dialogs/sql_console.js +++ b/public/javascripts/dialogs/sql_console.js @@ -20,15 +20,22 @@ const sqlConsole = (function() { async function execute() { const sqlQuery = queryEl.val(); - const results = await server.post("sql/execute", { + const result = await server.post("sql/execute", { query: sqlQuery }); + if (!result.success) { + showError(result.error); + return; + } + + const rows = result.rows; + resultHeadEl.empty(); resultBodyEl.empty(); - if (results.length > 0) { - const result = results[0]; + if (rows.length > 0) { + const result = rows[0]; const rowEl = $(""); for (const key in result) { @@ -38,7 +45,7 @@ const sqlConsole = (function() { resultHeadEl.append(rowEl); } - for (const result of results) { + for (const result of rows) { const rowEl = $(""); for (const key in result) { diff --git a/routes/api/sql.js b/routes/api/sql.js index b4c735fd7..9aab0a140 100644 --- a/routes/api/sql.js +++ b/routes/api/sql.js @@ -8,7 +8,18 @@ const sql = require('../../services/sql'); router.post('/execute', auth.checkApiAuth, async (req, res, next) => { const query = req.body.query; - res.send(await sql.getResults(query)); + try { + res.send({ + success: true, + rows: await sql.getResults(query) + }); + } + catch (e) { + res.send({ + success: false, + error: e.message + }); + } }); module.exports = router; \ No newline at end of file diff --git a/services/sql.js b/services/sql.js index f24c17726..03686a0dd 100644 --- a/services/sql.js +++ b/services/sql.js @@ -192,6 +192,8 @@ async function wrap(func) { catch (e) { log.error("Error executing query. Inner exception: " + e.stack + thisError.stack); + thisError.message = e.stack; + throw thisError; } } From 333735543ec24b8e6d2eb6dd641c6cf52110107d Mon Sep 17 00:00:00 2001 From: azivner Date: Tue, 19 Dec 2017 23:22:21 -0500 Subject: [PATCH 7/8] yet another attempt at fixing reporting sync changes to client --- app.js | 2 -- public/javascripts/messaging.js | 24 +++++++++++++++++------- routes/index.js | 4 +++- services/consistency_checks.js | 2 +- services/messaging.js | 32 ++++++++++++++++++++++++++++++-- services/ping_job.js | 30 ------------------------------ services/sync.js | 2 +- views/index.ejs | 3 ++- 8 files changed, 54 insertions(+), 45 deletions(-) delete mode 100644 services/ping_job.js diff --git a/app.js b/app.js index 6c9c01f70..241ccaebc 100644 --- a/app.js +++ b/app.js @@ -10,8 +10,6 @@ const FileStore = require('session-file-store')(session); const os = require('os'); const sessionSecret = require('./services/session_secret'); -require('./services/ping_job'); - const app = express(); // view engine setup diff --git a/public/javascripts/messaging.js b/public/javascripts/messaging.js index eca8aacd1..c393a45df 100644 --- a/public/javascripts/messaging.js +++ b/public/javascripts/messaging.js @@ -2,7 +2,6 @@ const messaging = (function() { const changesToPushCountEl = $("#changes-to-push-count"); - let ws = null; function logError(message) { console.log(now(), message); // needs to be separate from .trace() @@ -21,12 +20,15 @@ const messaging = (function() { if (message.type === 'sync') { lastPingTs = new Date().getTime(); - const syncData = message.data.filter(sync => sync.source_id !== glob.sourceId); - if (syncData.length > 0) { - console.log(now(), "Sync data: ", message); + if (message.data.length > 0) { + console.log(now(), "Sync data: ", message.data); + + lastSyncId = message.data[message.data.length - 1].id; } + const syncData = message.data.filter(sync => sync.source_id !== glob.sourceId); + if (syncData.some(sync => sync.entity_name === 'notes_tree')) { console.log(now(), "Reloading tree because of background changes"); @@ -59,17 +61,20 @@ const messaging = (function() { const protocol = document.location.protocol === 'https:' ? 'wss' : 'ws'; // use wss for secure messaging - ws = new WebSocket(protocol + "://" + location.host); + const ws = new WebSocket(protocol + "://" + location.host); ws.onopen = event => console.log(now(), "Connected to server with WebSocket"); ws.onmessage = messageHandler; ws.onclose = function(){ // Try to reconnect in 5 seconds setTimeout(() => connectWebSocket(), 5000); }; + + return ws; } - connectWebSocket(); + const ws = connectWebSocket(); + let lastSyncId = glob.maxSyncIdAtLoad; let lastPingTs = new Date().getTime(); let connectionBrokenNotification = null; @@ -92,7 +97,12 @@ const messaging = (function() { showMessage("Re-connected to server"); } - }, 3000); + + ws.send(JSON.stringify({ + type: 'ping', + lastSyncId: lastSyncId + })); + }, 1000); return { logError diff --git a/routes/index.js b/routes/index.js index ee326b404..a420ffc4b 100644 --- a/routes/index.js +++ b/routes/index.js @@ -4,10 +4,12 @@ const express = require('express'); const router = express.Router(); const auth = require('../services/auth'); const source_id = require('../services/source_id'); +const sql = require('../services/sql'); router.get('', auth.checkAuth, async (req, res, next) => { res.render('index', { - sourceId: await source_id.generateSourceId() + sourceId: await source_id.generateSourceId(), + maxSyncIdAtLoad: await sql.getSingleValue("SELECT MAX(id) FROM sync") }); }); diff --git a/services/consistency_checks.js b/services/consistency_checks.js index 5a74a727f..063bb36ab 100644 --- a/services/consistency_checks.js +++ b/services/consistency_checks.js @@ -47,7 +47,7 @@ async function runChecks() { await runSyncRowChecks("recent_notes", "note_tree_id", errorList); if (errorList.length > 0) { - messaging.sendMessage({type: 'consistency-checks-failed'}); + messaging.sendMessageToAllClients({type: 'consistency-checks-failed'}); } else { log.info("All consistency checks passed."); diff --git a/services/messaging.js b/services/messaging.js index c6e5842df..5773070a3 100644 --- a/services/messaging.js +++ b/services/messaging.js @@ -1,6 +1,9 @@ const WebSocket = require('ws'); const utils = require('./utils'); const log = require('./log'); +const sql = require('./sql'); +const options = require('./options'); +const sync_setup = require('./sync_setup'); let webSocketServer; @@ -29,6 +32,9 @@ function init(httpServer, sessionParser) { if (message.type === 'log-error') { log.error('JS Error: ' + message.error); } + else if (message.type === 'ping') { + sendPing(ws, message.lastSyncId); + } else { log.error('Unrecognized message: '); log.error(message); @@ -37,7 +43,15 @@ function init(httpServer, sessionParser) { }); } -async function sendMessage(message) { +async function sendMessage(client, message) { + const jsonStr = JSON.stringify(message); + + if (client.readyState === WebSocket.OPEN) { + client.send(jsonStr); + } +} + +async function sendMessageToAllClients(message) { const jsonStr = JSON.stringify(message); webSocketServer.clients.forEach(function each(client) { @@ -47,7 +61,21 @@ async function sendMessage(message) { }); } +async function sendPing(client, lastSentSyncId) { + const syncData = await sql.getResults("SELECT * FROM sync WHERE id > ?", [lastSentSyncId]); + + const lastSyncedPush = await options.getOption('last_synced_push'); + + const changesToPushCount = await sql.getSingleValue("SELECT COUNT(*) FROM sync WHERE id > ?", [lastSyncedPush]); + + await sendMessage(client, { + type: 'sync', + data: syncData, + changesToPushCount: sync_setup.isSyncSetup ? changesToPushCount : 0 + }); +} + module.exports = { init, - sendMessage + sendMessageToAllClients }; \ No newline at end of file diff --git a/services/ping_job.js b/services/ping_job.js deleted file mode 100644 index c9666ae77..000000000 --- a/services/ping_job.js +++ /dev/null @@ -1,30 +0,0 @@ -const sql = require('./sql'); -const messaging = require('./messaging'); -const options = require('./options'); -const sync_setup = require('./sync_setup'); - -let lastSentSyncId; - -async function sendPing() { - const syncData = await sql.getResults("SELECT * FROM sync WHERE id > ?", [lastSentSyncId]); - - const lastSyncedPush = await options.getOption('last_synced_push'); - - const changesToPushCount = await sql.getSingleValue("SELECT COUNT(*) FROM sync WHERE id > ?", [lastSyncedPush]); - - messaging.sendMessage({ - type: 'sync', - data: syncData, - changesToPushCount: sync_setup.isSyncSetup ? changesToPushCount : 0 - }); - - if (syncData.length > 0) { - lastSentSyncId = syncData[syncData.length - 1].id; - } -} - -sql.dbReady.then(async () => { - lastSentSyncId = await sql.getSingleValue("SELECT MAX(id) FROM sync"); - - setInterval(sendPing, 1000); -}); \ No newline at end of file diff --git a/services/sync.js b/services/sync.js index 1a1142c8f..505451ebe 100644 --- a/services/sync.js +++ b/services/sync.js @@ -267,7 +267,7 @@ async function checkContentHash(syncContext) { if (key !== 'recent_notes') { // let's not get alarmed about recent notes which get updated often and can cause failures in race conditions - await messaging.sendMessage({type: 'sync-hash-check-failed'}); + await messaging.sendMessageToAllClients({type: 'sync-hash-check-failed'}); } } } diff --git a/views/index.ejs b/views/index.ejs index c8e0d879f..3e7190345 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -301,7 +301,8 @@ const baseApiUrl = 'api/'; const glob = { activeDialog: null, - sourceId: '<%= sourceId %>' + sourceId: '<%= sourceId %>', + maxSyncIdAtLoad: <%= maxSyncIdAtLoad %> }; From 4ab9468f7269a089c3f9724a8f014d57b31be2cc Mon Sep 17 00:00:00 2001 From: azivner Date: Wed, 20 Dec 2017 19:52:01 -0500 Subject: [PATCH 8/8] 0.0.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c754cbda0..0e6617490 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "trilium", "description": "Trilium", - "version": "0.0.6", + "version": "0.0.7", "scripts": { "start": "node ./bin/www", "test-electron": "xo",