Ember-Data REST Adapter Nested Urls
Currently Ember Data’s REST adapter sucks for highly custom urls. Moreover, the store api as it is not flexible enough to support complex and nested urls. This gist proposes an api for solving this.
Draft by: Amiel and Igor Terzic
A similar api was already proposed by Sidonath → https://github.com/emberjs/data/pull/1078
Background & Rationale
There has been much discussion about nested urls in ember data. And quite a few attempts at supporting it.
As it is now,
buildURL
only receives a type
and an id
(in the cases where it is provided).
A solution,
proposed by @rjackson, is to provide the record to
buildURL
as well.
However, this is only possible when creating, updating, or deleteing a record,
and not possible when finding records (store.find('model')
, store.find('model', id)
).
High level api
The RESTAdapter would accept a url template.
var CommentAdapter = RESTAdapter.extend({
url: 'posts/:post.id/comments/:id'
})
This would then work automagically for sending the record to the correct url.
It is easy to see that this can be made to work for record.save()
and record.delete()
because createRecord
, updateRecord
, and deleteRecord
operate on records, and we can just match the properties from the url slug to those on the record. The variables declared in the slug would be looked up on the record itself.
Find() is a bit more tricky, becuase at the time it’s called there is no record to be accessed. Thus the find api would be
store.find(type, id, context)
For example
store.find('comment', 1, {postId:15})
While this seems a bit unwieldy I would argue it’s actually a simple generalization of how find works. Right now find accepts type and id in order to know how to access the record’s url. Though type and id are somewhat special there is no good reason to limit the api to these two, especially as there are servers in the wild that need more context in order to return records.
You could imagine a more pure implementation of store.find
and the RESTAdapter that looked like
//Just a thought exercise, not an actual proposal!
var RESTAdapter = Adapter.extend({
url: "/:type/:id"
})
store.find({id:1, type:'user'})
Building block/lower level apis
Hopefully most users wouldn’t need to use/extend these
buildUrl: (context, requestType) -> string
BuildUrl would do the actually work of taking a context object, mapping it’s variables according to the url template and returning a url string.
We considered having a urlContextForRecord
which would allow you to have a context object passed to buildUrl that had additional data compared to the data available on the record. For example some APIs might need to prepend currentUserId
in the route, but they only keep the user id in the global session object. I believe that this is an antipattern and should intentionally be made hard to do. A record should have all the properties needed to construct a url available on itself. As a workaround, it seems easy to use a record/adapter hook or injection to set currentUserId
or all models if you needed to.
For the same reason, the properties passed to find as the context, would get set on the record.
var comment = store.find('comment', 1, {postId: 15})
comment.get('postId') -> 15
This would also allow out of the box support for pathological APIs which have relationships declared in the URL and not in the JSON.
##Open questions
There is tension between making the context passed in to find
super easy to use vs making the relationships work nicely.
With a basic post, comments setup and an adapter that looks like:
var CommentAdapter = RESTAdapter.extend({
url: '/posts/:post.id/comments/:id'
})
It would be nice if you could do,
var comment = store.find('comment', 1, {post_id: 15})
However, as it is, you would need to do:
var comment = store.find('comment', 1, {post:{id: 15}})
And even worse, for some cases it might require actually creating the post first in order to pass it in as the context
var post = store.find('post', 15)
var comment = store.find('comment', 15, {post:post})
##Alternative approaches considered
Compared to having the properties instantiated on the model directly, we also thought about making them live under a urlParams
key so they couldn’t be actual relationships/attributes:
var comment = store.find('comment', 1, {post_id: 15})
comment.get('urlParams.post_id') -> 15
comment.get('post') -> null
This seems like a worse api overall, and harder to implement as you need to make sure your record doesn’t get into an inconsitent state, where urlParams don’t match the actual attribtues on the model.