From 43f2c6c756228a390360d89b73470465d0f9b52d Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Sat, 20 May 2017 00:16:50 +0200 Subject: [PATCH] sycnhronizer --- ReactNativeClient/src/base-model.js | 30 ++++++++- .../src/components/screens/note.js | 6 +- ReactNativeClient/src/database.js | 65 +++++++++++++++--- ReactNativeClient/src/models/note.js | 11 ++- ReactNativeClient/src/root.js | 6 +- ReactNativeClient/src/synchronizer.js | 67 ++++++++++++------- structure.sql | 5 ++ 7 files changed, 139 insertions(+), 51 deletions(-) diff --git a/ReactNativeClient/src/base-model.js b/ReactNativeClient/src/base-model.js index e38903813d..1953c5bd83 100644 --- a/ReactNativeClient/src/base-model.js +++ b/ReactNativeClient/src/base-model.js @@ -1,6 +1,5 @@ import { Log } from 'src/log.js'; import { Database } from 'src/database.js'; -import { Registry } from 'src/registry.js'; import { uuid } from 'src/uuid.js'; class BaseModel { @@ -9,6 +8,7 @@ class BaseModel { static ITEM_TYPE_FOLDER = 2; static tableInfo_ = null; static tableKeys_ = null; + static db_ = null; static tableName() { throw new Error('Must be overriden'); @@ -42,6 +42,20 @@ class BaseModel { return this.db().tableFieldNames(this.tableName()); } + static fields() { + return this.db().tableFields(this.tableName()); + } + + static new() { + let fields = this.fields(); + let output = {}; + for (let i = 0; i < fields.length; i++) { + let f = fields[i]; + output[f.name] = f.default; + } + return output; + } + static fromApiResult(apiResult) { let fieldNames = this.fieldNames(); let output = {}; @@ -78,6 +92,15 @@ class BaseModel { static saveQuery(o, isNew = 'auto') { if (isNew == 'auto') isNew = !o.id; + + let temp = {} + let fieldNames = this.fieldNames(); + for (let i = 0; i < fieldNames.length; i++) { + let n = fieldNames[i]; + if (n in o) temp[n] = o[n]; + } + o = temp; + let query = ''; let itemId = o.id; @@ -106,6 +129,8 @@ class BaseModel { query.id = itemId; + Log.info('Saving', o); + return query; } @@ -176,7 +201,8 @@ class BaseModel { } static db() { - return Registry.db(); + if (!this.db_) throw new Error('Accessing database before it has been initialised'); + return this.db_; } } diff --git a/ReactNativeClient/src/components/screens/note.js b/ReactNativeClient/src/components/screens/note.js index 771f61cd30..ce14f3661b 100644 --- a/ReactNativeClient/src/components/screens/note.js +++ b/ReactNativeClient/src/components/screens/note.js @@ -13,7 +13,7 @@ class NoteScreenComponent extends React.Component { constructor() { super(); - this.state = { note: Note.newNote() } + this.state = { note: Note.new() } } componentWillMount() { @@ -48,8 +48,6 @@ class NoteScreenComponent extends React.Component { } render() { - Log.info(this.state.note); - return ( @@ -65,7 +63,7 @@ class NoteScreenComponent extends React.Component { const NoteScreen = connect( (state) => { return { - note: state.selectedNoteId ? Note.byId(state.notes, state.selectedNoteId) : Note.newNote(state.selectedFolderId), + note: state.selectedNoteId ? Note.byId(state.notes, state.selectedNoteId) : Note.new(state.selectedFolderId), }; } )(NoteScreenComponent) diff --git a/ReactNativeClient/src/database.js b/ReactNativeClient/src/database.js index 6888009002..ca946424ec 100644 --- a/ReactNativeClient/src/database.js +++ b/ReactNativeClient/src/database.js @@ -26,8 +26,8 @@ CREATE TABLE notes ( author TEXT NOT NULL DEFAULT "", source_url TEXT NOT NULL DEFAULT "", is_todo BOOLEAN NOT NULL DEFAULT 0, - todo_due INT NOT NULL DEFAULT "", - todo_completed INT NOT NULL DEFAULT "", + todo_due INT NOT NULL DEFAULT 0, + todo_completed BOOLEAN NOT NULL DEFAULT 0, source_application TEXT NOT NULL DEFAULT "", application_data TEXT NOT NULL DEFAULT "", \`order\` INT NOT NULL DEFAULT 0 @@ -82,7 +82,9 @@ CREATE TABLE settings ( CREATE TABLE table_fields ( id INTEGER PRIMARY KEY, table_name TEXT, - field_name TEXT + field_name TEXT, + field_type INT, + field_default TEXT ); INSERT INTO version (version) VALUES (1); @@ -90,6 +92,11 @@ INSERT INTO version (version) VALUES (1); class Database { + static TYPE_INT = 1; + static TYPE_TEXT = 2; + static TYPE_BOOLEAN = 3; + static TYPE_NUMERIC = 4; + constructor() { this.debugMode_ = false; this.initialized_ = false; @@ -110,7 +117,7 @@ class Database { } open() { - this.db_ = SQLite.openDatabase({ name: '/storage/emulated/0/Download/joplin-15.sqlite' }, (db) => { + this.db_ = SQLite.openDatabase({ name: '/storage/emulated/0/Download/joplin-21.sqlite' }, (db) => { Log.info('Database was open successfully'); }, (error) => { Log.error('Cannot open database: ', error); @@ -119,20 +126,41 @@ class Database { return this.initialize(); } - static enumToId(type, s) { + static enumId(type, s) { if (type == 'settings') { if (s == 'int') return 1; if (s == 'string') return 2; } + if (type == 'fieldType') { + return this['TYPE_' + s]; + } throw new Error('Unknown enum type or value: ' + type + ', ' + s); } tableFieldNames(tableName) { + let tf = this.tableFields(tableName); + let output = []; + for (let i = 0; i < tf.length; i++) { + output.push(tf[i].name); + } + return output; + } + + tableFields(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]; } + static formatValue(type, value) { + if (value === null || value === undefined) return null; + if (type == this.TYPE_INT) return Number(value); + if (type == this.TYPE_TEXT) return value; + if (type == this.TYPE_BOOLEAN) return !!Number(value); + if (type == this.TYPE_NUMERIC) return Number(value); + throw new Error('Unknown type: ' + type); + } + sqlStringToLines(sql) { let output = []; let lines = sql.split("\n"); @@ -213,7 +241,7 @@ class Database { for (let key in data) { if (!data.hasOwnProperty(key)) continue; if (sql != '') sql += ', '; - sql += key + '=?'; + sql += '`' + key + '`=?'; params.push(data[key]); } @@ -251,9 +279,17 @@ class Database { if (!queries) queries = []; return this.exec('PRAGMA table_info("' + tableName + '")').then((pragmaResult) => { for (let i = 0; i < pragmaResult.rows.length; i++) { + let item = pragmaResult.rows.item(i); + // In SQLite, if the default value is a string it has double quotes around it, so remove them here + let defaultValue = item.dflt_value; + if (typeof defaultValue == 'string' && defaultValue.length >= 2 && defaultValue[0] == '"' && defaultValue[defaultValue.length - 1] == '"') { + defaultValue = defaultValue.substr(1, defaultValue.length - 2); + } let q = Database.insertQuery('table_fields', { table_name: tableName, - field_name: pragmaResult.rows.item(i).name, + field_name: item.name, + field_type: Database.enumId('fieldType', item.type), + field_default: defaultValue, }); queries.push(q); } @@ -288,10 +324,21 @@ class Database { 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); + this.tableFields_[row.table_name].push({ + name: row.field_name, + type: row.field_type, + default: Database.formatValue(row.field_type, row.field_default), + }); } + + Log.info(this.tableFields_); }); }).catch((error) => { + if (error && error.code != 0) { + Log.error(error); + return; + } + // 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. @@ -304,7 +351,7 @@ class Database { 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') + '")'); + tx.executeSql('INSERT INTO settings (`key`, `value`, `type`) VALUES ("clientId", "' + uuid.create() + '", "' + Database.enumId('settings', 'string') + '")'); }).then(() => { Log.info('Database schema created successfully'); // Calling initialize() now that the db has been created will make it go through diff --git a/ReactNativeClient/src/models/note.js b/ReactNativeClient/src/models/note.js index cd08eee299..257bb52426 100644 --- a/ReactNativeClient/src/models/note.js +++ b/ReactNativeClient/src/models/note.js @@ -19,13 +19,10 @@ class Note extends BaseModel { return true; } - static newNote(parentId = null) { - return { - id: null, - title: '', - body: '', - parent_id: parentId, - } + static new(parentId = '') { + let output = super.new(); + output.parent_id = parentId; + return output; } static previews(parentId) { diff --git a/ReactNativeClient/src/root.js b/ReactNativeClient/src/root.js index 89d31f7b4b..828a02ff51 100644 --- a/ReactNativeClient/src/root.js +++ b/ReactNativeClient/src/root.js @@ -165,7 +165,9 @@ class AppComponent extends React.Component { let db = new Database(); //db.setDebugEnabled(Registry.debugMode()); db.setDebugEnabled(false); + BaseModel.dispatch = this.props.dispatch; + BaseModel.db_ = db; db.open().then(() => { Log.info('Database is ready.'); @@ -187,10 +189,6 @@ class AppComponent extends React.Component { Log.info('Loading folders...'); - // Folder.noteIds('80a90393377b440bbf6edfe849bb87c5').then((ids) => { - // Log.info(ids); - // }); - Folder.all().then((folders) => { this.props.dispatch({ type: 'FOLDERS_UPDATE_ALL', diff --git a/ReactNativeClient/src/synchronizer.js b/ReactNativeClient/src/synchronizer.js index d232d89369..0c3feca730 100644 --- a/ReactNativeClient/src/synchronizer.js +++ b/ReactNativeClient/src/synchronizer.js @@ -3,6 +3,8 @@ 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'; +import { Note } from 'src/models/note.js'; +import { BaseModel } from 'src/base-model.js'; import { promiseChain } from 'src/promise-chain.js'; class Synchronizer { @@ -35,31 +37,36 @@ class Synchronizer { for (let i = 0; i < syncOperations.items.length; i++) { let syncOp = syncOperations.items[i]; if (syncOp.id > maxRevId) maxRevId = syncOp.id; + + let ItemClass = null; if (syncOp.item_type == 'folder') { + ItemClass = Folder; + } else if (syncOp.item_type == 'note') { + ItemClass = Note; + } - if (syncOp.type == 'create') { - chain.push(() => { - 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, { isNew: true, trackChanges: false }); - }); - } + if (syncOp.type == 'create') { + chain.push(() => { + let item = ItemClass.fromApiResult(syncOp.item); + // TODO: automatically handle NULL fields by checking type and default value of field + if ('parent_id' in item && !item.parent_id) item.parent_id = ''; + return ItemClass.save(item, { isNew: true, trackChanges: false }); + }); + } - if (syncOp.type == 'update') { - chain.push(() => { - return Folder.load(syncOp.item_id).then((folder) => { - folder = Folder.applyPatch(folder, syncOp.item); - return Folder.save(folder, { trackChanges: false }); - }); + if (syncOp.type == 'update') { + chain.push(() => { + return ItemClass.load(syncOp.item_id).then((item) => { + item = ItemClass.applyPatch(item, syncOp.item); + return ItemClass.save(item, { trackChanges: false }); }); - } + }); + } - if (syncOp.type == 'delete') { - chain.push(() => { - return Folder.delete(syncOp.item_id, { trackChanges: false }); - }); - } + if (syncOp.type == 'delete') { + chain.push(() => { + return ItemClass.delete(syncOp.item_id, { trackChanges: false }); + }); } } return promiseChain(chain); @@ -84,18 +91,28 @@ class Synchronizer { chain.push(() => { let p = null; + let ItemClass = null; + let path = null; + if (c.item_type == BaseModel.ITEM_TYPE_FOLDER) { + ItemClass = Folder; + path = 'folders'; + } else if (c.item_type == BaseModel.ITEM_TYPE_NOTE) { + ItemClass = Note; + path = 'notes'; + } + if (c.type == Change.TYPE_NOOP) { p = Promise.resolve(); } else if (c.type == Change.TYPE_CREATE) { - p = Folder.load(c.item_id).then((folder) => { - return this.api().put('folders/' + folder.id, null, folder); + p = ItemClass.load(c.item_id).then((item) => { + return this.api().put(path + '/' + item.id, null, item); }); } else if (c.type == Change.TYPE_UPDATE) { - p = Folder.load(c.item_id).then((folder) => { - return this.api().patch('folders/' + folder.id, null, folder); + p = ItemClass.load(c.item_id).then((item) => { + return this.api().patch(path + '/' + item.id, null, item); }); } else if (c.type == Change.TYPE_DELETE) { - return this.api().delete('folders/' + c.item_id); + return this.api().delete(path + '/' + c.item_id); } return p.then(() => { diff --git a/structure.sql b/structure.sql index e33b66a692..41db1c0249 100755 --- a/structure.sql +++ b/structure.sql @@ -26,6 +26,11 @@ CREATE TABLE `notes` ( `is_todo` tinyint(1) NOT NULL default '0', `todo_due` int(11) NOT NULL default '0', `todo_completed` int(11) NOT NULL default '0', + `application_data` varchar(1024) NOT NULL DEFAULT "", + `author` varchar(512) NOT NULL DEFAULT "", + `source` varchar(512) NOT NULL DEFAULT "", + `source_application` varchar(512) NOT NULL DEFAULT "", + `source_url` varchar(1024) NOT NULL DEFAULT "", PRIMARY KEY (`id`) ) CHARACTER SET=utf8;