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('system/store');
  9 
 10 /**
 11   @class
 12 
 13   A nested store can buffer changes to a parent store and then commit them
 14   all at once.  You usually will use a `NestedStore` as part of store chaining
 15   to stage changes to your object graph before sharing them with the rest of
 16   the application.
 17 
 18   Normally you will not create a nested store directly.  Instead, you can
 19   retrieve a nested store by using the `chain()` method.  When you are finished
 20   working with the nested store, `destroy()` will dispose of it.
 21 
 22   @extends SC.Store
 23   @since SproutCore 1.0
 24 */
 25 SC.NestedStore = SC.Store.extend(
 26 /** @scope SC.NestedStore.prototype */ {
 27 
 28   /**
 29     This is set to YES when there are changes that have not been committed
 30     yet.
 31 
 32     @type Boolean
 33     @default NO
 34   */
 35   hasChanges: NO,
 36 
 37   /**
 38     The parent store this nested store is chained to.  Nested stores must have
 39     a parent store in order to function properly.  Normally, you create a
 40     nested store using the `SC.Store#chain()` method and this property will be
 41     set for you.
 42 
 43     @type SC.Store
 44     @default null
 45   */
 46   parentStore: null,
 47 
 48   /**
 49     `YES` if the store is nested. Walk like a duck
 50 
 51     @type Boolean
 52     @default YES
 53   */
 54   isNested: YES,
 55 
 56   /**
 57     If YES, then the attribute hash state will be locked when you first
 58     read the data hash or status.  This means that if you retrieve a record
 59     then change the record in the parent store, the changes will not be
 60     visible to your nested store until you commit or discard changes.
 61 
 62     If `NO`, then the attribute hash will lock only when you write data.
 63 
 64     Normally you want to lock your attribute hash the first time you read it.
 65     This will make your nested store behave most consistently.  However, if
 66     you are using multiple sibling nested stores at one time, you may want
 67     to turn off this property so that changes from one store will be reflected
 68     in the other one immediately.  In this case you will be responsible for
 69     ensuring that the sibling stores do not edit the same part of the object
 70     graph at the same time.
 71 
 72     @type Boolean
 73     @default YES
 74   */
 75   lockOnRead: YES,
 76 
 77   /** @private
 78     Array contains the base revision for an attribute hash when it was first
 79     cloned from the parent store.  If the attribute hash is edited and
 80     committed, the commit will fail if the parent attributes hash has been
 81     edited since.
 82 
 83     This is a form of optimistic locking, hence the name.
 84 
 85     Each store gets its own array of locks, which are selectively populated
 86     as needed.
 87 
 88     Note that this is kept as an array because it will be stored as a dense
 89     array on some browsers, making it faster.
 90 
 91     @type Array
 92     @default null
 93   */
 94   locks: null,
 95 
 96   /** @private
 97     An array that includes the store keys that have changed since the store
 98     was last committed.  This array is used to sync data hash changes between
 99     chained stores.  For a log changes that may actually be committed back to
100     the server see the changelog property.
101 
102     @type SC.Set
103     @default YES
104   */
105   chainedChanges: null,
106 
107   // ..........................................................
108   // STORE CHAINING
109   //
110 
111   /**
112     `find()` cannot accept REMOTE queries in a nested store.  This override will
113     verify that condition for you.  See `SC.Store#find()` for info on using this
114     method.
115 
116     @param {SC.Query} query query object to use.
117     @returns {SC.Record|SC.RecordArray}
118   */
119   find: function(query) {
120     if (query && query.isQuery && query.get('location') !== SC.Query.LOCAL) {
121       throw new Error("SC.Store#find() can only accept LOCAL queries in nested stores");
122     }
123     return sc_super();
124   },
125 
126   /**
127     Propagate this store's changes to its parent.  If the store does not
128     have a parent, this has no effect other than to clear the change set.
129 
130     @param {Boolean} force if YES, does not check for conflicts first
131     @returns {SC.Store} receiver
132   */
133   commitChanges: function(force) {
134     if (this.get('hasChanges')) {
135       var pstore = this.get('parentStore');
136       pstore.commitChangesFromNestedStore(this, this.get('chainedChanges'), force);
137     }
138 
139     // clear out custom changes - even if there is nothing to commit.
140     this.reset();
141     return this;
142   },
143 
144   /**
145     An array of store keys for all conflicting records with the parent store. If there are no
146     conflicting records, this property will be null.
147 
148     @readonly
149     @field
150     @type Array
151     @default null
152   */
153   conflictedStoreKeys: function () {
154     var ret = null;
155 
156     if (this.get('hasChanges')) {
157       var pstore = this.get('parentStore'),
158           locks = this.locks,
159           revisions = pstore.revisions;
160 
161       if (locks && revisions) {
162         var changes = this.get('chainedChanges');
163 
164         for (var i = 0, len = changes.length; i < len; i++) {
165           var storeKey = changes[i],
166               lock = locks[storeKey] || 1,
167               revision = revisions[storeKey] || 1;
168 
169           // If the same revision for the item does not match the current revision, then someone has
170           // changed the data hash in this store and we have a conflict.
171           if (lock < revision) {
172             if (!ret) ret = [];
173             ret.push(storeKey);
174           }
175         }
176       }
177     }
178 
179     return ret;
180   }.property('chainedChanges').cacheable(),
181 
182   /**
183     Propagate this store's successful changes to its parent (if exists). At the end, it clears the
184     local, private status of the committed records therefore the method can be called several times
185     until the full transaction is successful or editing is abandoned
186 
187     @param {Boolean} force if YES, does not check for conflicts first
188     @returns {SC.Store} receiver
189   */
190   commitSuccessfulChanges: function(force) {
191     var chainedChanges = this.get('chainedChanges');
192 
193     if (this.get('hasChanges') && chainedChanges) {
194       var dataHashes = this.dataHashes,
195           revisions  = this.revisions,
196           statuses   = this.statuses,
197           editables  = this.editables,
198           locks      = this.locks;
199 
200       var successfulChanges = chainedChanges.filter( function(storeKey) {
201         var state = this.readStatus(storeKey);
202 
203         return state === SC.Record.READY_CLEAN || state === SC.Record.DESTROYED_CLEAN;
204       }, this );
205 
206       var pstore = this.get('parentStore');
207 
208       pstore.commitChangesFromNestedStore(this, successfulChanges, force);
209 
210       // remove the local status so these records that have been successfully committed on the server
211       // are no longer retrieved from this nested store but from the parent
212       for (var i = 0, len = successfulChanges.get('length'); i < len; i++) {
213         var storeKey = successfulChanges.objectAt(i);
214 
215         if (dataHashes && dataHashes.hasOwnProperty(storeKey)) { delete dataHashes[storeKey]; }
216         if (revisions && revisions.hasOwnProperty(storeKey)) { delete revisions[storeKey]; }
217         if (editables) { delete editables[storeKey]; }
218         if (locks) { delete locks[storeKey]; }
219         if (statuses && statuses.hasOwnProperty(storeKey)) { delete statuses[storeKey]; }
220 
221         chainedChanges.remove(storeKey);
222       }
223 
224       // Indicate that chainedChanges has changed.
225       if (successfulChanges.length > 0) { this.notifyPropertyChange('chainedChanges'); }
226     }
227 
228     return this;
229   },
230 
231   /**
232     Discard the changes made to this store and reset the store.
233 
234     @returns {SC.Store} receiver
235   */
236   discardChanges: function() {
237     // any locked records whose rev or lock rev differs from parent need to
238     // be notified.
239     var records, locks;
240     if ((records = this.records) && (locks = this.locks)) {
241       var pstore = this.get('parentStore'), psRevisions = pstore.revisions;
242       var revisions = this.revisions, storeKey, lock, rev;
243       for (storeKey in records) {
244         if (!records.hasOwnProperty(storeKey)) continue ;
245         if (!(lock = locks[storeKey])) continue; // not locked.
246 
247         rev = psRevisions[storeKey];
248         if ((rev !== lock) || (revisions[storeKey] > rev)) {
249           this._notifyRecordPropertyChange(parseInt(storeKey, 10));
250         }
251       }
252     }
253 
254     this.reset();
255     this.flush();
256     return this ;
257   },
258 
259   /**
260     When you are finished working with a chained store, call this method to
261     tear it down.  This will also discard any pending changes.
262 
263     @returns {SC.Store} receiver
264   */
265   destroy: function() {
266     this.discardChanges();
267 
268     var parentStore = this.get('parentStore');
269     if (parentStore) parentStore.willDestroyNestedStore(this);
270 
271     sc_super();
272     return this ;
273   },
274 
275   /**
276     Resets a store's data hash contents to match its parent.
277   */
278   reset: function() {
279     // requires a pstore to reset
280     var parentStore = this.get('parentStore');
281     if (!parentStore) SC.Store.NO_PARENT_STORE_ERROR.throw();
282 
283     // inherit data store from parent store.
284     this.dataHashes = SC.beget(parentStore.dataHashes);
285     this.revisions  = SC.beget(parentStore.revisions);
286     this.statuses   = SC.beget(parentStore.statuses);
287 
288     // beget nested records references
289     this.childRecords = parentStore.childRecords ? SC.beget(parentStore.childRecords) : {};
290     this.parentRecords = parentStore.parentRecords ? SC.beget(parentStore.parentRecords) : {};
291 
292     // also, reset private temporary objects
293     this.set('hasChanges', false);
294     this.chainedChanges = this.locks = this.editables = null;
295     this.changelog = null ;
296 
297     // TODO: Notify record instances
298   },
299 
300   /** @private
301 
302     Chain to parentstore
303   */
304   refreshQuery: function(query) {
305     var parentStore = this.get('parentStore');
306     if (parentStore) parentStore.refreshQuery(query);
307     return this ;
308   },
309 
310   /**
311     Returns the `SC.Error` object associated with a specific record.
312 
313     Delegates the call to the parent store.
314 
315     @param {Number} storeKey The store key of the record.
316 
317     @returns {SC.Error} SC.Error or null if no error associated with the record.
318   */
319   readError: function(storeKey) {
320     var parentStore = this.get('parentStore');
321     return parentStore ? parentStore.readError(storeKey) : null;
322   },
323 
324   /**
325     Returns the `SC.Error` object associated with a specific query.
326 
327     Delegates the call to the parent store.
328 
329     @param {SC.Query} query The SC.Query with which the error is associated.
330 
331     @returns {SC.Error} SC.Error or null if no error associated with the query.
332   */
333   readQueryError: function(query) {
334     var parentStore = this.get('parentStore');
335     return parentStore ? parentStore.readQueryError(query) : null;
336   },
337 
338   /** @private - adapt for nested store */
339   chainAutonomousStore: function(attrs, newStoreClass) {
340     SC.Store.NESTED_STORE_UNSUPPORTED_ERROR.throw();
341   },
342 
343   // ..........................................................
344   // CORE ATTRIBUTE API
345   //
346   // The methods in this layer work on data hashes in the store.  They do not
347   // perform any changes that can impact records.  Usually you will not need
348   // to use these methods.
349 
350   /**
351     Returns the current edit status of a storekey.  May be one of `INHERITED`,
352     `EDITABLE`, and `LOCKED`.  Used mostly for unit testing.
353 
354     @param {Number} storeKey the store key
355     @returns {Number} edit status
356   */
357   storeKeyEditState: function(storeKey) {
358     var editables = this.editables, locks = this.locks;
359     return (editables && editables[storeKey]) ? SC.Store.EDITABLE : (locks && locks[storeKey]) ? SC.Store.LOCKED : SC.Store.INHERITED ;
360   },
361 
362   /**  @private
363     Locks the data hash so that it iterates independently from the parent
364     store.
365   */
366   _lock: function(storeKey) {
367     var locks = this.locks, rev, editables,
368         pk, pr, path, tup, obj, key;
369 
370     // already locked -- nothing to do
371     if (locks && locks[storeKey]) return this;
372 
373     // create locks if needed
374     if (!locks) locks = this.locks = [];
375 
376     // fixup editables
377     editables = this.editables;
378     if (editables) editables[storeKey] = 0;
379 
380 
381     // if the data hash in the parent store is editable, then clone the hash
382     // for our own use.  Otherwise, just copy a reference to the data hash
383     // in the parent store. -- find first non-inherited state
384     var pstore = this.get('parentStore'), editState;
385     while(pstore && (editState=pstore.storeKeyEditState(storeKey)) === SC.Store.INHERITED) {
386       pstore = pstore.get('parentStore');
387     }
388 
389     if (pstore && editState === SC.Store.EDITABLE) {
390 
391       pk = this.childRecords[storeKey];
392       if (pk){
393         // Since this is a nested record we have to actually walk up the parent chain
394         // to get to the root parent and clone that hash. And then reconstruct the
395         // memory space linking.
396         this._lock(pk);
397         pr = this.parentRecords[pk];
398         if (pr) {
399           path = pr[storeKey];
400           tup = path ? SC.tupleForPropertyPath(path, this.dataHashes[pk]) : null;
401           if (tup){ obj = tup[0]; key = tup[1]; }
402           this.dataHashes[storeKey] = obj && key ? obj[key] : null;
403         }
404       }
405       else {
406         this.dataHashes[storeKey] = SC.clone(pstore.dataHashes[storeKey], YES);
407       }
408       if (!editables) editables = this.editables = [];
409       editables[storeKey] = 1 ; // mark as editable
410 
411     } else this.dataHashes[storeKey] = pstore.dataHashes[storeKey];
412 
413     // also copy the status + revision
414     this.statuses[storeKey] = this.statuses[storeKey];
415     rev = this.revisions[storeKey] = this.revisions[storeKey];
416 
417     // save a lock and make it not editable
418     locks[storeKey] = rev || 1;
419 
420     return this ;
421   },
422 
423   /** @private - adds chaining support */
424   readDataHash: function(storeKey) {
425     if (this.get('lockOnRead')) this._lock(storeKey);
426     return this.dataHashes[storeKey];
427   },
428 
429   /** @private - adds chaining support */
430   readEditableDataHash: function(storeKey) {
431 
432     // lock the data hash if needed
433     this._lock(storeKey);
434 
435     return sc_super();
436   },
437 
438   /** @private - adds chaining support -
439     Does not call sc_super because the implementation of the method vary too
440     much.
441   */
442   writeDataHash: function(storeKey, hash, status) {
443     var locks = this.locks, didLock = NO, rev ;
444 
445     // Update our dataHash and/or status, depending on what was passed in.
446     // Note that if no new hash was passed in, we'll lock the storeKey to
447     // properly fork our dataHash from our parent store.  Similarly, if no
448     // status was passed in, we'll save our own copy of the value.
449     if (hash) {
450       this.dataHashes[storeKey] = hash;
451     }
452     else {
453       this._lock(storeKey);
454       didLock = YES;
455     }
456 
457     if (status) {
458       this.statuses[storeKey] = status;
459     }
460     else {
461       if (!didLock) this.statuses[storeKey] = (this.statuses[storeKey] || SC.Record.READY_NEW);
462     }
463 
464     if (!didLock) {
465       rev = this.revisions[storeKey] = this.revisions[storeKey]; // copy ref
466 
467       // make sure we lock if needed.
468       if (!locks) locks = this.locks = [];
469       if (!locks[storeKey]) locks[storeKey] = rev || 1;
470     }
471 
472     // Also note that this hash is now editable.  (Even if we locked it,
473     // above, it may not have been marked as editable.)
474     var editables = this.editables;
475     if (!editables) editables = this.editables = [];
476     editables[storeKey] = 1 ; // use number for dense array support
477 
478     // propagate the data to the child records
479     this._updateChildRecordHashes(storeKey, hash, status);
480 
481     return this ;
482   },
483 
484   /** @private - adds chaining support */
485   removeDataHash: function(storeKey, status) {
486 
487     // record optimistic lock revision
488     var locks = this.locks;
489     if (!locks) locks = this.locks = [];
490     if (!locks[storeKey]) locks[storeKey] = this.revisions[storeKey] || 1;
491 
492     return sc_super();
493   },
494 
495   /** @private - bookkeeping for a single data hash. */
496   dataHashDidChange: function(storeKeys, rev, statusOnly, key) {
497     // update the revision for storeKey.  Use generateStoreKey() because that
498     // guarantees a universally (to this store hierarchy anyway) unique
499     // key value.
500     if (!rev) rev = SC.Store.generateStoreKey();
501     var isArray, len, idx, storeKey;
502 
503     isArray = SC.typeOf(storeKeys) === SC.T_ARRAY;
504     if (isArray) {
505       len = storeKeys.length;
506     } else {
507       len = 1;
508       storeKey = storeKeys;
509     }
510 
511     var changes = this.get('chainedChanges');
512     if (!changes) changes = this.chainedChanges = SC.Set.create();
513 
514     var that = this,
515         didAddChainedChanges = false;
516 
517     for (idx = 0; idx < len; idx++) {
518       if (isArray) storeKey = storeKeys[idx];
519 
520       this._lock(storeKey);
521       this.revisions[storeKey] = rev;
522 
523       if (!changes.contains(storeKey)) {
524         changes.add(storeKey);
525         didAddChainedChanges = true;
526       }
527 
528       this._notifyRecordPropertyChange(storeKey, statusOnly, key);
529 
530       // notify also the child records
531       this._propagateToChildren(storeKey, function(storeKey){
532         that.dataHashDidChange(storeKey, null, statusOnly, key);
533       });
534     }
535 
536     this.setIfChanged('hasChanges', YES);
537     if (didAddChainedChanges) {
538       this.notifyPropertyChange('chainedChanges');
539     }
540 
541     return this ;
542   },
543 
544   // ..........................................................
545   // SYNCING CHANGES
546   //
547 
548   /** @private - adapt for nested store */
549   commitChangesFromNestedStore: function(nestedStore, changes, force) {
550 
551     sc_super();
552 
553     // save a lock for each store key if it does not have one already
554     // also add each storeKey to my own changes set.
555     var pstore = this.get('parentStore'), psRevisions = pstore.revisions, i;
556     var myLocks = this.locks,
557         myChanges = this.get('chainedChanges'),
558         len,
559         storeKey;
560 
561     if (!myLocks) myLocks = this.locks = [];
562     if (!myChanges) myChanges = this.chainedChanges = SC.Set.create();
563 
564     len = changes.length ;
565     for(i=0;i<len;i++) {
566       storeKey = changes[i];
567       if (!myLocks[storeKey]) myLocks[storeKey] = psRevisions[storeKey]||1;
568       myChanges.add(storeKey);
569     }
570 
571     // Finally, mark store as dirty if we have changes
572     this.setIfChanged('hasChanges', myChanges.get('length') > 0);
573     this.notifyPropertyChange('chainedChanges');
574 
575     this.flush();
576 
577     return this ;
578   },
579 
580   // ..........................................................
581   // HIGH-LEVEL RECORD API
582   //
583 
584 
585   /** @private - adapt for nested store */
586   queryFor: function(recordType, conditions, params) {
587     return this.get('parentStore').queryFor(recordType, conditions, params);
588   },
589 
590   // ..........................................................
591   // CORE RECORDS API
592   //
593   // The methods in this section can be used to manipulate records without
594   // actually creating record instances.
595 
596   /** @private - adapt for nested store
597 
598     Unlike for the main store, for nested stores if isRefresh=YES, we'll throw
599     an error if the record is dirty.  We'll otherwise avoid setting our status
600     because that can disconnect us from upper and/or lower stores.
601   */
602   retrieveRecords: function(recordTypes, ids, storeKeys, isRefresh, callbacks) {
603     var pstore = this.get('parentStore'), idx, storeKey,
604       len = (!storeKeys) ? ids.length : storeKeys.length,
605       K = SC.Record, status;
606 
607     // Is this a refresh?
608     if (isRefresh) {
609       for(idx=0;idx<len;idx++) {
610         storeKey = !storeKeys ? pstore.storeKeyFor(recordTypes, ids[idx]) : storeKeys[idx];
611         status   = this.peekStatus(storeKey);
612 
613         // We won't allow calling retrieve on a dirty record in a nested store
614         // (although we do allow it in the main store).  This is because doing
615         // so would involve writing a unique status, and that would break the
616         // status hierarchy, so even though lower stores would complete the
617         // retrieval, the upper layers would never inherit the new statuses.
618         if (status & K.DIRTY) {
619           SC.Store.NESTED_STORE_RETRIEVE_DIRTY_ERROR.throw();
620         }
621         else {
622           // Not dirty?  Then abandon any status we had set (to re-establish
623           // any prototype linkage breakage) before asking our parent store to
624           // perform the retrieve.
625           var dataHashes = this.dataHashes,
626               revisions  = this.revisions,
627               statuses   = this.statuses,
628               editables  = this.editables,
629               locks      = this.locks;
630 
631           var changed    = NO;
632           var statusOnly = NO;
633 
634           if (dataHashes  &&  dataHashes.hasOwnProperty(storeKey)) {
635             delete dataHashes[storeKey];
636             changed = YES;
637           }
638           if (revisions   &&  revisions.hasOwnProperty(storeKey)) {
639             delete revisions[storeKey];
640             changed = YES;
641           }
642           if (editables) delete editables[storeKey];
643           if (locks) delete locks[storeKey];
644 
645           if (statuses  &&  statuses.hasOwnProperty(storeKey)) {
646             delete statuses[storeKey];
647             if (!changed) statusOnly = YES;
648             changed = YES;
649           }
650 
651           if (changed) this._notifyRecordPropertyChange(storeKey, statusOnly);
652         }
653       }
654     }
655 
656     return pstore.retrieveRecords(recordTypes, ids, storeKeys, isRefresh, callbacks);
657   },
658 
659   /** @private - adapt for nested store */
660   commitRecords: function(recordTypes, ids, storeKeys) {
661     if( this.get( "dataSource" ) )
662       return sc_super();
663     else
664       SC.Store.NESTED_STORE_UNSUPPORTED_ERROR.throw();
665   },
666 
667   /** @private - adapt for nested store */
668   commitRecord: function(recordType, id, storeKey) {
669     if( this.get( "dataSource" ) )
670       return sc_super();
671     else
672       SC.Store.NESTED_STORE_UNSUPPORTED_ERROR.throw();
673   },
674 
675   /** @private - adapt for nested store */
676   cancelRecords: function(recordTypes, ids, storeKeys) {
677     if( this.get( "dataSource" ) )
678       return sc_super();
679     else
680       SC.Store.NESTED_STORE_UNSUPPORTED_ERROR.throw();
681   },
682 
683   /** @private - adapt for nested store */
684   cancelRecord: function(recordType, id, storeKey) {
685     if( this.get( "dataSource" ) )
686       return sc_super();
687     else
688       SC.Store.NESTED_STORE_UNSUPPORTED_ERROR.throw();
689   },
690 
691   // ..........................................................
692   // DATA SOURCE CALLBACKS
693   //
694   // Methods called by the data source on the store
695 
696   /** @private - adapt for nested store */
697   dataSourceDidCancel: function(storeKey) {
698     if( this.get( "dataSource" ) )
699       return sc_super();
700     else
701       SC.Store.NESTED_STORE_UNSUPPORTED_ERROR.throw();
702   },
703 
704   /** @private - adapt for nested store */
705   dataSourceDidComplete: function(storeKey, dataHash, newId) {
706     if( this.get( "dataSource" ) )
707       return sc_super();
708     else
709       SC.Store.NESTED_STORE_UNSUPPORTED_ERROR.throw();
710   },
711 
712   /** @private - adapt for nested store */
713   dataSourceDidDestroy: function(storeKey) {
714     if( this.get( "dataSource" ) )
715       return sc_super();
716     else
717       SC.Store.NESTED_STORE_UNSUPPORTED_ERROR.throw();
718   },
719 
720   /** @private - adapt for nested store */
721   dataSourceDidError: function(storeKey, error) {
722     if( this.get( "dataSource" ) )
723       return sc_super();
724     else
725       SC.Store.NESTED_STORE_UNSUPPORTED_ERROR.throw();
726   },
727 
728   // ..........................................................
729   // PUSH CHANGES FROM DATA SOURCE
730   //
731 
732   /** @private - adapt for nested store */
733   pushRetrieve: function(recordType, id, dataHash, storeKey) {
734     if( this.get( "dataSource" ) )
735       return sc_super();
736     else
737       SC.Store.NESTED_STORE_UNSUPPORTED_ERROR.throw();
738   },
739 
740   /** @private - adapt for nested store */
741   pushDestroy: function(recordType, id, storeKey) {
742     if( this.get( "dataSource" ) )
743       return sc_super();
744     else
745       SC.Store.NESTED_STORE_UNSUPPORTED_ERROR.throw();
746   },
747 
748   /** @private - adapt for nested store */
749   pushError: function(recordType, id, error, storeKey) {
750     if( this.get( "dataSource" ) )
751       return sc_super();
752     else
753       SC.Store.NESTED_STORE_UNSUPPORTED_ERROR.throw();
754   }
755 
756 }) ;
757 
758