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/query');
  9 
 10 /**
 11   @class
 12 
 13   A Record is the core model class in SproutCore. It is analogous to
 14   NSManagedObject in Core Data and EOEnterpriseObject in the Enterprise
 15   Objects Framework (aka WebObjects), or ActiveRecord::Base in Rails.
 16 
 17   To create a new model class, in your SproutCore workspace, do:
 18 
 19       $ sc-gen model MyApp.MyModel
 20 
 21   This will create MyApp.MyModel in clients/my_app/models/my_model.js.
 22 
 23   The core attributes hash is used to store the values of a record in a
 24   format that can be easily passed to/from the server.  The values should
 25   generally be stored in their raw string form.  References to external
 26   records should be stored as primary keys.
 27 
 28   Normally you do not need to work with the attributes hash directly.
 29   Instead you should use get/set on normal record properties.  If the
 30   property is not defined on the object, then the record will check the
 31   attributes hash instead.
 32 
 33   You can bulk update attributes from the server using the
 34   `updateAttributes()` method.
 35 
 36   # Polymorphic Records
 37 
 38   SC.Record also supports polymorphism, which allows subclasses of a record type to share a common
 39   identity. Polymorphism is similar to inheritance (i.e. a polymorphic subclass inherits its parents
 40   properties), but differs in that polymorphic subclasses can be considered to be "equal" to each
 41   other and their superclass. This means that any memmber of the polymorphic class group should be
 42   able to stand in for any other member.
 43 
 44   These examples may help identify the difference. First, let's look at the classic inheritance
 45   model,
 46 
 47       // This is the "root" class. All subclasses of MyApp.Person will be unique from MyApp.Person.
 48       MyApp.Person = SC.Record.extend({});
 49 
 50       // As a subclass, MyApp.Female inherits from a MyApp.Person, but is not "equal" to it.
 51       MyApp.Female = MyApp.Person.extend({
 52         isFemale: true
 53       });
 54 
 55       // As a subclass, MyApp.Male inherits from a MyApp.Person, but is not "equal" to it.
 56       MyApp.Male = MyApp.Person.extend({
 57         isMale: true
 58       });
 59 
 60       // Load two unique records into the store.
 61       MyApp.store.createRecord(MyApp.Female, { guid: '1' });
 62       MyApp.store.createRecord(MyApp.Male, { guid: '2' });
 63 
 64       // Now we can see that these records are isolated from each other.
 65       var female = MyApp.store.find(MyApp.Person, '1'); // Returns an SC.Record.EMPTY record.
 66       var male = MyApp.store.find(MyApp.Person, '2'); // Returns an SC.Record.EMPTY record.
 67 
 68       // These records are MyApp.Person only.
 69       SC.kindOf(female, MyApp.Female); // false
 70       SC.kindOf(male, MyApp.Male); // false
 71 
 72   Next, let's make MyApp.Person a polymorphic class,
 73 
 74       // This is the "root" polymorphic class. All subclasses of MyApp.Person will be able to stand-in as a MyApp.Person.
 75       MyApp.Person = SC.Record.extend({
 76         isPolymorphic: true
 77       });
 78 
 79       // As a polymorphic subclass, MyApp.Female is "equal" to a MyApp.Person.
 80       MyApp.Female = MyApp.Person.extend({
 81         isFemale: true
 82       });
 83 
 84       // As a polymorphic subclass, MyApp.Male is "equal" to a MyApp.Person.
 85       MyApp.Male = MyApp.Person.extend({
 86         isMale: true
 87       });
 88 
 89       // Load two unique records into the store.
 90       MyApp.store.createRecord(MyApp.Female, { guid: '1' });
 91       MyApp.store.createRecord(MyApp.Male, { guid: '2' });
 92 
 93       // Now we can see that these records are in fact "equal" to each other. Which means that if we
 94       // search for "people", we will get "males" & "females".
 95       var female = MyApp.store.find(MyApp.Person, '1'); // Returns record.
 96       var male = MyApp.store.find(MyApp.Person, '2'); // Returns record.
 97 
 98       // These records are MyApp.Person as well as their unique subclass.
 99       SC.kindOf(female, MyApp.Female); // true
100       SC.kindOf(male, MyApp.Male); // true
101 
102   @extends SC.Object
103   @see SC.RecordAttribute
104   @since SproutCore 1.0
105 */
106 SC.Record = SC.Object.extend(
107 /** @scope SC.Record.prototype */ {
108 
109   //@if(debug)
110   /* BEGIN DEBUG ONLY PROPERTIES AND METHODS */
111 
112   /** @private
113     Creates string representation of record, with status.
114 
115     @returns {String}
116   */
117   toString: function () {
118     // We won't use 'readOnlyAttributes' here because accessing them directly
119     // avoids a SC.clone() -- we'll be careful not to edit anything.
120     var attrs = this.get('store').readDataHash(this.get('storeKey'));
121     return "%@(%@) %@".fmt(this.constructor.toString(), SC.inspect(attrs), this.statusString());
122   },
123 
124   /** @private
125     Creates string representation of record, with status.
126 
127     @returns {String}
128   */
129 
130   statusString: function () {
131     var ret = [], status = this.get('status');
132 
133     for(var prop in SC.Record) {
134       if(prop.match(/[A-Z_]$/) && SC.Record[prop]===status) {
135         ret.push(prop);
136       }
137     }
138 
139     return ret.join(" ");
140   },
141 
142   /* END DEBUG ONLY PROPERTIES AND METHODS */
143   //@endif
144 
145   /**
146     Walk like a duck
147 
148     @type Boolean
149     @default YES
150   */
151   isRecord: YES,
152 
153   // ----------------------------------------------------------------------------------------------
154   // Properties
155   //
156 
157   /**
158     Returns the id for the record instance.  The id is used to uniquely
159     identify this record instance from all others of the same type.  If you
160     have a `primaryKey set on this class, then the id will be the value of the
161     `primaryKey` property on the underlying JSON hash.
162 
163     @type String
164     @property
165     @dependsOn storeKey
166   */
167   id: function(key, value) {
168     if (value !== undefined) {
169       this.writeAttribute(this.get('primaryKey'), value);
170       return value;
171     } else {
172       return SC.Store.idFor(this.storeKey);
173     }
174   }.property('storeKey').cacheable(),
175 
176   /**
177     If you have nested records
178 
179     @type Boolean
180     @default NO
181   */
182   isParentRecord: NO,
183 
184   /**
185     This is the primary key used to distinguish records.  If the keys
186     match, the records are assumed to be identical.
187 
188     @type String
189     @default 'guid'
190   */
191   primaryKey: 'guid',
192 
193   /**
194     All records generally have a life cycle as they are created or loaded into
195     memory, modified, committed and finally destroyed.  This life cycle is
196     managed by the status property on your record.
197 
198     The status of a record is modelled as a finite state machine.  Based on the
199     current state of the record, you can determine which operations are
200     currently allowed on the record and which are not.
201 
202     In general, a record can be in one of five primary states:
203     `SC.Record.EMPTY`, `SC.Record.BUSY`, `SC.Record.READY`,
204     `SC.Record.DESTROYED`, `SC.Record.ERROR`.  These are all described in
205     more detail in the class mixin (below) where they are defined.
206 
207     @type Number
208     @property
209     @dependsOn storeKey
210   */
211   status: function() {
212     return this.store.readStatus(this.storeKey);
213   }.property('storeKey').cacheable(),
214 
215   /**
216     The store that owns this record.  All changes will be buffered into this
217     store and committed to the rest of the store chain through here.
218 
219     This property is set when the record instance is created and should not be
220     changed or else it will break the record behavior.
221 
222     @type SC.Store
223     @default null
224   */
225   store: null,
226 
227   /**
228     This is the store key for the record, it is used to link it back to the
229     dataHash. If a record is reused, this value will be replaced.
230 
231     You should not edit this store key but you may sometimes need to refer to
232     this store key when implementing a Server object.
233 
234     @type Number
235     @default null
236   */
237   storeKey: null,
238 
239   /**
240     YES when the record has been destroyed
241 
242     @type Boolean
243     @property
244     @dependsOn status
245   */
246   isDestroyed: function() {
247     return !!(this.get('status') & SC.Record.DESTROYED);
248   }.property('status').cacheable(),
249 
250   /**
251     `YES` when the record is in an editable state.  You can use this property to
252     quickly determine whether attempting to modify the record would raise an
253     exception or not.
254 
255     This property is both readable and writable.  Note however that if you
256     set this property to `YES` but the status of the record is anything but
257     `SC.Record.READY`, the return value of this property may remain `NO`.
258 
259     @type Boolean
260     @property
261     @dependsOn status
262   */
263   isEditable: function(key, value) {
264     if (value !== undefined) this._screc_isEditable = value;
265     if (this.get('status') & SC.Record.READY) return this._screc_isEditable;
266     else return NO;
267   }.property('status').cacheable(),
268 
269   /**
270     @private
271 
272     Backing value for isEditable
273   */
274   _screc_isEditable: YES, // default
275 
276   /**
277     `YES` when the record's contents have been loaded for the first time.  You
278     can use this to quickly determine if the record is ready to display.
279 
280     @type Boolean
281     @property
282     @dependsOn status
283   */
284   isLoaded: function() {
285     var K = SC.Record,
286         status = this.get('status');
287     return !((status===K.EMPTY) || (status===K.BUSY_LOADING) || (status===K.ERROR));
288   }.property('status').cacheable(),
289 
290   /**
291     If set, this should be an array of active relationship objects that need
292     to be notified whenever the underlying record properties change.
293     Currently this is only used by toMany relationships, but you could
294     possibly patch into this yourself also if you are building your own
295     relationships.
296 
297     Note this must be a regular Array - NOT any object implementing SC.Array.
298 
299     @type Array
300     @default null
301   */
302   relationships: null,
303 
304   /**
305     This will return the raw attributes that you can edit directly.  If you
306     make changes to this hash, be sure to call `beginEditing()` before you get
307     the attributes and `endEditing()` afterwards.
308 
309     @type Hash
310     @property
311   **/
312   attributes: function() {
313     var store    = this.get('store'),
314         storeKey = this.storeKey;
315     return store.readEditableDataHash(storeKey);
316   }.property(),
317 
318   /**
319     This will return the raw attributes that you cannot edit directly.  It is
320     useful if you want to efficiently look at multiple attributes in bulk.  If
321     you would like to edit the attributes, see the `attributes` property
322     instead.
323 
324     @type Hash
325     @property
326   **/
327   readOnlyAttributes: function() {
328     var store    = this.get('store'),
329         storeKey = this.storeKey,
330         ret      = store.readDataHash(storeKey);
331 
332     if (ret) ret = SC.clone(ret, YES);
333 
334     return ret;
335   }.property(),
336 
337   /**
338     The namespace which to retrieve the childRecord Types from
339 
340     @type String
341     @default null
342   */
343   nestedRecordNamespace: null,
344 
345   /**
346     Whether or not this is a nested Record.
347 
348     @type Boolean
349     @property
350   */
351   isNestedRecord: function(){
352     var store = this.get('store'), ret,
353         sk = this.get('storeKey'),
354         prKey = store.parentStoreKeyExists(sk);
355 
356     ret = prKey ? YES : NO;
357     return ret;
358   }.property().cacheable(),
359 
360   /**
361     The parent record if this is a nested record.
362 
363     @type Boolean
364     @property
365   */
366   parentRecord: function(){
367     var sk = this.storeKey, store = this.get('store');
368     return store.materializeParentRecord(sk);
369   }.property(),
370 
371   // ...............................
372   // CRUD OPERATIONS
373   //
374 
375   /**
376     Refresh the record from the persistent store.  If the record was loaded
377     from a persistent store, then the store will be asked to reload the
378     record data from the server.  If the record is new and exists only in
379     memory then this call will have no effect.
380 
381     @param {boolean} recordOnly optional param if you want to only THIS record
382       even if it is a child record.
383     @param {Function} callback optional callback that will fire when request finishes
384 
385     @returns {SC.Record} receiver
386   */
387   refresh: function(recordOnly, callback) {
388     var store = this.get('store'), rec, ro,
389         sk = this.get('storeKey'),
390         prKey = store.parentStoreKeyExists();
391 
392     // If we only want to commit this record or it doesn't have a parent record
393     // we will commit this record
394     ro = recordOnly || (SC.none(recordOnly) && SC.none(prKey));
395     if (ro){
396       store.refreshRecord(null, null, sk, callback);
397     } else if (prKey){
398       rec = store.materializeRecord(prKey);
399       rec.refresh(recordOnly, callback);
400     }
401 
402     return this;
403   },
404 
405   /**
406     Deletes the record along with any dependent records.  This will mark the
407     records destroyed in the store as well as changing the isDestroyed
408     property on the record to YES.  If this is a new record, this will avoid
409     creating the record in the first place.
410 
411     @param {boolean} recordOnly optional param if you want to only THIS record
412       even if it is a child record.
413 
414     @returns {SC.Record} receiver
415   */
416   destroy: function(recordOnly) {
417     var store = this.get('store'), rec, ro,
418         sk = this.get('storeKey'),
419         prKey = store.parentStoreKeyExists();
420 
421     // If we only want to commit this record or it doesn't have a parent record
422     // we will commit this record
423     ro = recordOnly || (SC.none(recordOnly) && SC.none(prKey));
424     if (ro){
425       store.destroyRecord(null, null, sk);
426       this.notifyPropertyChange('status');
427       // If there are any aggregate records, we might need to propagate our new
428       // status to them.
429       this.propagateToAggregates();
430 
431     } else if (prKey){
432       rec = store.materializeRecord(prKey);
433       rec.destroy(recordOnly);
434     }
435 
436     return this;
437   },
438 
439   /**
440     You can invoke this method anytime you need to make the record as dirty.
441     This will cause the record to be committed when you `commitChanges()`
442     on the underlying store.
443 
444     If you use the `writeAttribute()` primitive, this method will be called
445     for you.
446 
447     If you pass the key that changed it will ensure that observers are fired
448     only once for the changed property instead of `allPropertiesDidChange()`
449 
450     @param {String} key key that changed (optional)
451     @returns {SC.Record} receiver
452   */
453   recordDidChange: function(key) {
454 
455     // If we have a parent, they changed too!
456     var p = this.get('parentRecord');
457     if (p) p.recordDidChange();
458 
459     this.get('store').recordDidChange(null, null, this.get('storeKey'), key);
460     this.notifyPropertyChange('status');
461 
462     // If there are any aggregate records, we might need to propagate our new
463     // status to them.
464     this.propagateToAggregates();
465 
466     return this;
467   },
468 
469   toJSON: function(){
470     return this.get('attributes');
471   },
472 
473   // ...............................
474   // ATTRIBUTES
475   //
476 
477   /** @private
478     Current edit level.  Used to defer editing changes.
479   */
480   _editLevel: 0 ,
481 
482   /**
483     Defers notification of record changes until you call a matching
484     `endEditing()` method.  This method is called automatically whenever you
485     set an attribute, but you can call it yourself to group multiple changes.
486 
487     Calls to `beginEditing()` and `endEditing()` can be nested.
488 
489     @returns {SC.Record} receiver
490   */
491   beginEditing: function() {
492     this._editLevel++;
493     return this;
494   },
495 
496   /**
497     Notifies the store of record changes if this matches a top level call to
498     `beginEditing()`.  This method is called automatically whenever you set an
499     attribute, but you can call it yourself to group multiple changes.
500 
501     Calls to `beginEditing()` and `endEditing()` can be nested.
502 
503     @param {String} key key that changed (optional)
504     @returns {SC.Record} receiver
505   */
506   endEditing: function(key) {
507     if(--this._editLevel <= 0) {
508       this._editLevel = 0;
509       this.recordDidChange(key);
510     }
511     return this;
512   },
513 
514   /**
515     Reads the raw attribute from the underlying data hash.  This method does
516     not transform the underlying attribute at all.
517 
518     @param {String} key the attribute you want to read
519     @returns {Object} the value of the key, or undefined if it doesn't exist
520   */
521   readAttribute: function(key) {
522     var store = this.get('store'), storeKey = this.storeKey;
523     var attrs = store.readDataHash(storeKey);
524     return attrs ? attrs[key] : undefined;
525   },
526 
527   /**
528     Updates the passed attribute with the new value.  This method does not
529     transform the value at all.  If instead you want to modify an array or
530     hash already defined on the underlying json, you should instead get
531     an editable version of the attribute using `editableAttribute()`.
532 
533     @param {String} key the attribute you want to read
534     @param {Object} value the value you want to write
535     @param {Boolean} ignoreDidChange only set if you do NOT want to flag
536       record as dirty
537     @returns {SC.Record} receiver
538   */
539   writeAttribute: function(key, value, ignoreDidChange) {
540     var store    = this.get('store'),
541         storeKey = this.storeKey,
542         attrs;
543 
544     attrs = store.readEditableDataHash(storeKey);
545     if (!attrs) SC.Record.BAD_STATE_ERROR.throw();
546 
547     // if value is the same, do not flag record as dirty
548     if (value !== attrs[key]) {
549       if(!ignoreDidChange) this.beginEditing();
550       attrs[key] = value;
551 
552       // If the key is the primaryKey of the record, we need to tell the store
553       // about the change.
554       if (key === this.get('primaryKey')) {
555         SC.Store.replaceIdFor(storeKey, value);
556         this.propertyDidChange('id'); // Reset computed value
557       }
558 
559       if(!ignoreDidChange) { this.endEditing(key); }
560       else {
561         // We must still inform the store of the change so that it can track the change across stores.
562         store.dataHashDidChange(storeKey, null, undefined, key);
563       }
564     }
565     return this;
566   },
567 
568   /**
569     This will also ensure that any aggregate records are also marked dirty
570     if this record changes.
571 
572     Should not have to be called manually.
573   */
574   propagateToAggregates: function() {
575     var storeKey   = this.get('storeKey'),
576         recordType = SC.Store.recordTypeFor(storeKey),
577         aggregates = recordType.__sc_aggregate_keys,
578         idx, len, key, prop, val, recs;
579 
580     // if recordType aggregates are not set up yet, make sure to
581     // create the cache first
582     if (!aggregates) {
583       aggregates = [];
584       for (key in this) {
585         prop = this[key];
586         if (prop  &&  prop.isRecordAttribute  &&  prop.aggregate === YES) {
587           aggregates.push(key);
588         }
589       }
590       recordType.__sc_aggregate_keys = aggregates;
591     }
592 
593     // now loop through all aggregate properties and mark their related
594     // record objects as dirty
595     var K          = SC.Record,
596         dirty      = K.DIRTY,
597         readyNew   = K.READY_NEW,
598         destroyed  = K.DESTROYED,
599         readyClean = K.READY_CLEAN,
600         iter;
601 
602     /**
603       @private
604 
605       If the child is dirty, then make sure the parent gets a dirty
606       status.  (If the child is created or destroyed, there's no need,
607       because the parent will dirty itself when it modifies that
608       relationship.)
609 
610       @param {SC.Record} record to propagate to
611     */
612     iter =  function(rec) {
613       var childStatus, parentStore, parentStoreKey, parentStatus;
614 
615       if (rec) {
616         childStatus = this.get('status');
617         if ((childStatus & dirty)  ||
618             (childStatus & readyNew)  ||  (childStatus & destroyed)) {
619 
620           // Since the parent can cache 'status', and we might be called before
621           // it has been invalidated, we'll read the status directly rather than
622           // trusting the cache.
623           parentStore    = rec.get('store');
624           parentStoreKey = rec.get('storeKey');
625           parentStatus   = parentStore.peekStatus(parentStoreKey);
626           if (parentStatus === readyClean) {
627             // Note:  storeDidChangeProperties() won't put it in the
628             //        changelog!
629             rec.get('store').recordDidChange(rec.constructor, null, rec.get('storeKey'), null, YES);
630           }
631         }
632       }
633     };
634 
635     for(idx=0,len=aggregates.length;idx<len;++idx) {
636       key = aggregates[idx];
637       val = this.get(key);
638       recs = SC.kindOf(val, SC.ManyArray) ? val : [val];
639       recs.forEach(iter, this);
640     }
641   },
642 
643   /**
644     Called by the store whenever the underlying data hash has changed.  This
645     will notify any observers interested in data hash properties that they
646     have changed.
647 
648     @param {Boolean} statusOnly changed
649     @param {String} key that changed (optional)
650     @returns {SC.Record} receiver
651   */
652   storeDidChangeProperties: function(statusOnly, keys) {
653     // TODO:  Should this function call propagateToAggregates() at the
654     //        appropriate times?
655     if (statusOnly) this.notifyPropertyChange('status');
656     else {
657       if (keys) {
658         this.beginPropertyChanges();
659         keys.forEach(function(k) { this.notifyPropertyChange(k); }, this);
660         this.notifyPropertyChange('status');
661         this.endPropertyChanges();
662 
663       } else {
664         this.allPropertiesDidChange();
665       }
666 
667       // also notify manyArrays
668       var manyArrays = this.relationships,
669           loc        = manyArrays ? manyArrays.length : 0;
670       while(--loc>=0) manyArrays[loc].recordPropertyDidChange(keys);
671     }
672   },
673 
674   /**
675     Normalizing a record will ensure that the underlying hash conforms
676     to the record attributes such as their types (transforms) and default
677     values.
678 
679     This method will write the conforming hash to the store and return
680     the materialized record.
681 
682     By normalizing the record, you can use `.attributes()` and be
683     assured that it will conform to the defined model. For example, this
684     can be useful in the case where you need to send a JSON representation
685     to some server after you have used `.createRecord()`, since this method
686     will enforce the 'rules' in the model such as their types and default
687     values. You can also include null values in the hash with the
688     includeNull argument.
689 
690     @param {Boolean} includeNull will write empty (null) attributes
691     @returns {SC.Record} the normalized record
692   */
693 
694   normalize: function(includeNull) {
695     var primaryKey = this.primaryKey,
696         store      = this.get('store'),
697         storeKey   = this.get('storeKey'),
698         keysToKeep = {},
699         key, valueForKey, typeClass, recHash, attrValue, isRecord,
700         isChild, defaultVal, keyForDataHash, attr;
701 
702     var dataHash = store.readEditableDataHash(storeKey) || {};
703     recHash = store.readDataHash(storeKey);
704 
705     // For now we're going to be agnostic about whether ids should live in the
706     // hash or not.
707     keysToKeep[primaryKey] = YES;
708 
709     for (key in this) {
710       // make sure property is a record attribute.
711       valueForKey = this[key];
712       if (valueForKey) {
713         typeClass = valueForKey.typeClass;
714         if (typeClass) {
715           keyForDataHash = valueForKey.get('key') || key; // handle alt keys
716 
717           // As we go, we'll build up a key —> attribute mapping table that we
718           // can use when purging keys from the data hash that are not defined
719           // in the schema, below.
720           keysToKeep[keyForDataHash] = YES;
721 
722           isRecord = SC.typeOf(typeClass.call(valueForKey))===SC.T_CLASS;
723           isChild  = valueForKey.isNestedRecordTransform;
724           if (!isRecord && !isChild) {
725             attrValue = this.get(key);
726             if(attrValue!==undefined && (attrValue!==null || includeNull)) {
727               attr = this[key];
728               // if record attribute, make sure we transform with the fromType
729               if(SC.kindOf(attr, SC.RecordAttribute)) {
730                 attrValue = attr.fromType(this, key, attrValue);
731               }
732               dataHash[keyForDataHash] = attrValue;
733             }
734             else if(!includeNull) {
735               keysToKeep[keyForDataHash] = NO;
736             }
737 
738           } else if (isChild) {
739             attrValue = this.get(key);
740 
741             // Sometimes a child attribute property does not refer to a child record.
742             // Catch this and don't try to normalize.
743             if (attrValue && attrValue.normalize) {
744               attrValue.normalize();
745             }
746           } else if (isRecord) {
747             attrValue = recHash[keyForDataHash];
748             if (attrValue !== undefined) {
749               // write value already there
750               dataHash[keyForDataHash] = attrValue;
751             } else {
752               // or write default
753               defaultVal = valueForKey.get('defaultValue');
754 
755               // computed default value
756               if (SC.typeOf(defaultVal)===SC.T_FUNCTION) {
757                 dataHash[keyForDataHash] = defaultVal(this, key, defaultVal);
758               } else {
759                 // plain value
760                 dataHash[keyForDataHash] = defaultVal;
761               }
762             }
763           }
764         }
765       }
766     }
767 
768     // Finally, we'll go through the underlying data hash and remove anything
769     // for which no appropriate attribute is defined.  We can do this using
770     // the mapping table we prepared above.
771     for (key in dataHash) {
772       if (!keysToKeep[key]) {
773         // Deleting a key doesn't seem too common unless it's a mistake, so
774         // we'll log it in debug mode.
775         SC.debug("%@:  Deleting key from underlying data hash due to normalization:  %@", this, key);
776         delete dataHash[key];
777       }
778     }
779 
780     return this;
781   },
782 
783 
784 
785   /**
786     If you try to get/set a property not defined by the record, then this
787     method will be called. It will try to get the value from the set of
788     attributes.
789 
790     This will also check is `ignoreUnknownProperties` is set on the recordType
791     so that they will not be written to `dataHash` unless explicitly defined
792     in the model schema.
793 
794     @param {String} key the attribute being get/set
795     @param {Object} value the value to set the key to, if present
796     @returns {Object} the value
797   */
798   unknownProperty: function(key, value) {
799 
800     if (value !== undefined) {
801 
802       // first check if we should ignore unknown properties for this
803       // recordType
804       var storeKey = this.get('storeKey'),
805         recordType = SC.Store.recordTypeFor(storeKey);
806 
807       if(recordType.ignoreUnknownProperties===YES) {
808         this[key] = value;
809         return value;
810       }
811 
812       // if we're modifying the PKEY, then `SC.Store` needs to relocate where
813       // this record is cached. store the old key, update the value, then let
814       // the store do the housekeeping...
815       var primaryKey = this.get('primaryKey');
816       this.writeAttribute(key,value);
817 
818       // update ID if needed
819       if (key === primaryKey) {
820         SC.Store.replaceIdFor(storeKey, value);
821       }
822 
823     }
824     return this.readAttribute(key);
825   },
826 
827   /**
828     Lets you commit this specific record to the store which will trigger
829     the appropriate methods in the data source for you.
830 
831     @param {Hash} params optional additional params that will passed down
832       to the data source
833     @param {boolean} recordOnly optional param if you want to only commit a single
834       record if it has a parent.
835     @param {Function} callback optional callback that the store will fire once the
836     datasource finished committing
837     @returns {SC.Record} receiver
838   */
839   commitRecord: function(params, recordOnly, callback) {
840     var store = this.get('store'), rec, ro,
841         prKey = store.parentStoreKeyExists();
842 
843     // If we only want to commit this record or it doesn't have a parent record
844     // we will commit this record
845     ro = recordOnly || (SC.none(recordOnly) && SC.none(prKey));
846     if (ro){
847       store.commitRecord(undefined, undefined, this.get('storeKey'), params, callback);
848     } else if (prKey){
849       rec = store.materializeRecord(prKey);
850       rec.commitRecord(params, recordOnly, callback);
851     }
852     return this;
853   },
854 
855   // ..........................................................
856   // EMULATE SC.ERROR API
857   //
858 
859   /**
860     Returns `YES` whenever the status is SC.Record.ERROR.  This will allow you
861     to put the UI into an error state.
862 
863     @type Boolean
864     @property
865     @dependsOn status
866   */
867   isError: function() {
868     return !!(this.get('status') & SC.Record.ERROR);
869   }.property('status').cacheable(),
870 
871   /**
872     Returns the receiver if the record is in an error state.  Returns null
873     otherwise.
874 
875     @type SC.Record
876     @property
877     @dependsOn isError
878   */
879   errorValue: function() {
880     return this.get('isError') ? SC.val(this.get('errorObject')) : null;
881   }.property('isError').cacheable(),
882 
883   /**
884     Returns the current error object only if the record is in an error state.
885     If no explicit error object has been set, returns SC.Record.GENERIC_ERROR.
886 
887     @type SC.Error
888     @property
889     @dependsOn isError
890   */
891   errorObject: function() {
892     if (this.get('isError')) {
893       var store = this.get('store');
894       return store.readError(this.get('storeKey')) || SC.Record.GENERIC_ERROR;
895     } else return null;
896   }.property('isError').cacheable(),
897 
898   // ...............................
899   // PRIVATE
900   //
901 
902   /** @private
903     Sets the key equal to value.
904 
905     This version will first check to see if the property is an
906     `SC.RecordAttribute`, and if so, will ensure that its isEditable property
907     is `YES` before attempting to change the value.
908 
909     @param key {String} the property to set
910     @param value {Object} the value to set or null.
911     @returns {SC.Record}
912   */
913   set: function(key, value) {
914     var func = this[key];
915 
916     if (func && func.isProperty && func.get && !func.get('isEditable')) {
917       return this;
918     }
919     return sc_super();
920   },
921 
922   /**
923     Registers a child record with this parent record.
924 
925     If the parent already knows about the child record, return the cached
926     instance. If not, create the child record instance and add it to the child
927     record cache.
928 
929     @param {Hash} value The hash of attributes to apply to the child record.
930     @param {Integer} key The store key that we are asking for
931     @param {String} path The property path of the child record
932     @returns {SC.Record} the child record that was registered
933    */
934   registerNestedRecord: function(value, key, path) {
935     var store, psk = this.get('storeKey'), csk, childRecord, recordType;
936 
937     // if no path is entered it must be the key
938     if (SC.none(path)) path = key;
939     // if a record instance is passed, simply use the storeKey.  This allows
940     // you to pass a record from a chained store to get the same record in the
941     // current store.
942     if (value && value.get && value.get('isRecord')) {
943       childRecord = value;
944     }
945     else {
946       recordType = this._materializeNestedRecordType(value, key);
947       childRecord = this.createNestedRecord(recordType, value, psk, path);
948     }
949     if (childRecord){
950       this.isParentRecord = YES;
951       store = this.get('store');
952       csk = childRecord.get('storeKey');
953       store.registerChildToParent(psk, csk, path);
954     }
955 
956     return childRecord;
957   },
958 
959   /**
960     Unregisters a child record from its parent record.
961 
962     Since accessing a child (nested) record creates a new data hash for the
963     child and caches the child record and its relationship to the parent record,
964     it's important to clear those caches when the child record is overwritten
965     or removed.  This function tells the store to remove the child record from
966     the store's various child record caches.
967 
968     You should not need to call this function directly.  Simply setting the
969     child record property on the parent to a different value will cause the
970     previous child record to be unregistered.
971 
972     @param {String} path The property path of the child record.
973   */
974   unregisterNestedRecord: function(path) {
975     var childRecord, csk, store;
976 
977     store = this.get('store');
978     childRecord = this.getPath(path);
979     csk = childRecord.get('storeKey');
980     store.unregisterChildFromParent(csk);
981   },
982 
983   /**
984     @private
985 
986      private method that retrieves the `recordType` from the hash that is
987      provided.
988 
989      Important for use in polymorphism but you must have the following items
990      in the parent record:
991 
992      `nestedRecordNamespace` <= this is the object that has the `SC.Records`
993      defined
994 
995      @param {Hash} value The hash of attributes to apply to the child record.
996      @param {String} key the name of the key on the attribute
997      @param {SC.Record} the record that was materialized
998     */
999   _materializeNestedRecordType: function(value, key){
1000     var childNS, recordType;
1001 
1002     // Get the record type, first checking the "type" property on the hash.
1003     if (SC.typeOf(value) === SC.T_HASH) {
1004       // Get the record type.
1005       childNS = this.get('nestedRecordNamespace');
1006       if (value.type && !SC.none(childNS)) {
1007         recordType = childNS[value.type];
1008       }
1009     }
1010 
1011     // Maybe it's not a hash or there was no type property.
1012     if (!recordType && key && this[key]) {
1013       recordType = this[key].get('typeClass');
1014     }
1015 
1016     // When all else fails throw and exception.
1017     if (!recordType || !SC.kindOf(recordType, SC.Record)) {
1018       throw new Error('SC.Child: Error during transform: Invalid record type.');
1019     }
1020 
1021     return recordType;
1022   },
1023 
1024   /**
1025     Creates a new nested record instance.
1026 
1027     @param {SC.Record} recordType The type of the nested record to create.
1028     @param {Hash} hash The hash of attributes to apply to the child record.
1029     (may be null)
1030     @returns {SC.Record} the nested record created
1031    */
1032   createNestedRecord: function(recordType, hash, psk, path) {
1033     var store = this.get('store'), id, sk, cr = null;
1034 
1035     hash = hash || {}; // init if needed
1036 
1037     if (SC.none(store)) throw new Error('Error: during the creation of a child record: NO STORE ON PARENT!');
1038 
1039     // Check for a primary key in the child record hash and if not found, then
1040     // check for a custom id generation function and if we still have no id,
1041     // generate a unique (and re-createable) id based on the parent's
1042     // storeKey.  Having the generated id be re-createable is important so
1043     // that we don't keep making new storeKeys for the same child record each
1044     // time that it is reloaded.
1045     id = hash[recordType.prototype.primaryKey];
1046     if (!id) { id = this.generateIdForChild(cr); }
1047     if (!id) { id = psk + '.' + path; }
1048 
1049     // If there is an id, there may also be a storeKey.  If so, update the
1050     // hash for the child record in the store and materialize it.  If not,
1051     // then create the child record.
1052     sk = store.storeKeyExists(recordType, id);
1053     if (sk) {
1054       store.writeDataHash(sk, hash);
1055       cr = store.materializeRecord(sk);
1056     } else {
1057       cr = store.createRecord(recordType, hash, id);
1058     }
1059 
1060     return cr;
1061   },
1062 
1063   _nestedRecordKey: 0,
1064 
1065   /**
1066     Override this function if you want to have a special way of creating
1067     ids for your child records
1068 
1069     @param {SC.Record} childRecord
1070     @returns {String} the id generated
1071    */
1072   generateIdForChild: function(childRecord){}
1073 
1074 });
1075 
1076 // Class Methods
1077 SC.Record.mixin( /** @scope SC.Record */ {
1078 
1079   /**
1080     Whether to ignore unknown properties when they are being set on the record
1081     object. This is useful if you want to strictly enforce the model schema
1082     and not allow dynamically expanding it by setting new unknown properties
1083 
1084     @static
1085     @type Boolean
1086     @default NO
1087   */
1088   ignoreUnknownProperties: NO,
1089 
1090   // ..........................................................
1091   // CONSTANTS
1092   //
1093 
1094   /**
1095     Generic state for records with no local changes.
1096 
1097     Use a logical AND (single `&`) to test record status
1098 
1099     @static
1100     @constant
1101     @type Number
1102     @default 0x0001
1103   */
1104   CLEAN:            0x0001, // 1
1105 
1106   /**
1107     Generic state for records with local changes.
1108 
1109     Use a logical AND (single `&`) to test record status
1110 
1111     @static
1112     @constant
1113     @type Number
1114     @default 0x0002
1115   */
1116   DIRTY:            0x0002, // 2
1117 
1118   /**
1119     State for records that are still loaded.
1120 
1121     A record instance should never be in this state.  You will only run into
1122     it when working with the low-level data hash API on `SC.Store`. Use a
1123     logical AND (single `&`) to test record status
1124 
1125     @static
1126     @constant
1127     @type Number
1128     @default 0x0100
1129   */
1130   EMPTY:            0x0100, // 256
1131 
1132   /**
1133     State for records in an error state.
1134 
1135     Use a logical AND (single `&`) to test record status
1136 
1137     @static
1138     @constant
1139     @type Number
1140     @default 0x1000
1141   */
1142   ERROR:            0x1000, // 4096
1143 
1144   /**
1145     Generic state for records that are loaded and ready for use
1146 
1147     Use a logical AND (single `&`) to test record status
1148 
1149     @static
1150     @constant
1151     @type Number
1152     @default 0x0200
1153   */
1154   READY:            0x0200, // 512
1155 
1156   /**
1157     State for records that are loaded and ready for use with no local changes
1158 
1159     Use a logical AND (single `&`) to test record status
1160 
1161     @static
1162     @constant
1163     @type Number
1164     @default 0x0201
1165   */
1166   READY_CLEAN:      0x0201, // 513
1167 
1168 
1169   /**
1170     State for records that are loaded and ready for use with local changes
1171 
1172     Use a logical AND (single `&`) to test record status
1173 
1174     @static
1175     @constant
1176     @type Number
1177     @default 0x0202
1178   */
1179   READY_DIRTY:      0x0202, // 514
1180 
1181 
1182   /**
1183     State for records that are new - not yet committed to server
1184 
1185     Use a logical AND (single `&`) to test record status
1186 
1187     @static
1188     @constant
1189     @type Number
1190     @default 0x0203
1191   */
1192   READY_NEW:        0x0203, // 515
1193 
1194 
1195   /**
1196     Generic state for records that have been destroyed
1197 
1198     Use a logical AND (single `&`) to test record status
1199 
1200     @static
1201     @constant
1202     @type Number
1203     @default 0x0400
1204   */
1205   DESTROYED:        0x0400, // 1024
1206 
1207 
1208   /**
1209     State for records that have been destroyed and committed to server
1210 
1211     Use a logical AND (single `&`) to test record status
1212 
1213     @static
1214     @constant
1215     @type Number
1216     @default 0x0401
1217   */
1218   DESTROYED_CLEAN:  0x0401, // 1025
1219 
1220 
1221   /**
1222     State for records that have been destroyed but not yet committed to server
1223 
1224     Use a logical AND (single `&`) to test record status
1225 
1226     @static
1227     @constant
1228     @type Number
1229     @default 0x0402
1230   */
1231   DESTROYED_DIRTY:  0x0402, // 1026
1232 
1233 
1234   /**
1235     Generic state for records that have been submitted to data source
1236 
1237     Use a logical AND (single `&`) to test record status
1238 
1239     @static
1240     @constant
1241     @type Number
1242     @default 0x0800
1243   */
1244   BUSY:             0x0800, // 2048
1245 
1246 
1247   /**
1248     State for records that are still loading data from the server
1249 
1250     Use a logical AND (single `&`) to test record status
1251 
1252     @static
1253     @constant
1254     @type Number
1255     @default 0x0804
1256   */
1257   BUSY_LOADING:     0x0804, // 2052
1258 
1259 
1260   /**
1261     State for new records that were created and submitted to the server;
1262     waiting on response from server
1263 
1264     Use a logical AND (single `&`) to test record status
1265 
1266     @static
1267     @constant
1268     @type Number
1269     @default 0x0808
1270   */
1271   BUSY_CREATING:    0x0808, // 2056
1272 
1273 
1274   /**
1275     State for records that have been modified and submitted to server
1276 
1277     Use a logical AND (single `&`) to test record status
1278 
1279     @static
1280     @constant
1281     @type Number
1282     @default 0x0810
1283   */
1284   BUSY_COMMITTING:  0x0810, // 2064
1285 
1286 
1287   /**
1288     State for records that have requested a refresh from the server.
1289 
1290     Use a logical AND (single `&`) to test record status.
1291 
1292     @static
1293     @constant
1294     @type Number
1295     @default 0x0820
1296   */
1297   BUSY_REFRESH:     0x0820, // 2080
1298 
1299 
1300   /**
1301     State for records that have requested a refresh from the server.
1302 
1303     Use a logical AND (single `&`) to test record status
1304 
1305     @static
1306     @constant
1307     @type Number
1308     @default 0x0821
1309   */
1310   BUSY_REFRESH_CLEAN:  0x0821, // 2081
1311 
1312   /**
1313     State for records that have requested a refresh from the server.
1314 
1315     Use a logical AND (single `&`) to test record status
1316 
1317     @static
1318     @constant
1319     @type Number
1320     @default 0x0822
1321   */
1322   BUSY_REFRESH_DIRTY:  0x0822, // 2082
1323 
1324   /**
1325     State for records that have been destroyed and submitted to server
1326 
1327     Use a logical AND (single `&`) to test record status
1328 
1329     @static
1330     @constant
1331     @type Number
1332     @default 0x0840
1333   */
1334   BUSY_DESTROYING:  0x0840, // 2112
1335 
1336 
1337   // ..........................................................
1338   // ERRORS
1339   //
1340 
1341   /**
1342     Error for when you try to modify a record while it is in a bad
1343     state.
1344 
1345     @static
1346     @constant
1347     @type SC.Error
1348   */
1349   BAD_STATE_ERROR: SC.$error("Internal Inconsistency"),
1350 
1351   /**
1352     Error for when you try to create a new record that already exists.
1353 
1354     @static
1355     @constant
1356     @type SC.Error
1357   */
1358   RECORD_EXISTS_ERROR: SC.$error("Record Exists"),
1359 
1360   /**
1361     Error for when you attempt to locate a record that is not found
1362 
1363     @static
1364     @constant
1365     @type SC.Error
1366   */
1367   NOT_FOUND_ERROR: SC.$error("Not found "),
1368 
1369   /**
1370     Error for when you try to modify a record that is currently busy
1371 
1372     @static
1373     @constant
1374     @type SC.Error
1375   */
1376   BUSY_ERROR: SC.$error("Busy"),
1377 
1378   /**
1379     Generic unknown record error
1380 
1381     @static
1382     @constant
1383     @type SC.Error
1384   */
1385   GENERIC_ERROR: SC.$error("Generic Error"),
1386 
1387   /**
1388     If true, then searches for records of this type will return subclass instances. For example:
1389 
1390         Person = SC.Record.extend();
1391         Person.isPolymorphic = true;
1392 
1393         Male = Person.extend();
1394         Female = Person.extend();
1395 
1396     Using SC.Store#find, or a toOne or toMany relationship on Person will then return records of
1397     type Male and Female. Polymorphic record types must have unique GUIDs across all subclasses.
1398 
1399     @type Boolean
1400     @default NO
1401   */
1402   isPolymorphic: NO,
1403 
1404   /**
1405     @private
1406     The next child key to allocate.  A nextChildKey must always be greater than 0.
1407   */
1408   _nextChildKey: 0,
1409 
1410   // ..........................................................
1411   // CLASS METHODS
1412   //
1413 
1414   /**
1415     Helper method returns a new `SC.RecordAttribute` instance to map a simple
1416     value or to-one relationship.  At the very least, you should pass the
1417     type class you expect the attribute to have.  You may pass any additional
1418     options as well.
1419 
1420     Use this helper when you define SC.Record subclasses.
1421 
1422         MyApp.Contact = SC.Record.extend({
1423           firstName: SC.Record.attr(String, { isRequired: YES })
1424         });
1425 
1426     @param {Class} type the attribute type
1427     @param {Hash} opts the options for the attribute
1428     @returns {SC.RecordAttribute} created instance
1429   */
1430   attr: function(type, opts) {
1431     return SC.RecordAttribute.attr(type, opts);
1432   },
1433 
1434   /**
1435     Returns an `SC.RecordAttribute` that describes a fetched attribute.  When
1436     you reference this attribute, it will return an `SC.RecordArray` that uses
1437     the type as the fetch key and passes the attribute value as a param.
1438 
1439     Use this helper when you define SC.Record subclasses.
1440 
1441         MyApp.Group = SC.Record.extend({
1442           contacts: SC.Record.fetch('MyApp.Contact')
1443         });
1444 
1445     @param {SC.Record|String} recordType The type of records to load
1446     @param {Hash} opts the options for the attribute
1447     @returns {SC.RecordAttribute} created instance
1448   */
1449   fetch: function(recordType, opts) {
1450     return SC.FetchedAttribute.attr(recordType, opts);
1451   },
1452 
1453   /**
1454     Will return one of the following:
1455 
1456      1. `SC.ManyAttribute` that describes a record array backed by an
1457         array of guids stored in the underlying JSON.
1458      2. `SC.ChildrenAttribute` that describes a record array backed by a
1459         array of hashes.
1460 
1461     You can edit the contents of this relationship.
1462 
1463     For `SC.ManyAttribute`, If you set the inverse and `isMaster: NO` key,
1464     then editing this array will modify the underlying data, but the
1465     inverse key on the matching record will also be edited and that
1466     record will be marked as needing a change.
1467 
1468     @param {SC.Record|String} recordType The type of record to create
1469     @param {Hash} opts the options for the attribute
1470     @returns {SC.ManyAttribute|SC.ChildrenAttribute} created instance
1471   */
1472   toMany: function(recordType, opts) {
1473     opts = opts || {};
1474     var isNested = opts.nested || opts.isNested;
1475     var attr;
1476 
1477     this._throwUnlessRecordTypeDefined(recordType, 'toMany');
1478 
1479     if(isNested){
1480       //@if(debug)
1481       // Let's provide a little developer help for a common misunderstanding.
1482       if (!SC.none(opts.inverse)) {
1483         SC.error("Developer Error: Nested attributes (toMany and toOne with isNested: YES) may not have an inverse property. Nested attributes shouldn't exist as separate records with IDs and relationships; if they do, it's likely that they should be separate records.\n\nNote that if your API nests separate records for convenient requesting and transport, you should separate them in your data source rather than improperly modeling them with nested attributes.");
1484       }
1485       //@endif
1486       attr = SC.ChildrenAttribute.attr(recordType, opts);
1487     }
1488     else {
1489       attr = SC.ManyAttribute.attr(recordType, opts);
1490     }
1491     return attr;
1492   },
1493 
1494   /**
1495     Will return one of the following:
1496 
1497      1. `SC.SingleAttribute` that converts the underlying ID to a single
1498         record.  If you modify this property, it will rewrite the underlying
1499         ID. It will also modify the inverse of the relationship, if you set it.
1500      2. `SC.ChildAttribute` that you can edit the contents
1501         of this relationship.
1502 
1503     @param {SC.Record|String} recordType the type of the record to create
1504     @param {Hash} opts additional options
1505     @returns {SC.SingleAttribute|SC.ChildAttribute} created instance
1506   */
1507   toOne: function(recordType, opts) {
1508     opts = opts || {};
1509     var isNested = opts.nested || opts.isNested;
1510     var attr;
1511 
1512     this._throwUnlessRecordTypeDefined(recordType, 'toOne');
1513 
1514     if(isNested){
1515       //@if(debug)
1516       // Let's provide a little developer help for a common misunderstanding.
1517       if (!SC.none(opts.inverse)) {
1518         SC.error("Developer Error: Nested attributes (toMany and toOne with isNested: YES) may not have an inverse property. Nested attributes shouldn't exist as separate records with IDs and relationships; if they do, it's likely that they should be separate records.\n\nNote that if your API nests separate records for convenient requesting and transport, you should separate them in your data source rather than improperly modeling them with nested attributes.");
1519       }
1520       //@endif
1521       attr = SC.ChildAttribute.attr(recordType, opts);
1522     }
1523     else {
1524       attr = SC.SingleAttribute.attr(recordType, opts);
1525     }
1526     return attr;
1527   },
1528 
1529   _throwUnlessRecordTypeDefined: function(recordType, relationshipType) {
1530     if (!recordType) {
1531       throw new Error("Attempted to create " + relationshipType + " attribute with " +
1532             "undefined recordType. Did you forget to sc_require a dependency?");
1533     }
1534   },
1535 
1536   /**
1537     Returns all storeKeys mapped by Id for this record type.  This method is used mostly by the
1538     `SC.Store` and the Record to coordinate.  You will rarely need to call this method yourself.
1539 
1540     Note that for polymorpic record classes, all store keys are kept on the top-most polymorphic
1541     superclass. This ensures that store key by id requests at any level return only the one unique
1542     store key.
1543 
1544     @see SC.Record.storeKeysById
1545   */
1546   storeKeysById: function() {
1547     var superclass = this.superclass,
1548       key = SC.keyFor('storeKey', SC.guidFor(this)),
1549       ret = this[key];
1550 
1551     if (!ret) {
1552       if (this.isPolymorphic && superclass.isPolymorphic && superclass !== SC.Record) {
1553         ret = this[key] = superclass.storeKeysById();
1554       } else {
1555         ret = this[key] = {};
1556       }
1557     }
1558 
1559     return ret;
1560   },
1561 
1562   /**
1563     Given a primaryKey value for the record, returns the associated
1564     storeKey.  If the primaryKey has not been assigned a storeKey yet, it
1565     will be added.
1566 
1567     For the inverse of this method see `SC.Store.idFor()` and
1568     `SC.Store.recordTypeFor()`.
1569 
1570     @param {String} id a record id
1571     @returns {Number} a storeKey.
1572   */
1573   storeKeyFor: function(id) {
1574     var storeKeys = this.storeKeysById(),
1575         ret = storeKeys[id];
1576 
1577     if (!ret) {
1578       ret = SC.Store.generateStoreKey();
1579       SC.Store.idsByStoreKey[ret] = id;
1580       SC.Store.recordTypesByStoreKey[ret] = this;
1581       storeKeys[id] = ret;
1582     }
1583 
1584     return ret;
1585   },
1586 
1587   /**
1588     Given a primaryKey value for the record, returns the associated
1589     storeKey.  As opposed to `storeKeyFor()` however, this method
1590     will NOT generate a new storeKey but returned undefined.
1591 
1592     @param {String} id a record id
1593     @returns {Number} a storeKey.
1594   */
1595   storeKeyExists: function(id) {
1596     var storeKeys = this.storeKeysById(),
1597         ret = storeKeys[id];
1598 
1599     return ret;
1600   },
1601 
1602   /**
1603     Returns a record with the named ID in store.
1604 
1605     @param {SC.Store} store the store
1606     @param {String} id the record id or a query
1607     @returns {SC.Record} record instance
1608   */
1609   find: function(store, id) {
1610     return store.find(this, id);
1611   },
1612 
1613   /** @private - enhance extend to notify SC.Query and ensure polymorphic subclasses are marked as polymorphic as well. */
1614   extend: function() {
1615     var ret = SC.Object.extend.apply(this, arguments);
1616 
1617     if (SC.Query) SC.Query._scq_didDefineRecordType(ret);
1618 
1619     // All subclasses of a polymorphic class, must also be polymorphic.
1620     if (ret.prototype.hasOwnProperty('isPolymorphic')) {
1621       ret.isPolymorphic = ret.prototype.isPolymorphic;
1622       delete ret.prototype.isPolymorphic;
1623     }
1624 
1625     return ret;
1626   }
1627 
1628 });
1629