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