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 /**
  9   Indicates a value has a mixed state of both on and off.
 10 
 11   @type String
 12 */
 13 SC.MIXED_STATE = '__MIXED__';
 14 
 15 /** @class
 16 
 17   A DataSource connects an in-memory store to one or more server backends.
 18   To connect to a data backend on a server, subclass `SC.DataSource`
 19   and implement the necessary data source methods to communicate with the
 20   particular backend.
 21 
 22   ## Create a Data Source
 23 
 24   To implement the data source, subclass `SC.DataSource` in a file located
 25   either in the root level of your app or framework, or in a directory
 26   called "data_sources":
 27 
 28       MyApp.DataSource = SC.DataSource.extend({
 29         // implement the data source API...
 30       });
 31 
 32   ## Connect to a Data Source
 33 
 34   New SproutCore applications are wired up to fixtures as their data source.
 35   When you are ready to connect to a server, swap the use of fixtures with a
 36   call to the desired data source.
 37 
 38   In core.js:
 39 
 40       // change...
 41       store: SC.Store.create().from(SC.Record.fixtures)
 42 
 43       // to...
 44       store: SC.Store.create().from('MyApp.DataSource')
 45 
 46   Note that the data source class name is referenced by string since the file
 47   in which it is defined may not have been loaded yet. The first time a
 48   data store tries to access its data source it will look up the class name
 49   and instantiate that data source.
 50 
 51   ## Implement the Data Source API
 52 
 53   There are three methods that a data store invokes on its data source:
 54 
 55    * `fetch()` — called the first time you try to `find()` a query
 56      on a store or any time you refresh the record array after that.
 57    * `retrieveRecords()` — called when you access an individual
 58      record that has not been loaded yet
 59    * `commitRecords()` — called if the the store has changes
 60      pending and its `commitRecords()` method is invoked.
 61 
 62   The data store will call the `commitRecords()` method when records
 63   need to be created, updated, or deleted. If the server that the data source
 64   connects to handles these three actions in a uniform manner, it may be
 65   convenient to implement the `commitRecords()` to handle record
 66   creation, updating, and deletion.
 67 
 68   However, if the calls the data source will need to make to the server to
 69   create, update, and delete records differ from each other to a significant
 70   enough degree, it will be more convenient to rely on the default behavior
 71   of `commitRecords()` and instead implement the three methods that
 72   it will call by default:
 73 
 74    * `createRecords()` — called with a list of records that are new
 75      and need to be created on the server.
 76    * `updateRecords()` — called with a list of records that already
 77       exist on the server but that need to be updated.
 78    * `destroyRecords()` — called with a list of records that should
 79      be deleted on the server.
 80 
 81   ### Multiple records
 82 
 83   The `retrieveRecords()`, `createRecords()`, `updateRecords()` and
 84   `destroyRecords()` methods all work on multiple records. If your server
 85   API accommodates calls where you can  pass a list of records, this might
 86   be the best level at which to implement the Data Source API. On the other
 87   hand, if the server requires that you send commands for it for individual
 88   records, you can rely on the default implementation of these four methods,
 89   which will call the following for each individual record, one at a time:
 90 
 91    - `retrieveRecord()` — called to retrieve a single record.
 92    - `createRecord()` — called to create a single record.
 93    - `updateRecord()` — called to update a single record.
 94    - `destroyRecord()` — called to destroy a single record.
 95 
 96 
 97   ### Return Values
 98 
 99   All of the methods you implement must return one of three values:
100    - `YES` — all the records were handled.
101    - `NO` — none of the records were handled.
102    - `SC.MIXED_STATE` — some, but not all of the records were handled.
103 
104 
105   ### Store Keys
106 
107   Whenever a data store invokes one of the data source methods it does so
108   with a storeKeys or storeKey argument. Store keys are transient integers
109   assigned to each data hash when it is first loaded into the store. It is
110   used to track data hashes as they move up and down nested stores (even if
111   no associated record is ever created from it).
112 
113   When passed a storeKey you can use it to retrieve the status, data hash,
114   record type, or record ID, using the following data store methods:
115 
116    * `readDataHash(storeKey)` — returns the data hash associated with
117      a store key, if any.
118    * `readStatus(storeKey)` — returns the current record status
119      associated with the store key. May be `SC.Record.EMPTY`.
120    * `SC.Store.recordTypeFor(storeKey)` — returns the record type for
121      the associated store key.
122    * `recordType.idFor(storeKey)` — returns the record ID for
123      the associated store key. You must call this method on `SC.Record`
124      subclass itself, not on an instance of `SC.Record`.
125 
126   These methods are safe for reading data from the store. To modify data
127   in the data store you must use the store callbacks described below. The
128   store callbacks will ensure that the record states remain consistent.
129 
130   ### Store Callbacks
131 
132   When a data store calls a data source method, it puts affected records into
133   a `BUSY` state. To guarantee data integrity and consistency, these records
134   cannot be modified by the rest of the application while they are in the `BUSY`
135   state.
136 
137   Because records are "locked" while in the `BUSY` state, it is the data source's
138   responsibility to invoke a callback on the store for each record or query that
139   was passed to it and that the data source handled. To reduce the amount of work
140   that a data source must do, the data store will automatically unlock the relevant
141   records if the the data source method returned `NO`, indicating that the records
142   were unhandled.
143 
144   Although a data source can invoke callback methods at any time, they should
145   usually be invoked after receiving a response from the server. For example, when
146   the data source commits a change to a record by issuing a command to the server,
147   it waits for the server to acknowledge the command before invoking the
148   `dataSourceDidComplete()` callback.
149 
150   In some cases a data source may be able to assume a server's response and invoke
151   the callback on the store immediately. This can improve performance because the
152   record can be unlocked right away.
153 
154 
155   ### Record-Related Callbacks
156 
157   When `retrieveRecords()`, `commitRecords()`, or any of the related methods are
158   called on a data source, the store puts any records to be handled by the data
159   store in a `BUSY` state. To release the records the data source must invoke one
160   of the record-related callbacks on the store:
161 
162    * `dataSourceDidComplete(storeKey, dataHash, id)` — the most common
163      callback. You might use this callback when you have retrieved a record to
164      load its contents into the store. The callback tells the store that the data
165      source is finished with the storeKey in question. The `dataHash` and `id`
166      arguments are optional and will replace the current dataHash and/or id. Also
167      see "Loading Records" below.
168    * `dataSourceDidError(storeKey, error)` — a data source should call this
169      when a request could not be completed because an error occurred. The error
170      argument is optional and can contain more information about the error.
171    * `dataSourceDidCancel(storeKey)` — a data source should call this when
172      an operation is cancelled for some reason. This could be used when the user
173      is able to cancel an operation that is in progress.
174 
175   ### Loading Records into the Store
176 
177   Instead of orchestrating multiple `dataSourceDidComplete()` callbacks when loading
178   multiple records, a data source can call the `loadRecords()` method on the store,
179   passing in a `recordType`, and array of data hashes, and optionally an array of ids.
180   The `loadRecords()` method takes care of looking up storeKeys and calling the
181   `dataSourceDidComplete()` callback as needed.
182 
183   `loadRecords()` is often the most convenient way to get large blocks of data into
184   the store, especially in response to a `fetch()` or `retrieveRecords()` call.
185 
186 
187   ### Query-Related Callbacks
188 
189   Like records, queries that are passed through the `fetch()` method also have an
190   associated status property; accessed through the `status`  property on the record
191   array returned from `find()`. To properly reset this status, a data source must
192   invoke an appropriate query-related callback on the store. The callbacks for
193   queries are similar to those for records:
194 
195    * `dataSourceDidFetchQuery(query)` — the data source must call this when
196      it has completed fetching any related data for the query. This returns the
197      query results (i.e. the record array) status into a `READY` state. If the query is a 'remote'
198      type, the ordered array of store keys representing the results from the server must be passed
199      as a second argument.
200    * `dataSourceDidErrorQuery(query, error)` — the data source should call
201      this if it encounters an error in executing the query. This puts the query
202      results into an `ERROR` state.
203    * `dataSourceDidCancelQuery(query)` — the data source should call this
204      if loading the results is cancelled.
205 
206   @extend SC.Object
207   @since SproutCore 1.0
208 */
209 SC.DataSource = SC.Object.extend( /** @scope SC.DataSource.prototype */ {
210 
211   // ..........................................................
212   // SC.STORE ENTRY POINTS
213   //
214 
215 
216   /**
217 
218     Invoked by the store whenever it needs to retrieve data matching a
219     specific query, triggered by find().  This method is called anytime
220     you invoke SC.Store#find() with a query or SC.RecordArray#refresh().  You
221     should override this method to actually retrieve data from the server
222     needed to fulfill the query.  If the query is a remote query, then you
223     will also need to provide the contents of the query as well.
224 
225     ### Handling Local Queries
226 
227     Most queries you create in your application will be local queries.  Local
228     queries are populated automatically from whatever data you have in memory.
229     When your fetch() method is called on a local queries, all you need to do
230     is load any records that might be matched by the query into memory.
231 
232     The way you choose which queries to fetch is up to you, though usually it
233     can be something fairly straightforward such as loading all records of a
234     specified type.
235 
236     When you finish loading any data that might be required for your query,
237     you should always call SC.Store#dataSourceDidFetchQuery() to put the query
238     back into the READY state.  You should call this method even if you choose
239     not to load any new data into the store in order to notify that the store
240     that you think it is ready to return results for the query.
241 
242     ### Handling Remote Queries
243 
244     Remote queries are special queries whose results will be populated by the
245     server instead of from memory.  Usually you will only need to use this
246     type of query when loading large amounts of data from the server.
247 
248     Like local queries, to fetch a remote query you will need to load any data
249     you need to fetch from the server and add the records to the store.  Once
250     you are finished loading this data, however, you must also call
251     SC.Store#dataSourceDidFetchQuery() with the array of storeKeys that
252     represent the latest results from the server.
253 
254     If you want to support incremental loading from the server for remote
255     queries, you can do so by passing a SC.SparseArray instance instead of
256     a regular array of storeKeys and then populate the sparse array on demand.
257 
258     ### Handling Errors and Cancellations
259 
260     If you encounter an error while trying to fetch the results for a query
261     you can call SC.Store#dataSourceDidErrorQuery() instead.  This will put
262     the query results into an error state.
263 
264     If you had to cancel fetching a query before the results were returned,
265     you can instead call SC.Store#dataSourceDidCancelQuery().  This will set
266     the query back into the state it was in previously before it started
267     loading the query.
268 
269     ### Return Values
270 
271     When you return from this method, be sure to return a Boolean.  YES means
272     you handled the query, NO means you can't handle the query.  When using
273     a cascading data source, returning NO will mean the next data source will
274     be asked to fetch the same results as well.
275 
276     @param {SC.Store} store the requesting store
277     @param {SC.Query} query query describing the request
278     @returns {Boolean} YES if you can handle fetching the query, NO otherwise
279   */
280   fetch: function(store, query) {
281     return NO ; // do not handle anything!
282   },
283 
284   /**
285     Called by the store whenever it needs to load a specific set of store
286     keys.  The default implementation will call retrieveRecord() for each
287     storeKey.
288 
289     You should implement either retrieveRecord() or retrieveRecords() to
290     actually fetch the records referenced by the storeKeys .
291 
292     @param {SC.Store} store the requesting store
293     @param {Array} storeKeys
294     @param {Array} ids - optional
295     @returns {Boolean} YES if handled, NO otherwise
296   */
297   retrieveRecords: function(store, storeKeys, ids) {
298     return this._handleEach(store, storeKeys, this.retrieveRecord, ids);
299   },
300 
301   /**
302     Invoked by the store whenever it has one or more records with pending
303     changes that need to be sent back to the server.  The store keys will be
304     separated into three categories:
305 
306      - `createStoreKeys`: records that need to be created on server
307      - `updateStoreKeys`: existing records that have been modified
308      - `destroyStoreKeys`: records need to be destroyed on the server
309 
310     If you do not override this method yourself, this method will actually
311     invoke `createRecords()`, `updateRecords()`, and `destroyRecords()` on the
312     dataSource, passing each array of storeKeys.  You can usually implement
313     those methods instead of overriding this method.
314 
315     However, if your server API can sync multiple changes at once, you may
316     prefer to override this method instead.
317 
318     To support cascading data stores, be sure to return `NO` if you cannot
319     handle any of the keys, `YES` if you can handle all of the keys, or
320     `SC.MIXED_STATE` if you can handle some of them.
321 
322     @param {SC.Store} store the requesting store
323     @param {Array} createStoreKeys keys to create
324     @param {Array} updateStoreKeys keys to update
325     @param {Array} destroyStoreKeys keys to destroy
326     @param {Hash} params to be passed down to data source. originated
327       from the commitRecords() call on the store
328     @returns {Boolean} YES if data source can handle keys
329   */
330   commitRecords: function(store, createStoreKeys, updateStoreKeys, destroyStoreKeys, params) {
331     var uret, dret, ret;
332     if (createStoreKeys.length>0) {
333       ret = this.createRecords.call(this, store, createStoreKeys, params);
334     }
335 
336     if (updateStoreKeys.length>0) {
337       uret = this.updateRecords.call(this, store, updateStoreKeys, params);
338       ret = SC.none(ret) ? uret : (ret === uret) ? ret : SC.MIXED_STATE;
339     }
340 
341     if (destroyStoreKeys.length>0) {
342       dret = this.destroyRecords.call(this, store, destroyStoreKeys, params);
343       ret = SC.none(ret) ? dret : (ret === dret) ? ret : SC.MIXED_STATE;
344     }
345 
346     return ret || NO;
347   },
348 
349   /**
350     Invoked by the store whenever it needs to cancel one or more records that
351     are currently in-flight.  If any of the storeKeys match records you are
352     currently acting upon, you should cancel the in-progress operation and
353     return `YES`.
354 
355     If you implement an in-memory data source that immediately services the
356     other requests, then this method will never be called on your data source.
357 
358     To support cascading data stores, be sure to return `NO` if you cannot
359     retrieve any of the keys, `YES` if you can retrieve all of the, or
360     `SC.MIXED_STATE` if you can retrieve some of the.
361 
362     @param {SC.Store} store the requesting store
363     @param {Array} storeKeys array of storeKeys to retrieve
364     @returns {Boolean} YES if data source can handle keys
365   */
366   cancel: function(store, storeKeys) {
367     return NO;
368   },
369 
370   // ..........................................................
371   // BULK RECORD ACTIONS
372   //
373 
374   /**
375     Called from `commitRecords()` to commit modified existing records to the
376     store.  You can override this method to actually send the updated
377     records to your store.  The default version will simply call
378     `updateRecord()` for each storeKey.
379 
380     To support cascading data stores, be sure to return `NO` if you cannot
381     handle any of the keys, `YES` if you can handle all of the keys, or
382     `SC.MIXED_STATE` if you can handle some of them.
383 
384     @param {SC.Store} store the requesting store
385     @param {Array} storeKeys keys to update
386     @param {Hash} params
387       to be passed down to data source. originated from the commitRecords()
388       call on the store
389 
390     @returns {Boolean} YES, NO, or SC.MIXED_STATE
391 
392   */
393   updateRecords: function(store, storeKeys, params) {
394     return this._handleEach(store, storeKeys, this.updateRecord, null, params);
395   },
396 
397   /**
398     Called from `commitRecords()` to commit newly created records to the
399     store.  You can override this method to actually send the created
400     records to your store.  The default version will simply call
401     `createRecord()` for each storeKey.
402 
403     To support cascading data stores, be sure to return `NO` if you cannot
404     handle any of the keys, `YES` if you can handle all of the keys, or
405     `SC.MIXED_STATE` if you can handle some of them.
406 
407     @param {SC.Store} store the requesting store
408     @param {Array} storeKeys keys to update
409 
410     @param {Hash} params
411       to be passed down to data source. originated from the commitRecords()
412       call on the store
413 
414     @returns {Boolean} YES, NO, or SC.MIXED_STATE
415 
416   */
417   createRecords: function(store, storeKeys, params) {
418     return this._handleEach(store, storeKeys, this.createRecord, null, params);
419   },
420 
421   /**
422     Called from `commitRecords()` to commit destroyed records to the
423     store.  You can override this method to actually send the destroyed
424     records to your store.  The default version will simply call
425     `destroyRecord()` for each storeKey.
426 
427     To support cascading data stores, be sure to return `NO` if you cannot
428     handle any of the keys, `YES` if you can handle all of the keys, or
429     `SC.MIXED_STATE` if you can handle some of them.
430 
431     @param {SC.Store} store the requesting store
432     @param {Array} storeKeys keys to update
433     @param {Hash} params to be passed down to data source. originated
434       from the commitRecords() call on the store
435 
436     @returns {Boolean} YES, NO, or SC.MIXED_STATE
437 
438   */
439   destroyRecords: function(store, storeKeys, params) {
440     return this._handleEach(store, storeKeys, this.destroyRecord, null, params);
441   },
442 
443   /** @private
444     invokes the named action for each store key.  returns proper value
445   */
446   _handleEach: function(store, storeKeys, action, ids, params) {
447     var len = storeKeys.length, idx, ret, cur, idOrParams;
448 
449     for(idx=0;idx<len;idx++) {
450       idOrParams = ids ? ids[idx] : params;
451 
452       cur = action.call(this, store, storeKeys[idx], idOrParams);
453       if (ret === undefined) {
454         ret = cur ;
455       } else if (ret === YES) {
456         ret = (cur === YES) ? YES : SC.MIXED_STATE ;
457       } else if (ret === NO) {
458         ret = (cur === NO) ? NO : SC.MIXED_STATE ;
459       }
460     }
461     return !SC.none(ret) ? ret : null ;
462   },
463 
464 
465   // ..........................................................
466   // SINGLE RECORD ACTIONS
467   //
468 
469   /**
470     Called from `updatesRecords()` to update a single record.  This is the
471     most basic primitive to can implement to support updating a record.
472 
473     To support cascading data stores, be sure to return `NO` if you cannot
474     handle the passed storeKey or `YES` if you can.
475 
476     @param {SC.Store} store the requesting store
477     @param {Array} storeKey key to update
478     @param {Hash} params to be passed down to data source. originated
479       from the commitRecords() call on the store
480     @returns {Boolean} YES if handled
481   */
482   updateRecord: function(store, storeKey, params) {
483     return NO ;
484   },
485 
486   /**
487     Called from `retrieveRecords()` to retrieve a single record.
488 
489     @param {SC.Store} store the requesting store
490     @param {Array} storeKey key to retrieve
491     @param {String} id the id to retrieve
492     @returns {Boolean} YES if handled
493   */
494   retrieveRecord: function(store, storeKey, id) {
495     return NO ;
496   },
497 
498   /**
499     Called from `createdRecords()` to created a single record.  This is the
500     most basic primitive to can implement to support creating a record.
501 
502     To support cascading data stores, be sure to return `NO` if you cannot
503     handle the passed storeKey or `YES` if you can.
504 
505     @param {SC.Store} store the requesting store
506     @param {Array} storeKey key to update
507     @param {Hash} params to be passed down to data source. originated
508       from the commitRecords() call on the store
509     @returns {Boolean} YES if handled
510   */
511   createRecord: function(store, storeKey, params) {
512     return NO ;
513   },
514 
515   /**
516     Called from `destroyRecords()` to destroy a single record.  This is the
517     most basic primitive to can implement to support destroying a record.
518 
519     To support cascading data stores, be sure to return `NO` if you cannot
520     handle the passed storeKey or `YES` if you can.
521 
522     @param {SC.Store} store the requesting store
523     @param {Array} storeKey key to update
524     @param {Hash} params to be passed down to data source. originated
525       from the commitRecords() call on the store
526     @returns {Boolean} YES if handled
527   */
528   destroyRecord: function(store, storeKey, params) {
529     return NO ;
530   }
531 
532 });
533