1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

sycnhronizer

This commit is contained in:
Laurent Cozic 2017-05-20 00:16:50 +02:00
parent c030791f07
commit 43f2c6c756
7 changed files with 139 additions and 51 deletions

View File

@ -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_;
}
}

View File

@ -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 (
<View style={{flex: 1}}>
<ScreenHeader navState={this.props.navigation.state} />
@ -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)

View File

@ -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

View File

@ -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) {

View File

@ -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',

View File

@ -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(() => {

View File

@ -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;