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('data_sources/data_source');
  9 sc_require('models/record');
 10 
 11 /** @class
 12 
 13   TODO: Describe Class
 14 
 15   @extends SC.DataSource
 16   @since SproutCore 1.0
 17 */
 18 SC.FixturesDataSource = SC.DataSource.extend(
 19   /** @scope SC.FixturesDataSource.prototype */ {
 20 
 21   /**
 22     If YES then the data source will asynchronously respond to data requests
 23     from the server.  If you plan to replace the fixture data source with a
 24     data source that talks to a real remote server (using Ajax for example),
 25     you should leave this property set to YES so that Fixtures source will
 26     more accurately simulate your remote data source.
 27 
 28     If you plan to replace this data source with something that works with
 29     local storage, for example, then you should set this property to NO to
 30     accurately simulate the behavior of your actual data source.
 31 
 32     @type Boolean
 33   */
 34   simulateRemoteResponse: NO,
 35 
 36   /**
 37     If you set simulateRemoteResponse to YES, then the fixtures source will
 38     assume a response latency from your server equal to the msec specified
 39     here.  You should tune this to simulate latency based on the expected
 40     performance of your server network.  Here are some good guidelines:
 41 
 42      - 500: Simulates a basic server written in PHP, Ruby, or Python (not twisted) without a CDN in front for caching.
 43      - 250: (Default) simulates the average latency needed to go back to your origin server from anywhere in the world.  assumes your servers itself will respond to requests < 50 msec
 44      - 100: simulates the latency to a "nearby" server (i.e. same part of the world).  Suitable for simulating locally hosted servers or servers with multiple data centers around the world.
 45      - 50: simulates the latency to an edge cache node when using a CDN.  Life is really good if you can afford this kind of setup.
 46 
 47     @type Number
 48   */
 49   latency: 50,
 50 
 51   // ..........................................................
 52   // CANCELLING
 53   //
 54 
 55   /** @private */
 56   cancel: function(store, storeKeys) {
 57     return NO;
 58   },
 59 
 60 
 61   // ..........................................................
 62   // FETCHING
 63   //
 64 
 65   /** @private */
 66   fetch: function(store, query) {
 67 
 68     // can only handle local queries out of the box
 69     if (query.get('location') !== SC.Query.LOCAL) {
 70       SC.throw('SC.Fixture data source can only fetch local queries');
 71     }
 72 
 73     if (!query.get('recordType') && !query.get('recordTypes')) {
 74       SC.throw('SC.Fixture data source can only fetch queries with one or more record types');
 75     }
 76 
 77     if (this.get('simulateRemoteResponse')) {
 78       this.invokeLater(this._fetch, this.get('latency'), store, query);
 79 
 80     } else this._fetch(store, query);
 81   },
 82 
 83   /** @private
 84     Actually performs the fetch.
 85   */
 86   _fetch: function(store, query) {
 87 
 88     // NOTE: Assumes recordType or recordTypes is defined.  checked in fetch()
 89     var recordType = query.get('recordType'),
 90         recordTypes = query.get('recordTypes') || [recordType];
 91 
 92     // load fixtures for each recordType
 93     recordTypes.forEach(function(recordType) {
 94       if (SC.typeOf(recordType) === SC.T_STRING) {
 95         recordType = SC.objectForPropertyPath(recordType);
 96       }
 97 
 98       if (recordType) this.loadFixturesFor(store, recordType);
 99     }, this);
100 
101     // notify that query has now loaded - puts it into a READY state
102     store.dataSourceDidFetchQuery(query);
103   },
104 
105   // ..........................................................
106   // RETRIEVING
107   //
108 
109   /** @private */
110   retrieveRecords: function(store, storeKeys) {
111     // first let's see if the fixture data source can handle any of the
112     // storeKeys
113     var latency = this.get('latency'),
114         ret     = this.hasFixturesFor(storeKeys) ;
115     if (!ret) return ret ;
116 
117     if (this.get('simulateRemoteResponse')) {
118       this.invokeLater(this._retrieveRecords, latency, store, storeKeys);
119     } else this._retrieveRecords(store, storeKeys);
120 
121     return ret ;
122   },
123 
124   _retrieveRecords: function(store, storeKeys) {
125 
126     storeKeys.forEach(function(storeKey) {
127       var ret        = [],
128           recordType = SC.Store.recordTypeFor(storeKey),
129           id         = store.idFor(storeKey),
130           hash       = this.fixtureForStoreKey(store, storeKey);
131       ret.push(storeKey);
132       store.dataSourceDidComplete(storeKey, hash, id);
133     }, this);
134   },
135 
136   // ..........................................................
137   // UPDATE
138   //
139 
140   /** @private */
141   updateRecords: function(store, storeKeys, params) {
142     // first let's see if the fixture data source can handle any of the
143     // storeKeys
144     var latency = this.get('latency'),
145         ret     = this.hasFixturesFor(storeKeys) ;
146     if (!ret) return ret ;
147 
148     if (this.get('simulateRemoteResponse')) {
149       this.invokeLater(this._updateRecords, latency, store, storeKeys);
150     } else this._updateRecords(store, storeKeys);
151 
152     return ret ;
153   },
154 
155   _updateRecords: function(store, storeKeys) {
156     storeKeys.forEach(function(storeKey) {
157       var hash = store.readDataHash(storeKey);
158       this.setFixtureForStoreKey(store, storeKey, hash);
159       store.dataSourceDidComplete(storeKey);
160     }, this);
161   },
162 
163 
164   // ..........................................................
165   // CREATE RECORDS
166   //
167 
168   /** @private */
169   createRecords: function(store, storeKeys, params) {
170     // first let's see if the fixture data source can handle any of the
171     // storeKeys
172     var latency = this.get('latency');
173 
174     if (this.get('simulateRemoteResponse')) {
175       this.invokeLater(this._createRecords, latency, store, storeKeys);
176     } else this._createRecords(store, storeKeys);
177 
178     return YES ;
179   },
180 
181   _createRecords: function(store, storeKeys) {
182     storeKeys.forEach(function(storeKey) {
183       var id         = store.idFor(storeKey),
184           recordType = store.recordTypeFor(storeKey),
185           dataHash   = store.readDataHash(storeKey),
186           fixtures   = this.fixturesFor(recordType);
187 
188       if (!id) id = this.generateIdFor(recordType, dataHash, store, storeKey);
189       this._invalidateCachesFor(recordType, storeKey, id);
190       fixtures[id] = dataHash;
191 
192       store.dataSourceDidComplete(storeKey, null, id);
193     }, this);
194   },
195 
196   // ..........................................................
197   // DESTROY RECORDS
198   //
199 
200   /** @private */
201   destroyRecords: function(store, storeKeys, params) {
202     // first let's see if the fixture data source can handle any of the
203     // storeKeys
204     var latency = this.get('latency'),
205         ret     = this.hasFixturesFor(storeKeys) ;
206     if (!ret) return ret ;
207 
208     if (this.get('simulateRemoteResponse')) {
209       this.invokeLater(this._destroyRecords, latency, store, storeKeys);
210     } else this._destroyRecords(store, storeKeys);
211 
212     return ret ;
213   },
214 
215 
216   _destroyRecords: function(store, storeKeys) {
217     storeKeys.forEach(function(storeKey) {
218       var id         = store.idFor(storeKey),
219           recordType = store.recordTypeFor(storeKey),
220           fixtures   = this.fixturesFor(recordType);
221 
222       this._invalidateCachesFor(recordType, storeKey, id);
223       if (id) delete fixtures[id];
224       store.dataSourceDidDestroy(storeKey);
225     }, this);
226   },
227 
228   // ..........................................................
229   // INTERNAL METHODS/PRIMITIVES
230   //
231 
232   /**
233     Load fixtures for a given fetchKey into the store
234     and push it to the ret array.
235 
236     @param {SC.Store} store the store to load into
237     @param {SC.Record} recordType the record type to load
238     @param {SC.Array} ret is passed, array to add loaded storeKeys to.
239     @returns {SC.FixturesDataSource} receiver
240   */
241   loadFixturesFor: function(store, recordType, ret) {
242     var hashes   = [],
243         dataHashes, i, storeKey ;
244 
245     dataHashes = this.fixturesFor(recordType);
246 
247     for(i in dataHashes){
248       storeKey = recordType.storeKeyFor(i);
249       if (store.peekStatus(storeKey) === SC.Record.EMPTY) {
250         hashes.push(dataHashes[i]);
251       }
252       if (ret) ret.push(storeKey);
253     }
254 
255     // only load records that were not already loaded to avoid infinite loops
256     if (hashes && hashes.length>0) store.loadRecords(recordType, hashes);
257 
258     return this ;
259   },
260 
261 
262   /**
263     Generates an id for the passed record type.  You can override this if
264     needed.  The default generates a storekey and formats it as a string.
265 
266     @param {Class} recordType Subclass of SC.Record
267     @param {Hash} dataHash the data hash for the record
268     @param {SC.Store} store the store
269     @param {Number} storeKey store key for the item
270     @returns {String}
271   */
272   generateIdFor: function(recordType, dataHash, store, storeKey) {
273     return "@id%@".fmt(SC.Store.generateStoreKey());
274   },
275 
276   /**
277     Based on the storeKey it returns the specified fixtures
278 
279     @param {SC.Store} store the store
280     @param {Number} storeKey the storeKey
281     @returns {Hash} data hash or null
282   */
283   fixtureForStoreKey: function(store, storeKey) {
284     var id         = store.idFor(storeKey),
285         recordType = store.recordTypeFor(storeKey),
286         fixtures   = this.fixturesFor(recordType);
287     return fixtures ? fixtures[id] : null;
288   },
289 
290   /**
291     Update the data hash fixture for the named store key.
292 
293     @param {SC.Store} store the store
294     @param {Number} storeKey the storeKey
295     @param {Hash} dataHash
296     @returns {SC.FixturesDataSource} receiver
297   */
298   setFixtureForStoreKey: function(store, storeKey, dataHash) {
299     var id         = store.idFor(storeKey),
300         recordType = store.recordTypeFor(storeKey),
301         fixtures   = this.fixturesFor(recordType);
302     this._invalidateCachesFor(recordType, storeKey, id);
303     fixtures[id] = dataHash;
304     return this ;
305   },
306 
307   /**
308     Get the fixtures for the passed record type and prepare them if needed.
309     Return cached value when complete.
310 
311     @param {SC.Record} recordType
312     @returns {Hash} data hashes
313   */
314   fixturesFor: function(recordType) {
315     // get basic fixtures hash.
316     if (!this._fixtures) this._fixtures = {};
317     var fixtures = this._fixtures[SC.guidFor(recordType)];
318     if (fixtures) return fixtures ;
319 
320     // need to load fixtures.
321     var dataHashes = recordType ? recordType.FIXTURES : null,
322         len        = dataHashes ? dataHashes.length : 0,
323         primaryKey = recordType ? recordType.prototype.primaryKey : 'guid',
324         idx, dataHash, id ;
325 
326     this._fixtures[SC.guidFor(recordType)] = fixtures = {} ;
327     for(idx=0;idx<len;idx++) {
328       dataHash = dataHashes[idx];
329       id = dataHash[primaryKey];
330       if (!id) id = this.generateIdFor(recordType, dataHash);
331       fixtures[id] = dataHash;
332     }
333     return fixtures;
334   },
335 
336   /**
337     Returns YES if fixtures for a given recordType have already been loaded
338 
339     @param {SC.Record} recordType
340     @returns {Boolean} storeKeys
341   */
342   fixturesLoadedFor: function(recordType) {
343     if (!this._fixtures) return NO;
344     var ret = [], fixtures = this._fixtures[SC.guidFor(recordType)];
345     return fixtures ? YES: NO;
346   },
347 
348   /**
349     Resets the fixtures to their original values.
350 
351     @returns {SC.FixturesDataSource} receiver
352   */
353   reset: function(){
354     this._fixtures = null;
355     return this;
356   },
357 
358   /**
359     Returns YES or SC.MIXED_STATE if one or more of the storeKeys can be
360     handled by the fixture data source.
361 
362     @param {Array} storeKeys the store keys
363     @returns {Boolean} YES if all handled, MIXED_STATE if some handled
364   */
365   hasFixturesFor: function(storeKeys) {
366     var ret = NO ;
367     storeKeys.forEach(function(storeKey) {
368       if (ret !== SC.MIXED_STATE) {
369         var recordType = SC.Store.recordTypeFor(storeKey),
370             fixtures   = recordType ? recordType.FIXTURES : null ;
371         if (fixtures && fixtures.length && fixtures.length>0) {
372           if (ret === NO) ret = YES ;
373         } else if (ret === YES) ret = SC.MIXED_STATE ;
374       }
375     }, this);
376 
377     return ret ;
378   },
379 
380   /** @private
381     Invalidates any internal caches based on the recordType and optional
382     other parameters.  Currently this only invalidates the storeKeyCache used
383     for fetch, but it could invalidate others later as well.
384 
385     @param {SC.Record} recordType the type of record modified
386     @param {Number} storeKey optional store key
387     @param {String} id optional record id
388     @returns {SC.FixturesDataSource} receiver
389   */
390   _invalidateCachesFor: function(recordType, storeKey, id) {
391     var cache = this._storeKeyCache;
392     if (cache) delete cache[SC.guidFor(recordType)];
393     return this ;
394   }
395 
396 });
397 
398 /**
399   Default fixtures instance for use in applications.
400 
401   @property {SC.FixturesDataSource}
402 */
403 SC.Record.fixtures = SC.FixturesDataSource.create();
404