Custom REST actions (non CRUD actions)


#1

Hi,

I searched the web for some ideas or solutions, on how to call custom actions on my server, but unfortunately I doesn’t find anything.

For everyone, who doesn’t know, what I mean, here some examples:

  • /api/v1/places/:id/upvote
  • /api/v1/places/:id/downvote
  • /something/:id/rating
  • /something/:id/source

After searching, I started to think about the problem and the solution could be something like:

model.action('upvote')
model.action('rating', ratingData)

The model could then call store and adapter as it does on save. Also it should return a promise similar to save. I think it would also be nice to update the model with the response data from the server, if there is any. My opinion is, that it should be implemented as separate library at the moment.

A first problem I discovered, is that some methods needed for that, are not public.

I would like to hear your opinions and hints about it. Maybe someone has interest to contribute on starting a project to create an extension for ember-data.

EDIT: fixed a mistake in my examples.


#2

Hi, I think this is wrong approach to REST. What you trying to do is RPC its not REST service. Here you could find more about RESTfull services Roy Thomas Fielding rest dissertation


#3

Why, in my example above is upvote a nested resource for place . Now I POST to route /api/v1/places/1/upvote, which means: 'create a new resource upvote for place with id=1'.


#4

Yeah, this is RPC, but don’t let that stop you, There are some good use cases for it. Like sending an email or completing a purchase, where you’d like to explicitly tell the server to do something more than save the data. But I’d stick with REST for your rating example.

It seems fairly straightforward to add it. Maybe copy Store.adapterFor into your own code.


#5

If that’s the case, then just define it as a relationship and create the record.

Place = DS.Model.extend({
  upvotes: hasMany()
});

Upvote = DS.Model.extend({
  place: belongsTo('place'),
  user: belongsTo('user')
});

place.upvotes.createRecord({ user: user });
place.save();

#6

Actually, I don’t think “upvote” is a nested resource. It should be a custom action. My opinion is even REST API should not stop you from creating a few custom actions. In real world sometimes we need some custom actions to do specific things but not just save the resource. And I think custom action also makes back-end API design and test easier.

In fact you can also consider voting things as nested resource. But then the recommend way is something like this:

GET    /api/v1/places/1/votes
POST   /api/v1/places/1/votes  # upvote
DELETE /api/v1/places/1/votes  # downvote

For your use case I think one custom action is just ok:

POST   /api/v1/places/1/vote
DELETE /api/v1/places/1/vote

Or you can just use the default REST actions:

PUT /api/v1/places?vote=1
PUT /api/v1/places?vote=-1

But I think it does more than one thing in a single action, which is not a good solution.

I’m not familiar with Ember Data. I guess to make custom action works you need to add methods for model, store and adapter. Because model.save calls store.scheduleSave. I think it won’t be a very complex task. We just need store to have a new API, and let adapter to build the url for custom actions. All serialize/deserialize things will be the same.


#7
Place = DS.Model.extend({
  votes: hasMany()
});

PlaceController = Ember.ObjectController.extend({
  actions: {
    upvote: function() {
      var user = ...,
          vote = user.votes.createRecord({ user: user });
    }
  }
});

#8

I think it’s kind of overkill. Thats easily could be done with simple REST no need for some custom actions over rest. If you need to add server side logic to voting, you should attach event listener to your ‘save’ database model method(I assume that you use some kind of ORM e.g. AR on your server side). Or you could use triggers in your database(if possible). Just KISs.


#9

@BFalkner: Thanks for showing your solution. The problem is, that you didn’t always want to have the same model structure on the front end as on the back end. With your solution you end up with hundreds of Upvote instances, that in the end are only used to count them up, to show the overall number of up-votes. But the aggregation should be done by the database.

@alekso: Yes, it’s possible to do it with REST, but then you have to store the whole model instance only to up- or down-vote this model. Another possible solution and maybe more ‘emberish’ way is to update the model instance model.incrementProperty('votes') and to let the adapter decide how to call the server. But then you always have to implement a custom ModelAdapter with custom logic.


#10

I don’t know if is the best approach but, I have a similar case and I try with the code below and works fine, (maybe the path or another improves could be implemented), any feedback is welcome :smile:

App.Render = DS.Model.extend
    path: "/renders"

    approve: (-> =>
          $.post("#{@get('path')}/#{@get('id')}/approve")
    ).property('id')

App.RenderController = Em.ArrayController.extend
    actions:
        approve:
            @get(state_action)().then(
                (data) =>
                    render = data.render
                    if render.state
                       @set('state', render.state)
             )

#12

One way would be to extend a base Model class allowing you to upvote like this:

upvote: ->
  @post('%@/upvote'.fmt @get 'id').then =>
    @incrementProperty 'vote'

This way you can re-use the existing adapter

Model = DS.Model.extend 

  _ajax: (url, method, options) ->
    type    = @get 'constructor'
    adapter = @get('store').adapterFor type
    url     = '%@/%@'.fmt adapter.buildURL(type.typeKey), url

    adapter.ajax url, method, options


  post: (url, options) ->
    @_ajax url, 'POST', options

#13

Thanks amk, it works great. The only trouble I have is that it doesn’t pickup the validation errors if the server returns 422. Normally, when calling save on the record, it works fine. Any tips on how to trigger the same behaviour? I tried to look around the ember-data source code but didn’t figure it out. Thanks!


#14

@HakubJozak Hi, that post is a year old. Recommended way now is to just make your ajax request and then push the data to the store