At the time of writing I’m learning Ember-cli for the first time. It’s pretty tough as there’s not too much up-to-date documentation out there, but I’m slowly getting there, and I’m beginning to feel the power it brings.

One of the things I’ve been finding especially difficult is grokking how Ember consumes API data - specifically how it decides upon what the route it wants to consume looks like.

After hours of fruitless Googling and reading, I relented and hit IRC: #emberjs on Freenode to be specific. After speaking to a few helpful fellows on there, it became clear that Ember was not easily able to consume nested API routes just yet. Damn! That makes scoping your API data quite hard!

After an hour or so of work, I managed to apply my newly found knowledge into a fix that, when I have enough skill, I’ll be able to contribute back into ember in the form of an addon.

Ember Adapters

The first piece of insight I gained from speaking to the guys on IRC was that the Ember Adapters are for just what you might think they are: Adapting (duh).

So it was the Ember Adapter that I had to study in order to manipulate the url ember would calculate in order to make the request to the API.

After looking a little closer I found that the RESTAdapter (and all those that extend from it) uses a method called buildURL to build the URL for the request. And so it was buildURL I’d need to extend in order to support the deep, nested routes my API had.

The Plan

What I really wanted was to be able to define the chain between the model and all the relations in the scope of the API URL. For example, if we’re building a car, we’d need amongst other things a set of tires. In our case a relationship could be defined as:

Tires < Wheel < Axel < Chassis

So if the URL for ordering a tire for this specific car looked like:

POST /chassis/:chassis_id/axel/:axel_id/wheel/:wheel_id/tires

Then we’d want to define that when dealing with a tire, we need to build a url structure using the chassis, axel, and wheel.

So I decided that what I wanted in my adapter was the ability to define the parent records in scope we cared about as such:

import Adapter from './application';

export default Adapter.extend({
  parentRecords: function() { return ['chassis', 'axel', 'wheel']; }
});

This would be enough data to generate the path we defined above. The challenge was to enable buildURL to support this.

Implementing the change

Looking at the buildURL method in the docs, we can see that it requires three parameters: type, id, and record. record is the thing we really care about for our implementation: the plan is to take the result of parentRecords and loop through it in reverse (wheel to chassis) in order to find the IDs of each relation step-by-step.

The first thing I did was in my adapters/application.js, add a stubbed parentRecords method:

import DS from 'ember-data';
import Ember from 'ember';

export default DS.ActiveModelAdapter.extend({
  parentRecords: function() { return []; }
});

This would allow regular models to function flawlessly. Next I’d add an exact clone of the buildURL implementation available on github

At this point, everything should work (and not work) as before, but we now have a workspace to make changes safely and have them apply to all our models.

Next, I wrote a method which would traverse to the relation specified and return a chunk of the path, as well the cursor of the record that was traversed to. This would allow us to recursively traverse across all of the relations specified in parentRecords(), giving us a complete url!

getParentUrlChunk: function(record, type) {
  var cursor = record.get(type),
      id = cursor.get('id'),
      inflector = Ember.Inflector.inflector;
  return [
    [inflector.pluralize(type), id].join('/'),
    cursor
  ];
}

So if we ran this.getParentUrlChunk(tire, 'wheel'); we would expect to get back something like:

[ 'wheels/1', <[email protected]:wheel::ember432:1> ]

Once this was done, I could then add this to an appropriate part of buildURL:

buildURL: function(type, id, record) {

  var url = [],
  host = Ember.get(this, 'host'),
  prefix = this.urlPrefix(),
  typeCursor = record,
  _self = this,
  scope = [];

  this.parentRecords().reverse().forEach(function(parentType){
    var chunk  = null,
        cursor = null;
    [chunk, cursor] = _self.getParentUrlChunk(typeCursor, parentType);
    typeCursor = cursor;
    scope.unshift(chunk);
  });
  
  url.push(scope.join('/'));

  if (type) { url.push(this.pathForType(type)); }

  if (id && !Ember.isArray(id)) { url.push(encodeURIComponent(id)); }

  if (prefix) { url.unshift(prefix); }

  url = url.join('/');
  if (!host && url) { url = '/' + url; }

  return url;
}

And it works! All we need to do is extend this Adapter for each model which is Deeply nested.

The more you know