You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	sync tags
This commit is contained in:
		
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -76,7 +76,7 @@ async function saveNoteResources(note) { | ||||
| } | ||||
|  | ||||
| async function saveNoteTags(note) { | ||||
| 	let noteTagged = 0; | ||||
| 	let notesTagged = 0; | ||||
| 	for (let i = 0; i < note.tags.length; i++) { | ||||
| 		let tagTitle = note.tags[i]; | ||||
|  | ||||
| @@ -85,9 +85,9 @@ async function saveNoteTags(note) { | ||||
|  | ||||
| 		await Tag.addNote(tag.id, note.id); | ||||
|  | ||||
| 		noteTagged++; | ||||
| 		notesTagged++; | ||||
| 	} | ||||
| 	return noteTagged; | ||||
| 	return notesTagged; | ||||
| } | ||||
|  | ||||
| async function saveNoteToStorage(note, fuzzyMatching = false) { | ||||
| @@ -100,14 +100,14 @@ async function saveNoteToStorage(note, fuzzyMatching = false) { | ||||
| 		noteUpdated: false, | ||||
| 		noteSkipped: false, | ||||
| 		resourcesCreated: 0, | ||||
| 		noteTagged: 0, | ||||
| 		notesTagged: 0, | ||||
| 	}; | ||||
|  | ||||
| 	let resourcesCreated = await saveNoteResources(note); | ||||
| 	result.resourcesCreated += resourcesCreated; | ||||
|  | ||||
| 	let noteTagged = await saveNoteTags(note); | ||||
| 	result.noteTagged += noteTagged; | ||||
| 	let notesTagged = await saveNoteTags(note); | ||||
| 	result.notesTagged += notesTagged; | ||||
|  | ||||
| 	if (existingNote) { | ||||
| 		let diff = BaseModel.diffObjects(existingNote, note); | ||||
| @@ -148,7 +148,7 @@ function importEnex(parentFolderId, filePath, importOptions = null) { | ||||
| 			updated: 0, | ||||
| 			skipped: 0, | ||||
| 			resourcesCreated: 0, | ||||
| 			noteTagged: 0, | ||||
| 			notesTagged: 0, | ||||
| 		}; | ||||
|  | ||||
| 		let stream = fs.createReadStream(filePath); | ||||
| @@ -213,7 +213,7 @@ function importEnex(parentFolderId, filePath, importOptions = null) { | ||||
| 							progressState.skipped++; | ||||
| 						} | ||||
| 						progressState.resourcesCreated += result.resourcesCreated; | ||||
| 						progressState.noteTagged += result.noteTagged; | ||||
| 						progressState.notesTagged += result.notesTagged; | ||||
| 						importOptions.onProgress(progressState); | ||||
| 					}); | ||||
| 				}); | ||||
|   | ||||
| @@ -391,8 +391,8 @@ commands.push({ | ||||
| 				if (report.remotesToDelete) line.push(_('Remote items to delete: %d/%d.', report.deleteRemote, report.remotesToDelete)); | ||||
| 				if (report.localsToUdpate) line.push(_('Items to download: %d/%d.', report.createLocal + report.updateLocal, report.localsToUdpate)); | ||||
| 				if (report.localsToDelete) line.push(_('Local items to delete: %d/%d.', report.deleteLocal, report.localsToDelete)); | ||||
| 				//redrawnCalled = true; | ||||
| 				//vorpal.ui.redraw(line.join(' '));				 | ||||
| 				// redrawnCalled = true; | ||||
| 				// vorpal.ui.redraw(line.join(' ')); | ||||
| 			}, | ||||
| 			onMessage: (msg) => { | ||||
| 				if (redrawnCalled) vorpal.ui.redraw.done(); | ||||
|   | ||||
| @@ -45,7 +45,6 @@ function clearDatabase(id = null) { | ||||
| 	if (id === null) id = currentClient_; | ||||
|  | ||||
| 	let queries = [ | ||||
| 		'DELETE FROM changes', | ||||
| 		'DELETE FROM notes', | ||||
| 		'DELETE FROM folders', | ||||
| 		'DELETE FROM resources', | ||||
|   | ||||
| @@ -37,10 +37,6 @@ class BaseModel { | ||||
| 		throw new Error('Must be overriden'); | ||||
| 	} | ||||
|  | ||||
| 	static trackChanges() { | ||||
| 		return false; | ||||
| 	} | ||||
|  | ||||
| 	static trackDeleted() { | ||||
| 		return false; | ||||
| 	} | ||||
| @@ -106,7 +102,6 @@ class BaseModel { | ||||
| 		} else { | ||||
| 			options = Object.assign({}, options); | ||||
| 		} | ||||
| 		if (!('trackChanges' in options)) options.trackChanges = true; | ||||
| 		if (!('trackDeleted' in options)) options.trackDeleted = null; | ||||
| 		if (!('isNew' in options)) options.isNew = 'auto'; | ||||
| 		if (!('autoTimestamp' in options)) options.autoTimestamp = true; | ||||
| @@ -321,6 +316,7 @@ BaseModel.MODEL_TYPE_NOTE = 1; | ||||
| BaseModel.MODEL_TYPE_FOLDER = 2; | ||||
| BaseModel.MODEL_TYPE_SETTING = 3; | ||||
| BaseModel.MODEL_TYPE_RESOURCE = 4; | ||||
| BaseModel.MODEL_TYPE_TAG = 5; | ||||
| BaseModel.tableInfo_ = null; | ||||
| BaseModel.tableKeys_ = null; | ||||
| BaseModel.db_ = null; | ||||
|   | ||||
| @@ -60,7 +60,8 @@ CREATE TABLE tags ( | ||||
| 	id TEXT PRIMARY KEY, | ||||
| 	title TEXT NOT NULL DEFAULT "", | ||||
| 	created_time INT NOT NULL, | ||||
| 	updated_time INT NOT NULL | ||||
| 	updated_time INT NOT NULL, | ||||
| 	sync_time INT NOT NULL DEFAULT 0 | ||||
| ); | ||||
|  | ||||
| CREATE TABLE note_tags ( | ||||
| @@ -79,14 +80,6 @@ CREATE TABLE resources ( | ||||
| 	sync_time INT NOT NULL DEFAULT 0 | ||||
| ); | ||||
|  | ||||
| CREATE TABLE changes ( | ||||
| 	id INTEGER PRIMARY KEY, | ||||
| 	\`type\` INT, | ||||
| 	item_id TEXT, | ||||
| 	item_type INT, | ||||
| 	item_field TEXT | ||||
| ); | ||||
|  | ||||
| CREATE TABLE settings ( | ||||
| 	\`key\` TEXT PRIMARY KEY, | ||||
| 	\`value\` TEXT, | ||||
|   | ||||
| @@ -32,6 +32,7 @@ class BaseItem extends BaseModel { | ||||
| 			if (Number(item) === BaseModel.MODEL_TYPE_NOTE) return this.getClass('Note'); | ||||
| 			if (Number(item) === BaseModel.MODEL_TYPE_FOLDER) return this.getClass('Folder'); | ||||
| 			if (Number(item) === BaseModel.MODEL_TYPE_RESOURCE) return this.getClass('Resource'); | ||||
| 			if (Number(item) === BaseModel.MODEL_TYPE_TAG) return this.getClass('Tag'); | ||||
| 			throw new Error('Unknown type: ' + item); | ||||
| 		} | ||||
| 	} | ||||
| @@ -40,7 +41,9 @@ class BaseItem extends BaseModel { | ||||
| 	static async syncedItems() { | ||||
| 		let folders =  await this.getClass('Folder').modelSelectAll('SELECT id FROM folders WHERE sync_time > 0'); | ||||
| 		let notes = await this.getClass('Note').modelSelectAll('SELECT id FROM notes WHERE is_conflict = 0 AND sync_time > 0'); | ||||
| 		return folders.concat(notes); | ||||
| 		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); | ||||
| 	} | ||||
|  | ||||
| 	static pathToId(path) { | ||||
| @@ -52,11 +55,13 @@ class BaseItem extends BaseModel { | ||||
| 		return this.loadItemById(this.pathToId(path)); | ||||
| 	} | ||||
|  | ||||
| 	static loadItemById(id) { | ||||
| 		return this.getClass('Note').load(id).then((item) => { | ||||
| 	static async loadItemById(id) { | ||||
| 		let classes = ['Note', 'Folder', 'Resource', 'Tag']; | ||||
| 		for (let i = 0; i < classes.length; i++) { | ||||
| 			let item = await this.getClass(classes[i]).load(id); | ||||
| 			if (item) return item; | ||||
| 			return this.getClass('Folder').load(id); | ||||
| 		}); | ||||
| 		} | ||||
| 		return null; | ||||
| 	} | ||||
|  | ||||
| 	static loadItemByField(itemType, field, value) { | ||||
| @@ -86,7 +91,7 @@ class BaseItem extends BaseModel { | ||||
| 	} | ||||
|  | ||||
| 	static unserialize_format(type, propName, propValue) { | ||||
| 		if (propName == 'type_') return propValue; | ||||
| 		if (propName[propName.length - 1] == '_') return propValue; // Private property | ||||
|  | ||||
| 		let ItemClass = this.itemClass(type); | ||||
|  | ||||
| @@ -110,9 +115,17 @@ class BaseItem extends BaseModel { | ||||
| 		output.push(type == 'note' ? item.body : ''); | ||||
| 		output.push(''); | ||||
| 		for (let i = 0; i < shownKeys.length; i++) { | ||||
| 			let v = item[shownKeys[i]]; | ||||
| 			v = this.serialize_format(shownKeys[i], v); | ||||
| 			output.push(shownKeys[i] + ': ' + v); | ||||
| 			let key = shownKeys[i]; | ||||
| 			let value = null; | ||||
| 			if (typeof key === 'function') { | ||||
| 				let r = await key(); | ||||
| 				key = r.key; | ||||
| 				value = r.value; | ||||
| 			} else { | ||||
| 				value = this.serialize_format(key, item[key]); | ||||
| 			} | ||||
|  | ||||
| 			output.push(key + ': ' + value); | ||||
| 		} | ||||
|  | ||||
| 		return output.join("\n"); | ||||
| @@ -170,6 +183,9 @@ class BaseItem extends BaseModel { | ||||
| 		if (items.length) return { hasMore: true, items: items }; | ||||
|  | ||||
| 		items = await this.getClass('Note').modelSelectAll('SELECT * FROM notes WHERE sync_time < updated_time AND is_conflict = 0 LIMIT ' + limit); | ||||
| 		if (items.length) return { hasMore: true, items: items }; | ||||
|  | ||||
| 		items = await this.getClass('Tag').modelSelectAll('SELECT * FROM tags WHERE sync_time < updated_time LIMIT ' + limit); | ||||
| 		return { hasMore: items.length >= limit, items: items }; | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -1,101 +0,0 @@ | ||||
| import { BaseModel } from 'lib/base-model.js'; | ||||
| import { Log } from 'lib/log.js'; | ||||
|  | ||||
| class Change extends BaseModel { | ||||
|  | ||||
| 	static tableName() { | ||||
| 		return 'changes'; | ||||
| 	} | ||||
|  | ||||
| 	static newChange() { | ||||
| 		return { | ||||
| 			id: null, | ||||
| 			type: null, | ||||
| 			item_id: null, | ||||
| 			item_type: null, | ||||
| 			item_field: null, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	static all() { | ||||
| 		return this.db().selectAll('SELECT * FROM changes'); | ||||
| 	} | ||||
|  | ||||
| 	static deleteMultiple(ids) { | ||||
| 		if (ids.length == 0) return Promise.resolve(); | ||||
|  | ||||
| 		let queries = []; | ||||
| 		for (let i = 0; i < ids.length; i++) { | ||||
| 			queries.push(['DELETE FROM changes WHERE id = ?', [ids[i]]]); | ||||
| 		} | ||||
|  | ||||
| 		return this.db().transactionExecBatch(queries); | ||||
| 	} | ||||
|  | ||||
| 	static mergeChanges(changes) { | ||||
| 		let createdItems = []; | ||||
| 		let deletedItems = []; | ||||
| 		let itemChanges = {}; | ||||
|  | ||||
| 		for (let i = 0; i < changes.length; i++) { | ||||
| 			let change = changes[i]; | ||||
| 			let mergedChange = null; | ||||
|  | ||||
| 			if (itemChanges[change.item_id]) { | ||||
| 				mergedChange = itemChanges[change.item_id]; | ||||
| 			} else { | ||||
| 				mergedChange = { | ||||
| 					item_id: change.item_id, | ||||
| 					item_type: change.item_type, | ||||
| 					fields: [], | ||||
| 					ids: [], | ||||
| 					type: change.type, | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if (change.type == this.TYPE_CREATE) { | ||||
| 				createdItems.push(change.item_id); | ||||
| 			} else if (change.type == this.TYPE_DELETE) { | ||||
| 				deletedItems.push(change.item_id); | ||||
| 			} else if (change.type == this.TYPE_UPDATE) { | ||||
| 				if (mergedChange.fields.indexOf(change.item_field) < 0) { | ||||
| 					mergedChange.fields.push(change.item_field); | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			mergedChange.ids.push(change.id); | ||||
|  | ||||
| 			itemChanges[change.item_id] = mergedChange; | ||||
| 		} | ||||
|  | ||||
| 		let output = []; | ||||
|  | ||||
| 		for (let itemId in itemChanges) { | ||||
| 			if (!itemChanges.hasOwnProperty(itemId)) continue; | ||||
| 			let change = itemChanges[itemId]; | ||||
|  | ||||
| 			if (createdItems.indexOf(itemId) >= 0 && deletedItems.indexOf(itemId) >= 0) { | ||||
| 				// Item both created then deleted - skip | ||||
| 				change.type = this.TYPE_NOOP; | ||||
| 			} else if (deletedItems.indexOf(itemId) >= 0) { | ||||
| 				// Item was deleted at some point - just return one 'delete' event | ||||
| 				change.type = this.TYPE_DELETE; | ||||
| 			} else if (createdItems.indexOf(itemId) >= 0) { | ||||
| 				// Item was created then updated - just return one 'create' event with the latest changes | ||||
| 				change.type = this.TYPE_CREATE; | ||||
| 			} | ||||
|  | ||||
| 			output.push(change); | ||||
| 		} | ||||
|  | ||||
| 		return output; | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| Change.TYPE_NOOP = 0; | ||||
| Change.TYPE_CREATE = 1; | ||||
| Change.TYPE_UPDATE = 2; | ||||
| Change.TYPE_DELETE = 3; | ||||
|  | ||||
| export { Change }; | ||||
| @@ -25,10 +25,6 @@ class Folder extends BaseItem { | ||||
| 		return BaseModel.MODEL_TYPE_FOLDER; | ||||
| 	} | ||||
|  | ||||
| 	static trackChanges() { | ||||
| 		return true; | ||||
| 	} | ||||
|  | ||||
| 	static trackDeleted() { | ||||
| 		return true; | ||||
| 	} | ||||
|   | ||||
| @@ -24,10 +24,6 @@ class Note extends BaseItem { | ||||
| 		return BaseModel.MODEL_TYPE_NOTE; | ||||
| 	} | ||||
|  | ||||
| 	static trackChanges() { | ||||
| 		return true; | ||||
| 	} | ||||
|  | ||||
| 	static trackDeleted() { | ||||
| 		return true; | ||||
| 	} | ||||
|   | ||||
| @@ -16,17 +16,31 @@ class Tag extends BaseItem { | ||||
| 	static async serialize(item, type = null, shownKeys = null) { | ||||
| 		let fieldNames = this.fieldNames(); | ||||
| 		fieldNames.push('type_'); | ||||
| 		fieldNames.push(() => { | ||||
| 			 | ||||
| 		fieldNames.push(async () => { | ||||
| 			let noteIds = await this.tagNoteIds(item.id); | ||||
| 			console.info('NOTE IDS', noteIds); | ||||
| 			return { | ||||
| 				key: 'notes_', | ||||
| 				value: noteIds.join(','), | ||||
| 			}; | ||||
| 		}); | ||||
| 		lodash.pull(fieldNames, 'sync_time'); | ||||
| 		return super.serialize(item, 'tag', fieldNames); | ||||
| 	} | ||||
|  | ||||
| 	static tagNoteIds(tagId) { | ||||
| 		return this.db().selectAll('SELECT note_id FROM note_tags WHERE tag_id = ?', [tagId]); | ||||
| 	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; | ||||
| 	} | ||||
|  | ||||
| 	// TODO: in order for a sync to happen, the updated_time property should somehow be changed | ||||
| 	// whenever an tag is applied or removed from an item. Either the updated_time property | ||||
| 	// is changed here or by the caller? | ||||
|  | ||||
| 	static async addNote(tagId, noteId) { | ||||
| 		let hasIt = await this.hasNote(tagId, noteId); | ||||
| 		if (hasIt) return; | ||||
| @@ -35,7 +49,30 @@ class Tag extends BaseItem { | ||||
| 			tag_id: tagId, | ||||
| 			note_id: noteId, | ||||
| 		}); | ||||
| 		return this.db().exec(query); | ||||
|  | ||||
| 		await this.db().exec(query); | ||||
| 		//await this.save({ id: tagId, updated_time: time.unixMs() }); //type_: BaseModel.MODEL_TYPE_TAG | ||||
| 	} | ||||
|  | ||||
| 	static async addNotes(tagId, noteIds) { | ||||
| 		for (let i = 0; i < noteIds.length; i++) { | ||||
| 			await this.addNote(tagId, noteIds[i]); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Note: updated_time must not change since this is only called from | ||||
| 	// the synchronizer, which manages and sets the correct updated_time | ||||
| 	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) { | ||||
|   | ||||
| @@ -331,6 +331,11 @@ class Synchronizer { | ||||
| 							} | ||||
| 						} | ||||
|  | ||||
| 						if (newContent.type_ == BaseModel.MODEL_TYPE_TAG) { | ||||
| 							let noteIds = newContent.notes_.split(','); | ||||
| 							await ItemClass.setAssociatedNotes(newContent.id, noteIds); | ||||
| 						} | ||||
|  | ||||
| 						this.logSyncOperation(action, local, content, reason); | ||||
| 					} else { | ||||
| 						this.logSyncOperation(action, local, remote, reason); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user