Ember-data Serialize/Deserialize embedded records on 3rd level - ember.js

Pardon me for coming up with this title but I really don't know how to ask this so I'll just explain.
Model: Group (has) User (has) Post
Defined as:
// models/group.js
name: DS.attr('string'),
// models/user.js
name: DS.attr('string'),
group: DS.belongsTo('group')
// models/post.js
name: DS.attr('string'),
user: DS.belongsTo('user'),
When I request /posts, my server returns this embedded record:
{
"posts": [
{
"id": 1,
"name": "Whitey",
"user": {
"id": 1,
"name": "User 1",
"group": 2
}
}
]
}
Notice that the group didn't have the group record but an id instead.
With my serializers:
// serializers/user.js
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs: {
group: {embedded: 'always'}
}
});
// serializers/post.js
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs: {
user: {embedded: 'always'}
}
});
This expects that the User and Post model have embedded records in them. However, it entails a problem since in the json response doesn't have an embedded group record.
The question is, is there a way I can disable embedding records in the 3rd level?
Please help.

Here is a good article about serialization:
http://www.toptal.com/emberjs/a-thorough-guide-to-ember-data#embeddedRecordsMixin
Ember docs:
http://emberjs.com/api/data/classes/DS.EmbeddedRecordsMixin.html
Basically, what it says is that you have 2 options 1) serialize and 2) deserialize. Those two have 3 options:
'no' - don't include any data,
'id' or 'ids' - include id(s),
'records' - include data.
When you write {embedded: 'always'} this is shorthand for: {serialize: 'records', deserialize: 'records'}.
If you don't want the relationship sent at all write: {serialize: false}.
The Ember defaults for EmbeddedRecordsMixin are as follows:
BelongsTo: {serialize:'id', deserialize:'id'}
HasMany: {serialize:false, deserialize:'ids'}

Related

EmberJS hasMany doesn't fetch relationships

I have a problem with serializing my response which looks like:
{
"data": [
{
"id": 930,
"uniqueId": "0d3a04cb-231c-4998-b4d3-9436a0a3138e",
"name": "DRINKI",
"lastEditDate": "2018-02-12T13:30:32",
"lastEditDateUTC": "2018-02-12T12:30:32",
"deleted": false,
"discountable": true,
"productCategoryPointOfSales": []
},
{
"id": 921,
"uniqueId": "5fbf423a-4932-47ca-b32f-5d3612dd73ee",
"name": "BALOTYNKI SOLO",
"lastEditDate": "2019-02-07T14:20:15",
"lastEditDateUTC": "2019-02-07T13:20:15",
"deleted": false,
"label": "",
"color": "#a0a5a9",
"discountable": true,
"productCategoryPointOfSales": [
{
"id": 142,
"pointOfSaleUniqueId": "98e370f2-9d37-4473-9446-d82e442593fe",
"directionId": 54,
"directionUniqueId": "f0c986c0-ef85-4a46-86ea-cd997981fe8a",
"kitchenUniqueId": "f0c986c0-ef85-4a46-86ea-cd997981fe8a",
"inactive": false
}
]
}
],
"total": 0
}
And the error I get:
Encountered a relationship identifier without a type for the hasMany relationship 'productCategoryPointOfSales' on <category:5fbf423a-4932-47ca-b32f-5d3612dd73ee>, expected a json-api identifier with type 'product-category-point-of-sale' but found '{"id":"142","pointOfSaleUniqueId":"98e370f2-9d37-4473-9446-d82e442593fe","directionId":54,"directionUniqueId":"f0c986c0-ef85-4a46-86ea-cd997981fe8a","kitchenUniqueId":"f0c986c0-ef85-4a46-86ea-cd997981fe8a","inactive":false}'. Please check your serializer and make sure it is serializing the relationship payload into a JSON API format.
Models:
export default DS.Model.extend({
productCategoryPointOfSales: DS.hasMany('product-category-point-of-sale'),
uniqueId: DS.attr('string'),
name: DS.attr('string'),
label: DS.attr('string'),
color: DS.attr('string'),
discountable: DS.attr('boolean')
});
export default DS.Model.extend({
category: DS.belongsTo('category'),
pointOfSaleUniqueId: DS.attr('string'),
directionId: DS.attr('string'),
directionUniqueId: DS.attr('string'),
kitchenUniqueId: DS.attr('string'),
inactive: DS.attr('boolean')
});
And my serializer:
export default DS.RESTSerializer.extend(EmbeddedRecordMixin, {
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
payload = {
category: payload.data,
};
return this._super(store, primaryModelClass, payload, id, requestType);
},
primaryKey: 'uniqueId',
attrs: {
productCategoryPointOfSales: {embedded: 'always'}
}
});
I'm very new to EmberJS and have no idea how to solve this problem. I followed some tutorials and tried with EmbeddedRecordMixin but it didn't help me. Could you please help me figuring this out?
Your api payload doesn't match, to what ember-data's default JSONAPISerializer expects (no type attribute => missing type error).
You posted a custom serializer based on RESTSerializer, but it seems not to be in the right place, so ember-data still uses the default JSONAPISerializer. Also you might be better of with a JSONSerializer.
As your payload has different attributes for record id (uniqueIdand pointOfSaleUniqueId) you have to create a custom serializers per model, to set different primaryKey and embedded records specifics.
I've created an ember-twiddle example, it has three serializers.
An application serializer, as default:
// /app/serializers/application.js
import JSONSerializer from 'ember-data/serializers/json';
export default JSONSerializer.extend({
// use uniqueId as ember-data model id
primaryKey: 'uniqueId',
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
// extract data from payload, so JSONSerializer finds the records
let normalizedPayload = payload.data;
// call the JSONSerializer.normalizeResponse with the extracted payload
return this._super(store, primaryModelClass, normalizedPayload, id, requestType);
}
});
For category model, to support the embedded productCategoryPointOfSales, extend the application serializer and add EmbeddedRecordsMixin:
// /app/serializers/category.js
import ApplicationSerializer from './application';
import EmbeddedRecordsMixin from 'ember-data/serializers/embedded-records-mixin';
export default ApplicationSerializer.extend(EmbeddedRecordsMixin, {
attrs: {
productCategoryPointOfSales: { embedded: 'always' }
}
});
For productCategoryPointOfSale, to use a different primaryKey:
// /app/serializers/product-category-point-of-sale.js
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
primaryKey: 'pointOfSaleUniqueId'
});

Reflexive relation with nested data

I'm sorry if this is a basic question, but since I'm quite new to ember, I'd like to know if there is any best practice for a case like this. For example, I have the follow endpoints that returns the payloads below:
https://api.example.com/v1/user
[
{
"user": "user1",
"firstName": "Foo1",
"lastName": "Bar1",
"url": "https://api.example.com/v1/user/user1"
},
{
"user": "user2",
"firstName": "Foo2",
"lastName": "Bar2",
"url": "https://api.example.com/v1/user/user2"
}
]
And each of the "url" endpoint returns something like this:
https://api.example.com/v1/user/user1
{
"user": "user1",
"firstName": "Foo1",
"lastName": "Bar1",
"age": 21,
"address": "User1 Address"
... more info ...
}
We see that some properties in "/user" are repeated in "/user/user1".
What would be the best practice to create the "user" model?
Should I have two models? Like for example a "users" model for the "/user" and a "user" model for "/user/user1"?
Could somehow have just one model "user" that would fit both endpoints?
Thanks in advance!
This is almost the use case described in the one-to-one docs where you're defining the user data with one model and linking another model with a belongsTo attribute:
// app/models/user.js
import DS from 'ember-data';
export default DS.Model.extend({
user: DS.attr('string'),
firstName: DS.attr('string'),
lastName: DS.attr('string'),
url: DS.attr('string'),
profile: DS.belongsTo('profile')
});
then setup a profile model with any extra values you're wanting to add and define the belongsTo attribute also:
// app/models/profile.js
import DS from 'ember-data';
export default DS.Model.extend({
age: DS.attr('string'),
address: DS.attr('string'),
user: DS.belongsTo('user')
});
In your routes file you'll want to setup the user id to define your URL structure like so:
//app/router.js
Router.map(function() {
this.route('users');
this.route('user', { path: '/user/:user_id' });
});
Then finally you'll need to load the data retrieving the related records and loading them in via your route file.
// app/routes/user.js
import Route from '#ember/routing/route';
export default Route.extend({
model(params) {
return this.store.findRecord('user', params.user_id, {include: 'profile'});
}
});
It's worth pointing out that you may also need a serializer to massage the data into the format you're wanting.

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-data model relationships with array

I am having a hard time to understand how the model relationships works in ember-data. I understand the concept one to many or one to one or many to many, but I don't understand how to use it properly..
My API is sending me this data :
{
gameweek: [
{
commonID: '23',
content: 'blablabla',
game: [
{
commonID: '23',
gameID: 4,
title: 'first game'
},
{
commonID: '23',
gameID: 8,
title: 'second game'
}
]
},
{
commonID: '24',
content: 'blebleble'
game: [
{
commonID: '24',
gameID: 12,
title: 'another game'
}
]
}
]
}
As you can see I receive an array that contain some data and an other array.
I don't really know if how I should create my models, should I have just one model ? or multiple like this ? (correct me if its wrong) :
//gameweek.js
import DS from "ember-data";
export default DS.Model.extend({
commonID: DS.attr('string'),
title: DS.attr('string'),
games: DS.hasMany('game')
});
//game.js
import DS from "ember-data";
export default DS.Model.extend({
commonID: DS.attr('string'),
title: DS.attr('string'),
gameweek: DS.belongsTo('gameweek')
});
I would like to be able to save my arrays in the store and keep the relationships between them.
If I do a this.store.find('gameweek', {commonID: '23'} ); I would like to get also all of the game that are related the gameweek. (the commonID would be the same if they are related).
Do I have to create a custom serializer ?
So many questions, thanks for you help !
=============================
UPDATE :
I tried to extend the DS.RESTSerializer like this :
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
extractArray: function( store, type, record ) {
var gameweek = record.predictions;
gameweek.forEach(function( entry ) {
var data = entry.games.map(function(game) {
return game.gameID;
});
entry.games = data;
});
record = { prediction: gameweek };
return this._super( store, type, record );
}
});
This is mainly replacing my game array by an array of gameID, the new array looks like this :
{
gameweek: [
{
commonID: '23',
content: 'blablabla',
game: ["4", "8"]
},
{
commonID: '24',
content: 'blebleble'
game: ["12"]
}
]
}
But I get this error :
Error: Assertion Failed: Unable to find transform for 'integer'
I am not sure what to do here.
===================================
UPDATE2:
I tried this too :
//serializers/gameweek.js
import DS from "ember-data";
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
primaryKey: 'gameWeekID',
attrs: {
game: { embedded: 'load' }
},
extractArray: function( store, type, record ) {
// The array of object isn't at the root structure of the record
var record = record.predictions;
record = { prediction: record };
return this._super( store, type, record );
}
});
//serializers/game.js
import DS from "ember-data";
export default DS.RESTSerializer.extend({
primaryKey: 'gameID'
});
I got this error :
Error: Assertion Failed: Ember Data expected a number or string to represent the record(s) in the "games" relationship instead it found an object. If this is a polymorphic relationship please specify a "type" key. If this is an embedded relationship please include the "DS.EmbeddedRecordsMixin" and specify the "games" property in your serializer's attrs object.
In ember you dont save arrays as models you save objects as models. Ember data expects data back in a certain way so you will need to either do this server side or do it with a serializer.
You can save relationships no problem if your models are set clearly.
In the code below taken from embers site your model is a post. so when you query for a post this is what should be returned. The post has a relationship with comments. posts have many comments and comments belong to posts. In the post model there is the id for each comment that is related to that post and then the comments are included as a separate array. http://guides.emberjs.com/v1.12.0/models/the-rest-adapter/#toc_sideloaded-relationships
{
"post": {
"id": 1,
"title": "Node is not omakase",
"comments": [1, 2, 3]
},
"comments": [{
"id": 1,
"body": "But is it _lightweight_ omakase?"
},
{
"id": 2,
"body": "I for one welcome our new omakase overlords"
},
{
"id": 3,
"body": "Put me on the fast track to a delicious dinner"
}]
}
The related model files for the above structure are
app/models/post.js
export default DS.Model.extend({
title: DS.attr('string'),
comments: DS.hasMany('comment')
});
app/models/comment.js
export default DS.Model.extend({
body: DS.attr('string'),
post: DS.belongsTo('post')
});
I dont know if its just my head or its the end of the day but the above naming and structure layout is very confusing. Can you include a bit more detail or make the details less generic, it might help with trying to figure the data out.
You should have separate models for gameweek and game. Ember Data works better with more granular models, rather than trying to deal with model fields which are raw objects. This will make it easy to extend the game model in the future with additional functionality, such as computed properties and validators. So your proposed model structure is fine.
To make this work with your current JSON structure, however, you're going to have to declare that game is embedded. To do that
// serializers/gameweek.js
import ApplicationSerializer from './application';
import DS from 'ember-data';
export default ApplicationSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs: {
games: { embedded: 'load' }
}
});
We inherit from ApplicationSerializer as best practice so that if it specifies things they are inherited by this model-specific serializer.
In addition, Ember Data is going to insist on a unique id for each game, so it has a way to identify it in the store. If gameId is unique, you can use that as is, but you'll have to let Ember Data know, by saying
// serializers/game.js
import ApplicationSerializer from './application';
import DS from 'ember-data';
export default ApplicationSerializer.extend({
primaryKey: 'gameId'
});
Once you do this, you will no longer refer to the id as gameId; you will refer to it as id, and you do not need to, and should not, declare it as part of your model.

How to load hasMany in Ember?

I have two models:
App.Offer = DS.Model.extend({
name: DS.attr('string'),
description: DS.attr('string'),
products: DS.hasMany('product')
});
App.Product = DS.Model.extend({
name: DS.attr('string'),
description: DS.attr('string'),
offer: DS.belongsTo('offer')
});
And the server is answering with record and array of ids in this way (for example if the rest adapter asks for /offers/1):
{ "offer": [ { "id": 1, "name": "aaaaaaaaa", "description": "aaaa", "product_ids": [ 1, 2 ] } ] }
but now how can I get the products? I have a route like this:
App.OffersRoute = Ember.Route.extend({
model: function() {
var offer = this.get('store').find('offer', 1);
return offer
}
});
In Ember guide is written that if you want the products you should do:
offer.get('products');
Ok, but where should I put this? in the model hook? in a Controller property?
I've tried many things but I can see no network request to products?id[]=1&id[]=2 as I expected (the server is responding correctly to this request);
Can someone please give an example showing how I can find an offer, its products and use this data in my template?
If you're using the RESTAdapter your data needs to be in this format (if you don't want to return it in this format you can create a custom serializer and fix up the json).
2 differences:
the item under offer shouldn't be an array, since you were looking for a single item it should be an object
the key product_ids should be products, product_ids is the format that the ActiveModelAdapter/ActiveModelSerializer use.
JSON
{
"offer":
{
"id":1,
"name":"aaaaaaaaa",
"description":"aaaa",
"products":[
1,
2
]
}
}
The hasMany relationship should be marked as async if you're expecting it to be returned in a separate payload.
App.Offer = DS.Model.extend({
name: DS.attr('string'),
description: DS.attr('string'),
products: DS.hasMany('product', {async:true})
});
I hooked it up in jsbin below, but I didn't hook up a result from products?ids[]=1&ids[]=2 (note ids[]=, not id[]=), if you check the network tab you'll see the request being issued (but it'll crash since there is no result).
http://emberjs.jsbin.com/OxIDiVU/345/edit