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 });