Polymorphic relations in embedded relationships - ember.js

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.

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.

Relationship with 2 API's getting error

I have 2 API's:
http://example.api.com/api.json (this file have aprox 5mb)
http://api.com/:itemId
The 1st one have all data that i need, like this:
{
"realms": [
{"name":"Azralon","slug":"azralon"}],
"auctions": [
{"auc":828911977,"item":76139,"owner":"Bloodkina","bid":15294990,"buyout":16099990,"quantity":10,"timeLeft":"VERY_LONG"},
{"auc":828911979,"item":10000,"owner":"Bloodkina", "bid":15294990,"buyout":16099990,"quantity":100,"timeLeft":"VERY_LONG"},
{"auc":829305192,"item":98828,"owner":"Tempestivå","bid":15294990,"buyout":16099990,"quantity":5,"timeLeft":"VERY_LONG"},
{"auc":829305193,"item":98728,"owner":"Tempestivå", "bid":15294990,"buyout":16099990,"quantity":2,"timeLeft":"VERY_LONG"}
]}
The 2nd one have the name of the Items, but it only responds when i pass itemId as parameter. For example the item:76139, like http://api.com/76139
{
"id": 76139,
"description": "",
"name": "Wild Jade",
"icon": "inv_misc_gem_x4_rare_uncut_green",
}
I want to show the name of item and owner, but i getting an error like <DS.PromiseObject:ember71726> im my item field, the owner field is ok. How can i do this?? (it's the Blizzard API for auctions and items)
model/auction.js
import DS from 'ember-data';
export default DS.Model.extend({
auc: DS.attr('number'),
item: DS.belongsTo('item'), //items: DS.belongsTo('item'),
owner: DS.attr('string'),
});
model/item.js
import DS from 'ember-data';
export default DS.Model.extend({
auctions: DS.hasMany('auction'),
name: DS.attr('string')
});
routes/index.js
import Route from '#ember/routing/route';
export default Route.extend({
model(){
return this.store.findAll('auction');
}
});
routes/item.js
import Route from '#ember/routing/route';
export default Route.extend({
model(params){
return this.store.findRecord('item', params.item_id)
},
});
serializers/auction.js
import DS from 'ember-data';
export default DS.RESTSerializer.extend({
normalizeResponse (store, primaryModelClass, payload, id, requestType) {
return {
realms: payload.realms,
data: payload.auctions.map(ah=>{
return {
id: ah.auc,
type:'auction',
attributes: ah,
//Added this
relationships: {
item:{
data: {
id: ah.item,
type: 'item',
}
}
}
}
})
};
}
});
serializers/item.js
import DS from 'ember-data';
export default DS.JSONSerializer.extend({
normalizeResponse (store, primaryModelClass, payload, id, requestType) {
payload = {
data : payload,
id: payload.id,
name: payload.name
};
return this._super(store, primaryModelClass, payload, id, requestType)
}
});
templates/index.hbs
{{#each model as |auction|}}
<ul>
<li>{{auction.items.name}}</li>
<li>{{auction.quantity}}</li>
<li>{{auction.bid}}</li>
<li>{{auction.buyout}}</li>
<li>{{auction.timeLeft}}</li>
<li>{{auction.owner}}</li>
</ul>
{{/each}}
Hey Cas 👋 I'm going to try and answer this one as best I can while trying to describe some of the issues that you were probably facing along the way. I have it working locally so hopefully I am going to be able to communicate how you can get it working on your side.
Firstly, as I mentioned in my comment you seemed to have a missmatch between your API, your model and your template when it came to how you were referencing items. You need to make sure that each key is correct so they all match up. Here is my backend responder, my model and my template:
Backend responder using express-autoroute:
module.exports.autoroute = {
get: {
'/auctions': function getThings(req, res) {
res.json({
realms: [{
name: 'Azralon',
slug: 'azralon',
}],
auctions: [{
auc: 828911977,
item: 76139,
owner: 'Bloodkina',
bid: 15294990,
buyout: 16099990,
quantity: 10,
timeLeft: 'VERY_LONG',
},
...
});
},
},
};
Auction model (Ember)
import DS from 'ember-data';
export default DS.Model.extend({
auc: DS.attr('number'),
item: DS.belongsTo('item'),
owner: DS.attr('string'),
});
Application template (Ember)
{{#each model as |auction|}}
<ul>
<li>{{auction.item.name}}</li>
<li>{{auction.quantity}}</li>
<li>{{auction.bid}}</li>
<li>{{auction.buyout}}</li>
<li>{{auction.timeLeft}}</li>
<li>{{auction.owner}}</li>
</ul>
{{/each}}
As you can see the backend is responding with item as an attribute to an auction, the model is using item as it's attribute name and the template is also accessing the key item. This is what I meant when I said they needed to match 🙂
The second thing that I noticed is that your auction serialiser isn't saying anything about relationships. If you check out the JSON:API spec you will see how relationships are supposed to be defined, i.e. with a relationships object
I was able to get your thing working using the following code:
Auction Serializer (Ember)
import DS from 'ember-data';
export default DS.JSONAPISerializer.extend({
normalizeResponse (store, primaryModelClass, payload, id, requestType) {
return {
realms: payload.realms,
data: payload.auctions.map(ah => {
return {
id: ah.auc,
type:'auction',
attributes: ah,
relationships: {
item: {
data: {
id: ah.item,
type: 'item',
}
}
}
}
})
};
}
});
as you can see I'm building the relationships object and making sure that the item key matches.
The last issue I found was that your item serialiser wasn't working, I'm assuming that this is just that you didn't get this far because you successfully implemented the Auction Serializer. Here is my implementation:
Item Serializer (Ember):
import DS from 'ember-data';
export default DS.JSONAPISerializer.extend({
normalizeResponse (store, primaryModelClass, payload, id, requestType) {
return {
data : {
attributes: payload,
id: payload.id,
type: 'item',
},
};
}
});
As I said this is now working for me locally but let me know if you have any more issues 🙂

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 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.