diff --git a/chrome/content/zotero/xpcom/data/collection.js b/chrome/content/zotero/xpcom/data/collection.js index 2c7b4f9f2b..924aee83a7 100644 --- a/chrome/content/zotero/xpcom/data/collection.js +++ b/chrome/content/zotero/xpcom/data/collection.js @@ -67,6 +67,10 @@ Zotero.defineProperty(Zotero.Collection.prototype, 'version', { get: function () { return this._get('version'); }, set: function (val) { return this._set('version', val); } }); +Zotero.defineProperty(Zotero.Collection.prototype, 'clientVersion', { + get: function() { return this._get('clientVersion'); }, + set: function(val) { return this._set('clientVersion', val); } +}); Zotero.defineProperty(Zotero.Collection.prototype, 'synced', { get: function () { return this._get('synced'); }, set: function (val) { return this._set('synced', val); } diff --git a/chrome/content/zotero/xpcom/data/collections.js b/chrome/content/zotero/xpcom/data/collections.js index e38e29527d..0a4cecd197 100644 --- a/chrome/content/zotero/xpcom/data/collections.js +++ b/chrome/content/zotero/xpcom/data/collections.js @@ -38,6 +38,7 @@ Zotero.Collections = function () { libraryID: "O.libraryID", key: "O.key", version: "O.version", + clientVersion: "O.clientVersion", synced: "O.synced", deleted: "DC.collectionID IS NOT NULL AS deleted", diff --git a/chrome/content/zotero/xpcom/data/dataObject.js b/chrome/content/zotero/xpcom/data/dataObject.js index b59ba142cb..8844232725 100644 --- a/chrome/content/zotero/xpcom/data/dataObject.js +++ b/chrome/content/zotero/xpcom/data/dataObject.js @@ -23,6 +23,13 @@ ***** END LICENSE BLOCK ***** */ +let lazy = {}; +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + 'shouldTrackClientVersions', + 'extensions.zotero.httpServer.localAPI.enabled', +); + /** * @property {String} (readOnly) objectType * @property {String} (readOnly) libraryKey @@ -50,6 +57,7 @@ Zotero.DataObject = function () { this._dateAdded = null; this._dateModified = null; this._version = null; + this._clientVersion = null; this._synced = null; this._identified = false; this._parentID = null; @@ -1179,6 +1187,15 @@ Zotero.DataObject.prototype._finalizeSave = async function (env) { else if (env.skipCache) { Zotero.logError("skipCache is only for new objects"); } + + if (lazy.shouldTrackClientVersions) { + let libraryClientVersion = await this.library.incrementClientVersion(); + await Zotero.DB.queryAsync( + `UPDATE ${this.ObjectsClass.table} SET clientVersion = ? WHERE ${this.ObjectsClass.idColumn}=?`, + [libraryClientVersion, this.id] + ); + this._clientVersion = libraryClientVersion; + } }; @@ -1368,7 +1385,7 @@ Zotero.DataObject.prototype.toResponseJSON = function (options = {}) { let uri = Zotero.URI.getObjectURI(this); var json = { key: this.key, - version: this.version, + version: this.clientVersion, library: this.library.toResponseJSON({ ...options, includeGroupDetails: false }), links: { self: { diff --git a/chrome/content/zotero/xpcom/data/group.js b/chrome/content/zotero/xpcom/data/group.js index c5d824030a..8ae4b6987d 100644 --- a/chrome/content/zotero/xpcom/data/group.js +++ b/chrome/content/zotero/xpcom/data/group.js @@ -244,7 +244,7 @@ Zotero.Group.prototype.toResponseJSON = function (options = {}) { let uri = Zotero.URI.getGroupURI(this); return { id: this.id, - version: this.version, + version: this.clientVersion, links: { self: { href: Zotero.URI.toAPIURL(uri, options.apiURL), diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js index 99669a0931..f8828ef479 100644 --- a/chrome/content/zotero/xpcom/data/item.js +++ b/chrome/content/zotero/xpcom/data/item.js @@ -128,8 +128,8 @@ Zotero.defineProperty(Zotero.Item.prototype, 'itemID', { enumerable: false }); -for (let name of ['libraryID', 'key', 'dateAdded', 'dateModified', 'version', 'synced', - 'createdByUserID', 'lastModifiedByUserID']) { +for (let name of ['libraryID', 'key', 'dateAdded', 'dateModified', 'version', 'clientVersion', + 'synced', 'createdByUserID', 'lastModifiedByUserID']) { let prop = '_' + name; Zotero.defineProperty(Zotero.Item.prototype, name, { get: function () { return this[prop]; }, diff --git a/chrome/content/zotero/xpcom/data/items.js b/chrome/content/zotero/xpcom/data/items.js index 0f869f7bbc..fac5f86940 100644 --- a/chrome/content/zotero/xpcom/data/items.js +++ b/chrome/content/zotero/xpcom/data/items.js @@ -48,6 +48,7 @@ Zotero.Items = function () { libraryID: "O.libraryID", key: "O.key", version: "O.version", + clientVersion: "O.clientVersion", synced: "O.synced", createdByUserID: "createdByUserID", diff --git a/chrome/content/zotero/xpcom/data/library.js b/chrome/content/zotero/xpcom/data/library.js index 70f9ae0e7f..daddbb3885 100644 --- a/chrome/content/zotero/xpcom/data/library.js +++ b/chrome/content/zotero/xpcom/data/library.js @@ -39,6 +39,7 @@ Zotero.Library = function (params = {}) { this._storageDownloadNeeded = false; this._lastReadItemInSession = null; + this._lastClientVersionIncrementTransactionID = null; Zotero.Utilities.Internal.assignProps( this, @@ -48,6 +49,7 @@ Zotero.Library = function (params = {}) { 'editable', 'filesEditable', 'libraryVersion', + 'clientVersion', 'storageVersion', 'lastSync', 'archived' @@ -71,7 +73,7 @@ Zotero.Library = function (params = {}) { // DB columns Zotero.defineProperty(Zotero.Library, '_dbColumns', { value: Object.freeze([ - 'type', 'editable', 'filesEditable', 'version', 'storageVersion', 'lastSync', 'archived', 'isAdmin' + 'type', 'editable', 'filesEditable', 'version', 'clientVersion', 'storageVersion', 'lastSync', 'archived', 'isAdmin' ]) }); @@ -209,7 +211,7 @@ Zotero.defineProperty(Zotero.Library.prototype, 'allowsLinkedFiles', { // Create other accessors (function () { - let accessors = ['editable', 'filesEditable', 'storageVersion', 'archived', 'isAdmin']; + let accessors = ['editable', 'filesEditable', 'clientVersion', 'storageVersion', 'archived', 'isAdmin']; for (let i=0; i dataObject.version > since); + response.data = response.data.filter(dataObject => dataObject.clientVersion > since); } } @@ -226,8 +226,8 @@ class LocalAPIEndpoint { 'Link': Object.entries(links).map(([rel, url]) => `<${url}>; rel="${rel}"`).join(', ') }; let lastModifiedVersion = dataIsArray - ? Zotero.Libraries.get(requestData.libraryID).libraryVersion - : response.data.version; + ? Zotero.Libraries.get(requestData.libraryID).clientVersion + : response.data.clientVersion; if (lastModifiedVersion !== undefined) { headers['Last-Modified-Version'] = lastModifiedVersion; } diff --git a/resource/schema/userdata.sql b/resource/schema/userdata.sql index 78ff6d3566..af3ddd064e 100644 --- a/resource/schema/userdata.sql +++ b/resource/schema/userdata.sql @@ -1,4 +1,4 @@ --- 125 +-- 126 -- Copyright (c) 2009 Center for History and New Media -- George Mason University, Fairfax, Virginia, USA @@ -164,6 +164,7 @@ CREATE TABLE items ( libraryID INT NOT NULL, key TEXT NOT NULL, version INT NOT NULL DEFAULT 0, + clientVersion INT NOT NULL DEFAULT 0, synced INT NOT NULL DEFAULT 0, UNIQUE (libraryID, key), FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE @@ -295,6 +296,7 @@ CREATE TABLE collections ( libraryID INT NOT NULL, key TEXT NOT NULL, version INT NOT NULL DEFAULT 0, + clientVersion INT NOT NULL DEFAULT 0, synced INT NOT NULL DEFAULT 0, UNIQUE (libraryID, key), FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE, @@ -351,6 +353,7 @@ CREATE TABLE savedSearches ( libraryID INT NOT NULL, key TEXT NOT NULL, version INT NOT NULL DEFAULT 0, + clientVersion INT NOT NULL DEFAULT 0, synced INT NOT NULL DEFAULT 0, UNIQUE (libraryID, key), FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE @@ -395,6 +398,7 @@ CREATE TABLE libraries ( editable INT NOT NULL, filesEditable INT NOT NULL, version INT NOT NULL DEFAULT 0, + clientVersion INT NOT NULL DEFAULT 0, storageVersion INT NOT NULL DEFAULT 0, lastSync INT NOT NULL DEFAULT 0, archived INT NOT NULL DEFAULT 0, @@ -412,6 +416,7 @@ CREATE TABLE groups ( name TEXT NOT NULL, description TEXT NOT NULL, version INT NOT NULL, + clientVersion INT NOT NULL DEFAULT 0, FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE ); diff --git a/test/content/support.js b/test/content/support.js index 8b51d7df1c..971f588c72 100644 --- a/test/content/support.js +++ b/test/content/support.js @@ -650,7 +650,7 @@ var modifyDataObject = function (obj, params = {}, saveOptions) { default: obj.name = params.name !== undefined ? params.name : Zotero.Utilities.randomString(); } - return obj.saveTx(saveOptions); + return obj.save({ ...saveOptions, tx: !Zotero.DB.inTransaction() }); }; /** diff --git a/test/tests/dataObjectTest.js b/test/tests/dataObjectTest.js index 9aa9f21b1a..81bc33e988 100644 --- a/test/tests/dataObjectTest.js +++ b/test/tests/dataObjectTest.js @@ -52,6 +52,61 @@ describe("Zotero.DataObject", function () { }) }) + describe("#clientVersion", function () { + it("should be set to library clientVersion after creating object", async function () { + for (let type of types) { + let obj = await createDataObject(type); + assert.equal(obj.clientVersion, obj.library.clientVersion); + await obj.eraseTx(); + } + }); + + it("should increase after modifying object", async function () { + for (let type of types) { + let obj = await createDataObject(type); + let clientVersion = obj.clientVersion; + + await modifyDataObject(obj); + + assert.isAbove(obj.clientVersion, clientVersion); + assert.equal(obj.clientVersion, obj.library.clientVersion); + + await obj.eraseTx(); + } + }); + + it("should increase once per library per transaction", async function () { + for (let type of types) { + let obj1 = await createDataObject(type); + let obj2 = await createDataObject(type); + + let group = await getGroup(); + let obj3 = await createDataObject(type, { libraryID: group.libraryID }); + + assert.isBelow(obj1.clientVersion, obj2.clientVersion); + assert.equal(obj2.clientVersion, Zotero.Libraries.userLibrary.clientVersion); + + await modifyDataObject(obj1); + assert.isAbove(obj1.clientVersion, obj2.clientVersion); + assert.equal(obj1.clientVersion, Zotero.Libraries.userLibrary.clientVersion); + + let libraryVersionBefore = Zotero.Libraries.userLibrary.clientVersion; + let groupVersionBefore = group.clientVersion; + await Zotero.DB.executeTransaction(async () => { + await modifyDataObject(obj1); + await modifyDataObject(obj2); + await modifyDataObject(obj3); + }); + assert.equal(obj1.clientVersion, Zotero.Libraries.userLibrary.clientVersion); + assert.equal(obj1.clientVersion, obj2.clientVersion); + assert.notEqual(obj1.clientVersion, libraryVersionBefore); + + assert.equal(obj3.clientVersion, group.clientVersion); + assert.notEqual(obj3.clientVersion, groupVersionBefore); + } + }); + }); + describe("#synced", function () { it("should be set to false after creating object", async function () { for (let type of types) { diff --git a/test/tests/libraryTest.js b/test/tests/libraryTest.js index 872ed00f92..5edf54c7f8 100644 --- a/test/tests/libraryTest.js +++ b/test/tests/libraryTest.js @@ -63,6 +63,27 @@ describe("Zotero.Library", function () { }); }); + describe("#clientVersion", function () { + it("should be settable to increasing values", function () { + let library = new Zotero.Library(); + assert.throws(() => library.clientVersion = -2); + assert.throws(() => library.clientVersion = "a"); + assert.throws(() => library.clientVersion = 1.1); + assert.doesNotThrow(() => library.clientVersion = 0); + assert.doesNotThrow(() => library.clientVersion = 5); + }); + it("should not be possible to decrement", function () { + let library = new Zotero.Library(); + library.clientVersion = 5; + assert.throws(() => library.clientVersion = 0); + }); + it("should not be possible to set to -1", function () { + let library = new Zotero.Library(); + library.clientVersion = 5; + assert.throws(() => library.clientVersion = -1); + }); + }); + describe("#editable", function () { it("should return editable status", function () { let library = Zotero.Libraries.get(Zotero.Libraries.userLibraryID); diff --git a/test/tests/server_localAPITest.js b/test/tests/server_localAPITest.js index 39a16e67ce..76b385f4b7 100644 --- a/test/tests/server_localAPITest.js +++ b/test/tests/server_localAPITest.js @@ -325,11 +325,25 @@ describe("Local API Server", function () { describe("?since", function () { it("should filter the results", async function () { - let { response: response1 } = await apiGet('/users/0/items?since=' + (Zotero.Libraries.userLibrary.libraryVersion + 1)); + let version = Zotero.Libraries.userLibrary.clientVersion; + + let { response: response1 } = await apiGet('/users/0/items?since=' + version); assert.isEmpty(response1); let { response: response2 } = await apiGet('/users/0/items?since=0'); assert.lengthOf(response2, allItems.length); + + let tempItem = await createDataObject('item'); + let { response: response3 } = await apiGet('/users/0/items?since=' + version); + assert.lengthOf(response3, 1); + assert.equal(response3[0].key, tempItem.key); + assert.equal(response3[0].version, tempItem.clientVersion); + assert.equal(tempItem.clientVersion, version + 1); + + await tempItem.eraseTx(); + + let { response: response4 } = await apiGet('/users/0/items?since=' + version); + assert.lengthOf(response4, 0); }); });