mirror of
https://github.com/zotero/zotero.git
synced 2026-10-03 02:21:49 +00:00
Track data object versions locally
This commit is contained in:
parent
431e1dabde
commit
d270fd8a9f
17 changed files with 176 additions and 12 deletions
|
|
@ -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); }
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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]; },
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ Zotero.Items = function () {
|
|||
libraryID: "O.libraryID",
|
||||
key: "O.key",
|
||||
version: "O.version",
|
||||
clientVersion: "O.clientVersion",
|
||||
synced: "O.synced",
|
||||
|
||||
createdByUserID: "createdByUserID",
|
||||
|
|
|
|||
|
|
@ -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<accessors.length; i++) {
|
||||
let prop = Zotero.Library._colToProp(accessors[i]);
|
||||
Zotero.defineProperty(Zotero.Library.prototype, accessors[i], {
|
||||
|
|
@ -293,6 +295,20 @@ Zotero.Library.prototype._set = function (prop, val) {
|
|||
|
||||
break;
|
||||
|
||||
case '_libraryClientVersion':
|
||||
var newVal = Number.parseInt(val, 10);
|
||||
if (newVal != val) {
|
||||
throw new Error(`${prop} must be an integer (${typeof val} '${val}' given)`);
|
||||
}
|
||||
val = newVal;
|
||||
|
||||
if (val < 0) throw new Error(prop + ' must not be less than 0');
|
||||
|
||||
// Ensure that it is never decreasing
|
||||
if (val < this._libraryClientVersion) throw new Error(prop + ' cannot decrease');
|
||||
|
||||
break;
|
||||
|
||||
case '_libraryStorageVersion':
|
||||
var newVal = parseInt(val);
|
||||
if (newVal != val) {
|
||||
|
|
@ -350,6 +366,7 @@ Zotero.Library.prototype._loadDataFromRow = function (row) {
|
|||
this._libraryEditable = !!row._libraryEditable;
|
||||
this._libraryFilesEditable = !!row._libraryFilesEditable;
|
||||
this._libraryVersion = row._libraryVersion;
|
||||
this._libraryClientVersion = row._libraryClientVersion;
|
||||
this._libraryStorageVersion = row._libraryStorageVersion;
|
||||
this._libraryLastSync = row._libraryLastSync !== 0 ? new Date(row._libraryLastSync * 1000) : false;
|
||||
this._libraryArchived = !!row._libraryArchived;
|
||||
|
|
@ -751,3 +768,18 @@ Zotero.Library.prototype.hasItem = function (item) {
|
|||
}
|
||||
return item.libraryID == this.libraryID;
|
||||
}
|
||||
|
||||
Zotero.Library.prototype.incrementClientVersion = async function () {
|
||||
let transactionID = Zotero.DB.requireTransaction();
|
||||
if (transactionID === this._lastClientVersionIncrementTransactionID) {
|
||||
return this._libraryClientVersion;
|
||||
}
|
||||
|
||||
let clientVersion = await Zotero.DB.valueQueryAsync(
|
||||
"UPDATE libraries SET clientVersion = clientVersion + 1 WHERE libraryID=? RETURNING clientVersion",
|
||||
[this.libraryID]
|
||||
);
|
||||
this._libraryClientVersion = clientVersion;
|
||||
this._lastClientVersionIncrementTransactionID = transactionID;
|
||||
return clientVersion;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -86,6 +86,10 @@ Zotero.defineProperty(Zotero.Search.prototype, 'version', {
|
|||
get: function () { return this._get('version'); },
|
||||
set: function (val) { return this._set('version', val); }
|
||||
});
|
||||
Zotero.defineProperty(Zotero.Search.prototype, 'clientVersion', {
|
||||
get: function() { return this._get('clientVersion'); },
|
||||
set: function(val) { return this._set('clientVersion', val); }
|
||||
});
|
||||
Zotero.defineProperty(Zotero.Search.prototype, 'synced', {
|
||||
get: function () { return this._get('synced'); },
|
||||
set: function (val) { return this._set('synced', val); }
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ Zotero.Searches = function () {
|
|||
libraryID: "O.libraryID",
|
||||
key: "O.key",
|
||||
version: "O.version",
|
||||
clientVersion: "O.clientVersion",
|
||||
synced: "O.synced",
|
||||
deleted: "DS.savedSearchID IS NOT NULL AS deleted",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -588,6 +588,7 @@ Zotero.DBConnection.prototype.requireTransaction = function () {
|
|||
if (!this._transactionID) {
|
||||
throw new Error("Not in transaction");
|
||||
}
|
||||
return this._transactionID;
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3531,6 +3531,14 @@ Zotero.Schema = new function () {
|
|||
await Zotero.DB.queryAsync("UPDATE groups SET version = 0");
|
||||
}
|
||||
|
||||
else if (i == 126) {
|
||||
let clientVersionTables = ['items', 'collections', 'savedSearches', 'libraries', 'groups'];
|
||||
for (let table of clientVersionTables) {
|
||||
await Zotero.DB.queryAsync(`ALTER TABLE ${table} ADD COLUMN clientVersion INT NOT NULL DEFAULT 0`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If breaking compatibility or doing anything dangerous, clear minorUpdateFrom
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ class LocalAPIEndpoint {
|
|||
return this.makeResponse(400, 'text/plain', `Invalid 'since' value '${requestData.searchParams.get('since')}'`);
|
||||
}
|
||||
if (since !== 0) {
|
||||
response.data = response.data.filter(dataObject => 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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() });
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue