sproutcore / models / record.js
100644 816 lines (682 sloc) 25.563 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
// ========================================================================
// SproutCore -- JavaScript Application Framework
// Copyright ©2006-2008, Sprout Systems, Inc. and contributors.
// Portions copyright ©2008 Apple, Inc. All rights reserved.
// ========================================================================
 
require('system/object') ;
require('models/store') ;
 
/**
@class
 
A Record is the core model class in SproutCore. It is analogous to
NSManagedObject in Core Data and EOEnterpriseObject in the Enterprise
Objects Framework (aka WebObjects), or ActiveRecord::Base in Rails.
To create a new model class, in your SproutCore workspace, do:
{{{
$ sc-gen model my_app/my_model
}}}
 
This will create MyApp.MyModel in clients/my_app/models/my_model.js.
The core attributes hash is used to store the values of a record in a
format that can be easily passed to/from the server. The values should
generally be stored in their raw string form. References to external
records should be stored as primary keys.
Normally you do not need to work with the attributes hash directly.
Instead you should use get/set on normal record properties. If the
property is not defined on the object, then the record will check the
attributes hash instead.
You can bulk update attributes from the server using the
updateAttributes() method.
 
@extends SC.Object
@since SproutCore 1.0
*/
SC.Record = SC.Object.extend(
/** @scope SC.Record.prototype */ {
  
  // ...............................
  // PROPERTIES
  //
 
  /**
Override this with the properties you want the record to manage.
@field
@type {Array}
*/
  properties: ['guid'],
  
  /**
This is the primary key used to distinguish records. If the keys
match, the records are assumed to be identical.
@field
@type {String}
*/
  primaryKey: 'guid',
  
  /**
When a new empty record is created, this will be set to true. It will be
set to false again the first time the record is committed.
@field
@type {Boolean}
*/
  newRecord: false,
  
  /**
Set to non-zero whenever the record has uncommitted changes.
@field
@type {Number}
*/
  changeCount: 0,
  
  /**
Set to true when the record is deleted. Will cause it to be removed
from any member collections. Once no more objects hold references to it,
the property will be disabled.
@field
@type {Boolean}
*/
  isDeleted: false,
  
  // ...............................
  // CRUD OPERATIONS
  //
 
  /**
Set this URL to point to the type of resource this record is.
If you are using SC.Server, then put a '%@' where you expect the
primaryKey to be inserted to identify the record.
@field
@type {String}
*/
  resourceURL: null,
  
  /**
The item providing the data for this. Set to either the store or a
Server. Setting it to the Store will make refresh and commit effectively
null-ops.
@field
@type {SC.Store or SC.Server}
*/
  dataSource: SC.Store,
 
  /**
The URL where this record can be refreshed. Usually you would send the value
for this URL from the server in response to requests from Sproutcore.
@field
@type {String}
*/
  refreshURL: null,
 
  /**
The URL where this record can be updated. Usually you would send the value
for this URL from the server in response to requests from Sproutcore.
@field
@type {String}
*/
  updateURL: null,
 
  /**
The URL where this record can be destroyed. Usually you would send the value
for this URL from the server in response to requests from Sproutcore.
@field
@type {String}
*/
  destroyURL: null,
 
 
  init: function()
  {
    sc_super();
    
    var primaryKeyName = this.get('primaryKey');
    if (!this.get(primaryKeyName))
    {
      // no primary key passed for a new record.
      // we'll need to create one so that it can be cached in SC.Store
      // if this isn't desired behavior, override generateTempPrimaryKey to return false.
      var value = this.generateTempPrimaryKey();
      if (value) this.set(primaryKeyName, value);
    }
  },
 
  generateTempPrimaryKey: function()
  {
    return "@" + SC.guidFor(this);
  },
 
  /**
Invoked by the UI to request the model object be updated from the server.
Override to actually support server changes.
*/
  refresh: function() {
    if (!this.get('newRecord')) this.dataSource.refreshRecords([this]);
  },
  
  /**
Invoked by the UI to tell the model this record should be saved. Override
to support server changes. Note that this is used to support both the
create and update components of CRUD.
*/
  commit: function() {
    // no longer a new record once changes have been committed.
    if (this.get('newRecord')) {
      this.dataSource.createRecords([this]) ;
    } else {
      this.dataSource.commitRecords([this]) ;
    }
  },
  
  /**
This can delete the record. The non-server version just sets isDeleted.
*/
  destroy: function() { this.dataSource.destroyRecords([this]) ; },
 
  // ...............................
  // ATTRIBUTES
  //
  // The core attributes hash is used to store the values of a record in a
  // format that can be easily passed to/from the server. The values should
  // generally be stored in their raw string form. References to external
  // records should be stored as primary keys.
  //
  // Normally you do not need to work with the attributes hash directly.
  // Instead you should use get/set on normal record properties. If the
  // property is not defined on the object, then the record will check the
  // attributes hash instead.
  //
  // You can bulk update attributes from the server using the
  // updateAttributes() method.
  
  /**
Gets an attribute, converting it to the proper format.
@param {string} key the attribute you want to read
@returns {value} the value of the key, or null if it doesn't exist
**/
  readAttribute: function(key) {
    if (!this._cachedAttributes) this._cachedAttributes = {} ;
    var ret = this._cachedAttributes[key] ;
    if (ret === undefined) {
      var attr = this._attributes ;
      ret = (attr) ? attr[key] : undefined ;
      ret = ret || this[key] ; // also check properties...
      if (ret !== undefined) {
        var recordType = this._getRecordType(key+'Type') ;
        ret = this._propertyFromAttribute(ret, recordType) ;
      }
      this._cachedAttributes[key] = ret ;
    }
    return (ret === undefined) ? null : ret;
  },
 
  /**
Updates the attribute, converting it back to the property format.
@param {String} key the attribute you want to read
@param {Object} value the attribute you want to read
@returns {Object} the value of the key, or null if it doesn't exist
**/
  writeAttribute: function(key, value) {
    var recordType = this._getRecordType(key+'Type') ;
    var ret = this._attributeFromProperty(value, recordType) ;
    if (!this._attributes) this._attributes = {} ;
    this._attributes[key] = ret ;
    if (this._cachedAttributes) delete this._cachedAttributes[key]; // clear cache.
    this.recordDidChange() ;
    return value ;
  },
  
  /**
You can invoke this method anytime you need to make the record as dirty
and needing a commit to the server.
*/
  recordDidChange: function() {
    this.incrementProperty('changeCount') ;
  },
  
  /**
This will take the incoming set of attributes and update internal set.
Note that if the attributes have never been set, then the object you pass
in may become the new set of attribute. This assumes the attrs you pass
in will not be modified later. This method also assumes it is coming from
the server, so the change count will be reset.
@param {Object} newAttrs the new attributes for the object
@param {Boolean} replace should the overwrite the in-place attributes, or replace them entirely
@returns {Boolean} isLoaded is the object loaded?
**/
  updateAttributes: function(newAttrs, replace, isLoaded) {
    var changed = false ;
    if (this._attributes && (replace !== true)) {
      for(var key in newAttrs) {
        if (!newAttrs.hasOwnProperty(key)) continue ;
        if (!changed) changed = (this._attributes[key] != newAttrs[key]) ;
        this._attributes[key] = newAttrs[key] ;
      }
    } else {
      this._attributes = newAttrs ;
      changed = true ;
    }
    
    this._cachedAttributes = {} ; // reset cache.
    
    if (changed) {
      this.beginPropertyChanges() ;
      this.set('changeCount',0) ;
      this.set('isLoaded',isLoaded) ;
      this.allPropertiesDidChange() ;
      this.endPropertyChanges() ;
 
      if (SC.Store) SC.Store.recordDidChange(this) ;
    }
  },
  
  /**
This will return the current set of attributes as a hash you can send back to the server.
@returns {Object} the current attributes of the receiver
**/
  attributes: function() {
    return Object.clone(this._attributes) ;
  }.property(),
  
  /**
If you try to get/set a property not defined by the record, then this
method will be called. It will try to get the value from the set of
attributes.
@param {String} key the attribute being get/set
@param {Object} value the value to set the key to, if present
@returns {Object} the value
**/
  unknownProperty: function( key, value )
  {
    if (value !== undefined) {
      
      // if we're modifying the PKEY, then SC.Store needs to relocate where
      // this record is cached. store the old key, update the value, then let
      // the store do the housekeeping...
      var primaryKeyName = this.get('primaryKey');
      if (key == primaryKeyName)
      {
        var oldPrimaryKey = this.get(key);
        var newPrimaryKey = value;
      }
      
      this.writeAttribute(key,value);
      
      // no need to relocate if there wasn't an old key...
      if ((key == primaryKeyName) && oldPrimaryKey) SC.Store.relocateRecord( oldPrimaryKey, newPrimaryKey, this );
      
    } else {
      value = this.readAttribute(key);
    }
    return value;
  },
  
  _attributeFromProperty: function(value,recordType) {
    if (value && value instanceof Array) {
      var that = this;
      return value.map(function(v) {
        return that._attributeFromProperty(v,recordType);
      }) ;
    } else {
      var typeConverter = this._pickTypeConverter(recordType) ;
      if (typeConverter) return typeConverter(value,'out') ;
      if (recordType) {
        return (value) ? value.get(recordType.primaryKey()) : null ;
      } else return value ;
    }
  },
  
  _propertyFromAttribute: function(value,recordType) {
    if (value && value instanceof Array) {
      var max = value.get('length') ;
      var ret = new Array(max) ;
      for(var idx=0;idx<max;idx++) {
        var v = value.objectAt(idx) ;
        ret[idx] = this._propertyFromAttribute(v, recordType) ;
      }
      ret.ownerRecord = this ;
      return ret ;
      
    } else {
      var typeConverter = this._pickTypeConverter(recordType) ;
      if (typeConverter) return typeConverter(value,'in') ;
      if (recordType) {
        if (!value) return null ;
        return SC.Store.getRecordFor(value,recordType) ;
      } else return value ;
    }
  },
  
  _getRecordType: function(recordTypeKey) {
    var type = this[recordTypeKey] ;
    if (type && (typeof(type) == "string")) {
      type = eval(type) ; // look up type.
      if (type) this[recordTypeKey] = type ;
    }
    return type ;
  },
  
  // ...............................
  // SORTING AND COMPARING RECORDS
  //
  
  valueForSortKey: function(key) { return this.get(key); },
 
  /**
Compares the receiver to the passed object, using the array of keys to
determine the order. You can use this method as part of a call to sort()
on an array.
@param object {SC.Record} the other record
@param orderBy {Array} array of one or more keys. Optional.
@returns {Number} -1, 0, 1
*/
  compareTo: function(object, orderBy) {
    if (!orderBy) orderBy = [this.get('primaryKey')] ;
    var ret = SC.Record.SORT_SAME ; var loc ;
    for(loc=0; (ret == SC.Record.SORT_SAME && loc<orderBy.length); loc++) {
      var key = orderBy[loc] ;
      
      // determine order
      var asc = true ;
      if (key.match(/ DESC$/)) {
        asc = false; key = key.slice(0,-5);
      } else if (key.match(/ ASC$/)) {
        asc = true; key = key.slice(0,-4);
      }
 
      // if key contains a .dot then we need to get the value for the key.
      var keys = key.split('.') ;
      key = keys.shift() ;
      
      // get values for key.
      var a = this.valueForSortKey(key) ;
      var b = object.valueForSortKey(key) ;
      
      // convert the values to comparable values.
      a = this._comparableValueFor(a,keys) ;
      b = this._comparableValueFor(b,keys) ;
      
      // compare values
      if (asc) {
        ret = (a<b) ? SC.Record.SORT_BEFORE : ((a>b) ? SC.Record.SORT_AFTER : SC.Record.SORT_SAME) ;
      } else {
        ret = (a>b) ? SC.Record.SORT_BEFORE : ((a<b) ? SC.Record.SORT_AFTER : SC.Record.SORT_SAME) ;
      }
    }
    return ret ;
  },
  
  _comparableValueFor: function(value, keys) {
    if (keys && keys.length > 0) {
      var key ; var loc = 0 ;
      while(value && (loc < keys.length)) {
        key = keys[loc];
        value = (value.get) ? value.get(key) : value[key] ;
        loc++ ;
      }
    
    // handle records.
    } else value = SC.guidFor(value) ;
    return value ;
  },
  
  /**
Used to match records to a set of conditions. By default, this will
call matchCondition on each condition.
@param conditions {Hash} hash of conditions
@returns {Boolean} true if the receiver matches the hash of conditions.
*/
  matchConditions: function(conditions) {
    for(var key in conditions) {
      var value = conditions[key] ;
      if (value instanceof Array) {
        var loc = value.length ; var isMatch = false ;
        while(--loc >= 0) {
          if (this.matchCondition(key,value[loc])) isMatch = true ;
        }
        if (!isMatch) return false ;
      } else if (!this.matchCondition(key,value)) return false ;
    }
    return true ;
  },
 
  /**
Returns true if the value of key matches the passed value. This is used
by matchConditions().
@param key {String} the key name
@param value {Object} the value to match agains
@returns {Boolean} true if matched
*/
  matchCondition: function(key, value) {
    var recValue = this.get(key) ;
    var isMatch ;
    var loc ;
 
    // The passed in value appears to be another record instance.
    // just check for equality with the record as an optimization.
    if (value && value.primaryKey) {
      if (SC.$type(recValue) === SC.T_ARRAY) {
        loc = recValue.length ;
        while(--loc >= 0) {
          if (recValue === value) return true;
        }
      } else return recValue === value ;
 
    // Otherwise, do a more in-depth compare
    } else {
      if (SC.$type(recValue) === SC.T_ARRAY) {
        loc = recValue.length ;
        while(--loc >= 0) {
          if (this._matchValue(recValue[loc],value)) return true;
        }
      } else return this._matchValue(recValue,value) ;
    }
    return false ;
  },
 
  _matchValue: function(recValue,value) {
    // if we get here with recValue as a record, we must compare by guid, so grab it
    if (recValue && recValue.primaryKey) recValue = recValue.get(recValue.get('primaryKey')) ;
    var stringify = (value instanceof RegExp);
    if (stringify) {
      if (recValue == null) return false ;
      return recValue.toString().match(value) ;
    } else {
       return recValue==value ;
    }
  },
  
  // ...............................
  // PRIVATE
  //
  
  toString: function() {
    var that = this ;
    var ret = this.get('properties').map(function(key) {
      var value = that.get(key) ;
      if (typeof(value) == "string") value = '"' + value + '"' ;
      if (value === undefined) value = "(undefined)" ;
      if (value === null) value = "(null)" ;
      return [key,value].join('=') ;
    }) ;
    return this.constructor.toString() + '({ ' + ret.join(', ') + ' })' ;
  },
  
  propertyObserver: function(observing,target,key,value) {
    //if ((target == this) && this.properties.include(key)) this.incrementProperty('changeCount') ;
  },
  
  concatenatedProperties: ['properties'],
 
  /**
This method should be used by the server to push updated data into a
record. The data should be a hash with strings and arrays. This will
use any types you define to convert the values into their correct type.
Note that references to external objects should be a string with the
primaryKey value of the record.
@param data {Hash} the data hash
@param isLoaded {Boolean} true if the hash contains a full set of data for the record vs just a summary.
@returns {void}
*/
  updateProperties: function(data,isLoaded) {
    var rec = this ;
    
    // for each property, if there is a value in the passed data, convert it to
    // the configured type.
    this.beginPropertyChanges() ;
    if (isLoaded) this.set('isLoaded',true) ;
    try {
      var loc = this.properties.length ;
      while(--loc >= 0) {
        var prop = this.properties[loc] ;
        var newValue = data[prop] ;
 
        //if (prop == 'tags') debugger ;
        
        // handle null values
        if (newValue === null) {
          if (rec.get(prop) != null) rec.set(prop,null) ;
          
        // handle defined, non-null values
        } else if (newValue !== undefined) {
          
          var oldValue = rec.get(prop) ;
          
          // get type information
          var recordType = rec.get(prop + 'Type') ;
          var typeConverter = this._pickTypeConverter(recordType) ;
          if (typeConverter) recordType = null ;
 
          // if array, convert each object.
          var isSame ; var rec = this ;
          if (newValue instanceof Array) {
            newValue = newValue.map(function(nv) {
              return rec._convertValueIn(nv,typeConverter,recordType) ;
            }) ;
            isSame = newValue.isEqual(oldValue) ;
          } else {
            newValue = this._convertValueIn(newValue,typeConverter,recordType);
            isSame = newValue == oldValue ;
          }
          
          // set value
          if (!isSame) this.set(prop,newValue) ;
           
        }
      }
    }
    
    catch(e) {
      console.log(SC.guidFor(this) + ': Exception raised on UPDATE: ' + e) ;
    }
 
    this.endPropertyChanges() ;
    this.set('changeCount',0) ;
  },
  
  // this is used for the update. It should return a hash with current state
  // of the record. This uses the types to automatically marshall properties.
  getPropertyData: function() {
    var ret = {} ;
    var properties = this.get('properties') || []; var loc = properties.length;
    while(--loc >= 0) {
      var key = properties[loc] ;
      var value = this.get(key) ;
      var recordType = this[key + 'Type'] ;
      var typeConverter = this._pickTypeConverter(recordType) ;
      if (typeConverter) recordType = null ;
      
      // if there is a type, use that to make the conversion.
      if (value instanceof Array) {
        var ary = [] ;
        for(var vloc=0;vloc<value.length;vloc++) {
          var v = value[vloc] ;
          ary.push(this._convertValueOut(v,typeConverter,recordType));
        }
        value = ary ;
      } else value = this._convertValueOut(value,typeConverter,recordType);
      
      // set key
      ret[key] = value ;
    }
    return ret ;
  },
  
  _pickTypeConverter: function(recordType) {
    var typeConverter = null ;
    if (recordType && recordType.isTypeConverter) {
      typeConverter = recordType; recordType = null ;
    } else if(recordType) switch(recordType) {
      case Date:
        typeConverter = SC.Record.Date; recordType = null ;
        break ;
      case Number:
        typeConverter = SC.Record.Number; recordType = null;
        break;
      case String:
        typeConverter = null; recordType = null ;
        break ;
    }
    return typeConverter;
  },
  
  _convertValueOut: function(value,typeConverter,recordType) {
    if (typeConverter) return typeConverter(value,'out') ;
    if (recordType) {
      return (value) ? value.get(recordType.primaryKey()) : null ;
    } else return value ;
  },
  
  _convertValueIn: function(value,typeConverter,recordType) {
    if (typeConverter) return typeConverter(value,'in') ;
    if (recordType) {
      return SC.Store.getRecordFor(value,recordType) ;
    } else return value ;
  },
  
  // used by the store
  _storeKey: function() {
    return this.constructor._storeKey();
  }
  
     
}) ;
 
// Class Methods
SC.Record.mixin(
/** @static SC.Record */ {
 
  // Constants for sorting
  SORT_BEFORE: -1, SORT_AFTER: 1, SORT_SAME: 0,
 
  /**
Used to find the first object matching the specified conditions. You can
pass in either a simple guid or one or more hashes of conditions.
*/
  find: function(guid) {
    var args ;
    if (typeof(guid) == 'object') {
      args = SC.$A(arguments) ;
      args.push(this) ;
      var ret = SC.Store.findRecords.apply(SC.Store,args) ;
      return (ret && ret.length > 0) ? ret[0] : null ;
    } else return SC.Store._getRecordFor(guid,this) ;
  },
  
  findOrCreate: function(guid) {
    var ret = this.find(guid) ;
    if (!ret) {
      var opts = (typeof(guid) == "object") ? guid : { guid: guid } ;
      ret = this.create(opts) ;
      SC.Store.addRecord(ret) ;
    }
    return ret ;
  },
  
  // Same as find except returns all records matching the passed conditions.
  findAll: function(filter) {
    if (!filter) filter = {} ;
    args = SC.$A(arguments) ; args.push(this) ; // add type
    return SC.Store.findRecords.apply(SC.Store,args) ;
  },
  
  // Returns a collection with any passed settings and the receiver as a
  // record type.
  collection: function(opts) {
    if (!opts) opts = {} ;
    opts.recordType = this;
    return SC.Collection.create(opts) ;
  },
  
  /// POSSIBLY REMOVE?
  
  // defines coreRecordType as the first level of extension from SC.Record.
  // e.g. for SC.Record > Contact > Person, the core record type is Contact.
  extend: function() {
    var ret = SC.Object.extend.apply(this,arguments) ;
    if (ret.coreRecordType == null) ret.coreRecordType = ret ;
    return ret ;
  },
 
  // used by the store
  _storeKey: function() {
    return (this.coreRecordType) ? SC.guidFor(this.coreRecordType) : SC.guidFor(this) ;
  },
  
  primaryKey: function() { return this.prototype.primaryKey; },
 
  // this is set by extend to point to the core record type used to store
  // the record in the pool. The coreRecordType is always the first record
  // type created.
  coreRecordType: null,
 
  resourceURL: function() { return this.prototype.resourceURL; },
  
  // This will add a property function for your record with a collection
  // of records with the given type that belong to your record.
  hasMany: function(recordTypeString,conditionKey,opts) {
    opts = (opts === undefined) ? {} : Object.clone(opts) ;
    var conditions = opts.conditions || {} ;
    opts.conditions = conditions ;
 
    var privateKey = '_' + conditionKey + SC.generateGuid() ;
    return function() {
      if (!this[privateKey]) {
        var recordType = eval(recordTypeString);
        conditions[conditionKey] = this ;
        this[privateKey] = recordType.collection(opts) ;
        this[privateKey].refresh() ; // get the initial data set.
      }
      return this[privateKey] ;
    }.property();
  },
  
  // This will create a new record with the type. Include the data and an
  // optional data source.
  newRecord: function(attrs,dataSource) {
    if (!dataSource) dataSource = SC.Store ;
    var rec = this.create({ dataSource: dataSource }) ;
    rec.beginPropertyChanges();
    rec.set('newRecord',true);
    for(var key in attrs) {
      if (attrs.hasOwnProperty(key)) rec.set(key,attrs[key]) ;
    }
    rec.endPropertyChanges() ;
    SC.Store.addRecord(rec) ;
    return rec;
  }
    
}) ;
 
SC.Record.newObject = SC.Record.newRecord; // clone method
 
// Built in Type Converters. You can also use an SC.Record.
SC.Record.Date = function(value,direction) {
  if (direction == 'out') {
    if (value instanceof Date) value = value.utcFormat() ;
    
  } else if (typeof(value) == "string") {
    // try to parse date. trim any decimal numbers at end since Rails sends
    // this sometimes.
    var ret = Date.parseDate(value.replace(/\.\d+$/,'')) ;
    if (!ret) ret = new Date(value);
    if (ret) value = ret ;
  }
  return value ;
}.typeConverter() ;
 
SC.Record.Number = function(value,direction) {
  if (direction == 'out') {
    if (typeof(value) == "number") value = value.toString() ;
  
  } else if (typeof(value) == "string") {
    var ret = (value.match('.')) ? parseFloat(value) : parseInt(value,0) ;
    if (ret) value = ret ;
  }
  return value ;
}.typeConverter() ;
 
SC.Record.Flag = function(value, direction) {
  if (direction == 'out') {
    return value = (value) ? 't' : 'f' ;
  } else if (typeof(value) == "string") {
    return !('false0'.match(value.toLowerCase())) ;
  } else return (value) ? true : false ;
}.typeConverter() ;
 
SC.Record.Bool = SC.Record.Flag ;