2017-06-24 20:06:28 +02:00
|
|
|
import { BaseModel } from 'lib/base-model.js';
|
|
|
|
import { Database } from 'lib/database.js';
|
|
|
|
import { time } from 'lib/time-utils.js';
|
2017-07-16 14:53:59 +02:00
|
|
|
import { sprintf } from 'sprintf-js';
|
2017-06-15 20:18:48 +02:00
|
|
|
import moment from 'moment';
|
|
|
|
|
|
|
|
class BaseItem extends BaseModel {
|
|
|
|
|
|
|
|
static useUuid() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2017-07-06 21:48:17 +02:00
|
|
|
static loadClass(className, classRef) {
|
|
|
|
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
|
|
|
|
if (BaseItem.syncItemDefinitions_[i].className == className) {
|
|
|
|
BaseItem.syncItemDefinitions_[i].classRef = classRef;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new Error('Invalid class name: ' + className);
|
|
|
|
}
|
|
|
|
|
2017-07-02 14:02:07 +02:00
|
|
|
// Need to dynamically load the classes like this to avoid circular dependencies
|
|
|
|
static getClass(name) {
|
2017-07-06 21:48:17 +02:00
|
|
|
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
|
|
|
|
if (BaseItem.syncItemDefinitions_[i].className == name) {
|
|
|
|
return BaseItem.syncItemDefinitions_[i].classRef;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new Error('Invalid class name: ' + name);
|
2017-07-10 20:09:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static async syncedCount() {
|
2017-07-16 14:53:59 +02:00
|
|
|
// TODO
|
|
|
|
return 0;
|
|
|
|
// const ItemClass = this.itemClass(this.modelType());
|
|
|
|
// let sql = 'SELECT count(*) as total FROM `' + ItemClass.tableName() + '` WHERE updated_time <= sync_time';
|
|
|
|
// if (this.modelType() == BaseModel.TYPE_NOTE) sql += ' AND is_conflict = 0';
|
|
|
|
// const r = await this.db().selectOne(sql);
|
|
|
|
// return r.total;
|
2017-07-02 14:02:07 +02:00
|
|
|
}
|
|
|
|
|
2017-06-20 21:18:19 +02:00
|
|
|
static systemPath(itemOrId) {
|
|
|
|
if (typeof itemOrId === 'string') return itemOrId + '.md';
|
|
|
|
return itemOrId.id + '.md';
|
2017-06-15 20:18:48 +02:00
|
|
|
}
|
|
|
|
|
2017-06-17 20:40:08 +02:00
|
|
|
static itemClass(item) {
|
|
|
|
if (!item) throw new Error('Item cannot be null');
|
2017-06-19 00:06:10 +02:00
|
|
|
|
|
|
|
if (typeof item === 'object') {
|
|
|
|
if (!('type_' in item)) throw new Error('Item does not have a type_ property');
|
2017-07-02 14:02:07 +02:00
|
|
|
return this.itemClass(item.type_);
|
2017-06-19 00:06:10 +02:00
|
|
|
} else {
|
2017-07-04 00:08:14 +02:00
|
|
|
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
|
|
|
|
let d = BaseItem.syncItemDefinitions_[i];
|
|
|
|
if (Number(item) == d.type) return this.getClass(d.className);
|
|
|
|
}
|
2017-06-19 00:06:10 +02:00
|
|
|
throw new Error('Unknown type: ' + item);
|
|
|
|
}
|
2017-06-17 20:40:08 +02:00
|
|
|
}
|
|
|
|
|
2017-07-01 12:30:50 +02:00
|
|
|
// Returns the IDs of the items that have been synced at least once
|
2017-07-16 14:53:59 +02:00
|
|
|
static async syncedItems(syncTarget) {
|
|
|
|
if (!syncTarget) throw new Error('No syncTarget specified');
|
|
|
|
return await this.db().selectAll('SELECT item_id, item_type FROM sync_items WHERE sync_time > 0 AND sync_target = ?', [syncTarget]);
|
2017-07-01 12:30:50 +02:00
|
|
|
}
|
|
|
|
|
2017-06-15 20:18:48 +02:00
|
|
|
static pathToId(path) {
|
|
|
|
let s = path.split('.');
|
|
|
|
return s[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
static loadItemByPath(path) {
|
2017-07-02 12:34:07 +02:00
|
|
|
return this.loadItemById(this.pathToId(path));
|
|
|
|
}
|
|
|
|
|
2017-07-02 20:38:34 +02:00
|
|
|
static async loadItemById(id) {
|
2017-07-04 00:08:14 +02:00
|
|
|
let classes = this.syncItemClassNames();
|
2017-07-02 20:38:34 +02:00
|
|
|
for (let i = 0; i < classes.length; i++) {
|
|
|
|
let item = await this.getClass(classes[i]).load(id);
|
2017-06-15 20:18:48 +02:00
|
|
|
if (item) return item;
|
2017-07-02 20:38:34 +02:00
|
|
|
}
|
|
|
|
return null;
|
2017-06-15 20:18:48 +02:00
|
|
|
}
|
|
|
|
|
2017-06-25 09:52:25 +02:00
|
|
|
static loadItemByField(itemType, field, value) {
|
|
|
|
let ItemClass = this.itemClass(itemType);
|
|
|
|
return ItemClass.loadByField(field, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
static loadItem(itemType, id) {
|
|
|
|
let ItemClass = this.itemClass(itemType);
|
|
|
|
return ItemClass.load(id);
|
|
|
|
}
|
|
|
|
|
|
|
|
static deleteItem(itemType, id) {
|
|
|
|
let ItemClass = this.itemClass(itemType);
|
|
|
|
return ItemClass.delete(id);
|
|
|
|
}
|
|
|
|
|
2017-07-03 21:50:45 +02:00
|
|
|
static async delete(id, options = null) {
|
2017-07-11 20:17:23 +02:00
|
|
|
return this.batchDelete([id], options);
|
|
|
|
}
|
|
|
|
|
|
|
|
static async batchDelete(ids, options = null) {
|
2017-07-04 00:08:14 +02:00
|
|
|
let trackDeleted = true;
|
2017-07-03 21:50:45 +02:00
|
|
|
if (options && options.trackDeleted !== null && options.trackDeleted !== undefined) trackDeleted = options.trackDeleted;
|
|
|
|
|
2017-07-11 20:17:23 +02:00
|
|
|
await super.batchDelete(ids, options);
|
2017-07-03 21:50:45 +02:00
|
|
|
|
|
|
|
if (trackDeleted) {
|
2017-07-11 20:17:23 +02:00
|
|
|
let queries = [];
|
|
|
|
let now = time.unixMs();
|
|
|
|
for (let i = 0; i < ids.length; i++) {
|
|
|
|
queries.push({
|
|
|
|
sql: 'INSERT INTO deleted_items (item_type, item_id, deleted_time) VALUES (?, ?, ?)',
|
|
|
|
params: [this.modelType(), ids[i], now],
|
|
|
|
});
|
|
|
|
}
|
|
|
|
await this.db().transactionExecBatch(queries);
|
2017-07-03 21:50:45 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static deletedItems() {
|
|
|
|
return this.db().selectAll('SELECT * FROM deleted_items');
|
|
|
|
}
|
|
|
|
|
2017-07-10 21:16:59 +02:00
|
|
|
static async deletedItemCount() {
|
|
|
|
let r = await this.db().selectOne('SELECT count(*) as total FROM deleted_items');
|
|
|
|
return r['total'];
|
|
|
|
}
|
|
|
|
|
2017-07-03 21:50:45 +02:00
|
|
|
static remoteDeletedItem(itemId) {
|
|
|
|
return this.db().exec('DELETE FROM deleted_items WHERE item_id = ?', [itemId]);
|
|
|
|
}
|
|
|
|
|
2017-06-19 00:06:10 +02:00
|
|
|
static serialize_format(propName, propValue) {
|
2017-07-13 20:47:31 +02:00
|
|
|
if (['created_time', 'updated_time', 'sync_time'].indexOf(propName) >= 0) {
|
2017-06-15 20:18:48 +02:00
|
|
|
if (!propValue) return '';
|
2017-06-19 00:06:10 +02:00
|
|
|
propValue = moment.unix(propValue / 1000).utc().format('YYYY-MM-DDTHH:mm:ss.SSS') + 'Z';
|
2017-06-15 20:18:48 +02:00
|
|
|
} else if (propValue === null || propValue === undefined) {
|
|
|
|
propValue = '';
|
|
|
|
}
|
|
|
|
|
|
|
|
return propValue;
|
|
|
|
}
|
|
|
|
|
2017-06-19 00:06:10 +02:00
|
|
|
static unserialize_format(type, propName, propValue) {
|
2017-07-02 20:38:34 +02:00
|
|
|
if (propName[propName.length - 1] == '_') return propValue; // Private property
|
2017-06-15 20:18:48 +02:00
|
|
|
|
2017-06-19 00:06:10 +02:00
|
|
|
let ItemClass = this.itemClass(type);
|
|
|
|
|
2017-06-15 20:18:48 +02:00
|
|
|
if (['created_time', 'updated_time'].indexOf(propName) >= 0) {
|
|
|
|
if (!propValue) return 0;
|
2017-06-19 00:06:10 +02:00
|
|
|
propValue = moment(propValue, 'YYYY-MM-DDTHH:mm:ss.SSSZ').format('x');
|
2017-06-15 20:18:48 +02:00
|
|
|
} else {
|
2017-06-19 00:06:10 +02:00
|
|
|
propValue = Database.formatValue(ItemClass.fieldType(propName), propValue);
|
2017-06-15 20:18:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return propValue;
|
|
|
|
}
|
|
|
|
|
2017-07-02 17:46:03 +02:00
|
|
|
static async serialize(item, type = null, shownKeys = null) {
|
2017-06-29 22:52:52 +02:00
|
|
|
item = this.filter(item);
|
|
|
|
|
2017-07-13 23:26:45 +02:00
|
|
|
let output = {};
|
2017-06-15 20:18:48 +02:00
|
|
|
|
2017-07-13 20:47:31 +02:00
|
|
|
if ('title' in item && shownKeys.indexOf('title') >= 0) {
|
2017-07-13 23:26:45 +02:00
|
|
|
output.title = item.title;
|
2017-07-03 22:38:26 +02:00
|
|
|
}
|
|
|
|
|
2017-07-13 20:47:31 +02:00
|
|
|
if ('body' in item && shownKeys.indexOf('body') >= 0) {
|
2017-07-13 23:26:45 +02:00
|
|
|
output.body = item.body;
|
2017-07-03 22:38:26 +02:00
|
|
|
}
|
|
|
|
|
2017-07-13 23:26:45 +02:00
|
|
|
output.props = [];
|
|
|
|
|
2017-06-15 20:18:48 +02:00
|
|
|
for (let i = 0; i < shownKeys.length; i++) {
|
2017-07-02 20:38:34 +02:00
|
|
|
let key = shownKeys[i];
|
2017-07-13 20:47:31 +02:00
|
|
|
if (key == 'title' || key == 'body') continue;
|
|
|
|
|
2017-07-02 20:38:34 +02:00
|
|
|
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]);
|
|
|
|
}
|
|
|
|
|
2017-07-13 23:26:45 +02:00
|
|
|
output.props.push(key + ': ' + value);
|
2017-06-15 20:18:48 +02:00
|
|
|
}
|
|
|
|
|
2017-07-13 23:26:45 +02:00
|
|
|
let temp = [];
|
|
|
|
|
|
|
|
if (output.title) temp.push(output.title);
|
|
|
|
if (output.body) temp.push(output.body);
|
|
|
|
if (output.props.length) temp.push(output.props.join("\n"));
|
|
|
|
|
|
|
|
return temp.join("\n\n");
|
2017-06-15 20:18:48 +02:00
|
|
|
}
|
|
|
|
|
2017-07-02 17:46:03 +02:00
|
|
|
static async unserialize(content) {
|
2017-06-15 20:18:48 +02:00
|
|
|
let lines = content.split("\n");
|
|
|
|
let output = {};
|
|
|
|
let state = 'readingProps';
|
|
|
|
let body = [];
|
2017-07-05 20:31:11 +02:00
|
|
|
|
2017-06-15 20:18:48 +02:00
|
|
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
|
|
let line = lines[i];
|
|
|
|
|
|
|
|
if (state == 'readingProps') {
|
|
|
|
line = line.trim();
|
|
|
|
|
|
|
|
if (line == '') {
|
|
|
|
state = 'readingBody';
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
let p = line.indexOf(':');
|
|
|
|
if (p < 0) throw new Error('Invalid property format: ' + line + ": " + content);
|
|
|
|
let key = line.substr(0, p).trim();
|
|
|
|
let value = line.substr(p + 1).trim();
|
2017-06-19 00:06:10 +02:00
|
|
|
output[key] = value;
|
2017-06-15 20:18:48 +02:00
|
|
|
} else if (state == 'readingBody') {
|
|
|
|
body.splice(0, 0, line);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-19 00:06:10 +02:00
|
|
|
if (!output.type_) throw new Error('Missing required property: type_: ' + content);
|
|
|
|
output.type_ = Number(output.type_);
|
|
|
|
|
2017-07-03 22:38:26 +02:00
|
|
|
if (body.length) {
|
|
|
|
let title = body.splice(0, 2);
|
|
|
|
output.title = title[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (body.length) output.body = body.join("\n");
|
2017-06-15 20:18:48 +02:00
|
|
|
|
2017-06-19 00:06:10 +02:00
|
|
|
for (let n in output) {
|
|
|
|
if (!output.hasOwnProperty(n)) continue;
|
2017-07-02 17:46:03 +02:00
|
|
|
output[n] = await this.unserialize_format(output.type_, n, output[n]);
|
2017-06-19 00:06:10 +02:00
|
|
|
}
|
|
|
|
|
2017-06-15 20:18:48 +02:00
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
2017-07-16 14:53:59 +02:00
|
|
|
static async itemsThatNeedSync(syncTarget, limit = 100) {
|
|
|
|
const classNames = this.syncItemClassNames();
|
|
|
|
|
|
|
|
for (let i = 0; i < classNames.length; i++) {
|
|
|
|
const className = classNames[i];
|
|
|
|
const ItemClass = this.getClass(className);
|
|
|
|
const fieldNames = ItemClass.fieldNames(true);
|
|
|
|
fieldNames.push('sync_time');
|
|
|
|
|
|
|
|
let sql = sprintf(`
|
|
|
|
SELECT %s FROM %s
|
|
|
|
LEFT JOIN sync_items t ON t.item_id = %s.id
|
|
|
|
WHERE t.id IS NULL OR t.sync_time < %s.updated_time
|
|
|
|
LIMIT %d
|
|
|
|
`,
|
|
|
|
this.db().escapeFields(fieldNames),
|
|
|
|
this.db().escapeField(ItemClass.tableName()),
|
|
|
|
this.db().escapeField(ItemClass.tableName()),
|
|
|
|
this.db().escapeField(ItemClass.tableName()),
|
|
|
|
limit);
|
|
|
|
|
|
|
|
const items = await ItemClass.modelSelectAll(sql);
|
|
|
|
|
|
|
|
if (i >= classNames.length - 1) {
|
|
|
|
return { hasMore: items.length >= limit, items: items };
|
|
|
|
} else {
|
|
|
|
if (items.length) return { hasMore: true, items: items };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new Error('Unreachable');
|
|
|
|
|
|
|
|
//return this.modelSelectAll('SELECT * FROM folders WHERE sync_time < updated_time LIMIT ' + limit);
|
2017-07-02 14:02:07 +02:00
|
|
|
|
2017-07-16 14:53:59 +02:00
|
|
|
// let items = await this.getClass('Folder').modelSelectAll('SELECT * FROM folders WHERE sync_time < updated_time LIMIT ' + limit);
|
|
|
|
// if (items.length) return { hasMore: true, items: items };
|
2017-07-02 14:02:07 +02:00
|
|
|
|
2017-07-16 14:53:59 +02:00
|
|
|
// items = await this.getClass('Resource').modelSelectAll('SELECT * FROM resources WHERE sync_time < updated_time LIMIT ' + limit);
|
|
|
|
// if (items.length) return { hasMore: true, items: items };
|
2017-07-02 20:38:34 +02:00
|
|
|
|
2017-07-16 14:53:59 +02:00
|
|
|
// 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 };
|
2017-07-04 00:08:14 +02:00
|
|
|
|
2017-07-16 14:53:59 +02:00
|
|
|
// 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 };
|
2017-06-19 00:06:10 +02:00
|
|
|
}
|
|
|
|
|
2017-07-04 00:08:14 +02:00
|
|
|
static syncItemClassNames() {
|
|
|
|
return BaseItem.syncItemDefinitions_.map((def) => {
|
|
|
|
return def.className;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-07-14 20:02:45 +02:00
|
|
|
static modelTypeToClassName(type) {
|
|
|
|
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
|
|
|
|
if (BaseItem.syncItemDefinitions_[i].type == type) return BaseItem.syncItemDefinitions_[i].className;
|
|
|
|
}
|
|
|
|
throw new Error('Invalid type: ' + type);
|
|
|
|
}
|
|
|
|
|
2017-07-16 14:53:59 +02:00
|
|
|
static updateSyncTimeQueries(syncTarget, item, syncTime) {
|
|
|
|
const itemType = item.type_;
|
|
|
|
const itemId = item.id;
|
|
|
|
if (!itemType || !itemId || syncTime === undefined) throw new Error('Invalid parameters in updateSyncTimeQueries()');
|
|
|
|
|
|
|
|
return [
|
|
|
|
{
|
|
|
|
sql: 'DELETE FROM sync_items WHERE sync_target = ? AND item_type = ? AND item_id = ?',
|
|
|
|
params: [syncTarget, itemType, itemId],
|
|
|
|
},
|
|
|
|
{
|
|
|
|
sql: 'INSERT INTO sync_items (sync_target, item_type, item_id, sync_time) VALUES (?, ?, ?, ?)',
|
|
|
|
params: [syncTarget, itemType, itemId, syncTime],
|
|
|
|
}
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
static async saveSyncTime(syncTarget, item, syncTime) {
|
|
|
|
const queries = this.updateSyncTimeQueries(syncTarget, item, syncTime);
|
|
|
|
return this.db().transactionExecBatch(queries);
|
|
|
|
}
|
|
|
|
|
|
|
|
static async deleteOrphanSyncItems() {
|
|
|
|
const classNames = this.syncItemClassNames();
|
|
|
|
|
|
|
|
let queries = [];
|
|
|
|
for (let i = 0; i < classNames.length; i++) {
|
|
|
|
const className = classNames[i];
|
|
|
|
const ItemClass = this.getClass(className);
|
|
|
|
|
|
|
|
queries.push('DELETE FROM sync_items WHERE item_type = ' + ItemClass.modelType() + ' AND item_id NOT IN (SELECT id FROM ' + ItemClass.tableName() + ')');
|
|
|
|
}
|
|
|
|
|
|
|
|
await this.db().transactionExecBatch(queries);
|
|
|
|
}
|
|
|
|
|
2017-06-15 20:18:48 +02:00
|
|
|
}
|
|
|
|
|
2017-07-04 00:08:14 +02:00
|
|
|
// 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' },
|
|
|
|
];
|
|
|
|
|
2017-06-15 20:18:48 +02:00
|
|
|
export { BaseItem };
|