Working on an App with Ember RC6 and Ember-Data v0.13-54 along a custom server PHP API am running into some problems with Models relations (mainly many-to-many) and side loading.
The models in questions are:
App.Member = DS.Model.extend({
apiToken: DS.attr('string'),
apiTokenExpire: DS.attr('string'),
favourites: DS.hasMany('App.Presentation')
});
App.Presentation = DS.Model.extend(
{
title: DS.attr('string'),
description: DS.attr('string'),
date: DS.attr('date'),
category: DS.belongsTo('App.Category'),
tags: DS.hasMany('App.Tag'),
employees: DS.hasMany('App.Member'),
presentation: DS.belongsTo('App.File'),
presenterNotes: DS.belongsTo('App.File'),
cover: DS.belongsTo('App.Image')
});
To get the RESTAdapater to send the relation with the Member model I have:
App.APIRESTAdapter.map('App.Member', {
favourites: {embedded: 'always'}
});
When loading /members/1 the server returns:
{
"member": {
"api_token": "9fa236ad58726584d7b61bcf94b4dda37cbd3a24",
"api_token_expire": "1383832335",
"id": 1,
"favourite_ids": [ 3 ],
"group_ids": [ 2 ]
},
"presentations": [
{
"title": "That's a new one!",
"category_id": 2,
"id": 3,
"tag_ids": [ 1 ],
"employee_ids": [ 1 ]
}
]
}
But if I log get('member.favourites').mapProperty('id') I get an empty array and none of the favourites are actually added to the Member model.
If I disable the embedding of favourites on the RESTAdapter, all works fine. I am just wondering if there is something that I am missing or if there is some issues with how the JSON response is formatted?
EDIT:
Done some digging and seems that the issue comes from the fact that the relation names (favourites, employees) is different from the model names (Member, Presentation) which are used when sideloading data. Weird, since rev. 12 https://github.com/emberjs/data/blob/master/BREAKING_CHANGES.md models should be Sideload by Type.
Doing some tests I've added a new many-to-many relation for those 2 models:
App.Member gets presentations: DS.hasMany('App.Presentation')
App.Presentation get members: DS.hasMany('App.Member')
The JSON returned by the server is exactly the same, and when logging get('member.presentations') I now get the list of presentations as it should.
At this point this looks like a bug to me, but maybe am missing something? I've tried mappings on the RESTAdapater for favourites and employees but that didn't help... Maybe there is some other Adapter config that could help?
This isn't a sideloading issue but more a misunderstanding on my side about embedded data and what the configuration meant.
Since the Adapter was configured with:
App.APIRESTAdapter.map('App.Member', {
favourites: {embedded: 'always'}
});
The expected JSON response from the server is:
{
"member": {
"api_token": "b84fd204b37d3fa3cee8a2b2cac8bd4fd02635a1",
"api_token_expire": "1384027367",
"id": 1,
"favourites": [
{
"title": "Some kind of title",
"category_id": 1,
"id": 2,
"tag_ids": [ 1 , 2 ],
"employee_ids": [ 1 ]
}
]
}
}
So "favourite_ids": [ X, X, X ] should have been "favourites": [ HASH, HASH, HASH ] when records are flagged as embedded.
Related
I'm trying to modify my JSON payload in the REST serializer so I can use it in my Ember application as I want.
This is how the real JSON from the server looks like:
{
"folders": [
{
"file_ids": [
2001,
2002,
2003
],
"child_folder_ids": [
2
],
"id": 1,
"name": "Root folder",
"parent_folder_id": null,
"parent_folder_ids": []
}
],
"related_folders": [
{
"file_ids": [],
"child_folder_ids": [
3
],
"id": 2,
"name": "Child folder",
"parent_folder_id": 1,
"parent_folder_ids": [
1
]
}
]
}
This is how the Ember folder model looks like:
export default DS.Model.extend({
name: DS.attr('string'),
files: DS.hasMany('file'),
childFolders: DS.hasMany('folder', {inverse: 'parentFolder'}),
parentFolders: DS.hasMany('folder'),
parentFolder: DS.belongsTo('folder', {inverse: 'childFolders'}),
});
The childFolders and parentFolders are inside the related_folders in the JSON I'm getting from the server. Basically this are also folders so I want to use the folder model for this. I've tried to modify the payload in the REST serializer so the related folders will be part of the folders array.
This is how my serializer looks like:
export default DS.RESTSerializer.extend({
extractArray: function(store, type, payload) {
var rootFolders = payload.folders;
var childFolders = payload.related_folders;
var folders = [];
rootFolders.forEach(function(rootFolder) {
folders.push(rootFolder);
});
childFolders.forEach(function(childFolder) {
folders.push(childFolder);
});
payload = { folders: folders };
return this._super(store, type, payload);
}
});
At this moment I have my folders and related folders both in the store as folders but somehow I can not use the folder properties in my controller or components to use them in computed properties. I would like to determine which folders are root folders and which are child folders and then use the handlebars each helper in my template to show the right folders. Does anyone have an idea what I'm doing wrong or if there is a better way to fix this?
Hmm main problem was that I didn't extend my applicationSerializer in the folderSerializer. Now it's working.
I'm building an adapter to wrap the Keen.io API, so far I've been able to successfully load the projects resource, however the returned object looks like this:
{
partners_url: "/3.0/projects/<ID>/partners",
name: "Project Name",
url: "/3.0/projects/<ID>",
saved_queries: [ ],
events_url: "/3.0/projects/<ID>/events",
id: "<ID>",
organization_id: "<ORG ID>",
queries_url: "/3.0/projects/<ID>/queries",
api_key: "<API KEY>",
events: [
{
url: "/3.0/projects/<ID>/events/user_signup",
name: "user_signup"
},
{
url: "/3.0/projects/<ID>/events/user_converted",
name: "user_converted"
},
{
url: "/3.0/projects/<ID>/events/user_created_project",
name: "user_created_project"
}
]
}
Excluding a lot of cruft, Ember has no trouble mapping the name and id attributes using the RESTSerializer, but if I add an events relation to my Project model it blows up with:
Error while loading route: TypeError: Cannot set property 'store' of undefined
at Ember.Object.extend.modelFor (http://localhost:3000/assets/ember-data.js?body=1:9813:23)
at Ember.Object.extend.recordForId (http://localhost:3000/assets/ember-data.js?body=1:9266:21)
at deserializeRecordId (http://localhost:3000/assets/ember-data.js?body=1:10197:27)
at deserializeRecordIds (http://localhost:3000/assets/ember-data.js?body=1:10211:9)
at http://localhost:3000/assets/ember-data.js?body=1:10177:11
at http://localhost:3000/assets/ember-data.js?body=1:8518:20
at http://localhost:3000/assets/ember.js?body=1:3404:16
at Object.OrderedSet.forEach (http://localhost:3000/assets/ember.js?body=1:3247:10)
at Object.Map.forEach (http://localhost:3000/assets/ember.js?body=1:3402:10)
at Function.Model.reopenClass.eachRelationship (http://localhost:3000/assets/ember-data.js?body=1:8517:42)
From my investigation this seems to be because it can't find the inverse relation to map an Event back to a Project because there's no parent ID.
Is it possible to create a relation in Ember Data to support this? Or is it possible to modify the Serializer to append a projectId to each event before loading?
I'm using Ember 1.5.0-beta.4 and Ember Data 1.0.0-beta.7+canary.f482da04.
Assuming your Project model is setup the following way:
App.Project = DS.Model.extend({
events: DS.hasMany('event');
});
You need to make sure that the JSON from your API is in a certain shape that Ember-Data expects.
{
"project": {
"id": 1,
"events": ["1", "2"],
},
"events": [{
"id": "1",
"name": "foo"
}, {
"id": "2",
"name": "bar"
}]
}
You can, however, implement extractArrayin your model's serializer to transform the JSON from the server into something similar like the above example.
There's a working example and an explanation in the Ember docs.
I may be making a Promise faux pas but after authenticating a user I want to load the user's profile into the App.Session singleton that I've created:
App.Session.set(
'userProfile',
self.get('store').find('user',App.Session.get('userId'))
);
This results in the API call being made and a valid resultset being returned but for some reason in the debugger I get an empty result. Specifically, I do see the User.id but the rest of the columns are blank.
From the debugger, here's the JSON response:
{
"user": {
"id": "1",
"username": "jsmith",
"name": {
"first_name": "Joe",
"last_name": "Smith"
},
"emails": [
{
"id": "52153c0330063",
"name": "work-1",
"type": "other",
"address": "new#notreally.com",
"comments": "",
"status": "active",
"is_primary": false
},
{
"id": "52153d1b90ad0",
"name": "work-2",
"type": "other",
"address": "old#yesreally.com",
"comments": "",
"status": "active",
"is_primary": true
},
]
}
I'm a little new to Promises and so I thought maybe if I changed the code to:
self.get('store').find('user',App.Session.get('userId')).then( function(profile){
App.Session.set('userProfile', profile);
});
I felt pretty good about my new Promise acumen as I wrote this new code. Sadly my proud moment was greeted with failure. My second code snippet behaves precisely the same as the first one. Huh?
Can anyone help?
--------- ** UPDATE ** ---------
I've now including the model definition for User and a picture of the debugger window I made reference to.
User Model
App.RawTransform = DS.Transform.extend({
deserialize: function(serialized) {
return serialized;
},
serialize: function(deserialized) {
return deserialized;
}
});
App.NameTransform = DS.Transform.extend({
deserialize: function(serialized) {
return App.Name.create(serialized);
},
serialize: function(deserialized) {
return JSON.stringify(deserialized);
}
});
App.User = DS.Model.extend({
username: DS.attr("string"),
name: DS.attr("name"),
roles: DS.attr("raw"),
goals: DS.attr("raw"),
places: DS.attr("raw"),
emails: DS.attr("raw"),
networks: DS.attr("raw"),
relationships: DS.attr("raw"),
services: DS.attr("raw"),
uom: DS.attr("raw"),
});
Debug Window
Prior to login the model viewer looks like this:
Then after login it looks like this:
And then looking at the record details we see:
Ok, the answer seems to be down to two things. First of all, the second code snippet for handling the promise that I tried:
self.get('store').find('user',App.Session.get('userId')).then( function(profile){
App.Session.set('userProfile', profile);
});
is the correct way to go. The first method just leaves you with a "broken promise" in a "broken heart" sort of way not technically speaking but the point is it doesn't work.
The reason that my second promise implementation didn't work though was down to the Model indirectly and very specifically down to the deserializer I had put in place for Names.
I was scratching my head on this for second as the deserializer had worked back in the Ember-Data v0.1x world so I did what seemed appropriate ... I blamed Ember-Data. Come on, we've all done it. The fact is that Ember-Data had nothing to do with it and once I was willing to accept the blame I realised that it was simply a matter of not having moved my Name object over to the project I'm currently working on. Doh!
Here's my Ember-Data model:
Lrt.Option = DS.Model.extend({
option_relation_value: hasMany('option')
});
Here is an example of the JSON: (Shortened for the sake of this question)
{
"optionGroups": [],
"optionSubGroups": [
{
"id": "3",
"optionType": [
"80",
"81",
"82",
"83",
"84",
"248",
"278"
],
"title": "Option Group for 80"
}
],
"options": [
{
"id": "45",
"option_relation_value": [
"80"
]
},
{
"id": "80",
"option_relation_value": []
}
]
}
There are also "OptionGroup" and "OptionSubGroup" models which are sideloading options in.
The issue I'm having is that after adding in the 'hasMany' into the model, I can no longer do query the store for Options like this:
this.get('store').find('option')
It simply returns "0", however in the Ember Inspector, I get 400+ entries so I know the data loaded.
When using the chrome inspector and break on ALL Exceptions, it breaks on line 2246 of Ember-Data at the following line:
2246: Ember.assert('The id ' + id + ' has already been used with another record of type ' + type.toString() + '.', !id || !idToRecord[id]);
The error is:
"Cannot call method 'toString' of undefined"
"type" in this line is 'undefined'.
What am I doing wrong with this hasMany relationship?
I am using Ember-Data 1.0 Beta 2.
Technically, this is not side loading, it's really more like nested loading. That may have something to do with it.
For true side loading you'd want a structure like this as your outer SON response :
{
"option" : {
"id" : 3,
...
"options" : [45,80]
}
"options" : [
{ "id":45 , ... },
{ "id":80 , ... }
]
}
Here are the docs about JSON conventions : http://emberjs.com/guides/models/connecting-to-an-http-server/#toc_json-conventions The comments are an example of side loading.
I know that this works for separate model structures (post -> comment), but I'm not sure about self referential tree type structures.
I'm using a rest adapter with ember-data 1.0 and ember.js 1.0
given these models:
App.Customer = DS.Model.extend({
name: DS.attr('string'),
state: DS.belongsTo("State")
});
App.State = DS.Model.extend({
region: DS.attr('string'),
state: DS.attr('string'),
stateName: DS.attr('string'),
customers: DS.hasMany("Customer")
});
when I go to /#/states, I get this json response
{
"states": [
{
"region": "West",
"state": "AZ",
"stateName": "Arizona",
"id": "0x0000000000000324",
"customers": [
"0x00000000000001e5"
]
},
{
"region": "West",
"state": "CA",
"stateName": "California",
"id": "0x0000000000000325",
"customers": [
"0x00000000000001c0",
"0x00000000000001c4",
"0x00000000000001d4"
]
}
]
"customers" : [
{
}
]
}
Now, I have a couple of questions
1) What should I put in the Customers part ? A complete list of all the customers, or just a list of the customers that are specified in the state list ?
2) what data should I send back if I visit /#/customers ?
3) If I were to edit a customer. would I set it up so that the lookup/combo makes a separate request to the server ?
thanks for the help !
1) In your original JSON the customers array should only contain customers who are present in the state list.
2) This depends on your app. Generally you would want /customers to return all customers. Or maybe a paginated collection of customers. Or maybe only customers that the currently logged in user is allowed to see. Etc...
3) When you edit a customer Ember should already have the customer data loaded, so no additional lookup request should be required.