www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

dataObjects.js (29300B)


      1 /*
      2     ***** BEGIN LICENSE BLOCK *****
      3     
      4     Copyright © 2009 Center for History and New Media
      5                      George Mason University, Fairfax, Virginia, USA
      6                      http://zotero.org
      7     
      8     This file is part of Zotero.
      9     
     10     Zotero is free software: you can redistribute it and/or modify
     11     it under the terms of the GNU Affero General Public License as published by
     12     the Free Software Foundation, either version 3 of the License, or
     13     (at your option) any later version.
     14     
     15     Zotero is distributed in the hope that it will be useful,
     16     but WITHOUT ANY WARRANTY; without even the implied warranty of
     17     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     18     GNU Affero General Public License for more details.
     19     
     20     You should have received a copy of the GNU Affero General Public License
     21     along with Zotero.  If not, see <http://www.gnu.org/licenses/>.
     22     
     23     ***** END LICENSE BLOCK *****
     24 */
     25 
     26 
     27 Zotero.DataObjects = function () {
     28 	if (!this._ZDO_object) throw new Error('this._ZDO_object must be set before calling Zotero.DataObjects constructor');
     29 	
     30 	if (!this._ZDO_objects) {
     31 		this._ZDO_objects = Zotero.DataObjectUtilities.getObjectTypePlural(this._ZDO_object);
     32 	}
     33 	if (!this._ZDO_Object) {
     34 		this._ZDO_Object = this._ZDO_object.substr(0, 1).toUpperCase()
     35 			+ this._ZDO_object.substr(1);
     36 	}
     37 	if (!this._ZDO_Objects) {
     38 		this._ZDO_Objects = this._ZDO_objects.substr(0, 1).toUpperCase()
     39 			+ this._ZDO_objects.substr(1);
     40 	}
     41 	
     42 	if (!this._ZDO_id) {
     43 		this._ZDO_id = this._ZDO_object + 'ID';
     44 	}
     45 	
     46 	if (!this._ZDO_table) {
     47 		this._ZDO_table = this._ZDO_objects;
     48 	}
     49 	
     50 	if (!this.ObjectClass) {
     51 		this.ObjectClass = Zotero[this._ZDO_Object];
     52 	}
     53 	
     54 	this._objectCache = {};
     55 	this._objectKeys = {};
     56 	this._objectIDs = {};
     57 	this._loadedLibraries = {};
     58 	this._loadPromise = null;
     59 }
     60 
     61 Zotero.DataObjects.prototype._ZDO_idOnly = false;
     62 
     63 // Public properties
     64 Zotero.defineProperty(Zotero.DataObjects.prototype, 'idColumn', {
     65 	get: function() { return this._ZDO_id; }
     66 });
     67 Zotero.defineProperty(Zotero.DataObjects.prototype, 'table', {
     68 	get: function() { return this._ZDO_table; }
     69 });
     70 
     71 Zotero.defineProperty(Zotero.DataObjects.prototype, 'relationsTable', {
     72 	get: function() { return this._ZDO_object + 'Relations'; }
     73 });
     74 
     75 Zotero.defineProperty(Zotero.DataObjects.prototype, 'primaryFields', {
     76 	get: function () { return Object.keys(this._primaryDataSQLParts); }
     77 }, {lazy: true});
     78 
     79 Zotero.defineProperty(Zotero.DataObjects.prototype, "_primaryDataSQLWhere", {
     80 	value: "WHERE 1"
     81 });
     82 
     83 Zotero.defineProperty(Zotero.DataObjects.prototype, 'primaryDataSQLFrom', {
     84 	get: function() { return " " + this._primaryDataSQLFrom + " " + this._primaryDataSQLWhere; }
     85 }, {lateInit: true});
     86 
     87 Zotero.DataObjects.prototype.init = function() {
     88 	return this._loadIDsAndKeys();
     89 }
     90 
     91 
     92 Zotero.DataObjects.prototype.isPrimaryField = function (field) {
     93 	return this.primaryFields.indexOf(field) != -1;
     94 }
     95 
     96 
     97 /**
     98  * Retrieves one or more already-loaded items
     99  *
    100  * If an item hasn't been loaded, an error is thrown
    101  *
    102  * @param {Array|Integer} ids  An individual object id or an array of object ids
    103  * @return {Zotero.[Object]|Array<Zotero.[Object]>} A Zotero.[Object], if a scalar id was passed;
    104  *                                          otherwise, an array of Zotero.[Object]
    105  */
    106 Zotero.DataObjects.prototype.get = function (ids) {
    107 	if (Array.isArray(ids)) {
    108 		var singleObject = false;
    109 	}
    110 	else {
    111 		var singleObject = true;
    112 		ids = [ids];
    113 	}
    114 	
    115 	var toReturn = [];
    116 	
    117 	for (let i=0; i<ids.length; i++) {
    118 		let id = ids[i];
    119 		// Check if already loaded
    120 		if (!this._objectCache[id]) {
    121 			// If unloaded id is registered, throw an error
    122 			if (this._objectKeys[id]) {
    123 				throw new Zotero.Exception.UnloadedDataException(
    124 					this._ZDO_Object + " " + id + " not yet loaded"
    125 				);
    126 			}
    127 			// Otherwise ignore (which means returning false for a single id)
    128 			else {
    129 				continue;
    130 			}
    131 		}
    132 		toReturn.push(this._objectCache[id]);
    133 	}
    134 	
    135 	// If single id, return the object directly
    136 	if (singleObject) {
    137 		return toReturn.length ? toReturn[0] : false;
    138 	}
    139 	
    140 	return toReturn;
    141 };
    142 	
    143 	
    144 /**
    145  * Retrieves (and loads, if necessary) one or more items
    146  *
    147  * @param {Array|Integer} ids  An individual object id or an array of object ids
    148  * @param {Object} [options]
    149  * @param {Boolean} [options.noCache=false] - Don't add object to cache after loading
    150  * @return {Promise<Zotero.DataObject|Zotero.DataObject[]>} - A promise for either a data object,
    151  *     if a scalar id was passed, or an array of data objects, if an array of ids was passed
    152  */
    153 Zotero.DataObjects.prototype.getAsync = Zotero.Promise.coroutine(function* (ids, options) {
    154 	var toLoad = [];
    155 	var toReturn = [];
    156 	
    157 	if (!ids) {
    158 		throw new Error("No arguments provided");
    159 	}
    160 	
    161 	if (options && typeof options != 'object') {
    162 		throw new Error(`'options' must be an object, ${typeof options} given`);
    163 	}
    164 	
    165 	if (Array.isArray(ids)) {
    166 		var singleObject = false;
    167 	}
    168 	else {
    169 		var singleObject = true;
    170 		ids = [ids];
    171 	}
    172 	
    173 	for (let i=0; i<ids.length; i++) {
    174 		let id = ids[i];
    175 		
    176 		if (!Number.isInteger(id)) {
    177 			// TEMP: Re-enable test when removed
    178 			let e = new Error(`${this._ZDO_object} ID '${id}' is not an integer (${typeof id})`);
    179 			Zotero.logError(e);
    180 			id = parseInt(id);
    181 			//throw new Error(`${this._ZDO_object} ID '${id}' is not an integer (${typeof id})`);
    182 		}
    183 		
    184 		// Check if already loaded
    185 		if (this._objectCache[id]) {
    186 			toReturn.push(this._objectCache[id]);
    187 		}
    188 		else {
    189 			toLoad.push(id);
    190 		}
    191 	}
    192 	
    193 	// New object to load
    194 	if (toLoad.length) {
    195 		// Serialize loads
    196 		if (this._loadPromise && this._loadPromise.isPending()) {
    197 			yield this._loadPromise;
    198 		}
    199 		let deferred = Zotero.Promise.defer();
    200 		this._loadPromise = deferred.promise;
    201 		
    202 		let loaded = yield this._load(null, toLoad, options);
    203 		for (let i=0; i<toLoad.length; i++) {
    204 			let id = toLoad[i];
    205 			let obj = loaded[id];
    206 			if (!obj) {
    207 				Zotero.debug(this._ZDO_Object + " " + id + " doesn't exist", 2);
    208 				continue;
    209 			}
    210 			toReturn.push(obj);
    211 		}
    212 		deferred.resolve();
    213 	}
    214 	
    215 	// If single id, return the object directly
    216 	if (singleObject) {
    217 		return toReturn.length ? toReturn[0] : false;
    218 	}
    219 	
    220 	return toReturn;
    221 });
    222 
    223 
    224 /**
    225  * Get all loaded objects
    226  *
    227  * @return {Zotero.DataObject[]}
    228  */
    229 Zotero.DataObjects.prototype.getLoaded = function () {
    230 	return Object.keys(this._objectCache).map(id => this._objectCache[id]);
    231 }
    232 
    233 
    234 Zotero.DataObjects.prototype.getAllIDs = function (libraryID) {
    235 	var sql = `SELECT ${this._ZDO_id} FROM ${this._ZDO_table} WHERE libraryID=?`;
    236 	return Zotero.DB.columnQueryAsync(sql, [libraryID]);
    237 };
    238 
    239 
    240 Zotero.DataObjects.prototype.getAllKeys = function (libraryID) {
    241 	var sql = "SELECT key FROM " + this._ZDO_table + " WHERE libraryID=?";
    242 	return Zotero.DB.columnQueryAsync(sql, [libraryID]);
    243 };
    244 
    245 
    246 /**
    247  * @deprecated - use .libraryKey
    248  */
    249 Zotero.DataObjects.prototype.makeLibraryKeyHash = function (libraryID, key) {
    250 	Zotero.debug("WARNING: " + this._ZDO_Objects + ".makeLibraryKeyHash() is deprecated -- use .libraryKey instead");
    251 	return libraryID + '_' + key;
    252 }
    253 
    254 
    255 /**
    256  * @deprecated - use .libraryKey
    257  */
    258 Zotero.DataObjects.prototype.getLibraryKeyHash = function (obj) {
    259 	Zotero.debug("WARNING: " + this._ZDO_Objects + ".getLibraryKeyHash() is deprecated -- use .libraryKey instead");
    260 	return this.makeLibraryKeyHash(obj.libraryID, obj.key);
    261 }
    262 
    263 
    264 Zotero.DataObjects.prototype.parseLibraryKey = function (libraryKey) {
    265 	var [libraryID, key] = libraryKey.split('/');
    266 	return {
    267 		libraryID: parseInt(libraryID),
    268 		key: key
    269 	};
    270 }
    271 
    272 
    273 /**
    274  * @deprecated - Use Zotero.DataObjects.parseLibraryKey()
    275  */
    276 Zotero.DataObjects.prototype.parseLibraryKeyHash = function (libraryKey) {
    277 	Zotero.debug("WARNING: " + this._ZDO_Objects + ".parseLibraryKeyHash() is deprecated -- use .parseLibraryKey() instead");
    278 	var [libraryID, key] = libraryKey.split('_');
    279 	if (!key) {
    280 		return false;
    281 	}
    282 	return {
    283 		libraryID: parseInt(libraryID),
    284 		key: key
    285 	};
    286 }
    287 
    288 
    289 /**
    290  * Retrieves an object by its libraryID and key
    291  *
    292  * @param	{Integer}		libraryID
    293  * @param	{String}			key
    294  * @return	{Zotero.DataObject}			Zotero data object, or FALSE if not found
    295  */
    296 Zotero.DataObjects.prototype.getByLibraryAndKey = function (libraryID, key, options) {
    297 	var id = this.getIDFromLibraryAndKey(libraryID, key);
    298 	if (!id) {
    299 		return false;
    300 	}
    301 	return Zotero[this._ZDO_Objects].get(id, options);
    302 };
    303 
    304 
    305 /**
    306  * Asynchronously retrieves an object by its libraryID and key
    307  *
    308  * @param {Integer} - libraryID
    309  * @param {String} - key
    310  * @param {Object} [options]
    311  * @param {Boolean} [options.noCache=false] - Don't add object to cache after loading
    312  * @return {Promise<Zotero.DataObject>} - Promise for a data object, or FALSE if not found
    313  */
    314 Zotero.DataObjects.prototype.getByLibraryAndKeyAsync = Zotero.Promise.method(function (libraryID, key, options) {
    315 	var id = this.getIDFromLibraryAndKey(libraryID, key);
    316 	if (!id) {
    317 		return false;
    318 	}
    319 	return Zotero[this._ZDO_Objects].getAsync(id, options);
    320 });
    321 
    322 
    323 Zotero.DataObjects.prototype.exists = function (id) {
    324 	return !!this.getLibraryAndKeyFromID(id);
    325 }
    326 
    327 
    328 Zotero.DataObjects.prototype.existsByKey = function (key) {
    329 	return !!this.getIDFromLibraryAndKey(id);
    330 }
    331 
    332 
    333 /**
    334  * @return {Object} Object with 'libraryID' and 'key'
    335  */
    336 Zotero.DataObjects.prototype.getLibraryAndKeyFromID = function (id) {
    337 	var lk = this._objectKeys[id];
    338 	return lk ? { libraryID: lk[0], key: lk[1] } : false;
    339 }
    340 
    341 
    342 Zotero.DataObjects.prototype.getIDFromLibraryAndKey = function (libraryID, key) {
    343 	if (!libraryID) throw new Error("Library ID not provided");
    344 	// TEMP: Just warn for now
    345 	//if (!key) throw new Error("Key not provided");
    346 	if (!key) Zotero.logError("Key not provided");
    347 	return (this._objectIDs[libraryID] && this._objectIDs[libraryID][key])
    348 		? this._objectIDs[libraryID][key] : false;
    349 }
    350 
    351 
    352 Zotero.DataObjects.prototype.getOlder = Zotero.Promise.method(function (libraryID, date) {
    353 	if (!date || date.constructor.name != 'Date') {
    354 		throw ("date must be a JS Date in "
    355 			+ "Zotero." + this._ZDO_Objects + ".getOlder()")
    356 	}
    357 	
    358 	var sql = "SELECT ROWID FROM " + this._ZDO_table
    359 		+ " WHERE libraryID=? AND clientDateModified<?";
    360 	return Zotero.DB.columnQueryAsync(sql, [libraryID, Zotero.Date.dateToSQL(date, true)]);
    361 });
    362 
    363 
    364 Zotero.DataObjects.prototype.getNewer = Zotero.Promise.method(function (libraryID, date, ignoreFutureDates) {
    365 	if (!date || date.constructor.name != 'Date') {
    366 		throw ("date must be a JS Date in "
    367 			+ "Zotero." + this._ZDO_Objects + ".getNewer()")
    368 	}
    369 	
    370 	var sql = "SELECT ROWID FROM " + this._ZDO_table
    371 		+ " WHERE libraryID=? AND clientDateModified>?";
    372 	if (ignoreFutureDates) {
    373 		sql += " AND clientDateModified<=CURRENT_TIMESTAMP";
    374 	}
    375 	return Zotero.DB.columnQueryAsync(sql, [libraryID, Zotero.Date.dateToSQL(date, true)]);
    376 });
    377 
    378 
    379 /**
    380  * Gets the latest version for each object of a given type in the given library
    381  *
    382  * @return {Promise<Object>} - A promise for an object with object keys as keys and versions
    383  *                             as properties
    384  */
    385 Zotero.DataObjects.prototype.getObjectVersions = Zotero.Promise.coroutine(function* (libraryID, keys = null) {
    386 	var versions = {};
    387 	
    388 	if (keys) {
    389 		yield Zotero.Utilities.Internal.forEachChunkAsync(
    390 			keys,
    391 			Zotero.DB.MAX_BOUND_PARAMETERS - 1,
    392 			Zotero.Promise.coroutine(function* (chunk) {
    393 				var sql = "SELECT key, version FROM " + this._ZDO_table
    394 					+ " WHERE libraryID=? AND key IN (" + chunk.map(key => '?').join(', ') + ")";
    395 				var rows = yield Zotero.DB.queryAsync(sql, [libraryID].concat(chunk));
    396 				for (let i = 0; i < rows.length; i++) {
    397 					let row = rows[i];
    398 					versions[row.key] = row.version;
    399 				}
    400 			}.bind(this))
    401 		);
    402 	}
    403 	else {
    404 		let sql = "SELECT key, version FROM " + this._ZDO_table + " WHERE libraryID=?";
    405 		let rows = yield Zotero.DB.queryAsync(sql, [libraryID]);
    406 		for (let i = 0; i < rows.length; i++) {
    407 			let row = rows[i];
    408 			versions[row.key] = row.version;
    409 		}
    410 	}
    411 	
    412 	return versions;
    413 });
    414 
    415 
    416 /**
    417  * Bulk-load data type(s) of given objects if not loaded
    418  *
    419  * This would generally be used to load necessary data for cross-library search results, since those
    420  * results might include objects in libraries that haven't yet been loaded.
    421  *
    422  * @param {Zotero.DataObject[]} objects
    423  * @param {String[]} [dataTypes] - Data types to load, defaulting to all types
    424  * @return {Promise}
    425  */
    426 Zotero.DataObjects.prototype.loadDataTypes = Zotero.Promise.coroutine(function* (objects, dataTypes) {
    427 	if (!dataTypes) {
    428 		dataTypes = this.ObjectClass.prototype._dataTypes;
    429 	}
    430 	for (let dataType of dataTypes) {
    431 		let typeIDsByLibrary = {};
    432 		for (let obj of objects) {
    433 			if (obj._loaded[dataType]) {
    434 				continue;
    435 			}
    436 			if (!typeIDsByLibrary[obj.libraryID]) {
    437 				typeIDsByLibrary[obj.libraryID] = [];
    438 			}
    439 			typeIDsByLibrary[obj.libraryID].push(obj.id);
    440 		}
    441 		for (let libraryID in typeIDsByLibrary) {
    442 			yield this._loadDataTypeInLibrary(dataType, parseInt(libraryID), typeIDsByLibrary[libraryID]);
    443 		}
    444 	}
    445 });
    446 
    447 
    448 /**
    449  * Loads data for a given data type
    450  * @param {String} dataType
    451  * @param {Integer} libraryID
    452  * @param {Integer[]} [ids]
    453  */
    454 Zotero.DataObjects.prototype._loadDataTypeInLibrary = Zotero.Promise.coroutine(function* (dataType, libraryID, ids) {
    455 	var funcName = "_load" + dataType[0].toUpperCase() + dataType.substr(1)
    456 		// Single data types need an 's' (e.g., 'note' -> 'loadNotes()')
    457 		+ ((dataType.endsWith('s') || dataType.endsWith('Data') ? '' : 's'));
    458 	if (!this[funcName]) {
    459 		throw new Error(`Zotero.${this._ZDO_Objects}.${funcName} is not a function`);
    460 	}
    461 	
    462 	if (ids && ids.length == 0) {
    463 		return;
    464 	}
    465 	
    466 	var t = new Date;
    467 	var libraryName = Zotero.Libraries.get(libraryID).name;
    468 	
    469 	var idSQL = "";
    470 	if (ids) {
    471 		idSQL = " AND " + this.idColumn + " IN (" + ids.map(id => parseInt(id)).join(", ") + ")";
    472 	}
    473 	
    474 	Zotero.debug("Loading " + dataType + " for "
    475 		+ (ids
    476 			? ids.length + " " + (ids.length == 1 ? this._ZDO_object : this._ZDO_objects)
    477 			: this._ZDO_objects)
    478 		+ " in " + libraryName);
    479 	
    480 	yield this[funcName](libraryID, ids ? ids : [], idSQL);
    481 	
    482 	Zotero.debug(`Loaded ${dataType} in ${libraryName} in ${new Date() - t} ms`);
    483 });
    484 
    485 Zotero.DataObjects.prototype.loadAll = Zotero.Promise.coroutine(function* (libraryID, ids) {
    486 	var t = new Date();
    487 	var library = Zotero.Libraries.get(libraryID)
    488 	
    489 	Zotero.debug("Loading "
    490 		+ (ids ? ids.length : "all") + " "
    491 		+ (ids && ids.length == 1 ? this._ZDO_object : this._ZDO_objects)
    492 		+ " in " + library.name);
    493 	
    494 	if (!ids) {
    495 		library.setDataLoading(this._ZDO_object);
    496 	}
    497 	
    498 	let dataTypes = this.ObjectClass.prototype._dataTypes;
    499 	for (let i = 0; i < dataTypes.length; i++) {
    500 		yield this._loadDataTypeInLibrary(dataTypes[i], libraryID, ids);
    501 	}
    502 	
    503 	Zotero.debug(`Loaded ${this._ZDO_objects} in ${library.name} in ${new Date() - t} ms`);
    504 	
    505 	if (!ids) {
    506 		library.setDataLoaded(this._ZDO_object);
    507 	}
    508 });
    509 
    510 
    511 Zotero.DataObjects.prototype._loadPrimaryData = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL, options) {
    512 	var loaded = {};
    513 	
    514 	// If library isn't an integer (presumably false or null), skip it
    515 	if (parseInt(libraryID) != libraryID) {
    516 		libraryID = false;
    517 	}
    518 	
    519 	var sql = this.primaryDataSQL;
    520 	var params = [];
    521 	if (libraryID !== false) {
    522 		sql += ' AND O.libraryID=?';
    523 		params.push(libraryID);
    524 	}
    525 	if (ids.length) {
    526 		sql += ' AND O.' + this._ZDO_id + ' IN (' + ids.join(',') + ')';
    527 	}
    528 	
    529 	yield Zotero.DB.queryAsync(
    530 		sql,
    531 		params,
    532 		{
    533 			onRow: function (row) {
    534 				var id = row.getResultByName(this._ZDO_id);
    535 				var columns = Object.keys(this._primaryDataSQLParts);
    536 				var rowObj = {};
    537 				for (let i=0; i<columns.length; i++) {
    538 					rowObj[columns[i]] = row.getResultByIndex(i);
    539 				}
    540 				var obj;
    541 				
    542 				// Existing object -- reload in place
    543 				if (this._objectCache[id]) {
    544 					this._objectCache[id].loadFromRow(rowObj, true);
    545 					obj = this._objectCache[id];
    546 				}
    547 				// Object doesn't exist -- create new object and stuff in cache
    548 				else {
    549 					obj = this._getObjectForRow(rowObj);
    550 					obj.loadFromRow(rowObj, true);
    551 					if (!options || !options.noCache) {
    552 						this.registerObject(obj);
    553 					}
    554 				}
    555 				loaded[id] = obj;
    556 			}.bind(this)
    557 		}
    558 	);
    559 	
    560 	if (!ids) {
    561 		this._loadedLibraries[libraryID] = true;
    562 		
    563 		// If loading all objects, remove cached objects that no longer exist
    564 		for (let i in this._objectCache) {
    565 			let obj = this._objectCache[i];
    566 			if (libraryID !== false && obj.libraryID !== libraryID) {
    567 				continue;
    568 			}
    569 			if (!loaded[obj.id]) {
    570 				this.unload(obj.id);
    571 			}
    572 		}
    573 		
    574 		if (this._postLoad) {
    575 			this._postLoad(libraryID, ids);
    576 		}
    577 	}
    578 	
    579 	return loaded;
    580 });
    581 
    582 
    583 Zotero.DataObjects.prototype._loadRelations = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL) {
    584 	if (!this._relationsTable) {
    585 		throw new Error("Relations not supported for " + this._ZDO_objects);
    586 	}
    587 	
    588 	var sql = "SELECT " + this.idColumn + ", predicate, object "
    589 		+ `FROM ${this.table} LEFT JOIN ${this._relationsTable} USING (${this.idColumn}) `
    590 		+ "LEFT JOIN relationPredicates USING (predicateID) "
    591 		+ "WHERE libraryID=?" + idSQL;
    592 	var params = [libraryID];
    593 	
    594 	var lastID;
    595 	var rows = [];
    596 	var setRows = function (id, rows) {
    597 		var obj = this._objectCache[id];
    598 		if (!obj) {
    599 			throw new Error(this._ZDO_Object + " " + id + " not found");
    600 		}
    601 		
    602 		var relations = {};
    603 		function addRel(predicate, object) {
    604 			if (!relations[predicate]) {
    605 				relations[predicate] = [];
    606 			}
    607 			relations[predicate].push(object);
    608 		}
    609 		
    610 		for (let i = 0; i < rows.length; i++) {
    611 			let row = rows[i];
    612 			addRel(row.predicate, row.object);
    613 		}
    614 		
    615 		/*if (this._objectType == 'item') {
    616 			let getURI = Zotero.URI["get" + this._ObjectType + "URI"].bind(Zotero.URI);
    617 			let objectURI = getURI(this);
    618 			
    619 			// Related items are bidirectional, so include any pointing to this object
    620 			let objects = Zotero.Relations.getByPredicateAndObject(
    621 				Zotero.Relations.relatedItemPredicate, objectURI
    622 			);
    623 			for (let i = 0; i < objects.length; i++) {
    624 				addRel(Zotero.Relations.relatedItemPredicate, getURI(objects[i]));
    625 			}
    626 			
    627 			// Also include any owl:sameAs relations pointing to this object
    628 			objects = Zotero.Relations.getByPredicateAndObject(
    629 				Zotero.Relations.linkedObjectPredicate, objectURI
    630 			);
    631 			for (let i = 0; i < objects.length; i++) {
    632 				addRel(Zotero.Relations.linkedObjectPredicate, getURI(objects[i]));
    633 			}
    634 		}*/
    635 		
    636 		// Relations are stored as predicate-object pairs
    637 		obj._relations = this.flattenRelations(relations);
    638 		obj._loaded.relations = true;
    639 		obj._clearChanged('relations');
    640 	}.bind(this);
    641 	
    642 	yield Zotero.DB.queryAsync(
    643 		sql,
    644 		params,
    645 		{
    646 			noCache: ids.length != 1,
    647 			onRow: function (row) {
    648 				let id = row.getResultByIndex(0);
    649 				
    650 				if (lastID && id !== lastID) {
    651 					setRows(lastID, rows);
    652 					rows = [];
    653 				}
    654 				
    655 				lastID = id;
    656 				let predicate = row.getResultByIndex(1);
    657 				// No relations
    658 				if (predicate === null) {
    659 					return;
    660 				}
    661 				rows.push({
    662 					predicate,
    663 					object: row.getResultByIndex(2)
    664 				});
    665 			}.bind(this)
    666 		}
    667 	);
    668 	
    669 	if (lastID) {
    670 		setRows(lastID, rows);
    671 	}
    672 });
    673 
    674 
    675 /**
    676  * Flatten API JSON relations object into an array of unique predicate-object pairs
    677  *
    678  * @param {Object} relations - Relations object in API JSON format, with predicates as keys
    679  *                             and arrays of URIs as objects
    680  * @return {Array[]} - Predicate-object pairs
    681  */
    682 Zotero.DataObjects.prototype.flattenRelations = function (relations) {
    683 	var relationsFlat = [];
    684 	for (let predicate in relations) {
    685 		let object = relations[predicate];
    686 		if (Array.isArray(object)) {
    687 			object = Zotero.Utilities.arrayUnique(object);
    688 			for (let i = 0; i < object.length; i++) {
    689 				relationsFlat.push([predicate, object[i]]);
    690 			}
    691 		}
    692 		else if (typeof object == 'string') {
    693 			relationsFlat.push([predicate, object]);
    694 		}
    695 		else {
    696 			Zotero.debug(object, 1);
    697 			throw new Error("Invalid relation value");
    698 		}
    699 	}
    700 	return relationsFlat;
    701 }
    702 
    703 
    704 /**
    705  * Reload loaded data of loaded objects
    706  *
    707  * @param {Array|Number} ids - An id or array of ids
    708  * @param {Array} [dataTypes] - Data types to reload (e.g., 'primaryData'), or all loaded
    709  *                              types if not provided
    710    * @param {Boolean} [reloadUnchanged=false] - Reload even data that hasn't changed internally.
    711    *                                            This should be set to true for data that was
    712    *                                            changed externally (e.g., globally renamed tags).
    713    */
    714 Zotero.DataObjects.prototype.reload = Zotero.Promise.coroutine(function* (ids, dataTypes, reloadUnchanged) {
    715 	ids = Zotero.flattenArguments(ids);
    716 	
    717 	Zotero.debug('Reloading ' + (dataTypes ? '[' + dataTypes.join(', ') + '] for ' : '')
    718 		+ this._ZDO_objects + ' ' + ids);
    719 	
    720 	// If data types not specified, reload loaded data for each object individually.
    721 	// TODO: optimize
    722 	if (!dataTypes) {
    723 		for (let i=0; i<ids.length; i++) {
    724 			if (this._objectCache[ids[i]]) {
    725 				yield this._objectCache[ids[i]].reload(dataTypes, reloadUnchanged);
    726 			}
    727 		}
    728 		return;
    729 	}
    730 	
    731 	for (let dataType of dataTypes) {
    732 		let typeIDsByLibrary = {};
    733 		for (let id of ids) {
    734 			let obj = this._objectCache[id];
    735 			if (!obj || !obj._loaded[dataType] || obj._skipDataTypeLoad[dataType]
    736 					|| (!reloadUnchanged && !obj._changed[dataType])) {
    737 				continue;
    738 			}
    739 			if (!typeIDsByLibrary[obj.libraryID]) {
    740 				typeIDsByLibrary[obj.libraryID] = [];
    741 			}
    742 			typeIDsByLibrary[obj.libraryID].push(id);
    743 		}
    744 		for (let libraryID in typeIDsByLibrary) {
    745 			yield this._loadDataTypeInLibrary(dataType, parseInt(libraryID), typeIDsByLibrary[libraryID]);
    746 		}
    747 	}
    748 	
    749 	return true;
    750 });
    751 
    752 
    753 Zotero.DataObjects.prototype.reloadAll = function (libraryID) {
    754 	Zotero.debug("Reloading all " + this._ZDO_objects);
    755 	
    756 	// Remove objects not stored in database
    757 	var sql = "SELECT ROWID FROM " + this._ZDO_table;
    758 	var params = [];
    759 	if (libraryID !== undefined) {
    760 		sql += ' WHERE libraryID=?';
    761 		params.push(libraryID);
    762 	}
    763 	return Zotero.DB.columnQueryAsync(sql, params)
    764 	.then(function (ids) {
    765 		for (var id in this._objectCache) {
    766 			if (!ids || ids.indexOf(parseInt(id)) == -1) {
    767 				delete this._objectCache[id];
    768 			}
    769 		}
    770 		
    771 		// Reload data
    772 		this._loadedLibraries[libraryID] = false;
    773 		return this._load(libraryID);
    774 	});
    775 }
    776 
    777 
    778 Zotero.DataObjects.prototype.registerObject = function (obj) {
    779 	var id = obj.id;
    780 	var libraryID = obj.libraryID;
    781 	var key = obj.key;
    782 	
    783 	//Zotero.debug("Registering " + this._ZDO_object + " " + id + " as " + libraryID + "/" + key);
    784 	if (!this._objectIDs[libraryID]) {
    785 		this._objectIDs[libraryID] = {};
    786 	}
    787 	this._objectIDs[libraryID][key] = id;
    788 	this._objectKeys[id] = [libraryID, key];
    789 	this._objectCache[id] = obj;
    790 	obj._inCache = true;
    791 }
    792 
    793 Zotero.DataObjects.prototype.dropDeadObjectsFromCache = function() {
    794 	let ids = [];
    795 	for (let libraryID in this._objectIDs) {
    796 		if (Zotero.Libraries.exists(libraryID)) continue;
    797 		for (let key in this._objectIDs[libraryID]) {
    798 			ids.push(this._objectIDs[libraryID][key]);
    799 		}
    800 	}
    801 	
    802 	this.unload(ids);
    803 }
    804 
    805 /**
    806  * Clear object from internal array
    807  *
    808  * @param	int[]	ids		objectIDs
    809  */
    810 Zotero.DataObjects.prototype.unload = function () {
    811 	var ids = Zotero.flattenArguments(arguments);
    812 	for (var i=0; i<ids.length; i++) {
    813 		let id = ids[i];
    814 		let {libraryID, key} = this.getLibraryAndKeyFromID(id);
    815 		if (key) {
    816 			delete this._objectIDs[libraryID][key];
    817 			delete this._objectKeys[id];
    818 		}
    819 		delete this._objectCache[id];
    820 	}
    821 }
    822 
    823 
    824 /**
    825  * Set the version of objects, efficiently
    826  *
    827  * @param {Integer[]} ids - Ids of objects to update
    828  * @param {Boolean} version
    829  */
    830 Zotero.DataObjects.prototype.updateVersion = Zotero.Promise.method(function (ids, version) {
    831 	if (version != parseInt(version)) {
    832 		throw new Error("'version' must be an integer ('" + version + "' given)");
    833 	}
    834 	version = parseInt(version);
    835 	
    836 	let sql = "UPDATE " + this.table + " SET version=" + version + " "
    837 		+ "WHERE " + this.idColumn + " IN (";
    838 	return Zotero.Utilities.Internal.forEachChunkAsync(
    839 		ids,
    840 		Zotero.DB.MAX_BOUND_PARAMETERS,
    841 		Zotero.Promise.coroutine(function* (chunk) {
    842 			yield Zotero.DB.queryAsync(sql + chunk.map(() => '?').join(', ') + ')', chunk);
    843 			// Update the internal 'version' property of any loaded objects
    844 			for (let i = 0; i < chunk.length; i++) {
    845 				let id = chunk[i];
    846 				let obj = this._objectCache[id];
    847 				if (obj) {
    848 					obj.updateVersion(version, true);
    849 				}
    850 			}
    851 		}.bind(this))
    852 	);
    853 });
    854 
    855 
    856 /**
    857  * Set the sync state of objects, efficiently
    858  *
    859  * @param {Integer[]} ids - Ids of objects to update
    860  * @param {Boolean} synced
    861  */
    862 Zotero.DataObjects.prototype.updateSynced = Zotero.Promise.method(function (ids, synced) {
    863 	let sql = "UPDATE " + this.table + " SET synced=" + (synced ? 1 : 0) + " "
    864 		+ "WHERE " + this.idColumn + " IN (";
    865 	return Zotero.Utilities.Internal.forEachChunkAsync(
    866 		ids,
    867 		Zotero.DB.MAX_BOUND_PARAMETERS,
    868 		Zotero.Promise.coroutine(function* (chunk) {
    869 			yield Zotero.DB.queryAsync(sql + chunk.map(() => '?').join(', ') + ')', chunk);
    870 			// Update the internal 'synced' property of any loaded objects
    871 			for (let i = 0; i < chunk.length; i++) {
    872 				let id = chunk[i];
    873 				let obj = this._objectCache[id];
    874 				if (obj) {
    875 					obj.updateSynced(!!synced, true);
    876 				}
    877 			}
    878 		}.bind(this))
    879 	);
    880 });
    881 
    882 
    883 Zotero.DataObjects.prototype.isEditable = function (obj) {
    884 	var libraryID = obj.libraryID;
    885 	if (!libraryID) {
    886 		return true;
    887 	}
    888 	
    889 	if (!Zotero.Libraries.get(libraryID).editable) return false;
    890 	
    891 	if (obj.objectType == 'item' && obj.isAttachment()
    892 		&& (obj.attachmentLinkMode == Zotero.Attachments.LINK_MODE_IMPORTED_URL ||
    893 			obj.attachmentLinkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE)
    894 		&& !Zotero.Libraries.get(libraryID).filesEditable
    895 	) {
    896 		return false;
    897 	}
    898 	
    899 	return true;
    900 }
    901 
    902 Zotero.defineProperty(Zotero.DataObjects.prototype, "primaryDataSQL", {
    903 	get: function () {
    904 		return "SELECT "
    905 		+ Object.keys(this._primaryDataSQLParts).map((val) => this._primaryDataSQLParts[val]).join(', ')
    906 		+ this.primaryDataSQLFrom;
    907 	}
    908 }, {lazy: true});
    909 
    910 Zotero.DataObjects.prototype.getPrimaryDataSQLPart = function (part) {
    911 	var sql = this._primaryDataSQLParts[part];
    912 	if (!sql) {
    913 		throw new Error("Invalid primary data SQL part '" + part + "'");
    914 	}
    915 	return sql;
    916 }
    917 
    918 
    919 /**
    920  * Delete one or more objects from the database and caches
    921  *
    922  * @param {Integer|Integer[]} ids - Object ids
    923  * @param {Object} [options] - See Zotero.DataObject.prototype.erase
    924  * @param {Function} [options.onProgress] - f(progress, progressMax)
    925  * @return {Promise}
    926  */
    927 Zotero.DataObjects.prototype.erase = Zotero.Promise.coroutine(function* (ids, options = {}) {
    928 	ids = Zotero.flattenArguments(ids);
    929 	yield Zotero.DB.executeTransaction(function* () {
    930 		for (let i = 0; i < ids.length; i++) {
    931 			let obj = yield this.getAsync(ids[i]);
    932 			if (!obj) {
    933 				continue;
    934 			}
    935 			yield obj.erase(options);
    936 			if (options.onProgress) {
    937 				options.onProgress(i + 1, ids.length);
    938 			}
    939 		}
    940 		this.unload(ids);
    941 	}.bind(this));
    942 });
    943 
    944 
    945 // TEMP: remove
    946 Zotero.DataObjects.prototype._load = Zotero.Promise.coroutine(function* (libraryID, ids, options) {
    947 	var loaded = {};
    948 
    949 	// If library isn't an integer (presumably false or null), skip it
    950 	if (parseInt(libraryID) != libraryID) {
    951 		libraryID = false;
    952 	}
    953 
    954 	if (libraryID === false && !ids) {
    955 		throw new Error("Either libraryID or ids must be provided");
    956 	}
    957 
    958 	if (libraryID !== false && this._loadedLibraries[libraryID]) {
    959 		return loaded;
    960 	}
    961 
    962 	var sql = this.primaryDataSQL;
    963 	var params = [];
    964 	if (libraryID !== false) {
    965 		sql += ' AND O.libraryID=?';
    966 		params.push(libraryID);
    967 	}
    968 	if (ids) {
    969 		sql += ' AND O.' + this._ZDO_id + ' IN (' + ids.join(',') + ')';
    970 	}
    971 
    972 	var t = new Date();
    973 	yield Zotero.DB.queryAsync(
    974 		sql,
    975 		params,
    976 		{
    977 			onRow: function (row) {
    978 				var id = row.getResultByName(this._ZDO_id);
    979 				var columns = Object.keys(this._primaryDataSQLParts);
    980 				var rowObj = {};
    981 				for (let i=0; i<columns.length; i++) {
    982 					rowObj[columns[i]] = row.getResultByIndex(i);
    983 				}
    984 				var obj;
    985 
    986 				// Existing object -- reload in place
    987 				if (this._objectCache[id]) {
    988 					this._objectCache[id].loadFromRow(rowObj, true);
    989 					obj = this._objectCache[id];
    990 				}
    991 				// Object doesn't exist -- create new object and stuff in cache
    992 				else {
    993 					obj = this._getObjectForRow(rowObj);
    994 					obj.loadFromRow(rowObj, true);
    995 					if (!options || !options.noCache) {
    996 						this.registerObject(obj);
    997 					}
    998 				}
    999 				loaded[id] = obj;
   1000 			}.bind(this)
   1001 		}
   1002 	);
   1003 	Zotero.debug("Loaded " + this._ZDO_objects + " in " + ((new Date) - t) + "ms");
   1004 
   1005 	if (!ids) {
   1006 		this._loadedLibraries[libraryID] = true;
   1007 
   1008 		// If loading all objects, remove cached objects that no longer exist
   1009 		for (let i in this._objectCache) {
   1010 			let obj = this._objectCache[i];
   1011 			if (libraryID !== false && obj.libraryID !== libraryID) {
   1012 				continue;
   1013 			}
   1014 			if (!loaded[obj.id]) {
   1015 				this.unload(obj.id);
   1016 			}
   1017 		}
   1018 
   1019 		if (this._postLoad) {
   1020 			this._postLoad(libraryID, ids);
   1021 		}
   1022 	}
   1023 
   1024 	return loaded;
   1025 });
   1026 
   1027 
   1028 
   1029 Zotero.DataObjects.prototype._getObjectForRow = function(row) {
   1030 	return new Zotero[this._ZDO_Object];
   1031 };
   1032 
   1033 Zotero.DataObjects.prototype._loadIDsAndKeys = Zotero.Promise.coroutine(function* () {
   1034 	var sql = "SELECT ROWID AS id, libraryID, key FROM " + this._ZDO_table;
   1035 	var rows = yield Zotero.DB.queryAsync(sql);
   1036 	for (let i=0; i<rows.length; i++) {
   1037 		let row = rows[i];
   1038 		this._objectKeys[row.id] = [row.libraryID, row.key];
   1039 		if (!this._objectIDs[row.libraryID]) {
   1040 			this._objectIDs[row.libraryID] = {};
   1041 		}
   1042 		this._objectIDs[row.libraryID][row.key] = row.id;
   1043 	}
   1044 });