EmberJS hasMany doesn't fetch relationships - ember.js

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'
});

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 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 Serialize/Deserialize embedded records on 3rd level

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'}

Ember.js - Error - "Assertion failed: You must include an `id` in a hash passed to `push`"

I'm getting this error after I save a post (title, text) to a mongodb database via a REST API written with Express and refresh the browser. I've already set my primary key to '_id' and have been reading about possibly normalizing the data?
Here is the payload from the server (only 1 post in db):
{
"posts": [
{
"title": "The Title",
"text": "Lorem ipsum",
"_id": "52c22892381e452d1d000010",
"__v": 0
}
]
}
The controller:
App.PostsController = Ember.ArrayController.extend({
actions: {
createPost: function() {
// Dummy content for now
var to_post = this.store.createRecord('post', {
title: 'The Title',
text: 'Lorem ipsum'
});
to_post.save();
}
}
});
The model:
App.Post = DS.Model.extend({
title: DS.attr('string'),
text: DS.attr('string')
});
Serializer:
App.MySerializer = DS.RESTSerializer.extend({
primaryKey: function(type){
return '_id';
}
});
Adapter:
App.ApplicationAdapter = DS.RESTAdapter.extend({
namespace: 'api'
});
Any help is much appreciated! Please let me know if you need any other info.
Thanks
When using custom adapters/serializers the naming is important. If you want it to apply to the entire application it should be called ApplicationSerializer
App.ApplicationSerializer = DS.RESTSerializer.extend({
primaryKey: '_id'
});
Adapter:
App.ApplicationAdapter = DS.RESTAdapter.extend({
namespace: 'api'
});
If you just want it to apply to a single model (this applies to adapter's as well)
App.PostSerializer = DS.RESTSerializer.extend({
primaryKey: '_id'
});
I had this same error but after some debugging discovered it was caused by my rest API not returning the saved objects json.