You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Started synchronizer
This commit is contained in:
		| @@ -5,6 +5,11 @@ import { uuid } from 'src/uuid.js'; | |||||||
|  |  | ||||||
| class BaseModel { | class BaseModel { | ||||||
|  |  | ||||||
|  | 	static ITEM_TYPE_NOTE = 1; | ||||||
|  | 	static ITEM_TYPE_FOLDER = 2; | ||||||
|  | 	static tableInfo_ = null; | ||||||
|  | 	static tableKeys_ = null; | ||||||
|  |  | ||||||
| 	static tableName() { | 	static tableName() { | ||||||
| 		throw new Error('Must be overriden'); | 		throw new Error('Must be overriden'); | ||||||
| 	} | 	} | ||||||
| @@ -13,6 +18,14 @@ class BaseModel { | |||||||
| 		return false; | 		return false; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	static itemType() { | ||||||
|  | 		throw new Error('Must be overriden'); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	static trackChanges() { | ||||||
|  | 		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]; | ||||||
| @@ -20,12 +33,31 @@ class BaseModel { | |||||||
| 		return null; | 		return null; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	static save(o) { | 	static fieldNames() { | ||||||
| 		let isNew = !o.id; | 		return this.db().tableFieldNames(this.tableName()); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	static fromApiResult(apiResult) { | ||||||
|  | 		let fieldNames = this.fieldNames(); | ||||||
|  | 		let output = {}; | ||||||
|  | 		for (let i = 0; i < fieldNames.length; i++) { | ||||||
|  | 			let f = fieldNames[i]; | ||||||
|  | 			output[f] = f in apiResult ? apiResult[f] : null; | ||||||
|  | 		} | ||||||
|  | 		return output; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	static saveQuery(o, isNew = 'auto') { | ||||||
|  | 		if (isNew == 'auto') isNew = !o.id;		 | ||||||
| 		let query = ''; | 		let query = ''; | ||||||
|  | 		let itemId = o.id; | ||||||
|  |  | ||||||
| 		if (isNew) { | 		if (isNew) { | ||||||
| 			if (this.useUuid()) o.id = uuid.create(); | 			if (this.useUuid()) { | ||||||
|  | 				o = Object.assign({}, o); | ||||||
|  | 				itemId = uuid.create(); | ||||||
|  | 				o.id = itemId; | ||||||
|  | 			} | ||||||
| 			query = Database.insertQuery(this.tableName(), o); | 			query = Database.insertQuery(this.tableName(), o); | ||||||
| 		} else { | 		} else { | ||||||
| 			let where = { id: o.id }; | 			let where = { id: o.id }; | ||||||
| @@ -34,7 +66,44 @@ class BaseModel { | |||||||
| 			query = Database.updateQuery(this.tableName(), temp, where); | 			query = Database.updateQuery(this.tableName(), temp, where); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return this.db().exec(query.sql, query.params).then(() => { return o; }); | 		query.id = itemId; | ||||||
|  |  | ||||||
|  | 		return query; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	static save(o, trackChanges = true, isNew = 'auto') { | ||||||
|  | 		if (isNew == 'auto') isNew = !o.id; | ||||||
|  | 		let query = this.saveQuery(o, isNew); | ||||||
|  |  | ||||||
|  | 		return this.db().transaction((tx) => { | ||||||
|  | 			tx.executeSql(query.sql, query.params); | ||||||
|  |  | ||||||
|  | 			if (trackChanges && this.trackChanges()) { | ||||||
|  | 				// Cannot import this class the normal way due to cyclical dependencies between Change and BaseModel | ||||||
|  | 				// which are not handled by React Native. | ||||||
|  | 				const { Change } = require('src/models/change.js'); | ||||||
|  |  | ||||||
|  | 				let change = Change.newChange(); | ||||||
|  | 				change.type = isNew ? Change.TYPE_CREATE : Change.TYPE_UPDATE; | ||||||
|  | 				change.item_id = query.id; | ||||||
|  | 				change.item_type = this.itemType(); | ||||||
|  |  | ||||||
|  | 				let changeQuery = Change.saveQuery(change); | ||||||
|  | 				tx.executeSql(changeQuery.sql, changeQuery.params); | ||||||
|  |  | ||||||
|  | 				// TODO: item field for UPDATE | ||||||
|  | 			} | ||||||
|  | 		}).then(() => { | ||||||
|  | 			o = Object.assign({}, o); | ||||||
|  | 			o.id = query.id; | ||||||
|  |  | ||||||
|  | 			this.dispatch({ | ||||||
|  | 				type: 'FOLDERS_UPDATE_ONE', | ||||||
|  | 				folder: o, | ||||||
|  | 			}); | ||||||
|  |  | ||||||
|  | 			return o; | ||||||
|  | 		}); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	static delete(id) { | 	static delete(id) { | ||||||
|   | |||||||
| @@ -29,17 +29,6 @@ class NotesScreenComponent extends React.Component { | |||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	loginButton_press = () => { |  | ||||||
| 		this.props.dispatch({ |  | ||||||
| 			type: 'Navigation/NAVIGATE', |  | ||||||
| 			routeName: 'Login', |  | ||||||
| 		}); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	syncButton_press = () => { |  | ||||||
| 		Log.info('SYNC'); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	deleteFolder_onPress = (folderId) => { | 	deleteFolder_onPress = (folderId) => { | ||||||
| 		Folder.delete(folderId).then(() => { | 		Folder.delete(folderId).then(() => { | ||||||
| 			this.props.dispatch({ | 			this.props.dispatch({ | ||||||
| @@ -77,10 +66,6 @@ class NotesScreenComponent extends React.Component { | |||||||
| 			<View style={{flex: 1}}> | 			<View style={{flex: 1}}> | ||||||
| 				<ScreenHeader title={title} navState={this.props.navigation.state} menuOptions={this.menuOptions()} /> | 				<ScreenHeader title={title} navState={this.props.navigation.state} menuOptions={this.menuOptions()} /> | ||||||
| 				<NoteList style={{flex: 1}}/> | 				<NoteList style={{flex: 1}}/> | ||||||
| 				<View style={{flexDirection: 'row'}}> |  | ||||||
| 					<Button title="Login" onPress={this.loginButton_press} /> |  | ||||||
| 					<Button title="Sync" onPress={this.syncButton_press} /> |  | ||||||
| 				</View> |  | ||||||
| 				<ActionButton parentFolderId={this.props.selectedFolderId}></ActionButton> | 				<ActionButton parentFolderId={this.props.selectedFolderId}></ActionButton> | ||||||
| 			</View> | 			</View> | ||||||
| 		); | 		); | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import SQLite from 'react-native-sqlite-storage'; | import SQLite from 'react-native-sqlite-storage'; | ||||||
| import { Log } from 'src/log.js'; | import { Log } from 'src/log.js'; | ||||||
| import { uuid } from 'src/uuid.js'; | import { uuid } from 'src/uuid.js'; | ||||||
|  | import { PromiseChain } from 'src/promise-chain.js'; | ||||||
|  |  | ||||||
| const structureSql = ` | const structureSql = ` | ||||||
| CREATE TABLE folders ( | CREATE TABLE folders ( | ||||||
| @@ -65,7 +66,7 @@ CREATE TABLE version ( | |||||||
| ); | ); | ||||||
|  |  | ||||||
| CREATE TABLE changes ( | CREATE TABLE changes ( | ||||||
|     id INTEGER PRIMARY KEY, | 	id INTEGER PRIMARY KEY, | ||||||
| 	\`type\` INT, | 	\`type\` INT, | ||||||
| 	item_id TEXT, | 	item_id TEXT, | ||||||
| 	item_type INT, | 	item_type INT, | ||||||
| @@ -73,11 +74,17 @@ CREATE TABLE changes ( | |||||||
| ); | ); | ||||||
|  |  | ||||||
| CREATE TABLE settings ( | CREATE TABLE settings ( | ||||||
|     \`key\` TEXT PRIMARY KEY, | 	\`key\` TEXT PRIMARY KEY, | ||||||
| 	\`value\` TEXT, | 	\`value\` TEXT, | ||||||
| 	\`type\` INT | 	\`type\` INT | ||||||
| ); | ); | ||||||
|  |  | ||||||
|  | CREATE TABLE table_fields ( | ||||||
|  | 	id INTEGER PRIMARY KEY, | ||||||
|  | 	table_name TEXT, | ||||||
|  | 	field_name TEXT | ||||||
|  | ); | ||||||
|  |  | ||||||
| INSERT INTO version (version) VALUES (1); | INSERT INTO version (version) VALUES (1); | ||||||
| `; | `; | ||||||
|  |  | ||||||
| @@ -86,6 +93,7 @@ class Database { | |||||||
| 	constructor() { | 	constructor() { | ||||||
| 		this.debugMode_ = false; | 		this.debugMode_ = false; | ||||||
| 		this.initialized_ = false; | 		this.initialized_ = false; | ||||||
|  | 		this.tableFields_ = null; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	setDebugEnabled(v) { | 	setDebugEnabled(v) { | ||||||
| @@ -102,13 +110,13 @@ class Database { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	open() { | 	open() { | ||||||
| 		this.db_ = SQLite.openDatabase({ name: '/storage/emulated/0/Download/joplin-7.sqlite' }, (db) => { | 		this.db_ = SQLite.openDatabase({ name: '/storage/emulated/0/Download/joplin-10.sqlite' }, (db) => { | ||||||
| 			Log.info('Database was open successfully'); | 			Log.info('Database was open successfully'); | ||||||
| 		}, (error) => { | 		}, (error) => { | ||||||
| 			Log.error('Cannot open database: ', error); | 			Log.error('Cannot open database: ', error); | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		return this.updateSchema(); | 		return this.initialize(); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	static enumToId(type, s) { | 	static enumToId(type, s) { | ||||||
| @@ -119,6 +127,12 @@ class Database { | |||||||
| 		throw new Error('Unknown enum type or value: ' + type + ', ' + s); | 		throw new Error('Unknown enum type or value: ' + type + ', ' + s); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	tableFieldNames(tableName) { | ||||||
|  | 		if (!this.tableFields_) throw new Error('Fields have not been loaded yet'); | ||||||
|  | 		if (!this.tableFields_[tableName]) throw new Error('Unknown table: ' + tableName); | ||||||
|  | 		return this.tableFields_[tableName]; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	sqlStringToLines(sql) { | 	sqlStringToLines(sql) { | ||||||
| 		let output = []; | 		let output = []; | ||||||
| 		let lines = sql.split("\n"); | 		let lines = sql.split("\n"); | ||||||
| @@ -138,7 +152,7 @@ class Database { | |||||||
|  |  | ||||||
| 	logQuery(sql, params = null) { | 	logQuery(sql, params = null) { | ||||||
| 		if (!this.debugMode()) return; | 		if (!this.debugMode()) return; | ||||||
| 		Log.debug('DB: ' + sql, params); | 		//Log.debug('DB: ' + sql, params); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	selectOne(sql, params = null) { | 	selectOne(sql, params = null) { | ||||||
| @@ -220,36 +234,121 @@ class Database { | |||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	updateSchema() { | 	refreshTableFields() { | ||||||
| 		Log.info('Checking for database schema update...'); | 		return this.exec('SELECT name FROM sqlite_master WHERE type="table"').then((tableResults) => { | ||||||
|  | 			let chain = []; | ||||||
|  | 			for (let i = 0; i < tableResults.rows.length; i++) { | ||||||
|  | 				let row = tableResults.rows.item(i); | ||||||
|  | 				let tableName = row.name; | ||||||
|  | 				if (tableName == 'android_metadata') continue; | ||||||
|  | 				if (tableName == 'table_fields') continue; | ||||||
|  |  | ||||||
| 		return new Promise((resolve, reject) => { | 				chain.push((queries) => { | ||||||
| 			this.selectOne('SELECT * FROM version LIMIT 1').then((row) => { | 					if (!queries) queries = []; | ||||||
| 				Log.info('Current database version', row); | 					return this.exec('PRAGMA table_info("' + tableName + '")').then((pragmaResult) => { | ||||||
| 				resolve(); | 						for (let i = 0; i < pragmaResult.rows.length; i++) { | ||||||
| 				// TODO: version update logic | 							let q = Database.insertQuery('table_fields', { | ||||||
| 			}).catch((error) => { | 								table_name: tableName, | ||||||
| 				// Assume that error was: | 								field_name: pragmaResult.rows.item(i).name, | ||||||
| 				// { message: 'no such table: version (code 1): , while compiling: SELECT * FROM version', code: 0 } | 							}); | ||||||
| 				// which means the database is empty and the tables need to be created. | 							queries.push(q); | ||||||
|  | 						} | ||||||
|  | 						return queries; | ||||||
|  | 					}); | ||||||
|  | 				}); | ||||||
|  | 			} | ||||||
|  |  | ||||||
| 				Log.info('Database is new - creating the schema...'); | 			return PromiseChain.exec(chain).then((queries) => { | ||||||
|  | 				return this.transaction((tx) => { | ||||||
| 				let statements = this.sqlStringToLines(structureSql) | 					tx.executeSql('DELETE FROM table_fields'); | ||||||
| 				this.transaction((tx) => { | 					for (let i = 0; i < queries.length; i++) { | ||||||
| 					for (let i = 0; i < statements.length; i++) { | 						tx.executeSql(queries[i].sql, queries[i].params); | ||||||
| 						tx.executeSql(statements[i]); |  | ||||||
| 					} | 					} | ||||||
| 					tx.executeSql('INSERT INTO settings (`key`, `value`, `type`) VALUES ("clientId", "' + uuid.create() + '", "' + Database.enumToId('settings', 'string') + '")'); |  | ||||||
| 				}).then(() => { |  | ||||||
| 					resolve('Database schema created successfully'); |  | ||||||
| 				}).catch((error) => { |  | ||||||
| 					reject(error); |  | ||||||
| 				}); | 				}); | ||||||
| 			}); | 			}); | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	initialize() { | ||||||
|  | 		Log.info('Checking for database schema update...'); | ||||||
|  |  | ||||||
|  | 		return this.selectOne('SELECT * FROM version LIMIT 1').then((row) => { | ||||||
|  | 			Log.info('Current database version', row); | ||||||
|  | 			// TODO: version update logic | ||||||
|  |  | ||||||
|  | 			// TODO: only do this if db has been updated: | ||||||
|  | 			return this.refreshTableFields(); | ||||||
|  | 		}).then(() => { | ||||||
|  | 			return this.exec('SELECT * FROM table_fields').then((r) => { | ||||||
|  | 				this.tableFields_ = {}; | ||||||
|  | 				for (let i = 0; i < r.rows.length; i++) { | ||||||
|  | 					let row = r.rows.item(i); | ||||||
|  | 					if (!this.tableFields_[row.table_name]) this.tableFields_[row.table_name] = []; | ||||||
|  | 					this.tableFields_[row.table_name].push(row.field_name); | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 		}).catch((error) => { | ||||||
|  | 			// Assume that error was: | ||||||
|  | 			// { message: 'no such table: version (code 1): , while compiling: SELECT * FROM version', code: 0 } | ||||||
|  | 			// which means the database is empty and the tables need to be created. | ||||||
|  | 			// If it's any other error there's nothing we can do anyway. | ||||||
|  |  | ||||||
|  | 			Log.info('Database is new - creating the schema...'); | ||||||
|  |  | ||||||
|  | 			let statements = this.sqlStringToLines(structureSql) | ||||||
|  | 			return this.transaction((tx) => { | ||||||
|  | 				for (let i = 0; i < statements.length; i++) { | ||||||
|  | 					tx.executeSql(statements[i]); | ||||||
|  | 				} | ||||||
|  | 				tx.executeSql('INSERT INTO settings (`key`, `value`, `type`) VALUES ("clientId", "' + uuid.create() + '", "' + Database.enumToId('settings', 'string') + '")'); | ||||||
|  | 			}).then(() => { | ||||||
|  | 				Log.info('Database schema created successfully'); | ||||||
|  | 				// Calling initialize() now that the db has been created will make it go through | ||||||
|  | 				// the normal db update process (applying any additional patch). | ||||||
|  | 				return this.initialize(); | ||||||
|  | 			}) | ||||||
|  | 		}); | ||||||
|  |  | ||||||
|  | 		// return new Promise((resolve, reject) => { | ||||||
|  | 		// 	this.selectOne('SELECT * FROM version LIMIT 1').then((row) => { | ||||||
|  | 		// 		Log.info('Current database version', row); | ||||||
|  | 		// 		// TODO: version update logic | ||||||
|  | 		// 		// TODO: only do this if db has been updated | ||||||
|  | 		// 		return this.refreshTableFields(); | ||||||
|  | 		// 	}).then(() => { | ||||||
|  | 		// 		return this.exec('SELECT * FROM table_fields').then((r) => { | ||||||
|  | 		// 			this.tableFields_ = {}; | ||||||
|  | 		// 			for (let i = 0; i < r.rows.length; i++) { | ||||||
|  | 		// 				let row = r.rows.item(i); | ||||||
|  | 		// 				if (!this.tableFields_[row.table_name]) this.tableFields_[row.table_name] = []; | ||||||
|  | 		// 				this.tableFields_[row.table_name].push(row.field_name); | ||||||
|  | 		// 			} | ||||||
|  | 		// 		}); | ||||||
|  | 		// 	}).catch((error) => { | ||||||
|  | 		// 		// Assume that error was: | ||||||
|  | 		// 		// { message: 'no such table: version (code 1): , while compiling: SELECT * FROM version', code: 0 } | ||||||
|  | 		// 		// which means the database is empty and the tables need to be created. | ||||||
|  |  | ||||||
|  | 		// 		Log.info('Database is new - creating the schema...'); | ||||||
|  |  | ||||||
|  | 		// 		let statements = this.sqlStringToLines(structureSql) | ||||||
|  | 		// 		this.transaction((tx) => { | ||||||
|  | 		// 			for (let i = 0; i < statements.length; i++) { | ||||||
|  | 		// 				tx.executeSql(statements[i]); | ||||||
|  | 		// 			} | ||||||
|  | 		// 			tx.executeSql('INSERT INTO settings (`key`, `value`, `type`) VALUES ("clientId", "' + uuid.create() + '", "' + Database.enumToId('settings', 'string') + '")'); | ||||||
|  | 		// 		}).then(() => { | ||||||
|  | 		// 			Log.info('Database schema created successfully'); | ||||||
|  | 		// 			// Calling initialize() now that the db has been created will make it go through | ||||||
|  | 		// 			// the normal db update process (applying any additional patch). | ||||||
|  | 		// 			return this.initialize(); | ||||||
|  | 		// 		}).catch((error) => { | ||||||
|  | 		// 			reject(error); | ||||||
|  | 		// 		}); | ||||||
|  | 		// 	}); | ||||||
|  | 		// }); | ||||||
|  | 	} | ||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export { Database }; | export { Database }; | ||||||
							
								
								
									
										37
									
								
								ReactNativeClient/src/models/change.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								ReactNativeClient/src/models/change.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | |||||||
|  | import { BaseModel } from 'src/base-model.js'; | ||||||
|  | import { Log } from 'src/log.js'; | ||||||
|  |  | ||||||
|  | class Change extends BaseModel { | ||||||
|  |  | ||||||
|  | 	static TYPE_UNKNOWN = 0; | ||||||
|  | 	static TYPE_CREATE = 1; | ||||||
|  | 	static TYPE_UPDATE = 2; | ||||||
|  | 	static TYPE_DELETE = 3; | ||||||
|  |  | ||||||
|  | 	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 folders').then((r) => { | ||||||
|  | 	// 		let output = []; | ||||||
|  | 	// 		for (let i = 0; i < r.rows.length; i++) { | ||||||
|  | 	// 			output.push(r.rows.item(i)); | ||||||
|  | 	// 		} | ||||||
|  | 	// 		return output; | ||||||
|  | 	// 	}); | ||||||
|  | 	// } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export { Change }; | ||||||
| @@ -11,6 +11,14 @@ class Folder extends BaseModel { | |||||||
| 		return true; | 		return true; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	static itemType() { | ||||||
|  | 		return BaseModel.ITEM_TYPE_FOLDER; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	static trackChanges() { | ||||||
|  | 		return true; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	static newFolder() { | 	static newFolder() { | ||||||
| 		return { | 		return { | ||||||
| 			id: null, | 			id: null, | ||||||
|   | |||||||
| @@ -11,6 +11,14 @@ class Note extends BaseModel { | |||||||
| 		return true; | 		return true; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	static itemType() { | ||||||
|  | 		return BaseModel.ITEM_TYPE_NOTE; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	static trackChanges() { | ||||||
|  | 		return true; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	static newNote(parentId = null) { | 	static newNote(parentId = null) { | ||||||
| 		return { | 		return { | ||||||
| 			id: null, | 			id: null, | ||||||
|   | |||||||
| @@ -7,9 +7,9 @@ class Setting extends BaseModel { | |||||||
| 	static defaults_ = { | 	static defaults_ = { | ||||||
| 		'clientId': { value: '', type: 'string' }, | 		'clientId': { value: '', type: 'string' }, | ||||||
| 		'sessionId': { value: '', type: 'string' }, | 		'sessionId': { value: '', type: 'string' }, | ||||||
| 		'lastUpdateTime': { value: '', type: 'int' }, |  | ||||||
| 		'user.email': { value: '', type: 'string' }, | 		'user.email': { value: '', type: 'string' }, | ||||||
| 		'user.session': { value: '', type: 'string' }, | 		'user.session': { value: '', type: 'string' }, | ||||||
|  | 		'sync.lastRevId': { value: 0, type: 'int' }, | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	static tableName() { | 	static tableName() { | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								ReactNativeClient/src/promise-chain.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								ReactNativeClient/src/promise-chain.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | class PromiseChain { | ||||||
|  |  | ||||||
|  | 	static exec(chain) { | ||||||
|  | 		let output = new Promise((resolve, reject) => { resolve(); }); | ||||||
|  | 		for (let i = 0; i < chain.length; i++) { | ||||||
|  | 			let f = chain[i]; | ||||||
|  | 			output = output.then(f); | ||||||
|  | 		} | ||||||
|  | 		return output; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export { PromiseChain }; | ||||||
| @@ -9,6 +9,7 @@ import { addNavigationHelpers } from 'react-navigation'; | |||||||
| import { Log } from 'src/log.js' | import { Log } from 'src/log.js' | ||||||
| import { Note } from 'src/models/note.js' | import { Note } from 'src/models/note.js' | ||||||
| import { Folder } from 'src/models/folder.js' | import { Folder } from 'src/models/folder.js' | ||||||
|  | import { BaseModel } from 'src/base-model.js' | ||||||
| import { Database } from 'src/database.js' | import { Database } from 'src/database.js' | ||||||
| import { Registry } from 'src/registry.js' | import { Registry } from 'src/registry.js' | ||||||
| import { ItemList } from 'src/components/item-list.js' | import { ItemList } from 'src/components/item-list.js' | ||||||
| @@ -18,6 +19,7 @@ import { FolderScreen } from 'src/components/screens/folder.js' | |||||||
| import { FoldersScreen } from 'src/components/screens/folders.js' | import { FoldersScreen } from 'src/components/screens/folders.js' | ||||||
| import { LoginScreen } from 'src/components/screens/login.js' | import { LoginScreen } from 'src/components/screens/login.js' | ||||||
| import { Setting } from 'src/models/setting.js' | import { Setting } from 'src/models/setting.js' | ||||||
|  | import { Synchronizer } from 'src/synchronizer.js' | ||||||
| import { MenuContext } from 'react-native-popup-menu'; | import { MenuContext } from 'react-native-popup-menu'; | ||||||
|  |  | ||||||
| let defaultState = { | let defaultState = { | ||||||
| @@ -163,6 +165,8 @@ class AppComponent extends React.Component { | |||||||
| 		let db = new Database(); | 		let db = new Database(); | ||||||
| 		db.setDebugEnabled(Registry.debugMode()); | 		db.setDebugEnabled(Registry.debugMode()); | ||||||
|  |  | ||||||
|  | 		BaseModel.dispatch = this.props.dispatch; | ||||||
|  |  | ||||||
| 		db.open().then(() => { | 		db.open().then(() => { | ||||||
| 			Log.info('Database is ready.'); | 			Log.info('Database is ready.'); | ||||||
| 			Registry.setDb(db); | 			Registry.setDb(db); | ||||||
| @@ -174,6 +178,8 @@ class AppComponent extends React.Component { | |||||||
| 			Log.info('Client ID', Setting.value('clientId')); | 			Log.info('Client ID', Setting.value('clientId')); | ||||||
| 			Log.info('User', user); | 			Log.info('User', user); | ||||||
|  |  | ||||||
|  | 			Registry.api().setSession(user.session); | ||||||
|  |  | ||||||
| 			this.props.dispatch({ | 			this.props.dispatch({ | ||||||
| 				type: 'USER_SET', | 				type: 'USER_SET', | ||||||
| 				user: user, | 				user: user, | ||||||
| @@ -189,8 +195,11 @@ class AppComponent extends React.Component { | |||||||
| 			}).catch((error) => { | 			}).catch((error) => { | ||||||
| 				Log.warn('Cannot load folders', error); | 				Log.warn('Cannot load folders', error); | ||||||
| 			}); | 			}); | ||||||
|  | 		}).then(() => { | ||||||
|  | 			let synchronizer = new Synchronizer(); | ||||||
|  | 			synchronizer.start(); | ||||||
| 		}).catch((error) => { | 		}).catch((error) => { | ||||||
| 			Log.error('Cannot initialize database:', error); | 			Log.error('Initialization error:', error); | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										70
									
								
								ReactNativeClient/src/synchronizer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								ReactNativeClient/src/synchronizer.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | |||||||
|  | import { Registry } from 'src/registry.js'; | ||||||
|  | import { Log } from 'src/log.js'; | ||||||
|  | import { Setting } from 'src/models/setting.js'; | ||||||
|  | import { Change } from 'src/models/change.js'; | ||||||
|  | import { Folder } from 'src/models/folder.js'; | ||||||
|  |  | ||||||
|  | class Synchronizer { | ||||||
|  |  | ||||||
|  | 	constructor() { | ||||||
|  | 		this.state_ = 'idle'; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	state() { | ||||||
|  | 		return this.state_; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	db() { | ||||||
|  | 		return Registry.db(); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	api() { | ||||||
|  | 		return Registry.api(); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	switchState(state) { | ||||||
|  | 		Log.info('Sync: switching state to: ' + state); | ||||||
|  |  | ||||||
|  | 		if (state == 'downloadChanges') { | ||||||
|  | 			this.api().get('synchronizer', { last_id: Setting.value('sync.lastRevId') }).then((syncOperations) => { | ||||||
|  | 				let promise = new Promise((resolve, reject) => { resolve(); }); | ||||||
|  | 				for (let i = 0; i < syncOperations.items.length; i++) { | ||||||
|  | 					let syncOp = syncOperations.items[i]; | ||||||
|  | 					if (syncOp.item_type == 'folder') { | ||||||
|  | 						if (syncOp.type == 'create') { | ||||||
|  | 							promise = promise.then(() => { | ||||||
|  | 								let folder = Folder.fromApiResult(syncOp.item); | ||||||
|  | 								// TODO: automatically handle NULL fields by checking type and default value of field | ||||||
|  | 								if (!folder.parent_id) folder.parent_id = ''; | ||||||
|  | 								return Folder.save(folder, true, true); | ||||||
|  | 							}); | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				promise.then(() => { | ||||||
|  | 					Log.info('All items synced.'); | ||||||
|  | 				}).catch((error) => { | ||||||
|  | 					Log.warn('Sync error', error); | ||||||
|  | 				}); | ||||||
|  | 			}); | ||||||
|  | 		} else { | ||||||
|  |  | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	start() { | ||||||
|  |  | ||||||
|  | 		if (this.state() != 'idle') { | ||||||
|  | 			Log.info("Sync: cannot start synchronizer because synchronization already in progress. State: " + this.state()); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		Log.info('Sync: start'); | ||||||
|  |  | ||||||
|  | 		this.switchState('downloadChanges'); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export { Synchronizer }; | ||||||
| @@ -5,6 +5,15 @@ class WebApi { | |||||||
|  |  | ||||||
| 	constructor(baseUrl) { | 	constructor(baseUrl) { | ||||||
| 		this.baseUrl_ = baseUrl; | 		this.baseUrl_ = baseUrl; | ||||||
|  | 		this.session_ = null; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	setSession(v) { | ||||||
|  | 		this.session_ = v; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	session() { | ||||||
|  | 		return this.session_; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	makeRequest(method, path, query, data) { | 	makeRequest(method, path, query, data) { | ||||||
| @@ -38,14 +47,18 @@ class WebApi { | |||||||
| 		if (o.method != 'GET' && o.method != 'DELETE') { | 		if (o.method != 'GET' && o.method != 'DELETE') { | ||||||
| 			cmd.push("--data '" + stringify(data) + "'"); | 			cmd.push("--data '" + stringify(data) + "'"); | ||||||
| 		} | 		} | ||||||
| 		cmd.push(r.url); | 		cmd.push("'" + r.url + "'"); | ||||||
| 		return cmd.join(' '); | 		return cmd.join(' '); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	exec(method, path, query, data) { | 	exec(method, path, query, data) { | ||||||
| 		let that = this; | 		return new Promise((resolve, reject) => { | ||||||
| 		return new Promise(function(resolve, reject) { | 			if (this.session_) { | ||||||
| 			let r = that.makeRequest(method, path, query, data); | 				query = query ? Object.assign({}, query) : {}; | ||||||
|  | 				if (!query.session) query.session = this.session_; | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			let r = this.makeRequest(method, path, query, data); | ||||||
|  |  | ||||||
| 			Log.debug(WebApi.toCurl(r, data)); | 			Log.debug(WebApi.toCurl(r, data)); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -26,7 +26,7 @@ function config($name) { | |||||||
| 		'baseUrl' => $baseUrl, | 		'baseUrl' => $baseUrl, | ||||||
| 		'clientId' => 'E3E3E3E3E3E3E3E3E3E3E3E3E3E3E3E3', | 		'clientId' => 'E3E3E3E3E3E3E3E3E3E3E3E3E3E3E3E3', | ||||||
| 		'email' => 'laurent@cozic.net', | 		'email' => 'laurent@cozic.net', | ||||||
| 		'password' => '123456789', | 		'password' => '12345678', | ||||||
| 	); | 	); | ||||||
| 	if (isset($config[$name])) return $config[$name]; | 	if (isset($config[$name])) return $config[$name]; | ||||||
| 	throw new Exception('Unknown config: ' . $name); | 	throw new Exception('Unknown config: ' . $name); | ||||||
|   | |||||||
| @@ -381,7 +381,7 @@ class BaseModel extends \Illuminate\Database\Eloquent\Model { | |||||||
|  |  | ||||||
| 		if ($this->isVersioned) { | 		if ($this->isVersioned) { | ||||||
| 			if (count($changedFields)) { | 			if (count($changedFields)) { | ||||||
| 				$this->recordChanges($isNew ? 'create' : 'update', $changedFields); | 				$this->trackChanges($isNew ? 'create' : 'update', $changedFields); | ||||||
| 			} | 			} | ||||||
| 			$this->changedDiffableFields = array(); | 			$this->changedDiffableFields = array(); | ||||||
| 		} | 		} | ||||||
| @@ -395,13 +395,13 @@ class BaseModel extends \Illuminate\Database\Eloquent\Model { | |||||||
| 		$output = parent::delete(); | 		$output = parent::delete(); | ||||||
|  |  | ||||||
| 		if (count($this->isVersioned)) { | 		if (count($this->isVersioned)) { | ||||||
| 			$this->recordChanges('delete'); | 			$this->trackChanges('delete'); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return $output; | 		return $output; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	protected function recordChanges($type, $changedFields = array()) { | 	protected function trackChanges($type, $changedFields = array()) { | ||||||
| 		if ($type == 'delete') { | 		if ($type == 'delete') { | ||||||
| 			$change = $this->newChange($type); | 			$change = $this->newChange($type); | ||||||
| 			$change->save(); | 			$change->save(); | ||||||
|   | |||||||
| @@ -46,6 +46,7 @@ class Change extends BaseModel { | |||||||
| 			$itemIdToChange[$change->item_id] = $change; | 			$itemIdToChange[$change->item_id] = $change; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  |  | ||||||
| 		$output = array(); | 		$output = array(); | ||||||
| 		foreach ($itemIdToChange as $itemId => $change) { | 		foreach ($itemIdToChange as $itemId => $change) { | ||||||
| 			if (in_array($itemId, $createdItems) && in_array($itemId, $deletedItems)) { | 			if (in_array($itemId, $createdItems) && in_array($itemId, $deletedItems)) { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user