Querying resource subsets with Ember Data


#1

I am trying to figure out how to leverage ember-data (1.0.0-beta.11) to consume the following API endpoints:

  • /artists/picasso
  • /artists/recent
  • /artists/popular
  • /artists/favorites

My router is pretty straightforward:

Router.map ->
  ...
  @resource 'artists', ->
    @route 'favorites'
    @route 'recent'
    @route 'following'
    @route 'popular'
 
  @resource 'artist', path: '/artists/:artist_id'

The route to view a single artist:

ArtistRoute = Ember.Route.extend(...mixins...,
  model: (params) ->
    @store.find('artist', params.artist_id)

This makes a request to /api/artists/:artist_id

I’m now trying to figure out how to formulate a call to the store to grab a simple subsection of the artists, let’s say the popular ones. I can obviously get it to hit the proper endpoint by pretending that the resource is singular like this:

ArtistsPopularRoute = Ember.Route.extend(...mixins...,
  model: ->
    @store.find('artist', 'popular')

but then the store expects a single record instead of an array.

I have seen other posts arguing that I should simply be filtering those records, but I personally find /api/artists?popular=true or api/artists?filter=popular to be quite awkward on the backend.

I’m willing to overwrite adapter methods, but am totally new to ember-data and hoping there’s someone who’s already run into this type of issue.


#2

Although I’m sure there will be a more conventional approach in the future, I wanted to share my approach and hopefully get some feedback.

I sketched out several possibilities, all of which involve a custom ArtistAdapter, and some of which involved changes to the model hook in ArtistsPopularRoute:

  1. override buildURL and find methods and have total control/responsibility over the entire flow. This kept the current syntax
@store.find('artist', 'popular')

but means I need to distinguish between the cases where I’m attempting to find a single resource (the artist resource at /artists/1) and grabbing a subset (get me the popular artists at /artists/popular). That implementation looked something like:

ArtistAdapter = DS.RESTAdapter.extend
  subsets: ['popular', 'recent', 'following']
  ...
  find: (store, type, id, record) =>
    if @get('subsets').contains(id)
      @findMany(store, type, id, record)
    else
      @_super(store, type, id, record)
  
  buildURL: (type, id, record) ->
    # keeping this simple...
    if id == 'popular' then '/api/artists/popular'
  ...

I had expected a call to findMany to simply work, as it should be using buildURL under the hood to resolve the URL (which it does), and then push the returned artists array onto the store.

Unfortunately, this doesn’t work, as Ember Data sets the requestType on the serializer based on the initial call (in our case find), which causes an exception to be raised when pushing to the store, as the store is expecting only a single Artist instead of the array we’re returning.

  1. call find, passing a pseudo model like this:
@store.find('popular')

At least in this case the store would expect an array back, and the real type could be surmised based on the route, but this would require overwriting find again, with a more complex version of buildURL. In addition, it is extremely surprising behavior if you’re familiar with the different ways of calling find, so I rejected this almost immediately.

  1. pass the subset as a query argument to find and define a custom findQuery

Although it is a private method, it has all the characteristics I cared about: no need to define custom serializer behavior, straightforward version of buildURL, and it seems flexible in case I add additional arguments such as pagination. Most importantly, it just works.

So the model hook now looks like this:

ArtistPopularRoute = Ember.Route.extend(...mixins...,
  model: ->
    @store.find('artist', { subset: 'popular'})
)

And the corresponding ArtistAdapter:

ArtistAdapter = DS.RESTAdapter.extend(
  subsets: Em.A(['popular', 'recent', 'following'])
  namespace: 'api'

  findQuery: (store, type, query) ->
    subset = query.subset
    delete query.subset
    @ajax(@buildURL(type.typeKey, subset), 'GET', { data: query })

  buildURL: (type, id, record) ->
    namespace = Ember.get(this, 'namespace')
    subsets = Ember.get(this, 'subsets')
    if subsets.contains(id)
      "#{namespace}/artists/#{id}"
    else
      @_super(type, id, record)
)

With this in place, I’m at least able to keep moving forward. I plan on extracting this behavior to a Mixin so I can simply define a subsets property on each route that relies on this behavior. While I’m happy that this appears to work, it’s probably not ideal.

If anyone has any feedback, I’d be happy to hear it.