Ember {embedded: 'always' } on model Vs Serializer - ember.js

I was reading through Ember docs and some examples on working with Embedded Object like JSON in Ember.
I came across the EmbeddedRecordsMixin feature and saw that we can write code like below to tell it is embedded record.
import DS from 'ember-data';
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs: {
author: { embedded: 'always' },
}
});
Qouting the below from Ember page
Note that this use of { embedded: 'always' } is unrelated to the { embedded: 'always' } that is defined as an option on DS.attr as part of defining a model while working with the ActiveModelSerializer. Nevertheless, using { embedded: 'always' } as an option to DS.attr is not a valid way to setup embedded records.
And i have also seen model written like this.
App.Child = DS.Model.extend({
name: DS.attr('string'),
toys: DS.hasMany('toy', {embedded: 'always'}),
});
Where child object has toys object embedded.
Going by the first example, can i write the child serailizer as below?
App.ChildSerializer = DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs: {
toys: {embedded: 'always'}
}
});
Can someone help me understand the difference between these two {embedded: 'always'} and what to use when?
Thanks

Short answer: yes you can and you should.
Well, as far as I know, ember (especialy ember-data) is build to work perfectly with a Rails backEnd.
Rails have a module called ActiveModelSerializer to serialize resources and their related attributes and relationships. Into this module, you can use an option embedded: 'always' to serialize the whole targeted relationship and not only the ids when your client ask for a ressource.
If you use it Rails side (server), you can handle it Ember side (client) by putting this option into your model if you want your ember-data store to handle it easily. It's just an 'echo' to this ActiveModelSerializer module functionality.
On the other side, if for example you create/update an object with many relationships, there is 2 ways to deal with it. The first is to first save object's relationships and then, on success, save the object itself. The second is to send it at once to your server with the option {embedded: 'always'} into your model's serializer, into the relationship you want to send at the same time (embedded) at the object itself.
Ember probably encourage to use this into the serializer, because putting this into model seems only related to a specific Rails option, and it's not straightforward at all. Moreover, putting this into the serializer fulfill this role, with or without ActiveModelSerializer.
Hope it's clear for you, if not, let me know so I can edit this.

Related

Can't get relationships with Ember Mirage

I'm currently writing tests for my App written with EmberJS. I'm using Mirage.
I have the two following models:
mirage/models/paperwork.js
export default Model.extend({
customer: belongsTo('customer'),
paperwork_products: hasMany('paperwork-product', { inverse: 'paperwork' }),
mirage/models/paperwork-product.js
export default Model.extend({
paperwork: belongsTo('paperwork', { inverse: 'paperwork_products' }),
});
In my scenario, I'm creating my datas like this:
const paperwork = server.create('paperwork');
const paperworkProduct = server.create('paperwork-product', { paperwork });
paperwork.paperwork_products.add(paperworkProduct);
My route:
export default ApplicationRoute.extend({
model(params) {
return this.store.findRecord('paperwork', params.paperwork_id, { include: 'paperwork_products' }),
},
});
The problem is that I can't access paperwork.paperwork_products in my template. It's undefined (other paperwork attributes are here, but not relationship). I already even tried to put a debugger in my mirage/config.js when routes are declared. My paperwork exists, and his "paperwork_products" too. But I can't get paperwork_products data in my template.
What am I doing wrong ? I think I must change something in my :
this.get('v1/paperworks/:id');
But I don't know what ...
Thanks in advance !
Edit: Here are my real Ember models:
models/paperwork.js
export default DS.Model.extend({
customer: DS.belongsTo('customer'),
paperwork_products: DS.hasMany('paperwork-product', { async: true }),
});
models/paperwork-product.js
export default DS.Model.extend({
paperwork: DS.belongsTo('paperwork'),
});
Yesterday I tried to compare the real JsonApi response from my back, and Mirage response, and I saw that in the relationships hash, my relationship "paperwork_products" was changed to paperwork-products (with Mirage). So there is a problem with relationships with an underscore or models with dash ...
In config.js, I tried to mock JSONAPI Backend, and it works wells. Just replaced "paperwork-products" by "paperwork_products"
Mirage response :
"relationships":{
"customer":{
"data":{
"type":"customers",
"id":"1"
}
},
"paperwork-products":{
"data":[
{
"type":"paperwork-products",
"id":"1"
}
]
}
}
Should be :
"relationships":{
"customer":{
"data":{
"type":"customers",
"id":"1"
}
},
"paperwork_products":{
"data":[
{
"type":"paperwork_products",
"id":"1"
}
]
}
}
My other models with hasMany relationships do not have any problems.
To confirm, do you have Ember Data models setup with the same relationships? Without those, things may. It work very well ...
If you do, could you post those models as well?
Also, as an FYI, Mirage 0.3.0 comes with an auto-sync setup that will read your Ember Data models and create corresponding Mirage models without any work. It's been lovely ...
Edit:
I would suggest you rework your Ember Data model to use camel cased relationships. If you do the following:
models/paperwork.js
export default DS.Model.extend({
customer: DS.belongsTo('customer'),
paperworkProducts: DS.hasMany('paperwork-product', { async: true }),
});
I would expect it to work without issue, as Ember Data automatically translates camelCased relationships to the appropriate JSON-API key
Does that work for you?

Could someone explain how async works with belongsTo/hasMany relationships?

I had this code in my ember app:
var FooModel = DS.Model.extend({
bars: DS.hasMany( 'bar', { async: true, inverse: 'foo'} )
});
var BarModel = DS.Model.extend({
foo: DS.belongsTo( 'foo', { async: true, inverse: 'bars'} )
});
Edit: Using
Ember : 1.13.7
Ember Data : 1.13.8
But when I went to render foo.bars, they wouldn't be loaded unless I used the browser back and forward buttons. Reloading the page would cause the foo.bars to disappear again.
When I changed the code to this:
var FooModel = DS.Model.extend({
bars: DS.hasMany( 'bar', { async: true } )
});
var BarModel = DS.Model.extend({
});
Everything works as it should, and I'm just really confused as to why. Especially since I took that original code from another ember app where it was working just fine (although there might have been some adapter/serializer magic going on that I don't know about). Edit: The app where it was working is using
Ember : 1.4.0
Ember Data : 1.0.0-beta.7+canary.b45e23ba
Handlebars : 1.3.0
Edit: Using REST adapter for both
Async in relatioships
{ async: true} is the default value for relationships in Ember since 1.13 which means that in most cases you define the relationship without specifying the async value at all. It means that related records will not be loaded into the store until required. This is almost always the desired way to go since it prevents blocking by returning a promise. Remember that promises require a different way of programming since the process no longer is procedural step by step through the code. You manage promises by chaining a .then(function(param){// handle fulfilled promise here});.
There are cases where { async: false} could be beneficial. For example in a one to one relationship and you wanted both sides of the relationship to be loaded immediately.
Why your code does not work
I don't know for sure but it seems that the code as you have written it and it could be a bug in the code. The same as with async above you also do not actually need to specify the inverse value here either. According to Ember documentation:
Ember Data will do its best to discover which relationships map to one another. Explicit Inverses
Your application as a simple one to many relationship should work fine simply with:
var FooModel = DS.Model.extend({
bars: DS.hasMany( 'bar' )
});
var BarModel = DS.Model.extend({
foo: DS.belongsTo( 'foo' )
});
I have replied a similar question What is an “async relationship”? / {async: true} vs. {async: false}
Async relationships (default)
Accessing the relationship will return a promise.
post.get('comments').then((comments) => {
// now we can work with the comments
});
If the data is not available and the relationship is accessed (for example, from a template or a "consumed" computed property), Ember Data will automatically fetch the resources.
Read more about Relationships as Promises
Sync relationships (from docs)
Ember Data resolves sync relationships with the related resources
available in its local store, hence it is expected these resources
to be loaded before or along-side the primary resource.
BelongsTo
export default DS.Model.extend({
post: DS.belongsTo('post', {
async: false
})
});
In contrast to async relationship, accessing a sync relationship
will always return the record (Model instance) for the existing
local resource, or null. But it will error on access when
a related resource is known to exist and it has not been loaded.
let post = comment.get('post');
HasMany
export default DS.Model.extend({
comments: DS.hasMany('comment', {
async: false
})
});
In contrast to async relationship, accessing a sync relationship
will always return a DS.ManyArray instance
containing the existing local resources. But it will error on access
when any of the known related resources have not been loaded.
post.get('comments').forEach((comment) => {
});
If you are using links with sync relationships, you have to use
ref.reload to fetch the resources.

EmberJS embedded items in payload JSONAPI

Ember : 1.13.3
Ember Data : 1.13.5
jQuery : 1.11.3
I am trying to send a JSON payload using ember-data from my EmberJS client to my server. I want to send the entire object graph to the server on saving the project, as I don't want to send multiple requests. I wouldn't mind sending multiple requests, but I am worried about what happens if one of the requests fails in the middle and the data on the server will not be correct.
I wanted to use JSONAPI (http://jsonapi.org/format/#document-compound-documents) as that is becoming the default adapter in Ember. Also, there is a few C# libraries that handle this format, so I thought it would be quite straightforward. However, after reading the spec, it seems that I cannot embed objects if they do not have an id. EmberJS also does not attach the child objects to the JSON either even though I have specified { async: false, embedded: 'always' }) on the DS.attr.
My question is: If an application is used in such a way that an object graph is created on the client side, how do you use JSONAPI format to send the entire object graph to the server? Do I have to generate ids on the client side to satisfy the JSONAPI standard? Then once they get to the server just ignore them so they get saved with an id generated by the ORM?
Here is my labelGroup model:
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string'),
labels: DS.hasMany('label-model', { async: false, embedded: 'always' })
});
Here is my project model:
import DS from 'ember-data';
export default DS.Model.extend(DS.EmbeddedRecordsMixin, {
name: DS.attr('string'),
labelGroups: DS.hasMany('labelGroup', { async: false, embedded: 'always'})
});
Here is the POST that I get after doing a save() on the project:
{
"data":{
"attributes":{"name":"Project"},
"relationships":{
"label-groups":{
"data":[
{"type":"label-groups","id":null},
{"type":"label-groups","id":null},
{"type":"label-groups","id":null},
{"type":"label-groups","id":null},
{"type":"label-groups","id":null},
{"type":"label-groups","id":null},
{"type":"label-groups","id":null}
]
}
},
"type":"label-projects"
}
}
UPDATE: I tried using https://www.npmjs.com/package/ember-cli-uuid to generate client side ids which it has. However the data getting output does not include the extra objects, only a reference to their ids. I expected to see an "included" property as specified here:http://jsonapi.org/format/#document-compound-documents, but it is not there.
{
"data":{
"id":"7b4544ee-91cd-493d-8b10-52040e68c283",
"attributes":{"name":"Project"},
"relationships":{
"label-groups":{
"data":[
{"type":"label-groups","id":"08115273-e82a-4d46-93ea-232ce071fb78"},
{"type":"label-groups","id":"9ca94fe9-8077-411e-98d2-1694c6fecce4"},
{"type":"label-groups","id":"d629f1e8-7962-404d-8034-38229ab21f77"},
{"type":"label-groups","id":"c6bda655-5489-4760-847b-bf02239bb2c5"},
{"type":"label-groups","id":"f6fef249-2d1d-43f0-ba64-24b7ff8b5637"},
{"type":"label-groups","id":"a7db25bf-52c8-477b-83e4-64e7c76b072e"},
{"type":"label-groups","id":"f3b5fbb3-261a-4b3d-b481-b9352f8ce2d6"}
]
}
},
"type":"label-projects"
}
}
Ember-data has no support for what you want at the moment. So ember-data will not save your relationships data in a save payload.
But its possible to do this your own by using a custom adapter and serializer.
I strongly recommend you to checkout the API and then look into the source.
If you call .save() on your Model the createRecord method is called on your adapter.
Here serializeIntoHash on the serializer is called to serialize the Model.
serializeIntoHash calls serialize, where serializeBelongsTo and serializeHasMany is called.
Now you can just override serializeHasMany and modify the hasMany before the line:
json[payloadKey] = hasMany;
Here you have the type and the ids as they are sent by ember-data. You could just .forEach the data on the hasMany and then fetch the store for the data and build your included array.
I hope this helps you to understand the serializer and the adapter so you can modify it to do whatever you want pretty easy. Actually this is the best part about ember-data. The structure of the adapter and the serializer, which allows easy modifications.

Ember Data - Handle API error responses for a hasmany relationship that is accessed through a template

I have a model that is being accessed through a hasmany relationship from a handlebars template
models/post.js
export default DS.Model.extend({
comments: DS.hasMany('comment', {async: true}),
});
template/post.hbs
{{#each comment in post.comments}}
I'm using the REST adapter and need to handle success and failure responses for the GET /posts/:id/comments that is being done from the handlebars template helper.
If this was being done in a controller/route, I could just do something like:
this.store.find('post', post_id)
.then(function(post) {
post.get('comments);
}, function(errors) {
// handle errors
});
Where do I do the equivalent when accessed through the template? Any help or words or wisdom are very much appreciated!
Looks like the hooks for this would be available at the adapter level but I decided to explicitly load the model in the controller instead of relying on the templates.

How to access Ember Model from the Route in Ember CLI

Perhaps doing this with regular Ember application having all the code sitting in app.js would have been much easier for me. But since I'm using Ember CLI I'm having trouble accessing my model in my Route. I'm still trying to learn how to use Ember CLI so please help me out.
As I just want to fire AJAX calls and get the data to render on my UI, I downloaded and added Ember Model library to my project. I don't see a need of using Ember Data. This is the Ember Model documentation I'm referring: https://github.com/ebryn/ember-model#example-usage. With that said, here's my directory structure that Ember CLI proposed:
|-app
|-controllers
| |-customers.js
|-models
| |-customers.js
|-routes
| |-customers.js
|-templates
| |-customers.hbs
|-app.js
|-index.html
|-main.js
|-router.js
This is much simpler representation of the actual project structure that I have just to focus on the problem. As proposed in Ember Model documentation I added following code to my Customers model (model\customers.js):
export default Ember.Model.extend({
nameid: attr(),
firstname: attr(),
middlename: attr(),
lastname: attr(),
prefixname: attr(),
suffixname: attr()
});
this.url = "http://restapi/api/customers";
this.adapter = Ember.RESTAdapter.create();
Notice that I had to do the "export default" instead of "App.Customers = Ember.Model.extend...". This is the Ember CLI convention. So when I try to access the model I created in my Customers Route I get error "Error while loading route: ReferenceError: App is not defined"..
Customers Route code:
export default Ember.Route.extend({
model: function () {
App.Customers.find();
},
actions: {
addnew: function(){
//logic of saving edited customer
alert('customer created!');
}
}
});
I tried this.model() - Returns an object of type supperWrapper and this.modelFor() - Returns null.
Please suggest how to get an handle of my model in its route so that I can perform CRUD operations provided out-of-the-box by Ember Model.
Thanks!
I suggest you change the file name of the model to singular e.g. customer.js.
If you want to access the model class within the route file you have to import the model. Since Ember CLI uses ES6 module syntax you can't / shouldn't access anything directly on the App object. This should be done via import statements or Ember internally via the resolver.
import Customer from "../models/customer";
Now you can use it in the model hook. There is also another error in your example code, you have to return the promise from the find call.
model: function () {
return Customer.find();
},
I'm curious why you picked Ember Model over Ember Data, because for this example you would need less code with Ember Data and it would be more like the Ember way AFAIK.