Consuming Nested API Routes with Ember
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’, <ember@model: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.