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