1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 sc_require('models/record');
  9 sc_require('models/record_attribute');
 10 sc_require('system/many_array');
 11 
 12 /** @class
 13 
 14   ManyAttribute is a subclass of `RecordAttribute` and handles to-many
 15   relationships.
 16 
 17   Relationships in the client are meant to mirror the relationships that
 18   the real data has in the remote data store on the server. For example,
 19   if a `Parent` record on the server has an array of `Child` ids, then it is
 20   appropriate for the `MyApp.Parent` model in the SproutCore app to have a `toMany`
 21   relationship to the `MyApp.Child` in the app. In this way, changes to the
 22   relationship in the client will best match how the data should be committed
 23   to the server.
 24 
 25   There are many ways you can configure a `ManyAttribute`:
 26 
 27       contacts: SC.Record.toMany('MyApp.Contact', {
 28         inverse: 'group', // set the key used to represent the inverse
 29         isMaster: YES|NO, // indicate whether changing this should dirty
 30         transform: function(), // transforms value <=> storeKey,
 31         isEditable: YES|NO, make editable or not,
 32         through: 'taggings' // set a relationship this goes through
 33       });
 34 
 35   Note: When setting ( `.set()` ) the value of a `toMany` attribute, make sure
 36   to pass in an array of `SC.Record` objects.
 37 
 38   ## Using new Records in Relationships
 39 
 40   Because relationships are based on `id`, new records created in the client
 41   (that don't have an `id`) are typically not able to be assigned to a
 42   relationship until after they have been committed to the server. However,
 43   because it's unwieldy to manually update relationships after the real `id` is
 44   known, `SC.ManyAttribute` through `SC.ManyArray`, allows new records to be added
 45   that don't yet have an `id`.
 46 
 47   As long as the `supportNewRecords` property is true, adding records without an
 48   `id `to the relationship will assign unique temporary ids to the new records.
 49 
 50   *Note:* You must update the relationship after the new records are successfully
 51   committed and have real ids. This is done by calling `updateNewRecordId()`
 52   on the many array. In the future this should be automatic.
 53 
 54   @extends SC.RecordAttribute
 55   @since SproutCore 1.0
 56 */
 57 SC.ManyAttribute = SC.RecordAttribute.extend(
 58   /** @scope SC.ManyAttribute.prototype */ {
 59 
 60   /**
 61     Set the foreign key on content objects that represent the inversion of
 62     this relationship. The inverse property should be a `toOne()` or
 63     `toMany()` relationship as well. Modifying this many array will modify
 64     the `inverse` property as well.
 65 
 66     @type String
 67   */
 68   inverse: null,
 69 
 70   /**
 71     If `YES` then modifying this relationships will mark the owner record
 72     dirty. If set to `NO`, then modifying this relationship will not alter
 73     this record.  You should use this property only if you have an inverse
 74     property also set. Only one of the inverse relationships should be marked
 75     as master so you can control which record should be committed.
 76 
 77     @type Boolean
 78   */
 79   isMaster: YES,
 80 
 81   /**
 82     If set and you have an inverse relationship, will be used to determine the
 83     order of an object when it is added to an array. You can pass a function
 84     or an array of property keys.
 85 
 86     @property {Function|Array}
 87   */
 88   orderBy: null,
 89 
 90   /**
 91     Determines whether the new record support of `SC.ManyArray` should be
 92     enabled or not.
 93 
 94     Normally, all records in the relationship should already have been previously
 95     committed to a remote data store and have an actual `id`. However, with
 96     `supportNewRecords` set to true, adding records without an `id `to the
 97     relationship will assign unique temporary ids to the new records.
 98 
 99     *Note:* You must update the relationship after the new records are successfully
100     committed and have real ids. This is done by calling `updateNewRecordId()`
101     on the many array. In the future this should be automatic.
102 
103     If you wish to turn this off, SC.ManyArray will throw an exception if you
104     add a record without an id to the relationship. If you use temporary `id`s
105     for new record, you will need to manually update the relationship, but
106     run the risk of committing inverse records with temporary `id`s in their
107     datahashes.
108 
109     @type Boolean
110     @default true
111     @since SproutCore 1.11.0
112   */
113   supportNewRecords: true,
114 
115   // ..........................................................
116   // LOW-LEVEL METHODS
117   //
118 
119   /**  @private - adapted for to many relationship */
120   toType: function(record, key, value) {
121     var type      = this.get('typeClass'),
122         supportNewRecords = this.get('supportNewRecords'),
123         attrKey   = this.get('key') || key,
124         arrayKey  = SC.keyFor('__manyArray__', SC.guidFor(this)),
125         ret       = record[arrayKey],
126         rel;
127 
128     // lazily create a ManyArray one time.  after that always return the
129     // same object.
130     if (!ret) {
131       ret = SC.ManyArray.create({
132         recordType:    type,
133         record:        record,
134         propertyName:  attrKey,
135         manyAttribute: this,
136         supportNewRecords: supportNewRecords
137       });
138 
139       record[arrayKey] = ret ; // save on record
140       rel = record.get('relationships');
141       if (!rel) record.set('relationships', rel = []);
142       rel.push(ret); // make sure we get notified of changes...
143 
144     }
145 
146     return ret;
147   },
148 
149   /** @private - adapted for to many relationship */
150   fromType: function(record, key, value) {
151     var ret = [];
152 
153     if(!SC.isArray(value)) throw new Error("Expects toMany attribute to be an array");
154 
155     var len = value.get('length');
156     for(var i=0;i<len;i++) {
157       ret[i] = value.objectAt(i).get('id');
158     }
159 
160     return ret;
161   },
162 
163   /**
164     Called by an inverse relationship whenever the receiver is no longer part
165     of the relationship.  If this matches the inverse setting of the attribute
166     then it will update itself accordingly.
167 
168     You should never call this directly.
169 
170     @param {SC.Record} the record owning this attribute
171     @param {String} key the key for this attribute
172     @param {SC.Record} inverseRecord record that was removed from inverse
173     @param {String} key key on inverse that was modified
174     @returns {void}
175   */
176   inverseDidRemoveRecord: function(record, key, inverseRecord, inverseKey) {
177     var manyArray = record.get(key);
178     if (manyArray) {
179       manyArray.removeInverseRecord(inverseRecord);
180     }
181   },
182 
183   /**
184     Called by an inverse relationship whenever the receiver is added to the
185     inverse relationship.  This will set the value of this inverse record to
186     the new record.
187 
188     You should never call this directly.
189 
190     @param {SC.Record} the record owning this attribute
191     @param {String} key the key for this attribute
192     @param {SC.Record} inverseRecord record that was added to inverse
193     @param {String} key key on inverse that was modified
194     @returns {void}
195   */
196   inverseDidAddRecord: function(record, key, inverseRecord, inverseKey) {
197     var manyArray = record.get(key);
198     if (manyArray) {
199       manyArray.addInverseRecord(inverseRecord);
200     }
201   }
202 
203 });
204