You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	handle deleted sync items
This commit is contained in:
		| @@ -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 | ||||
							
								
								
									
										40
									
								
								CliClient/tests/base-model.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								CliClient/tests/base-model.js
									
									
									
									
									
										Normal 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(); | ||||
| 	}); | ||||
|  | ||||
| }); | ||||
| @@ -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(); | ||||
| 	// }); | ||||
|  | ||||
| }); | ||||
| @@ -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'); | ||||
|  | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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'; | ||||
|   | ||||
| @@ -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(); | ||||
| 	} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user