Ember-Data - Recursive relationship - ember.js

I'm using ember-app-kit with ember-data. I have the following two models created using ember-data. Node has a recursive relationship and Tag has a reference to the Node.
Node = DS.Model.extend({
name: DS.attr('string'),
createdAt: DS.attr('date'),
tags: DS.hasMany('tag', {async: true}),
children: DS.hasMany('node', {async: true, inverse: 'parent'}),
parent: DS.belongsTo('node', {async: true, inverse: 'children'})
});
Tag = DS.Model.extend({
name: DS.attr('string'),
parent: DS.belongsTo('node', {async: true})
});
Whenever I try to reassign a tag to a different parent the changes do not persist.
NodeController = Ember.Controller.extend({
actions: {
update: function(newParentNode) {
var node = model;
node.set('parent', newParentNode);
node.save().then(function(updatedNode){
updatedNode.get('parent');// returns null
}); //doesn't work
}
}
});
I found a similar question but the difference is that Tag also has a relationship to Node. Is it possible to update a parent-child relationship by only updating the child?
Update #1:
When examining the output from ember-data in the console I noticed that nothing about the parent child relationship is communicated on the save. The request payload to the backend is sent as the following:
{ "node": { "name": "node4", "createdAt": null } }
Nothing about the parent/child relationship is transmitted. However if I reassign a tag to a different node the backend receives the following:
{"tag": {"name":"tag123", "parent":"66c8ec8c-3790-45b2-8669-e9581102376d"} }
I tried simplifying my node model by removing the inverses. This allowed me to reassign parents, but ember-data started to send the children relationship on the requests as the means of updating the relationship.
{ "node": {
"name": "node4",
"createdAt": null,
"children": ["66c8ec8c-3790-45b2-8669-e9581102376d"]
}
}
While this gets the job done, it is not ideal. As there could many children and sending the ids of every child will bloat the request payload. Is there no way for ember-data to send the updated info of child with its new parent relationship key? Also this change caused error messages to start being raised within Qunit.
You defined the 'parent' relationship on (subclass of DS.Model), but multiple possible inverse relationships of type (subclass of DS.Model) were found on (subclass of DS.Model)

Related

Ember Data with hasMany relation on embedded data

I'm using Ember 2.4.1 and trying to build the hierarchical tree with self reference model.
Here is the my Ember model
// app/models/category.js
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string'),
parent: DS.belongsTo('category', { inverse: 'children' }),
children: DS.hasMany('category', { embedded: 'always', async: false, inverse: 'parent' })
});
And server response in route /categories with embedded data (I use RESTAdapter on Ember side):
{"categories":[
{
"id":4,
"name":"Root element w/o children",
"type":"Category",
"children":[]
},
{
"id":5,
"name":"Root element with a child",
"type":"Category",
"children":[
{
"id":6,
"name":"A child",
"type":"Category",
"children":[]
}
]
}
]}
As you can see Category with id 5 has a child with id 6 and it embedded. But while Ember loading the page it makes an error: Assertion Failed: You looked up the 'children' relationship on a 'category' with id 5 but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async (DS.hasMany({ async: true }))
If I include the category with id 6 in JSON-root like {categories: [{id:4, …}, {id:5, …}, {id:6, …}]} error will disappear but I don't know why I need embedding in this case. May be I don't understand how embedding works in Ember.
So I'd like to ask advice how to make properly work Ember Data with embedded response without duplicating it and without async: true.
UPD #TheCompiler answered on my question in comments. Adding serializer with mixin solve my problem:
// app/serializers/category.js
import DS from 'ember-data';
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs: {
children: { embedded: 'always' }
}
});
No need to duplicate the record. The Rest Adapter just doesn't play nice with nested JSON objects. Try formatting the data like this:
{"categories":[
{
"id":4,
"name":"Root element w/o children",
"type":"Category",
"children":[]
},
{
"id":5,
"name":"Root element with a child",
"type":"Category",
"children":[6]
},
{
"id":6,
"name":"A child",
"type":"Category",
"children":[]
}
]}

Ember.js how to properly initialize localstorage-adapter? (hasMany with async: true is not persisted)

I'm using ember.js with localstorage-adaper.js.
My problem started with an already answered question: EmberJS - record with hasMany relation fails to load
So having the models:
// Models
App.List = DS.Model.extend({
name: DS.attr('string'),
items: DS.hasMany('item')
});
App.Item = DS.Model.extend({
name: DS.attr('string') ,
list: DS.belongsTo('list')
});
When the #/list/1 template got rendered the the items weren't shown on the page and an assertion failed was thrown in the console:
Assertion failed: You looked up the 'items' relationship on 'App.List:ember236:1' but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async (DS.attr({ async: true }))
As specified, the solution is to make the hasMany async like this:
App.List = DS.Model.extend({
name: DS.attr('string'),
items: DS.hasMany('item',{async:true})
});
It works great for that scenario!
Next:
I'm loading data from the server and push it into the store when the application first loads like this: (You can find the JSBin for the example here: http://jsbin.com/lejizo/1/)
var data = { //in real life this is fetched through an AJAX request
'list': { id: '1', name: 'The List', items: ['1','2'] },
'items': {
'1': { id: '1', name: 'item 1', list: '1' },
'2': { id: '2', name: 'item 2', list: '1' }
}
};
...
this.store.push('list', data.list).save();
this.store.pushMany('item', data.items).forEach(function (item) {
item.save();
});
After setting the async:true option, I've noticed that the items ids are not persisted anymore into the localstorage. The JSON looks like this:
{"App.List":{"records":{"1":{"id":"1","name":"The List","items":[]}}},"App.Item":{"records":{}}}
Obviously the items are not shown since there is no reference to them.
I might think that I should find another way to populate the locastorage at first! How?
Or there is another workaround for this situation?
Working with aync:true on hasMany relationships beside the foreign keys not being persisted to the localstorae, also causes additional problems inside controllers. You get the Promise, not the actual object, so you always must use list.get('items').then(/*code here*/) (check out this question I've posted Ember local-storage How to use hasMany with async: true in controllers?), which in some scenarios where you use it inside a loop might cause a stackoverflow.
Using localstorage, you'll allways have all the data on the client side. It really makes no sense to work with async:true. The problem is that ember expects an array of vanilla objects for the list.items, instead of just ids. This is described here DS.FixtureAdapter loses fixture data with hasMany async attributes
There is an easy workaround for getting rid of async:true in your hasMany relationships. Please note that you have all needed data in localstorage! Ember doesn't throw the: "Assertion failed: You looked up the 'items' relationship on 'App.List:ember236:1' but some of the associated records were not loaded..." error anymore if he sees the items inside the memory store.
SOLUTION:
Ember.Route.reopen({
beforeModel: function(transition){
this.store.find('list');
this.store.find('items');
}
});
We override the Ember.Route object, so that in the beforeModel hook we call store.find('object'). What this does is forces Ember to load data from localstorage to the "in memory" store! You won't need the async:true anymore and no error will be thrown. Also, at first initialization the foreign keys will be persisted too!
You have to do this everytime (in the Route super class) because you'll never know on which route a refresh will occur. Also, for a store of ~10 models with up to 50 records, it runs in ~50-70ms. If this seems to much in your scenario, make sure you do this call only on the route you want and only for the models it needs.
Also, if you override beforeModel inside your routes, make sure you call
this._super(transition)
Hope this helps!
You defined your data wrong, try this:
var data = { //in real life this is fetched through an AJAX request
'list': { id: '1', name: 'The List', items: [1,2] },
'items': [
{ id: '1', name: 'item 1', list: '1' },
{ id: '2', name: 'item 2', list: '1' }
}];
}
First you do not need a index 1,2,... in front of your items.
Second hasMany relationship should be return an array that is why items is wrap as an array.
So to fix your issue you either have to fix the data from the server, or write a serializer to massage the data.
Hope it helps!

Link to a nested emberData object in emberJS

I'm using emberData and I have the following model
App.Product = DS.Model.extend({
page_title: DS.attr('string'),
image: DS.attr('string'),
shop: DS.belongsTo('App.Shop', {embedded: true}),
style: (function() {
return "background-image:url('" + this.get("image") + "')";
})
});
The JSON data looks like this:
{
id: 1,
image: 'imageUrl',
shop: {
id: 2,
name: 'shopName'
}
}
In my template I want to link to the a page to display the shop
<img {{bindAttr src="image"}}>
{{#linkTo "shop" shop}}Store{{/linkTo}}
Unfortunately it links to http://localhost:3000/#/shop/undefined
It doesn't make sense to embed the model that it belongsTo. The breaking changes document states that you embed objects in a parent object:
From BREAKING_CHANGES.md
Embedded in Parent
An adapter can save one-to-many relationships by embedding IDs (or
records) in the parent object. In this case, the relationship is not
considered acknowledged until both the old parent and new parent have
acknowledged the change.
In this case, the adapter should keep track of the old parent and new
parent, and acknowledge the relationship change once both have
acknowledged. If one of the two sides does not exist (e.g. the new
parent does not exist because of nulling out the belongs-to
relationship), the adapter should acknowledge the relationship once
the other side has acknowledged.
Your fixture must be:
App.Product.FIXTURES = [{
id: 1,
image: "imageUrl",
shop_id: 2
}];

How to fetch embedded objects in one request with ember-data and couchdb-adapter

I am working on an application using ember.js and a couch DB backend. So far, i used ember-resource as database driver, but I am considering switching to ember-data, since this seems to be more sustainable.
Since I am working with couch DB, I am using the Couch DB-Adapter.
The documents in my database contain complete object structures, so I have to specify embedded objects in the database driver.
But although I am specifying my sub-objects as embedded, ember-data seems to fetch these objects with separate requests, instead of just getting them out of the main json.
My object definitions are as follows:
App.UserProfile = DS.Model.extend({
type: DS.attr('string'),
fullname: DS.attr('string'),
email: DS.attr('string'),
pictureUrl: DS.attr('string'),
social: DS.hasMany('App.SocialWebAccount', { embedded: true }),
.....
});
App.SocialWebAccount = DS.Model.extend({
profile: DS.belongsTo('CaiMan.UserProfile'),
site: DS.attr('string'),
account: DS.attr('string'),
.....
});
and the server data ist something like this:
{
"_id": "thoherr",
"_rev": "55-d4abcb745b42fe61f1a2f3b31c461cce",
"type": "UserProfile",
"fullname": "Thomas Herrmann",
"email": "test#thoherr.de",
"pictureUrl": "",
"social": [
{
"site": "socialFacebook",
"account": "thoherr"
},
{
"site": "socialXing",
"account": "Thomas_Herrmann7"
},
{
"site": "socialEmail",
"account": "test#thoherr.de"
}
]
}
After loading, the UserProfile does contain an ArrayProxy for my social data, which is populated by three entries, but they are all undefined instead of instances of SocialWebAccount!
If i try to access this array, ember-data seems to do a separate database access to fetch the data, which then leads to an error because the couch DB-adapter accesses an _id field, which is not available in undefined....
What am i missing?
I thought the "embedded" flag signals that the data is already in the json and the objects can be instantiated from the json?
Why does ember-data try to fetch the embedded data?
Any hint?
It seems that the embedded option has changed recently. I found some information in the test files on the ember-data github.
In these test files, the embedded content is defined like this
Comment = App.Comment = DS.Model.extend({
title: attr('string'),
user: DS.belongsTo(User)
});
Post = App.Post = DS.Model.extend({
title: attr('string'),
comments: DS.hasMany(Comment)
});
Adapter = DS.RESTAdapter.extend();
Adapter.map(Comment, {
user: { embedded: 'always' }
});
or
Adapter = DS.RESTAdapter.extend();
Adapter.map(Comment, {
user: { embedded: 'load' }
});
'always' seems to be used for embedded data without ids (your case),eg
id: 1,
title: "Why not use a more lightweight solution?",
user: {
name: "mongodb_expert"
}
'load' seems to be used for embedded data with ids
id: 1,
user: {
id: 2,
name: "Yehuda Katz"
}
Hoping it will help in your particular case. I've had a lot of trouble with hasMany relationships recently (I had to modify my adapter to get it work)

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