dataObject.js (36758B)
1 /* 2 ***** BEGIN LICENSE BLOCK ***** 3 4 Copyright © 2013 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 * @property {String} (readOnly) objectType 28 * @property {String} (readOnly) libraryKey 29 * @property {String|false|undefined} parentKey - False if no parent, or undefined if not 30 * applicable (e.g. search objects) 31 * @property {Integer|false|undefined} parentID - False if no parent, or undefined if not 32 * applicable (e.g. search objects) 33 */ 34 35 Zotero.DataObject = function () { 36 let objectType = this._objectType; 37 this._ObjectType = objectType[0].toUpperCase() + objectType.substr(1); 38 this._objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType); 39 this._ObjectTypePlural = this._objectTypePlural[0].toUpperCase() + this._objectTypePlural.substr(1); 40 this._ObjectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType); 41 42 this._id = null; 43 this._libraryID = null; 44 this._key = null; 45 this._dateAdded = null; 46 this._dateModified = null; 47 this._version = null; 48 this._synced = null; 49 this._identified = false; 50 this._parentID = null; 51 this._parentKey = null; 52 53 this._relations = []; 54 55 // Set in dataObjects.js 56 this._inCache = false; 57 58 this._loaded = {}; 59 this._skipDataTypeLoad = {}; 60 this._markAllDataTypeLoadStates(false); 61 62 this._clearChanged(); 63 }; 64 65 Zotero.DataObject.prototype._objectType = 'dataObject'; 66 Zotero.DataObject.prototype._dataTypes = ['primaryData']; 67 68 Zotero.defineProperty(Zotero.DataObject.prototype, 'objectType', { 69 get: function() { return this._objectType; } 70 }); 71 Zotero.defineProperty(Zotero.DataObject.prototype, 'id', { 72 get: function() { return this._id; } 73 }); 74 Zotero.defineProperty(Zotero.DataObject.prototype, 'libraryID', { 75 get: function() { return this._libraryID; } 76 }); 77 Zotero.defineProperty(Zotero.DataObject.prototype, 'library', { 78 get: function () { 79 return Zotero.Libraries.get(this._libraryID); 80 } 81 }); 82 Zotero.defineProperty(Zotero.DataObject.prototype, 'key', { 83 get: function() { return this._key; } 84 }); 85 Zotero.defineProperty(Zotero.DataObject.prototype, 'libraryKey', { 86 get: function() { return this._libraryID + "/" + this._key; } 87 }); 88 Zotero.defineProperty(Zotero.DataObject.prototype, 'parentKey', { 89 get: function () { return this._getParentKey(); }, 90 set: function(v) { return this._setParentKey(v); } 91 }); 92 Zotero.defineProperty(Zotero.DataObject.prototype, 'parentID', { 93 get: function() { return this._getParentID(); }, 94 set: function(v) { return this._setParentID(v); } 95 }); 96 97 Zotero.defineProperty(Zotero.DataObject.prototype, '_canHaveParent', { 98 value: true 99 }); 100 101 Zotero.defineProperty(Zotero.DataObject.prototype, 'ObjectsClass', { 102 get: function() { return this._ObjectsClass; } 103 }); 104 105 106 Zotero.DataObject.prototype._get = function (field) { 107 if (field != 'id') this._disabledCheck(); 108 109 if (this['_' + field] !== null) { 110 return this['_' + field]; 111 } 112 if (field != 'libraryID' && field != 'key' && field != 'id') { 113 this._requireData('primaryData'); 114 } 115 return null; 116 } 117 118 119 Zotero.DataObject.prototype._set = function (field, value) { 120 this._disabledCheck(); 121 122 if (field == 'id' || field == 'libraryID' || field == 'key') { 123 return this._setIdentifier(field, value); 124 } 125 126 this._requireData('primaryData'); 127 128 switch (field) { 129 case 'name': 130 value = value.trim().normalize(); 131 break; 132 133 case 'version': 134 value = parseInt(value); 135 break; 136 137 case 'synced': 138 value = !!value; 139 break; 140 } 141 142 if (this['_' + field] != value || field == 'synced') { 143 this._markFieldChange(field, this['_' + field]); 144 if (!this._changed.primaryData) { 145 this._changed.primaryData = {}; 146 } 147 this._changed.primaryData[field] = true; 148 149 switch (field) { 150 default: 151 this['_' + field] = value; 152 } 153 } 154 } 155 156 157 Zotero.DataObject.prototype._setIdentifier = function (field, value) { 158 switch (field) { 159 case 'id': 160 value = Zotero.DataObjectUtilities.checkDataID(value); 161 if (this._id) { 162 if (value === this._id) { 163 return; 164 } 165 throw new Error("ID cannot be changed"); 166 } 167 if (this._key) { 168 throw new Error("Cannot set id if key is already set"); 169 } 170 break; 171 172 case 'libraryID': 173 value = Zotero.DataObjectUtilities.checkLibraryID(value); 174 break; 175 176 case 'key': 177 if (this._libraryID === null) { 178 throw new Error("libraryID must be set before key"); 179 } 180 value = Zotero.DataObjectUtilities.checkKey(value); 181 if (this._key) { 182 if (value === this._key) { 183 return; 184 } 185 throw new Error("Key cannot be changed"); 186 } 187 if (this._id) { 188 throw new Error("Cannot set key if id is already set"); 189 } 190 } 191 192 if (value === this['_' + field]) { 193 return; 194 } 195 196 // If primary data is loaded, the only allowed identifier change is libraryID, and then only 197 // for unidentified objects, and then only either if a libraryID isn't yet set (because 198 // primary data gets marked as loaded when fields are set for new items, but some methods 199 // (setCollections(), save()) automatically set the user library ID after that if none is 200 // specified), or for searches (for the sake of the library switcher in the advanced search 201 // window, though that could probably be rewritten) 202 if (this._loaded.primaryData) { 203 if (!(!this._identified && field == 'libraryID' 204 && (!this._libraryID || this._objectType == 'search'))) { 205 throw new Error("Cannot change " + field + " after object is already loaded"); 206 } 207 } 208 209 if (field == 'id' || field == 'key') { 210 this._identified = true; 211 } 212 213 this['_' + field] = value; 214 } 215 216 217 /** 218 * Get the id of the parent object 219 * 220 * @return {Integer|false|undefined} The id of the parent object, false if none, or undefined 221 * on object types to which it doesn't apply (e.g., searches) 222 */ 223 Zotero.DataObject.prototype._getParentID = function () { 224 if (this._parentID !== null) { 225 return this._parentID; 226 } 227 if (!this._parentKey) { 228 if (this._objectType == 'search') { 229 return undefined; 230 } 231 return false; 232 } 233 return this._parentID = this.ObjectsClass.getIDFromLibraryAndKey(this._libraryID, this._parentKey); 234 } 235 236 237 /** 238 * Set the id of the parent object 239 * 240 * @param {Number|false} [id=false] 241 * @return {Boolean} True if changed, false if stayed the same 242 */ 243 Zotero.DataObject.prototype._setParentID = function (id) { 244 return this._setParentKey( 245 id 246 ? this.ObjectsClass.getLibraryAndKeyFromID(Zotero.DataObjectUtilities.checkDataID(id)).key 247 : false 248 ); 249 } 250 251 252 Zotero.DataObject.prototype._getParentKey = function () { 253 if (!this._canHaveParent) { 254 return undefined; 255 } 256 return this._parentKey ? this._parentKey : false 257 } 258 259 /** 260 * Set the key of the parent object 261 * 262 * @param {String|false} [key=false] 263 * @return {Boolean} True if changed, false if stayed the same 264 */ 265 Zotero.DataObject.prototype._setParentKey = function(key) { 266 if (!this._canHaveParent) { 267 throw new Error("Cannot set parent key for " + this._objectType); 268 } 269 270 key = Zotero.DataObjectUtilities.checkKey(key) || false; 271 272 if (key === this._parentKey || (!this._parentKey && !key)) { 273 return false; 274 } 275 this._markFieldChange('parentKey', this._parentKey); 276 this._changed.parentKey = true; 277 this._parentKey = key; 278 this._parentID = null; 279 return true; 280 } 281 282 // 283 // Relations 284 // 285 /** 286 * Returns all relations of the object 287 * 288 * @return {Object} - Object with predicates as keys and arrays of values 289 */ 290 Zotero.DataObject.prototype.getRelations = function () { 291 this._requireData('relations'); 292 293 var relations = {}; 294 for (let i=0; i<this._relations.length; i++) { 295 let rel = this._relations[i]; 296 // Relations are stored internally as predicate-object pairs 297 let p = rel[0]; 298 if (!relations[p]) { 299 relations[p] = []; 300 } 301 relations[p].push(rel[1]); 302 } 303 return relations; 304 } 305 306 307 /** 308 * Returns all relations of the object with a given predicate 309 * 310 * @return {String[]} - URIs linked to this object with the given predicate 311 */ 312 Zotero.DataObject.prototype.getRelationsByPredicate = function (predicate) { 313 this._requireData('relations'); 314 315 if (!predicate) { 316 throw new Error("Predicate not provided"); 317 } 318 319 var relations = []; 320 for (let i=0; i<this._relations.length; i++) { 321 let rel = this._relations[i]; 322 // Relations are stored internally as predicate-object pairs 323 let p = rel[0]; 324 if (p !== predicate) { 325 continue; 326 } 327 relations.push(rel[1]); 328 } 329 return relations; 330 } 331 332 333 /** 334 * @return {Boolean} - True if the relation has been queued, false if it already exists 335 */ 336 Zotero.DataObject.prototype.addRelation = function (predicate, object) { 337 this._requireData('relations'); 338 339 if (!predicate) { 340 throw new Error("Predicate not provided"); 341 } 342 if (!object) { 343 throw new Error("Object not provided"); 344 } 345 346 for (let i = 0; i < this._relations.length; i++) { 347 let rel = this._relations[i]; 348 if (rel[0] == predicate && rel[1] == object) { 349 Zotero.debug("Relation " + predicate + " - " + object + " already exists for " 350 + this._objectType + " " + this.libraryKey); 351 return false; 352 } 353 } 354 355 this._markFieldChange('relations', this._relations); 356 this._changed.relations = true; 357 this._relations.push([predicate, object]); 358 return true; 359 } 360 361 362 Zotero.DataObject.prototype.hasRelation = function (predicate, object) { 363 this._requireData('relations'); 364 365 for (let i = 0; i < this._relations.length; i++) { 366 let rel = this._relations[i]; 367 if (rel[0] == predicate && rel[1] == object) { 368 return true 369 } 370 } 371 return false; 372 } 373 374 375 Zotero.DataObject.prototype.removeRelation = function (predicate, object) { 376 this._requireData('relations'); 377 378 for (let i = 0; i < this._relations.length; i++) { 379 let rel = this._relations[i]; 380 if (rel[0] == predicate && rel[1] == object) { 381 Zotero.debug("Removing relation " + predicate + " - " + object + " from " 382 + this._objectType + " " + this.libraryKey); 383 this._markFieldChange('relations', this._relations); 384 this._changed.relations = true; 385 this._relations.splice(i, 1); 386 return true; 387 } 388 } 389 390 Zotero.debug("Relation " + predicate + " - " + object + " did not exist for " 391 + this._objectType + " " + this.libraryKey); 392 return false; 393 } 394 395 396 /** 397 * Updates the object's relations 398 * 399 * @param {Object} newRelations Object with predicates as keys and URI[] as values 400 * @return {Boolean} True if changed, false if stayed the same 401 */ 402 Zotero.DataObject.prototype.setRelations = function (newRelations) { 403 this._requireData('relations'); 404 405 if (typeof newRelations != 'object') { 406 throw new Error(`Relations must be an object (${typeof newRelations} given)`); 407 } 408 409 var oldRelations = this._relations; 410 411 // Limit predicates to letters and colons for now 412 for (let p in newRelations) { 413 if (!/^[a-z]+:[a-z]+$/i.test(p)) { 414 throw new Error(`Invalid relation predicate '${p}'`); 415 } 416 } 417 418 // Relations are stored internally as a flat array with individual predicate-object pairs, 419 // so convert the incoming relations to that 420 var newRelationsFlat = this.ObjectsClass.flattenRelations(newRelations); 421 422 var changed = false; 423 if (oldRelations.length != newRelationsFlat.length) { 424 changed = true; 425 } 426 else { 427 let sortFunc = function (a, b) { 428 if (a[0] < b[0]) return -1; 429 if (a[0] > b[0]) return 1; 430 if (a[1] < b[1]) return -1; 431 if (a[1] > b[1]) return 1; 432 return 0; 433 }; 434 oldRelations.sort(sortFunc); 435 newRelationsFlat.sort(sortFunc); 436 437 for (let i=0; i<oldRelations.length; i++) { 438 if (oldRelations[i][0] != newRelationsFlat[i][0] 439 || oldRelations[i][1] != newRelationsFlat[i][1]) { 440 changed = true; 441 break; 442 } 443 } 444 } 445 446 if (!changed) { 447 Zotero.debug("Relations have not changed for " + this._objectType + " " + this.libraryKey, 4); 448 return false; 449 } 450 451 this._markFieldChange('relations', this._relations); 452 this._changed.relations = true; 453 this._relations = newRelationsFlat; 454 return true; 455 } 456 457 458 /** 459 * Return an object in the specified library equivalent to this object 460 * 461 * Use Zotero.Collection.getLinkedCollection() and Zotero.Item.getLinkedItem() instead of 462 * calling this directly. 463 * 464 * @param {Integer} [libraryID] 465 * @return {Promise<Zotero.DataObject|false>} Linked object, or false if not found 466 */ 467 Zotero.DataObject.prototype._getLinkedObject = Zotero.Promise.coroutine(function* (libraryID, bidirectional) { 468 if (!libraryID) { 469 throw new Error("libraryID not provided"); 470 } 471 472 if (libraryID == this._libraryID) { 473 throw new Error(this._ObjectType + " is already in library " + libraryID); 474 } 475 476 var predicate = Zotero.Relations.linkedObjectPredicate; 477 var libraryObjectPrefix = Zotero.URI.getLibraryURI(libraryID) 478 + "/" + this._objectTypePlural + "/"; 479 480 // Try the relations with this as a subject 481 var uris = this.getRelationsByPredicate(predicate); 482 for (let i = 0; i < uris.length; i++) { 483 let uri = uris[i]; 484 if (uri.startsWith(libraryObjectPrefix)) { 485 let obj = yield Zotero.URI['getURI' + this._ObjectType](uri); 486 if (!obj) { 487 Zotero.debug("Referenced linked " + this._objectType + " '" + uri + "' not found " 488 + "in Zotero." + this._ObjectType + "::getLinked" + this._ObjectType + "()", 2); 489 continue; 490 } 491 return obj; 492 } 493 } 494 495 // Then try relations with this as an object 496 if (bidirectional) { 497 var thisURI = Zotero.URI['get' + this._ObjectType + 'URI'](this); 498 var objects = Zotero.Relations.getByPredicateAndObject( 499 this._objectType, predicate, thisURI 500 ); 501 for (let i = 0; i < objects.length; i++) { 502 let obj = objects[i]; 503 if (obj.objectType != this._objectType) { 504 Zotero.logError("Found linked object of different type " 505 + "(expected " + this._objectType + ", found " + obj.objectType + ")"); 506 continue; 507 } 508 if (obj.libraryID == libraryID) { 509 return obj; 510 } 511 } 512 } 513 514 return false; 515 }); 516 517 518 /** 519 * Add a linked-item relation to a pair of objects 520 * 521 * A separate save() is not required. 522 * 523 * @param {Zotero.DataObject} object 524 * @param {Promise<Boolean>} 525 */ 526 Zotero.DataObject.prototype._addLinkedObject = Zotero.Promise.coroutine(function* (object) { 527 if (object.libraryID == this._libraryID) { 528 throw new Error("Can't add linked " + this._objectType + " in same library"); 529 } 530 531 var predicate = Zotero.Relations.linkedObjectPredicate; 532 var thisURI = Zotero.URI['get' + this._ObjectType + 'URI'](this); 533 var objectURI = Zotero.URI['get' + this._ObjectType + 'URI'](object); 534 535 var exists = this.hasRelation(predicate, objectURI); 536 if (exists) { 537 Zotero.debug(this._ObjectTypePlural + " " + this.libraryKey 538 + " and " + object.libraryKey + " are already linked"); 539 return false; 540 } 541 542 // If one of the items is a personal library, store relation with that. Otherwise, use 543 // current item's library (which in calling code is the new, copied item, since that's what 544 // the user definitely has access to). 545 var userLibraryID = Zotero.Libraries.userLibraryID; 546 if (this.libraryID == userLibraryID || object.libraryID != userLibraryID) { 547 this.addRelation(predicate, objectURI); 548 yield this.save({ 549 skipDateModifiedUpdate: true, 550 skipSelect: true 551 }); 552 } 553 else { 554 object.addRelation(predicate, thisURI); 555 yield object.save({ 556 skipDateModifiedUpdate: true, 557 skipSelect: true 558 }); 559 } 560 561 return true; 562 }); 563 564 565 // 566 // Bulk data loading functions 567 // 568 // These are called by Zotero.DataObjects.prototype.loadDataType(). 569 // 570 Zotero.DataObject.prototype.loadPrimaryData = Zotero.Promise.coroutine(function* (reload, failOnMissing) { 571 if (this._loaded.primaryData && !reload) return; 572 573 var id = this._id; 574 var key = this._key; 575 var libraryID = this._libraryID; 576 577 if (!id && !key) { 578 throw new Error('ID or key not set in Zotero.' + this._ObjectType + '.loadPrimaryData()'); 579 } 580 581 var columns = [], join = [], where = []; 582 var primaryFields = this.ObjectsClass.primaryFields; 583 var idField = this.ObjectsClass.idColumn; 584 for (let i=0; i<primaryFields.length; i++) { 585 let field = primaryFields[i]; 586 // If field not already set 587 if (field == idField || this['_' + field] === null || reload) { 588 columns.push(this.ObjectsClass.getPrimaryDataSQLPart(field)); 589 } 590 } 591 if (!columns.length) { 592 return; 593 } 594 595 // This should match Zotero.*.primaryDataSQL, but without 596 // necessarily including all columns 597 var sql = "SELECT " + columns.join(", ") + this.ObjectsClass.primaryDataSQLFrom; 598 if (id) { 599 sql += " AND O." + idField + "=? "; 600 var params = id; 601 } 602 else { 603 sql += " AND O.key=? AND O.libraryID=? "; 604 var params = [key, libraryID]; 605 } 606 sql += (where.length ? ' AND ' + where.join(' AND ') : ''); 607 var row = yield Zotero.DB.rowQueryAsync(sql, params); 608 609 if (!row) { 610 if (failOnMissing) { 611 throw new Error(this._ObjectType + " " + (id ? id : libraryID + "/" + key) 612 + " not found in Zotero." + this._ObjectType + ".loadPrimaryData()"); 613 } 614 this._clearChanged('primaryData'); 615 616 // If object doesn't exist, mark all data types as loaded 617 this._markAllDataTypeLoadStates(true); 618 619 return; 620 } 621 622 this.loadFromRow(row, reload); 623 }); 624 625 626 /** 627 * Reloads loaded, changed data 628 * 629 * @param {String[]} [dataTypes] - Data types to reload, or all loaded types if not provide 630 * @param {Boolean} [reloadUnchanged=false] - Reload even data that hasn't changed internally. 631 * This should be set to true for data that was 632 * changed externally (e.g., globally renamed tags). 633 */ 634 Zotero.DataObject.prototype.reload = Zotero.Promise.coroutine(function* (dataTypes, reloadUnchanged) { 635 if (!this._id) { 636 return; 637 } 638 639 if (!dataTypes) { 640 dataTypes = Object.keys(this._loaded).filter(type => this._loaded[type]); 641 } 642 643 if (dataTypes && dataTypes.length) { 644 for (let i=0; i<dataTypes.length; i++) { 645 let dataType = dataTypes[i]; 646 if (!this._loaded[dataType] || this._skipDataTypeLoad[dataType] 647 || (!reloadUnchanged && !this._changed[dataType] && !this._dataTypesToReload.has(dataType))) { 648 continue; 649 } 650 yield this.loadDataType(dataType, true); 651 this._dataTypesToReload.delete(dataType); 652 } 653 } 654 }); 655 656 /** 657 * Checks whether a given data type has been loaded 658 * 659 * @param {String} [dataType=primaryData] Data type to check 660 * @throws {Zotero.DataObjects.UnloadedDataException} If not loaded, unless the 661 * data has not yet been "identified" 662 */ 663 Zotero.DataObject.prototype._requireData = function (dataType) { 664 if (this._loaded[dataType] === undefined) { 665 throw new Error(dataType + " is not a valid data type for " + this._ObjectType + " objects"); 666 } 667 668 if (dataType != 'primaryData') { 669 this._requireData('primaryData'); 670 } 671 672 if (!this._identified) { 673 this._loaded[dataType] = true; 674 } 675 else if (!this._loaded[dataType]) { 676 throw new Zotero.Exception.UnloadedDataException( 677 "'" + dataType + "' not loaded for " + this._objectType + " (" 678 + this._id + "/" + this._libraryID + "/" + this._key + ")", 679 dataType 680 ); 681 } 682 } 683 684 685 /** 686 * Loads data for a given data type 687 * @param {String} dataType 688 * @param {Boolean} reload 689 * @param {Promise} 690 */ 691 Zotero.DataObject.prototype.loadDataType = function (dataType, reload) { 692 return this._ObjectsClass._loadDataTypeInLibrary(dataType, this.libraryID, [this.id]); 693 } 694 695 Zotero.DataObject.prototype.loadAllData = Zotero.Promise.coroutine(function* (reload) { 696 for (let i=0; i<this._dataTypes.length; i++) { 697 let type = this._dataTypes[i]; 698 if (!this._skipDataTypeLoad[type]) { 699 yield this.loadDataType(type, reload); 700 } 701 } 702 }); 703 704 Zotero.DataObject.prototype._markAllDataTypeLoadStates = function (loaded) { 705 for (let i = 0; i < this._dataTypes.length; i++) { 706 this._loaded[this._dataTypes[i]] = loaded; 707 } 708 } 709 710 /** 711 * Save old version of data that's being changed, to pass to the notifier 712 * @param {String} field 713 * @param {} oldValue 714 */ 715 Zotero.DataObject.prototype._markFieldChange = function (field, oldValue) { 716 // New method (changedData) 717 if (field == 'tags') { 718 if (Array.isArray(oldValue)) { 719 this._changedData[field] = [...oldValue]; 720 } 721 else { 722 this._changedData[field] = oldValue; 723 } 724 return; 725 } 726 727 // Only save if object already exists and field not already changed 728 if (!this.id || this._previousData[field] !== undefined) { 729 return; 730 } 731 if (Array.isArray(oldValue)) { 732 this._previousData[field] = []; 733 Object.assign(this._previousData[field], oldValue) 734 } 735 else { 736 this._previousData[field] = oldValue; 737 } 738 } 739 740 741 Zotero.DataObject.prototype.hasChanged = function() { 742 var changed = Object.keys(this._changed).filter(dataType => this._changed[dataType]) 743 .concat( 744 Object.keys(this._changedData).filter(dataType => this._changedData[dataType]) 745 ); 746 if (changed.length == 1 747 && changed[0] == 'primaryData' 748 && Object.keys(this._changed.primaryData).length == 1 749 && this._changed.primaryData.synced 750 && this._previousData.synced == this._synced) { 751 return false; 752 } 753 return !!changed.length; 754 } 755 756 757 /** 758 * Clears log of changed values 759 * @param {String} [dataType] data type/field to clear. Defaults to clearing everything 760 */ 761 Zotero.DataObject.prototype._clearChanged = function (dataType) { 762 if (dataType) { 763 delete this._changed[dataType]; 764 delete this._previousData[dataType]; 765 delete this._changedData[dataType]; 766 } 767 else { 768 this._changed = {}; 769 this._previousData = {}; 770 this._changedData = {}; 771 this._dataTypesToReload = new Set(); 772 } 773 } 774 775 /** 776 * Clears field change log 777 * @param {String} field 778 */ 779 Zotero.DataObject.prototype._clearFieldChange = function (field) { 780 delete this._previousData[field]; 781 delete this._changedData[field]; 782 } 783 784 785 /** 786 * Mark a data type as requiring a reload when the current save finishes. The changed state is cleared 787 * before the new data is saved to the database (so that further updates during the save process don't 788 * get lost), so we need to separately keep track of what changed. 789 */ 790 Zotero.DataObject.prototype._markForReload = function (dataType) { 791 this._dataTypesToReload.add(dataType); 792 } 793 794 795 Zotero.DataObject.prototype.isEditable = function () { 796 return Zotero.Libraries.get(this.libraryID).editable; 797 } 798 799 800 Zotero.DataObject.prototype.editCheck = function () { 801 let library = Zotero.Libraries.get(this.libraryID); 802 if ((this._objectType == 'collection' || this._objectType == 'search') 803 && library.libraryType == 'publications') { 804 throw new Error(this._ObjectTypePlural + " cannot be added to My Publications"); 805 } 806 807 if (library.libraryType == 'feed') { 808 return; 809 } 810 811 if (!this.isEditable()) { 812 throw new Error("Cannot edit " + this._objectType + " in read-only library " 813 + Zotero.Libraries.get(this.libraryID).name); 814 } 815 } 816 817 /** 818 * Save changes to database 819 * 820 * @param {Object} [options] 821 * @param {Boolean} [options.skipCache] - Don't save add new object to the cache; if set, object 822 * is disabled after save 823 * @param {Boolean} [options.skipDateModifiedUpdate] 824 * @param {Boolean} [options.skipClientDateModifiedUpdate] 825 * @param {Boolean} [options.skipNotifier] - Don't trigger Zotero.Notifier events 826 * @param {Boolean} [options.skipSelect] - Don't select object automatically in trees 827 * @param {Boolean} [options.skipSyncedUpdate] - Don't automatically set 'synced' to false 828 * @return {Promise<Integer|Boolean>} Promise for itemID of new item, 829 * TRUE on item update, or FALSE if item was unchanged 830 */ 831 Zotero.DataObject.prototype.save = Zotero.Promise.coroutine(function* (options = {}) { 832 var env = { 833 options: Object.assign({}, options), 834 transactionOptions: {} 835 }; 836 837 if (!env.options.tx && !Zotero.DB.inTransaction()) { 838 Zotero.logError("save() called on Zotero." + this._ObjectType + " without a wrapping " 839 + "transaction -- use saveTx() instead"); 840 Zotero.debug((new Error).stack, 2); 841 env.options.tx = true; 842 } 843 844 if (env.options.skipAll) { 845 [ 846 'skipDateModifiedUpdate', 847 'skipClientDateModifiedUpdate', 848 'skipSyncedUpdate', 849 'skipEditCheck', 850 'skipNotifier', 851 'skipSelect' 852 ].forEach(x => env.options[x] = true); 853 } 854 855 var proceed = yield this._initSave(env); 856 if (!proceed) return false; 857 858 if (env.isNew) { 859 Zotero.debug('Saving data for new ' + this._objectType + ' to database', 4); 860 } 861 else { 862 Zotero.debug('Updating database with new ' + this._objectType + ' data', 4); 863 } 864 865 try { 866 if (Zotero.DataObject.prototype._finalizeSave == this._finalizeSave) { 867 throw new Error("_finalizeSave not implemented for Zotero." + this._ObjectType); 868 } 869 870 env.notifierData = {}; 871 // Pass along any 'notifierData' values 872 if (env.options.notifierData) { 873 Object.assign(env.notifierData, env.options.notifierData); 874 } 875 if (env.options.skipSelect) { 876 env.notifierData.skipSelect = true; 877 } 878 if (!env.isNew) { 879 env.changed = this._previousData; 880 } 881 882 // Create transaction 883 let result 884 if (env.options.tx) { 885 result = yield Zotero.DB.executeTransaction(function* () { 886 Zotero.DataObject.prototype._saveData.call(this, env); 887 yield this._saveData(env); 888 yield Zotero.DataObject.prototype._finalizeSave.call(this, env); 889 return this._finalizeSave(env); 890 }.bind(this), env.transactionOptions); 891 } 892 // Use existing transaction 893 else { 894 Zotero.DB.requireTransaction(); 895 Zotero.DataObject.prototype._saveData.call(this, env); 896 yield this._saveData(env); 897 yield Zotero.DataObject.prototype._finalizeSave.call(this, env); 898 result = this._finalizeSave(env); 899 } 900 this._postSave(env); 901 return result; 902 } 903 catch(e) { 904 return this._recoverFromSaveError(env, e) 905 .catch(function(e2) { 906 Zotero.debug(e2, 1); 907 }) 908 .then(function() { 909 if (env.options.errorHandler) { 910 env.options.errorHandler(e); 911 } 912 else { 913 Zotero.logError(e); 914 } 915 throw e; 916 }) 917 } 918 }); 919 920 921 Zotero.DataObject.prototype.saveTx = function (options = {}) { 922 options = Object.assign({}, options); 923 options.tx = true; 924 return this.save(options); 925 } 926 927 928 Zotero.DataObject.prototype._initSave = Zotero.Promise.coroutine(function* (env) { 929 // Default to user library if not specified 930 if (this.libraryID === null) { 931 this._libraryID = Zotero.Libraries.userLibraryID; 932 } 933 934 env.isNew = !this.id; 935 936 if (!env.options.skipEditCheck) { 937 this.editCheck(); 938 } 939 940 let targetLib = Zotero.Libraries.get(this.libraryID); 941 if (!targetLib.isChildObjectAllowed(this._objectType)) { 942 throw new Error("Cannot add " + this._objectType + " to a " + targetLib.libraryType + " library"); 943 } 944 945 if (!this.hasChanged()) { 946 Zotero.debug(this._ObjectType + ' ' + this.id + ' has not changed', 4); 947 return false; 948 } 949 950 // Undo registerObject() on failure 951 if (env.isNew) { 952 var func = function () { 953 this.ObjectsClass.unload(this._id); 954 }.bind(this); 955 if (env.options.tx) { 956 env.transactionOptions.onRollback = func; 957 } 958 else { 959 Zotero.DB.addCurrentCallback("rollback", func); 960 } 961 } 962 963 env.relationsToRegister = []; 964 env.relationsToUnregister = []; 965 966 return true; 967 }); 968 969 Zotero.DataObject.prototype._saveData = function (env) { 970 var libraryID = env.libraryID = this.libraryID || Zotero.Libraries.userLibraryID; 971 var key = env.key = this._key = this.key ? this.key : this._generateKey(); 972 973 env.sqlColumns = []; 974 env.sqlValues = []; 975 976 if (env.isNew) { 977 env.sqlColumns.push( 978 'libraryID', 979 'key' 980 ); 981 env.sqlValues.push( 982 libraryID, 983 key 984 ); 985 } 986 987 if (this._changed.primaryData && this._changed.primaryData.version) { 988 env.sqlColumns.push('version'); 989 env.sqlValues.push(this.version || 0); 990 } 991 992 if (this._changed.primaryData && this._changed.primaryData.synced) { 993 env.sqlColumns.push('synced'); 994 env.sqlValues.push(this.synced ? 1 : 0); 995 } 996 // Set synced to 0 by default 997 else if (!env.isNew && !env.options.skipSyncedUpdate) { 998 env.sqlColumns.push('synced'); 999 env.sqlValues.push(0); 1000 } 1001 1002 if (env.isNew || !env.options.skipClientDateModifiedUpdate) { 1003 env.sqlColumns.push('clientDateModified'); 1004 env.sqlValues.push(Zotero.DB.transactionDateTime); 1005 } 1006 }; 1007 1008 Zotero.DataObject.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) { 1009 // Relations 1010 if (this._changed.relations) { 1011 let toAdd, toRemove; 1012 // Convert to individual JSON objects, diff, and convert back 1013 if (this._previousData.relations) { 1014 let oldRelationsJSON = this._previousData.relations.map(x => JSON.stringify(x)); 1015 let newRelationsJSON = this._relations.map(x => JSON.stringify(x)); 1016 toAdd = Zotero.Utilities.arrayDiff(newRelationsJSON, oldRelationsJSON) 1017 .map(x => JSON.parse(x)); 1018 toRemove = Zotero.Utilities.arrayDiff(oldRelationsJSON, newRelationsJSON) 1019 .map(x => JSON.parse(x)); 1020 } 1021 else { 1022 toAdd = this._relations; 1023 toRemove = []; 1024 } 1025 1026 if (toAdd.length) { 1027 let sql = "INSERT INTO " + this._objectType + "Relations " 1028 + "(" + this._ObjectsClass.idColumn + ", predicateID, object) VALUES "; 1029 // Convert predicates to ids 1030 for (let i = 0; i < toAdd.length; i++) { 1031 toAdd[i][0] = yield Zotero.RelationPredicates.add(toAdd[i][0]); 1032 env.relationsToRegister.push([toAdd[i][0], toAdd[i][1]]); 1033 } 1034 yield Zotero.DB.queryAsync( 1035 sql + toAdd.map(x => "(?, ?, ?)").join(", "), 1036 toAdd.map(x => [this.id, x[0], x[1]]) 1037 .reduce((x, y) => x.concat(y)) 1038 ); 1039 } 1040 1041 if (toRemove.length) { 1042 for (let i = 0; i < toRemove.length; i++) { 1043 let sql = "DELETE FROM " + this._objectType + "Relations " 1044 + "WHERE " + this._ObjectsClass.idColumn + "=? AND predicateID=? AND object=?"; 1045 yield Zotero.DB.queryAsync( 1046 sql, 1047 [ 1048 this.id, 1049 (yield Zotero.RelationPredicates.add(toRemove[i][0])), 1050 toRemove[i][1] 1051 ] 1052 ); 1053 env.relationsToUnregister.push([toRemove[i][0], toRemove[i][1]]); 1054 } 1055 } 1056 } 1057 1058 if (env.isNew) { 1059 if (!env.skipCache) { 1060 // Register this object's identifiers in Zotero.DataObjects. This has to happen here so 1061 // that the object exists for the reload() in objects' finalizeSave methods. 1062 this.ObjectsClass.registerObject(this); 1063 } 1064 // If object isn't being reloaded, disable it, since its data may be out of date 1065 else { 1066 this._disabled = true; 1067 } 1068 } 1069 else if (env.skipCache) { 1070 Zotero.logError("skipCache is only for new objects"); 1071 } 1072 }); 1073 1074 1075 /** 1076 * Actions to perform after DB transaction 1077 */ 1078 Zotero.DataObject.prototype._postSave = function (env) { 1079 for (let i = 0; i < env.relationsToRegister.length; i++) { 1080 let rel = env.relationsToRegister[i]; 1081 Zotero.debug(rel); 1082 Zotero.Relations.register(this._objectType, this.id, rel[0], rel[1]); 1083 } 1084 for (let i = 0; i < env.relationsToUnregister.length; i++) { 1085 let rel = env.relationsToUnregister[i]; 1086 Zotero.Relations.unregister(this._objectType, this.id, rel[0], rel[1]); 1087 } 1088 }; 1089 1090 1091 Zotero.DataObject.prototype._recoverFromSaveError = Zotero.Promise.coroutine(function* (env) { 1092 yield this.reload(null, true); 1093 this._clearChanged(); 1094 }); 1095 1096 1097 /** 1098 * Update object version, efficiently 1099 * 1100 * Used by sync code 1101 * 1102 * @param {Integer} version 1103 * @param {Boolean} [skipDB=false] 1104 */ 1105 Zotero.DataObject.prototype.updateVersion = Zotero.Promise.coroutine(function* (version, skipDB) { 1106 if (!this.id) { 1107 throw new Error("Cannot update version of unsaved " + this._objectType); 1108 } 1109 if (version != parseInt(version)) { 1110 throw new Error("'version' must be an integer"); 1111 } 1112 1113 this._version = parseInt(version); 1114 1115 if (!skipDB) { 1116 var cl = this.ObjectsClass; 1117 var sql = "UPDATE " + cl.table + " SET version=? WHERE " + cl.idColumn + "=?"; 1118 yield Zotero.DB.queryAsync(sql, [parseInt(version), this.id]); 1119 } 1120 1121 if (this._changed.primaryData && this._changed.primaryData.version) { 1122 if (Objects.keys(this._changed.primaryData).length == 1) { 1123 delete this._changed.primaryData; 1124 } 1125 else { 1126 delete this._changed.primaryData.version; 1127 } 1128 } 1129 }); 1130 1131 /** 1132 * Update object sync status, efficiently 1133 * 1134 * Used by sync code 1135 * 1136 * @param {Boolean} synced 1137 * @param {Boolean} [skipDB=false] 1138 */ 1139 Zotero.DataObject.prototype.updateSynced = Zotero.Promise.coroutine(function* (synced, skipDB) { 1140 if (!this.id) { 1141 throw new Error("Cannot update sync status of unsaved " + this._objectType); 1142 } 1143 if (typeof synced != 'boolean') { 1144 throw new Error("'synced' must be a boolean"); 1145 } 1146 1147 this._synced = synced; 1148 1149 if (!skipDB) { 1150 var cl = this.ObjectsClass; 1151 var sql = "UPDATE " + cl.table + " SET synced=? WHERE " + cl.idColumn + "=?"; 1152 yield Zotero.DB.queryAsync(sql, [synced ? 1 : 0, this.id]); 1153 } 1154 1155 if (this._changed.primaryData && this._changed.primaryData.synced) { 1156 if (Object.keys(this._changed.primaryData).length == 1) { 1157 delete this._changed.primaryData; 1158 } 1159 else { 1160 delete this._changed.primaryData.synced; 1161 } 1162 } 1163 }); 1164 1165 /** 1166 * Delete object from database 1167 * 1168 * @param {Object} [options] 1169 * @param {Boolean} [options.deleteItems] - Move descendant items to trash (Collection only) 1170 * @param {Boolean} [options.skipDeleteLog] - Don't add to sync delete log 1171 */ 1172 Zotero.DataObject.prototype.erase = Zotero.Promise.coroutine(function* (options = {}) { 1173 if (!options || typeof options != 'object') { 1174 throw new Error("'options' must be an object (" + typeof options + ")"); 1175 } 1176 1177 var env = { 1178 options: Object.assign({}, options) 1179 }; 1180 1181 if (!env.options.tx && !Zotero.DB.inTransaction()) { 1182 Zotero.logError("erase() called on Zotero." + this._ObjectType + " without a wrapping " 1183 + "transaction -- use eraseTx() instead"); 1184 Zotero.debug((new Error).stack, 2); 1185 env.options.tx = true; 1186 } 1187 1188 let proceed = yield this._initErase(env); 1189 if (!proceed) return false; 1190 1191 Zotero.debug('Deleting ' + this.objectType + ' ' + this.id); 1192 1193 if (env.options.tx) { 1194 return Zotero.DB.executeTransaction(function* () { 1195 yield this._eraseData(env); 1196 yield this._finalizeErase(env); 1197 }.bind(this)) 1198 } 1199 else { 1200 Zotero.DB.requireTransaction(); 1201 yield this._eraseData(env); 1202 yield this._finalizeErase(env); 1203 } 1204 }); 1205 1206 Zotero.DataObject.prototype.eraseTx = function (options) { 1207 options = options || {}; 1208 options.tx = true; 1209 return this.erase(options); 1210 }; 1211 1212 Zotero.DataObject.prototype._initErase = Zotero.Promise.method(function (env) { 1213 env.notifierData = {}; 1214 env.notifierData[this.id] = { 1215 libraryID: this.libraryID, 1216 key: this.key 1217 }; 1218 1219 if (!env.options.skipEditCheck) this.editCheck(); 1220 1221 if (env.options.skipDeleteLog) { 1222 env.notifierData[this.id].skipDeleteLog = true; 1223 } 1224 1225 return true; 1226 }); 1227 1228 Zotero.DataObject.prototype._finalizeErase = Zotero.Promise.coroutine(function* (env) { 1229 // Delete versions from sync cache 1230 if (this._objectType != 'feedItem') { 1231 yield Zotero.Sync.Data.Local.deleteCacheObjectVersions( 1232 this.objectType, this._libraryID, this._key 1233 ); 1234 } 1235 1236 Zotero.DB.addCurrentCallback("commit", function () { 1237 this.ObjectsClass.unload(env.deletedObjectIDs || this.id); 1238 }.bind(this)); 1239 1240 if (!env.options.skipNotifier) { 1241 Zotero.Notifier.queue( 1242 'delete', 1243 this._objectType, 1244 Object.keys(env.notifierData).map(id => parseInt(id)), 1245 env.notifierData, 1246 env.options.notifierQueue 1247 ); 1248 } 1249 }); 1250 1251 1252 Zotero.DataObject.prototype.toResponseJSON = function (options = {}) { 1253 // TODO: library block? 1254 1255 var json = { 1256 key: this.key, 1257 version: this.version, 1258 meta: {}, 1259 data: this.toJSON(options) 1260 }; 1261 if (options.version) { 1262 json.version = json.data.version = options.version; 1263 } 1264 return json; 1265 } 1266 1267 1268 Zotero.DataObject.prototype._preToJSON = function (options) { 1269 var env = { options }; 1270 env.mode = options.mode || 'new'; 1271 if (env.mode == 'patch') { 1272 if (!options.patchBase) { 1273 throw new Error("Cannot use patch mode if patchBase not provided"); 1274 } 1275 } 1276 else if (options.patchBase) { 1277 if (options.mode) { 1278 Zotero.debug("Zotero.Item.toJSON: ignoring provided patchBase in " + env.mode + " mode", 2); 1279 } 1280 // If patchBase provided and no explicit mode, use 'patch' 1281 else { 1282 env.mode = 'patch'; 1283 } 1284 } 1285 return env; 1286 } 1287 1288 Zotero.DataObject.prototype._postToJSON = function (env) { 1289 if (env.mode == 'patch') { 1290 env.obj = Zotero.DataObjectUtilities.patch(env.options.patchBase, env.obj); 1291 } 1292 if (env.options.includeVersion === false) { 1293 delete env.obj.version; 1294 } 1295 return env.obj; 1296 } 1297 1298 1299 /** 1300 * Generates data object key 1301 * @return {String} key 1302 */ 1303 Zotero.DataObject.prototype._generateKey = function () { 1304 return Zotero.Utilities.generateObjectKey(); 1305 } 1306 1307 Zotero.DataObject.prototype._disabledCheck = function () { 1308 if (this._disabled) { 1309 Zotero.logError(this._ObjectType + " is disabled -- " 1310 + "use Zotero." + this._ObjectTypePlural + ".getAsync()"); 1311 } 1312 }