Implementing remote data source adaptors

The data source adaptor API is fairly simple, but can be difficult to use if you aren't told that the data sources act independently of the rest of the application and only the store interacts with them. This means that our application doesn't really know what the data source is doing, which is fine because the application need only be concerned with those records that it is using and their current status.

Because each data source is unique to its remote counterpart, we can't work through every possible scenario. Instead, we'll look at writing a fairly standard JSON adaptor which you can modify to match your particular backend as needed.

To create a data source, we extend SC.DataSource and typically place the file in a directory called data_sources.

For example, if we wanted to create a main data source, we would place the code in the app directory under datasources/main_data_source.js:

// @singleton - Data source for MyApp.
MyApp.mainDataSource = SC.DataSource.create({
  // …
});

To assign the data source to the store, we can use the from helper method:

// MyApp.
MyApp = SC.Application.create({

  store: SC.Store.create().from('MyApp.mainDataSource'),

});

This is equivalent to writing the following code snippet:

  store: SC.Store.create({
    dataSource: 'MyApp.mainDataSource'
  }),

Note

We use a string to set the dataSource property so that we don't have to worry about the load order of the code. The real data source object will be looked up the first time it is needed.

Reading records

At this point, we are ready to actually request records on behalf of the store. Reading records is the most complicated feature to explain, so we'll do that first. The reason it is the most complicated is because there are three different types of reads that we can support. The first type is a fetch, which occurs the first time you try to find a query on the store. To fill queries, the store will call fetch on its data source, passing in itself and the query as arguments. All the data source needs to do is determine how to fill the query according to the remote data source it is working with, and then tell the store when it is done.

The following code is an example of filling both a local and a remote query for MyApp.Person records. In this example, the data source looks at the query type in order to determine which resource from the server will best fill its needs.

/** Triggered by MyApp.store.find(query) */
fetch: function (store, query) {
  var handled = true,
    recordType = query.recordType,
    request,
    url;

  // Fetching person records will be our queue to load resources from the server.
  if (recordType === MyApp.Person) {
    if (query.get('isLocal')) {

      // Local (i.e. client ordered results)
      url = '/friends';
    } else {
      // Remote (i.e. server ordered results)
      url = '/top_ten';
    }

    // Send the request.
    request = SC.Request.getUrl(url)
      .json()
      .notify(this, '_didFetch', {
        query: query,
        store: store
      })
      .send();
  } else {

    // This query wasn't handled by the data source.
    handled = false;
  }

  return handled;
},

/** @private Callback for JSON fetch requests. */
_didFetch: function (response, params) {
  if (SC.ok(response)) {
    var body = response.get('body'), 
      query = params.query,
      store = params.store,
      storeKeys;

    // Load the new records into the store.
    storeKeys = store.loadRecords(MyApp.Person, body.people);

    // Indicate that the query has been filled (storeKeys will be ignored for local queries).
    store.dataSourceDidFetchQuery(query, storeKeys);
}

To indicate that a query has been filled, we call dataSourceDidFetchQuery on the store. For a remote query, we must pass the ordered array of store keys for the results as well. Note that before the call to dataSourceDidFetchQuery, the record array for the query will be in the BUSY_LOADING state, and will move to the READY_CLEAN state afterwards. Also note that the data source returns true only if it handles the request.

The next type of read is for retrieving a single record which occurs when we attempt to find a record by ID that doesn't exist in the store. Again, the store will ask its data source to fill the request for the record, this time by calling retrieveRecord on the data source. This type of request is even simpler to implement in the data source. All we need to do is call loadRecord with the data we receive.

For example:

/** Triggered by MyApp.store.find(RecordClass, id) */
retrieveRecord: function (store, storeKey, id) {
  var handled = true,
    recordType = store.recordTypeFor(storeKey),
    request;

  if (recordType === MyApp.Item) {
    // Send the request.
    request = SC.Request.getUrl('/items/' + id)
      .json()
      .notify(this, '_didRetrieve', {
        store: store,
        recordType: recordType
      })
      .send();
  } else {

    // This retrieve wasn't handled by the data source.
    handled = false;
  }

  return handled;
},

/** @private Callback for JSON retrieve requests. */
_didRetrieve: function (response, params) {
  if (SC.ok(response)) {
    var store = params.store,
      recordType = params.recordType,
      body = response.get('body'),

    store.loadRecord(recordType, body);
  }
}

Finally, there is the third type of read, which is to retrieve multiple records by ID at once by implementing retrieveRecords in the data source. This is different from the others because this type of read is triggered by passing a set of specific record IDs to the store's retrieveRecords method. However, it is rare to find an API that allows you to retrieve multiple records by ID in a single request. In any case, you would handle it in the data source exactly the same way you handle retrieveRecord, just by substituting multiple IDs in place of the single ID.

Creating, updating, and destroying records

Now that we've seen how to implement reading, you will find that the other methods are simpler and very similar, whether we're creating, updating, or destroying the records. The only usual difference between them is that the actual request method made is different (for example, POST vs. GET), and that we don't need to call loadRecord when destroying records.

The following code is an example data source support for creating and editing records with a JSON backend:

/** Triggered by readyNewRecord.commitRecord() */
createRecord: function (store, storeKey, params) {
  var handled = true,
    recordType = store.recordTypeFor(storeKey),
    request;

  if (recordType === MyApp.Item) {
    // Send the request.
    request = SC.Request.postUrl('/items)
      .json()
      .notify(this, '_didCreateOrUpdate', {
        store: store,
        recordType: recordType
      })
      .send();
  } else {

    // This create wasn't handled by the data source.
    handled = false;
  }

  return handled;
},

/** Triggered by readyDirtyRecord.commitRecord() */
updateRecord: function (store, storeKey, params) {
  var handled = true,
    id = store.idFor(storeKey),
    recordType = store.recordTypeFor(storeKey),
    request;

  if (recordType === MyApp.Item) {
    // Send the request.
    request = SC.Request.putUrl('/items/' + id)
      .json()
      .notify(this, '_didCreateOrUpdate', {
        store: store,
        recordType: recordType
      })
      .send();
  } else {

    // This update wasn't handled by the data source.
    handled = false;
  }

  return handled;
},

/** @private Callback for JSON create and update requests. */
_didCreateOrUpdate: function (response, params) {
  if (SC.ok(response)) {
    var store = params.store,
      recordType = params.recordType,
      body = response.get('body'),

    store.loadRecord(recordType, body);
  }
}

In this particular example, because creating and updating are so similar, we can in fact use the same callback function for the request. Again, this will all depend on the particular backend you are working with.

Finally, the last method to implement on the data source is destroyRecord, which we would do in a very similar fashion.

For example:

/** Triggered by destroyedDirtyRecord.commitRecord() */
destroyRecord: function (store, storeKey, params) {
  var handled = true,
    id = store.idFor(storeKey),
    recordType = store.recordTypeFor(storeKey),
    request;

  if (recordType === MyApp.Item) {
    // Send the request.
    request = SC.Request.deleteUrl('/items/' + id)
      .json()
      .notify(this, '_didDestroy', {
        store: store,
        storeKey: storeKey
      })
      .send();
  } else {

    // This destroy wasn't handled by the data source.
    handled = false;
  }

  return handled;
},

/** @private Callback for JSON destroy requests. */
_didDestroy: function (response, params) {
  if (SC.ok(response)) {
    var store = params.store,
      storeKey = params.recordType;

    // Indicate that the record has indeed been destroyed.
    store.dataSourceDidDestroy(storeKey);
  }
}

Note

There are methods that can be used to modify several records with a single request if the remote data source supports it. These methods are createRecords, updateRecords, and destroyRecords.

With these examples, it actually takes care of all the fundamentals of creating a data source. Just remember that the actual request and response handling code in your application is specific to the remote data source that you are working with and so, you may find that your actual data source code looks quite different to these examples. But still, you will use the same store callbacks in the same manner: dataSourceDidFetchQuery, loadRecord, loadRecords, and dataSourceDidDestroy.

The only other thing I need to do is remind you of the fixtures data source, SC.FixturesDataSource, that we saw in Chapter 1, Introducing SproutCore. Fixtures are essential when the remote source isn't quite ready for use. But even if the remote source is usable, developing against fixture data is much faster and provides you with a well-known environment to work in.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.206.254