Support 'successful' property in upload response
Save uploaded data to cache, and update local object if necessary (which it mostly shouldn't be except for invalid characters and HTML filtering in notes) Also add some upload and JSON tests
This commit is contained in:
parent
70d9b9870c
commit
4600318ad7
|
@ -678,14 +678,43 @@ Zotero.Collection.prototype.fromJSON = Zotero.Promise.coroutine(function* (json)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
Zotero.Collection.prototype.toJSON = Zotero.Promise.coroutine(function* (options) {
|
Zotero.Collection.prototype.toResponseJSON = Zotero.Promise.coroutine(function* (options = {}) {
|
||||||
var obj = {};
|
var json = yield this.constructor._super.prototype.toResponseJSON.apply(this, options);
|
||||||
|
|
||||||
|
// TODO: library block?
|
||||||
|
|
||||||
|
// creatorSummary
|
||||||
|
var firstCreator = this.getField('firstCreator');
|
||||||
|
if (firstCreator) {
|
||||||
|
json.meta.creatorSummary = firstCreator;
|
||||||
|
}
|
||||||
|
// parsedDate
|
||||||
|
var parsedDate = Zotero.Date.multipartToSQL(this.getField('date', true, true));
|
||||||
|
if (parsedDate) {
|
||||||
|
// 0000?
|
||||||
|
json.meta.parsedDate = parsedDate;
|
||||||
|
}
|
||||||
|
// numChildren
|
||||||
|
if (this.isRegularItem()) {
|
||||||
|
json.meta.numChildren = this.numChildren();
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
Zotero.Collection.prototype.toJSON = Zotero.Promise.coroutine(function* (options = {}) {
|
||||||
|
var env = this._preToJSON(options);
|
||||||
|
var mode = env.mode;
|
||||||
|
|
||||||
|
var obj = env.obj = {};
|
||||||
obj.key = this.key;
|
obj.key = this.key;
|
||||||
obj.version = this.version;
|
obj.version = this.version;
|
||||||
|
|
||||||
obj.name = this.name;
|
obj.name = this.name;
|
||||||
obj.parentCollection = this.parentKey ? this.parentKey : false;
|
obj.parentCollection = this.parentKey ? this.parentKey : false;
|
||||||
obj.relations = {}; // TEMP
|
obj.relations = {}; // TEMP
|
||||||
return obj;
|
|
||||||
|
return this._postToJSON(env);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1190,6 +1190,47 @@ Zotero.DataObject.prototype._finalizeErase = function (env) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Zotero.DataObject.prototype.toResponseJSON = Zotero.Promise.coroutine(function* (options) {
|
||||||
|
// TODO: library block?
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: this.key,
|
||||||
|
version: this.version,
|
||||||
|
meta: {},
|
||||||
|
data: yield this.toJSON(options)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
Zotero.DataObject.prototype._preToJSON = function (options) {
|
||||||
|
var env = { options };
|
||||||
|
env.mode = options.mode || 'new';
|
||||||
|
if (env.mode == 'patch') {
|
||||||
|
if (!options.patchBase) {
|
||||||
|
throw new Error("Cannot use patch mode if patchBase not provided");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (options.patchBase) {
|
||||||
|
if (options.mode) {
|
||||||
|
Zotero.debug("Zotero.Item.toJSON: ignoring provided patchBase in " + env.mode + " mode", 2);
|
||||||
|
}
|
||||||
|
// If patchBase provided and no explicit mode, use 'patch'
|
||||||
|
else {
|
||||||
|
env.mode = 'patch';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
Zotero.DataObject.prototype._postToJSON = function (env) {
|
||||||
|
if (env.mode == 'patch') {
|
||||||
|
env.obj = Zotero.DataObjectUtilities.patch(env.options.patchBase, env.obj);
|
||||||
|
}
|
||||||
|
return env.obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates data object key
|
* Generates data object key
|
||||||
* @return {String} key
|
* @return {String} key
|
||||||
|
|
|
@ -101,6 +101,42 @@ Zotero.DataObjectUtilities = {
|
||||||
return Zotero[className]
|
return Zotero[className]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
patch: function (base, obj) {
|
||||||
|
var target = {};
|
||||||
|
Object.assign(target, obj);
|
||||||
|
|
||||||
|
for (let i in base) {
|
||||||
|
switch (i) {
|
||||||
|
case 'key':
|
||||||
|
case 'version':
|
||||||
|
case 'dateModified':
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If field from base exists in the new version, delete it if it's the same
|
||||||
|
if (i in target) {
|
||||||
|
if (!this._fieldChanged(i, base[i], target[i])) {
|
||||||
|
delete target[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If field from base doesn't exist in new version, clear it
|
||||||
|
else {
|
||||||
|
switch (i) {
|
||||||
|
case 'deleted':
|
||||||
|
target[i] = false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
target[i] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return target;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine whether two API JSON objects are equivalent
|
* Determine whether two API JSON objects are equivalent
|
||||||
*
|
*
|
||||||
|
@ -129,24 +165,9 @@ Zotero.DataObjectUtilities = {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let changed;
|
let changed = this._fieldChanged(field, val1, val2);
|
||||||
|
if (changed) {
|
||||||
switch (field) {
|
return true;
|
||||||
case 'creators':
|
|
||||||
case 'collections':
|
|
||||||
case 'tags':
|
|
||||||
case 'relations':
|
|
||||||
changed = this["_" + field + "Changed"](val1, val2);
|
|
||||||
if (changed) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
changed = val1 !== val2;
|
|
||||||
if (changed) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
skipFields[field] = true;
|
skipFields[field] = true;
|
||||||
|
@ -170,6 +191,20 @@ Zotero.DataObjectUtilities = {
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_fieldChanged: function (fieldName, field1, field2) {
|
||||||
|
switch (fieldName) {
|
||||||
|
case 'collections':
|
||||||
|
case 'conditions':
|
||||||
|
case 'creators':
|
||||||
|
case 'tags':
|
||||||
|
case 'relations':
|
||||||
|
return this["_" + fieldName + "Changed"](field1, field2);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return field1 !== field2;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
_creatorsChanged: function (data1, data2) {
|
_creatorsChanged: function (data1, data2) {
|
||||||
if (!data2 || data1.length != data2.length) return true;
|
if (!data2 || data1.length != data2.length) return true;
|
||||||
for (let i = 0; i < data1.length; i++) {
|
for (let i = 0; i < data1.length; i++) {
|
||||||
|
|
|
@ -534,7 +534,6 @@ Zotero.DataObjects.prototype._load = Zotero.Promise.coroutine(function* (library
|
||||||
return loaded;
|
return loaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
// getPrimaryDataSQL() should use "O" for the primary table alias
|
|
||||||
var sql = this.primaryDataSQL;
|
var sql = this.primaryDataSQL;
|
||||||
var params = [];
|
var params = [];
|
||||||
if (libraryID !== false) {
|
if (libraryID !== false) {
|
||||||
|
@ -551,7 +550,7 @@ Zotero.DataObjects.prototype._load = Zotero.Promise.coroutine(function* (library
|
||||||
params,
|
params,
|
||||||
{
|
{
|
||||||
onRow: function (row) {
|
onRow: function (row) {
|
||||||
var id = row.getResultByIndex(this._ZDO_id);
|
var id = row.getResultByName(this._ZDO_id);
|
||||||
var columns = Object.keys(this._primaryDataSQLParts);
|
var columns = Object.keys(this._primaryDataSQLParts);
|
||||||
var rowObj = {};
|
var rowObj = {};
|
||||||
for (let i=0; i<columns.length; i++) {
|
for (let i=0; i<columns.length; i++) {
|
||||||
|
|
|
@ -4008,32 +4008,11 @@ Zotero.Item.prototype.fromJSON = Zotero.Promise.coroutine(function* (json) {
|
||||||
/**
|
/**
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
*/
|
*/
|
||||||
Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options) {
|
Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options = {}) {
|
||||||
options = options || {};
|
var env = this._preToJSON(options);
|
||||||
|
var mode = env.mode;
|
||||||
|
|
||||||
if (options) {
|
var obj = env.obj = {};
|
||||||
var mode = options.mode;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var mode = 'new';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode == 'patch') {
|
|
||||||
if (!options.patchBase) {
|
|
||||||
throw new Error("Cannot use patch mode if patchBase not provided");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (options.patchBase) {
|
|
||||||
if (options.mode) {
|
|
||||||
Zotero.debug("Zotero.Item.toJSON: ignoring provided patchBase in " + mode + " mode", 2);
|
|
||||||
}
|
|
||||||
// If patchBase provided and no explicit mode, use 'patch'
|
|
||||||
else {
|
|
||||||
mode = 'patch';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var obj = {};
|
|
||||||
obj.key = this.key;
|
obj.key = this.key;
|
||||||
obj.version = this.version;
|
obj.version = this.version;
|
||||||
obj.itemType = Zotero.ItemTypes.getName(this.itemTypeID);
|
obj.itemType = Zotero.ItemTypes.getName(this.itemTypeID);
|
||||||
|
@ -4106,39 +4085,12 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options) {
|
||||||
obj.dateModified = Zotero.Date.sqlToISO8601(this.dateModified);
|
obj.dateModified = Zotero.Date.sqlToISO8601(this.dateModified);
|
||||||
if (obj.accessDate) obj.accessDate = Zotero.Date.sqlToISO8601(obj.accessDate);
|
if (obj.accessDate) obj.accessDate = Zotero.Date.sqlToISO8601(obj.accessDate);
|
||||||
|
|
||||||
if (mode == 'patch') {
|
return this._postToJSON(env);
|
||||||
for (let i in options.patchBase) {
|
|
||||||
switch (i) {
|
|
||||||
case 'key':
|
|
||||||
case 'version':
|
|
||||||
case 'dateModified':
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i in obj) {
|
|
||||||
if (obj[i] === options.patchBase[i]) {
|
|
||||||
delete obj[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
obj[i] = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
Zotero.Item.prototype.toResponseJSON = Zotero.Promise.coroutine(function* (options) {
|
Zotero.Item.prototype.toResponseJSON = Zotero.Promise.coroutine(function* (options = {}) {
|
||||||
var json = {
|
var json = yield this.constructor._super.prototype.toResponseJSON.apply(this, options);
|
||||||
key: this.key,
|
|
||||||
version: this.version,
|
|
||||||
meta: {},
|
|
||||||
data: yield this.toJSON(options)
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: library block?
|
|
||||||
|
|
||||||
// creatorSummary
|
// creatorSummary
|
||||||
var firstCreator = this.getField('firstCreator');
|
var firstCreator = this.getField('firstCreator');
|
||||||
|
|
|
@ -822,15 +822,24 @@ Zotero.Search.prototype.fromJSON = Zotero.Promise.coroutine(function* (json) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Zotero.Collection.prototype.toResponseJSON = Zotero.Promise.coroutine(function* (options = {}) {
|
||||||
|
var json = yield this.constructor._super.prototype.toResponseJSON.apply(this, options);
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
|
||||||
Zotero.Search.prototype.toJSON = Zotero.Promise.coroutine(function* (options) {
|
|
||||||
var obj = {};
|
Zotero.Search.prototype.toJSON = Zotero.Promise.coroutine(function* (options = {}) {
|
||||||
|
var env = this._preToJSON(options);
|
||||||
|
var mode = env.mode;
|
||||||
|
|
||||||
|
var obj = env.obj = {};
|
||||||
obj.key = this.key;
|
obj.key = this.key;
|
||||||
obj.version = this.version;
|
obj.version = this.version;
|
||||||
obj.name = this.name;
|
obj.name = this.name;
|
||||||
yield this.loadConditions();
|
yield this.loadConditions();
|
||||||
obj.conditions = this.getConditions();
|
obj.conditions = this.getConditions();
|
||||||
return obj;
|
|
||||||
|
return this._postToJSON(env);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -269,8 +269,6 @@ Zotero.Sync.APIClient.prototype = {
|
||||||
|
|
||||||
|
|
||||||
uploadObjects: Zotero.Promise.coroutine(function* (libraryType, libraryTypeID, objectType, method, version, objects) {
|
uploadObjects: Zotero.Promise.coroutine(function* (libraryType, libraryTypeID, objectType, method, version, objects) {
|
||||||
throw new Error("Uploading disabled");
|
|
||||||
|
|
||||||
if (method != 'POST' && method != 'PATCH') {
|
if (method != 'POST' && method != 'PATCH') {
|
||||||
throw new Error("Invalid method '" + method + "'");
|
throw new Error("Invalid method '" + method + "'");
|
||||||
}
|
}
|
||||||
|
|
|
@ -550,9 +550,8 @@ Zotero.Sync.Data.Engine.prototype._startUpload = Zotero.Promise.coroutine(functi
|
||||||
let objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType);
|
let objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType);
|
||||||
let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType);
|
let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType);
|
||||||
|
|
||||||
let ids = objectIDs[objectType];
|
|
||||||
let queue = [];
|
let queue = [];
|
||||||
for (let id of ids) {
|
for (let id of objectIDs[objectType]) {
|
||||||
queue.push({
|
queue.push({
|
||||||
id: id,
|
id: id,
|
||||||
json: null,
|
json: null,
|
||||||
|
@ -604,26 +603,53 @@ Zotero.Sync.Data.Engine.prototype._startUpload = Zotero.Promise.coroutine(functi
|
||||||
Zotero.debug(json);
|
Zotero.debug(json);
|
||||||
|
|
||||||
libraryVersion = json.libraryVersion;
|
libraryVersion = json.libraryVersion;
|
||||||
yield Zotero.Libraries.setVersion(this.libraryID, json.libraryVersion);
|
|
||||||
|
|
||||||
// Mark successful and unchanged objects as synced with new version
|
// Mark successful and unchanged objects as synced with new version,
|
||||||
var toRemove = [];
|
// and save uploaded JSON to cache
|
||||||
for (let state of ['success', 'unchanged']) {
|
let ids = [];
|
||||||
|
let toSave = [];
|
||||||
|
let toCache = [];
|
||||||
|
for (let state of ['successful', 'unchanged']) {
|
||||||
for (let index in json.results[state]) {
|
for (let index in json.results[state]) {
|
||||||
let key = json.results[state][index];
|
let current = json.results[state][index];
|
||||||
|
// 'successful' includes objects, not keys
|
||||||
|
let key = state == 'successful' ? current.key : current;
|
||||||
|
|
||||||
if (key != batch[index].key) {
|
if (key != batch[index].key) {
|
||||||
throw new Error("Key mismatch (" + key + " != " + batch[index].key + ")");
|
throw new Error("Key mismatch (" + key + " != " + batch[index].key + ")");
|
||||||
}
|
}
|
||||||
let obj = yield objectsClass.getByLibraryAndKeyAsync(
|
|
||||||
this.libraryID, key, { noCache: true }
|
let obj = objectsClass.getByLibraryAndKey(this.libraryID, key, { noCache: true })
|
||||||
);
|
ids.push(obj.id);
|
||||||
obj.version = json.libraryVersion;
|
|
||||||
yield Zotero.Sync.Data.Local.markObjectAsSynced(obj)
|
if (state == 'successful') {
|
||||||
|
// Update local object with saved data if necessary
|
||||||
|
yield obj.fromJSON(current.data);
|
||||||
|
toSave.push(obj);
|
||||||
|
toCache.push(current);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let j = yield obj.toJSON();
|
||||||
|
j.version = json.libraryVersion;
|
||||||
|
toCache.push(j);
|
||||||
|
}
|
||||||
|
|
||||||
numSuccessful++;
|
numSuccessful++;
|
||||||
// Remove from batch to mark as successful
|
// Remove from batch to mark as successful
|
||||||
delete batch[index];
|
delete batch[index];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
yield Zotero.Sync.Data.Local.saveCacheObjects(
|
||||||
|
objectType, this.libraryID, toCache
|
||||||
|
);
|
||||||
|
yield Zotero.DB.executeTransaction(function* () {
|
||||||
|
for (let i = 0; i < toSave.length; i++) {
|
||||||
|
yield toSave[i].save();
|
||||||
|
}
|
||||||
|
yield Zotero.Libraries.setVersion(this.libraryID, json.libraryVersion);
|
||||||
|
objectsClass.updateVersion(ids, json.libraryVersion);
|
||||||
|
objectsClass.updateSynced(ids, true);
|
||||||
|
}.bind(this));
|
||||||
|
|
||||||
// Handle failed objects
|
// Handle failed objects
|
||||||
for (let index in json.results.failed) {
|
for (let index in json.results.failed) {
|
||||||
|
|
|
@ -159,6 +159,27 @@ Zotero.Sync.Data.Local = {
|
||||||
throw new Error("'json' must be an array");
|
throw new Error("'json' must be an array");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!jsonArray.length) {
|
||||||
|
Zotero.debug("No " + Zotero.DataObjectUtilities.getObjectTypePlural(objectType)
|
||||||
|
+ " to save to sync cache");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonArray = jsonArray.map(o => {
|
||||||
|
if (o.key === undefined) {
|
||||||
|
throw new Error("Missing 'key' property in JSON");
|
||||||
|
}
|
||||||
|
if (o.version === undefined) {
|
||||||
|
throw new Error("Missing 'version' property in JSON");
|
||||||
|
}
|
||||||
|
// If direct data object passed, wrap in fake response object
|
||||||
|
return o.data === undefined ? {
|
||||||
|
key: o.key,
|
||||||
|
version: o.version,
|
||||||
|
data: o
|
||||||
|
} : o;
|
||||||
|
});
|
||||||
|
|
||||||
Zotero.debug("Saving to sync cache:");
|
Zotero.debug("Saving to sync cache:");
|
||||||
Zotero.debug(jsonArray);
|
Zotero.debug(jsonArray);
|
||||||
|
|
||||||
|
@ -174,12 +195,6 @@ Zotero.Sync.Data.Local = {
|
||||||
var params = [];
|
var params = [];
|
||||||
for (let i = 0; i < chunk.length; i++) {
|
for (let i = 0; i < chunk.length; i++) {
|
||||||
let o = chunk[i];
|
let o = chunk[i];
|
||||||
if (o.key === undefined) {
|
|
||||||
throw new Error("Missing 'key' property in JSON");
|
|
||||||
}
|
|
||||||
if (o.version === undefined) {
|
|
||||||
throw new Error("Missing 'version' property in JSON");
|
|
||||||
}
|
|
||||||
params.push(libraryID, o.key, syncObjectTypeID, o.version, JSON.stringify(o));
|
params.push(libraryID, o.key, syncObjectTypeID, o.version, JSON.stringify(o));
|
||||||
}
|
}
|
||||||
return Zotero.DB.queryAsync(
|
return Zotero.DB.queryAsync(
|
||||||
|
|
|
@ -260,12 +260,24 @@ var createGroup = Zotero.Promise.coroutine(function* (props) {
|
||||||
//
|
//
|
||||||
// Data objects
|
// Data objects
|
||||||
//
|
//
|
||||||
function createUnsavedDataObject(objectType, params) {
|
/**
|
||||||
|
* @param {String} objectType - 'collection', 'item', 'search'
|
||||||
|
* @param {Object} [params]
|
||||||
|
* @param {Integer} [params.libraryID]
|
||||||
|
* @param {String} [params.itemType] - Item type
|
||||||
|
* @param {String} [params.title] - Item title
|
||||||
|
* @param {Boolean} [params.setTitle] - Assign a random item title
|
||||||
|
* @param {String} [params.name] - Collection/search name
|
||||||
|
* @param {Integer} [params.parentID]
|
||||||
|
* @param {String} [params.parentKey]
|
||||||
|
* @param {Boolean} [params.synced]
|
||||||
|
* @param {Integer} [params.version]
|
||||||
|
*/
|
||||||
|
function createUnsavedDataObject(objectType, params = {}) {
|
||||||
if (!objectType) {
|
if (!objectType) {
|
||||||
throw new Error("Object type not provided");
|
throw new Error("Object type not provided");
|
||||||
}
|
}
|
||||||
|
|
||||||
params = params || {};
|
|
||||||
if (objectType == 'item') {
|
if (objectType == 'item') {
|
||||||
var param = params.itemType || 'book';
|
var param = params.itemType || 'book';
|
||||||
}
|
}
|
||||||
|
@ -275,14 +287,14 @@ function createUnsavedDataObject(objectType, params) {
|
||||||
}
|
}
|
||||||
switch (objectType) {
|
switch (objectType) {
|
||||||
case 'item':
|
case 'item':
|
||||||
if (params.title) {
|
if (params.title !== undefined || params.setTitle) {
|
||||||
obj.setField('title', params.title);
|
obj.setField('title', params.title !== undefined ? params.title : Zotero.Utilities.randomString());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'collection':
|
case 'collection':
|
||||||
case 'search':
|
case 'search':
|
||||||
obj.name = params.name !== undefined ? params.name : "Test";
|
obj.name = params.name !== undefined ? params.name : Zotero.Utilities.randomString();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
var allowedParams = ['parentID', 'parentKey', 'synced', 'version'];
|
var allowedParams = ['parentID', 'parentKey', 'synced', 'version'];
|
||||||
|
@ -294,12 +306,32 @@ function createUnsavedDataObject(objectType, params) {
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
var createDataObject = Zotero.Promise.coroutine(function* (objectType, params, saveOptions) {
|
var createDataObject = Zotero.Promise.coroutine(function* (objectType, params = {}, saveOptions) {
|
||||||
var obj = createUnsavedDataObject(objectType, params);
|
var obj = createUnsavedDataObject(objectType, params);
|
||||||
yield obj.saveTx(saveOptions);
|
yield obj.saveTx(saveOptions);
|
||||||
return obj;
|
return obj;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getNameProperty(objectType) {
|
||||||
|
return objectType == 'item' ? 'title' : 'name';
|
||||||
|
}
|
||||||
|
|
||||||
|
var modifyDataObject = Zotero.Promise.coroutine(function* (obj, params = {}) {
|
||||||
|
switch (obj.objectType) {
|
||||||
|
case 'item':
|
||||||
|
yield obj.loadItemData();
|
||||||
|
obj.setField(
|
||||||
|
'title',
|
||||||
|
params.title !== undefined ? params.title : Zotero.Utilities.randomString()
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
obj.name = params.name !== undefined ? params.name : Zotero.Utilities.randomString();
|
||||||
|
}
|
||||||
|
return obj.saveTx();
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a promise for the error thrown by a promise, or false if none
|
* Return a promise for the error thrown by a promise, or false if none
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -709,56 +709,96 @@ describe("Zotero.Item", function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("#toJSON()", function () {
|
describe("#toJSON()", function () {
|
||||||
it("should output only fields with values in default mode", function* () {
|
describe("default mode", function () {
|
||||||
var itemType = "book";
|
it("should output only fields with values", function* () {
|
||||||
var title = "Test";
|
var itemType = "book";
|
||||||
|
var title = "Test";
|
||||||
|
|
||||||
var item = new Zotero.Item(itemType);
|
var item = new Zotero.Item(itemType);
|
||||||
item.setField("title", title);
|
item.setField("title", title);
|
||||||
var id = yield item.saveTx();
|
var id = yield item.saveTx();
|
||||||
item = yield Zotero.Items.getAsync(id);
|
item = yield Zotero.Items.getAsync(id);
|
||||||
var json = yield item.toJSON();
|
var json = yield item.toJSON();
|
||||||
|
|
||||||
assert.equal(json.itemType, itemType);
|
assert.equal(json.itemType, itemType);
|
||||||
assert.equal(json.title, title);
|
assert.equal(json.title, title);
|
||||||
assert.isUndefined(json.date);
|
assert.isUndefined(json.date);
|
||||||
assert.isUndefined(json.numPages);
|
assert.isUndefined(json.numPages);
|
||||||
})
|
})
|
||||||
|
})
|
||||||
it("should output all fields in 'full' mode", function* () {
|
|
||||||
var itemType = "book";
|
describe("'full' mode", function () {
|
||||||
var title = "Test";
|
it("should output all fields", function* () {
|
||||||
|
var itemType = "book";
|
||||||
var item = new Zotero.Item(itemType);
|
var title = "Test";
|
||||||
item.setField("title", title);
|
|
||||||
var id = yield item.saveTx();
|
var item = new Zotero.Item(itemType);
|
||||||
item = yield Zotero.Items.getAsync(id);
|
item.setField("title", title);
|
||||||
var json = yield item.toJSON({ mode: 'full' });
|
var id = yield item.saveTx();
|
||||||
assert.equal(json.title, title);
|
item = yield Zotero.Items.getAsync(id);
|
||||||
assert.equal(json.date, "");
|
var json = yield item.toJSON({ mode: 'full' });
|
||||||
assert.equal(json.numPages, "");
|
assert.equal(json.title, title);
|
||||||
})
|
assert.equal(json.date, "");
|
||||||
|
assert.equal(json.numPages, "");
|
||||||
it("should output only fields that differ in 'patch' mode", function* () {
|
})
|
||||||
var itemType = "book";
|
})
|
||||||
var title = "Test";
|
|
||||||
var date = "2015-05-12";
|
describe("'patch' mode", function () {
|
||||||
|
it("should output only fields that differ", function* () {
|
||||||
var item = new Zotero.Item(itemType);
|
var itemType = "book";
|
||||||
item.setField("title", title);
|
var title = "Test";
|
||||||
var id = yield item.saveTx();
|
var date = "2015-05-12";
|
||||||
item = yield Zotero.Items.getAsync(id);
|
|
||||||
var patchBase = yield item.toJSON();
|
var item = new Zotero.Item(itemType);
|
||||||
|
item.setField("title", title);
|
||||||
item.setField("date", date);
|
var id = yield item.saveTx();
|
||||||
yield item.saveTx();
|
item = yield Zotero.Items.getAsync(id);
|
||||||
var json = yield item.toJSON({
|
var patchBase = yield item.toJSON();
|
||||||
patchBase: patchBase
|
|
||||||
|
item.setField("date", date);
|
||||||
|
yield item.saveTx();
|
||||||
|
var json = yield item.toJSON({
|
||||||
|
patchBase: patchBase
|
||||||
|
})
|
||||||
|
assert.isUndefined(json.itemType);
|
||||||
|
assert.isUndefined(json.title);
|
||||||
|
assert.equal(json.date, date);
|
||||||
|
assert.isUndefined(json.numPages);
|
||||||
|
assert.isUndefined(json.deleted);
|
||||||
|
assert.isUndefined(json.creators);
|
||||||
|
assert.isUndefined(json.relations);
|
||||||
|
assert.isUndefined(json.tags);
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should include changed 'deleted' field", function* () {
|
||||||
|
// True to false
|
||||||
|
var item = new Zotero.Item('book');
|
||||||
|
item.deleted = true;
|
||||||
|
var id = yield item.saveTx();
|
||||||
|
item = yield Zotero.Items.getAsync(id);
|
||||||
|
var patchBase = yield item.toJSON();
|
||||||
|
|
||||||
|
item.deleted = false;
|
||||||
|
var json = yield item.toJSON({
|
||||||
|
patchBase: patchBase
|
||||||
|
})
|
||||||
|
assert.isUndefined(json.title);
|
||||||
|
assert.isFalse(json.deleted);
|
||||||
|
|
||||||
|
// False to true
|
||||||
|
var item = new Zotero.Item('book');
|
||||||
|
item.deleted = false;
|
||||||
|
var id = yield item.saveTx();
|
||||||
|
item = yield Zotero.Items.getAsync(id);
|
||||||
|
var patchBase = yield item.toJSON();
|
||||||
|
|
||||||
|
item.deleted = true;
|
||||||
|
var json = yield item.toJSON({
|
||||||
|
patchBase: patchBase
|
||||||
|
})
|
||||||
|
assert.isUndefined(json.title);
|
||||||
|
assert.isTrue(json.deleted);
|
||||||
})
|
})
|
||||||
assert.isUndefined(json.itemType);
|
|
||||||
assert.isUndefined(json.title);
|
|
||||||
assert.equal(json.date, date);
|
|
||||||
assert.isUndefined(json.numPages);
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -122,39 +122,9 @@ describe("Zotero.Sync.Data.Engine", function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("Syncing", function () {
|
describe("Syncing", function () {
|
||||||
it("should perform a sync for a new library", function* () {
|
it("should download items into a new library", function* () {
|
||||||
({ engine, client, caller } = yield setup());
|
({ engine, client, caller } = yield setup());
|
||||||
|
|
||||||
server.respond(function (req) {
|
|
||||||
if (req.method == "POST" && req.url == baseURL + "users/1/items") {
|
|
||||||
let ifUnmodifiedSince = req.requestHeaders["If-Unmodified-Since-Version"];
|
|
||||||
if (ifUnmodifiedSince == 0) {
|
|
||||||
req.respond(412, {}, "Library has been modified since specified version");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ifUnmodifiedSince == 3) {
|
|
||||||
let json = JSON.parse(req.requestBody);
|
|
||||||
req.respond(
|
|
||||||
200,
|
|
||||||
{
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Last-Modified-Version": 3
|
|
||||||
},
|
|
||||||
JSON.stringify({
|
|
||||||
success: {
|
|
||||||
"0": json[0].key,
|
|
||||||
"1": json[1].key
|
|
||||||
},
|
|
||||||
unchanged: {},
|
|
||||||
failed: {}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
var headers = {
|
var headers = {
|
||||||
"Last-Modified-Version": 3
|
"Last-Modified-Version": 3
|
||||||
};
|
};
|
||||||
|
@ -280,6 +250,251 @@ describe("Zotero.Sync.Data.Engine", function () {
|
||||||
assert.isTrue(obj.synced);
|
assert.isTrue(obj.synced);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should upload new full items and subsequent patches", function* () {
|
||||||
|
({ engine, client, caller } = yield setup());
|
||||||
|
|
||||||
|
var libraryID = Zotero.Libraries.userLibraryID;
|
||||||
|
var lastLibraryVersion = 5;
|
||||||
|
yield Zotero.Libraries.setVersion(libraryID, lastLibraryVersion);
|
||||||
|
|
||||||
|
var types = Zotero.DataObjectUtilities.getTypes();
|
||||||
|
var objects = {};
|
||||||
|
var objectResponseJSON = {};
|
||||||
|
var objectVersions = {};
|
||||||
|
for (let type of types) {
|
||||||
|
objects[type] = [yield createDataObject(type, { setTitle: true })];
|
||||||
|
objectVersions[type] = {};
|
||||||
|
objectResponseJSON[type] = yield Zotero.Promise.all(objects[type].map(o => o.toResponseJSON()));
|
||||||
|
}
|
||||||
|
|
||||||
|
server.respond(function (req) {
|
||||||
|
if (req.method == "POST") {
|
||||||
|
assert.equal(
|
||||||
|
req.requestHeaders["If-Unmodified-Since-Version"], lastLibraryVersion
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let type of types) {
|
||||||
|
let typePlural = Zotero.DataObjectUtilities.getObjectTypePlural(type);
|
||||||
|
if (req.url == baseURL + "users/1/" + typePlural) {
|
||||||
|
let json = JSON.parse(req.requestBody);
|
||||||
|
assert.lengthOf(json, 1);
|
||||||
|
assert.equal(json[0].key, objects[type][0].key);
|
||||||
|
assert.equal(json[0].version, 0);
|
||||||
|
if (type == 'item') {
|
||||||
|
assert.equal(json[0].title, objects[type][0].getField('title'));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
assert.equal(json[0].name, objects[type][0].name);
|
||||||
|
}
|
||||||
|
let objectJSON = objectResponseJSON[type][0];
|
||||||
|
objectJSON.version = ++lastLibraryVersion;
|
||||||
|
objectJSON.data.version = lastLibraryVersion;
|
||||||
|
req.respond(
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Last-Modified-Version": lastLibraryVersion
|
||||||
|
},
|
||||||
|
JSON.stringify({
|
||||||
|
successful: {
|
||||||
|
"0": objectJSON
|
||||||
|
},
|
||||||
|
unchanged: {},
|
||||||
|
failed: {}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
objectVersions[type][objects[type][0].key] = lastLibraryVersion;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
yield engine.start();
|
||||||
|
|
||||||
|
assert.equal(Zotero.Libraries.getVersion(libraryID), lastLibraryVersion);
|
||||||
|
for (let type of types) {
|
||||||
|
// Make sure objects were set to the correct version and marked as synced
|
||||||
|
assert.lengthOf((yield Zotero.Sync.Data.Local.getUnsynced(libraryID, type)), 0);
|
||||||
|
let key = objects[type][0].key;
|
||||||
|
let version = objects[type][0].version;
|
||||||
|
assert.equal(version, objectVersions[type][key]);
|
||||||
|
// Make sure uploaded objects were added to cache
|
||||||
|
let cached = yield Zotero.Sync.Data.Local.getCacheObject(type, libraryID, key, version);
|
||||||
|
assert.typeOf(cached, 'object');
|
||||||
|
assert.equal(cached.key, key);
|
||||||
|
assert.equal(cached.version, version);
|
||||||
|
|
||||||
|
yield modifyDataObject(objects[type][0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
({ engine, client, caller } = yield setup());
|
||||||
|
|
||||||
|
server.respond(function (req) {
|
||||||
|
if (req.method == "POST") {
|
||||||
|
assert.equal(
|
||||||
|
req.requestHeaders["If-Unmodified-Since-Version"], lastLibraryVersion
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let type of types) {
|
||||||
|
let typePlural = Zotero.DataObjectUtilities.getObjectTypePlural(type);
|
||||||
|
if (req.url == baseURL + "users/1/" + typePlural) {
|
||||||
|
let json = JSON.parse(req.requestBody);
|
||||||
|
assert.lengthOf(json, 1);
|
||||||
|
let j = json[0];
|
||||||
|
let o = objects[type][0];
|
||||||
|
assert.equal(j.key, o.key);
|
||||||
|
assert.equal(j.version, objectVersions[type][o.key]);
|
||||||
|
if (type == 'item') {
|
||||||
|
assert.equal(j.title, o.getField('title'));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
assert.equal(j.name, o.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify PATCH semantics instead of POST (i.e., only changed fields)
|
||||||
|
let changedFieldsExpected = ['key', 'version'];
|
||||||
|
if (type == 'item') {
|
||||||
|
changedFieldsExpected.push('title', 'dateModified');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
changedFieldsExpected.push('name');
|
||||||
|
}
|
||||||
|
let changedFields = Object.keys(j);
|
||||||
|
assert.lengthOf(
|
||||||
|
changedFields, changedFieldsExpected.length, "same " + type + " length"
|
||||||
|
);
|
||||||
|
assert.sameMembers(
|
||||||
|
changedFields, changedFieldsExpected, "same " + type + " members"
|
||||||
|
);
|
||||||
|
let objectJSON = objectResponseJSON[type][0];
|
||||||
|
objectJSON.version = ++lastLibraryVersion;
|
||||||
|
objectJSON.data.version = lastLibraryVersion;
|
||||||
|
req.respond(
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Last-Modified-Version": lastLibraryVersion
|
||||||
|
},
|
||||||
|
JSON.stringify({
|
||||||
|
successful: {
|
||||||
|
"0": objectJSON
|
||||||
|
},
|
||||||
|
unchanged: {},
|
||||||
|
failed: {}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
objectVersions[type][o.key] = lastLibraryVersion;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
yield engine.start();
|
||||||
|
|
||||||
|
assert.equal(Zotero.Libraries.getVersion(libraryID), lastLibraryVersion);
|
||||||
|
for (let type of types) {
|
||||||
|
// Make sure objects were set to the correct version and marked as synced
|
||||||
|
assert.lengthOf((yield Zotero.Sync.Data.Local.getUnsynced(libraryID, type)), 0);
|
||||||
|
let o = objects[type][0];
|
||||||
|
let key = o.key;
|
||||||
|
let version = o.version;
|
||||||
|
assert.equal(version, objectVersions[type][key]);
|
||||||
|
// Make sure uploaded objects were added to cache
|
||||||
|
let cached = yield Zotero.Sync.Data.Local.getCacheObject(type, libraryID, key, version);
|
||||||
|
assert.typeOf(cached, 'object');
|
||||||
|
assert.equal(cached.key, key);
|
||||||
|
assert.equal(cached.version, version);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'collection':
|
||||||
|
assert.isFalse(cached.data.parentCollection);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'item':
|
||||||
|
assert.equal(cached.data.dateAdded, Zotero.Date.sqlToISO8601(o.dateAdded));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'search':
|
||||||
|
assert.typeOf(cached.data.conditions, 'object');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should update local objects with remotely saved version after uploading if necessary", function* () {
|
||||||
|
({ engine, client, caller } = yield setup());
|
||||||
|
|
||||||
|
var libraryID = Zotero.Libraries.userLibraryID;
|
||||||
|
var lastLibraryVersion = 5;
|
||||||
|
yield Zotero.Libraries.setVersion(libraryID, lastLibraryVersion);
|
||||||
|
|
||||||
|
var types = Zotero.DataObjectUtilities.getTypes();
|
||||||
|
var objects = {};
|
||||||
|
var objectResponseJSON = {};
|
||||||
|
var objectNames = {};
|
||||||
|
for (let type of types) {
|
||||||
|
objects[type] = [yield createDataObject(type, { setTitle: true })];
|
||||||
|
objectNames[type] = {};
|
||||||
|
objectResponseJSON[type] = yield Zotero.Promise.all(objects[type].map(o => o.toResponseJSON()));
|
||||||
|
}
|
||||||
|
|
||||||
|
server.respond(function (req) {
|
||||||
|
if (req.method == "POST") {
|
||||||
|
assert.equal(
|
||||||
|
req.requestHeaders["If-Unmodified-Since-Version"], lastLibraryVersion
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let type of types) {
|
||||||
|
let typePlural = Zotero.DataObjectUtilities.getObjectTypePlural(type);
|
||||||
|
if (req.url == baseURL + "users/1/" + typePlural) {
|
||||||
|
let key = objects[type][0].key;
|
||||||
|
let objectJSON = objectResponseJSON[type][0];
|
||||||
|
objectJSON.version = ++lastLibraryVersion;
|
||||||
|
objectJSON.data.version = lastLibraryVersion;
|
||||||
|
let prop = type == 'item' ? 'title' : 'name';
|
||||||
|
objectNames[type][key] = objectJSON.data[prop] = Zotero.Utilities.randomString();
|
||||||
|
req.respond(
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Last-Modified-Version": lastLibraryVersion
|
||||||
|
},
|
||||||
|
JSON.stringify({
|
||||||
|
successful: {
|
||||||
|
"0": objectJSON
|
||||||
|
},
|
||||||
|
unchanged: {},
|
||||||
|
failed: {}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
yield engine.start();
|
||||||
|
|
||||||
|
assert.equal(Zotero.Libraries.getVersion(libraryID), lastLibraryVersion);
|
||||||
|
for (let type of types) {
|
||||||
|
// Make sure local objects were updated with new metadata and marked as synced
|
||||||
|
assert.lengthOf((yield Zotero.Sync.Data.Local.getUnsynced(libraryID, type)), 0);
|
||||||
|
let o = objects[type][0];
|
||||||
|
let key = o.key;
|
||||||
|
let version = o.version;
|
||||||
|
let name = objectNames[type][key];
|
||||||
|
if (type == 'item') {
|
||||||
|
yield o.loadItemData();
|
||||||
|
assert.equal(name, o.getField('title'));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
assert.equal(name, o.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
it("should make only one request if in sync", function* () {
|
it("should make only one request if in sync", function* () {
|
||||||
yield Zotero.Libraries.setVersion(Zotero.Libraries.userLibraryID, 5);
|
yield Zotero.Libraries.setVersion(Zotero.Libraries.userLibraryID, 5);
|
||||||
({ engine, client, caller } = yield setup());
|
({ engine, client, caller } = yield setup());
|
||||||
|
|
Loading…
Reference in New Issue
Block a user