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:
Dan Stillman 2015-07-22 05:21:32 -04:00
parent 70d9b9870c
commit 4600318ad7
12 changed files with 578 additions and 187 deletions

View File

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

View File

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

View File

@ -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,25 +165,10 @@ Zotero.DataObjectUtilities = {
continue; continue;
} }
let changed; let changed = this._fieldChanged(field, val1, val2);
switch (field) {
case 'creators':
case 'collections':
case 'tags':
case 'relations':
changed = this["_" + field + "Changed"](val1, val2);
if (changed) { if (changed) {
return true; 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++) {

View File

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

View File

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

View File

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

View File

@ -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 + "'");
} }

View File

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

View File

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

View File

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

View File

@ -709,7 +709,8 @@ 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 () {
it("should output only fields with values", function* () {
var itemType = "book"; var itemType = "book";
var title = "Test"; var title = "Test";
@ -724,8 +725,10 @@ describe("Zotero.Item", function () {
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* () { describe("'full' mode", function () {
it("should output all fields", function* () {
var itemType = "book"; var itemType = "book";
var title = "Test"; var title = "Test";
@ -738,8 +741,10 @@ describe("Zotero.Item", function () {
assert.equal(json.date, ""); assert.equal(json.date, "");
assert.equal(json.numPages, ""); assert.equal(json.numPages, "");
}) })
})
it("should output only fields that differ in 'patch' mode", function* () { describe("'patch' mode", function () {
it("should output only fields that differ", function* () {
var itemType = "book"; var itemType = "book";
var title = "Test"; var title = "Test";
var date = "2015-05-12"; var date = "2015-05-12";
@ -759,6 +764,41 @@ describe("Zotero.Item", function () {
assert.isUndefined(json.title); assert.isUndefined(json.title);
assert.equal(json.date, date); assert.equal(json.date, date);
assert.isUndefined(json.numPages); 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);
})
}) })
}) })

View File

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