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