Structuring models with ember data - ember.js

I've started using ember data and I'm having some issues getting started. If my json structure for ingredients is:
[
{
"name":"flax seed",
"retailer":"www.retailer.com",
"nutrient_info":[
{
"type":"vitamin A",
"amount":"50mg"
},
{
"type":"calcium",
"amount":"30mg"
}
]
},
{
"name":"soy milk",
"retailer":"www.retailer-two.com",
"nutrient_info":[
{
"type":"vitamin D",
"amount":"500mg"
},
{
"type":"niacin",
"amount":"5000mg"
}
]
},
{ other ingredients... }
]
I think this is how I would define my models:
var attr = DS.attr,
hasMany = DS.hasMany,
belongsTo = DS.belongsTo
App.Ingredients = DS.Model.extend({
// id: attr('number'), // don't include id in model?
name: attr('string'),
retailer: attr('string'),
nutrientinfo: hasMany('nutrients')
})
App.Nutrients = DS.Model.extend({
type: attr('string'),
amount: attr('string'),
ingredient: belongsTo('ingredients')
})
What should the server payload look like, and would I need to customize the REST adapter? Do I need to define the ingredient id: attr() in the model?
Any help in clarifying some of these concepts is appreciated.

Generally model definitions are singular (additionally I changed nutrientinfo to nutrient_info):
App.Ingredient = DS.Model.extend({
// id: attr('number'), // don't include id in model?
name: attr('string'),
retailer: attr('string'),
nutrient_info: hasMany('nutrient')
})
App.Nutrient = DS.Model.extend({
type: attr('string'),
amount: attr('string'),
ingredient: belongsTo('ingredient')
})
The format would need to be as follows (from the endpoint, or using a serializer)
{
// Ingredient records
ingredients:[
{
id:1,
"name":"flax seed",
"retailer":"www.retailer.com",
"nutrient_info":[1,2]
},
{
id:2,
"name":"soy milk",
"retailer":"www.retailer-two.com",
"nutrient_info":[3,4]
},
{ other ingredients... }
],
// Nutrient records
nutrients: [
{
id:1,
"type":"vitamin A",
"amount":"50mg",
ingredient:1
},
{
id:2,
"type":"calcium",
"amount":"30mg",
ingredient:1
},
{
id:3,
"type":"vitamin D",
"amount":"500mg",
ingredient:2
},
{
id:4,
"type":"niacin",
"amount":"5000mg",
ingredient:2
}
]
}
Here's an example using a serializer and your json, I've had to manually assign ids (despite this being invalid, you should send down ids, or use UUIDs), but this should give you an idea of how to use the serializer:
App.IngredientSerializer = DS.RESTSerializer.extend({
extractArray: function(store, type, payload, id, requestType) {
var ingredients = payload,
nutrientId = 0,
ingredientId = 0,
ids = [],
nutrients = [];
ingredients.forEach(function(ing) {
ing.id = ingredientId++;
var nInfo = ing.nutrient_info,
nIds = [];
nInfo.forEach(function(n){
n.id = nutrientId++;
n.ingredient = ing.id;
nIds.push(n.id);
nutrients.push(n);
});
ing.nutrient_info = nIds;
});
payload = {ingredients:ingredients, nutrients:nutrients};
return this._super(store, type, payload, id, requestType);
}
});
http://emberjs.jsbin.com/OxIDiVU/537/edit

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.

Ember Data: saving polymorphic relationships

I'm having trouble saving "hasMany" polymorphic records in Ember Data (1.0.0-beta.15). It looks as if Ember Data isn't setting the "type" property of the polymorphic relationship. Relationships in serialized records look like:
"roles": ["1", "2"]
When I expect them to look more like:
"roles":[{
"id": "1",
"type": "professionalRole"
}, {
"id": "2",
"type": "personalRole"
}
];
I see the following error in the console:
TypeError: Cannot read property 'typeKey' of undefined
If the records come back from the server in the expected format, all is well. The error only occurs when Ember Data creates the relationship.
I experience this using the FixtureAdapter, LocalStorageAdapter, and the RESTAdapter. I've read every piece of documentation I can find on the subject, but I cannot see my mistake.
I've created a CodePen to demonstrate the problem, but I'll also paste that code below.
window.App = Ember.Application.create();
App.ApplicationAdapter = DS.FixtureAdapter;
App.Person = DS.Model.extend({
name: DS.attr(),
roles: DS.hasMany('role')
});
App.Role = DS.Model.extend({
title: DS.attr(),
person: DS.belongsTo('person', {
polymorphic: true
})
});
App.ProfessionalRole = App.Role.extend({
rank: DS.attr()
});
App.ApplicationRoute = Ember.Route.extend({
setupController: function(controller) {
var person = this.store.createRecord('person', {
name: 'James'
});
var role = this.store.createRecord('professionalRole', {
title: 'Code Reviewer',
rank: 'Expert'
});
var promises = Ember.RSVP.hash({
person: person.save(),
role: role.save()
});
promises.catch(function() {
controller.set('initialSaveResult', 'Failure');
});
promises.then(function(resolved) {
controller.set('initialSaveResult', 'Success!');
var resolvedPerson = resolved.person;
var resolvedRole = resolved.role;
// Either/both of these break it
//resolvedRole.set('person', resolvedPerson);
resolvedPerson.get('roles').addObject(resolvedRole);
var innerPromises = Ember.RSVP.hash({
person: resolvedPerson.save(),
role: resolvedRole.save()
});
innerPromises.catch(function() {
controller.set('secondSaveResult', 'Failure');
});
innerPromises.then(function() {
controller.set('secondSaveResult', 'Success!');
});
});
}
});
App.ApplicationController = Ember.Controller.extend({
initialSaveResult: "Loading...",
secondSaveResult: "Loading..."
});

HasMany Polymorphic Relationship In Ember Data

I'm really struggling to understand how polymorphic relationships worm in Ember Data (Beta 11) and cannot find any update information on how to set them up and what is expected in the JSON payload. I'm trying to create a feed of items (think facebook feed) where you have different types of items in the feed. My modeling looks something like the following.
App.Feedable = DS.Model.extend({
activities: DS.hasMany('activity')
});
App.Activity = DS.Model.extend({
feedable: DS.belongsTo('feedable', { polymorphic: true, async: false })
});
App.MemberLikeShare = DS.Model.extend({
status: DS.attr('string')
});
App.PhotoShare = DS.Model.extend({
status: DS.attr('string'),
photo: DS.attr('string')
});
When I do a fetch at /activities I send back JSON that looks like the following:
{
activities: [
{
id: 1,
feedable: { id: 1, type: 'memberLikeShare' }
},
{
id: 4,
feedable: { id: 4, type: 'memberLikeShare' }
},
{
id: 5,
feedable: { id: 5, type: 'photoShare' }
}
],
member_like_shares: [
{
id: 1,
status: 'Foo'
},
{
id: 4,
status: 'Bar'
}
],
photo_shares: [
{id: 5, photo: 'example.jpg'}
]
}
When this runs I get an error like:
You can only add a 'feedable' record to this relationship Error: Assertion Failed: You can only add a 'feedable' record to this relationship
I'm assuming my relationships are wrong or I'm sending the wrong JSON?
polymorphic relationships should extend the base type.
App.Feedable = DS.Model.extend({
activities: DS.hasMany('activity')
});
App.MemberLikeShare = App.Feedable.extend({
status: DS.attr('string')
});
App.PhotoShare = App.Feedable.extend({
status: DS.attr('string'),
photo: DS.attr('string')
});
I'd also expect them to define the activities on them.
member_like_shares: [
{
id: 1,
status: 'Foo',
activites: [1,2,3,4]
},
{
id: 4,
status: 'Bar',
activites: [1,2,3,4]
}
],
photo_shares: [
{
id: 5,
photo: 'example.jpg',
activites: [1,2,3,4]
}
]

Ember embedded json errors out #You must include an id in a hash passed to push

Getting this error . Not sure why i am getting this error....would appreciate if someone can help me spot why this is erroring out.
Error while loading route: Error: Assertion Failed: You must include an id in a hash passed to push at new Error (native) at Error.Ember.Error
from the other posts related to similar error this has to do with a json where primary key is not handled correctly. But my json response looks correct.
****here are model objects:****
var PersonInfo = DS.Model.extend({
first: DS.attr('string'),
last : DS.attr('string'),
addresses: DS.hasMany('personAddress', {embedded: 'always'})
});
Ember.Inflector.inflector.irregular("personInfo", "peopleInfo");
export default PersonInfo;
var Address = DS.Model.extend({
type: DS.attr('string'),
personInfo: DS.belongsTo('personInfo')
});
export default Address;
****here is my deserializer:****
var PersonInfoSerializer = DS.ActiveModelSerializer.extend({
primaryKey: 'id',
extractArray: function(store, type, payload, id, requestType) {
var peopleInfo =payload.peopleInfo;
var adds = [];
// debugger;
peopleInfo.forEach(function(personInfo){
var addresses = personInfo.addresses,
addressIds = addresses.mapProperty('id');
adds.push(addresses);
personInfo.addresses = addressIds;
});
payload.addresses = adds;
return this._super(store, type, payload, id, requestType); }
});
export default PersonInfoSerializer;
****and here is the json response which i am mocking in API STUB****
server.get('/peopleInfo', function(req, res) {
var person_info = {
"peopleInfo": [{
"id": "1",
"first": "Tom",
"last": "Dale",
"addresses": [{
"id": "1",
"type": "Home"
}, {
"id": "2",
"type": "Work"
}]
}]
};
res.send(person_info);
});
I'm not sure why you were using the ActiveModelSerializer, but it doesn't really buy you anything if your data isn't coming down in the format that Rails generally provides.
You're data wasn't being formatted correctly. Additionally there is no need to write {embedded:'always'} that does nothing anymore. You'll probably want to look at the transition document https://github.com/emberjs/data/blob/master/TRANSITION.md .
App.PersonInfoSerializer = DS.RESTSerializer.extend({
extractArray: function(store, type, payload, id, requestType) {
var peopleInfo =payload.peopleInfo;
var adds = [];
peopleInfo.forEach(function(personInfo){
//debugger;
var addresses = personInfo.addresses,
addressIds = addresses.getEach('id');
adds = adds.concat(addresses);
personInfo.addresses = addressIds;
});
payload.personAddresses = adds;
return this._super(store, type, payload, id, requestType);
}
});
http://emberjs.jsbin.com/OxIDiVU/477/edit

Correct usage of store.loadMany() function

I'm trying to figure out how to populate a table from a JSON object.
My JSON is a structurated object:
{
id: 0,
list: [{ username:'user_1',online:true, user:0 },
{ username:'user_2',online:true, user:0 }]
}
My Model is defined as follow:
MyTalk.WUser = DS.Model.extend({
list: DS.hasMany('MyTalk.User')
});
MyTalk.User = DS.Model.extend({
username: DS.attr('string'), // primary key
online: DS.attr('boolean'),
user: DS.belongsTo('MyTalk.WUser')
});
I am using a custom Adapter for ember-data:
DS.SocketAdapter = DS.RESTAdapter.extend(MyTalk.WebSocketConnection, {
// code not relevant
}
DS.SocketAdapter.map('MyTalk.WUser', {
list: {embedded: 'always'}
});
DS.SocketAdapter.map('MyTalk.User', {
primaryKey: 'username'
});
MyTalk.Store = DS.Store.extend({
revision: 12,
adapter: DS.SocketAdapter.create({})
});
Now I would load my data. I run in Chrome command line the following statements:
var store = DS.get('defaultStore');
var obj = {
id: 0,
list: [{ username:'user_1',online:true, user:0 },
{ username:'user_2',online:true, user:0 }]
};
var store.loadMany(MyTalk.WUser,obj);
var record = MyTalk.WUser.find(0);
record.serialize();
But it returns no record:
> Object {list: Array[0]}
thanks in advance!!
If you want to allow the adapter to deserialize embedded records (or perform any custom deserialization, for that matter), you'll need to load your data through the adapter rather than directly into the store.
var store = DS.get('defaultStore'),
obj = {
id: 0,
list: [{ username:'user_1', online:true, user:0 },
{ username:'user_2', online:true, user:0 }]
},
type = MyTalk.WUser,
adapter = store.adapterForType(type);
adapter.load(store, type, obj);