You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Fixed syncing of tags
This commit is contained in:
		
							
								
								
									
										13019
									
								
								CliClient/tests/logs/log.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13019
									
								
								CliClient/tests/logs/log.txt
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -486,7 +486,7 @@ describe('Synchronizer', function() { | ||||
| 		await synchronizer().start(); | ||||
| 		let remoteNoteIds = await Tag.tagNoteIds(tag.id); | ||||
| 		expect(remoteNoteIds.length).toBe(2); | ||||
| 		Tag.removeNote(tag.id, n1.id); | ||||
| 		await Tag.removeNote(tag.id, n1.id); | ||||
| 		remoteNoteIds = await Tag.tagNoteIds(tag.id); | ||||
| 		expect(remoteNoteIds.length).toBe(1); | ||||
| 		await synchronizer().start(); | ||||
|   | ||||
| @@ -18,7 +18,7 @@ let fileApi_ = null; | ||||
| let currentClient_ = 1; | ||||
|  | ||||
| const logger = new Logger(); | ||||
| logger.addTarget('file', { path: __dirname + '/data/log-test.txt' }); | ||||
| logger.addTarget('file', { path: __dirname + '/../tests/logs/log.txt' }); | ||||
| //logger.addTarget('console'); | ||||
| logger.setLevel(Logger.LEVEL_DEBUG); | ||||
|  | ||||
|   | ||||
| @@ -16,6 +16,7 @@ | ||||
| 				"CliClient/app/lib", | ||||
| 				"CliClient/tests/src", | ||||
| 				"CliClient/tests/fuzzing", | ||||
| 				"CliClient/tests/fuzzing -*", | ||||
| 				"ReactNativeClient/node_modules", | ||||
| 				"ReactNativeClient/android/app/build", | ||||
| 				"ReactNativeClient/android/build", | ||||
|   | ||||
| @@ -65,9 +65,12 @@ CREATE TABLE tags ( | ||||
| ); | ||||
|  | ||||
| CREATE TABLE note_tags ( | ||||
| 	id INTEGER PRIMARY KEY, | ||||
| 	id TEXT PRIMARY KEY, | ||||
| 	note_id TEXT NOT NULL, | ||||
| 	tag_id TEXT NOT NULL | ||||
| 	tag_id TEXT NOT NULL, | ||||
| 	created_time INT NOT NULL, | ||||
| 	updated_time INT NOT NULL, | ||||
| 	sync_time INT NOT NULL DEFAULT 0 | ||||
| ); | ||||
|  | ||||
| CREATE TABLE resources ( | ||||
| @@ -135,15 +138,6 @@ class Database { | ||||
| 		return this.logger_; | ||||
| 	} | ||||
|  | ||||
| 	// setDebugMode(v) { | ||||
| 	// 	//this.driver_.setDebugMode(v); | ||||
| 	// 	this.debugMode_ = v; | ||||
| 	// } | ||||
|  | ||||
| 	// debugMode() { | ||||
| 	// 	return this.debugMode_; | ||||
| 	// } | ||||
|  | ||||
| 	initialized() { | ||||
| 		return this.initialized_; | ||||
| 	} | ||||
|   | ||||
| @@ -9,15 +9,13 @@ class BaseItem extends BaseModel { | ||||
| 		return true; | ||||
| 	} | ||||
|  | ||||
| 	static trackDeleted() { | ||||
| 		return false; | ||||
| 	} | ||||
|  | ||||
| 	// Need to dynamically load the classes like this to avoid circular dependencies | ||||
| 	static getClass(name) { | ||||
| 		if (!this.classes_) this.classes_ = {}; | ||||
| 		if (this.classes_[name]) return this.classes_[name]; | ||||
| 		this.classes_[name] = require('lib/models/' + name.toLowerCase() + '.js')[name]; | ||||
| 		let filename = name.toLowerCase(); | ||||
| 		if (name == 'NoteTag') filename = 'note-tag'; | ||||
| 		this.classes_[name] = require('lib/models/' + filename + '.js')[name]; | ||||
| 		return this.classes_[name]; | ||||
| 	} | ||||
|  | ||||
| @@ -33,10 +31,15 @@ class BaseItem extends BaseModel { | ||||
| 			if (!('type_' in item)) throw new Error('Item does not have a type_ property'); | ||||
| 			return this.itemClass(item.type_); | ||||
| 		} else { | ||||
| 			if (Number(item) === BaseModel.TYPE_NOTE) return this.getClass('Note'); | ||||
| 			if (Number(item) === BaseModel.TYPE_FOLDER) return this.getClass('Folder'); | ||||
| 			if (Number(item) === BaseModel.TYPE_RESOURCE) return this.getClass('Resource'); | ||||
| 			if (Number(item) === BaseModel.TYPE_TAG) return this.getClass('Tag'); | ||||
| 			for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) { | ||||
| 				let d = BaseItem.syncItemDefinitions_[i]; | ||||
| 				if (Number(item) == d.type) return this.getClass(d.className); | ||||
| 			} | ||||
| 			// if (Number(item) === BaseModel.TYPE_NOTE) return this.getClass('Note'); | ||||
| 			// if (Number(item) === BaseModel.TYPE_FOLDER) return this.getClass('Folder'); | ||||
| 			// if (Number(item) === BaseModel.TYPE_RESOURCE) return this.getClass('Resource'); | ||||
| 			// if (Number(item) === BaseModel.TYPE_TAG) return this.getClass('Tag'); | ||||
| 			// if (Number(item) === BaseModel.TYPE_NOTE_TAG) return this.getClass('NoteTag'); | ||||
| 			throw new Error('Unknown type: ' + item); | ||||
| 		} | ||||
| 	} | ||||
| @@ -47,7 +50,8 @@ class BaseItem extends BaseModel { | ||||
| 		let notes = await this.getClass('Note').modelSelectAll('SELECT id FROM notes WHERE is_conflict = 0 AND sync_time > 0'); | ||||
| 		let resources = await this.getClass('Resource').modelSelectAll('SELECT id FROM resources WHERE sync_time > 0'); | ||||
| 		let tags = await this.getClass('Tag').modelSelectAll('SELECT id FROM tags WHERE sync_time > 0'); | ||||
| 		return folders.concat(notes).concat(resources).concat(tags); | ||||
| 		let noteTags = await this.getClass('NoteTag').modelSelectAll('SELECT id FROM note_tags WHERE sync_time > 0'); | ||||
| 		return folders.concat(notes).concat(resources).concat(tags).concat(noteTags); | ||||
| 	} | ||||
|  | ||||
| 	static pathToId(path) { | ||||
| @@ -60,7 +64,7 @@ class BaseItem extends BaseModel { | ||||
| 	} | ||||
|  | ||||
| 	static async loadItemById(id) { | ||||
| 		let classes = ['Note', 'Folder', 'Resource', 'Tag']; | ||||
| 		let classes = this.syncItemClassNames(); | ||||
| 		for (let i = 0; i < classes.length; i++) { | ||||
| 			let item = await this.getClass(classes[i]).load(id); | ||||
| 			if (item) return item; | ||||
| @@ -84,7 +88,7 @@ class BaseItem extends BaseModel { | ||||
| 	} | ||||
|  | ||||
| 	static async delete(id, options = null) { | ||||
| 		let trackDeleted = this.trackDeleted(); | ||||
| 		let trackDeleted = true; | ||||
| 		if (options && options.trackDeleted !== null && options.trackDeleted !== undefined) trackDeleted = options.trackDeleted; | ||||
|  | ||||
| 		await super.delete(id, options); | ||||
| @@ -215,9 +219,30 @@ class BaseItem extends BaseModel { | ||||
| 		if (items.length) return { hasMore: true, items: items }; | ||||
|  | ||||
| 		items = await this.getClass('Tag').modelSelectAll('SELECT * FROM tags WHERE sync_time < updated_time LIMIT ' + limit); | ||||
| 		if (items.length) return { hasMore: true, items: items }; | ||||
|  | ||||
| 		items = await this.getClass('NoteTag').modelSelectAll('SELECT * FROM note_tags WHERE sync_time < updated_time LIMIT ' + limit); | ||||
| 		return { hasMore: items.length >= limit, items: items }; | ||||
| 	} | ||||
|  | ||||
| 	static syncItemClassNames() { | ||||
| 		return BaseItem.syncItemDefinitions_.map((def) => { | ||||
| 			return def.className; | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| // Also update: | ||||
| // - itemsThatNeedSync() | ||||
| // - syncedItems() | ||||
|  | ||||
| BaseItem.syncItemDefinitions_ = [ | ||||
| 	{ type: BaseModel.TYPE_NOTE, className: 'Note' }, | ||||
| 	{ type: BaseModel.TYPE_FOLDER, className: 'Folder' }, | ||||
| 	{ type: BaseModel.TYPE_RESOURCE, className: 'Resource' }, | ||||
| 	{ type: BaseModel.TYPE_TAG, className: 'Tag' }, | ||||
| 	{ type: BaseModel.TYPE_NOTE_TAG, className: 'NoteTag' }, | ||||
| ]; | ||||
|  | ||||
| export { BaseItem }; | ||||
| @@ -24,10 +24,6 @@ class Folder extends BaseItem { | ||||
| 	static modelType() { | ||||
| 		return BaseModel.TYPE_FOLDER; | ||||
| 	} | ||||
|  | ||||
| 	static trackDeleted() { | ||||
| 		return true; | ||||
| 	} | ||||
| 	 | ||||
| 	static newFolder() { | ||||
| 		return { | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| import { Database } from 'lib/database.js'; | ||||
| import { BaseItem } from 'lib/models/base-item.js'; | ||||
| import { BaseModel } from 'lib/base-model.js'; | ||||
| import lodash  from 'lodash'; | ||||
|  | ||||
|  | ||||
| class Tag extends BaseItem { | ||||
| class NoteTag extends BaseItem { | ||||
|  | ||||
| 	static tableName() { | ||||
| 		return 'note_tags'; | ||||
| @@ -12,92 +13,12 @@ class Tag extends BaseItem { | ||||
| 		return BaseModel.TYPE_NOTE_TAG; | ||||
| 	} | ||||
|  | ||||
| 	// static async serialize(item, type = null, shownKeys = null) { | ||||
| 	// 	let fieldNames = this.fieldNames(); | ||||
| 	// 	fieldNames.push('type_'); | ||||
| 	// 	fieldNames.push(async () => { | ||||
| 	// 		let noteIds = await this.tagNoteIds(item.id); | ||||
| 	// 		return { | ||||
| 	// 			key: 'notes_', | ||||
| 	// 			value: noteIds.join(','), | ||||
| 	// 		}; | ||||
| 	// 	}); | ||||
| 	// 	lodash.pull(fieldNames, 'sync_time'); | ||||
| 	// 	return super.serialize(item, 'tag', fieldNames); | ||||
| 	// } | ||||
|  | ||||
| 	// static async tagNoteIds(tagId) { | ||||
| 	// 	let rows = await this.db().selectAll('SELECT note_id FROM note_tags WHERE tag_id = ?', [tagId]); | ||||
| 	// 	let output = []; | ||||
| 	// 	for (let i = 0; i < rows.length; i++) { | ||||
| 	// 		output.push(rows[i].note_id); | ||||
| 	// 	} | ||||
| 	// 	return output; | ||||
| 	// } | ||||
|  | ||||
| 	// static async notes(tagId) { | ||||
| 	// 	let noteIds = await this.tagNoteIds(tagId); | ||||
| 	// 	if (!noteIds.length) return []; | ||||
|  | ||||
| 	// 	let noteIdsSql = noteIds.join('","'); | ||||
| 	// 	noteIdsSql = '"' + noteIdsSql + '"'; | ||||
| 	// 	let options = { | ||||
| 	// 		conditions: ['id IN (' + noteIdsSql + ')'], | ||||
| 	// 	}; | ||||
|  | ||||
| 	// 	return Note.search(options); | ||||
| 	// } | ||||
|  | ||||
| 	// static async addNote(tagId, noteId) { | ||||
| 	// 	let hasIt = await this.hasNote(tagId, noteId); | ||||
| 	// 	if (hasIt) return; | ||||
|  | ||||
| 	// 	let query = Database.insertQuery('note_tags', { | ||||
| 	// 		tag_id: tagId, | ||||
| 	// 		note_id: noteId, | ||||
| 	// 	}); | ||||
|  | ||||
| 	// 	await this.db().exec(query); | ||||
| 	// 	await this.save({ id: tagId, updated_time: time.unixMs() }); | ||||
| 	// } | ||||
|  | ||||
| 	// static async removeNote(tagId, noteId) { | ||||
| 	// 	await this.db().exec('DELETE FROM note_tags WHERE tag_id = ? AND note_id = ?', [tagId, noteId]); | ||||
| 	// 	await this.save({ id: tagId, updated_time: time.unixMs() }); | ||||
| 	// } | ||||
|  | ||||
| 	// // Note: updated_time must not change here since this is only called from | ||||
| 	// // save(), which already handles how the updated_time property is set. | ||||
| 	// static async setAssociatedNotes_(tagId, noteIds) { | ||||
| 	// 	let queries = [{ | ||||
| 	// 		sql: 'DELETE FROM note_tags WHERE tag_id = ?', | ||||
| 	// 		params: [tagId], | ||||
| 	// 	}]; | ||||
|  | ||||
| 	// 	for (let i = 0; i < noteIds.length; i++) { | ||||
| 	// 		queries.push(Database.insertQuery('note_tags', { tag_id: tagId, note_id: noteIds[i] })); | ||||
| 	// 	} | ||||
|  | ||||
| 	// 	return this.db().transactionExecBatch(queries); | ||||
| 	// } | ||||
|  | ||||
| 	// static async hasNote(tagId, noteId) { | ||||
| 	// 	let r = await this.db().selectOne('SELECT note_id FROM note_tags WHERE tag_id = ? AND note_id = ? LIMIT 1', [tagId, noteId]); | ||||
| 	// 	return !!r; | ||||
| 	// } | ||||
|  | ||||
| 	// static async save(o, options = null) { | ||||
| 	// 	let result = await super.save(o, options); | ||||
|  | ||||
| 	// 	if (options && options.applyMetadataChanges === true) { | ||||
| 	// 		if (o.notes_) { | ||||
| 	// 			let noteIds = o.notes_.split(','); | ||||
| 	// 			await this.setAssociatedNotes_(o.id, noteIds); | ||||
| 	// 		} | ||||
| 	// 	} | ||||
|  | ||||
| 	// 	return result; | ||||
| 	// } | ||||
| 	static async serialize(item, type = null, shownKeys = null) { | ||||
| 		let fieldNames = this.fieldNames(); | ||||
| 		fieldNames.push('type_'); | ||||
| 		lodash.pull(fieldNames, 'sync_time'); | ||||
| 		return super.serialize(item, 'note_tag', fieldNames); | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -24,10 +24,6 @@ class Note extends BaseItem { | ||||
| 		return BaseModel.TYPE_NOTE; | ||||
| 	} | ||||
|  | ||||
| 	static trackDeleted() { | ||||
| 		return true; | ||||
| 	} | ||||
|  | ||||
| 	static new(parentId = '') { | ||||
| 		let output = super.new(); | ||||
| 		output.parent_id = parentId; | ||||
| @@ -76,28 +72,6 @@ class Note extends BaseItem { | ||||
| 		} | ||||
|  | ||||
| 		return this.search(options); | ||||
|  | ||||
| 		// let sql = 'SELECT ' + this.previewFieldsSql() + ' FROM notes WHERE is_conflict = 0 AND parent_id = ?'; | ||||
| 		// let params = [parentId]; | ||||
| 		// if (options.itemTypes && options.itemTypes.length) { | ||||
| 		// 	if (options.itemTypes.indexOf('note') >= 0 && options.itemTypes.indexOf('todo') >= 0) { | ||||
| 		// 		// Fetch everything | ||||
| 		// 	} else if (options.itemTypes.indexOf('note') >= 0) { | ||||
| 		// 		sql += ' AND is_todo = 0'; | ||||
| 		// 	} else if (options.itemTypes.indexOf('todo') >= 0) { | ||||
| 		// 		sql += ' AND is_todo = 1'; | ||||
| 		// 	} | ||||
| 		// } | ||||
|  | ||||
| 		// if (options.titlePattern) { | ||||
| 		// 	let pattern = options.titlePattern.replace(/\*/g, '%'); | ||||
| 		// 	sql += ' AND title LIKE ?'; | ||||
| 		// 	params.push(pattern); | ||||
| 		// } | ||||
|  | ||||
| 		// let query = this.applySqlOptions(options, sql, params); | ||||
|  | ||||
| 		// return this.modelSelectAll(query.sql, query.params); | ||||
| 	} | ||||
|  | ||||
| 	static preview(noteId) { | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { BaseModel } from 'lib/base-model.js'; | ||||
| import { Database } from 'lib/database.js'; | ||||
| import { BaseItem } from 'lib/models/base-item.js'; | ||||
| import { NoteTag } from 'lib/models/note-tag.js'; | ||||
| import { Note } from 'lib/models/note.js'; | ||||
| import { time } from 'lib/time-utils.js'; | ||||
| import lodash  from 'lodash'; | ||||
| @@ -18,13 +19,6 @@ class Tag extends BaseItem { | ||||
| 	static async serialize(item, type = null, shownKeys = null) { | ||||
| 		let fieldNames = this.fieldNames(); | ||||
| 		fieldNames.push('type_'); | ||||
| 		fieldNames.push(async () => { | ||||
| 			let noteIds = await this.tagNoteIds(item.id); | ||||
| 			return { | ||||
| 				key: 'notes_', | ||||
| 				value: noteIds.join(','), | ||||
| 			}; | ||||
| 		}); | ||||
| 		lodash.pull(fieldNames, 'sync_time'); | ||||
| 		return super.serialize(item, 'tag', fieldNames); | ||||
| 	} | ||||
| @@ -55,33 +49,17 @@ class Tag extends BaseItem { | ||||
| 		let hasIt = await this.hasNote(tagId, noteId); | ||||
| 		if (hasIt) return; | ||||
|  | ||||
| 		let query = Database.insertQuery('note_tags', { | ||||
| 		return NoteTag.save({ | ||||
| 			tag_id: tagId, | ||||
| 			note_id: noteId, | ||||
| 		}); | ||||
|  | ||||
| 		await this.db().exec(query); | ||||
| 		await this.save({ id: tagId, updated_time: time.unixMs() }); | ||||
| 	} | ||||
|  | ||||
| 	static async removeNote(tagId, noteId) { | ||||
| 		await this.db().exec('DELETE FROM note_tags WHERE tag_id = ? AND note_id = ?', [tagId, noteId]); | ||||
| 		await this.save({ id: tagId, updated_time: time.unixMs() }); | ||||
| 	} | ||||
|  | ||||
| 	// Note: updated_time must not change here since this is only called from | ||||
| 	// save(), which already handles how the updated_time property is set. | ||||
| 	static async setAssociatedNotes_(tagId, noteIds) { | ||||
| 		let queries = [{ | ||||
| 			sql: 'DELETE FROM note_tags WHERE tag_id = ?', | ||||
| 			params: [tagId], | ||||
| 		}]; | ||||
|  | ||||
| 		for (let i = 0; i < noteIds.length; i++) { | ||||
| 			queries.push(Database.insertQuery('note_tags', { tag_id: tagId, note_id: noteIds[i] })); | ||||
| 		let noteTags = await NoteTag.modelSelectAll('SELECT id FROM note_tags WHERE tag_id = ? and note_id = ?', [tagId, noteId]); | ||||
| 		for (let i = 0; i < noteTags.length; i++) { | ||||
| 			await NoteTag.delete(noteTags[i].id); | ||||
| 		} | ||||
|  | ||||
| 		return this.db().transactionExecBatch(queries); | ||||
| 	} | ||||
|  | ||||
| 	static async hasNote(tagId, noteId) { | ||||
| @@ -89,19 +67,6 @@ class Tag extends BaseItem { | ||||
| 		return !!r; | ||||
| 	} | ||||
|  | ||||
| 	static async save(o, options = null) { | ||||
| 		let result = await super.save(o, options); | ||||
|  | ||||
| 		if (options && options.applyMetadataChanges === true) { | ||||
| 			if (o.notes_) { | ||||
| 				let noteIds = o.notes_.split(','); | ||||
| 				await this.setAssociatedNotes_(o.id, noteIds); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return result; | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| export { Tag }; | ||||
		Reference in New Issue
	
	Block a user