Ember Data is a wonderful tool, and frequently makes you feel like dealing with a REST or JSONAPI back end
is super easy, and downright magical. When your API conforms to the standards laid out by REST or JSONAPI,
you basically do not need any adapters or serializers, and can just define your attrs
, belongsTo
, and
hasMany
relationships, and things “just work”.
However, when things do not conform, as they should, and you have to customize things, it can get a bit hairy. I recently had a particular situation that was giving me a lot of trouble, so I reached out to the Ember Data Master himself, @runspired, for help.
We had a relationship like website -> page -> pageViewStats
, where each website could have multiple pages and each
page would have a single pageViewStats
object. However, we had an endpoint of the form
/api/v3/websites/{websiteId}/pageViewStats
, and this endpoint would return an array of objects
of type pageViewStat
.
This array would need to live on the website
model as a hasMany
relationship.
// models/website.js
import Model from 'ember-data/model';
import { hasMany } from 'ember-data/relationships';
export default Model.extend({
pageViewStats: hasMany('pageViewStat', {
async: true
})
});
Then, however, since we did not have a url that conformed to REST standards, we had to do some magic to
add this relationship into links
, which we did in the website serializer
. This technique was learned
from a blog post by
David Tang.
// serializers/website.js
import DS from 'ember-data';
export default DS.RESTSerializer.extend({
normalizeFindAllResponse(store, type, payload) {
payload.websites.forEach((website) => {
website.links = {
pageViewStats: `/api/v3/websites/${website.id}/pageViewStats`
};
});
return this._super(...arguments);
},
normalizeFindRecordResponse(store, type, payload) {
payload.website.links = {
pageViewStats: `/api/v3/websites/${website.id}/pageViewStats`
};
return this._super(...arguments);
}
});
This correctly setup our hasMany
relationship so if all we needed was that, we would be done, but we
did not actually consume the website.pageViewStats
array directly, rather we needed a belongsTo
relationship on a third model, page
, which would map to an entry in our hasMany
array where
page.id === pageViewStats.id
. Since we were not using the values from the hasMany
directly, and
never called get
on them, we had to make sure they were loaded in the model hook.
model(params) {
return this.store.findRecord('website', params.id)
.then(website => {
return website.hasMany('pageViewStats').load().then(() => {
return website;
});
});
},
With our data successfully loaded into the model and the hasMany
triggered, we could then be sure the data
was available, and ready to map to our belongsTo
relationship. We would need to define a belongsTo
in
the page
model and the inverse to it in the pageViewStat
model.
// models/page.js
import Model from 'ember-data/model';
import { belongsTo } from 'ember-data/relationships';
export default Model.extend({
pageViewStats: belongsTo('pageViewStat', { async: false })
});
//models/page-view-stat.js
import Model from 'ember-data/model';
import { belongsTo } from 'ember-data/relationships';
export default Model.extend({
page: belongsTo('page', { async: false })
});
Finally, we had to setup a relationship in the pageViewStat
serializer, by looping through the hasMany
records,
and setting the page
relationship on each of them.
// serializers/page-view-stat.js
import DS from 'ember-data';
export default DS.JSONSerializer.extend({
normalizeResponse(store, ModelClass, rawPayload, id, requestType) {
let normalized = this._super(
store,
ModelClass,
rawPayload,
id,
requestType
);
if (requestType === 'findHasMany') {
normalized.data.forEach((resource) => {
let r = (resource.relationships = resource.relationships || {});
r.page = { data: { type: 'page', id: resource.id } };
});
}
return normalized;
}
});
This allowed us to have one aggregated data set sent down from one API call, which populated the hasMany
,
and also allowed us to access the specific pageViewStat
records associated with each page
record by
just checking page.pageViewStats
, which accomplished our ultimate goal of displaying a table of page
records, and listing the pageViewStat
values for each. Keep in mind our endpoints were not correctly
REST or JSONAPI formatted, and neither of these relationships existed from our API, so we manually forced
them in.
Hopefully this helps someone struggling, as I did, with how to setup relationships for something that is not related at all from the back end!