Karol Galanciak - Ruby on Rails and Ember.js consultant

Implementing non-RESTful Actions With Ember Data

In my recent post I mentioned some strategies of handling non-strictly CRUD / RESTful actions in API. One of them was adding extra actions beyond creating, updating and deleting resources. As it’s not a standard solution and some data layers on client side (like Ember Data) don’t handle it out-of-box, I was asked by some developers what’s the best way to handle such actions. So let’s see how can we hack into Ember Data and make it smooth.

Our use case: publishing articles

Let’s reuse the example from the previous blog post - the articles and publishing process. What we are going to do is to add publish function to our Article model, which simply sends PATCH request to API endpoint with /api/articles/:id/publish URL.

First thought about implementation could be using Ember.$.ajax call with manually passing URL and all the data. But that’s not really a great solution. For simple cases it may work, but what about sending some custom headers, using namespaces and other options? For these reasons we should encapsulate all the logic within adapter - the layer that’s responsible for communication with the API. So let’s add publish function to our article adapter for executing the request and use this function from article model.

1
2
3
4
5
6
7
8
9
10
11
12
13
// app/adapters/article.js

import ApplicationAdapter from './application';

export default ApplicationAdapter.extend({
  publish(id) {
    return this.ajax(this.urlForPublishAction(id), 'PUT');
  },

  urlForPublishAction(id) {
    return `${this.buildURL('article', id)}/publish`;
  }
});

There are some interesting things going on here: the first one is that we use ajax function defined in adapter, which is also utilized when peforming all other requests in Ember Data. It takes care of setting up options like headers, proper success and error handling etc., so we should always use this function instead of simple Ember.$.ajax. Another thing is buildURL function, which builds, well, URL for given resource represented by model name (with given id if present) considering adapter options like host or namespace. By using these functions we ensure the consistency between all API calls.

To make it work we just need to find proper adapter for article model and call publish function on this adapter from the model:

1
2
3
4
5
6
7
8
9
10
11
12
// app/models/article.js

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

export default DS.Model.extend({
  publish() {
    let modelName = this.constructor.modelName;
    let adapter = this.store.adapterFor(modelName);
    return adapter.publish(this.get('id'));
  }
});

To avoid hardcoding model name we take it from the constructor, then use it as a name of adapter we want to fetch from owner (or container in Ember versions prior to 2.3) and finally call the proper method on adapter with id as argument.

What if we needed to pass the serialized attributes as well? The third argument of ajax function in adapter is a hash, so we would need to pass the serialized article as data param there. Here’s a quick example, starting from the model:

1
2
3
4
5
6
7
8
9
10
11
12
// app/models/article.js

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

export default DS.Model.extend({
  publish() {
    let modelName = this.constructor.modelName;
    let adapter = this.store.adapterFor(modelName);
    return adapter.publish(this.get('id'), this.serialize());
  }
});

and here’s the adapter:

1
2
3
4
5
6
7
8
9
10
11
12
13
// app/adapters/article.js

import ApplicationAdapter from './application';

export default ApplicationAdapter.extend({
  publish(id, serializedData) {
    return this.ajax(this.urlForPublishAction(id), 'PUT', { data: serializedData });
  },

  urlForPublishAction(id) {
    return `${this.buildURL('article', id)}/publish`;
  }
});

And that’s it!

Wrapping up

Non-RESTful actions are not supported out of the box by Ember Data, but they are not that hard to implement when using adapters.

Edit: Thanks to Wesley Workman for mentioning adapterFor and suggesting using it in favour of owner / container API.

Comments