diff --git a/CliClient/run_test.sh b/CliClient/run_test.sh index 9f67d25532..fc57c8380c 100755 --- a/CliClient/run_test.sh +++ b/CliClient/run_test.sh @@ -5,5 +5,7 @@ rm -f "$CLIENT_DIR/tests-build/src" mkdir -p "$CLIENT_DIR/tests-build/data" ln -s "$CLIENT_DIR/build/src" "$CLIENT_DIR/tests-build" -npm run build && NODE_PATH="$CLIENT_DIR/tests-build/" npm test tests-build/synchronizer.js +npm run build && NODE_PATH="$CLIENT_DIR/tests-build/" npm test tests-build/base-model.js + +#npm run build && NODE_PATH="$CLIENT_DIR/tests-build/" npm test tests-build/synchronizer.js #npm run build && NODE_PATH="$CLIENT_DIR/tests-build/" npm test tests-build/services/note-folder-service.js \ No newline at end of file diff --git a/CliClient/tests/base-model.js b/CliClient/tests/base-model.js new file mode 100644 index 0000000000..8528960f6c --- /dev/null +++ b/CliClient/tests/base-model.js @@ -0,0 +1,40 @@ +import { time } from 'src/time-utils.js'; +import { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient } from 'test-utils.js'; +import { createFoldersAndNotes } from 'test-data.js'; +import { Folder } from 'src/models/folder.js'; +import { Note } from 'src/models/note.js'; +import { Setting } from 'src/models/setting.js'; +import { BaseItem } from 'src/models/base-item.js'; +import { BaseModel } from 'src/base-model.js'; + +process.on('unhandledRejection', (reason, p) => { + console.error('Unhandled promise rejection at: Promise', p, 'reason:', reason); +}); + +describe('BaseItem', function() { + + beforeEach( async (done) => { + await setupDatabaseAndSynchronizer(1); + switchClient(1); + done(); + }); + + it('should create a deleted_items record', async (done) => { + let folder = await Folder.save({ title: 'folder1' }); + + await Folder.delete(folder.id); + + let items = await BaseModel.deletedItems(); + + expect(items.length).toBe(1); + expect(items[0].item_id).toBe(folder.id); + expect(items[0].item_type).toBe(folder.type_); + + let folders = await Folder.all(); + + expect(folders.length).toBe(0); + + done(); + }); + +}); \ No newline at end of file diff --git a/CliClient/tests/synchronizer.js b/CliClient/tests/synchronizer.js index e691f0b526..53a10783f7 100644 --- a/CliClient/tests/synchronizer.js +++ b/CliClient/tests/synchronizer.js @@ -9,7 +9,6 @@ import { BaseModel } from 'src/base-model.js'; process.on('unhandledRejection', (reason, p) => { console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); - // application specific logging, throwing an error, or other logic here }); async function localItemsSameAsRemote(locals, expect) { @@ -22,11 +21,6 @@ async function localItemsSameAsRemote(locals, expect) { let path = BaseItem.systemPath(dbItem); let remote = await fileApi().stat(path); - // console.info('======================='); - // console.info(remote); - // console.info(dbItem); - // console.info('======================='); - expect(!!remote).toBe(true); expect(remote.updated_time).toBe(dbItem.updated_time); @@ -218,4 +212,81 @@ describe('Synchronizer', function() { }); + + + + + + + + + // it('should delete local items', async (done) => { + // let folder1 = await Folder.save({ title: "folder1" }); + // let note1 = await Note.save({ title: "un", parent_id: folder1.id }); + // await synchronizer().start(); + + // switchClient(2); + + // await synchronizer().start(); + + // await sleep(0.1); + + // await Note.delete(note1.id); + + // await synchronizer().start(); + + // switchClient(1); + + // let files = await fileApi().list(); + // console.info(files); + + // // await synchronizer().start(); + + // // note1 = await Note.load(note1.id); + + // // expect(!note1).toBe(true); + + // done(); + // }); + + + + + + + + + + + + + // it('should delete remote items', async (done) => { + // let folder1 = await Folder.save({ title: "folder1" }); + // let note1 = await Note.save({ title: "un", parent_id: folder1.id }); + // await synchronizer().start(); + + // switchClient(2); + + // await synchronizer().start(); + + // await sleep(0.1); + + // await Note.delete(note1.id); + + // await synchronizer().start(); + + // switchClient(1); + + // let files = await fileApi().list(); + // console.info(files); + + // await synchronizer().start(); + + // note1 = await Note.load(note1.id); + + // expect(!note1).toBe(true); + + // done(); + // }); + }); \ No newline at end of file diff --git a/ReactNativeClient/src/base-model.js b/ReactNativeClient/src/base-model.js index ad81aab5b6..9f63ae7a86 100644 --- a/ReactNativeClient/src/base-model.js +++ b/ReactNativeClient/src/base-model.js @@ -37,6 +37,10 @@ class BaseModel { return false; } + static trackDeleted() { + return false; + } + static byId(items, id) { for (let i = 0; i < items.length; i++) { if (items[i].id == id) return items[i]; @@ -239,6 +243,10 @@ class BaseModel { }); } + static deletedItems() { + return this.db().selectAll('SELECT * FROM deleted_items'); + } + static delete(id, options = null) { options = this.modOptions(options); @@ -248,6 +256,10 @@ class BaseModel { } return this.db().exec('DELETE FROM ' + this.tableName() + ' WHERE id = ?', [id]).then(() => { + if (this.trackDeleted()) { + return this.db().exec('INSERT INTO deleted_items (item_type, item_id, deleted_time) VALUES (?, ?, ?)', [this.itemType(), id, time.unixMs()]); + } + // if (options.trackChanges && this.trackChanges()) { // const { Change } = require('src/models/change.js'); diff --git a/ReactNativeClient/src/database.js b/ReactNativeClient/src/database.js index 9f7c5f269b..f019458dd4 100644 --- a/ReactNativeClient/src/database.js +++ b/ReactNativeClient/src/database.js @@ -36,6 +36,13 @@ CREATE TABLE notes ( \`order\` INT NOT NULL DEFAULT 0 ); +CREATE TABLE deleted_items ( + id TEXT PRIMARY KEY, + item_type INT NOT NULL, + item_id TEXT NOT NULL, + deleted_time INT NOT NULL +); + CREATE TABLE tags ( id TEXT PRIMARY KEY, title TEXT, diff --git a/ReactNativeClient/src/models/folder.js b/ReactNativeClient/src/models/folder.js index e4728485ac..ca09af23b1 100644 --- a/ReactNativeClient/src/models/folder.js +++ b/ReactNativeClient/src/models/folder.js @@ -25,6 +25,10 @@ class Folder extends BaseItem { static trackChanges() { return true; } + + static trackDeleted() { + return true; + } static newFolder() { return { @@ -33,8 +37,18 @@ class Folder extends BaseItem { } } - static noteIds(id) { - return this.db().selectAll('SELECT id FROM notes WHERE parent_id = ?', [id]).then((rows) => { + static syncedNoteIds() { + return this.db().selectAll('SELECT id FROM notes WHERE sync_time > 0').then((rows) => { + let output = []; + for (let i = 0; i < rows.length; i++) { + output.push(rows[i].id); + } + return output; + }); + } + + static noteIds(parentId) { + return this.db().selectAll('SELECT id FROM notes WHERE parent_id = ?', [parentId]).then((rows) => { let output = []; for (let i = 0; i < rows.length; i++) { let row = rows[i]; @@ -46,6 +60,8 @@ class Folder extends BaseItem { static delete(folderId, options = null) { return this.load(folderId).then((folder) => { + if (!folder) throw new Error('Trying to delete non-existing folder: ' + folderId); + if (!!folder.is_default) { throw new Error(_('Cannot delete the default list')); } @@ -72,7 +88,6 @@ class Folder extends BaseItem { static loadNoteByField(folderId, field, value) { return this.modelSelectAll('SELECT * FROM notes WHERE `parent_id` = ? AND `' + field + '` = ?', [folderId, value]); - //return this.db().selectOne('SELECT * FROM notes WHERE `parent_id` = ? AND `' + field + '` = ?', [folderId, value]); } static async all(includeNotes = false) { diff --git a/ReactNativeClient/src/models/note.js b/ReactNativeClient/src/models/note.js index bf4640e9d0..c75043fbfd 100644 --- a/ReactNativeClient/src/models/note.js +++ b/ReactNativeClient/src/models/note.js @@ -24,6 +24,10 @@ class Note extends BaseItem { return true; } + static trackDeleted() { + return true; + } + static new(parentId = '') { let output = super.new(); output.parent_id = parentId; diff --git a/ReactNativeClient/src/services/note-folder-service.js b/ReactNativeClient/src/services/note-folder-service.js index 651094d3fe..d93a151a25 100644 --- a/ReactNativeClient/src/services/note-folder-service.js +++ b/ReactNativeClient/src/services/note-folder-service.js @@ -1,5 +1,10 @@ // A service that handle notes and folders in a uniform way + +// TODO: remote this service +// - Move setting of geo-location to GUI side (only for note explicitely created on client +// - Don't do diffing - make caller explicitely set model properties that need to be saved + import { BaseService } from 'src/base-service.js'; import { BaseModel } from 'src/base-model.js'; import { BaseItem } from 'src/models/base-item.js'; diff --git a/ReactNativeClient/src/synchronizer.js b/ReactNativeClient/src/synchronizer.js index 9af8bd4c5d..072a864d09 100644 --- a/ReactNativeClient/src/synchronizer.js +++ b/ReactNativeClient/src/synchronizer.js @@ -48,7 +48,13 @@ class Synchronizer { let updateSyncTimeOnly = true; if (!remote) { - action = 'createRemote'; + if (!local.sync_time) { + action = 'createRemote'; + } else { + // Note or folder was modified after having been deleted remotely + action = local.type_ == BaseModel.MODEL_TYPE_NOTE ? 'noteConflict' : 'folderConflict'; + // TODO: handle conflict + } } else { if (remote.updated_time > local.sync_time) { // Since, in this loop, we are only dealing with notes that require sync, if the @@ -107,10 +113,12 @@ class Synchronizer { // At this point all the local items that have changed have been pushed to remote // or handled as conflicts, so no conflict is possible after this. + let remoteIds = []; let remotes = await this.api().list(); for (let i = 0; i < remotes.length; i++) { let remote = remotes[i]; let path = remote.path; + remoteIds.push(BaseItem.pathToId(path)); if (donePaths.indexOf(path) > 0) continue; let action = null; @@ -143,6 +151,18 @@ class Synchronizer { } } + // ------------------------------------------------------------------------ + // Search, among the local IDs, those that don't exist remotely, which + // means the item has been deleted. + // ------------------------------------------------------------------------ + + // let noteIds = Folder.syncedNoteIds(); + // for (let i = 0; i < noteIds.length; i++) { + // if (remoteIds.indexOf(noteIds[i]) < 0) { + // console.info('Sync action (3): Delete ' + noteIds[i]); + // await Note.delete(noteIds[i]); + // } + // } return Promise.resolve(); }