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