Ember sideload data not linked - ember.js

I'm new to using Ember and was assigned to an ongoing project and need to resolve the following:
export default class OrderModel extends Model.extend(LoadableModel) {
#attr('string') status;
#attr('number') total;
#hasMany('order-item', { async: true }) orderItems;
}
export default class OrderItemModel extends Model.extend(LoadableModel) {
#attr('number', { defaultValue: 0 }) discount;
#attr('number', { defaultValue: 0 }) price;
#hasMany('item-fix', { async: false }) fixes;
}
export default class ItemFixModel extends Model.extend(LoadableModel) {
#attr('number', { defaultValue: 0 }) price;
}
and when I do let order = await this.store.findRecord('order', order_id, { reload: true });
the json response is:
data: {
type: "orders",
id: "1584",
attributes: {
status: "in_progress",
total: 1300
},
relationships: {
order-items: {
data: [
{
type: "order-items",
id: "1801
}
]
}
}
},
included: [
{
type: "order-items"
id: "1801",
attributes: {
discount: 0,
price: 1200
},
relationships: {
item-fixes: {
data: [
{
type: "item-fixes",
id: "335"
}
]
}
},
{
type: "item-fixes",
id: "335",
attributes: {
price: 100
}
}
]
but when I inspect the orderItem inside the order variable, the itemFixes are empty, but the is in the sideload of the response.
¿How can I link this nested relationship?
Also, here is the serializer.
export default DS.JSONAPISerializer.extend({
serialize(snapshot) {
let serialized = this._super(...arguments);
let { adapterOptions } = snapshot;
if (adapterOptions && adapterOptions.addPromotionCode) {
return { code: serialized.data.attributes.code }
}
serialized.included = A([]);
snapshot.eachRelationship((key, relationship) => {
if (relationship.kind === 'belongsTo') {
if (!isBlank(snapshot.belongsTo(key))) {
let node = snapshot.belongsTo(key).record.serialize({ includeId: true }).data;
delete node.relationships;
serialized.included.pushObject(node);
}
} else if (relationship.kind === 'hasMany') {
if (!isBlank(snapshot.hasMany(key))) {
snapshot.hasMany(key).forEach(ele => {
let node = ele.record.serialize({ includeId: true }).data;
delete node.relationships;
serialized.included.pushObject(node);
});
}
}
});
return serialized;
}
});

In your response the relationship name is item-fixes but in your model it's just fixes. It must be the same.

Related

Mirage server GETs data but POST fails

I have the mirage models:
// mirage/models/country.js
import { Model, belongsTo, hasMany } from 'miragejs';
export default Model.extend({
name: '',
iso3166_1_alpha3: '',
capitol_city: belongsTo('city', {inverse: null}),
cities: hasMany('city', {inverse: 'country'})
});
and:
// mirage/models/city.js
import { Model, belongsTo } from 'miragejs';
export default Model.extend({
name: '',
country: belongsTo('country', {inverse: 'cities'})
});
and the serializer:
// mirage/serializers/application.js
import { camelize, capitalize, underscore } from '#ember/string';
import { JSONAPISerializer } from 'miragejs';
export default class ApplicationSerializer extends JSONAPISerializer
{
alwaysIncludeLinkageData = true;
keyForAttribute(attr) {
return underscore(attr);
};
keyForRelationship(modelName) {
return underscore(modelName);
};
typeKeyForModel(model) {
return capitalize(camelize(model.modelName));
};
};
When I run the tests:
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
module('Unit | Mirage | mirage models', function (hooks) {
setupTest(hooks);
setupMirage(hooks);
test('it retrieves the country', async function (assert) {
const server = this.server;
let city = server.create('city', { id: '1', name: 'Paris' });
server.create(
'country',
{
id: 'FR',
name: 'France',
iso3166_1_alpha3: 'FRA',
capitol_city: city
}
);
let response = await fetch('/api/countries')
assert.strictEqual(response.status, 200, "Should have created the model");
let json = await response.json();
assert.deepEqual(
json,
{
data: [
{
type: 'Country',
id: 'FR',
attributes: {
name: 'France',
iso3166_1_alpha3: 'FRA',
},
relationships: {
capitol_city: {data: {type: 'City', id: '1'}},
cities: {data: []},
}
}
]
}
)
});
test('it creates the country', async function (assert) {
const server = this.server;
server.create('city', { id: '1', name: 'Paris' });
let response = await fetch(
'/api/countries',
{
method: 'POST',
headers: {'Countent-Type': 'application/json'},
body: JSON.stringify(
{
data: {
id: 'FR',
type: 'Country',
attributes: {
iso3166_1_alpha3: 'FRA',
name: 'France',
},
relationships: {
capitol_city: { data: { type: 'City', id: '1'} },
cities: { data: [{ type: 'City', id: '1'}] }
}
}
}
)
}
);
console.log((await response.json()).message);
assert.strictEqual(response.status, 201, "Should have created the model");
});
});
The first one passes and the second one fails with the message:
Mirage: You're passing the relationship 'capitol_city' to the 'country' model via a POST to '/api/countries', but you did not define the 'capitol_city' association on the 'country' model.
How can I get Mirage to recognise the capitol_city attribute on the model?
Mirage is opinionated with regards to the format of attributes and expects the attributes to be in camelCase (and not snake_case).
Unfortunately the Ember CLI Mirage model relationships documentation does not mention this expectation and all the examples use single-word attributes. Even more unfortunately, Mirage will work with snake_case attributes for simple GET requests and when directly creating models through the API; it is only when you make a request to POST/PUT/PATCH a model into the server that it fails and the message will (confusingly) refer to the snake case attribute which has been defined. (See the Mirage source code for where it fails.)
To solve it, convert the attributes to camel case:
// mirage/models/country.js
import { Model, belongsTo, hasMany } from 'miragejs';
export default Model.extend({
name: '',
iso31661Alpha3: 0,
capitolCity: belongsTo('city', {inverse: null}),
cities: hasMany('city', {inverse: 'country'})
});
and change it in the tests as well:
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
module('Unit | Mirage | mirage models', function (hooks) {
setupTest(hooks);
setupMirage(hooks);
test('it retrieves the country', async function (assert) {
const server = (this as any).server;
let city = server.create('city', { id: '1', name: 'Paris' });
server.create(
'country',
{
id: 'FR',
name: 'France',
iso31661Alpha3: 'FRA',
capitolCity: city
}
);
let response = await fetch('/api/countries')
assert.strictEqual(response.status, 200, "Should have created the model");
let json = await response.json();
console.log(JSON.stringify(json));
assert.deepEqual(
json,
{
data: [
{
type: 'Country',
id: 'FR',
attributes: {
name: 'France',
iso3166_1_alpha3: 'FRA',
},
relationships: {
capitol_city: {data: {type: 'City', id: '1'}},
cities: {data: []},
}
}
]
}
)
});
test('it creates the country', async function (assert) {
const server = (this as any).server;
let city = server.create('city', { id: '1', name: 'Paris' });
let response = await fetch(
'/api/countries',
{
method: 'POST',
headers: {'Countent-Type': 'application/json'},
body: JSON.stringify(
{
data: {
id: 'FR',
type: 'Country',
attributes: {
iso3166_1_alpha3: 'FRA',
name: 'France',
},
relationships: {
capitol_city: { data: { type: 'City', id: '1'} },
cities: { data: [{ type: 'City', id: '1'}] }
}
}
}
)
}
);
console.log((await response.json()).message);
assert.strictEqual(response.status, 201, "Should have created the model");
});
});
However, once you convert it to camel case then the attribute iso31661Alpha3 does not get formatted correctly in the output so you have to manually change the serializer for the country model:
// mirage/serializers/country.js
import ApplicationSerializer from './application';
export default class CountrySerializer extends ApplicationSerializer
{
keyForAttribute(attr: string) {
switch(attr)
{
case 'iso31661Alpha3': return 'iso3166_1_alpha3';
default: return super.keyForAttribute(attr);
}
};
};
Once the attributes are in the correct case then it will work.

How do I serialize a has-many-through relationship with Ember's JSONAPIAdapter?

I have three models: User, Group, and Membership. A User has many Groups through Memberships. I have a form to invite a new user and assign them to zero or more groups all at once.
What I Want
The JSON I expect to send to the server for POST /users looks like
{
data: {
type: 'user',
id: null,
attributes: { name: 'Sam Sample' }
},
relationships: {
memberships: {
data: [
{
type: 'membership',
id: null,
relationships: {
group: {
data: {
type: 'group',
id: 12345
}
}
}
},
{
type: 'membership',
id: null,
relationships: {
group: {
data: {
type: 'group',
id: 67890
}
}
}
}
]
}
}
}
What I Tried
I tried adding serialize: true to the relevant serializers:
// serializer:user
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
attrs: {
memberships: { serialize: true }
}
})
// serializer:membership
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
attrs: {
group: { serialize: true }
}
})
That gets me some of the JSON I expect, but not all of it. Specifically, I get the membership objects, but not the groups within them
{
data: {
type: 'user',
id: null,
attributes: { name: 'Sam Sample' }
},
relationships: {
memberships: {
data: [
{
type: 'membership',
id: null
},
{
type: 'membership',
id: null
}
]
}
}
}

get from model and then set a new property on it

I have a component:
App.MyChildComponent = Ember.Component.extend({
addTooltips: Ember.on('didInsertElement', function() {
var me = this;
var metrics = this.get('option.metrics');
metrics.forEach(function(e, i) {
me.get('option.metrics').objectAt(i - 1).set('tooltipDisabled', true);
});
});
})
Which is generated inside an each loop by a different component:
App.MyParentComponent = Ember.Component.extend({
...
})
And the template of MyParentComponent is:
{{#each option in options}}
{{my-child option=option}}
{{/each}}
All this, is called by a view with a template like this:
{{my-parent options=options}}
options is defined in the model of the view with:
App.MyViewModel = Ember.Object.extend({
options: Ember.A([
{ metrics: Ember.A([
{ name: 'a' },
{ name: 'b' },
{ name: 'c' }
]) },
{ metrics: Ember.A([
{ name: 'd' },
{ name: 'e' },
{ name: 'f' }
]) },
{ metrics: Ember.A([
{ name: 'g' },
{ name: 'h' },
{ name: 'i' }
]) }
]),
});
When I run me.get('option.metrics').objectAt(i - 1).set('tooltipDisabled', true); I get:
Uncaught TypeError: me.get(...).objectAt(...).set is not a function
What am I doing wrong?
Vanilla JavaScript objects don't have set methods. Use Ember.Objects instead:
App.MyViewModel = Ember.Object.extend({
options: Ember.A([
{ metrics: Ember.A([
Ember.Object.create({ name: 'a' }),
// ...
]) }
]),
});
Demo.

How to pass an object to Ember.Handlebars.helper

I'm trying to find out how to pass an Ember object through Handlebars to a helper function. Any ideas of how to do this?
Data
groups = [
{
id:'as93-2dsa',
name:'read'
},
{
id:'dk20-d23k',
name: 'write'
},
{
id: 'mn20-8al3',
name: 'execute'
}
];
user = {
id:'lk30-a8bk',
name: 'Seth'
groupIDs:[
'as93-2dsa',
'lk30-a8bk'
]
}
HTML
{{inGroup groups user}}
Controller
Ember.Handlebars.helper('inGroup', function(groups, user) {
/**
* At this point I would expect groups to be and object and user to be an object
* but both return classes
*/
console.log(groups);
//Class {toString: function, __ember1418251045900: "ember646", __nextSuper: undefined, __ember_meta__: Object, constructor: function…}
console.log(groups.toString());
// "<DS.PromiseArray:ember646>"
console.log(user);
//Class {id: "5CF3A051-12B4-436C-B43A-5696F2792B55", store: Class, container: Container, _changesToSync: Object, _deferredTriggers: Array[0]…}
console.log(user.toString());
// "<nfors-app#model:user::ember526:5CF3A051-12B4-436C-B43A-5696F2792B55>"
/**
* I would like to access groups[i].id and user.groupIds in a way such as the following
*/
for (var i = 0; i < groups.length; i++) {
if (user.groupIds.indexof(groups[i].id) !== -1) {
return true;
}
}
return false;
});
User Model
import DS from 'ember-data';
import EmberValidations from 'ember-validations';
export default DS.Model.extend(EmberValidations.Mixin, {
name: DS.attr('string'),
groupIds: DS.hasMany('groupIds', { async: true }),
validations: {
name: {
presence: true,
length: { minimum: 5 }
}
}
});

Sencha Touch 2 list/store

I have a problem with an list-component on the sencha touch 2 framework. My problem is, that my list doesn't show the calculated distance between the current position and the places.
First I use an navigation view with a list (name + distance) when the user click the name more details about them appears (+ the back-button will generated automatic). If the back-button is pressed the list shows the correct distance in the list. But I really need the distance is shown at the first time. I tried a lot but nothing helps.
I use a model and a store:
'Ext.define('Guide.store.ProjekteList', {
extend: 'Ext.data.Store',
config:{
model: "Guide.model.ProjekteList",
autoLoad:true,
sorters: ['distance'],
storeId: 'ProjekteList',
proxy: {
type: 'ajax',
url : 'PHP/get_MainList.php',
reader: {
type: 'json',
rootProperty:'items'
}
},
listeners: {
load : function(){
this.each(function(store){
var newData = getDis(store.data);
});//each
}//load func
}// listener
}//config
});// klasse
var getDis = function(dataset) {
var geo = Ext.create('Ext.util.Geolocation', {
autoUpdate: false,
listeners: {
locationupdate: function(geo) {
polat = geo.getLatitude();
polng = geo.getLongitude();
var B1 = dataset.Lat / 180 * Math.PI;
var B2 = polat / 180 * Math.PI;
var L1 = dataset.Lng / 180 * Math.PI;
var L2 = polng / 180 * Math.PI;
var zwi = Math.acos(Math.sin(B1)*Math.sin(B2) + Math.cos(B1)*Math.cos(B2)*Math.cos(L2-L1));
var r = 6378.137; //km
dataset.distance = r * zwi;
dataset.distance = Math.round(dataset.distance*100)/100;
},
locationerror: function(geo, bTimeout, bPermissionDenied, bLocationUnavailable, message) {
if(bTimeout){
alert('Timeout occurred.');
} else {
alert('Error occurred.');
}
}
}
});
geo.updateLocation();
return dataset;
};'
and my model:
'Ext.define('Guide.model.ProjekteList', {
extend: 'Ext.data.Model',
config: {
fields: ['Projektname', 'Lat', 'Lng', 'distance', 'ID'],
}
});
here is my navigation view:
'Ext.define('Guide.view.ProjekteList', {
extend: 'Ext.navigation.View',
xtype: 'projektelist',
config: {
title: 'Orte',
iconCls:'Projekte' ,
id: 'listButton',
items:[
{
xtype: 'list',
onItemDisclosure: true,
/* plugins: [
{
xclass: 'Ext.plugin.ListPaging',
autoPaging: false,
}
], */
title: 'Sehenswerte Orte',
store: 'ProjekteList',
itemId: 'liste',
itemTpl: '<h2>{Projektname}</h2> Entfernung: {distance} km',
listeners: {
show: function(){
this.refresh();
} //show function
}, //listeners
} // item
] //items
}// config
}); '
and my controller
Ext.define("Guide.controller.ProjekteList", {
extend: "Ext.app.Controller",
views: ['ProjektList'],
config: {
refs: {
projekt: 'projektelist',
},
control: {
'projektelist list': {
itemtap: 'showDetail'
}
}
},
showDetail: function(list, index, element, record) {
var projektid = record.get('ID');
Ext.StoreMgr.get('ProjektDetail').setProxy({url:'PHP/get_Detail.php?
ID='+projektid}).load();
this.getProjekt().push({
xtype: 'projektdetails',
});
} // showDetail function
});
Thanks in advance!
I found the solution:
only change the listener in the navigation view from "show" to "painted" and it works fine.