www

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

library.js (20350B)


      1 /*
      2     ***** BEGIN LICENSE BLOCK *****
      3     
      4     Copyright © 2015 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 Zotero.Library = function(params = {}) {
     27 	let objectType = this._objectType;
     28 	this._ObjectType = Zotero.Utilities.capitalize(objectType);
     29 	this._objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType);
     30 	this._ObjectTypePlural = Zotero.Utilities.capitalize(this._objectTypePlural);
     31 	
     32 	this._changed = {};
     33 	
     34 	this._dataLoaded = {};
     35 	this._dataLoadedDeferreds = {};
     36 	
     37 	this._hasCollections = null;
     38 	this._hasSearches = null;
     39 	this._storageDownloadNeeded = false;
     40 	
     41 	Zotero.Utilities.assignProps(
     42 		this,
     43 		params,
     44 		[
     45 			'libraryType',
     46 			'editable',
     47 			'filesEditable',
     48 			'libraryVersion',
     49 			'storageVersion',
     50 			'lastSync',
     51 			'archived'
     52 		]
     53 	);
     54 	
     55 	// Return a proxy so that we can disable the object once it's deleted
     56 	return new Proxy(this, {
     57 		get: function(obj, prop) {
     58 			if (obj._disabled && !(prop == 'libraryID' || prop == 'id')) {
     59 				throw new Error("Library (" + obj.libraryID + ") has been disabled");
     60 			}
     61 			return obj[prop];
     62 		}
     63 	});
     64 };
     65 
     66 /**
     67  * Non-prototype properties
     68  */
     69 // DB columns
     70 Zotero.defineProperty(Zotero.Library, '_dbColumns', {
     71 	value: Object.freeze([
     72 		'type', 'editable', 'filesEditable', 'version', 'storageVersion', 'lastSync', 'archived'
     73 	])
     74 });
     75 
     76 // Converts DB column name to (internal) object property
     77 Zotero.Library._colToProp = function(c) {
     78 	return "_library" + Zotero.Utilities.capitalize(c);
     79 }
     80 
     81 // Select all columns in a unique manner, so we can JOIN tables with same column names (e.g. version)
     82 Zotero.defineProperty(Zotero.Library, '_rowSQLSelect', {
     83 	value: "L.libraryID, " + Zotero.Library._dbColumns.map(c => "L." + c + " AS " + Zotero.Library._colToProp(c)).join(", ")
     84 		+ ", (SELECT COUNT(*)>0 FROM collections C WHERE C.libraryID=L.libraryID) AS hasCollections"
     85 		+ ", (SELECT COUNT(*)>0 FROM savedSearches S WHERE S.libraryID=L.libraryID) AS hasSearches"
     86 });
     87 
     88 // The actual select statement for above columns
     89 Zotero.defineProperty(Zotero.Library, '_rowSQL', {
     90 	value: "SELECT " + Zotero.Library._rowSQLSelect + " FROM libraries L"
     91 });
     92 
     93 /**
     94  * Prototype properties
     95  */
     96 Zotero.defineProperty(Zotero.Library.prototype, '_objectType', {
     97 	value: 'library'
     98 });
     99 
    100 Zotero.defineProperty(Zotero.Library.prototype, '_childObjectTypes', {
    101 	value: Object.freeze(['item', 'collection', 'search'])
    102 });
    103 
    104 // Valid library types
    105 Zotero.defineProperty(Zotero.Library.prototype, 'libraryTypes', {
    106 	value: Object.freeze(['user'])
    107 });
    108 
    109 // Immutable libraries
    110 Zotero.defineProperty(Zotero.Library.prototype, 'fixedLibraries', {
    111 	value: Object.freeze(['user'])
    112 });
    113 
    114 Zotero.defineProperty(Zotero.Library.prototype, 'libraryID', {
    115 	get: function() { return this._libraryID; },
    116 	set: function(id) { throw new Error("Cannot change library ID"); }
    117 });
    118 
    119 Zotero.defineProperty(Zotero.Library.prototype, 'id', {
    120 	get: function() { return this.libraryID; },
    121 	set: function(val) { return this.libraryID = val; }
    122 });
    123 
    124 Zotero.defineProperty(Zotero.Library.prototype, 'libraryType', {
    125 	get: function() { return this._get('_libraryType'); },
    126 	set: function(v) { return this._set('_libraryType', v); }
    127 });
    128 
    129 /**
    130  * Get the library-type-specific id for the library (e.g., userID for user library,
    131  * groupID for group library)
    132  *
    133  * @property
    134  */
    135 Zotero.defineProperty(Zotero.Library.prototype, 'libraryTypeID', {
    136 	get: function () {
    137 		switch (this._libraryType) {
    138 		case 'user':
    139 			return Zotero.Users.getCurrentUserID();
    140 		
    141 		case 'group':
    142 			return Zotero.Groups.getGroupIDFromLibraryID(this._libraryID);
    143 		
    144 		default:
    145 			throw new Error(`Tried to get library type id for ${this._libraryType} library`);
    146 		}
    147 	}
    148 });
    149 
    150 Zotero.defineProperty(Zotero.Library.prototype, 'libraryVersion', {
    151 	get: function() { return this._get('_libraryVersion'); },
    152 	set: function(v) { return this._set('_libraryVersion', v); }
    153 });
    154 
    155 
    156 Zotero.defineProperty(Zotero.Library.prototype, 'syncable', {
    157 	get: function () { return this._libraryType != 'feed'; }
    158 });
    159 
    160 
    161 Zotero.defineProperty(Zotero.Library.prototype, 'lastSync', {
    162 	get: function() { return this._get('_libraryLastSync'); }
    163 });
    164 
    165 
    166 Zotero.defineProperty(Zotero.Library.prototype, 'name', {
    167 	get: function() {
    168 		if (this._libraryType == 'user') {
    169 			return Zotero.getString('pane.collections.library');
    170 		}
    171 		
    172 		// This property is provided by the extending objects (Group, Feed) for other library types
    173 		throw new Error('Unhandled library type "' + this._libraryType + '"');
    174 	}
    175 });
    176 
    177 Zotero.defineProperty(Zotero.Library.prototype, 'treeViewID', {
    178 	get: function () {
    179 		return "L" + this._libraryID
    180 	}
    181 });
    182 
    183 Zotero.defineProperty(Zotero.Library.prototype, 'treeViewImage', {
    184 	get: function () {
    185 		return "chrome://zotero/skin/treesource-library" + Zotero.hiDPISuffix + ".png";
    186 	}
    187 });
    188 
    189 Zotero.defineProperty(Zotero.Library.prototype, 'hasTrash', {
    190 	value: true
    191 });
    192 
    193 Zotero.defineProperty(Zotero.Library.prototype, 'allowsLinkedFiles', {
    194 	value: true
    195 });
    196 
    197 // Create other accessors
    198 (function() {
    199 	let accessors = ['editable', 'filesEditable', 'storageVersion', 'archived'];
    200 	for (let i=0; i<accessors.length; i++) {
    201 		let prop = Zotero.Library._colToProp(accessors[i]);
    202 		Zotero.defineProperty(Zotero.Library.prototype, accessors[i], {
    203 			get: function() { return this._get(prop); },
    204 			set: function(v) { return this._set(prop, v); }
    205 		})
    206 	}
    207 })()
    208 
    209 Zotero.defineProperty(Zotero.Library.prototype, 'storageDownloadNeeded', {
    210 	get: function () { return this._storageDownloadNeeded; },
    211 	set: function (val) { this._storageDownloadNeeded = !!val; },
    212 })
    213 
    214 Zotero.Library.prototype._isValidProp = function(prop) {
    215 	let prefix = '_library';
    216 	if (prop.indexOf(prefix) !== 0 || prop.length == prefix.length) {
    217 		return false;
    218 	}
    219 	
    220 	let col = prop.substr(prefix.length);
    221 	col = col.charAt(0).toLowerCase() + col.substr(1);
    222 	
    223 	return Zotero.Library._dbColumns.indexOf(col) != -1;
    224 }
    225 
    226 Zotero.Library.prototype._get = function(prop) {
    227 	if (!this._isValidProp(prop)) {
    228 		throw new Error('Unknown property "' + prop + '"');
    229 	}
    230 	
    231 	return this[prop];
    232 }
    233 
    234 Zotero.Library.prototype._set = function(prop, val) {
    235 	if (!this._isValidProp(prop)) {
    236 		throw new Error('Unknown property "' + prop + '"');
    237 	}
    238 	
    239 	// Ensure proper format
    240 	switch(prop) {
    241 		case '_libraryType':
    242 			if (this.libraryTypes.indexOf(val) == -1) {
    243 				throw new Error('Invalid library type "' + val + '"');
    244 			}
    245 			
    246 			if (this.libraryID !== undefined) {
    247 				throw new Error("Library type cannot be changed for a saved library");
    248 			}
    249 			
    250 			if (this.fixedLibraries.indexOf(val) != -1) {
    251 				throw new Error('Cannot create library of type "' + val + '"');
    252 			}
    253 			break;
    254 		
    255 		case '_libraryEditable':
    256 		case '_libraryFilesEditable':
    257 			if (['user'].indexOf(this._libraryType) != -1) {
    258 				throw new Error('Cannot change ' + prop + ' for ' + this._libraryType + ' library');
    259 			}
    260 			val = !!val;
    261 			
    262 			// Setting 'editable' to false should also set 'filesEditable' to false
    263 			if (prop == '_libraryEditable' && !val) {
    264 				this._set('_libraryFilesEditable', false);
    265 			}
    266 			break;
    267 		
    268 		case '_libraryVersion':
    269 			var newVal = Number.parseInt(val, 10);
    270 			if (newVal != val) {
    271 				throw new Error(`${prop} must be an integer (${typeof val} '${val}' given)`);
    272 			}
    273 			val = newVal
    274 			
    275 			// Allow -1 to indicate that a full sync is needed
    276 			if (val < -1) throw new Error(prop + ' must not be less than -1');
    277 			
    278 			// Ensure that it is never decreasing, unless it is being set to -1
    279 			if (val != -1 && val < this._libraryVersion) throw new Error(prop + ' cannot decrease');
    280 			
    281 			break;
    282 		
    283 		case '_libraryStorageVersion':
    284 			var newVal = parseInt(val);
    285 			if (newVal != val) {
    286 				throw new Error(`${prop} must be an integer (${typeof val} '${val}' given)`);
    287 			}
    288 			val = newVal;
    289 			
    290 			// Ensure that it is never decreasing
    291 			if (val < this._libraryStorageVersion) throw new Error(prop + ' cannot decrease');
    292 			break;
    293 		
    294 		case '_libraryLastSync':
    295 			if (!val) {
    296 				val = false;
    297 			} else if (!(val instanceof Date)) {
    298 				throw new Error(prop + ' must be a Date object or falsy');
    299 			} else {
    300 				// Storing to DB will drop milliseconds, so, for consistency, we drop it now
    301 				val = new Date(Math.floor(val.getTime()/1000) * 1000);
    302 			}
    303 			break;
    304 		
    305 		case '_libraryArchived':
    306 			if (['user', 'feeds'].indexOf(this._libraryType) != -1) {
    307 				throw new Error('Cannot change ' + prop + ' for ' + this._libraryType + ' library');
    308 			}
    309 			if (val && this._libraryEditable) {
    310 				throw new Error('Cannot set editable library as archived');
    311 			}
    312 			val = !!val;
    313 			break;
    314 	}
    315 	
    316 	if (this[prop] == val) return; // Unchanged
    317 	
    318 	if (this._changed[prop]) {
    319 		// Catch attempts to re-set already set fields before saving
    320 		Zotero.debug('Warning: Attempting to set unsaved ' + this._objectType + ' property "' + prop + '"', 2, true);
    321 	}
    322 	
    323 	this._changed[prop] = true;
    324 	this[prop] = val;
    325 }
    326 
    327 Zotero.Library.prototype._loadDataFromRow = function(row) {
    328 	if (this._libraryID !== undefined && this._libraryID !== row.libraryID) {
    329 		Zotero.debug("Warning: library ID changed in Zotero.Library._loadDataFromRow", 2, true);
    330 	}
    331 	
    332 	this._libraryID = row.libraryID;
    333 	this._libraryType = row._libraryType;
    334 	
    335 	this._libraryEditable = !!row._libraryEditable;
    336 	this._libraryFilesEditable = !!row._libraryFilesEditable;
    337 	this._libraryVersion = row._libraryVersion;
    338 	this._libraryStorageVersion = row._libraryStorageVersion;
    339 	this._libraryLastSync =  row._libraryLastSync !== 0 ? new Date(row._libraryLastSync * 1000) : false;
    340 	this._libraryArchived = !!row._libraryArchived;
    341 	
    342 	this._hasCollections = !!row.hasCollections;
    343 	this._hasSearches = !!row.hasSearches;
    344 	
    345 	this._changed = {};
    346 }
    347 
    348 Zotero.Library.prototype._reloadFromDB = Zotero.Promise.coroutine(function* () {
    349 	let sql = Zotero.Library._rowSQL + ' WHERE libraryID=?';
    350 	let row = yield Zotero.DB.rowQueryAsync(sql, [this.libraryID]);
    351 	this._loadDataFromRow(row);
    352 });
    353 
    354 /**
    355  * Load object data in this library
    356  */
    357 Zotero.Library.prototype.loadAllDataTypes = Zotero.Promise.coroutine(function* () {
    358 	yield Zotero.SyncedSettings.loadAll(this.libraryID);
    359 	yield Zotero.Collections.loadAll(this.libraryID);
    360 	yield Zotero.Searches.loadAll(this.libraryID);
    361 	yield Zotero.Items.loadAll(this.libraryID);
    362 });
    363 
    364 //
    365 // Methods to handle promises that are resolved when object data is loaded for the library
    366 //
    367 Zotero.Library.prototype.getDataLoaded = function (objectType) {
    368 	return this._dataLoaded[objectType] || null;
    369 };
    370 
    371 Zotero.Library.prototype.setDataLoading = function (objectType) {
    372 	if (this._dataLoadedDeferreds[objectType]) {
    373 		throw new Error("Items already loading for library " + this.libraryID);
    374 	}
    375 	this._dataLoadedDeferreds[objectType] = Zotero.Promise.defer();
    376 };
    377 
    378 Zotero.Library.prototype.getDataLoadedPromise = function (objectType) {
    379 	return this._dataLoadedDeferreds[objectType]
    380 		? this._dataLoadedDeferreds[objectType].promise : null;
    381 };
    382 
    383 Zotero.Library.prototype.setDataLoaded = function (objectType) {
    384 	this._dataLoaded[objectType] = true;
    385 	this._dataLoadedDeferreds[objectType].resolve();
    386 };
    387 
    388 /**
    389  * Wait for a given data type to load, loading it now if necessary
    390  */
    391 Zotero.Library.prototype.waitForDataLoad = Zotero.Promise.coroutine(function* (objectType) {
    392 	if (this.getDataLoaded(objectType)) return;
    393 	
    394 	let promise = this.getDataLoadedPromise(objectType);
    395 	// If items are already being loaded, wait for them
    396 	if (promise) {
    397 		yield promise;
    398 	}
    399 	// Otherwise load them now
    400 	else {
    401 		let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType);
    402 		yield objectsClass.loadAll(this.libraryID);
    403 	}
    404 });
    405 
    406 Zotero.Library.prototype.isChildObjectAllowed = function(type) {
    407 	return this._childObjectTypes.indexOf(type) != -1;
    408 };
    409 
    410 Zotero.Library.prototype.updateLastSyncTime = function() {
    411 	this._set('_libraryLastSync', new Date());
    412 };
    413 
    414 Zotero.Library.prototype.saveTx = function(options) {
    415 	options = options || {};
    416 	options.tx = true;
    417 	return this.save(options);
    418 }
    419 
    420 Zotero.Library.prototype.save = Zotero.Promise.coroutine(function* (options) {
    421 	options = options || {};
    422 	var env = {
    423 		options: options,
    424 		transactionOptions: options.transactionOptions || {}
    425 	};
    426 	
    427 	if (!env.options.tx && !Zotero.DB.inTransaction()) {
    428 		Zotero.logError("save() called on Zotero.Library without a wrapping "
    429 			+ "transaction -- use saveTx() instead", 2, true);
    430 		env.options.tx = true;
    431 	}
    432 	
    433 	var proceed = yield this._initSave(env)
    434 		.catch(Zotero.Promise.coroutine(function* (e) {
    435 			if (!env.isNew && Zotero.Libraries.exists(this.libraryID)) {
    436 				// Reload from DB and reset this._changed, so this is not a permanent failure
    437 				yield this._reloadFromDB();
    438 			}
    439 			throw e;
    440 		}).bind(this));
    441 	
    442 	if (!proceed) return false;
    443 	
    444 	if (env.isNew) {
    445 		Zotero.debug('Saving data for new ' + this._objectType + ' to database', 4);
    446 	}
    447 	else {
    448 		Zotero.debug('Updating database with new ' + this._objectType + ' data', 4);
    449 	}
    450 	
    451 	try {
    452 		env.notifierData = {};
    453 		if (env.options.skipSelect) {
    454 			env.notifierData.skipSelect = true;
    455 		}
    456 		
    457 		// Create transaction
    458 		if (env.options.tx) {
    459 			return Zotero.DB.executeTransaction(function* () {
    460 				yield this._saveData(env);
    461 				yield this._finalizeSave(env);
    462 			}.bind(this), env.transactionOptions);
    463 		}
    464 		// Use existing transaction
    465 		else {
    466 			Zotero.DB.requireTransaction();
    467 			yield this._saveData(env);
    468 			yield this._finalizeSave(env);
    469 		}
    470 	} catch(e) {
    471 		Zotero.debug(e, 1);
    472 		throw e;
    473 	}
    474 });
    475 
    476 Zotero.Library.prototype._initSave = Zotero.Promise.method(function(env) {
    477 	if (this._libraryID === undefined) {
    478 		env.isNew = true;
    479 		
    480 		if (!this._libraryType) {
    481 			throw new Error("libraryType must be set before saving");
    482 		}
    483 		
    484 		if (typeof this._libraryEditable != 'boolean') {
    485 			throw new Error("editable must be set before saving");
    486 		}
    487 		
    488 		if (typeof this._libraryFilesEditable != 'boolean') {
    489 			throw new Error("filesEditable must be set before saving");
    490 		}
    491 	} else {
    492 		Zotero.Libraries._ensureExists(this._libraryID);
    493 		
    494 		if (!Object.keys(this._changed).length) {
    495 			Zotero.debug(`No data changed in ${this._objectType} ${this.id} -- not saving`, 4);
    496 			return false;
    497 		}
    498 	}
    499 	
    500 	return true;
    501 });
    502 
    503 Zotero.Library.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
    504 	// Collect changed columns
    505 	let changedCols = [],
    506 		params = [];
    507 	for (let i=0; i<Zotero.Library._dbColumns.length; i++) {
    508 		let col = Zotero.Library._dbColumns[i];
    509 		let prop = Zotero.Library._colToProp(col);
    510 		
    511 		if (this._changed[prop]) {
    512 			changedCols.push(col);
    513 			
    514 			let val = this[prop];
    515 			if (col == 'lastSync') {
    516 				// convert to integer
    517 				val = val ? Math.floor(val.getTime() / 1000) : 0;
    518 			}
    519 			else if (typeof val == 'boolean') {
    520 				val = val ? 1 : 0;
    521 			}
    522 			
    523 			params.push(val);
    524 		}
    525 	}
    526 	
    527 	if (env.isNew) {
    528 		let id = Zotero.ID.get('libraries');
    529 		changedCols.unshift('libraryID');
    530 		params.unshift(id);
    531 		
    532 		let sql = "INSERT INTO libraries (" + changedCols.join(", ") + ") "
    533 			+ "VALUES (" + Array(params.length).fill("?").join(", ") + ")";
    534 		yield Zotero.DB.queryAsync(sql, params);
    535 		
    536 		this._libraryID = id;
    537 	} else if (changedCols.length) {
    538 		params.push(this.libraryID);
    539 		let sql = "UPDATE libraries SET " + changedCols.map(v => v + "=?").join(", ")
    540 			+ " WHERE libraryID=?";
    541 		yield Zotero.DB.queryAsync(sql, params);
    542 		
    543 		// Since these are Zotero.Library properties, the 'modify' for the inheriting object may not
    544 		// get triggered, so call it here too
    545 		if (!env.options.skipNotifier && this.libraryType != 'user') {
    546 			Zotero.Notifier.queue('modify', this.libraryType, this.libraryTypeID);
    547 		}
    548 	} else {
    549 		Zotero.debug("Library data did not change for " + this._objectType + " " + this.id, 5);
    550 	}
    551 });
    552 
    553 Zotero.Library.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) {
    554 	this._changed = {};
    555 	
    556 	if (env.isNew) {
    557 		// Re-fetch from DB to get auto-filled defaults
    558 		yield this._reloadFromDB();
    559 		
    560 		Zotero.Libraries.register(this);
    561 		
    562 		yield this.loadAllDataTypes();
    563 	}
    564 });
    565 
    566 Zotero.Library.prototype.eraseTx = function(options) {
    567 	options = options || {};
    568 	options.tx = true;
    569 	return this.erase(options);
    570 };
    571 
    572 Zotero.Library.prototype.erase = Zotero.Promise.coroutine(function* (options) {
    573 	options = options || {};
    574 	var env = {
    575 		options: options,
    576 		transactionOptions: options.transactionOptions || {}
    577 	};
    578 	
    579 	if (!env.options.tx && !Zotero.DB.inTransaction()) {
    580 		Zotero.logError("erase() called on Zotero." + this._ObjectType + " without a wrapping "
    581 			+ "transaction -- use eraseTx() instead");
    582 		Zotero.debug((new Error).stack, 2);
    583 		env.options.tx = true;
    584 	}
    585 	
    586 	var proceed = yield this._initErase(env);
    587 	if (!proceed) return false;
    588 	
    589 	Zotero.debug('Deleting ' + this._objectType + ' ' + this.id);
    590 	
    591 	try {
    592 		env.notifierData = {};
    593 		
    594 		if (env.options.tx) {
    595 			yield Zotero.DB.executeTransaction(function* () {
    596 				yield this._eraseData(env);
    597 				yield this._finalizeErase(env);
    598 			}.bind(this), env.transactionOptions);
    599 		} else {
    600 			Zotero.DB.requireTransaction();
    601 			yield this._eraseData(env);
    602 			yield this._finalizeErase(env);
    603 		}
    604 	} catch(e) {
    605 		Zotero.debug(e, 1);
    606 		throw e;
    607 	}
    608 });
    609 
    610 Zotero.Library.prototype._initErase = Zotero.Promise.method(function(env) {
    611 	if (this.libraryID === undefined) {
    612 		throw new Error("Attempting to erase an unsaved library");
    613 	}
    614 	
    615 	Zotero.Libraries._ensureExists(this.libraryID);
    616 	
    617 	if (this.fixedLibraries.indexOf(this._libraryType) != -1) {
    618 		throw new Error("Cannot erase library of type '" + this._libraryType + "'");
    619 	}
    620 	
    621 	return true;
    622 });
    623 
    624 Zotero.Library.prototype._eraseData = Zotero.Promise.coroutine(function* (env) {
    625 	yield Zotero.DB.queryAsync("DELETE FROM libraries WHERE libraryID=?", this.libraryID);
    626 	// TODO: Emit event so this doesn't have to be here
    627 	yield Zotero.Fulltext.clearLibraryVersion(this.libraryID);
    628 });
    629 
    630 Zotero.Library.prototype._finalizeErase = Zotero.Promise.coroutine(function* (env) {
    631 	Zotero.Libraries.unregister(this.libraryID);
    632 	
    633 	// Clear cached child objects
    634 	for (let i=0; i<this._childObjectTypes.length; i++) {
    635 		let type = this._childObjectTypes[i];
    636 		Zotero.DataObjectUtilities.getObjectsClassForObjectType(type)
    637 			.dropDeadObjectsFromCache();
    638 	}
    639 	
    640 	this._disabled = true;
    641 });
    642 
    643 Zotero.Library.prototype.hasCollections = function () {
    644 	if (this._hasCollections === null) {
    645 		throw new Error("Collection data has not been loaded");
    646 	}
    647 	
    648 	return this._hasCollections;
    649 }
    650 
    651 Zotero.Library.prototype.updateCollections = Zotero.Promise.coroutine(function* () {
    652 	let sql = 'SELECT COUNT(*)>0 FROM collections WHERE libraryID=?';
    653 	this._hasCollections = !!(yield Zotero.DB.valueQueryAsync(sql, this.libraryID));
    654 });
    655 
    656 Zotero.Library.prototype.hasSearches = function () {
    657 	if (this._hasSearches === null) {
    658 		throw new Error("Saved search data has not been loaded");
    659 	}
    660 	
    661 	return this._hasSearches;
    662 }
    663 
    664 Zotero.Library.prototype.updateSearches = Zotero.Promise.coroutine(function* () {
    665 	let sql = 'SELECT COUNT(*)>0 FROM savedSearches WHERE libraryID=?';
    666 	this._hasSearches = !!(yield Zotero.DB.valueQueryAsync(sql, this.libraryID));
    667 });
    668 
    669 Zotero.Library.prototype.hasItems = Zotero.Promise.coroutine(function* () {
    670 	if (!this.id) {
    671 		throw new Error("Library is not saved yet");
    672 	}
    673 	let sql = 'SELECT COUNT(*)>0 FROM items WHERE libraryID=?';
    674 	// Don't count old <=4.0 Quick Start Guide items
    675 	if (this.libraryID == Zotero.Libraries.userLibraryID) {
    676 		sql += "AND key NOT IN ('ABCD2345', 'ABCD3456')";
    677 	}
    678 	return !!(yield Zotero.DB.valueQueryAsync(sql, this.libraryID));
    679 });
    680 
    681 Zotero.Library.prototype.hasItem = function (item) {
    682 	if (!(item instanceof Zotero.Item)) {
    683 		throw new Error("item must be a Zotero.Item");
    684 	}
    685 	return item.libraryID == this.libraryID;
    686 }