zotero/chrome/content/zotero/xpcom/data/dataObjects.js

1034 lines
28 KiB
JavaScript

/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2009 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
Zotero.DataObjects = function () {
if (!this._ZDO_object) throw new Error('this._ZDO_object must be set before calling Zotero.DataObjects constructor');
if (!this._ZDO_objects) {
this._ZDO_objects = Zotero.DataObjectUtilities.getObjectTypePlural(this._ZDO_object);
}
if (!this._ZDO_Object) {
this._ZDO_Object = this._ZDO_object.substr(0, 1).toUpperCase()
+ this._ZDO_object.substr(1);
}
if (!this._ZDO_Objects) {
this._ZDO_Objects = this._ZDO_objects.substr(0, 1).toUpperCase()
+ this._ZDO_objects.substr(1);
}
if (!this._ZDO_id) {
this._ZDO_id = this._ZDO_object + 'ID';
}
if (!this._ZDO_table) {
this._ZDO_table = this._ZDO_objects;
}
if (!this.ObjectClass) {
this.ObjectClass = Zotero[this._ZDO_Object];
}
this._objectCache = {};
this._objectKeys = {};
this._objectIDs = {};
this._loadedLibraries = {};
this._loadPromise = null;
}
Zotero.DataObjects.prototype._ZDO_idOnly = false;
// Public properties
Zotero.defineProperty(Zotero.DataObjects.prototype, 'idColumn', {
get: function() { return this._ZDO_id; }
});
Zotero.defineProperty(Zotero.DataObjects.prototype, 'table', {
get: function() { return this._ZDO_table; }
});
Zotero.defineProperty(Zotero.DataObjects.prototype, 'relationsTable', {
get: function() { return this._ZDO_object + 'Relations'; }
});
Zotero.defineProperty(Zotero.DataObjects.prototype, 'primaryFields', {
get: function () { return Object.keys(this._primaryDataSQLParts); }
}, {lazy: true});
Zotero.defineProperty(Zotero.DataObjects.prototype, "_primaryDataSQLWhere", {
value: "WHERE 1"
});
Zotero.defineProperty(Zotero.DataObjects.prototype, 'primaryDataSQLFrom', {
get: function() { return " " + this._primaryDataSQLFrom + " " + this._primaryDataSQLWhere; }
}, {lateInit: true});
Zotero.DataObjects.prototype.init = function() {
return this._loadIDsAndKeys();
}
Zotero.DataObjects.prototype.isPrimaryField = function (field) {
return this.primaryFields.indexOf(field) != -1;
}
/**
* Retrieves one or more already-loaded items
*
* If an item hasn't been loaded, an error is thrown
*
* @param {Array|Integer} ids An individual object id or an array of object ids
* @return {Zotero.[Object]|Array<Zotero.[Object]>} A Zotero.[Object], if a scalar id was passed;
* otherwise, an array of Zotero.[Object]
*/
Zotero.DataObjects.prototype.get = function (ids) {
if (Array.isArray(ids)) {
var singleObject = false;
}
else {
var singleObject = true;
ids = [ids];
}
var toReturn = [];
for (let i=0; i<ids.length; i++) {
let id = ids[i];
// Check if already loaded
if (!this._objectCache[id]) {
// If unloaded id is registered, throw an error
if (this._objectKeys[id]) {
throw new Zotero.Exception.UnloadedDataException(
this._ZDO_Object + " " + id + " not yet loaded"
);
}
// Otherwise ignore (which means returning false for a single id)
else {
continue;
}
}
toReturn.push(this._objectCache[id]);
}
// If single id, return the object directly
if (singleObject) {
return toReturn.length ? toReturn[0] : false;
}
return toReturn;
};
/**
* Retrieves (and loads, if necessary) one or more items
*
* @param {Array|Integer} ids An individual object id or an array of object ids
* @param {Object} [options]
* @param {Boolean} [options.noCache=false] - Don't add object to cache after loading
* @return {Promise<Zotero.DataObject|Zotero.DataObject[]>} - A promise for either a data object,
* if a scalar id was passed, or an array of data objects, if an array of ids was passed
*/
Zotero.DataObjects.prototype.getAsync = Zotero.Promise.coroutine(function* (ids, options) {
var toLoad = [];
var toReturn = [];
if (!ids) {
throw new Error("No arguments provided");
}
if (options && typeof options != 'object') {
throw new Error(`'options' must be an object, ${typeof options} given`);
}
if (Array.isArray(ids)) {
var singleObject = false;
}
else {
var singleObject = true;
ids = [ids];
}
for (let i=0; i<ids.length; i++) {
let id = ids[i];
if (!Number.isInteger(id)) {
// TEMP: Re-enable test when removed
let e = new Error(`${this._ZDO_object} ID '${id}' is not an integer (${typeof id})`);
Zotero.logError(e);
id = parseInt(id);
//throw new Error(`${this._ZDO_object} ID '${id}' is not an integer (${typeof id})`);
}
// Check if already loaded
if (this._objectCache[id]) {
toReturn.push(this._objectCache[id]);
}
else {
toLoad.push(id);
}
}
// New object to load
if (toLoad.length) {
// Serialize loads
if (this._loadPromise && this._loadPromise.isPending()) {
yield this._loadPromise;
}
let deferred = Zotero.Promise.defer();
this._loadPromise = deferred.promise;
let loaded = yield this._load(null, toLoad, options);
for (let i=0; i<toLoad.length; i++) {
let id = toLoad[i];
let obj = loaded[id];
if (!obj) {
Zotero.debug(this._ZDO_Object + " " + id + " doesn't exist", 2);
continue;
}
toReturn.push(obj);
}
deferred.resolve();
}
// If single id, return the object directly
if (singleObject) {
return toReturn.length ? toReturn[0] : false;
}
return toReturn;
});
/**
* Get all loaded objects
*
* @return {Zotero.DataObject[]}
*/
Zotero.DataObjects.prototype.getLoaded = function () {
return Object.keys(this._objectCache).map(id => this._objectCache[id]);
}
Zotero.DataObjects.prototype.getAllKeys = function (libraryID) {
var sql = "SELECT key FROM " + this._ZDO_table + " WHERE libraryID=?";
return Zotero.DB.columnQueryAsync(sql, [libraryID]);
};
/**
* @deprecated - use .libraryKey
*/
Zotero.DataObjects.prototype.makeLibraryKeyHash = function (libraryID, key) {
Zotero.debug("WARNING: " + this._ZDO_Objects + ".makeLibraryKeyHash() is deprecated -- use .libraryKey instead");
return libraryID + '_' + key;
}
/**
* @deprecated - use .libraryKey
*/
Zotero.DataObjects.prototype.getLibraryKeyHash = function (obj) {
Zotero.debug("WARNING: " + this._ZDO_Objects + ".getLibraryKeyHash() is deprecated -- use .libraryKey instead");
return this.makeLibraryKeyHash(obj.libraryID, obj.key);
}
Zotero.DataObjects.prototype.parseLibraryKey = function (libraryKey) {
var [libraryID, key] = libraryKey.split('/');
return {
libraryID: parseInt(libraryID),
key: key
};
}
/**
* @deprecated - Use Zotero.DataObjects.parseLibraryKey()
*/
Zotero.DataObjects.prototype.parseLibraryKeyHash = function (libraryKey) {
Zotero.debug("WARNING: " + this._ZDO_Objects + ".parseLibraryKeyHash() is deprecated -- use .parseLibraryKey() instead");
var [libraryID, key] = libraryKey.split('_');
if (!key) {
return false;
}
return {
libraryID: parseInt(libraryID),
key: key
};
}
/**
* Retrieves an object by its libraryID and key
*
* @param {Integer} libraryID
* @param {String} key
* @return {Zotero.DataObject} Zotero data object, or FALSE if not found
*/
Zotero.DataObjects.prototype.getByLibraryAndKey = function (libraryID, key, options) {
var id = this.getIDFromLibraryAndKey(libraryID, key);
if (!id) {
return false;
}
return Zotero[this._ZDO_Objects].get(id, options);
};
/**
* Asynchronously retrieves an object by its libraryID and key
*
* @param {Integer} - libraryID
* @param {String} - key
* @param {Object} [options]
* @param {Boolean} [options.noCache=false] - Don't add object to cache after loading
* @return {Promise<Zotero.DataObject>} - Promise for a data object, or FALSE if not found
*/
Zotero.DataObjects.prototype.getByLibraryAndKeyAsync = Zotero.Promise.method(function (libraryID, key, options) {
var id = this.getIDFromLibraryAndKey(libraryID, key);
if (!id) {
return false;
}
return Zotero[this._ZDO_Objects].getAsync(id, options);
});
Zotero.DataObjects.prototype.exists = function (id) {
return !!this.getLibraryAndKeyFromID(id);
}
/**
* @return {Object} Object with 'libraryID' and 'key'
*/
Zotero.DataObjects.prototype.getLibraryAndKeyFromID = function (id) {
var lk = this._objectKeys[id];
return lk ? { libraryID: lk[0], key: lk[1] } : false;
}
Zotero.DataObjects.prototype.getIDFromLibraryAndKey = function (libraryID, key) {
if (!libraryID) throw new Error("Library ID not provided");
// TEMP: Just warn for now
//if (!key) throw new Error("Key not provided");
if (!key) Zotero.logError("Key not provided");
return (this._objectIDs[libraryID] && this._objectIDs[libraryID][key])
? this._objectIDs[libraryID][key] : false;
}
Zotero.DataObjects.prototype.getOlder = Zotero.Promise.method(function (libraryID, date) {
if (!date || date.constructor.name != 'Date') {
throw ("date must be a JS Date in "
+ "Zotero." + this._ZDO_Objects + ".getOlder()")
}
var sql = "SELECT ROWID FROM " + this._ZDO_table
+ " WHERE libraryID=? AND clientDateModified<?";
return Zotero.DB.columnQueryAsync(sql, [libraryID, Zotero.Date.dateToSQL(date, true)]);
});
Zotero.DataObjects.prototype.getNewer = Zotero.Promise.method(function (libraryID, date, ignoreFutureDates) {
if (!date || date.constructor.name != 'Date') {
throw ("date must be a JS Date in "
+ "Zotero." + this._ZDO_Objects + ".getNewer()")
}
var sql = "SELECT ROWID FROM " + this._ZDO_table
+ " WHERE libraryID=? AND clientDateModified>?";
if (ignoreFutureDates) {
sql += " AND clientDateModified<=CURRENT_TIMESTAMP";
}
return Zotero.DB.columnQueryAsync(sql, [libraryID, Zotero.Date.dateToSQL(date, true)]);
});
/**
* Gets the latest version for each object of a given type in the given library
*
* @return {Promise<Object>} - A promise for an object with object keys as keys and versions
* as properties
*/
Zotero.DataObjects.prototype.getObjectVersions = Zotero.Promise.coroutine(function* (libraryID, keys = null) {
var versions = {};
if (keys) {
yield Zotero.Utilities.Internal.forEachChunkAsync(
keys,
Zotero.DB.MAX_BOUND_PARAMETERS - 1,
Zotero.Promise.coroutine(function* (chunk) {
var sql = "SELECT key, version FROM " + this._ZDO_table
+ " WHERE libraryID=? AND key IN (" + chunk.map(key => '?').join(', ') + ")";
var rows = yield Zotero.DB.queryAsync(sql, [libraryID].concat(chunk));
for (let i = 0; i < rows.length; i++) {
let row = rows[i];
versions[row.key] = row.version;
}
}.bind(this))
);
}
else {
let sql = "SELECT key, version FROM " + this._ZDO_table + " WHERE libraryID=?";
let rows = yield Zotero.DB.queryAsync(sql, [libraryID]);
for (let i = 0; i < rows.length; i++) {
let row = rows[i];
versions[row.key] = row.version;
}
}
return versions;
});
/**
* Bulk-load data type(s) of given objects if not loaded
*
* This would generally be used to load necessary data for cross-library search results, since those
* results might include objects in libraries that haven't yet been loaded.
*
* @param {Zotero.DataObject[]} objects
* @param {String[]} [dataTypes] - Data types to load, defaulting to all types
* @return {Promise}
*/
Zotero.DataObjects.prototype.loadDataTypes = Zotero.Promise.coroutine(function* (objects, dataTypes) {
if (!dataTypes) {
dataTypes = this.ObjectClass.prototype._dataTypes;
}
for (let dataType of dataTypes) {
let typeIDsByLibrary = {};
for (let obj of objects) {
if (obj._loaded[dataType]) {
continue;
}
if (!typeIDsByLibrary[obj.libraryID]) {
typeIDsByLibrary[obj.libraryID] = [];
}
typeIDsByLibrary[obj.libraryID].push(obj.id);
}
for (let libraryID in typeIDsByLibrary) {
yield this._loadDataTypeInLibrary(dataType, parseInt(libraryID), typeIDsByLibrary[libraryID]);
}
}
});
/**
* Loads data for a given data type
* @param {String} dataType
* @param {Integer} libraryID
* @param {Integer[]} [ids]
*/
Zotero.DataObjects.prototype._loadDataTypeInLibrary = Zotero.Promise.coroutine(function* (dataType, libraryID, ids) {
var funcName = "_load" + dataType[0].toUpperCase() + dataType.substr(1)
// Single data types need an 's' (e.g., 'note' -> 'loadNotes()')
+ ((dataType.endsWith('s') || dataType.endsWith('Data') ? '' : 's'));
if (!this[funcName]) {
throw new Error(`Zotero.${this._ZDO_Objects}.${funcName} is not a function`);
}
if (ids && ids.length == 0) {
return;
}
var t = new Date;
var libraryName = Zotero.Libraries.get(libraryID).name;
var idSQL = "";
if (ids) {
idSQL = " AND " + this.idColumn + " IN (" + ids.map(id => parseInt(id)).join(", ") + ")";
}
Zotero.debug("Loading " + dataType + " for "
+ (ids
? ids.length + " " + (ids.length == 1 ? this._ZDO_object : this._ZDO_objects)
: this._ZDO_objects)
+ " in " + libraryName);
yield this[funcName](libraryID, ids ? ids : [], idSQL);
Zotero.debug(`Loaded ${dataType} in ${libraryName} in ${new Date() - t} ms`);
});
Zotero.DataObjects.prototype.loadAll = Zotero.Promise.coroutine(function* (libraryID, ids) {
var t = new Date();
var library = Zotero.Libraries.get(libraryID)
Zotero.debug("Loading "
+ (ids ? ids.length : "all") + " "
+ (ids && ids.length == 1 ? this._ZDO_object : this._ZDO_objects)
+ " in " + library.name);
if (!ids) {
library.setDataLoading(this._ZDO_object);
}
let dataTypes = this.ObjectClass.prototype._dataTypes;
for (let i = 0; i < dataTypes.length; i++) {
yield this._loadDataTypeInLibrary(dataTypes[i], libraryID, ids);
}
Zotero.debug(`Loaded ${this._ZDO_objects} in ${library.name} in ${new Date() - t} ms`);
if (!ids) {
library.setDataLoaded(this._ZDO_object);
}
});
Zotero.DataObjects.prototype._loadPrimaryData = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL, options) {
var loaded = {};
// If library isn't an integer (presumably false or null), skip it
if (parseInt(libraryID) != libraryID) {
libraryID = false;
}
var sql = this.primaryDataSQL;
var params = [];
if (libraryID !== false) {
sql += ' AND O.libraryID=?';
params.push(libraryID);
}
if (ids.length) {
sql += ' AND O.' + this._ZDO_id + ' IN (' + ids.join(',') + ')';
}
yield Zotero.DB.queryAsync(
sql,
params,
{
onRow: function (row) {
var id = row.getResultByName(this._ZDO_id);
var columns = Object.keys(this._primaryDataSQLParts);
var rowObj = {};
for (let i=0; i<columns.length; i++) {
rowObj[columns[i]] = row.getResultByIndex(i);
}
var obj;
// Existing object -- reload in place
if (this._objectCache[id]) {
this._objectCache[id].loadFromRow(rowObj, true);
obj = this._objectCache[id];
}
// Object doesn't exist -- create new object and stuff in cache
else {
obj = this._getObjectForRow(rowObj);
obj.loadFromRow(rowObj, true);
if (!options || !options.noCache) {
this.registerObject(obj);
}
}
loaded[id] = obj;
}.bind(this)
}
);
if (!ids) {
this._loadedLibraries[libraryID] = true;
// If loading all objects, remove cached objects that no longer exist
for (let i in this._objectCache) {
let obj = this._objectCache[i];
if (libraryID !== false && obj.libraryID !== libraryID) {
continue;
}
if (!loaded[obj.id]) {
this.unload(obj.id);
}
}
if (this._postLoad) {
this._postLoad(libraryID, ids);
}
}
return loaded;
});
Zotero.DataObjects.prototype._loadRelations = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL) {
if (!this._relationsTable) {
throw new Error("Relations not supported for " + this._ZDO_objects);
}
var sql = "SELECT " + this.idColumn + ", predicate, object "
+ `FROM ${this.table} LEFT JOIN ${this._relationsTable} USING (${this.idColumn}) `
+ "LEFT JOIN relationPredicates USING (predicateID) "
+ "WHERE libraryID=?" + idSQL;
var params = [libraryID];
var lastID;
var rows = [];
var setRows = function (id, rows) {
var obj = this._objectCache[id];
if (!obj) {
throw new Error(this._ZDO_Object + " " + id + " not found");
}
var relations = {};
function addRel(predicate, object) {
if (!relations[predicate]) {
relations[predicate] = [];
}
relations[predicate].push(object);
}
for (let i = 0; i < rows.length; i++) {
let row = rows[i];
addRel(row.predicate, row.object);
}
/*if (this._objectType == 'item') {
let getURI = Zotero.URI["get" + this._ObjectType + "URI"].bind(Zotero.URI);
let objectURI = getURI(this);
// Related items are bidirectional, so include any pointing to this object
let objects = Zotero.Relations.getByPredicateAndObject(
Zotero.Relations.relatedItemPredicate, objectURI
);
for (let i = 0; i < objects.length; i++) {
addRel(Zotero.Relations.relatedItemPredicate, getURI(objects[i]));
}
// Also include any owl:sameAs relations pointing to this object
objects = Zotero.Relations.getByPredicateAndObject(
Zotero.Relations.linkedObjectPredicate, objectURI
);
for (let i = 0; i < objects.length; i++) {
addRel(Zotero.Relations.linkedObjectPredicate, getURI(objects[i]));
}
}*/
// Relations are stored as predicate-object pairs
obj._relations = this.flattenRelations(relations);
obj._loaded.relations = true;
obj._clearChanged('relations');
}.bind(this);
yield Zotero.DB.queryAsync(
sql,
params,
{
noCache: ids.length != 1,
onRow: function (row) {
let id = row.getResultByIndex(0);
if (lastID && id !== lastID) {
setRows(lastID, rows);
rows = [];
}
lastID = id;
let predicate = row.getResultByIndex(1);
// No relations
if (predicate === null) {
return;
}
rows.push({
predicate,
object: row.getResultByIndex(2)
});
}.bind(this)
}
);
if (lastID) {
setRows(lastID, rows);
}
});
/**
* Flatten API JSON relations object into an array of unique predicate-object pairs
*
* @param {Object} relations - Relations object in API JSON format, with predicates as keys
* and arrays of URIs as objects
* @return {Array[]} - Predicate-object pairs
*/
Zotero.DataObjects.prototype.flattenRelations = function (relations) {
var relationsFlat = [];
for (let predicate in relations) {
let object = relations[predicate];
if (Array.isArray(object)) {
object = Zotero.Utilities.arrayUnique(object);
for (let i = 0; i < object.length; i++) {
relationsFlat.push([predicate, object[i]]);
}
}
else if (typeof object == 'string') {
relationsFlat.push([predicate, object]);
}
else {
Zotero.debug(object, 1);
throw new Error("Invalid relation value");
}
}
return relationsFlat;
}
/**
* Reload loaded data of loaded objects
*
* @param {Array|Number} ids - An id or array of ids
* @param {Array} [dataTypes] - Data types to reload (e.g., 'primaryData'), or all loaded
* types if not provided
* @param {Boolean} [reloadUnchanged=false] - Reload even data that hasn't changed internally.
* This should be set to true for data that was
* changed externally (e.g., globally renamed tags).
*/
Zotero.DataObjects.prototype.reload = Zotero.Promise.coroutine(function* (ids, dataTypes, reloadUnchanged) {
ids = Zotero.flattenArguments(ids);
Zotero.debug('Reloading ' + (dataTypes ? '[' + dataTypes.join(', ') + '] for ' : '')
+ this._ZDO_objects + ' ' + ids);
// If data types not specified, reload loaded data for each object individually.
// TODO: optimize
if (!dataTypes) {
for (let i=0; i<ids.length; i++) {
if (this._objectCache[ids[i]]) {
yield this._objectCache[ids[i]].reload(dataTypes, reloadUnchanged);
}
}
return;
}
for (let dataType of dataTypes) {
let typeIDsByLibrary = {};
for (let id of ids) {
let obj = this._objectCache[id];
if (!obj || !obj._loaded[dataType] || obj._skipDataTypeLoad[dataType]
|| (!reloadUnchanged && !obj._changed[dataType])) {
continue;
}
if (!typeIDsByLibrary[obj.libraryID]) {
typeIDsByLibrary[obj.libraryID] = [];
}
typeIDsByLibrary[obj.libraryID].push(id);
}
for (let libraryID in typeIDsByLibrary) {
yield this._loadDataTypeInLibrary(dataType, parseInt(libraryID), typeIDsByLibrary[libraryID]);
}
}
return true;
});
Zotero.DataObjects.prototype.reloadAll = function (libraryID) {
Zotero.debug("Reloading all " + this._ZDO_objects);
// Remove objects not stored in database
var sql = "SELECT ROWID FROM " + this._ZDO_table;
var params = [];
if (libraryID !== undefined) {
sql += ' WHERE libraryID=?';
params.push(libraryID);
}
return Zotero.DB.columnQueryAsync(sql, params)
.then(function (ids) {
for (var id in this._objectCache) {
if (!ids || ids.indexOf(parseInt(id)) == -1) {
delete this._objectCache[id];
}
}
// Reload data
this._loadedLibraries[libraryID] = false;
return this._load(libraryID);
});
}
Zotero.DataObjects.prototype.registerObject = function (obj) {
var id = obj.id;
var libraryID = obj.libraryID;
var key = obj.key;
//Zotero.debug("Registering " + this._ZDO_object + " " + id + " as " + libraryID + "/" + key);
if (!this._objectIDs[libraryID]) {
this._objectIDs[libraryID] = {};
}
this._objectIDs[libraryID][key] = id;
this._objectKeys[id] = [libraryID, key];
this._objectCache[id] = obj;
obj._inCache = true;
}
Zotero.DataObjects.prototype.dropDeadObjectsFromCache = function() {
let ids = [];
for (let libraryID in this._objectIDs) {
if (Zotero.Libraries.exists(libraryID)) continue;
for (let key in this._objectIDs[libraryID]) {
ids.push(this._objectIDs[libraryID][key]);
}
}
this.unload(ids);
}
/**
* Clear object from internal array
*
* @param int[] ids objectIDs
*/
Zotero.DataObjects.prototype.unload = function () {
var ids = Zotero.flattenArguments(arguments);
for (var i=0; i<ids.length; i++) {
let id = ids[i];
let {libraryID, key} = this.getLibraryAndKeyFromID(id);
if (key) {
delete this._objectIDs[libraryID][key];
delete this._objectKeys[id];
}
delete this._objectCache[id];
}
}
/**
* Set the version of objects, efficiently
*
* @param {Integer[]} ids - Ids of objects to update
* @param {Boolean} version
*/
Zotero.DataObjects.prototype.updateVersion = Zotero.Promise.method(function (ids, version) {
if (version != parseInt(version)) {
throw new Error("'version' must be an integer ('" + version + "' given)");
}
version = parseInt(version);
let sql = "UPDATE " + this.table + " SET version=" + version + " "
+ "WHERE " + this.idColumn + " IN (";
return Zotero.Utilities.Internal.forEachChunkAsync(
ids,
Zotero.DB.MAX_BOUND_PARAMETERS,
Zotero.Promise.coroutine(function* (chunk) {
yield Zotero.DB.queryAsync(sql + chunk.map(() => '?').join(', ') + ')', chunk);
// Update the internal 'version' property of any loaded objects
for (let i = 0; i < chunk.length; i++) {
let id = chunk[i];
let obj = this._objectCache[id];
if (obj) {
obj.updateVersion(version, true);
}
}
}.bind(this))
);
});
/**
* Set the sync state of objects, efficiently
*
* @param {Integer[]} ids - Ids of objects to update
* @param {Boolean} synced
*/
Zotero.DataObjects.prototype.updateSynced = Zotero.Promise.method(function (ids, synced) {
let sql = "UPDATE " + this.table + " SET synced=" + (synced ? 1 : 0) + " "
+ "WHERE " + this.idColumn + " IN (";
return Zotero.Utilities.Internal.forEachChunkAsync(
ids,
Zotero.DB.MAX_BOUND_PARAMETERS,
Zotero.Promise.coroutine(function* (chunk) {
yield Zotero.DB.queryAsync(sql + chunk.map(() => '?').join(', ') + ')', chunk);
// Update the internal 'synced' property of any loaded objects
for (let i = 0; i < chunk.length; i++) {
let id = chunk[i];
let obj = this._objectCache[id];
if (obj) {
obj.updateSynced(!!synced, true);
}
}
}.bind(this))
);
});
Zotero.DataObjects.prototype.isEditable = function (obj) {
var libraryID = obj.libraryID;
if (!libraryID) {
return true;
}
if (!Zotero.Libraries.get(libraryID).editable) return false;
if (obj.objectType == 'item' && obj.isAttachment()
&& (obj.attachmentLinkMode == Zotero.Attachments.LINK_MODE_IMPORTED_URL ||
obj.attachmentLinkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE)
&& !Zotero.Libraries.get(libraryID).filesEditable
) {
return false;
}
return true;
}
Zotero.defineProperty(Zotero.DataObjects.prototype, "primaryDataSQL", {
get: function () {
return "SELECT "
+ Object.keys(this._primaryDataSQLParts).map((val) => this._primaryDataSQLParts[val]).join(', ')
+ this.primaryDataSQLFrom;
}
}, {lazy: true});
Zotero.DataObjects.prototype.getPrimaryDataSQLPart = function (part) {
var sql = this._primaryDataSQLParts[part];
if (!sql) {
throw new Error("Invalid primary data SQL part '" + part + "'");
}
return sql;
}
/**
* Delete one or more objects from the database and caches
*
* @param {Integer|Integer[]} ids - Object ids
* @param {Object} [options] - See Zotero.DataObject.prototype.erase
* @param {Function} [options.onProgress] - f(progress, progressMax)
* @return {Promise}
*/
Zotero.DataObjects.prototype.erase = Zotero.Promise.coroutine(function* (ids, options = {}) {
ids = Zotero.flattenArguments(ids);
yield Zotero.DB.executeTransaction(function* () {
for (let i = 0; i < ids.length; i++) {
let obj = yield this.getAsync(ids[i]);
if (!obj) {
continue;
}
yield obj.erase(options);
if (options.onProgress) {
options.onProgress(i + 1, ids.length);
}
}
this.unload(ids);
}.bind(this));
});
// TEMP: remove
Zotero.DataObjects.prototype._load = Zotero.Promise.coroutine(function* (libraryID, ids, options) {
var loaded = {};
// If library isn't an integer (presumably false or null), skip it
if (parseInt(libraryID) != libraryID) {
libraryID = false;
}
if (libraryID === false && !ids) {
throw new Error("Either libraryID or ids must be provided");
}
if (libraryID !== false && this._loadedLibraries[libraryID]) {
return loaded;
}
var sql = this.primaryDataSQL;
var params = [];
if (libraryID !== false) {
sql += ' AND O.libraryID=?';
params.push(libraryID);
}
if (ids) {
sql += ' AND O.' + this._ZDO_id + ' IN (' + ids.join(',') + ')';
}
var t = new Date();
yield Zotero.DB.queryAsync(
sql,
params,
{
onRow: function (row) {
var id = row.getResultByName(this._ZDO_id);
var columns = Object.keys(this._primaryDataSQLParts);
var rowObj = {};
for (let i=0; i<columns.length; i++) {
rowObj[columns[i]] = row.getResultByIndex(i);
}
var obj;
// Existing object -- reload in place
if (this._objectCache[id]) {
this._objectCache[id].loadFromRow(rowObj, true);
obj = this._objectCache[id];
}
// Object doesn't exist -- create new object and stuff in cache
else {
obj = this._getObjectForRow(rowObj);
obj.loadFromRow(rowObj, true);
if (!options || !options.noCache) {
this.registerObject(obj);
}
}
loaded[id] = obj;
}.bind(this)
}
);
Zotero.debug("Loaded " + this._ZDO_objects + " in " + ((new Date) - t) + "ms");
if (!ids) {
this._loadedLibraries[libraryID] = true;
// If loading all objects, remove cached objects that no longer exist
for (let i in this._objectCache) {
let obj = this._objectCache[i];
if (libraryID !== false && obj.libraryID !== libraryID) {
continue;
}
if (!loaded[obj.id]) {
this.unload(obj.id);
}
}
if (this._postLoad) {
this._postLoad(libraryID, ids);
}
}
return loaded;
});
Zotero.DataObjects.prototype._getObjectForRow = function(row) {
return new Zotero[this._ZDO_Object];
};
Zotero.DataObjects.prototype._loadIDsAndKeys = Zotero.Promise.coroutine(function* () {
var sql = "SELECT ROWID AS id, libraryID, key FROM " + this._ZDO_table;
var rows = yield Zotero.DB.queryAsync(sql);
for (let i=0; i<rows.length; i++) {
let row = rows[i];
this._objectKeys[row.id] = [row.libraryID, row.key];
if (!this._objectIDs[row.libraryID]) {
this._objectIDs[row.libraryID] = {};
}
this._objectIDs[row.libraryID][row.key] = row.id;
}
});