polymorphic hasMany and belongsTo relationships in ember-data rev 12 - ember.js

I am having trouble implementing what I understand to be a polymorphic relationship using ember-data rev12.
I have the following models:
App.Project = DS.Model.extend
lists: DS.hasMany('App.List', { polymorphic: true })
App.Proposal = DS.Model.extend
lists: DS.hasMany('App.List', { polymorphic: true })
App.Employee = DS.Model.extend
lists: DS.hasMany('App.List', { polymorphic: true })
App.List = DS.Model.extend
name: DS.attr('string')
#project: DS.belongsTo('App.Project', { polymorphic: true })
And I am trying to create a new list from the project router like so.
App.ProjectRoute = Ember.Route.extend
events:
newList: (project) ->
lists = project.get('lists')
list = App.List.createRecord(name: 'list1')
lists.pushObject(list)
#store.commit()
But the request to the server is setting the polymorphic keys incorrectly.
I was expecting the payload to look like:
{ list: { name: list1, listable_type: project, listable_id: 100 } }
But got:
{ list: { name: list1, project_type: project, project_id: 100 } }
What am I missing? Is there a way to define the polymorphic type or key?.
Here is my temporary hack
https://gist.github.com/arenoir/5409904

First thing, based on the current models you have, you don't need to use polymorphic associations. And you may not want to set the polymorphic option on both end of the relationship.
If you want to have your payload to contain listable, you just need to rename your attribute:
App.List = DS.Model.extend
name: DS.attr('string')
listable: DS.belongsTo('App.Project', { polymorphic: true })
UPDATE
Based on my understanding of your classes, it is a List that can belong to different types. So you should define your models like this:
App.Listable = DS.Model.extend
lists: DS.hasMany('App.List')
App.Project = App.Listable.extend
App.Proposal = App.Listable.extend
App.Employee = App.Listable.extend
App.List = DS.Model.extend
name: DS.attr('string')
listable: DS.belongsTo('App.Listable', { polymorphic: true })
and the payload will look like this:
{list: {name: "list1", listable_type: 'project', listable_id: 100}}
I don't know if you also want a list to be added to several listables at the same time. Based on the names of your models, I'm tempted to believe that it's not what you want: a list should contain different objects (project, proposal, employee). and not a project can have multiple lists.

Related

Why is a "ManyToOne" relation treated differently when serializing in ember-data?

My question is highly related to "Ember Data: Saving relationships" but I don't care about embedding - I just want a working OneToMany (bi-directional) relationship. Take for example the following models:
App.Child = DS.Model.extend({
name: DS.attr('string'),
toys: DS.hasMany('toy'),
});
App.Toy = DS.Model.extend({
name: DS.attr('string'),
child: DS.belongsTo('child')
});
and the following object creations/saves:
var store = this.get('store');
store.createRecord('child', {name: 'Herbert'}).save().then(child => {
return store.createRecord('toy', {name: 'Kazoo', child: child}).save())
}).then(toy => {
child.get('toys').pushObject(toy);
return child.save();
});
I would expect the child, when serialized, to reference the toy. E.g. something like
{
'name': 'Herbert',
'toys': [ 1 ]
}
But it doesn't. Because this is a "manyToOne" relation ship and ember-data won't serialize these: https://github.com/emberjs/data/blob/v1.0.0-beta.18/packages/ember-data/lib/serializers/json-serializer.js#L656
If you make it a ManyToNone relation by removing the belongsTo it will work but you will lose the back reference.
Why is this special behaviour? Why is ManyToOne that different from ManyToNne or ManyToMany that it deserves such special treatment?
Where is this behaviour documented? I totally missed it and assumed it was a bug in the Serializer / Adapter I'm using.
What is the correct way to achieve my desired serialization?
I ended up creating my own, trivially modified serializer:
import EmberPouch from 'ember-pouch';
export default EmberPouch.Serializer.extend({
serializeHasMany: function(snapshot, json, relationship) {
// make sure hasMany relationships are serialized.
// http://stackoverflow.com/questions/20714858/ember-data-saving-relationships
var key = relationship.key;
if (this._canSerialize(key)) {
var payloadKey;
// if provided, use the mapping provided by `attrs` in
// the serializer
payloadKey = this._getMappedKey(key);
if (payloadKey === key && this.keyForRelationship) {
payloadKey = this.keyForRelationship(key, "hasMany", "serialize");
}
var relationshipType = snapshot.type.determineRelationshipType(relationship);
if (relationshipType === 'manyToNone' || relationshipType === 'manyToMany' || relationshipType === "manyToOne") {
json[payloadKey] = snapshot.hasMany(key, { ids: true });
// TODO support for polymorphic manyToNone and manyToMany relationships
}
}
},
});
the main difference being that it also accepts manyToOne relations,
and use it in application/adapter.js:
export default EmberPouch.Adapter.extend({
defaultSerializer: "pouchserial",
....
In this specific (pouch) case it would probably be better to store the reference to the parent on the child only so the parent doesn't need updates when children are added (which also avoids conflicts).

Ember-data: How do I get an aggregated list of models in a collection on a different collection (#each not working)

I am trying to get an aggregated list of employees. Each organization is a model that hasMany employees.
var Organization = DS.Model.extend({
name: attr('string'),
employees: hasMany('employee', { async: true })
});
On my controller, I have this property.
employees: function(){
var employees =[];
this.get('organizations').forEach(function(organization){
employees.pushObjects(organization.get('employees'));
});
return employees;
}.property('organizations.#each.employees.isFulfilled')
When I loop through these on the template, the employees are being loaded, I can see the api returning the data (Im using async: true), but the return value of employees is still an empty array.
It seems like I might be listening to the wrong thing on the property. Please help.
Ember doesn't support dependent properties multiple levels deep.
http://emberjs.com/guides/object-model/computed-properties-and-aggregate-data/
And your employees being an async property will be empty when you attempt to push in this fashion. You'd need to move the fulfilled employees a level higher (something like this):
var Organization = DS.Model.extend({
name: attr('string'),
employees: hasMany('employee', { async: true }),
loadedEmployees: function(){
return this.get('employees');
}.property('employees.[]')
});
With a computed property using loadedEmployees
employees: function(){
var employees =[];
this.get('organizations').forEach(function(organization){
employees.pushObjects(organization.get('loadedEmployees'));
});
return employees;
}.property('organizations.#each.loadedEmployees')

Ember Data: How do I know if a relationship was changed?

I'm writing a custom data adapter (doesn't talk to webserver) and I need to know whether hasMany relationship was changed or not to persist relationship data.
App.Note = DS.Model.extend({
content: attr('string'),
createdAt: attr('date'),
tags: DS.hasMany('tag', { async: true })
});
App.Tag = DS.Model.extend({
name: DS.attr('string'),
notes: DS.hasMany('note')
});
I need to implement adapter's updateRecord:
updateRecord: function(store, type, record) {
// record.changedAttributes() returns changed attributes but not relationships
}
In my case list of tags attached to note may change (via note.get('tags').addObject(..) / removeObject(..)) so I need to get a diff. What's the best way to do that? I can remove all tags in a database and insert new tags but that's not efficient at all

Ember-data polymorphic associations

Has anybody come up with an answer for polymorphic associations and ember-data?
We would need some way of being able to query the type at the other end of the relationship from what I can tell.
Anybody any thoughts on this?
With the latest ember-data build you can now use polymorphic associations:
You need to configure your Models to make it polymorphic:
/* polymorphic hasMany */
App.User = DS.Model.extend({
messages: DS.hasMany(App.Message, {polymorphic: true})
});
App.Message = DS.Model.extend({
created_at: DS.attr('date'),
user: DS.belongsTo(App.User)
});
App.Post = App.Message.extend({
title: DS.attr('string')
});
/* polymorphic belongsTo */
App.Comment = App.Message.extend({
body: DS.attr('string'),
message: DS.belongsTo(App.Message, {polymorphic: true})
});
You also need to configure alias properties on your RESTAdapter
DS.RESTAdapter.configure('App.Post' {
alias: 'post'
});
DS.RESTAdapter.configure('App.Comment' {
alias: 'comment'
});
The result expected from your server should be like this:
{
user: {
id: 3,
// For a polymorphic hasMany
messages: [
{id: 1, type: "post"},
{id: 1, type: "comment"}
]
},
comment: {
id: 1,
// For a polymorphic belongsTo
message_id: 1,
message_type: "post"
}
}
More information in this github thread
So I have something. It's not finished, or entirely clean, but it works. Basically, I use a mixin to bypass the Ember associations entirely. I'm sure that this could be rolled into the adapter or the store, but for now this works.
Polymorphic models come through the the JSON with an itemId and itemType:
App.Follow = DS.Model.extend
user: DS.belongsTo('App.User')
itemId: DS.attr("number")
itemType: DS.attr("string")
I add a mixin to the models that are associated with it :
App.Hashtag = DS.Model.extend App.Polymorphicable,
follows:(->
name: DS.attr("string")
#polymorphicFilter(App.Follow, "Hashtag")
).property('changeCount') #changeCount gives us something to bind to
followers: (->
#get('follows').map((item)->item.get('user'))
).property('follows')
The mixin implements three methods, one that updates the changeCount, one that returns the model's type and the polymorphicFilter method that filters a model by itemType and id:
App.Polymorphicable = Ember.Mixin.create
changeCount: 1
polymorphicFilter: (model, itemType)->
App.store.filter model,
(data) =>
if data.get('itemId')
#get('id') is data.get('itemId').toString() and data.get('itemType') is itemType
itemType:()->
#constructor.toString().split('.')[1]
updatePolymorphicRelationships:()->
#incrementProperty('changeCount')
The controller layer is protected from all this jankyness, except for having to call updatePolymorphicRelationship to make sure the bindings fire:
App.HashtagController = Ember.ObjectController.extend
follow:()->
App.Follow.createRecord({
user: #get('currentUserController.content')
itemId: #get('id')
itemType: #get('content').itemType()
})
#this provides a way to bind and update. Could be refactored into a didSave()
#callback on the polymorphic model.
#get('content').updatePolymorphicRelationships()
App.store.commit()
That's what I have so far. I'm trying to keep things in the model layer as it's just one step removed from the adapter layer. If it looks like Ember Data is not going to look at polymorphics at all in future, then it would make sense to pull this all up to a higher level, but for now, this works and leaves my controllers (relatively) clean.
Polymorphic associations are now supported in ember data
https://github.com/emberjs/data/commit/e4f7c3707217c6ccc0453deee9ecb34bd65c28b9

How to use Ember-Data associations in conjunction with an ArrayProxy

I am trying to migrated my app to using Ember-Data as it's persistence mechanism. One thing that strikes me is that I'm not sure if it's still possible to use an arrayProxy for aggregate properties of a hasMany association. In my previous iteration I didn't have any explicit associations, just controllers tied together by specific properties. Now I'd like to take advantage of the association functionality in ember-data, but I am getting errors when I trie to bind the content of my array proxy to the "children" property of the DS.Model. My code is below and there is a jsfiddle here: http://jsfiddle.net/sohara/7p6gb/22/
The error I get is:
Uncaught TypeError: Object App.recipeController.content.ingredients has no method 'addArrayObserver'
I would like to be able to retain a controller layer, even if the data associations are controlleed at the model level. It'd also (ideally) like the child objects to be embedded in the json representation of the parent object in order to avoid multiple server requests.
window.App = Ember.Application.create();
App.store = DS.Store.create({
revision: 3,
adapter: DS.fixtureAdapter
});
App.Ingredient = DS.Model.extend({
name: DS.attr('string'),
price: DS.attr('string')
});
App.Recipe = DS.Model.extend({
name: DS.attr('string'),
ingredients: DS.hasMany('App.Ingredient', {embedded: true} )
});
App.Recipe.FIXTURES = [
{id: 1, name: 'Pizza', ingredients: [{id: 1, name: 'tomato sauce', price: 2, recipeId: 1}]}
];
App.recipeController = Ember.Object.create({
content: App.store.find(App.Recipe, 1)
});
App.ingredientsController = Ember.ArrayProxy.create({
content: 'App.recipeController.content.ingredients',
totalWeigth: function() {
var price = 0;
items = this.get('content');
items.forEach(function(item) {
weight += price;
});
}.property()
});
In App.ingredientsController you need to have contentBinding: 'App.recipeController.content.ingredients', instead of content: ...