1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-12 08:54:00 +02:00
joplin/ReactNativeClient/lib/models/base-item.js

484 lines
14 KiB
JavaScript
Raw Normal View History

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';
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
}
2017-07-16 18:06:05 +02:00
static async syncedCount(syncTarget) {
const ItemClass = this.itemClass(this.modelType());
const itemType = ItemClass.modelType();
// The fact that we don't check if the item_id still exist in the corresponding item table, means
// that the returned number might be innaccurate (for example if a sync operation was cancelled)
const sql = 'SELECT count(*) as total FROM sync_items WHERE sync_target = ? AND item_type = ?';
const r = await this.db().selectOne(sql, [ syncTarget, itemType ]);
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-07-18 22:03:07 +02:00
static isSystemPath(path) {
// 1b175bb38bba47baac22b0b47f778113.md
if (!path || !path.length) return false;
let p = path.split('/');
p = p[p.length - 1];
p = p.split('.');
if (p.length != 2) return false;
return p[0].length == 32 && p[1] == 'md';
}
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-23 16:11:44 +02:00
static async syncedItemIds(syncTarget) {
if (!syncTarget) throw new Error('No syncTarget specified');
2017-07-23 16:11:44 +02:00
let temp = await this.db().selectAll('SELECT item_id FROM sync_items WHERE sync_time > 0 AND sync_target = ?', [syncTarget]);
let output = [];
for (let i = 0; i < temp.length; i++) {
output.push(temp[i].item_id);
}
return output;
2017-07-01 12:30:50 +02:00
}
2017-06-15 20:18:48 +02:00
static pathToId(path) {
2017-07-19 00:14:20 +02:00
let p = path.split('/');
let s = p[p.length - 1].split('.');
2017-06-15 20:18:48 +02:00
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-18 21:27:10 +02:00
// Don't create a deleted_items entry when conflicted notes are deleted
// since no other client have (or should have) them.
let conflictNoteIds = [];
if (this.modelType() == BaseModel.TYPE_NOTE) {
const conflictNotes = await this.db().selectAll('SELECT id FROM notes WHERE id IN ("' + ids.join('","') + '") AND is_conflict = 1');
conflictNoteIds = conflictNotes.map((n) => { return n.id });
}
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-19 21:15:55 +02:00
const syncTargetIds = Database.enumIds('syncTarget');
2017-07-11 20:17:23 +02:00
let queries = [];
let now = time.unixMs();
for (let i = 0; i < ids.length; i++) {
2017-07-18 21:27:10 +02:00
if (conflictNoteIds.indexOf(ids[i]) >= 0) continue;
2017-07-19 21:15:55 +02:00
// For each deleted item, for each sync target, we need to add an entry in deleted_items.
// That way, each target can later delete the remote item.
for (let j = 0; j < syncTargetIds.length; j++) {
queries.push({
sql: 'INSERT INTO deleted_items (item_type, item_id, deleted_time, sync_target) VALUES (?, ?, ?, ?)',
params: [this.modelType(), ids[i], now, syncTargetIds[j]],
});
}
2017-07-11 20:17:23 +02:00
}
await this.db().transactionExecBatch(queries);
2017-07-03 21:50:45 +02:00
}
}
2017-07-19 21:15:55 +02:00
static deletedItems(syncTarget) {
return this.db().selectAll('SELECT * FROM deleted_items WHERE sync_target = ?', [syncTarget]);
2017-07-03 21:50:45 +02:00
}
2017-07-19 21:15:55 +02:00
static async deletedItemCount(syncTarget) {
let r = await this.db().selectOne('SELECT count(*) as total FROM deleted_items WHERE sync_target = ?', [syncTarget]);
2017-07-10 21:16:59 +02:00
return r['total'];
}
2017-07-19 21:15:55 +02:00
static remoteDeletedItem(syncTarget, itemId) {
return this.db().exec('DELETE FROM deleted_items WHERE item_id = ? AND sync_target = ?', [itemId, syncTarget]);
2017-07-03 21:50:45 +02:00
}
2017-06-19 00:06:10 +02:00
static serialize_format(propName, propValue) {
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
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
}
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];
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;
}
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);
2017-07-19 21:15:55 +02:00
let fieldNames = ItemClass.fieldNames('items');
// // NEVER SYNCED:
// 'SELECT * FROM [ITEMS] WHERE id NOT INT (SELECT item_id FROM sync_items WHERE sync_target = ?)'
// // CHANGED:
// 'SELECT * FROM [ITEMS] items JOIN sync_items s ON s.item_id = items.id WHERE sync_target = ? AND'
2017-07-18 00:22:22 +02:00
let extraWhere = className == 'Note' ? 'AND is_conflict = 0' : '';
2017-07-19 21:15:55 +02:00
// First get all the items that have never been synced under this sync target
let sql = sprintf(`
2017-07-19 21:15:55 +02:00
SELECT %s
FROM %s items
WHERE id NOT IN (
SELECT item_id FROM sync_items WHERE sync_target = %d
)
%s
LIMIT %d
`,
this.db().escapeFields(fieldNames),
this.db().escapeField(ItemClass.tableName()),
2017-07-19 21:15:55 +02:00
Number(syncTarget),
2017-07-18 00:22:22 +02:00
extraWhere,
limit);
2017-07-19 21:15:55 +02:00
let neverSyncedItem = await ItemClass.modelSelectAll(sql);
//for (let i = 0; i < neverSyncedItem.length; i++) neverSyncedItem[i].sync_time = 0;
// console.info(sql);
// console.info('NEVER', neverSyncedItem);
// Secondly get the items that have been synced under this sync target but that have been changed since then
const newLimit = limit - neverSyncedItem.length;
let changedItems = [];
if (newLimit > 0) {
fieldNames.push('sync_time');
let sql = sprintf(`
SELECT %s FROM %s items
JOIN sync_items s ON s.item_id = items.id
WHERE sync_target = %d
AND s.sync_time < items.updated_time
%s
LIMIT %d
`,
this.db().escapeFields(fieldNames),
this.db().escapeField(ItemClass.tableName()),
Number(syncTarget),
extraWhere,
newLimit);
changedItems = await ItemClass.modelSelectAll(sql);
}
// console.info('CHANGED', changedItems);
const items = neverSyncedItem.concat(changedItems);
if (i >= classNames.length - 1) {
return { hasMore: items.length >= limit, items: items };
} else {
if (items.length) return { hasMore: true, items: items };
}
2017-07-19 21:15:55 +02:00
//let extraWhere = className == 'Note' ? 'AND is_conflict = 0' : '';
// First get all the items that have never been synced under this sync target
// let sql = sprintf(`
// SELECT %s FROM %s items
// LEFT JOIN sync_items t ON t.item_id = items.id
// WHERE (t.id IS NULL OR t.sync_target != %d) %s
// LIMIT %d
// `,
// this.db().escapeFields(fieldNames),
// this.db().escapeField(ItemClass.tableName()),
// Number(syncTarget),
// extraWhere,
// limit);
// let neverSyncedItem = await ItemClass.modelSelectAll(sql);
// for (let i = 0; i < neverSyncedItem.length; i++) neverSyncedItem[i].sync_time = 0;
// console.info(sql);
// console.info('NEVER', neverSyncedItem);
// // Secondly get the items that have been synced under this sync target but that have been changed since then
// const newLimit = limit - neverSyncedItem.length;
// let changedItems = [];
// if (newLimit > 0) {
// let sql = sprintf(`
// SELECT %s FROM %s items
// LEFT JOIN sync_items t ON t.item_id = items.id
// WHERE (t.sync_time < items.updated_time AND t.sync_target = %d) %s
// LIMIT %d
// `,
// this.db().escapeFields(fieldNames),
// this.db().escapeField(ItemClass.tableName()),
// Number(syncTarget),
// extraWhere,
// newLimit);
// changedItems = await ItemClass.modelSelectAll(sql);
// }
// console.info('CHANGED', changedItems);
// const items = neverSyncedItem.concat(changedItems);
// 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');
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);
}
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);
2017-07-16 18:06:05 +02:00
let selectSql = 'SELECT id FROM ' + ItemClass.tableName();
if (ItemClass.modelType() == this.TYPE_NOTE) selectSql += ' WHERE is_conflict = 0';
queries.push('DELETE FROM sync_items WHERE item_type = ' + ItemClass.modelType() + ' AND item_id NOT IN (' + selectSql + ')');
}
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 };