1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-13 22:12:50 +02:00

handle deleted sync items

This commit is contained in:
Laurent Cozic
2017-06-19 23:18:24 +01:00
parent c6ed94b446
commit b3939887b8
9 changed files with 187 additions and 11 deletions

View File

@@ -5,5 +5,7 @@ rm -f "$CLIENT_DIR/tests-build/src"
mkdir -p "$CLIENT_DIR/tests-build/data" mkdir -p "$CLIENT_DIR/tests-build/data"
ln -s "$CLIENT_DIR/build/src" "$CLIENT_DIR/tests-build" 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 #npm run build && NODE_PATH="$CLIENT_DIR/tests-build/" npm test tests-build/services/note-folder-service.js

View File

@@ -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();
});
});

View File

@@ -9,7 +9,6 @@ import { BaseModel } from 'src/base-model.js';
process.on('unhandledRejection', (reason, p) => { process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
// application specific logging, throwing an error, or other logic here
}); });
async function localItemsSameAsRemote(locals, expect) { async function localItemsSameAsRemote(locals, expect) {
@@ -22,11 +21,6 @@ async function localItemsSameAsRemote(locals, expect) {
let path = BaseItem.systemPath(dbItem); let path = BaseItem.systemPath(dbItem);
let remote = await fileApi().stat(path); let remote = await fileApi().stat(path);
// console.info('=======================');
// console.info(remote);
// console.info(dbItem);
// console.info('=======================');
expect(!!remote).toBe(true); expect(!!remote).toBe(true);
expect(remote.updated_time).toBe(dbItem.updated_time); 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();
// });
}); });

View File

@@ -37,6 +37,10 @@ class BaseModel {
return false; return false;
} }
static trackDeleted() {
return false;
}
static byId(items, id) { static byId(items, id) {
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
if (items[i].id == id) return items[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) { static delete(id, options = null) {
options = this.modOptions(options); options = this.modOptions(options);
@@ -248,6 +256,10 @@ class BaseModel {
} }
return this.db().exec('DELETE FROM ' + this.tableName() + ' WHERE id = ?', [id]).then(() => { 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()) { // if (options.trackChanges && this.trackChanges()) {
// const { Change } = require('src/models/change.js'); // const { Change } = require('src/models/change.js');

View File

@@ -36,6 +36,13 @@ CREATE TABLE notes (
\`order\` INT NOT NULL DEFAULT 0 \`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 ( CREATE TABLE tags (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
title TEXT, title TEXT,

View File

@@ -26,6 +26,10 @@ class Folder extends BaseItem {
return true; return true;
} }
static trackDeleted() {
return true;
}
static newFolder() { static newFolder() {
return { return {
id: null, id: null,
@@ -33,8 +37,18 @@ class Folder extends BaseItem {
} }
} }
static noteIds(id) { static syncedNoteIds() {
return this.db().selectAll('SELECT id FROM notes WHERE parent_id = ?', [id]).then((rows) => { 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 = []; let output = [];
for (let i = 0; i < rows.length; i++) { for (let i = 0; i < rows.length; i++) {
let row = rows[i]; let row = rows[i];
@@ -46,6 +60,8 @@ class Folder extends BaseItem {
static delete(folderId, options = null) { static delete(folderId, options = null) {
return this.load(folderId).then((folder) => { return this.load(folderId).then((folder) => {
if (!folder) throw new Error('Trying to delete non-existing folder: ' + folderId);
if (!!folder.is_default) { if (!!folder.is_default) {
throw new Error(_('Cannot delete the default list')); throw new Error(_('Cannot delete the default list'));
} }
@@ -72,7 +88,6 @@ class Folder extends BaseItem {
static loadNoteByField(folderId, field, value) { static loadNoteByField(folderId, field, value) {
return this.modelSelectAll('SELECT * FROM notes WHERE `parent_id` = ? AND `' + field + '` = ?', [folderId, 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) { static async all(includeNotes = false) {

View File

@@ -24,6 +24,10 @@ class Note extends BaseItem {
return true; return true;
} }
static trackDeleted() {
return true;
}
static new(parentId = '') { static new(parentId = '') {
let output = super.new(); let output = super.new();
output.parent_id = parentId; output.parent_id = parentId;

View File

@@ -1,5 +1,10 @@
// A service that handle notes and folders in a uniform way // 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 { BaseService } from 'src/base-service.js';
import { BaseModel } from 'src/base-model.js'; import { BaseModel } from 'src/base-model.js';
import { BaseItem } from 'src/models/base-item.js'; import { BaseItem } from 'src/models/base-item.js';

View File

@@ -48,7 +48,13 @@ class Synchronizer {
let updateSyncTimeOnly = true; let updateSyncTimeOnly = true;
if (!remote) { 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 { } else {
if (remote.updated_time > local.sync_time) { if (remote.updated_time > local.sync_time) {
// Since, in this loop, we are only dealing with notes that require sync, if the // 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 // 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. // or handled as conflicts, so no conflict is possible after this.
let remoteIds = [];
let remotes = await this.api().list(); let remotes = await this.api().list();
for (let i = 0; i < remotes.length; i++) { for (let i = 0; i < remotes.length; i++) {
let remote = remotes[i]; let remote = remotes[i];
let path = remote.path; let path = remote.path;
remoteIds.push(BaseItem.pathToId(path));
if (donePaths.indexOf(path) > 0) continue; if (donePaths.indexOf(path) > 0) continue;
let action = null; 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(); return Promise.resolve();
} }