Skip to content

Instantly share code, notes, and snippets.

@jamesarosen
Created March 6, 2015 19:48
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save jamesarosen/12f1c1c1775d3bd9cd87 to your computer and use it in GitHub Desktop.
Ember-Data Notes

Ember Data Notes

A Little Background

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.

Store

  • central clearinghouse of data
  • singleton

Adapter

  • understands how to fetch and save collections and objects
  • one base instance (we use ActiveModelAdapter, which inherits from RestAdapter)
  • can override on per-model-class basis

Pattern: Customize URL

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;
  }
});

Challenging Pattern: Subset Collections

In Sierra, there are some collection classes that represent subsets of all of the type of model. For example, various collections of Users:

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.

Pattern: Compound Keys

Invoices 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 Invoices 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;
  }
});

Serializer

  • understands how to turn API responses into Ember properties and vice versa

Example: Avoid Reserved Property Names

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);
  }
});

Model

  • 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

Example: Director Types

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'
};

Anti-Pattern: API Access in Model

# 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.

Anti-Pattern: Presentation Logic in Model

# 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment