Create parent and child models in the same commit - ember.js

I have the following models:
App.Offers = DS.Model.extend({
name: DS.attr('string'),
createdBy: DS.belongsTo('App.Users'),
products: DS.hasMany('App.Products'),
startDate: DS.attr('string'),
endDate: DS.attr('string')
}
App.Products = DS.Model.extend({
description: DS.attr('string'),
img: DS.attr('string'),
offer: DS.belongsTo('App.Offers'),
}
Using these two models I create both an offer (parent) and a product (child):
var offer = App.Offers.createRecord({/*fields*/}),
product = App.Products.createRecord({/*fields*/});
offer.get('products').pushObject(product);
offer.get("store").commit();
When I do this the problem is that the parent needs the id of the child and the child the id of the parent in order to set its FK.
I looked up issues and PRs in the ember-data repo and I found this: https://github.com/emberjs/data/pull/440 . It suggests wrapping createRecord in a waitForParents function which basically creates the parent and then the child. While I have tried their suggestion I still can't create my records. The problem is that even though the request for the parent is made first, it still needs the id of the child (which is not created yet). The parent request is send with the following payload:
{/*other_fields*/,"products":["/api/v1/products//"]}
Note the missing id of the product in the url.

I managed to fix it by following the suggestion in this PR: https://github.com/emberjs/data/issues/437. They basically suggested creating a waitForParents function and wrap it around createRecord. This function checks if there's a belongsTo relationship and defers the code execution for that creation until the parent is created (by adding an observer to the id field).
In my case I had to override addHasMany in the serializer as well.
I hope that helps other people with the same problem.

Related

Create a record for a model other than the current routes model

I have an items route that lists items, when I click one I go to an item route that shows the one item. Pretty standard and I've set it up such that Ember magic is taking care of the dynamic part of the item route.
In the item route I have an action handler that I want to create a bid on that item, items have many bids and bids belong to one item and one user.
In my item route I've tried just calling
var newBid = this.store.createRecord('bid', {
user_id: 29,
item_id: 2565,
bid_amt: 600
});
newBid.save().then(function(bid){
console.log('saved bid\n');
console.log(bid);
}).catch(function(reason){
console.log(reason)
});
But in the console I'm getting
Uncaught TypeError: DS.default.Attr is not a function
Is there a way to do this with ember-data without having to switch to a straight Ajax call?
EDIT:
My adapter looks like this
export default DS.RESTAdapter.extend({
namespace: 'api'
});
The item model looks like this
export default DS.Model.extend({
...
canceled: DS.attr('boolean'),
bid: DS.hasMany('bid')
});
User model looks like this
export default DS.Model.extend({
bid: DS.hasMany('bid')
});
And the bid model looks like this
export default DS.Model.extend({
...
user: DS.belongsTo('user'),
item: DS.belongsTo('item')
});
I took out all the other attributes on the models as the models are quite large and I don't think they matter, there are no defaults being set for any of them.
In ember Data you'll always have to pass the whole model, but not only the id, to a related item.
So in your case you'd have to do something like:
let user = this.store.peekRecord('user',29);
let item = this.store.peekRecord('item', 2565);
var newBid = this.store.createRecord('bid', {
user: user,
item: item,
bid_amt: 600
});
Note, that if the requested records are not yet in your store, but need to be fetched from the server you'd have to call findrecords instead of peekRecords and you'll receive a promise only. In that case you'll have to wait for the promises to resolve before creating the new record!!!

Ember, the right way of creating interface for saving records with hasmany relationships

I have these models:
Gmcontrolpanel.Offer = DS.Model.extend({
name: DS.attr('string'),
date: DS.attr('string'),
duration: DS.attr('number'),
products: DS.hasMany('product', {async: true}),
});
Gmcontrolpanel.Product = DS.Model.extend({
name: DS.attr('string'),
description: DS.attr('string'),
offer: DS.belongsTo('offer'),
variations: DS.hasMany('variation', {async: true})
});
Gmcontrolpanel.Variation = DS.Model.extend({
description: DS.attr('string'),
quantity: DS.attr('number'),
price: DS.attr('string'),
product: DS.belongsTo('product')
});
I'm trying to build a reusable interface for creating/editing an offer; i've made two separate views for inserting a product and inserting a variation;
the product view has a + and a - button to add or remove products, and the same for variation view;
the model for the route is:
model: function() {
return this.get('store').createRecord('offer');
}
What I want is that, when clicking on save button, all (offer, products and variations) are saved;
First of all: which one is the best way of implementing this? containerViews? collectionViews or {{#each}} loops?
And then, how can I create the child records and bind them to the input fields on the child views? I mean: I can create a new product record every time a productView is inserted and the same for variations, but when saving how can I get all these records and set properly all the relationships fields?
Here is a skeleton example of how to set up the relationship:
var newOffer= store.createRecord('offer', {name:....});
//or you can do newOffer.set('name',...);
var newVariation = store.createRecord('variation', {description:.....});
var newProduct = store.createRecord('product', {name:..., description:.....});
newProduct.get('variations').pushObject(newVariation);
newOffer.get('products').pushObject(newProduct);
But for saving the model and persisting it in db, there is one slight problem. Saves are per model, so even when you have the relationship set up properly when we do save on offer model, it doesnot embed the data associated with hasMany relationed models. So we could do something like this :
Note: I have read about bulk save but haven't tried it yet - you might want to give it a shot but if that didnt work then i would do save on each model from bottom up like
newVariation.save().then(function(){
newProduct.get('variations').pushObject(newVariation);
//since the variation model is already saved, it has id associated with the model
//so ember data now knows that it should set variations as variations:[id of the variation model we just saved] when sending post request for product
newProduct.save().then{ //same pattern as above }
}, function(){//handle failure}
Here the case was simple, we had just one variation and one product but you may have multiple of them. We can do rsvp.all to sync up the promises for saves but it is bit sluggish becuse you have to make separate api calls for each save and since you may have multiple variations and products, the no of ajax calls can be bit insane. One way of getting around this would be that you create your own json structure by looping through the models, and combine the models into single json and make a single post request with jQuery ajax api call, save the content in your db and then make use of pushPayload(
http://emberjs.com/api/data/classes/DS.Store.html#method_pushPayload) to load up all the data back to the store.
This is what i have done in similar situation but there might be more elgant solutions out there, so i would wait on more opinions on this.
As for the view thing, i would think you would need a view for product only, this is what im thinking:
//offer.hbs
Bunch of form elemnts to update name description
+ - buttons to add product
{{#each product}}
{{#view productView product=this}}//this is just a normal view
{{/each}}
// your template associated with productView will be like
+ - buttons to add variations to product
{{#each product.variations}}
Show form elments to update name and description
{{/each}}
How about if we give this a try in setupController
setupController:function(controller, model){
model.reload().then(function(data){controller.set('model', data);});
}
Or, what if you create an action to transition to edit mode and in that action you reload the model first and within its then hook, you do the transitionToRoute with reloaded model data. Something like:
goToEdit: function(model){
model.reload().then(function(data){transitionToRoute('offer.edit', data});
}

EmberData belongsTo not working with just ID

I am trying to create a belongsTo relation, but i am always getting the following error:
Error while loading route: Error: Assertion Failed: You must include
an id in a hash passed to push
My model definition looks like this:
GambifyApp.Bet = DS.Model.extend({
scoreT1: DS.attr('string'),
scoreT2: DS.attr('string'),
user: DS.belongsTo('user')
});
Within my Json return I have simply
{
id:128433,
user:8926,
points:0,
game:94,
scoreT1:2,
scoreT2:2
}
The user value under user is my user id. Regarding Documentation(http://emberjs.com/guides/models/the-rest-adapter/#toc_relationships) it should look exactly like this. But its causing me this error. If I change my "user" property to an attribute everything is working fine.
Update:
Found the problem in my serializer that is extracting all relations and adds them as sideloaded models. Of course it was not handling the case where this relation is only an id instread of the whole object.
If you aren't including the data associated with user the relationship should be async.
GambifyApp.Bet = DS.Model.extend({
scoreT1: DS.attr('string'),
scoreT2: DS.attr('string'),
user: DS.belongsTo('user', {async:true})
});

Why do I need to "reopenClass" to set the url for an ember-data model?

I found that if I try to include the url in the original definition of an ember-data model it blows up in my REST adapter but if I simply "reopenClass" it's fine.
What is the technical reason behind this? (below is the working example)
CodeCamp.Speaker = DS.Model.extend({
id: DS.attr('number'),
name: DS.attr('string'),
session: DS.belongsTo('CodeCamp.Session')
});
CodeCamp.Speaker.reopenClass({
url: 'sessions/%#/speakers'
});
Calling extend on an object sets instance attributes, whereas reopenClass sets class attributes.
The url attribute is a class-level attribute,
Ember.get(CodeCamp.Speaker, 'url')
as opposed to:
speaker = CodeCamp.Speaker.createObject()
Ember.get(speaker, 'name')
Note also that you can extend an instance by using simply reopen. Emberjs' docu contains an example which you find at http://emberjs.com/guides/object-model/reopening-classes-and-instances/

How to change Record on client directly, without transaction

I have a very common situation, if there is a term for it, well I am not aware of it then.
A record is having fields: id, enabled, text, etc...
and having POST /record/enable to enable or disable record, as it invoke is bigger process on server.
So, once callback from normal POST is received, I want to update record.enabled to true locally, which should not be part of any transaction. and should be updated directly.
How to achieve this?? Or what is better alternative for such requirement?
I think something along these lines should do:
App.PObject = DS.Model.extend({
id: DS.attr('number'),
name: DS.attr('string'),
nestedObject: function() {
if (!this.nestedObj) {
this.nestedObj = Ember.Object.create({
active: false
});
}
return this.nestedObj
}.property()
});
As i have seen so far, Model is dirty only when its attributes, that are defined as DS.attr, change.
EDIT: Realized later that, This solution doesn't work.
Proper term of my problem is: transient field
Transient field of an Object is ignored for serialization/deserialization (like Java ORMs ignores transient fields) http://en.wikipedia.org/wiki/Transient_(computer_programming
I have figured out a solution. Its really simple.
App.PObject = DS.Model.extend({
id: DS.attr('number'),
name: DS.attr('string'),
//will not be part of Ember-data persistence, but will be populated on GET
enabled: null
});
Object (Record) properties which are not attributes are ignored.