RestModel is a library for interacting with a REST API in Ember. Its goal is to provide a library with predictable behavior and which can be easily extended to meet custom needs.
This fork of RESTModel is no longer maintained and will not receive future updates. Use ember-data instead.
bower install heroku/rest-model --save
Using RestModel is as simple as extending it to create a custom model class, and providing a URL for that model:
var App = RestModel.extend().reopenClass({
url: '/apps'
});
App
can now be used to fetch resources from the /apps
endpoint:
// Fetch all apps (GET /apps):
App.all().then(function(apps) {
apps[0].constructor === App;
});
// Fetch a single app (GET /apps/:id):
App.find(1).then(function(app) {
app.constructor === App;
});
App
s can also be created, fetched, updated, and destroyed:
var existingApp = App.create({ id: params.id });
existingApp.fetch().then(function(existingApp) {
existingApp.get('name') === 'existing-app-name';
});
var newApp = App.create({ name: 'new-app-name' });
newApp.save().then(function(newApp) {
newApp.get('id') === 2;
return newApp.delete();
}).then(function() {
// newApp has been deleted
});
Working with nested resources is as simple as providing a nested URL with placeholder segments, and providing parent objects and keys for each record:
var Post = RestModel.extend().reopenClass({
url: '/posts'
});
var Comment = RestModel.extend().reopenClass({
url: '/posts/:post/comments'
});
var post = Post.create({ id: 1 });
Comment.all(post).then(function(comments) { // GET /posts/1/comments
comments[0].constructor === Comment;
});
Comment.find(post, 2).then(function(comment) { // GET /posts/1/comments/2
comment.constructor === Comment;
});
var comment = Comment.create({ parents: [post], body: 'hello' });
comment.save().then(function(comment) { // POST /posts/1/comments
comment.constructor === Comment;
});
There is no concept of belongs-to/has-many in RestModel. All models are managed
individually, and parents
can be used on any record—their primary keys
will be interpolated as is appropriate into the URL.
If resources are behind a custom namespace, one can be provided via the
namespace
property on the class:
var Model = RestModel.extend().reopenClass({
namespace: 'api'
});
var App = Model.extend().reopenClass({
url: '/apps'
});
App.all(); // GET `/api/apps`
Your API may allow you to find resources by, for example, both id
and name
.
RestModel supports this via the primaryKeys
array property on the class, which
defaults to ['id']
. Any time RestModel needs a primary key to fetch or save a
record, it will iterate over these keys in order until it finds one for which
the model has a value.
var App = RestModel.extend().reopenClass({
url: '/apps',
primaryKeys: ['id', 'name']
});
Now, say a user visits /apps/my-app
in your Ember app. Your route will want
to fetch the App
model for them, and RestModel can fetch it by name
, since
that value is provided in primaryKeys
:
var AppsRoute = Ember.Route.extend({
model: function(params) {
return App.create({ name: params.name }).fetch();
}
});
All subsequent API requests for that App
instance will be made using id
,
assuming your API returns this property.
RestModel provides an isDirty
property on each instance that returns true
if
attributes on the record have been changed from their original values. When a
record is successfully save()
d, the new values are considered the "original
values".
In order to determine isDirty
, RestModel requires that you provide an array
called attrs
that contains the attributes to dirty check against:
var App = RestModel.extend({
attrs: ['name']
}).reopenClass({
url: '/apps'
});
var app = App.create({ name: 'foo' });
app.get('isDirty'); // false
app.set('name', 'bar');
app.get('isDirty'); // true
Assuming that a record has an attrs
array defined, it can be reverted to its
original values if it has changed. Remember that changing a record and then
saving it will cause the record to consider its new properties its "original
properties", and it won't revert.
var app = App.create({ name: 'foo' });
app.get('name'); // 'foo'
app.set('name', 'bar');
app.get('name'); // 'bar'
app.revert();
app.get('name'); // 'foo'
When a record is saved or updated, #serialize
is called on it, which is a
method that returns a JSON string. By default, this method will either serialize
any properties in a serializedProperties
property array on the record, or it
will simply call JSON.stringify(this)
if no such property exists.
This method can be overridden easily, as long as it returns a JSON string.
When a record is returned from the API, ::deserialize
is called on its class,
with the API response object as the argument. By default, this method simply
calls return this.create(object)
, returning an instance of the class.
When an array is returned, ::deserializeArray
is called on its class, with the
API response array as the argument. By default, this simply returns a map of
calling ::deserialize
with each member of the array.
These methods can be overridden for custom API response deserialization.
Each class can choose to implement a getBeforeSend
function. This function
should return a function whose single argument is a jQuery XMLHttpRequest
object (jqXHR
). Custom request headers can be added and removed here, as it
will be called on every AJAX request for this class.
The getBeforeSend
method itself receives an object of options, including
things like the request method (e.g. 'GET'
) to be used by the impending AJAX
request.
var App = RestModel.extend().reopenClass({
url: '/apps',
getBeforeSend: function(options) {
return function(jqXHR) {
jqXHR.setRequestHeader('foo', 'bar');
}
}
})
Before a release, RestModel should be built with a non-uglified and an uglified
version into its dist
directory:
npm install
npm run build
$ npm install
$ bower install
$ npm test
While I created and maintain this project, it was done while I was an employee of Heroku on the Human Interfaces Team, and they were kind enough to allow me to open source the work. Heroku is awesome.