Side-Load Like a Pro with Ember-RESTless

Article summary

Side-loading is an efficient way for a developer to pull multiple pieces of relevant JSON data (i.e. data for multiple model types) from a single HTTP request in a client-server implementation. Rather than requiring a client to make multiple requests to fetch the full set of relevant data, side-loading automatically sends all relevant data back from the server.

For example, if you were implementing a web page to display all blog posts and their comments, you would need to fetch all the BlogPost and Comment records. By side-loading the Comment data along with the BlogPost data, all relevant data can be received with one single request. The side-loaded data might look something like this example, which shows two posts and three total comments, each associated to a particular post:

{
  "posts": [
    { "id": 1, 
      "title": "Side-load Like a Pro With Ember-RESTless", 
      "body": "...", 
      "author": "Matt Rozema" 
    }, {
      "id": 2,
      "title": "Another really good blog post",
      "body": "...",
      "author": "John Doe"
    }
  ],
  "comments": [
    { "id": 1, "author": "John Doe", "comment": "Amazing post!",  "post_id": 1 },
    { "id": 2, "author": "Jane Doe", "comment": "Life Changing!", "post_id": 1 },
    { "id": 3, "author": "Mrs. Doe", "comment": "You rock!",      "post_id": 2 }
  ]
}

On my current project, we are building a web application using Ember.js and a persistence layer called Ember-RESTless. Ember-RESTless is a lightweight alternative to the ever-so-popular Ember Data. We chose Ember-RESTless for this project because it doesn’t require full-blown Ember Data support, and also due to Ember Data’s instability at the time of project kickoff. Overall, it was a good choice, and working with it has been relatively painless compared to some of the alternatives. However, one of the challenges of using Ember-RESTless is a general lack of support for side-loading records.

The Problem

Ember-RESTless does provide a few small helpers for side-loading. The loadMany and load APIs take in raw JSON and load it into an Ember Object, which can then be set onto the model. However, the problem is:

  1. We usually don’t have direct access to the raw JSON.
  2. Even if we did, it still requires manual and non-pragmatic code (DRY, anyone?) to load the side-loaded data onto the model.

These problems are enough to discourage a lot of developers from side-loading, or to make them search for another persistence layer. In fact, one of my coworkers commented on our Ember channel in Slack earlier this year:

“I’ve realized that Ember-RESTLess doesn’t actually support side-loading particularly well, so I may go on a hunt for another data persistence framework…again.”

Hark, hunt no more! Rather than shy away from using side-loading in Ember-RESTless, my team and I decided to build our own side-loading module.

The Goods

Our solution was to create a class that defines a loadSingle and loadMany API, which wraps the loadMany Ember-RESTless API. Based on the structure of the model, it will determine the data relationships between the resource class (the model being directly fetched) and the side-loaded keys. Once it determines the relevant side-loaded records (models) based on ID(s), it loads the side-loaded data onto the resource class’ model, so all relevant data is available to the view and controller.

Below is the definition of the loadMany API from the SideLoader class. I placed numerous comments to describe the algorithm.


loadMany: function(resourceClass) {
  // GET the items from the 'resourceClass' endpoint
  return resourceClass.fetch().then(items => {
    var sideLoad = {};
    var mainKey = pluralize(resourceClass.resourceName);

    // Dig into the depths of Ember-RESTless to find the raw JSON
    // returned from this request
    var rawData = items.currentRequest._result;

    // Create a hash of Ember Objects for each piece of side-loaded
    // data, using the Ember-RESTless loadMany() API...
    Object.keys(rawData).forEach(key => {
      if(key !== mainKey) {
        // Find the model of the side-loaded record
        var sideClass = RL.client.adapter.serializer
                        .modelFor(singularize(key.dasherize()));
        sideLoad[key] = sideClass.loadMany(rawData[key]);
      }
    });

    // For each record (item) in the resourceClass and for each 
    // type of side-loaded record, determine the relationship and 
    // set the relevant side-loaded records onto the main object
    items.forEach(item => {
      Object.keys(sideLoad).forEach(sideKey => {

        // We expect the convention of dependent keys to have the name:
        // 'attributeId' or 'attributeIds', and 'attribute' would be the 
        // JSON key.
        var sideRecords = sideLoad[sideKey];
        var camelKey = singularize(sideKey).camelize();
        var idKey = camelKey + "Ids";
        var foreignKey = resourceClass.resourceName.camelize() + "Id";
        
        // Test for "has and belongs to many" or "belongs to":
        // If the main record contains the ID of one of the side-loaded
        // records, then it is "has and belongs to many" or "belongs to"
        if(item.get(idKey)) {
          item.set(sideKey.camelize(), sideRecords.filter(sideRec => {
            return item.get(idKey).contains(sideRec.get('id'));
          }));
        } 
        // Test for "has many":
        // If the side-record contains the foreign key, then it is a "has many"
        else if (sideRecords.get('firstObject').get(foreignKey)) {
          item.set(sideKey.camelize(), sideRecords.filter(sideRec => {
            return sideRec.get(foreignKey) === item.get('id');
          }));
        }
      });
    });
    return items;
  });
}

With this in place, and some glue code to export a SideLoader object, a developer can now simply side-load data with the following model hook:


import BlogPost from '../../models/blog-post'; 

export default Ember.Route.extend({ 
  model: function() { 
    return SideLoader.loadMany(BlogPost); 
  } 
}); 

and the following model definitions:


var BlogPost = RL.Model.extend({ 
  title: RL.attr(),
  body: RL.attr(),
  author: RL.attr()
}); 

var Comment = RL.Model.extend({
  author: RL.attr(),
  comment: RL.attr(),
  postId: RL.attr()
}); 

export default BlogPost;
export default Comment; 

This solution will fetch all blog posts and also automatically load the side-loaded comments for each blog post (i.e. a BlogPost object will have a comments property containing an array of comments)!

You can see the full code for loadMany and loadSingle APIs here.

Have you ever faced a similar problem in Ember-RESTless (or any other persistence layer)? How did you solve it?