1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 sc_require('models/record'); 9 10 /** 11 @class 12 13 14 The Store is where you can find all of your dataHashes. Stores can be 15 chained for editing purposes and committed back one chain level at a time 16 all the way back to a persistent data source. 17 18 Every application you create should generally have its own store objects. 19 Once you create the store, you will rarely need to work with the store 20 directly except to retrieve records and collections. 21 22 Internally, the store will keep track of changes to your json data hashes 23 and manage syncing those changes with your data source. A data source may 24 be a server, local storage, or any other persistent code. 25 26 @extends SC.Object 27 @since SproutCore 1.0 28 */ 29 SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ { 30 31 /** 32 An (optional) name of the store, which can be useful during debugging, 33 especially if you have multiple nested stores. 34 35 @type String 36 */ 37 name: null, 38 39 /** 40 An array of all the chained stores that current rely on the receiver 41 store. 42 43 @type Array 44 */ 45 nestedStores: null, 46 47 /** 48 The data source is the persistent storage that will provide data to the 49 store and save changes. You normally will set your data source when you 50 first create your store in your application. 51 52 @type SC.DataSource 53 */ 54 dataSource: null, 55 56 /** 57 This type of store is not nested. 58 59 @default NO 60 @type Boolean 61 */ 62 isNested: NO, 63 64 /** 65 This type of store is not nested. 66 67 @default NO 68 @type Boolean 69 */ 70 commitRecordsAutomatically: NO, 71 72 // .......................................................... 73 // DATA SOURCE SUPPORT 74 // 75 76 /** 77 Convenience method. Sets the current data source to the passed property. 78 This will also set the store property on the dataSource to the receiver. 79 80 If you are using this from the `core.js` method of your app, you may need to 81 just pass a string naming your data source class. If this is the case, 82 then your data source will be instantiated the first time it is requested. 83 84 @param {SC.DataSource|String} dataSource the data source 85 @returns {SC.Store} receiver 86 */ 87 from: function(dataSource) { 88 this.set('dataSource', dataSource); 89 return this ; 90 }, 91 92 // lazily convert data source to real object 93 _getDataSource: function() { 94 var ret = this.get('dataSource'); 95 if (typeof ret === SC.T_STRING) { 96 ret = SC.requiredObjectForPropertyPath(ret); 97 if (ret.isClass) ret = ret.create(); 98 this.set('dataSource', ret); 99 } 100 return ret; 101 }, 102 103 /** 104 Convenience method. Creates a `CascadeDataSource` with the passed 105 data source arguments and sets the `CascadeDataSource` as the data source 106 for the receiver. 107 108 @param {SC.DataSource...} dataSource one or more data source arguments 109 @returns {SC.Store} receiver 110 */ 111 cascade: function(dataSource) { 112 var dataSources; 113 114 // Fast arguments access. 115 // Accessing `arguments.length` is just a Number and doesn't materialize the `arguments` object, which is costly. 116 dataSources = new Array(arguments.length); // SC.A(arguments) 117 for (var i = 0, len = dataSources.length; i < len; i++) { dataSources[i] = arguments[i]; } 118 119 dataSource = SC.CascadeDataSource.create({ 120 dataSources: dataSources 121 }); 122 return this.from(dataSource); 123 }, 124 125 // .......................................................... 126 // STORE CHAINING 127 // 128 129 /** 130 Returns a new nested store instance that can be used to buffer changes 131 until you are ready to commit them. When you are ready to commit your 132 changes, call `commitChanges()` or `destroyChanges()` and then `destroy()` 133 when you are finished with the chained store altogether. 134 135 store = MyApp.store.chain(); 136 .. edit edit edit 137 store.commitChanges().destroy(); 138 139 @param {Hash} attrs optional attributes to set on new store 140 @param {Class} newStoreClass optional the class of the newly-created nested store (defaults to SC.NestedStore) 141 @returns {SC.NestedStore} new nested store chained to receiver 142 */ 143 chain: function(attrs, newStoreClass) { 144 if (!attrs) attrs = {}; 145 attrs.parentStore = this; 146 147 if (newStoreClass) { 148 // Ensure the passed-in class is a type of nested store. 149 if (SC.typeOf(newStoreClass) !== 'class') throw new Error("%@ is not a valid class".fmt(newStoreClass)); 150 if (!SC.kindOf(newStoreClass, SC.NestedStore)) throw new Error("%@ is not a type of SC.NestedStore".fmt(newStoreClass)); 151 } 152 else { 153 newStoreClass = SC.NestedStore; 154 } 155 156 // Replicate parent records references 157 attrs.childRecords = this.childRecords ? SC.clone(this.childRecords) : {}; 158 attrs.parentRecords = this.parentRecords ? SC.clone(this.parentRecords) : {}; 159 160 var ret = newStoreClass.create(attrs), 161 nested = this.nestedStores; 162 163 if (!nested) nested = this.nestedStores = []; 164 nested.push(ret); 165 return ret ; 166 }, 167 168 /** 169 Creates an autonomous nested store that is connected to the data source. 170 171 Use this kind of nested store to ensure that all records that are committed into the main store 172 are first of all committed to the server. 173 174 For example, 175 176 nestedStore = store.chainAutonomousStore(); 177 178 // ... commit all changes from the nested store to the remote data store 179 nestedStore.commitRecords(); 180 181 // or commit the changes of a nested store's record to the remote data store ... 182 nestedRecord.commitRecord(); 183 184 ## Resolving nested store commits with the main store 185 186 When the committed changes are deemed successful (either by observing the status of the modified 187 record(s) or by using callbacks with commitRecord/commitRecords), the changes can be passed back 188 to the main store. 189 190 In the case that the commits are all successful, simply commit to the main store using 191 `commitSuccessfulChanges`. Note, that using `commitSuccessfulChanges` rather than the standard 192 `commitChanges` ensures that only clean changes propagate back to the main store. 193 194 For example, 195 196 nestedStore.commitSuccessfulChanges(); 197 198 In the case that some or all of the commits fail, you can still use `commitSuccessfulChanges` to 199 update only those commits that have succeeded in the main store or wait until all commits have 200 succeeded. Regardless, it will be up to your application to act on the failures and work with 201 the user to resolve all failures until the nested store records are all clean. 202 203 204 @param {Hash} [attrs] attributes to set on new store 205 @param {Class} [newStoreClass] the class of the newly-created nested store (defaults to SC.NestedStore) 206 @returns {SC.NestedStore} new nested store chained to receiver 207 */ 208 chainAutonomousStore: function(attrs, newStoreClass) { 209 var newAttrs = attrs ? SC.clone( attrs ) : {}; 210 var source = this._getDataSource(); 211 212 newAttrs.dataSource = source; 213 return this.chain( newAttrs, newStoreClass ); 214 }, 215 216 /** @private 217 218 Called by a nested store just before it is destroyed so that the parent 219 can remove the store from its list of nested stores. 220 221 @returns {SC.Store} receiver 222 */ 223 willDestroyNestedStore: function(nestedStore) { 224 if (this.nestedStores) { 225 this.nestedStores.removeObject(nestedStore); 226 } 227 return this ; 228 }, 229 230 /** 231 Used to determine if a nested store belongs directly or indirectly to the 232 receiver. 233 234 @param {SC.Store} store store instance 235 @returns {Boolean} YES if belongs 236 */ 237 hasNestedStore: function(store) { 238 while(store && (store !== this)) store = store.get('parentStore'); 239 return store === this ; 240 }, 241 242 // .......................................................... 243 // SHARED DATA STRUCTURES 244 // 245 246 /** @private 247 JSON data hashes indexed by store key. 248 249 *IMPORTANT: Property is not observable* 250 251 Shared by a store and its child stores until you make edits to it. 252 253 @type Hash 254 */ 255 dataHashes: null, 256 257 /** @private 258 The current status of a data hash indexed by store key. 259 260 *IMPORTANT: Property is not observable* 261 262 Shared by a store and its child stores until you make edits to it. 263 264 @type Hash 265 */ 266 statuses: null, 267 268 /** @private 269 This array contains the revisions for the attributes indexed by the 270 storeKey. 271 272 *IMPORTANT: Property is not observable* 273 274 Revisions are used to keep track of when an attribute hash has been 275 changed. A store shares the revisions data with its parent until it 276 starts to make changes to it. 277 278 @type Hash 279 */ 280 revisions: null, 281 282 /** 283 Array indicates whether a data hash is possibly in use by an external 284 record for editing. If a data hash is editable then it may be modified 285 at any time and therefore chained stores may need to clone the 286 attributes before keeping a copy of them. 287 288 Note that this is kept as an array because it will be stored as a dense 289 array on some browsers, making it faster. 290 291 @type Array 292 */ 293 editables: null, 294 295 /** 296 A set of storeKeys that need to be committed back to the data source. If 297 you call `commitRecords()` without passing any other parameters, the keys 298 in this set will be committed instead. 299 300 @type SC.Set 301 */ 302 changelog: null, 303 304 /** 305 An array of `SC.Error` objects associated with individual records in the 306 store (indexed by store keys). 307 308 Errors passed form the data source in the call to dataSourceDidError() are 309 stored here. 310 311 @type Array 312 */ 313 recordErrors: null, 314 315 /** 316 A hash of `SC.Error` objects associated with queries (indexed by the GUID 317 of the query). 318 319 Errors passed from the data source in the call to 320 `dataSourceDidErrorQuery()` are stored here. 321 322 @type Hash 323 */ 324 queryErrors: null, 325 326 /** 327 A hash of child Records and there immediate parents 328 */ 329 childRecords: null, 330 331 /** 332 A hash of parent records with registered children 333 */ 334 parentRecords: null, 335 336 // .......................................................... 337 // CORE ATTRIBUTE API 338 // 339 // The methods in this layer work on data hashes in the store. They do not 340 // perform any changes that can impact records. Usually you will not need 341 // to use these methods. 342 343 /** 344 Returns the current edit status of a store key. May be one of 345 `EDITABLE` or `LOCKED`. Used mostly for unit testing. 346 347 @param {Number} storeKey the store key 348 @returns {Number} edit status 349 */ 350 storeKeyEditState: function(storeKey) { 351 var editables = this.editables; 352 return (editables && editables[storeKey]) ? SC.Store.EDITABLE : SC.Store.LOCKED ; 353 }, 354 355 /** 356 Returns the data hash for the given `storeKey`. This will also 'lock' 357 the hash so that further edits to the parent store will no 358 longer be reflected in this store until you reset. 359 360 @param {Number} storeKey key to retrieve 361 @returns {Hash} data hash or null 362 */ 363 readDataHash: function(storeKey) { 364 return this.dataHashes[storeKey]; 365 }, 366 367 /** 368 Returns the data hash for the `storeKey`, cloned so that you can edit 369 the contents of the attributes if you like. This will do the extra work 370 to make sure that you only clone the attributes one time. 371 372 If you use this method to modify data hash, be sure to call 373 `dataHashDidChange()` when you make edits to record the change. 374 375 @param {Number} storeKey the store key to retrieve 376 @returns {Hash} the attributes hash 377 */ 378 readEditableDataHash: function(storeKey) { 379 // read the value - if there is no hash just return; nothing to do 380 var ret = this.dataHashes[storeKey]; 381 if (!ret) return ret ; // nothing to do. 382 383 // clone data hash if not editable 384 var editables = this.editables; 385 if (!editables) editables = this.editables = []; 386 if (!editables[storeKey]) { 387 editables[storeKey] = 1 ; // use number to store as dense array 388 ret = this.dataHashes[storeKey] = SC.clone(ret, YES); 389 } 390 return ret; 391 }, 392 393 /** 394 Reads a property from the hash - cloning it if needed so you can modify 395 it independently of any parent store. This method is really only well 396 tested for use with toMany relationships. Although it is public you 397 generally should not call it directly. 398 399 @param {Number} storeKey storeKey of data hash 400 @param {String} propertyName property to read 401 @returns {Object} editable property value 402 */ 403 readEditableProperty: function(storeKey, propertyName) { 404 var hash = this.readEditableDataHash(storeKey), 405 editables = this.editables[storeKey], // get editable info... 406 ret = hash[propertyName]; 407 408 // editables must be made into a hash so that we can keep track of which 409 // properties have already been made editable 410 if (editables === 1) editables = this.editables[storeKey] = {}; 411 412 // clone if needed 413 if (!editables[propertyName]) { 414 ret = hash[propertyName]; 415 if (ret && ret.isCopyable) ret = hash[propertyName] = ret.copy(YES); 416 editables[propertyName] = YES ; 417 } 418 419 return ret ; 420 }, 421 422 /** 423 Replaces the data hash for the `storeKey`. This will lock the data hash 424 and mark them as cloned. This will also call `dataHashDidChange()` for 425 you. 426 427 Note that the hash you set here must be a different object from the 428 original data hash. Once you make a change here, you must also call 429 `dataHashDidChange()` to register the changes. 430 431 If the data hash does not yet exist in the store, this method will add it. 432 Pass the optional status to edit the status as well. 433 434 @param {Number} storeKey the store key to write 435 @param {Hash} hash the new hash 436 @param {String} status the new hash status 437 @returns {SC.Store} receiver 438 */ 439 writeDataHash: function(storeKey, hash, status) { 440 441 // update dataHashes and optionally status. 442 if (hash) this.dataHashes[storeKey] = hash; 443 if (status) this.statuses[storeKey] = status ; 444 445 // also note that this hash is now editable 446 var editables = this.editables; 447 if (!editables) editables = this.editables = []; 448 editables[storeKey] = 1 ; // use number for dense array support 449 450 // propagate the data to the child records 451 this._updateChildRecordHashes(storeKey, hash, status); 452 453 return this ; 454 }, 455 456 /** @private 457 458 Called by writeDataHash to update the child record hashes starting from the new (parent) data hash. 459 460 @returns {SC.Store} receiver 461 */ 462 _updateChildRecordHashes: function(storeKey, hash, status) { 463 var processedPaths={}; 464 // Update the child record hashes in place. 465 if (!SC.none(this.parentRecords) ) { 466 var children = this.parentRecords[storeKey] || {}, 467 childHash; 468 469 for (var key in children) { 470 if (children.hasOwnProperty(key)) { 471 if (hash) { 472 var childPath = children[key]; 473 childPath = childPath.split('.'); 474 if (childPath.length > 1) { 475 childHash = hash[childPath[0]][childPath[1]]; 476 } else { 477 childHash = hash[childPath[0]]; 478 } 479 480 if (!processedPaths[hash[childPath[0]]]){ 481 // update data hash: required to push changes beyond the first nesting level 482 this.writeDataHash(key, childHash, status); 483 } 484 485 if (childPath.length > 1 && ! processedPaths[hash[childPath[0]]]) { 486 // save it so that we don't processed it over and over 487 processedPaths[hash[childPath[0]]]=true; 488 489 // force fetching of all children records by invoking the children_attribute wrapper code 490 // and then interating the list in an empty loop 491 // Ugly, but there's basically no other way to do it at the moment, other than 492 // leaving this broken as it was before 493 var that = this; 494 this.invokeLast(function() { 495 that.records[storeKey].get(childPath[0]).forEach(function (it) {}); 496 }); 497 } 498 } else { 499 this.writeDataHash(key, null, status); 500 } 501 } 502 } 503 } 504 }, 505 506 /** 507 Removes the data hash from the store. This does not imply a deletion of 508 the record. You could be simply unloading the record. Either way, 509 removing the dataHash will be synced back to the parent store but not to 510 the server. 511 512 Note that you can optionally pass a new status to go along with this. If 513 you do not pass a status, it will change the status to `SC.RECORD_EMPTY` 514 (assuming you just unloaded the record). If you are deleting the record 515 you may set it to `SC.Record.DESTROYED_CLEAN`. 516 517 Be sure to also call `dataHashDidChange()` to register this change. 518 519 @param {Number} storeKey 520 @param {String} status optional new status 521 @returns {SC.Store} receiver 522 */ 523 removeDataHash: function(storeKey, status) { 524 // don't use delete -- that will allow parent dataHash to come through 525 this.dataHashes[storeKey] = null; 526 this.statuses[storeKey] = status || SC.Record.EMPTY; 527 528 // hash is gone and therefore no longer editable 529 var editables = this.editables; 530 if (editables) editables[storeKey] = 0 ; 531 532 return this ; 533 }, 534 535 /** 536 Reads the current status for a storeKey. This will also lock the data 537 hash. If no status is found, returns `SC.RECORD_EMPTY`. 538 539 @param {Number} storeKey the store key 540 @returns {Number} status 541 */ 542 readStatus: function(storeKey) { 543 // use readDataHash to handle optimistic locking. this could be inlined 544 // but for now this minimized copy-and-paste code. 545 this.readDataHash(storeKey); 546 return this.statuses[storeKey] || SC.Record.EMPTY; 547 }, 548 549 /** 550 Reads the current status for the storeKey without actually locking the 551 record. Usually you won't need to use this method. It is mostly used 552 internally. 553 554 @param {Number} storeKey the store key 555 @returns {Number} status 556 */ 557 peekStatus: function(storeKey) { 558 return this.statuses[storeKey] || SC.Record.EMPTY; 559 }, 560 561 /** 562 Writes the current status for a storeKey. If the new status is 563 `SC.Record.ERROR`, you may also pass an optional error object. Otherwise 564 this param is ignored. 565 566 @param {Number} storeKey the store key 567 @param {String} newStatus the new status 568 @param {SC.Error} error optional error object 569 @returns {SC.Store} receiver 570 */ 571 writeStatus: function(storeKey, newStatus) { 572 var that = this, 573 ret; 574 // use writeDataHash for now to handle optimistic lock. maximize code 575 // reuse. 576 ret = this.writeDataHash(storeKey, null, newStatus); 577 this._propagateToChildren(storeKey, function(storeKey) { 578 that.writeStatus(storeKey, newStatus); 579 }); 580 return ret; 581 }, 582 583 /** 584 Call this method whenever you modify some editable data hash to register 585 with the Store that the attribute values have actually changed. This will 586 do the book-keeping necessary to track the change across stores including 587 managing locks. 588 589 @param {Number|Array} storeKeys one or more store keys that changed 590 @param {Number} rev optional new revision number. normally leave null 591 @param {Boolean} statusOnly (optional) YES if only status changed 592 @param {String} key that changed (optional) 593 @returns {SC.Store} receiver 594 */ 595 dataHashDidChange: function(storeKeys, rev, statusOnly, key) { 596 // update the revision for storeKey. Use generateStoreKey() because that 597 // guarantees a universally (to this store hierarchy anyway) unique 598 // key value. 599 if (!rev) rev = SC.Store.generateStoreKey(); 600 var isArray, len, idx, storeKey; 601 602 isArray = SC.typeOf(storeKeys) === SC.T_ARRAY; 603 if (isArray) { 604 len = storeKeys.length; 605 } else { 606 len = 1; 607 storeKey = storeKeys; 608 } 609 610 var that = this; 611 for(idx=0;idx<len;idx++) { 612 if (isArray) storeKey = storeKeys[idx]; 613 this.revisions[storeKey] = rev; 614 this._notifyRecordPropertyChange(storeKey, statusOnly, key); 615 616 this._propagateToChildren(storeKey, function(storeKey){ 617 that.dataHashDidChange(storeKey, null, statusOnly, key); 618 }); 619 } 620 621 return this ; 622 }, 623 624 /** @private 625 Will push all changes to a the recordPropertyChanges property 626 and execute `flush()` once at the end of the runloop. 627 */ 628 _notifyRecordPropertyChange: function(storeKey, statusOnly, key) { 629 630 var records = this.records, 631 nestedStores = this.get('nestedStores'), 632 K = SC.Store, 633 rec, editState, len, idx, store, status, keys; 634 635 // pass along to nested stores 636 len = nestedStores ? nestedStores.length : 0 ; 637 for(idx=0;idx<len;idx++) { 638 store = nestedStores[idx]; 639 status = store.peekStatus(storeKey); // important: peek avoids read-lock 640 editState = store.storeKeyEditState(storeKey); 641 642 // when store needs to propagate out changes in the parent store 643 // to nested stores 644 if (editState === K.INHERITED) { 645 store._notifyRecordPropertyChange(storeKey, statusOnly, key); 646 647 } else if (status & SC.Record.BUSY) { 648 // make sure nested store does not have any changes before resetting 649 if(store.get('hasChanges')) K.CHAIN_CONFLICT_ERROR.throw(); 650 store.reset(); 651 } 652 } 653 654 // store info in changes hash and schedule notification if needed. 655 var changes = this.recordPropertyChanges; 656 if (!changes) { 657 changes = this.recordPropertyChanges = 658 { storeKeys: SC.CoreSet.create(), 659 records: SC.CoreSet.create(), 660 hasDataChanges: SC.CoreSet.create(), 661 propertyForStoreKeys: {} }; 662 } 663 664 changes.storeKeys.add(storeKey); 665 666 if (records && (rec=records[storeKey])) { 667 changes.records.push(storeKey); 668 669 // If there are changes other than just the status we need to record 670 // that information so we do the right thing during the next flush. 671 // Note that if we're called multiple times before flush and one call 672 // has `statusOnly=true` and another has `statusOnly=false`, the flush 673 // will (correctly) operate in `statusOnly=false` mode. 674 if (!statusOnly) changes.hasDataChanges.push(storeKey); 675 676 // If this is a key specific change, make sure that only those 677 // properties/keys are notified. However, if a previous invocation of 678 // `_notifyRecordPropertyChange` specified that all keys have changed, we 679 // need to respect that. 680 if (key) { 681 if (!(keys = changes.propertyForStoreKeys[storeKey])) { 682 keys = changes.propertyForStoreKeys[storeKey] = SC.CoreSet.create(); 683 } 684 685 // If it's '*' instead of a set, then that means there was a previous 686 // invocation that said all keys have changed. 687 if (keys !== '*') { 688 keys.add(key); 689 } 690 } 691 else { 692 // Mark that all properties have changed. 693 changes.propertyForStoreKeys[storeKey] = '*'; 694 } 695 } 696 697 this.invokeOnce(this.flush); 698 return this; 699 }, 700 701 /** 702 Delivers any pending changes to materialized records. Normally this 703 happens once, automatically, at the end of the RunLoop. If you have 704 updated some records and need to update records immediately, however, 705 you may call this manually. 706 707 @returns {SC.Store} receiver 708 */ 709 flush: function() { 710 if (!this.recordPropertyChanges) return this; 711 712 var changes = this.recordPropertyChanges, 713 storeKeys = changes.storeKeys, 714 hasDataChanges = changes.hasDataChanges, 715 records = changes.records, 716 propertyForStoreKeys = changes.propertyForStoreKeys, 717 recordTypes = SC.CoreSet.create(), 718 rec, recordType, statusOnly, keys; 719 720 storeKeys.forEach(function(storeKey) { 721 if (records.contains(storeKey)) { 722 statusOnly = hasDataChanges.contains(storeKey) ? NO : YES; 723 rec = this.records[storeKey]; 724 keys = propertyForStoreKeys ? propertyForStoreKeys[storeKey] : null; 725 726 // Are we invalidating all keys? If so, don't pass any to 727 // storeDidChangeProperties. 728 if (keys === '*') keys = null; 729 730 // remove it so we don't trigger this twice 731 records.remove(storeKey); 732 733 if (rec) rec.storeDidChangeProperties(statusOnly, keys); 734 } 735 736 recordType = SC.Store.recordTypeFor(storeKey); 737 recordTypes.add(recordType); 738 739 }, this); 740 741 if (storeKeys.get('length') > 0) this._notifyRecordArrays(storeKeys, recordTypes); 742 743 storeKeys.clear(); 744 hasDataChanges.clear(); 745 records.clear(); 746 // Provide full reference to overwrite 747 this.recordPropertyChanges.propertyForStoreKeys = {}; 748 749 return this; 750 }, 751 752 /** 753 Resets the store content. This will clear all internal data for all 754 records, resetting them to an EMPTY state. You generally do not want 755 to call this method yourself, though you may override it. 756 757 @returns {SC.Store} receiver 758 */ 759 reset: function() { 760 761 // create a new empty data store 762 this.dataHashes = {} ; 763 this.revisions = {} ; 764 this.statuses = {} ; 765 this.records = {}; 766 this.childRecords = {}; 767 this.parentRecords = {}; 768 769 // also reset temporary objects and errors 770 this.chainedChanges = this.locks = this.editables = null; 771 this.changelog = null ; 772 this.recordErrors = null; 773 this.queryErrors = null; 774 775 var dataSource = this.get('dataSource'); 776 if (dataSource && dataSource.reset) { dataSource.reset(); } 777 778 var records = this.records, storeKey; 779 if (records) { 780 for(storeKey in records) { 781 if (!records.hasOwnProperty(storeKey)) continue ; 782 this._notifyRecordPropertyChange(parseInt(storeKey, 10), NO); 783 } 784 } 785 786 // Also reset all pre-created recordArrays. 787 var ra, raList = this.get('recordArrays'); 788 if (raList) { 789 while ((ra = raList.pop())) { 790 ra.destroy(); 791 } 792 raList.clear(); 793 this.set('recordArrays', null); 794 } 795 796 this.set('hasChanges', NO); 797 }, 798 799 /** @private 800 Called by a nested store on a parent store to commit any changes from the 801 store. This will copy any changed dataHashes as well as any persistent 802 change logs. 803 804 If the parentStore detects a conflict with the optimistic locking, it will 805 raise an exception before it makes any changes. If you pass the 806 force flag then this detection phase will be skipped and the changes will 807 be applied even if another resource has modified the store in the mean 808 time. 809 810 @param {SC.Store} nestedStore the child store 811 @param {SC.Set} changes the set of changed store keys 812 @param {Boolean} force 813 @returns {SC.Store} receiver 814 */ 815 commitChangesFromNestedStore: function (nestedStore, changes, force) { 816 // first, check for optimistic locking problems 817 if (!force && nestedStore.get('conflictedStoreKeys')) { 818 SC.Store.CHAIN_CONFLICT_ERROR.throw(); 819 } 820 821 // OK, no locking issues. So let's just copy them changes. 822 // get local reference to values. 823 var len = changes.length, i, storeKey, myDataHashes, myStatuses, 824 myEditables, myRevisions, myParentRecords, myChildRecords, 825 chDataHashes, chStatuses, chRevisions, chParentRecords, chChildRecords; 826 827 myRevisions = this.revisions ; 828 myDataHashes = this.dataHashes; 829 myStatuses = this.statuses; 830 myEditables = this.editables ; 831 myParentRecords = this.parentRecords ? this.parentRecords : this.parentRecords ={} ; 832 myChildRecords = this.childRecords ? this.childRecords : this.childRecords = {} ; 833 834 // setup some arrays if needed 835 if (!myEditables) myEditables = this.editables = [] ; 836 chDataHashes = nestedStore.dataHashes; 837 chRevisions = nestedStore.revisions ; 838 chStatuses = nestedStore.statuses; 839 chParentRecords = nestedStore.parentRecords || {}; 840 chChildRecords = nestedStore.childRecords || {}; 841 842 for(i=0;i<len;i++) { 843 storeKey = changes[i]; 844 845 // now copy changes 846 myDataHashes[storeKey] = chDataHashes[storeKey]; 847 myStatuses[storeKey] = chStatuses[storeKey]; 848 myRevisions[storeKey] = chRevisions[storeKey]; 849 myParentRecords[storeKey] = chParentRecords[storeKey]; 850 myChildRecords[storeKey] = chChildRecords[storeKey]; 851 852 myEditables[storeKey] = 0 ; // always make dataHash no longer editable 853 854 this._notifyRecordPropertyChange(storeKey, NO); 855 } 856 857 // add any records to the changelog for commit handling 858 var myChangelog = this.changelog, chChangelog = nestedStore.changelog; 859 if (chChangelog) { 860 if (!myChangelog) myChangelog = this.changelog = SC.CoreSet.create(); 861 myChangelog.addEach(chChangelog); 862 } 863 this.changelog = myChangelog; 864 865 // immediately flush changes to notify records - nested stores will flush 866 // later on. 867 if (!this.get('parentStore')) this.flush(); 868 869 return this ; 870 }, 871 872 // .......................................................... 873 // HIGH-LEVEL RECORD API 874 // 875 876 /** 877 Finds a single record instance with the specified `recordType` and id or 878 an array of records matching some query conditions. 879 880 Finding a Single Record 881 --- 882 883 If you pass a single `recordType` and id, this method will return an 884 actual record instance. If the record has not been loaded into the store 885 yet, this method will ask the data source to retrieve it. If no data 886 source indicates that it can retrieve the record, then this method will 887 return `null`. 888 889 Note that if the record needs to be retrieved from the server, then the 890 record instance returned from this method will not have any data yet. 891 Instead it will have a status of `SC.Record.READY_LOADING`. You can 892 monitor the status property to be notified when the record data is 893 available for you to use it. 894 895 Find a Collection of Records 896 --- 897 898 If you pass only a record type or a query object, you can instead find 899 all records matching a specified set of conditions. When you call 900 `find()` in this way, it will create a query if needed and pass it to the 901 data source to fetch the results. 902 903 If this is the first time you have fetched the query, then the store will 904 automatically ask the data source to fetch any records related to it as 905 well. Otherwise you can refresh the query results at anytime by calling 906 `refresh()` on the returned `RecordArray`. 907 908 You can detect whether a RecordArray is fetching from the server by 909 checking its status. 910 911 Examples 912 --- 913 914 Finding a single record: 915 916 MyApp.store.find(MyApp.Contact, "23"); // returns MyApp.Contact 917 918 Finding all records of a particular type: 919 920 MyApp.store.find(MyApp.Contact); // returns SC.RecordArray of contacts 921 922 923 Finding all contacts with first name John: 924 925 var query = SC.Query.local(MyApp.Contact, "firstName = %@", "John"); 926 MyApp.store.find(query); // returns SC.RecordArray of contacts 927 928 Finding all contacts using a remote query: 929 930 var query = SC.Query.remote(MyApp.Contact); 931 MyApp.store.find(query); // returns SC.RecordArray filled by server 932 933 @param {SC.Record|String} recordType the expected record type 934 @param {String} id the id to load 935 @returns {SC.Record} record instance or null 936 */ 937 find: function(recordType, id) { 938 939 // if recordType is passed as string, find object 940 if (SC.typeOf(recordType)===SC.T_STRING) { 941 recordType = SC.objectForPropertyPath(recordType); 942 } 943 944 // handle passing a query... 945 if ((arguments.length === 1) && !(recordType && recordType.get && recordType.get('isRecord'))) { 946 if (!recordType) throw new Error("SC.Store#find() must pass recordType or query"); 947 if (!recordType.isQuery) { 948 recordType = SC.Query.local(recordType); 949 } 950 return this._findQuery(recordType, YES, YES); 951 952 // handle finding a single record 953 } else { 954 return this._findRecord(recordType, id); 955 } 956 }, 957 958 /** @private */ 959 _findQuery: function(query, createIfNeeded, refreshIfNew) { 960 961 // lookup the local RecordArray for this query. 962 var cache = this._scst_recordArraysByQuery, 963 key = SC.guidFor(query), 964 ret, ra ; 965 if (!cache) cache = this._scst_recordArraysByQuery = {}; 966 ret = cache[key]; 967 968 // if a RecordArray was not found, then create one and also add it to the 969 // list of record arrays to update. 970 if (!ret && createIfNeeded) { 971 cache[key] = ret = SC.RecordArray.create({ store: this, query: query }); 972 973 ra = this.get('recordArrays'); 974 if (!ra) this.set('recordArrays', ra = SC.Set.create()); 975 ra.add(ret); 976 977 if (refreshIfNew) this.refreshQuery(query); 978 } 979 980 this.flush(); 981 return ret ; 982 }, 983 984 /** @private */ 985 _findRecord: function(recordType, id) { 986 987 var storeKey ; 988 989 // if a record instance is passed, simply use the storeKey. This allows 990 // you to pass a record from a chained store to get the same record in the 991 // current store. 992 if (recordType && recordType.get && recordType.get('isRecord')) { 993 storeKey = recordType.get('storeKey'); 994 995 // otherwise, lookup the storeKey for the passed id. look in subclasses 996 // as well. 997 } else storeKey = id ? recordType.storeKeyFor(id) : null; 998 999 if (storeKey && (this.peekStatus(storeKey) === SC.Record.EMPTY)) { 1000 storeKey = this.retrieveRecord(recordType, id); 1001 } 1002 1003 // now we have the storeKey, materialize the record and return it. 1004 return storeKey ? this.materializeRecord(storeKey) : null ; 1005 }, 1006 1007 // .......................................................... 1008 // RECORD ARRAY OPERATIONS 1009 // 1010 1011 /** 1012 Called by the record array just before it is destroyed. This will 1013 de-register it from receiving future notifications. 1014 1015 You should never call this method yourself. Instead call `destroy()` on 1016 the `RecordArray` directly. 1017 1018 @param {SC.RecordArray} recordArray the record array 1019 @returns {SC.Store} receiver 1020 */ 1021 recordArrayWillDestroy: function(recordArray) { 1022 var cache = this._scst_recordArraysByQuery, 1023 set = this.get('recordArrays'); 1024 1025 if (cache) delete cache[SC.guidFor(recordArray.get('query'))]; 1026 if (set) set.remove(recordArray); 1027 return this ; 1028 }, 1029 1030 /** 1031 Called by the record array whenever it needs the data source to refresh 1032 its contents. Nested stores will actually just pass this along to the 1033 parent store. The parent store will call `fetch()` on the data source. 1034 1035 You should never call this method yourself. Instead call `refresh()` on 1036 the `RecordArray` directly. 1037 1038 @param {SC.Query} query the record array query to refresh 1039 @returns {SC.Store} receiver 1040 */ 1041 refreshQuery: function(query) { 1042 if (!query) throw new Error("refreshQuery() requires a query"); 1043 1044 var cache = this._scst_recordArraysByQuery, 1045 recArray = cache ? cache[SC.guidFor(query)] : null, 1046 source = this._getDataSource(); 1047 1048 if (source && source.fetch) { 1049 if (recArray) recArray.storeWillFetchQuery(query); 1050 source.fetch.call(source, this, query); 1051 } 1052 1053 return this ; 1054 }, 1055 1056 /** @private 1057 Will ask all record arrays that have been returned from `find` 1058 with an `SC.Query` to check their arrays with the new `storeKey`s 1059 1060 @param {SC.IndexSet} storeKeys set of storeKeys that changed 1061 @param {SC.Set} recordTypes 1062 @returns {SC.Store} receiver 1063 */ 1064 _notifyRecordArrays: function(storeKeys, recordTypes) { 1065 var recordArrays = this.get('recordArrays'); 1066 if (!recordArrays) return this; 1067 1068 recordArrays.forEach(function(recArray) { 1069 if (recArray) recArray.storeDidChangeStoreKeys(storeKeys, recordTypes); 1070 }, this); 1071 1072 return this ; 1073 }, 1074 1075 1076 // .......................................................... 1077 // LOW-LEVEL HELPERS 1078 // 1079 1080 /** 1081 Array of all records currently in the store with the specified 1082 type. This method only reflects the actual records loaded into memory and 1083 therefore is not usually needed at runtime. However you will often use 1084 this method for testing. 1085 1086 @param {SC.Record} recordType the record type 1087 @returns {SC.Array} array instance - usually SC.RecordArray 1088 */ 1089 recordsFor: function(recordType) { 1090 var storeKeys = [], 1091 storeKeysById = recordType.storeKeysById(), 1092 id, storeKey, ret; 1093 1094 // collect all non-empty store keys 1095 for(id in storeKeysById) { 1096 storeKey = storeKeysById[id]; // get the storeKey 1097 if (this.readStatus(storeKey) !== SC.RECORD_EMPTY) { 1098 storeKeys.push(storeKey); 1099 } 1100 } 1101 1102 if (storeKeys.length>0) { 1103 ret = SC.RecordArray.create({ store: this, storeKeys: storeKeys }); 1104 } else ret = storeKeys; // empty array 1105 return ret ; 1106 }, 1107 1108 /** @private */ 1109 _CACHED_REC_ATTRS: {}, 1110 1111 /** @private */ 1112 _CACHED_REC_INIT: function() {}, 1113 1114 /** 1115 Given a `storeKey`, return a materialized record. You will not usually 1116 call this method yourself. Instead it will used by other methods when 1117 you find records by id or perform other searches. 1118 1119 If a `recordType` has been mapped to the storeKey, then a record instance 1120 will be returned even if the data hash has not been requested yet. 1121 1122 Each Store instance returns unique record instances for each storeKey. 1123 1124 @param {Number} storeKey The storeKey for the dataHash. 1125 @returns {SC.Record} Returns a record instance. 1126 */ 1127 materializeRecord: function(storeKey) { 1128 var records = this.records, 1129 ret, recordType, attrs; 1130 1131 // look up in cached records 1132 if (!records) records = this.records = {}; // load cached records 1133 ret = records[storeKey]; 1134 if (ret) return ret; 1135 1136 // not found -- OK, create one then. 1137 recordType = SC.Store.recordTypeFor(storeKey); 1138 if (!recordType) return null; // not recordType registered, nothing to do 1139 1140 // Populate the attributes. 1141 attrs = this._CACHED_REC_ATTRS ; 1142 attrs.storeKey = storeKey ; 1143 attrs.store = this ; 1144 1145 // We do a little gymnastics here to prevent record initialization before we've 1146 // received and cached a copy of the object. This is because if initialization 1147 // triggers downstream effects which call materializeRecord for the same record, 1148 // we won't have a copy of it cached yet, causing another copy to be created 1149 // and resulting in a stack overflow at best and a really hard-to-diagnose bug 1150 // involving two instances of the same record floating around at worst. 1151 1152 // Override _object_init to prevent premature initialization. 1153 var _object_init = recordType.prototype._object_init; 1154 recordType.prototype._object_init = this._CACHED_REC_INIT; 1155 // Create the record (but don't init it). 1156 ret = records[storeKey] = recordType.create(); 1157 // Repopulate the _object_init method and run initialization. 1158 recordType.prototype._object_init = ret._object_init = _object_init; 1159 ret._object_init([attrs]); 1160 1161 return ret ; 1162 }, 1163 1164 // .......................................................... 1165 // CORE RECORDS API 1166 // 1167 // The methods in this section can be used to manipulate records without 1168 // actually creating record instances. 1169 1170 /** 1171 Creates a new record instance with the passed `recordType` and `dataHash`. 1172 You can also optionally specify an id or else it will be pulled from the 1173 data hash. 1174 1175 Example: 1176 1177 MyApp.Record = SC.Record.extend({ 1178 attrA: SC.Record.attr(String, { defaultValue: 'def' }), 1179 isAttrB: SC.Record.attr(Boolean, { key: 'attr_b' }), 1180 primaryKey: 'pKey' 1181 }); 1182 1183 // If you don't provide a value and have designated a defaultValue, the 1184 // defaultValue will be used. 1185 MyApp.store.createRecord(MyApp.Record).get('attributes'); 1186 > { attrA: 'def' } 1187 1188 // If you use a key on an attribute, you can specify the key name or the 1189 // attribute name when creating the record, but if you specify both, only 1190 // the key name will be used. 1191 MyApp.store.createRecord(MyApp.Record, { isAttrB: YES }).get('attributes'); 1192 > { attr_b: YES } 1193 MyApp.store.createRecord(MyApp.Record, { attr_b: YES }).get('attributes'); 1194 > { attr_b: YES } 1195 MyApp.store.createRecord(MyApp.Record, { isAttrB: NO, attr_b: YES }).get('attributes'); 1196 > { attr_b: YES } 1197 1198 Note that the record will not yet be saved back to the server. To save 1199 a record to the server, call `commitChanges()` on the store. 1200 1201 @param {SC.Record} recordType the record class to use on creation 1202 @param {Hash} dataHash the JSON attributes to assign to the hash. 1203 @param {String} id (optional) id to assign to record 1204 1205 @returns {SC.Record} Returns the created record 1206 */ 1207 createRecord: function(recordType, dataHash, id) { 1208 var primaryKey, prototype, storeKey, status, K = SC.Record, changelog, defaultVal, ret; 1209 1210 //initialize dataHash if necessary 1211 dataHash = (dataHash ? dataHash : {}); 1212 1213 // First, try to get an id. If no id is passed, look it up in the 1214 // dataHash. 1215 if (!id && (primaryKey = recordType.prototype.primaryKey)) { 1216 id = dataHash[primaryKey]; 1217 // if still no id, check if there is a defaultValue function for 1218 // the primaryKey attribute and assign that 1219 defaultVal = recordType.prototype[primaryKey] ? recordType.prototype[primaryKey].defaultValue : null; 1220 if(!id && SC.typeOf(defaultVal)===SC.T_FUNCTION) { 1221 id = dataHash[primaryKey] = defaultVal(); 1222 } 1223 } 1224 1225 // Next get the storeKey - base on id if available 1226 storeKey = id ? recordType.storeKeyFor(id) : SC.Store.generateStoreKey(); 1227 1228 // now, check the state and do the right thing. 1229 status = this.readStatus(storeKey); 1230 1231 // check state 1232 // any busy or ready state or destroyed dirty state is not allowed 1233 if ((status & K.BUSY) || 1234 (status & K.READY) || 1235 (status === K.DESTROYED_DIRTY)) { 1236 (id ? K.RECORD_EXISTS_ERROR : K.BAD_STATE_ERROR).throw(); 1237 1238 // allow error or destroyed state only with id 1239 } else if (!id && (status===SC.DESTROYED_CLEAN || status===SC.ERROR)) { 1240 K.BAD_STATE_ERROR.throw(); 1241 } 1242 1243 // Store the dataHash and setup initial status. 1244 this.writeDataHash(storeKey, dataHash, K.READY_NEW); 1245 1246 // Register the recordType with the store. 1247 SC.Store.replaceRecordTypeFor(storeKey, recordType); 1248 this.dataHashDidChange(storeKey); 1249 1250 // If the attribute wasn't provided in the dataHash, attempt to insert a 1251 // default value. We have to do this after materializing the record, 1252 // because the defaultValue property may be a function that expects 1253 // the record as an argument. 1254 ret = this.materializeRecord(storeKey); 1255 prototype = recordType.prototype; 1256 for (var prop in prototype) { 1257 var propPrototype = prototype[ prop ]; 1258 if (propPrototype && propPrototype.isRecordAttribute) { 1259 // Use the record attribute key if it is defined. 1260 var attrKey = propPrototype.key || prop; 1261 1262 if (!dataHash.hasOwnProperty(attrKey)) { 1263 if (dataHash.hasOwnProperty(prop)) { 1264 // If the attribute key doesn't exist but the name does, fix it up. 1265 // (i.e. the developer has a record attribute `endDate` with a key 1266 // `end_date` on a record and when they created the record they 1267 // provided `endDate` not `end_date`) 1268 dataHash[ attrKey ] = dataHash[ prop ]; 1269 delete dataHash[ prop ]; 1270 } else { 1271 // If the attribute doesn't exist in the hash at all, check for a 1272 // default value to use instead. 1273 defaultVal = propPrototype.defaultValue; 1274 if (defaultVal) { 1275 if (SC.typeOf(defaultVal)===SC.T_FUNCTION) dataHash[ attrKey ] = SC.copy(defaultVal(ret, attrKey), YES); 1276 else dataHash[ attrKey ] = SC.copy(defaultVal, YES); 1277 } 1278 } 1279 } else if (attrKey !== prop && dataHash.hasOwnProperty(prop)) { 1280 // If both attrKey and prop are provided, use attrKey only. 1281 delete dataHash[ prop ]; 1282 } 1283 } 1284 } 1285 1286 // Record is now in a committable state -- add storeKey to changelog 1287 changelog = this.changelog; 1288 if (!changelog) changelog = SC.Set.create(); 1289 changelog.add(storeKey); 1290 this.changelog = changelog; 1291 1292 // if commit records is enabled 1293 if(this.get('commitRecordsAutomatically')){ 1294 this.invokeLast(this.commitRecords); 1295 } 1296 1297 // Propagate the status to any aggregate records before returning. 1298 if (ret) ret.propagateToAggregates(); 1299 return ret; 1300 }, 1301 1302 /** 1303 Creates an array of new records. You must pass an array of `dataHash`es 1304 plus a `recordType` and, optionally, an array of ids. This will create an 1305 array of record instances with the same record type. 1306 1307 If you need to instead create a bunch of records with different data types 1308 you can instead pass an array of `recordType`s, one for each data hash. 1309 1310 @param {SC.Record|Array} recordTypes class or array of classes 1311 @param {Array} dataHashes array of data hashes 1312 @param {Array} ids (optional) ids to assign to records 1313 @returns {Array} array of materialized record instances. 1314 */ 1315 createRecords: function(recordTypes, dataHashes, ids) { 1316 var ret = [], recordType, id, isArray, len = dataHashes.length, idx ; 1317 isArray = SC.typeOf(recordTypes) === SC.T_ARRAY; 1318 if (!isArray) recordType = recordTypes; 1319 for(idx=0;idx<len;idx++) { 1320 if (isArray) recordType = recordTypes[idx] || SC.Record; 1321 id = ids ? ids[idx] : undefined ; 1322 ret.push(this.createRecord(recordType, dataHashes[idx], id)); 1323 } 1324 return ret ; 1325 }, 1326 1327 1328 /** 1329 Unloads a record, removing the data hash from the store. If you try to 1330 unload a record that is already destroyed then this method will have no effect. 1331 If you unload a record that does not exist or an error then an exception 1332 will be raised. 1333 1334 @param {SC.Record} recordType the recordType 1335 @param {String} id the record id 1336 @param {Number} storeKey (optional) if passed, ignores recordType and id 1337 @returns {SC.Store} receiver 1338 */ 1339 unloadRecord: function(recordType, id, storeKey, newStatus) { 1340 if (storeKey === undefined) storeKey = recordType.storeKeyFor(id); 1341 var status = this.readStatus(storeKey), K = SC.Record; 1342 newStatus = newStatus || K.EMPTY; 1343 // handle status - ignore if destroying or destroyed 1344 if ((status === K.BUSY_DESTROYING) || (status & K.DESTROYED)) { 1345 return this; // nothing to do 1346 1347 // error out if empty 1348 } else if (status & K.BUSY) { 1349 K.BUSY_ERROR.throw(); 1350 1351 // otherwise, destroy in dirty state 1352 } else status = newStatus ; 1353 1354 // remove the data hash, set the new status and remove the cached record. 1355 this.removeDataHash(storeKey, status); 1356 this.dataHashDidChange(storeKey); 1357 delete this.records[storeKey]; 1358 1359 // If this record is a parent record, unregister all of its child records. 1360 var that = this; 1361 this._propagateToChildren(storeKey, function (storeKey) { 1362 that.unregisterChildFromParent(storeKey); 1363 }); 1364 1365 // If this record is a parent record, its child records have been cleared so also clear the 1366 // cached reference as well. 1367 if (this.parentRecords) { 1368 delete this.parentRecords[storeKey]; 1369 } 1370 1371 return this ; 1372 }, 1373 1374 /** 1375 Unloads a group of records. If you have a set of record ids, unloading 1376 them this way can be faster than retrieving each record and unloading 1377 it individually. 1378 1379 You can pass either a single `recordType` or an array of `recordType`s. If 1380 you pass a single `recordType`, then the record type will be used for each 1381 record. If you pass an array, then each id must have a matching record 1382 type in the array. 1383 1384 You can optionally pass an array of `storeKey`s instead of the `recordType` 1385 and ids. In this case the first two parameters will be ignored. This 1386 is usually only used by low-level internal methods. You will not usually 1387 unload records this way. 1388 1389 @param {SC.Record|Array} recordTypes class or array of classes 1390 @param {Array} [ids] ids to unload 1391 @param {Array} [storeKeys] store keys to unload 1392 @returns {SC.Store} receiver 1393 */ 1394 unloadRecords: function(recordTypes, ids, storeKeys, newStatus) { 1395 var len, isArray, idx, id, recordType, storeKey; 1396 1397 if (storeKeys === undefined) { 1398 isArray = SC.typeOf(recordTypes) === SC.T_ARRAY; 1399 if (!isArray) recordType = recordTypes; 1400 if (ids === undefined) { 1401 len = isArray ? recordTypes.length : 1; 1402 for (idx = 0; idx < len; idx++) { 1403 if (isArray) recordType = recordTypes[idx]; 1404 storeKeys = this.storeKeysFor(recordType); 1405 this.unloadRecords(undefined, undefined, storeKeys, newStatus); 1406 } 1407 } else { 1408 len = ids.length; 1409 for (idx = 0; idx < len; idx++) { 1410 if (isArray) recordType = recordTypes[idx] || SC.Record; 1411 id = ids ? ids[idx] : undefined; 1412 this.unloadRecord(recordType, id, undefined, newStatus); 1413 } 1414 } 1415 } else { 1416 len = storeKeys.length; 1417 for (idx = 0; idx < len; idx++) { 1418 storeKey = storeKeys ? storeKeys[idx] : undefined; 1419 this.unloadRecord(undefined, undefined, storeKey, newStatus); 1420 } 1421 } 1422 1423 return this; 1424 }, 1425 1426 /** 1427 Destroys a record, removing the data hash from the store and adding the 1428 record to the destroyed changelog. If you try to destroy a record that is 1429 already destroyed then this method will have no effect. If you destroy a 1430 record that does not exist or an error then an exception will be raised. 1431 1432 @param {SC.Record} recordType the recordType 1433 @param {String} id the record id 1434 @param {Number} storeKey (optional) if passed, ignores recordType and id 1435 @returns {SC.Store} receiver 1436 */ 1437 destroyRecord: function(recordType, id, storeKey) { 1438 if (storeKey === undefined) storeKey = recordType.storeKeyFor(id); 1439 var status = this.readStatus(storeKey), changelog, K = SC.Record; 1440 1441 // handle status - ignore if destroying or destroyed 1442 if ((status === K.BUSY_DESTROYING) || (status & K.DESTROYED)) { 1443 return this; // nothing to do 1444 1445 // error out if empty 1446 } else if (status === K.EMPTY) { 1447 K.NOT_FOUND_ERROR.throw(); 1448 1449 // error out if busy 1450 } else if (status & K.BUSY) { 1451 K.BUSY_ERROR.throw(); 1452 1453 // if new status, destroy in clean state 1454 } else if (status === K.READY_NEW) { 1455 status = K.DESTROYED_CLEAN ; 1456 this.removeDataHash(storeKey, status) ; 1457 1458 // otherwise, destroy in dirty state 1459 } else status = K.DESTROYED_DIRTY ; 1460 1461 // remove the data hash, set new status 1462 this.writeStatus(storeKey, status); 1463 this.dataHashDidChange(storeKey); 1464 1465 // add/remove change log 1466 changelog = this.changelog; 1467 if (!changelog) changelog = this.changelog = SC.Set.create(); 1468 1469 if (status & K.DIRTY) { changelog.add(storeKey); } 1470 else { changelog.remove(storeKey); } 1471 this.changelog=changelog; 1472 1473 // if commit records is enabled 1474 if(this.get('commitRecordsAutomatically')){ 1475 this.invokeLast(this.commitRecords); 1476 } 1477 1478 var that = this; 1479 this._propagateToChildren(storeKey, function(storeKey){ 1480 that.destroyRecord(null, null, storeKey); 1481 }); 1482 1483 return this ; 1484 }, 1485 1486 /** 1487 Destroys a group of records. If you have a set of record ids, destroying 1488 them this way can be faster than retrieving each record and destroying 1489 it individually. 1490 1491 You can pass either a single `recordType` or an array of `recordType`s. If 1492 you pass a single `recordType`, then the record type will be used for each 1493 record. If you pass an array, then each id must have a matching record 1494 type in the array. 1495 1496 You can optionally pass an array of `storeKey`s instead of the `recordType` 1497 and ids. In this case the first two parameters will be ignored. This 1498 is usually only used by low-level internal methods. You will not usually 1499 destroy records this way. 1500 1501 @param {SC.Record|Array} recordTypes class or array of classes 1502 @param {Array} ids ids to destroy 1503 @param {Array} storeKeys (optional) store keys to destroy 1504 @returns {SC.Store} receiver 1505 */ 1506 destroyRecords: function(recordTypes, ids, storeKeys) { 1507 var len, isArray, idx, id, recordType, storeKey; 1508 if(storeKeys===undefined){ 1509 len = ids.length; 1510 isArray = SC.typeOf(recordTypes) === SC.T_ARRAY; 1511 if (!isArray) recordType = recordTypes; 1512 for(idx=0;idx<len;idx++) { 1513 if (isArray) recordType = recordTypes[idx] || SC.Record; 1514 id = ids ? ids[idx] : undefined ; 1515 this.destroyRecord(recordType, id, undefined); 1516 } 1517 }else{ 1518 len = storeKeys.length; 1519 for(idx=0;idx<len;idx++) { 1520 storeKey = storeKeys ? storeKeys[idx] : undefined ; 1521 this.destroyRecord(undefined, undefined, storeKey); 1522 } 1523 } 1524 return this ; 1525 }, 1526 1527 /** 1528 register a Child Record to the parent 1529 */ 1530 registerChildToParent: function(parentStoreKey, childStoreKey, path){ 1531 var parentRecords, childRecords, oldPk, oldChildren, pkRef; 1532 1533 // Check the child to see if it has a parent 1534 childRecords = this.childRecords || {}; 1535 parentRecords = this.parentRecords || {}; 1536 1537 // first rid of the old parent 1538 oldPk = childRecords[childStoreKey]; 1539 if (oldPk){ 1540 oldChildren = parentRecords[oldPk]; 1541 delete oldChildren[childStoreKey]; 1542 // this.recordDidChange(null, null, oldPk, key); 1543 } 1544 pkRef = parentRecords[parentStoreKey] || {}; 1545 pkRef[childStoreKey] = path || YES; 1546 parentRecords[parentStoreKey] = pkRef; 1547 childRecords[childStoreKey] = parentStoreKey; 1548 1549 // sync the status of the child 1550 this.writeStatus(childStoreKey, this.statuses[parentStoreKey]); 1551 this.childRecords = childRecords; 1552 this.parentRecords = parentRecords; 1553 }, 1554 1555 /** 1556 Unregister the Child Record from its Parent. This will cause the Child 1557 Record to be removed from the store. 1558 1559 @param {Number} childStoreKey storeKey to unregister 1560 */ 1561 unregisterChildFromParent: function(childStoreKey) { 1562 var childRecords, oldPk, storeKeys, 1563 recordType = this.recordTypeFor(childStoreKey), 1564 id = this.idFor(childStoreKey), 1565 that = this; 1566 1567 // Check the child to see if it has a parent 1568 childRecords = this.childRecords; 1569 1570 // Remove the child. 1571 // 1. from the cache of data hashes 1572 // 2. from the cache of record objects 1573 // 3. from the cache of child record store keys 1574 this.removeDataHash(childStoreKey); 1575 if (this.records) { 1576 delete this.records[childStoreKey]; 1577 } 1578 1579 if (childRecords) { 1580 // Remove the parent's connection to the child. This doesn't remove the 1581 // parent store key from the cache of parent store keys if the parent 1582 // no longer has any other registered children, because the amount of effort 1583 // to determine that would not be worth the miniscule memory savings. 1584 oldPk = childRecords[childStoreKey]; 1585 if (oldPk) { 1586 delete this.parentRecords[oldPk][childStoreKey]; 1587 } 1588 1589 delete childRecords[childStoreKey]; 1590 } 1591 1592 // 4. from the cache of ids 1593 // 5. from the cache of store keys 1594 delete SC.Store.idsByStoreKey[childStoreKey]; 1595 storeKeys = recordType.storeKeysById(); 1596 delete storeKeys[id]; 1597 1598 this._propagateToChildren(childStoreKey, function(storeKey) { 1599 that.unregisterChildFromParent(storeKey); 1600 }); 1601 }, 1602 1603 /** 1604 materialize the parent when passing in a store key for the child 1605 */ 1606 materializeParentRecord: function(childStoreKey){ 1607 var pk, crs; 1608 if (SC.none(childStoreKey)) return null; 1609 crs = this.childRecords; 1610 pk = crs ? this.childRecords[childStoreKey] : null ; 1611 if (SC.none(pk)) return null; 1612 1613 return this.materializeRecord(pk); 1614 }, 1615 1616 /** 1617 function for retrieving a parent record key 1618 1619 @param {Number} storeKey The store key of the parent 1620 */ 1621 parentStoreKeyExists: function(storeKey){ 1622 if (SC.none(storeKey)) return ; 1623 var crs = this.childRecords || {}; 1624 return crs[storeKey]; 1625 }, 1626 1627 /** 1628 function that propagates a function call to all children 1629 */ 1630 _propagateToChildren: function(storeKey, func){ 1631 // Handle all the child Records 1632 if ( SC.none(this.parentRecords) ) return; 1633 var children = this.parentRecords[storeKey] || {}; 1634 if (SC.none(func)) return; 1635 for (var key in children) { 1636 // for .. in makes the key a String, but be sure to pass a Number to the 1637 // function. 1638 if (children.hasOwnProperty(key)) func(parseInt(key, 10)); 1639 } 1640 }, 1641 1642 /** 1643 Notes that the data for the given record id has changed. The record will 1644 be committed to the server the next time you commit the root store. Only 1645 call this method on a record in a READY state of some type. 1646 1647 @param {SC.Record} recordType the recordType 1648 @param {String} id the record id 1649 @param {Number} storeKey (optional) if passed, ignores recordType and id 1650 @param {String} key that changed (optional) 1651 @param {Boolean} if the change is to statusOnly (optional) 1652 @returns {SC.Store} receiver 1653 */ 1654 recordDidChange: function(recordType, id, storeKey, key, statusOnly) { 1655 if (storeKey === undefined) storeKey = recordType.storeKeyFor(id); 1656 var status = this.readStatus(storeKey), changelog, K = SC.Record; 1657 1658 // BUSY_LOADING, BUSY_CREATING, BUSY_COMMITTING, BUSY_REFRESH_CLEAN 1659 // BUSY_REFRESH_DIRTY, BUSY_DESTROYING 1660 if (status & K.BUSY) { 1661 K.BUSY_ERROR.throw(); 1662 1663 // if record is not in ready state, then it is not found. 1664 // ERROR, EMPTY, DESTROYED_CLEAN, DESTROYED_DIRTY 1665 } else if (!(status & K.READY)) { 1666 K.NOT_FOUND_ERROR.throw(); 1667 1668 // otherwise, make new status READY_DIRTY unless new. 1669 // K.READY_CLEAN, K.READY_DIRTY, ignore K.READY_NEW 1670 } else { 1671 if (status !== K.READY_NEW) this.writeStatus(storeKey, K.READY_DIRTY); 1672 } 1673 1674 // record data hash change 1675 this.dataHashDidChange(storeKey, null, statusOnly, key); 1676 1677 // record in changelog 1678 changelog = this.changelog ; 1679 if (!changelog) changelog = this.changelog = SC.Set.create() ; 1680 changelog.add(storeKey); 1681 this.changelog = changelog; 1682 1683 // if commit records is enabled 1684 if(this.get('commitRecordsAutomatically')){ 1685 this.invokeLast(this.commitRecords); 1686 } 1687 1688 return this ; 1689 }, 1690 1691 /** 1692 Mark a group of records as dirty. The records will be committed to the 1693 server the next time you commit changes on the root store. If you have a 1694 set of record ids, marking them dirty this way can be faster than 1695 retrieving each record and destroying it individually. 1696 1697 You can pass either a single `recordType` or an array of `recordType`s. If 1698 you pass a single `recordType`, then the record type will be used for each 1699 record. If you pass an array, then each id must have a matching record 1700 type in the array. 1701 1702 You can optionally pass an array of `storeKey`s instead of the `recordType` 1703 and ids. In this case the first two parameters will be ignored. This 1704 is usually only used by low-level internal methods. 1705 1706 @param {SC.Record|Array} recordTypes class or array of classes 1707 @param {Array} ids ids to destroy 1708 @param {Array} storeKeys (optional) store keys to destroy 1709 @returns {SC.Store} receiver 1710 */ 1711 recordsDidChange: function(recordTypes, ids, storeKeys) { 1712 var len, isArray, idx, id, recordType, storeKey; 1713 if(storeKeys===undefined){ 1714 len = ids.length; 1715 isArray = SC.typeOf(recordTypes) === SC.T_ARRAY; 1716 if (!isArray) recordType = recordTypes; 1717 for(idx=0;idx<len;idx++) { 1718 if (isArray) recordType = recordTypes[idx] || SC.Record; 1719 id = ids ? ids[idx] : undefined ; 1720 storeKey = storeKeys ? storeKeys[idx] : undefined ; 1721 this.recordDidChange(recordType, id, storeKey); 1722 } 1723 }else{ 1724 len = storeKeys.length; 1725 for(idx=0;idx<len;idx++) { 1726 storeKey = storeKeys ? storeKeys[idx] : undefined ; 1727 this.recordDidChange(undefined, undefined, storeKey); 1728 } 1729 } 1730 return this ; 1731 }, 1732 1733 /** 1734 Retrieves a set of records from the server. If the records has 1735 already been loaded in the store, then this method will simply return. 1736 Otherwise if your store has a `dataSource`, this will call the 1737 `dataSource` to retrieve the record. Generally you will not need to 1738 call this method yourself. Instead you can just use `find()`. 1739 1740 This will not actually create a record instance but it will initiate a 1741 load of the record from the server. You can subsequently get a record 1742 instance itself using `materializeRecord()`. 1743 1744 @param {SC.Record|Array} recordTypes class or array of classes 1745 @param {Array} ids ids to retrieve 1746 @param {Array} storeKeys (optional) store keys to retrieve 1747 @param {Boolean} isRefresh 1748 @param {Function|Array} callback function or array of functions 1749 @returns {Array} storeKeys to be retrieved 1750 */ 1751 retrieveRecords: function(recordTypes, ids, storeKeys, isRefresh, callbacks) { 1752 1753 var source = this._getDataSource(), 1754 isArray = SC.typeOf(recordTypes) === SC.T_ARRAY, 1755 hasCallbackArray = SC.typeOf(callbacks) === SC.T_ARRAY, 1756 len = (!storeKeys) ? ids.length : storeKeys.length, 1757 ret = [], 1758 rev = SC.Store.generateStoreKey(), 1759 K = SC.Record, 1760 recordType, idx, storeKey, status, ok, callback; 1761 1762 if (!isArray) recordType = recordTypes; 1763 1764 // if no storeKeys were passed, map recordTypes + ids 1765 for(idx=0;idx<len;idx++) { 1766 1767 // collect store key 1768 if (storeKeys) { 1769 storeKey = storeKeys[idx]; 1770 } else { 1771 if (isArray) recordType = recordTypes[idx]; 1772 storeKey = recordType.storeKeyFor(ids[idx]); 1773 } 1774 //collect the callback 1775 callback = hasCallbackArray ? callbacks[idx] : callbacks; 1776 1777 // collect status and process 1778 status = this.readStatus(storeKey); 1779 1780 // K.EMPTY, K.ERROR, K.DESTROYED_CLEAN - initial retrieval 1781 if ((status === K.EMPTY) || (status === K.ERROR) || (status === K.DESTROYED_CLEAN)) { 1782 this.writeStatus(storeKey, K.BUSY_LOADING); 1783 this.dataHashDidChange(storeKey, rev, YES); 1784 ret.push(storeKey); 1785 this._setCallbackForStoreKey(storeKey, callback, hasCallbackArray, storeKeys); 1786 // otherwise, ignore record unless isRefresh is YES. 1787 } else if (isRefresh) { 1788 // K.READY_CLEAN, K.READY_DIRTY, ignore K.READY_NEW 1789 if (status & K.READY) { 1790 this.writeStatus(storeKey, K.BUSY_REFRESH | (status & 0x03)) ; 1791 this.dataHashDidChange(storeKey, rev, YES); 1792 ret.push(storeKey); 1793 this._setCallbackForStoreKey(storeKey, callback, hasCallbackArray, storeKeys); 1794 // K.BUSY_DESTROYING, K.BUSY_COMMITTING, K.BUSY_CREATING 1795 } else if ((status === K.BUSY_DESTROYING) || (status === K.BUSY_CREATING) || (status === K.BUSY_COMMITTING)) { 1796 K.BUSY_ERROR.throw(); 1797 1798 // K.DESTROY_DIRTY, bad state... 1799 } else if (status === K.DESTROYED_DIRTY) { 1800 K.BAD_STATE_ERROR.throw(); 1801 1802 // ignore K.BUSY_LOADING, K.BUSY_REFRESH_CLEAN, K.BUSY_REFRESH_DIRTY 1803 } 1804 } 1805 } 1806 1807 // now retrieve storeKeys from dataSource. if there is no dataSource, 1808 // then act as if we couldn't retrieve. 1809 ok = NO; 1810 if (source) ok = source.retrieveRecords.call(source, this, ret, ids); 1811 1812 // if the data source could not retrieve or if there is no source, then 1813 // simulate the data source calling dataSourceDidError on those we are 1814 // loading for the first time or dataSourceDidComplete on refreshes. 1815 if (!ok) { 1816 len = ret.length; 1817 rev = SC.Store.generateStoreKey(); 1818 for(idx=0;idx<len;idx++) { 1819 storeKey = ret[idx]; 1820 status = this.readStatus(storeKey); 1821 if (status === K.BUSY_LOADING) { 1822 this.writeStatus(storeKey, K.ERROR); 1823 this.dataHashDidChange(storeKey, rev, YES); 1824 1825 } else if (status & K.BUSY_REFRESH) { 1826 this.writeStatus(storeKey, K.READY | (status & 0x03)); 1827 this.dataHashDidChange(storeKey, rev, YES); 1828 } 1829 } 1830 ret.length = 0 ; // truncate to indicate that none could refresh 1831 } 1832 return ret ; 1833 }, 1834 1835 _TMP_RETRIEVE_ARRAY: [], 1836 1837 _callback_queue: {}, 1838 1839 /** 1840 @private 1841 stores the callbacks for the storeKeys that are inflight 1842 **/ 1843 _setCallbackForStoreKey: function(storeKey, callback, hasCallbackArray, storeKeys){ 1844 var queue = this._callback_queue; 1845 if(hasCallbackArray) queue[storeKey] = {callback: callback, otherKeys: storeKeys}; 1846 else queue[storeKey] = callback; 1847 }, 1848 1849 /** 1850 @private 1851 Retrieves and calls callback for `storeKey` if exists, also handles if a single callback is 1852 needed for one key.. 1853 **/ 1854 _retrieveCallbackForStoreKey: function(storeKey){ 1855 var queue = this._callback_queue, 1856 callback = queue[storeKey], 1857 allFinished, keys; 1858 if(callback){ 1859 if(SC.typeOf(callback) === SC.T_FUNCTION){ 1860 callback.call(); //args? 1861 delete queue[storeKey]; //cleanup 1862 } 1863 else if(SC.typeOf(callback) === SC.T_HASH){ 1864 callback.completed = YES; 1865 keys = callback.storeKeys; 1866 keys.forEach(function(key){ 1867 if(!queue[key].completed) allFinished = YES; 1868 }); 1869 if(allFinished){ 1870 callback.callback.call(); // args? 1871 //cleanup 1872 keys.forEach(function(key){ 1873 delete queue[key]; 1874 }); 1875 } 1876 1877 } 1878 } 1879 }, 1880 1881 /* 1882 @private 1883 1884 */ 1885 _cancelCallback: function(storeKey){ 1886 var queue = this._callback_queue; 1887 if(queue[storeKey]){ 1888 delete queue[storeKey]; 1889 } 1890 }, 1891 1892 1893 /** 1894 Retrieves a record from the server. If the record has already been loaded 1895 in the store, then this method will simply return. Otherwise if your 1896 store has a `dataSource`, this will call the `dataSource` to retrieve the 1897 record. Generally you will not need to call this method yourself. 1898 Instead you can just use `find()`. 1899 1900 This will not actually create a record instance but it will initiate a 1901 load of the record from the server. You can subsequently get a record 1902 instance itself using `materializeRecord()`. 1903 1904 @param {SC.Record} recordType class 1905 @param {String} id id to retrieve 1906 @param {Number} storeKey (optional) store key 1907 @param {Boolean} isRefresh 1908 @param {Function} callback (optional) 1909 @returns {Number} storeKey that was retrieved 1910 */ 1911 retrieveRecord: function(recordType, id, storeKey, isRefresh, callback) { 1912 var array = this._TMP_RETRIEVE_ARRAY, 1913 ret; 1914 1915 if (storeKey) { 1916 array[0] = storeKey; 1917 storeKey = array; 1918 id = null ; 1919 } else { 1920 array[0] = id; 1921 id = array; 1922 } 1923 1924 ret = this.retrieveRecords(recordType, id, storeKey, isRefresh, callback); 1925 array.length = 0 ; 1926 return ret[0]; 1927 }, 1928 1929 /** 1930 Refreshes a record from the server. If the record has already been loaded 1931 in the store, then this method will request a refresh from the 1932 `dataSource`. Otherwise it will attempt to retrieve the record. 1933 1934 @param {SC.Record} recordType the expected record type 1935 @param {String} id to id of the record to load 1936 @param {Number} storeKey (optional) optional store key 1937 @param {Function} callback (optional) when refresh completes 1938 @returns {Boolean} YES if the retrieval was a success. 1939 */ 1940 refreshRecord: function(recordType, id, storeKey, callback) { 1941 return !!this.retrieveRecord(recordType, id, storeKey, YES, callback); 1942 }, 1943 1944 /** 1945 Refreshes a set of records from the server. If the records has already been loaded 1946 in the store, then this method will request a refresh from the 1947 `dataSource`. Otherwise it will attempt to retrieve them. 1948 1949 @param {SC.Record|Array} recordTypes class or array of classes 1950 @param {Array} ids ids to destroy 1951 @param {Array} storeKeys (optional) store keys to destroy 1952 @param {Function} callback (optional) when refresh completes 1953 @returns {Boolean} YES if the retrieval was a success. 1954 */ 1955 refreshRecords: function(recordTypes, ids, storeKeys, callback) { 1956 var ret = this.retrieveRecords(recordTypes, ids, storeKeys, YES, callback); 1957 return ret && ret.length>0; 1958 }, 1959 1960 /** 1961 Commits the passed store keys or ids. If no `storeKey`s are given, 1962 it will commit any records in the changelog. 1963 1964 Based on the current state of the record, this will ask the data 1965 source to perform the appropriate actions 1966 on the store keys. 1967 1968 @param {Array} recordTypes the expected record types (SC.Record) 1969 @param {Array} ids to commit 1970 @param {SC.Set} storeKeys to commit 1971 @param {Hash} params optional additional parameters to pass along to the 1972 data source 1973 @param {Function|Array} callback function or array of callbacks 1974 1975 @returns {Boolean} if the action was succesful. 1976 */ 1977 commitRecords: function(recordTypes, ids, storeKeys, params, callbacks) { 1978 var source = this._getDataSource(), 1979 isArray = SC.typeOf(recordTypes) === SC.T_ARRAY, 1980 hasCallbackArray = SC.typeOf(callbacks) === SC.T_ARRAY, 1981 retCreate= [], retUpdate= [], retDestroy = [], 1982 rev = SC.Store.generateStoreKey(), 1983 K = SC.Record, 1984 recordType, idx, storeKey, status, ret, len, callback; 1985 1986 // If no params are passed, look up storeKeys in the changelog property. 1987 // Remove any committed records from changelog property. 1988 1989 if(!recordTypes && !ids && !storeKeys){ 1990 storeKeys = this.changelog; 1991 } 1992 1993 len = storeKeys ? storeKeys.get('length') : (ids ? ids.get('length') : 0); 1994 1995 for(idx=0;idx<len;idx++) { 1996 1997 // collect store key 1998 if (storeKeys) { 1999 storeKey = storeKeys[idx]; 2000 } else { 2001 if (isArray) recordType = recordTypes[idx] || SC.Record; 2002 else recordType = recordTypes; 2003 storeKey = recordType.storeKeyFor(ids[idx]); 2004 } 2005 2006 //collect the callback 2007 callback = hasCallbackArray ? callbacks[idx] : callbacks; 2008 2009 // collect status and process 2010 status = this.readStatus(storeKey); 2011 2012 if (status === K.ERROR) { 2013 K.NOT_FOUND_ERROR.throw(); 2014 } 2015 else { 2016 if(status === K.READY_NEW) { 2017 this.writeStatus(storeKey, K.BUSY_CREATING); 2018 this.dataHashDidChange(storeKey, rev, YES); 2019 retCreate.push(storeKey); 2020 this._setCallbackForStoreKey(storeKey, callback, hasCallbackArray, storeKeys); 2021 } else if (status === K.READY_DIRTY) { 2022 this.writeStatus(storeKey, K.BUSY_COMMITTING); 2023 this.dataHashDidChange(storeKey, rev, YES); 2024 retUpdate.push(storeKey); 2025 this._setCallbackForStoreKey(storeKey, callback, hasCallbackArray, storeKeys); 2026 } else if (status === K.DESTROYED_DIRTY) { 2027 this.writeStatus(storeKey, K.BUSY_DESTROYING); 2028 this.dataHashDidChange(storeKey, rev, YES); 2029 retDestroy.push(storeKey); 2030 this._setCallbackForStoreKey(storeKey, callback, hasCallbackArray, storeKeys); 2031 } else if (status === K.DESTROYED_CLEAN) { 2032 this.dataHashDidChange(storeKey, rev, YES); 2033 } 2034 // ignore K.EMPTY, K.READY_CLEAN, K.BUSY_LOADING, K.BUSY_CREATING, K.BUSY_COMMITTING, 2035 // K.BUSY_REFRESH_CLEAN, K_BUSY_REFRESH_DIRTY, KBUSY_DESTROYING 2036 } 2037 } 2038 2039 // now commit storeKeys to dataSource 2040 if (source && (len>0 || params)) { 2041 ret = source.commitRecords.call(source, this, retCreate, retUpdate, retDestroy, params); 2042 } 2043 2044 //remove all committed changes from changelog 2045 if (ret && !recordTypes && !ids) { 2046 if (storeKeys === this.changelog) { 2047 this.changelog = null; 2048 } 2049 else { 2050 this.changelog.removeEach(storeKeys); 2051 } 2052 } 2053 return ret ; 2054 }, 2055 2056 /** 2057 Commits the passed store key or id. Based on the current state of the 2058 record, this will ask the data source to perform the appropriate action 2059 on the store key. 2060 2061 You have to pass either the id or the storeKey otherwise it will return 2062 NO. 2063 2064 @param {SC.Record} recordType the expected record type 2065 @param {String} id the id of the record to commit 2066 @param {Number} storeKey the storeKey of the record to commit 2067 @param {Hash} params optional additional params that will passed down 2068 to the data source 2069 @param {Function|Array} callback function or array of functions 2070 @returns {Boolean} if the action was successful. 2071 */ 2072 commitRecord: function(recordType, id, storeKey, params, callback) { 2073 var array = this._TMP_RETRIEVE_ARRAY, 2074 ret ; 2075 if (id === undefined && storeKey === undefined ) return NO; 2076 if (storeKey !== undefined) { 2077 array[0] = storeKey; 2078 storeKey = array; 2079 id = null ; 2080 } else { 2081 array[0] = id; 2082 id = array; 2083 } 2084 2085 ret = this.commitRecords(recordType, id, storeKey, params, callback); 2086 array.length = 0 ; 2087 return ret; 2088 }, 2089 2090 /** 2091 Cancels an inflight request for the passed records. Depending on the 2092 server implementation, this could cancel an entire request, causing 2093 other records to also transition their current state. 2094 2095 @param {SC.Record|Array} recordTypes class or array of classes 2096 @param {Array} ids ids to destroy 2097 @param {Array} storeKeys (optional) store keys to destroy 2098 @returns {SC.Store} the store. 2099 */ 2100 cancelRecords: function(recordTypes, ids, storeKeys) { 2101 var source = this._getDataSource(), 2102 isArray = SC.typeOf(recordTypes) === SC.T_ARRAY, 2103 K = SC.Record, 2104 ret = [], 2105 status, len, idx, id, recordType, storeKey; 2106 2107 len = (storeKeys === undefined) ? ids.length : storeKeys.length; 2108 for(idx=0;idx<len;idx++) { 2109 if (isArray) recordType = recordTypes[idx] || SC.Record; 2110 else recordType = recordTypes || SC.Record; 2111 2112 id = ids ? ids[idx] : undefined ; 2113 2114 if(storeKeys===undefined){ 2115 storeKey = recordType.storeKeyFor(id); 2116 }else{ 2117 storeKey = storeKeys ? storeKeys[idx] : undefined ; 2118 } 2119 if(storeKey) { 2120 status = this.readStatus(storeKey); 2121 2122 if ((status === K.EMPTY) || (status === K.ERROR)) { 2123 K.NOT_FOUND_ERROR.throw(); 2124 } 2125 ret.push(storeKey); 2126 this._cancelCallback(storeKey); 2127 } 2128 } 2129 2130 if (source) source.cancel.call(source, this, ret); 2131 2132 return this ; 2133 }, 2134 2135 /** 2136 Cancels an inflight request for the passed record. Depending on the 2137 server implementation, this could cancel an entire request, causing 2138 other records to also transition their current state. 2139 2140 @param {SC.Record|Array} recordTypes class or array of classes 2141 @param {Array} ids ids to destroy 2142 @param {Array} storeKeys (optional) store keys to destroy 2143 @returns {SC.Store} the store. 2144 */ 2145 cancelRecord: function(recordType, id, storeKey) { 2146 var array = this._TMP_RETRIEVE_ARRAY, 2147 ret ; 2148 2149 if (storeKey !== undefined) { 2150 array[0] = storeKey; 2151 storeKey = array; 2152 id = null ; 2153 } else { 2154 array[0] = id; 2155 id = array; 2156 } 2157 2158 ret = this.cancelRecords(recordType, id, storeKey); 2159 array.length = 0 ; 2160 return this; 2161 }, 2162 2163 /** 2164 Convenience method can be called by the store or other parts of your 2165 application to load a record into the store. This method will take a 2166 recordType and a data hashes and either add or update the 2167 record in the store. 2168 2169 The loaded records will be in an `SC.Record.READY_CLEAN` state, indicating 2170 they were loaded from the data source and do not need to be committed 2171 back before changing. 2172 2173 This method will check the state of the storeKey and call either 2174 `pushRetrieve()` or `dataSourceDidComplete()`. The standard state constraints 2175 for these methods apply here. 2176 2177 The return value will be the `storeKey` used for the push. This is often 2178 convenient to pass into `loadQuery()`, if you are fetching a remote query. 2179 2180 If you are upgrading from a pre SproutCore 1.0 application, this method 2181 is the closest to the old `updateRecord()`. 2182 2183 @param {SC.Record} recordType the record type 2184 @param {Array} dataHash to update 2185 @param {Array} id optional. if not passed lookup on the hash 2186 @returns {String} store keys assigned to these id 2187 */ 2188 loadRecord: function(recordType, dataHash, id) { 2189 var K = SC.Record, 2190 ret, primaryKey, storeKey; 2191 2192 // save lookup info 2193 recordType = recordType || SC.Record; 2194 primaryKey = recordType.prototype.primaryKey; 2195 2196 2197 // push each record 2198 id = id || dataHash[primaryKey]; 2199 ret = storeKey = recordType.storeKeyFor(id); // needed to cache 2200 2201 if (this.readStatus(storeKey) & K.BUSY) { 2202 this.dataSourceDidComplete(storeKey, dataHash, id); 2203 } else this.pushRetrieve(recordType, id, dataHash, storeKey); 2204 2205 // return storeKey 2206 return ret ; 2207 }, 2208 2209 /** 2210 Convenience method can be called by the store or other parts of your 2211 application to load records into the store. This method will take a 2212 recordType and an array of data hashes and either add or update the 2213 record in the store. 2214 2215 The loaded records will be in an `SC.Record.READY_CLEAN` state, indicating 2216 they were loaded from the data source and do not need to be committed 2217 back before changing. 2218 2219 This method will check the state of each storeKey and call either 2220 `pushRetrieve()` or `dataSourceDidComplete()`. The standard state 2221 constraints for these methods apply here. 2222 2223 The return value will be the storeKeys used for each push. This is often 2224 convenient to pass into `loadQuery()`, if you are fetching a remote query. 2225 2226 If you are upgrading from a pre SproutCore 1.0 application, this method 2227 is the closest to the old `updateRecords()`. 2228 2229 @param {SC.Record} recordTypes the record type or array of record types 2230 @param {Array} dataHashes array of data hashes to update 2231 @param {Array} [ids] array of ids. if not passed lookup on hashes 2232 @returns {Array} store keys assigned to these ids 2233 */ 2234 // TODO: No reason for first argument to be an array. The developer can just call loadRecords multiple times with different record type each time. Would save us the need to check if recordTypes is an Array or not. 2235 loadRecords: function (recordTypes, dataHashes, ids) { 2236 var isArray = SC.typeOf(recordTypes) === SC.T_ARRAY, 2237 len = dataHashes.get('length'), 2238 ret = [], 2239 recordType, 2240 id, primaryKey, idx, dataHash; 2241 2242 // save lookup info 2243 if (!isArray) { 2244 recordType = recordTypes || SC.Record; 2245 primaryKey = recordType.prototype.primaryKey; 2246 } 2247 2248 // push each record 2249 for (idx = 0; idx < len; idx++) { 2250 dataHash = dataHashes.objectAt(idx); 2251 if (isArray) { 2252 recordType = recordTypes.objectAt(idx) || SC.Record; 2253 primaryKey = recordType.prototype.primaryKey ; 2254 } 2255 2256 id = (ids) ? ids.objectAt(idx) : dataHash[primaryKey]; 2257 2258 ret[idx] = this.loadRecord(recordType, dataHash, id); 2259 } 2260 2261 // return storeKeys 2262 return ret; 2263 }, 2264 2265 /** 2266 Returns the `SC.Error` object associated with a specific record. 2267 2268 @param {Number} storeKey The store key of the record. 2269 2270 @returns {SC.Error} SC.Error or undefined if no error associated with the record. 2271 */ 2272 readError: function(storeKey) { 2273 var errors = this.recordErrors ; 2274 return errors ? errors[storeKey] : undefined ; 2275 }, 2276 2277 /** 2278 Returns the `SC.Error` object associated with a specific query. 2279 2280 @param {SC.Query} query The SC.Query with which the error is associated. 2281 2282 @returns {SC.Error} SC.Error or undefined if no error associated with the query. 2283 */ 2284 readQueryError: function(query) { 2285 var errors = this.queryErrors ; 2286 return errors ? errors[SC.guidFor(query)] : undefined ; 2287 }, 2288 2289 // .......................................................... 2290 // DATA SOURCE CALLBACKS 2291 // 2292 // Mathods called by the data source on the store 2293 2294 /** 2295 Called by a `dataSource` when it cancels an inflight operation on a 2296 record. This will transition the record back to it non-inflight state. 2297 2298 @param {Number} storeKey record store key to cancel 2299 @returns {SC.Store} receiver 2300 */ 2301 dataSourceDidCancel: function(storeKey) { 2302 var status = this.readStatus(storeKey), 2303 K = SC.Record; 2304 2305 // EMPTY, ERROR, READY_CLEAN, READY_NEW, READY_DIRTY, DESTROYED_CLEAN, 2306 // DESTROYED_DIRTY 2307 if (!(status & K.BUSY)) { 2308 K.BAD_STATE_ERROR.throw(); // should never be called in this state 2309 } 2310 2311 // otherwise, determine proper state transition 2312 switch(status) { 2313 case K.BUSY_LOADING: 2314 status = K.EMPTY; 2315 break ; 2316 2317 case K.BUSY_CREATING: 2318 status = K.READY_NEW; 2319 break; 2320 2321 case K.BUSY_COMMITTING: 2322 status = K.READY_DIRTY ; 2323 break; 2324 2325 case K.BUSY_REFRESH_CLEAN: 2326 status = K.READY_CLEAN; 2327 break; 2328 2329 case K.BUSY_REFRESH_DIRTY: 2330 status = K.READY_DIRTY ; 2331 break ; 2332 2333 case K.BUSY_DESTROYING: 2334 status = K.DESTROYED_DIRTY ; 2335 break; 2336 2337 default: 2338 K.BAD_STATE_ERROR.throw() ; 2339 } 2340 this.writeStatus(storeKey, status) ; 2341 this.dataHashDidChange(storeKey, null, YES); 2342 this._cancelCallback(storeKey); 2343 2344 return this ; 2345 }, 2346 2347 /** 2348 Called by a data source when it creates or commits a record. Passing an 2349 optional id will remap the `storeKey` to the new record id. This is 2350 required when you commit a record that does not have an id yet. 2351 2352 @param {Number} storeKey record store key to change to READY_CLEAN state 2353 @param {Hash} dataHash optional data hash to replace current hash 2354 @param {Object} newId optional new id to replace the old one 2355 @returns {SC.Store} receiver 2356 */ 2357 dataSourceDidComplete: function(storeKey, dataHash, newId) { 2358 var status = this.readStatus(storeKey), K = SC.Record, statusOnly; 2359 2360 // EMPTY, ERROR, READY_CLEAN, READY_NEW, READY_DIRTY, DESTROYED_CLEAN, 2361 // DESTROYED_DIRTY 2362 if (!(status & K.BUSY)) { 2363 K.BAD_STATE_ERROR.throw(); // should never be called in this state 2364 } 2365 2366 // otherwise, determine proper state transition 2367 if(status === K.BUSY_DESTROYING) { 2368 K.BAD_STATE_ERROR.throw(); 2369 } else status = K.READY_CLEAN; 2370 2371 this.writeStatus(storeKey, status); 2372 if (dataHash) this.writeDataHash(storeKey, dataHash, status); 2373 if (newId) { SC.Store.replaceIdFor(storeKey, newId); } 2374 2375 statusOnly = dataHash || newId ? NO : YES; 2376 this.dataHashDidChange(storeKey, null, statusOnly); 2377 2378 // Force record to refresh its cached properties based on store key 2379 var record = this.materializeRecord(storeKey); 2380 if (record !== null) { 2381 // If the record's id property has been computed, ensure that it re-computes. 2382 if (newId) { record.propertyDidChange('id'); } 2383 record.notifyPropertyChange('status'); 2384 } 2385 //update callbacks 2386 this._retrieveCallbackForStoreKey(storeKey); 2387 2388 return this ; 2389 }, 2390 2391 /** 2392 Called by a data source when it has destroyed a record. This will 2393 transition the record to the proper state. 2394 2395 @param {Number} storeKey record store key to cancel 2396 @returns {SC.Store} receiver 2397 */ 2398 dataSourceDidDestroy: function(storeKey) { 2399 var status = this.readStatus(storeKey), K = SC.Record; 2400 2401 // EMPTY, ERROR, READY_CLEAN, READY_NEW, READY_DIRTY, DESTROYED_CLEAN, 2402 // DESTROYED_DIRTY 2403 if (!(status & K.BUSY)) { 2404 K.BAD_STATE_ERROR.throw(); // should never be called in this state 2405 } 2406 // otherwise, determine proper state transition 2407 else{ 2408 status = K.DESTROYED_CLEAN ; 2409 } 2410 this.removeDataHash(storeKey, status) ; 2411 this.dataHashDidChange(storeKey); 2412 2413 // Force record to refresh its cached properties based on store key 2414 var record = this.materializeRecord(storeKey); 2415 if (record !== null) { 2416 record.notifyPropertyChange('status'); 2417 } 2418 2419 this._retrieveCallbackForStoreKey(storeKey); 2420 2421 return this ; 2422 }, 2423 2424 /** 2425 Converts the passed record into an error object. 2426 2427 @param {Number} storeKey record store key to error 2428 @param {SC.Error} error [optional] an SC.Error instance to associate with storeKey 2429 @returns {SC.Store} receiver 2430 */ 2431 dataSourceDidError: function(storeKey, error) { 2432 var status = this.readStatus(storeKey), errors = this.recordErrors, K = SC.Record; 2433 2434 // EMPTY, ERROR, READY_CLEAN, READY_NEW, READY_DIRTY, DESTROYED_CLEAN, 2435 // DESTROYED_DIRTY 2436 if (!(status & K.BUSY)) { K.BAD_STATE_ERROR.throw(); } 2437 2438 // otherwise, determine proper state transition 2439 else status = K.ERROR ; 2440 2441 // Add the error to the array of record errors (for lookup later on if necessary). 2442 if (error && error.isError) { 2443 if (!errors) errors = this.recordErrors = []; 2444 errors[storeKey] = error; 2445 } 2446 2447 this.writeStatus(storeKey, status) ; 2448 this.dataHashDidChange(storeKey, null, YES); 2449 2450 // Force record to refresh its cached properties based on store key 2451 var record = this.materializeRecord(storeKey); 2452 if (record) { 2453 record.notifyPropertyChange('status'); 2454 } 2455 2456 this._retrieveCallbackForStoreKey(storeKey); 2457 return this ; 2458 }, 2459 2460 // .......................................................... 2461 // PUSH CHANGES FROM DATA SOURCE 2462 // 2463 2464 /** 2465 Call by the data source whenever you want to push new data out of band 2466 into the store. 2467 2468 @param {Class} recordType the SC.Record subclass 2469 @param {Object} id the record id or null 2470 @param {Hash} dataHash data hash to load 2471 @param {Number} storeKey optional store key. 2472 @returns {Number|Boolean} storeKey if push was allowed, NO if not 2473 */ 2474 pushRetrieve: function(recordType, id, dataHash, storeKey) { 2475 var K = SC.Record, status; 2476 2477 if(storeKey===undefined) storeKey = recordType.storeKeyFor(id); 2478 status = this.readStatus(storeKey); 2479 if(status === K.EMPTY || status === K.ERROR || status === K.READY_CLEAN || status === K.DESTROYED_CLEAN) { 2480 2481 status = K.READY_CLEAN; 2482 if(dataHash === undefined) this.writeStatus(storeKey, status) ; 2483 else this.writeDataHash(storeKey, dataHash, status) ; 2484 2485 if (id && this.idFor(storeKey) !== id) { 2486 SC.Store.replaceIdFor(storeKey, id); 2487 2488 // If the record's id property has been computed, ensure that it re-computes. 2489 var record = this.materializeRecord(storeKey); 2490 record.propertyDidChange('id'); 2491 } 2492 this.dataHashDidChange(storeKey); 2493 2494 return storeKey; 2495 } 2496 //conflicted (ready) 2497 return NO; 2498 }, 2499 2500 /** 2501 Call by the data source whenever you want to push a deletion into the 2502 store. 2503 2504 @param {Class} recordType the SC.Record subclass 2505 @param {Object} id the record id or null 2506 @param {Number} storeKey optional store key. 2507 @returns {Number|Boolean} storeKey if push was allowed, NO if not 2508 */ 2509 pushDestroy: function(recordType, id, storeKey) { 2510 var K = SC.Record, status; 2511 2512 if(storeKey===undefined){ 2513 storeKey = recordType.storeKeyFor(id); 2514 } 2515 status = this.readStatus(storeKey); 2516 if(status === K.EMPTY || status === K.ERROR || status === K.READY_CLEAN || status === K.DESTROYED_CLEAN){ 2517 status = K.DESTROYED_CLEAN; 2518 this.removeDataHash(storeKey, status) ; 2519 this.dataHashDidChange(storeKey); 2520 return storeKey; 2521 } 2522 //conflicted (destroy) 2523 return NO; 2524 }, 2525 2526 /** 2527 Call by the data source whenever you want to push an error into the 2528 store. 2529 2530 @param {Class} recordType the SC.Record subclass 2531 @param {Object} id the record id or null 2532 @param {SC.Error} error [optional] an SC.Error instance to associate with id or storeKey 2533 @param {Number} storeKey optional store key. 2534 @returns {Number|Boolean} storeKey if push was allowed, NO if not 2535 */ 2536 pushError: function(recordType, id, error, storeKey) { 2537 var K = SC.Record, status, errors = this.recordErrors; 2538 2539 if(storeKey===undefined) storeKey = recordType.storeKeyFor(id); 2540 status = this.readStatus(storeKey); 2541 2542 if(status === K.EMPTY || status === K.ERROR || status === K.READY_CLEAN || status === K.DESTROYED_CLEAN){ 2543 status = K.ERROR; 2544 2545 // Add the error to the array of record errors (for lookup later on if necessary). 2546 if (error && error.isError) { 2547 if (!errors) errors = this.recordErrors = []; 2548 errors[storeKey] = error; 2549 } 2550 2551 this.writeStatus(storeKey, status) ; 2552 this.dataHashDidChange(storeKey, null, YES); 2553 return storeKey; 2554 } 2555 //conflicted (error) 2556 return NO; 2557 }, 2558 2559 // .......................................................... 2560 // FETCH CALLBACKS 2561 // 2562 2563 // **NOTE**: although these method works on RecordArray instances right now. 2564 // They could be optimized to actually share query results between nested 2565 // stores. This is why these methods are implemented here instead of 2566 // directly on `Query` or `RecordArray` objects. 2567 2568 /** @deprecated 2569 2570 @param {SC.Query} query the query you are loading. must be remote. 2571 @param {SC.Array} storeKeys array of store keys 2572 @returns {SC.Store} receiver 2573 */ 2574 loadQueryResults: function(query, storeKeys) { 2575 //@if(debug) 2576 if (query.get('location') === SC.Query.LOCAL) { 2577 throw new Error("Developer Error: You should not call loadQueryResults with a local query. You need to use dataSourceDidFetchQuery instead."); 2578 } else { 2579 SC.warn("Developer Warning: loadQueryResults has been deprecated in favor of using dataSourceDidFetchQuery for both local and remote queries. With remote queries, include the store keys when calling dataSourceDidFetchQuery."); 2580 } 2581 //@endif 2582 2583 return this.dataSourceDidFetchQuery(query, storeKeys); 2584 }, 2585 2586 /** 2587 Called by your data source whenever you finish fetching the results of a 2588 query. This will put the record array for the query into a READY_CLEAN 2589 state if it was previously loading or refreshing. 2590 2591 # Handling REMOTE queries 2592 2593 Note that if the query is REMOTE, then you must first load the results 2594 into the store using `loadRecords()` and pass the ordered array of store 2595 keys returned by `loadRecords()` into this method. 2596 2597 For example, 2598 2599 storeKeys = store.loadRecords(MyApp.SomeType, body.contacts); 2600 store.dataSourceDidFetchQuery(query, storeKeys); 2601 2602 # Automatic updates 2603 2604 When you call this method the record array for the query will notify that 2605 its contents have changed. If the query is LOCAL then the contents will 2606 update automatically to include any new records you added to the store. 2607 If the query is REMOTE the contents will update to be the ordered records 2608 for the passed in store keys. 2609 2610 # Incremental loading for REMOTE queries 2611 2612 If you want to support incremental loading, then pass an SC.SparseArray 2613 object to hold the store keys. This will allow you to load results 2614 incrementally and provide more store keys as you do. 2615 2616 See the SC.SparseArray documentation for more information. 2617 2618 @param {SC.Query} query The query you fetched 2619 @param {Array} [storeKeys] Ordered array of store keys as returned by a remote query. NOTE: Required for remote queries. 2620 @returns {SC.Store} receiver 2621 */ 2622 dataSourceDidFetchQuery: function (query, storeKeys) { 2623 var recArray = this._findQuery(query, YES, NO); 2624 2625 // Set the ordered array of store keys for remote queries. 2626 if (recArray && query.get('isRemote')) { 2627 //@if(debug) 2628 // Prevent confusion between local and remote requests. 2629 if (SC.none(storeKeys)) { 2630 throw new Error("Developer Error: The storeKeys argument in dataSourceDidFetchQuery is not optional for remote queries. For a remote query you must include the ordered array of store keys for the loaded records (even if it's an empty array)."); 2631 } 2632 //@endif 2633 2634 recArray.set('storeKeys', storeKeys); 2635 } 2636 2637 return this._scstore_dataSourceDidFetchQuery(query); 2638 }, 2639 2640 /** @private */ 2641 _scstore_dataSourceDidFetchQuery: function (query) { 2642 var recArray = this._findQuery(query, NO, NO), 2643 nestedStores = this.get('nestedStores'), 2644 loc = nestedStores ? nestedStores.get('length') : 0; 2645 2646 // fix query if needed 2647 if (recArray) recArray.storeDidFetchQuery(query); 2648 2649 // notify nested stores 2650 while(--loc >= 0) { 2651 nestedStores[loc]._scstore_dataSourceDidFetchQuery(query); 2652 } 2653 2654 return this ; 2655 }, 2656 2657 /** 2658 Called by your data source if it cancels fetching the results of a query. 2659 This will put any RecordArray's back into its original state (READY or 2660 EMPTY). 2661 2662 @param {SC.Query} query the query you cancelled 2663 @returns {SC.Store} receiver 2664 */ 2665 dataSourceDidCancelQuery: function(query) { 2666 return this._scstore_dataSourceDidCancelQuery(query, YES); 2667 }, 2668 2669 _scstore_dataSourceDidCancelQuery: function(query, createIfNeeded) { 2670 var recArray = this._findQuery(query, createIfNeeded, NO), 2671 nestedStores = this.get('nestedStores'), 2672 loc = nestedStores ? nestedStores.get('length') : 0; 2673 2674 // fix query if needed 2675 if (recArray) recArray.storeDidCancelQuery(query); 2676 2677 // notify nested stores 2678 while(--loc >= 0) { 2679 nestedStores[loc]._scstore_dataSourceDidCancelQuery(query, NO); 2680 } 2681 2682 return this ; 2683 }, 2684 2685 /** 2686 Called by your data source if it encountered an error loading the query. 2687 This will put the query into an error state until you try to refresh it 2688 again. 2689 2690 @param {SC.Query} query the query with the error 2691 @param {SC.Error} error [optional] an SC.Error instance to associate with query 2692 @returns {SC.Store} receiver 2693 */ 2694 dataSourceDidErrorQuery: function(query, error) { 2695 var errors = this.queryErrors; 2696 2697 // Add the error to the array of query errors (for lookup later on if necessary). 2698 if (error && error.isError) { 2699 if (!errors) errors = this.queryErrors = {}; 2700 errors[SC.guidFor(query)] = error; 2701 } 2702 2703 return this._scstore_dataSourceDidErrorQuery(query, YES); 2704 }, 2705 2706 _scstore_dataSourceDidErrorQuery: function(query, createIfNeeded) { 2707 var recArray = this._findQuery(query, createIfNeeded, NO), 2708 nestedStores = this.get('nestedStores'), 2709 loc = nestedStores ? nestedStores.get('length') : 0; 2710 2711 // fix query if needed 2712 if (recArray) recArray.storeDidErrorQuery(query); 2713 2714 // notify nested stores 2715 while(--loc >= 0) { 2716 nestedStores[loc]._scstore_dataSourceDidErrorQuery(query, NO); 2717 } 2718 2719 return this ; 2720 }, 2721 2722 // .......................................................... 2723 // INTERNAL SUPPORT 2724 // 2725 2726 /** @private */ 2727 init: function() { 2728 sc_super(); 2729 this.reset(); 2730 }, 2731 2732 2733 toString: function() { 2734 // Include the name if the client has specified one. 2735 var name = this.get('name'); 2736 if (!name) { 2737 return sc_super(); 2738 } 2739 else { 2740 var ret = sc_super(); 2741 return "%@ (%@)".fmt(name, ret); 2742 } 2743 }, 2744 2745 2746 // .......................................................... 2747 // PRIMARY KEY CONVENIENCE METHODS 2748 // 2749 2750 /** 2751 Given a `storeKey`, return the `primaryKey`. 2752 2753 @param {Number} storeKey the store key 2754 @returns {String} primaryKey value 2755 */ 2756 idFor: function(storeKey) { 2757 return SC.Store.idFor(storeKey); 2758 }, 2759 2760 /** 2761 Given a storeKey, return the recordType. 2762 2763 @param {Number} storeKey the store key 2764 @returns {SC.Record} record instance 2765 */ 2766 recordTypeFor: function(storeKey) { 2767 return SC.Store.recordTypeFor(storeKey) ; 2768 }, 2769 2770 /** 2771 Given a `recordType` and `primaryKey`, find the `storeKey`. If the 2772 `primaryKey` has not been assigned a `storeKey` yet, it will be added. 2773 2774 @param {SC.Record} recordType the record type 2775 @param {String} primaryKey the primary key 2776 @returns {Number} storeKey 2777 */ 2778 storeKeyFor: function(recordType, primaryKey) { 2779 return recordType.storeKeyFor(primaryKey); 2780 }, 2781 2782 /** 2783 Given a `primaryKey` value for the record, returns the associated 2784 `storeKey`. As opposed to `storeKeyFor()` however, this method 2785 will **NOT** generate a new `storeKey` but returned `undefined`. 2786 2787 @param {SC.Record} recordType the record type 2788 @param {String} primaryKey the primary key 2789 @returns {Number} a storeKey. 2790 */ 2791 storeKeyExists: function(recordType, primaryKey) { 2792 return recordType.storeKeyExists(primaryKey); 2793 }, 2794 2795 /** 2796 Finds all `storeKey`s of a certain record type in this store 2797 and returns an array. 2798 2799 @param {SC.Record} recordType 2800 @returns {Array} set of storeKeys 2801 */ 2802 storeKeysFor: function(recordType) { 2803 var ret = [], 2804 isEnum = recordType && recordType.isEnumerable, 2805 recType, storeKey, isMatch ; 2806 2807 if (!this.statuses) return ret; 2808 for(storeKey in SC.Store.recordTypesByStoreKey) { 2809 recType = SC.Store.recordTypesByStoreKey[storeKey]; 2810 2811 // if same record type and this store has it 2812 if (isEnum) isMatch = recordType.contains(recType); 2813 else isMatch = recType === recordType; 2814 2815 if(isMatch && this.statuses[storeKey]) ret.push(parseInt(storeKey, 10)); 2816 } 2817 2818 return ret; 2819 }, 2820 2821 /** 2822 Finds all `storeKey`s in this store 2823 and returns an array. 2824 2825 @returns {Array} set of storeKeys 2826 */ 2827 storeKeys: function() { 2828 var ret = [], storeKey; 2829 if(!this.statuses) return ret; 2830 2831 for(storeKey in this.statuses) { 2832 // if status is not empty 2833 if(this.statuses[storeKey] !== SC.Record.EMPTY) { 2834 ret.push(parseInt(storeKey, 10)); 2835 } 2836 } 2837 2838 return ret; 2839 }, 2840 2841 /** 2842 Returns string representation of a `storeKey`, with status. 2843 2844 @param {Number} storeKey 2845 @returns {String} 2846 */ 2847 statusString: function(storeKey) { 2848 var rec = this.materializeRecord(storeKey); 2849 return rec.statusString(); 2850 } 2851 2852 }) ; 2853 2854 SC.Store.mixin(/** @scope SC.Store.prototype */{ 2855 2856 /** 2857 Standard error raised if you try to commit changes from a nested store 2858 and there is a conflict. 2859 2860 @type Error 2861 */ 2862 CHAIN_CONFLICT_ERROR: SC.$error("Nested Store Conflict"), 2863 2864 /** 2865 Standard error if you try to perform an operation on a nested store 2866 without a parent. 2867 2868 @type Error 2869 */ 2870 NO_PARENT_STORE_ERROR: SC.$error("Parent Store Required"), 2871 2872 /** 2873 Standard error if you try to perform an operation on a nested store that 2874 is only supported in root stores. 2875 2876 @type Error 2877 */ 2878 NESTED_STORE_UNSUPPORTED_ERROR: SC.$error("Unsupported In Nested Store"), 2879 2880 /** 2881 Standard error if you try to retrieve a record in a nested store that is 2882 dirty. (This is allowed on the main store, but not in nested stores.) 2883 2884 @type Error 2885 */ 2886 NESTED_STORE_RETRIEVE_DIRTY_ERROR: SC.$error("Cannot Retrieve Dirty Record in Nested Store"), 2887 2888 /** 2889 Data hash state indicates the data hash is currently editable 2890 2891 @type String 2892 */ 2893 EDITABLE: 'editable', 2894 2895 /** 2896 Data hash state indicates the hash no longer tracks changes from a 2897 parent store, but it is not editable. 2898 2899 @type String 2900 */ 2901 LOCKED: 'locked', 2902 2903 /** 2904 Data hash state indicates the hash is tracking changes from the parent 2905 store and is not editable. 2906 2907 @type String 2908 */ 2909 INHERITED: 'inherited', 2910 2911 /** @private 2912 This array maps all storeKeys to primary keys. You will not normally 2913 access this method directly. Instead use the `idFor()` and 2914 `storeKeyFor()` methods on `SC.Record`. 2915 */ 2916 idsByStoreKey: [], 2917 2918 /** @private 2919 Maps all `storeKey`s to a `recordType`. Once a `storeKey` is associated 2920 with a `primaryKey` and `recordType` that remains constant throughout the 2921 lifetime of the application. 2922 */ 2923 recordTypesByStoreKey: {}, 2924 2925 /** @private 2926 Maps some `storeKeys` to query instance. Once a `storeKey` is associated 2927 with a query instance, that remains constant through the lifetime of the 2928 application. If a `Query` is destroyed, it will remove itself from this 2929 list. 2930 2931 Don't access this directly. Use queryFor(). 2932 */ 2933 queriesByStoreKey: [], 2934 2935 /** @private 2936 The next store key to allocate. A storeKey must always be greater than 0 2937 */ 2938 nextStoreKey: 1, 2939 2940 /** 2941 Generates a new store key for use. 2942 2943 @type Number 2944 */ 2945 generateStoreKey: function() { return this.nextStoreKey++; }, 2946 2947 /** 2948 Given a `storeKey` returns the `primaryKey` associated with the key. 2949 If no `primaryKey` is associated with the `storeKey`, returns `null`. 2950 2951 @param {Number} storeKey the store key 2952 @returns {String} the primary key or null 2953 */ 2954 idFor: function(storeKey) { 2955 return this.idsByStoreKey[storeKey] ; 2956 }, 2957 2958 /** 2959 Given a `storeKey`, returns the query object associated with the key. If 2960 no query is associated with the `storeKey`, returns `null`. 2961 2962 @param {Number} storeKey the store key 2963 @returns {SC.Query} query query object 2964 */ 2965 queryFor: function(storeKey) { 2966 return this.queriesByStoreKey[storeKey]; 2967 }, 2968 2969 /** 2970 Given a `storeKey` returns the `SC.Record` class associated with the key. 2971 If no record type is associated with the store key, returns `null`. 2972 2973 The SC.Record class will only be found if you have already called 2974 storeKeyFor() on the record. 2975 2976 @param {Number} storeKey the store key 2977 @returns {SC.Record} the record type 2978 */ 2979 recordTypeFor: function(storeKey) { 2980 return this.recordTypesByStoreKey[storeKey]; 2981 }, 2982 2983 /** 2984 Swaps the `primaryKey` mapped to the given storeKey with the new 2985 `primaryKey`. If the `storeKey` is not currently associated with a record 2986 this will raise an exception. 2987 2988 @param {Number} storeKey the existing store key 2989 @param {String} newPrimaryKey the new primary key 2990 @returns {SC.Store} receiver 2991 */ 2992 replaceIdFor: function(storeKey, newId) { 2993 var oldId = this.idsByStoreKey[storeKey], 2994 recordType, storeKeys; 2995 2996 if (oldId !== newId) { // skip if id isn't changing 2997 2998 recordType = this.recordTypeFor(storeKey); 2999 if (!recordType) { 3000 throw new Error("replaceIdFor: storeKey %@ does not exist".fmt(storeKey)); 3001 } 3002 3003 // map one direction... 3004 this.idsByStoreKey[storeKey] = newId; 3005 3006 // then the other... 3007 storeKeys = recordType.storeKeysById() ; 3008 delete storeKeys[oldId]; 3009 storeKeys[newId] = storeKey; 3010 } 3011 3012 return this ; 3013 }, 3014 3015 /** 3016 Swaps the `recordType` recorded for a given `storeKey`. Normally you 3017 should not call this method directly as it can damage the store behavior. 3018 This method is used by other store methods to set the `recordType` for a 3019 `storeKey`. 3020 3021 @param {Integer} storeKey the store key 3022 @param {SC.Record} recordType a record class 3023 @returns {SC.Store} receiver 3024 */ 3025 replaceRecordTypeFor: function(storeKey, recordType) { 3026 this.recordTypesByStoreKey[storeKey] = recordType; 3027 return this ; 3028 } 3029 3030 }); 3031