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 /** @class 9 Provides support for having relationships propagate by 10 data provided by the data source. 11 12 This means the following interaction is now valid: 13 14 App = { store: SC.Store.create(SC.RelationshipSupport) }; 15 16 App.Person = SC.Record.extend({ 17 primaryKey: 'name', 18 19 name: SC.Record.attr(String), 20 21 power: SC.Record.toOne('App.Power', { 22 isMaster: NO, 23 inverse: 'person' 24 }) 25 }); 26 27 App.Power = SC.Record.extend({ 28 person: SC.Record.toOne('App.Person', { 29 isMaster: YES, 30 inverse: 'power' 31 }) 32 }); 33 34 var zan = App.store.createRecord(App.Person, { name: 'Zan' }), 35 jayna = App.store.createRecord(App.Person, { name: 'Janya' }); 36 37 // Wondertwins activate! 38 var glacier = App.store.loadRecords(App.Power, [{ 39 guid: 'petunia', // Shape of... 40 person: 'Jayna' 41 }, { 42 guid: 'drizzle', // Form of... 43 person: 'Zan' 44 }]); 45 46 47 Leveraging this mixin requires your records to be unambiguously 48 defined. Note that this mixin does not take into account record 49 relationship created / destroyed on `dataSourceDidComplete`, 50 `writeAttribute`, etc. The only support here is for `pushRetrieve`, 51 `pushDestroy`, and `loadRecords` (under the hood, `loadRecord(s)` uses 52 `pushRetrieve`). 53 54 This mixin also supports lazily creating records when a related 55 record is pushed in from the store (but it doesn't exist). 56 57 This means that the previous example could have been simplified 58 to this: 59 60 App.Power = SC.Record.extend({ 61 person: SC.Record.toOne('App.Person', { 62 isMaster: YES, 63 lazilyInstantiate: YES, 64 inverse: 'power' 65 }) 66 }); 67 68 // Wondertwins activate! 69 var glacier = App.store.loadRecords(App.Power, [{ 70 guid: 'petunia', // Shape of... 71 person: 'Jayna' 72 }, { 73 guid: 'drizzle', // Form of... 74 person: 'Zan' 75 }]); 76 77 78 When the `loadRecords` call is done, there will be four unmaterialized 79 records in the store, giving the exact same result as the former 80 example. 81 82 As a side note, this mixin was developed primarily for use 83 in a real-time backend that provides data to SproutCore 84 as soon as it gets it (no transactions). This means streaming 85 APIs / protocols like the Twitter streaming API or XMPP (an IM 86 protocol) can be codified easier. 87 88 @since SproutCore 1.6 89 */ 90 SC.RelationshipSupport = { 91 92 /** @private 93 Relinquish many records. 94 95 This happens when a master record (`isMaster` = `YES`) removes a reference 96 to related records, either through `pushRetrieve` or `pushDestroy`. 97 */ 98 _srs_inverseDidRelinquishRelationships: function (recordType, ids, attr, inverseId) { 99 ids.forEach(function (id) { 100 this._srs_inverseDidRelinquishRelationship(recordType, id, attr, inverseId); 101 }, this); 102 }, 103 104 /** @private 105 Relinquish the record, removing the reference of the record being 106 destroyed on any related records. 107 */ 108 _srs_inverseDidRelinquishRelationship: function (recordType, id, toAttr, relativeID) { 109 var storeKey = recordType.storeKeyFor(id), 110 dataHash = this.readDataHash(storeKey), 111 key = toAttr.inverse, 112 proto = recordType.prototype; 113 114 if (!dataHash || !key) return; 115 116 if (SC.instanceOf(proto[key], SC.SingleAttribute)) { 117 delete dataHash[key]; 118 } else if (SC.instanceOf(proto[key], SC.ManyAttribute) && 119 SC.typeOf(dataHash[key]) === SC.T_ARRAY) { 120 121 dataHash[key].removeObject(relativeID); 122 123 // If there is a materialized record with a ManyArray we have to clear the 124 // internal cache of the ManyArray. Otherwise calling `objectAt` on the 125 // Many Array will retrieve the cached record. 126 var record = this.records[storeKey]; 127 if (record) { 128 record.get(key)._records = null; 129 } 130 } 131 132 this.pushRetrieve(recordType, id, dataHash, undefined, true); 133 }, 134 135 /** @private 136 Add a relationship to many inverse records. 137 138 This happens when a master record (`isMaster` = `YES`) adds a reference 139 to another record on a `pushRetrieve`. 140 */ 141 _srs_inverseDidAddRelationships: function (recordType, ids, attr, inverseId) { 142 ids.forEach(function (id) { 143 this._srs_inverseDidAddRelationship(recordType, id, attr, inverseId); 144 }, this); 145 }, 146 147 148 /** @private 149 Add a relationship to an inverse record. 150 151 If the flag lazilyInstantiate is set to YES, then the inverse record will be 152 created lazily. 153 154 @param {SC.Record} recordType The inverse record type. 155 @param {String} id The id of the recordType to add. 156 @param {SC.RecordAttribute} toAttr The record attribute that represents 157 the relationship being created. 158 @param {String} relativeID The ID of the model that needs to have it's 159 relationship updated. 160 */ 161 _srs_inverseDidAddRelationship: function (recordType, id, toAttr, relativeID) { 162 var storeKey = recordType.storeKeyFor(id), 163 dataHash = this.readDataHash(storeKey), 164 status = this.peekStatus(storeKey), 165 proto = recordType.prototype, 166 key = toAttr.inverse, 167 hashKey = proto[key], 168 primaryAttr = proto[proto.primaryKey], 169 shouldRecurse = false; 170 171 // in case the SC.RecordAttribute defines a `key` field, we need to use that 172 hashKey = (hashKey && hashKey.get && hashKey.get('key') || hashKey.key) || key; 173 174 if ((status === SC.Record.EMPTY) && 175 (SC.typeOf(toAttr.lazilyInstantiate) === SC.T_FUNCTION && toAttr.lazilyInstantiate() || 176 SC.typeOf(toAttr.lazilyInstantiate) !== SC.T_FUNCTION && toAttr.lazilyInstantiate)) { 177 178 if (!SC.none(primaryAttr) && primaryAttr.typeClass && 179 SC.typeOf(primaryAttr.typeClass()) === SC.T_CLASS) { 180 181 // Recurse to create the record that this primaryKey points to iff it 182 // also should be created if the record is empty. 183 // Identifies chained relationships where the object up the chain 184 // doesn't exist yet. 185 186 // TODO: this can lead to an infinite recursion if the relationship 187 // graph is cyclic 188 shouldRecurse = true; 189 } 190 dataHash = {}; 191 dataHash[proto.primaryKey] = id; 192 } 193 194 if (!dataHash || !key) return; 195 196 if (SC.instanceOf(proto[key], SC.SingleAttribute)) { 197 dataHash[hashKey] = relativeID; 198 } else if (SC.instanceOf(proto[key], SC.ManyAttribute)) { 199 dataHash[hashKey] = dataHash[hashKey] || []; 200 201 if (dataHash[hashKey].indexOf(relativeID) < 0) { 202 dataHash[hashKey].push(relativeID); 203 } 204 } 205 206 this.pushRetrieve(recordType, id, dataHash, undefined, !shouldRecurse); 207 }, 208 209 // .......................................................... 210 // ASYNCHRONOUS RECORD RELATIONSHIPS 211 // 212 213 /** @private 214 Iterates over keys on the recordType prototype, looking for RecordAttributes 215 that have relationships (toOne or toMany). 216 217 @param {SC.Record} recordType The record type to do introspection on to see 218 if it has any RecordAttributes that have relationships to other records. 219 @param {String} id The id of the record being pushed in. 220 @param {Number} storeKey The storeKey 221 */ 222 _srs_pushIterator: function (recordType, id, storeKey, lambda) { 223 var proto = recordType.prototype, 224 attr, currentHash, key, inverseType; 225 226 if (typeof storeKey === "undefined") { 227 storeKey = recordType.storeKeyFor(id); 228 } 229 230 currentHash = this.readDataHash(storeKey) || {}; 231 232 for (key in proto) { 233 attr = proto[key]; 234 if (attr && attr.typeClass && attr.inverse && attr.isMaster) { 235 inverseType = attr.typeClass(); 236 237 if (SC.typeOf(inverseType) !== SC.T_CLASS) continue; 238 239 lambda.apply(this, [inverseType, currentHash, attr, 240 attr.get && attr.get('key') || key]); 241 } 242 } 243 }, 244 245 246 /** 247 Disassociate records that are related to the one being destroyed iff this 248 record has `isMaster` set to `YES`. 249 */ 250 pushDestroy: function (original, recordType, id, storeKey) { 251 var existingIDs; 252 253 this._srs_pushIterator(recordType, id, storeKey, 254 function (inverseType, currentHash, toAttr, keyValue) { 255 // update old relationships 256 existingIDs = [currentHash[keyValue] || null].flatten().compact().uniq(); 257 this._srs_inverseDidRelinquishRelationships(inverseType, existingIDs, toAttr, id); 258 }); 259 260 return original(recordType, id, storeKey); 261 }.enhance(), 262 263 /** 264 Associate records that are added via a pushRetrieve, and update subsequent 265 relationships to ensure that the master-slave relationship is kept intact. 266 267 For use cases, see the test for pushRelationships. 268 269 The `ignore` argument is only set to true when adding the inverse 270 relationship (to prevent recursion). 271 */ 272 pushRetrieve: function (original, recordType, id, dataHash, storeKey, ignore) { 273 // avoid infinite recursions when additional changes are propogated 274 // from `_srs_inverseDidAddRelationship` 275 if (!ignore) { 276 var existingIDs, inverseIDs; 277 278 this._srs_pushIterator(recordType, id, storeKey, 279 /** @ignore 280 @param {SC.Record} inverseType - in a Master-Slave 281 relationship when pushing master, Slave is the inverse type 282 @param {Object} currentHash - the hash in the data store (data to be replaced by `dataHash`) 283 @param {SC.RecordAttribute} toAttr - key in `recordType.prototype` that names isMaster and has an inverse 284 @param {String} keyValue - the property name in the datahash that defines this foreign key relationship 285 */ 286 function (inverseType, currentHash, toAttr, keyValue) { 287 288 // Find the new relations. 289 inverseIDs = [dataHash[keyValue] || null].flatten().compact().uniq(); 290 291 // Find the old relations. 292 existingIDs = [currentHash[keyValue] || null].flatten().compact().uniq(); 293 existingIDs = existingIDs.filter( 294 function (el) { 295 return inverseIDs.indexOf(el) === -1; 296 }); 297 298 // Update the relationships. 299 this._srs_inverseDidRelinquishRelationships(inverseType, existingIDs, toAttr, id); 300 this._srs_inverseDidAddRelationships(inverseType, inverseIDs, toAttr, id); 301 }); 302 } 303 304 storeKey = storeKey || recordType.storeKeyFor(id); 305 306 return original(recordType, id, dataHash, storeKey); 307 }.enhance() 308 }; 309