These are some notes on the workings of Ember-Data. They written from the perspective of migrating a Backbone app to Ember. Below, "Sierra" is the Backbone app, while "Tango" is its successor.
- central clearinghouse of data
- singleton
- understands how to fetch and save collections and objects
- one base instance (we use
ActiveModelAdapter
, which inherits fromRestAdapter
) - can override on per-model-class basis
In Sierra, URL calculation happens in the model:
class Sierra.Models.CustomerDetails extends Sierra.Model
url: -> "/customer/details/#{@get('id')}"
In Tango, that becomes a very simple CustomerDetailsAdapter
:
ApplicationAdapter.extend({
buildURL: function(_typeKey, id, _record) {
return '/customer/details/' + id;
}
});
In Sierra, there are some collection classes that represent subsets of
all of the type of model. For example, various collections of User
s:
class Sierra.Collections.NoServiceCustomers extends Sierra.Collections.Customers
url: '/metrics/no_service_customers'
class Sierra.Collections.NewCustomers extends Sierra.Collections.Customers
url: -> '/metrics/customers'
In Tango, there are no collection classes. Instead, store.find('user')
returns
all users (via GET /users/
), while store.findQuery('user', { page: 1 })
returns just the first page (via GET /users/?page=1
).
Unfortunately, there is no good solution in Ember-Data for this. I've opened an issue about it.
Invoice
s have URLs like
/billing/year/2015/month/02?no_list=true&explicit_customer_id=j832nfiwi2
.
Thus, the "key" for that would be [ '2015', '02', 'j832nfiwi2' ]
.
class Sierra.Models.Invoice extends Sierra.Model
url: ->
[year, month, customer_id] = [@get('year'), @get('month'), @get('customer_id')]
url = '/billing'
if year? and month?
url += "/year/#{year}/month/#{month}"
url += "?no_list=true"
if customer_id? and customer_id.length > 0
url += "&explicit_customer_id=#{customer_id}"
return url
In Tango, you might expect to be able to use a composite key:
Ember.Route.extend({
model: function(params) {
this.store.find('invoice', [ params.year, params.month, params.customer_id ]);
}
});
Unfortunately, Ember-Data doesn't like composite keys.
It needs keys to be Strings so it can use them as entries in a map. In
the above example, it thinks you're trying to find all Invoice
s with that
Array as some sort of malformed filter.
Instead, we need to encode the composite key as a string when we fetch:
Ember.Route.extend({
model: function(params) {
const id = [ params.year, params.month, params.customer_id ].join('--');
return this.store.find('invoice', id);
}
});
and then decode it in the adapter:
ApplicationAdapter.extend({
buildURL: function(_typeKey, encodedID, _record) {
const parts = encodedID.split('--');
var url = '/billing/year/%@/month/%@?no_list=true'.fmt(pars[0], parts[1]);
if (parts[2]) {
url += '&explicit_customer_id' + parts[2];
}
return url;
}
});
- understands how to turn API responses into Ember properties and vice versa
If the API returns a property -- say, type
-- that conflicts with some internal
property of Ember.Object
, DS.Model
, or another core class, then we can
convert the property name during serialization and deserialization. For example:
ApplicationSerializer.extend({
normalizePayload: function(payload) {
payload._type = payload.type;
delete payload.type;
return payload;
},
keyForAttribute: function(attr) {
if (attr === '_type') { return 'type'; }
return this._super(attr);
}
});
- in-page representation of server state
- best to keep small (smaller than Sierra models)
- can declare has-many or belongs-to relationships to other models
In Sierra, the Director
maps from an API field called type
to a more
readable type_name
:
class Sierra.Models.Director extends Sierra.ResourceModel
type_name: -> if @get('type') == 1 then "Random" else "Round-robin"
That's the sort of computed property that's a good fit for the model. In
an internationalized world, I might instead have the model property return
more "computer-friendly" symbolic names and convert them to human-friendly
strings in the UI layer (in a component or via the t
helper in a template):
DS.Model.extend({
typeName: function() {
return this.get('type') === 1 ? 'random' : 'round-robin';
}.property('type')
});
Ember.I18n.translations = {
'director.type.random': 'Random',
'director.type.round-robin': 'Round-Robin'
};
# Sierra.Models.User.prototype.verify_with_recovery_code
verify_with_recovery_code: (code, options) ->
Fastly.post '/two_factor_auth/verify_by_recovery_code', { code: code }, options
That belongs in a Service.
# Sierra.Models.PricingPlan.prototype.monthly_cost
monthly_cost: ->
cost = parseFloat @get('cost')
return 'Free' if cost == 0
"$#{cost} / mo"
That belongs in a Component or Helper.