Ember-data: How to deal with inheritance objects - ember.js

I have a JSON-Api backend and I can't find the good way to deal with inheritance objects, because the Route ressource name is the parent Class but the "type" attribute in the payload correspond to the child class. Example of payload (GET /api/pets):
{
"data": [
{
"id": "1",
"type": "cat",
"attributes": {
"name": "Tom"
}
},
{
"id": "2",
"type": "dog",
"attributes": {
"name": "Max"
}
]
}
Now, I load it with a Route:
# app/routes/pets.js
import Route from '#ember/routing/route';
export default Route.extend({
model() {
return this.get('store').findAll('pet');
}
});
Models:
# app/models/pet.js
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string')
});
# app/models/cat.js
import DS from 'ember-data';
import Pet from './pet';
export default Pet.extend({
});
# app/models/dog.js
import DS from 'ember-data';
import Pet from './pet';
export default Pet.extend({
});
And finally, I want to display them on a simple page:
# app/templates/devices.hbs
<h3>Pets list</h3>
{{#each model as |pet|}}
<h4>{{pet.name}}</h4>
{{/each}}
The objects are well loaded by the Route (I saw them with the Ember inspector), but there are not Pet objects, so they are not rendered. At this point, I don't know how to deal with that, without changing the api structure.

This is going to take a little work, but probably won't be too bad.
First you need to create a Pet serializer: ember generate serializer pet.
Then you can override the normalize hook to return a different model based on the attributes in the response:
https://emberjs.com/api/ember-data/3.3/classes/DS.JSONAPISerializer/methods/normalize?anchor=normalize
Or if there aren't any actual differences between dogs/cats, you could just move the type into the attributes field in the same normalize hook.

Related

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.

Converting backend json response to Ember store expected format

I am pretty new to ember and trying to do some POC's to fit our existing applications.
Using ember 1.13.12 and ember data 1.13.15
I have two models,
company.js
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string'),
employee: DS.hasMany('employee',{async:true}),
});
employee.js
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string'),
alias: DS.attr('string')
});
The serializers extend the JSONSerializer and sets the primarykey for both models and the option embedded always for employee model.
Backend json response for /employee/7, - this results in error - Ember.A(...) map not a function
{
"name": "abc"
"employee": {
"employeeId": 7,
"name": "employee7",
"alias": "e7"
}
}
Backend json response for /employees - this gets pushed to the ember store without issues
{
"name": "abc"
"employee": [
{
"employeeId": 1,
"name": "employee1",
"alias": "e1"
},
{
"employeeId": 7,
"name": "employee7",
"alias": "e7"
}
]
}
With the above response I faced two issues,
Not having an 'id' for the 'company' model to push to the store (Workaround ended up changing the backend response to have an 'id')
When I try getting a single employee say employee/7/ the json response from backend does not return as an array (Workaround added one of the normalize hook and added a line payload.employee = [payload.employee] and it worked)
Is there a way I can get past the above issues without changing the backend response and workarounds.

Ember-Data 2.1.0 hasMany not working (cannot read property 'replace' of undefined)

I searched everywhere but can't seem to find an answer for this simple problem here on SO.
Problem:
I have a hasMany relationship in a model that is loaded by the route in a findAll(). Payload looks fin according to many answers I've seen here, but I get "TypeError: Cannot read property 'replace' of undefined". More details below.
How can I get this hasMany relationship to work? I'm using asynch:false and sending the sideload as recommended.
Using: Ember 2.1.0, Ember-Data 2.1.0.
Stacktrace:
TypeError: Cannot read property 'replace' of undefined
at Object.func (ember.debug.js:36026)
at Object.Cache.get (ember.debug.js:13165)
at decamelize (ember.debug.js:36068)
at Object.func (ember.debug.js:35974)
at Object.Cache.get (ember.debug.js:13165)
at Object.dasherize (ember.debug.js:36072)
at ember$data$lib$system$normalize$model$name$$normalizeModelName (normalize-model-name.js:13)
at ember$data$lib$serializers$json$serializer$$default.extend.modelNameFromPayloadKey (json-api-serializer.js:267)
at ember$data$lib$serializers$json$serializer$$default.extend._extractType (json-api-serializer.js:258)
at ember$data$lib$serializers$json$serializer$$default.extend.normalize (json-api-serializer.js:290)
Route:
app/routes/search.js
export default Ember.Route.extend({
model(params) {
if(params.query){
return this.store.findAll('search-result');
}
return null;
},
actions:{
sendSearch: function(queryString){
this.store.unloadAll('search-result');
this.refresh();
}
}
});
Models:
app/models/search-result.js
import DS from 'ember-data';
export default DS.Model.extend({
title: DS.attr('string'),
description: DS.attr('string'),
url: DS.attr('string'),
tags: DS.hasMany('search-result-tag', {async:false})
});
app/models/search-result-tag.js
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string'),
});
Adapter (for search-result)
import DS from 'ember-data';
export default DS.RESTAdapter.extend({
host: 'http://localhost:8080',
urlForFindRecord(id, modelName, snapshot) {
let url = this._super(...arguments);
let query = Ember.get(snapshot, 'adapterOptions.query');
if (query) {
url += '?' + Ember.$.param(query); // assumes no query params are present already
}
return url;
},
urlForFindAll(modelName) {
var queryDict = {};
location.search.substr(1).split("&").forEach(function(item) {queryDict[item.split("=")[0]] = item.split("=")[1]})
let url = this._super(...arguments);
let query = queryDict['query'];
if (query) {
url += '?query=' + query; // assumes no query params are present already
}
return url;
}
});
Payload
{
"search-result-tags": [
{
"name": "this-is-tag-#-0",
"id": 0
}
],
"search-results": [
{
"description": "This is description for blabla2",
"id": 0,
"title": "Blabla 2",
"url": "http://blablabla2.com",
"tags": []
},
{
"description": "This is description for blabla",
"id": 1,
"title": "Blabla",
"url": "http://blabla.com",
"tags": [
0
]
}
]
}
You need to use the RESTSerializer in addition to the RESTAdapter. So app/serializers/application.js would be -
import DS from 'ember-data';
export default DS.RESTSerializer.extend({
});
See the docs. You may need to override keyForAttribute, if you need to change cases / underscores of your keys.
Note that if you are using Rails API for the backend you want ActiveModelAdapter and ActiveModelSerializer, which are available as an addon.

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.

Polymorphic relations in embedded relationships

I have a ember cli project and I am trying to replicate a simple scenario in which I have a Post model which hasMany Comment model. This relationship is polymorphic. I have two type Body Comment and Title Comment.
// app/models/post.js
import DS from 'ember-data';
export default DS.Model.extend({
entry: DS.attr('string'),
comments: DS.hasMany('comment', {polymorphic: true})
});
// app/models/comment.js
import DS from 'ember-data';
export default DS.Model.extend({
text: DS.attr('string'),
post: DS.belongsTo('post')
});
// app/models/body.js
import DS from 'ember-data';
import Comment from './comment';
export default Comment.extend({
body: DS.attr('string')
});
// app/models/title.js
import DS from 'ember-data';
import Comment from './comment';
export default Comment.extend({
title: DS.attr('string')
});
I have a serializer for Post model
import DS from 'ember-data';
export default DS.ActiveModelSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs: {
comments: {
embedded: 'always'
}
}
});
JSON returned by the server on GET /posts/1 is
{
"posts": {
"id": "1",
"entry": "This is first post",
"comments": [
{
"id": "1",
"post": "1",
"type": "body",
"text": "This is the first comment on first post",
"body": "This is a body comment"
},
{
"id": "2",
"post": "1",
"type": "title",
"text": "This is the second comment on first post",
"title": "This is a title comment"
}
]
}
}
But Ember data is failing in deserializing the comments with the following error:
Error while processing route: index Cannot read property 'typeKey' of undefined TypeError: Cannot read property 'typeKey' of undefined
at Ember.Object.extend.modelFor (http://localhost:4200/assets/vendor.js:71619:22)
at Ember.Object.extend.recordForId (http://localhost:4200/assets/vendor.js:71074:25)
at deserializeRecordId (http://localhost:4200/assets/vendor.js:72099:27)
at deserializeRecordIds (http://localhost:4200/assets/vendor.js:72116:9)
at http://localhost:4200/assets/vendor.js:72081:11
at http://localhost:4200/assets/vendor.js:70135:20
at http://localhost:4200/assets/vendor.js:17687:20
at Object.OrderedSet.forEach (http://localhost:4200/assets/vendor.js:17530:14)
at Object.Map.forEach (http://localhost:4200/assets/vendor.js:17685:14)
at Function.Model.reopenClass.eachRelationship (http://localhost:4200/assets/vendor.js:70134:42)
This happens when the following code is executed:
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return this.store.find('post', 1);
},
setupController: function(controller, model) {
console.log("Post entry: " + model.get('entry'));
var comments = model.get('comments');
comments.forEach(function(comment){
console.log("Comment: " + comment.get('text'));
console.log(typeof comment);
//console.log("Comment Body " + comment.get('body'));
//console.log("Comment Title " + comment.get('title'));
});
}
});
Please help me in understanding if I am doing something wrong and if yes then what is the correct way to solve a requirement like this.
So #Dipayan you aren't necessarily doing anything wrong here, Ember Data just doesn't yet support HasMany Embedded Polymorphic relationships yet. Polymorphic embedded hasMany relationship. It will give the TypeError: Cannot read property 'typeKey' of undefined if you try to use it, which is precisely what you're getting.
If you have control of how your server handles the data you can keep the polymorphic, but lose the embedded, and instead use side-loading. For an example and good explanation of using polymorphic in this way check out Topal's Guide to Ember Data.