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