Ember-Data: How do "mappings" work - javascript

I'm currently trying to put something together with ember + emberdata + router + asp.net web api. Most of it seem to work, however I stuck in an error message I get when ember-data tries to findAll through the adapter for my models.
In my backend I have a model like this (C#):
public class Genre {
[Key]
public int Id { get; set; }
[Required]
[StringLength(50, MinimumLength=3)]
public string Name { get; set; }
}
Which in my app I represent it like this using ember-data:
App.Genre = DS.Model.extend({
id: DS.attr("number"),
name: DS.attr("string")
}).reopenClass({
url: 'api/genre'
});
I have also a Store defined in my App using the RESTAdapter like so:
App.store = DS.Store.create({
revision: 4,
adapter: DS.RESTAdapter.create({
bulkCommit: false
})
});
And the store is used in my controller as below:
App.GenreController = Ember.ArrayController.extend({
content: App.store.findAll(App.Genre),
selectedGenre: null
});
The router is defined as
App.router = Em.Router.create({
enableLogging: true,
location: 'hash',
root: Ember.Route.extend({
//...
genre: Em.Route.extend({
route: '/genre',
index: Ember.Route.extend({
connectOutlets: function (router, context) {
router.get('applicationController').connectOutlet('genre');
}
})
}),
//...
})
})
When I run my application, I get the following message for every object that has this same structure:
Uncaught Error: assertion failed: Your server returned a hash with the
key 0 but you have no mappings
For reference, here's the json the service is returning:
[
{
"id": 1,
"name": "Action"
},
{
"id": 2,
"name": "Drama"
},
{
"id": 3,
"name": "Comedy"
},
{
"id": 4,
"name": "Romance"
}
]
I cannot tell exactly what the problem is and since the assertion is mentioning that I need mapping, I'd like to know:
What this mapping is and how to use it.
Since the returned json is an array, should I be using a different type of controller in my app ,or is there anything I should know about when working with this type of json in ember-data? or should I change the JsonFormatter options in the server?
Any help is welcome.
I can definitely add more information if you feel this isn't enough to understand the problem.
EDIT: I've changed a few things in my backend and now my findAll() equivalent action in the server serializes the the output as the following json:
{
"genres": [
{ "id": 1, "name": "Action" },
{ "id": 2, "name": "Drama" },
{ "id": 3, "name": "Comedy" },
{ "id": 4, "name": "Romance" }
]
}
But I still can't get it to populate my models in the client and my error message has changed to this:
Uncaught Error: assertion failed: Your server returned a hash with the
key genres but you have no mappings
Not sure what else I might be doing wrong.
The method that throws this exception is sideload and checks for the mappings like this:
sideload: function (store, type, json, root) {
var sideloadedType, mappings, loaded = {};
loaded[root] = true;
for (var prop in json) {
if (!json.hasOwnProperty(prop)) { continue; }
if (prop === root) { continue; }
sideloadedType = type.typeForAssociation(prop);
if (!sideloadedType) {
mappings = get(this, 'mappings');
Ember.assert("Your server returned a hash with the key " + prop + " but you have no mappings", !!mappings);
//...
This call sideloadedType = type.typeForAssociation(prop); returns undefined and then I get the error message. The method typeForAssociation() checks for the for 'associationsByName' key which returns an empty Ember.Map.
Still no solution for this at the moment.
By the way...
My action is now like this:
// GET api/genres
public object GetGenres() {
return new { genres = context.Genres.AsQueryable() };
}
// GET api/genres
//[Queryable]
//public IQueryable<Genre> GetGenres()
//{
// return context.Genres.AsQueryable();
//}
I had to remove the original implementation which gets serialized by json.NET as I could not find config options to produce a json output as Ember-Data expects ( as in {resource_name : [json, json,...]}). Side effect of this is that I've lost built-in OData support, but I'd like to keep it. Does anyone know how could I configure it to produce different json for a collection?

The mapping can be defined in the DS.RESTAdapter. I think you could try to define something like this:
App.Store = DS.Store.extend({
adapter: DS.RESTAdapter.create({
bulkCommit: true,
mappings: {
genres: App.Genre
},
// you can also define plurals, if there is a unregular plural
// usually, RESTAdapter simply add a 's' for plurals.
// for example at work we have to define something like this
plurals: {
business_process: 'business_processes'
//else it tries to fetch business_processs
}
}),
revision: 4
});
Hope this resolves your problem.
Update:
At this time, this is not well documented, I don't remember if we found it by ourself reading the code, or perhaps Tom Dale pointed on it.
Anyway, here is the point for plurals
For the mappings, I think we were driven by the same error as you, and either we tried, either Tom teached us about this.

The RESTAdapter expects the returned JSON to be of the form:
{
"genres": [{
"id": 1,
"name": "action"
},{
"id": 2,
"name": "Drama"
}]
}
The tests are a good source of documentation, see https://github.com/emberjs/data/blob/master/packages/ember-data/tests/unit/rest_adapter_test.js#L315-329

I'm using Ember Data rev. 11 and it seems that the plurals config in DS.RESTAdapter.create never works. I looked into the codes and found a solution as following:
App.Adapter = DS.RESTAdapter.extend({
bulkCommit: false
})
App.Adapter.configure('plurals', {
series: 'series'
})

Related

Ember.js linking application to real api after mirage success

As I'm going deeper and deeper into Ember.js application building process I hit another wall.
Before I was using mirage with great success - I just copy output from API that I wanted build around to mirage fixtures and it was working great.
Now I have problem with making it work with real API.
I first disabled mirage in config/environment.js
ember g adapter filter
import ApplicationAdapter from './application';
export default ApplicationAdapter.extend({
findAll: function(store, type, label) {
var url = `${this.host}/${this.namespace}/${type.modelName}`;
console.log(`${url}`);
return this.ajax(url, 'GET');
},
});
The application adapter looks like this
import DS from 'ember-data';
export default DS.JSONAPIAdapter.extend({
host: 'http://127.0.0.1:1234',
namespace: 'api',
headers: Ember.computed(function(){
return {"secret": "1234"};
})
});
And that way when I enter /filter
app/routes/filter/index
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return this.store.findAll('filter');
}
});
I can see that url is build ok as http://127.0.0.1:1234/api/filter and there is no 404 but I get error
Error while processing route: filter.index The adapter operation was aborted EmberError#http://127.0.0.1:4200/assets/vendor.js:29616:15
and as I don't fully grasp the know-how of Ember Inspector im trying to figure this out somehow
My filter model that worked before (with mirage) looks like this:
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr(),
url: DS.attr()
});
The api returns list [{"id":1, "name": "namex", "url": "http://"},{"id":2, "name": "namey", "url": "http://"}]
I'm sure If its the way api return data as its not "filters" as I had problem with plurals before.
Edit: THIS RESOLVE THe problem
import DS from 'ember-data';
export default DS.RESTSerializer.extend(
{
normalizeFindAllResponse(store, type, payload)
{
var data = [];
payload.forEach(
function(item, index, enumerable)
{
var ob = {};
Ember.set(ob, 'id', item.id);
Ember.set(ob, 'type', 'filter');
Ember.set(ob, 'attributes', item);
data[index]=ob;
console.log(data[index]);
}
);
return {
data: data
};
}
});
This is almost the fix but not quite.
I can access some of model attributes like name, but there is a array object with 3 arrays inside, 2 of them are array the one in middle is Getter (what ever is that) and I cannot access it as its not array anymore. So im not sure if it binding to object correctly this way. Also I wasn't able to do anything with "data" because no matter what RESTAdapter here and there I put it would ignore it and ask for object with data/meta/errors attribute... dunno if its a bug or not.
As you are using JSONAPIAdapter your api response should follow the JSON format specification.
{
"data": [{
"type": "filter",
"id": "1",
"attributes": {
"name": "namex",
"url": "http://"
}
}, {
"type": "filter",
"id": "2",
"attributes": {
"name": "namey",
"url": "http://"
}
}]
}
or if you are not following JSONAPI in that case either you can change application adapter to extend RESTAdapter.
import DS from 'ember-data';
export default DS.RESTAdapter.extend({
host: 'http://127.0.0.1:1234',
namespace: 'api',
headers: Ember.computed(function(){
return {"secret": "1234"};
})
});
RESTAdapter response would be like,
{"filters":[{"id":1, "name": "namex", "url": "http://"},{"id":2, "name": "namey", "url": "http://"}]}
either you need to send it like above format or manipulate it to produce the required format.(override normalizeFindAllResponse)

creating an ember-data application serialzer

I've been trying to use ember data for a few days now. The problem is the api I have does not match the strcuture ember-data would like to receive.
item:
{
"data": {
"id": 1,
"username": "rodzzlessa",
"slug": "rodzzlessa"
}
}
collection:
{
"data": [
{
"id": 34,
"name": "HDG common nails 50lbs",
"slug": "hdg-common-nails-50lbs4569",
"description": "Accusantium ipsam impedit omnis sint dolorum.",
"image_src": "nail-box-angle.png",
"categories": {
"data": [
{
"id": 2,
"name": "nails",
"image_src": "nails-icon.png"
}
]
}
}
]
}
from what I've seen I need to modify the extractSingle and extractArray methods. There really isn't an online resource showing what the methods are really doing and how to modify them. I check the ember-data api guide but the example is so specific and its not an application serializer so it seems much easier the problem they are solving. I tried finding an ember-data book but they don't exist yet. Are there any online resources to help me?
/***** UPDATE SO FAR ******/
So I was able to create a application serializer to handle the collection and items:
export default DS.RESTSerializer.extend({
extractArray: function(store, typeClass, payload) {
var root_key = Ember.Inflector.inflector.pluralize(typeClass.typeKey);
payload[root_key] = payload.data;
delete payload.data;
return this._super(store, typeClass, payload);
},
extractSingle: function(store, typeClass, payload, id){
var root_key = typeClass.typeKey;
payload[root_key] = payload.data;
delete payload.data;
return this._super(store, typeClass, payload, id);
}
});
Now the only problem I have is dealing with relationships I started messing around with the normalize method this is what I have so far:
normalize: function(typeClass, hash, prop){
self = this;
typeClass.eachRelationship(function(key, relationship){
if(relationship.kind === "hasMany"){
if( ! hash[key]){
return hash;
}
var hashed = hash[key].data;
hash[key] = hashed;
return hashed;
}
});
return this._super(typeClass, hash, prop);
}
Now the error I get from ember-data is this:
Assertion Failed: Ember Data expected a number or string to represent the record(s) in the `categories` relationship instead it found an object

Ember data 1.0 beta how map embedded data

It seems that ember data have many changing up to version 1.0 beta. All works great with version 0.13. now I want update to higher version. In my case we have an embedded model 'user'
App.Post = DS.Model.extend({
subject: DS.attr('string'),
created: DS.attr('number'),
fcreated: function(){
debugger;
var d = new Date(this.get('created'));
return d.toLocaleDateString();
}.property('created'),
reporter: DS.belongsTo('user')
}
App.ApplicationAdapter = DS.RESTAdapter.extend({
namespace: 'restws'
});
The Json from server looks like this.
{
"posts": [
{
"id": "5226f2670364e70ae7d77266",
"subject": "Text",
"created": 1325410935048,
"reporter": {
"id": "5226f2660364e70ae7d771e2",
"firstName": "Doris",
"lastName": "Baumertr"
}
}
I get the following error code 'Uncaught TypeError: Cannot call method 'toString' of undefined'. In the ember source code I see, that in ember-data.js line 2236 the function throw the error 'buildRecord: function(type, id, data) .. ' After debugging I see that the properties type is undefined id is set with the correct id and data is undefined?
What is the mistake? How I can map the embedded data?
Here's actually the exact extractSingle method that you need to implement
App.PostSerializer = DS.RESTSerializer.extend({
extractSingle: function(store, type, payload, id, requestType) {
if(typeof payload.post.reporter !== "undefined") {
var reporter_id = payload.post.reporter.id;
payload.users = [payload.post.reporter];
payload.post.reporter = reporter_id;
}
return this._super.apply(this, arguments);
}
});
Here's a jsbin http://jsbin.com/EKItexU/1/edit?html,js,output
Note, that I had to redefine ajax method in RESTAdapter to emulate the server returning your JSON.
Also, if you are sideloading users in your JSON, than you'll have to update this method so it doesn't overwrite sideloaded users in your payload (payload.users property)
Support for embedded records is gone (for now).
You can handle embedded records yourself by implementing extractSingle and reorganizing your JSON payload.
Please read here for more info about the transition: https://github.com/emberjs/data/blob/master/TRANSITION.md#embedded-records
Hope it helps.

How to create a custom Serializer for Ember data

I have an API that returns JSON that is not properly formatted for Ember's consumption.
Instead of this (what ember is expecting):
{ events: [
{ id: 1, title: "Event 1", description: "Learn Ember" },
{ id: 2, title: "Event 2", description: "Learn Ember 2" }
]}
I get:
{ events: [
{ event: { id: 1, "Event 1", description: "Learn Ember" }},
{ event: { id: 2, "Event 2", description: "Learn Ember 2" }}
]}
So if I understood correctly, I need to create a custom Serializer to modify the JSON.
var store = DS.Store.create({
adapter: DS.RESTAdapter.create({
serializer: DS.Serializer.create({
// which hook should I override??
})
})
});
I've read the code comment related to the DS.Serializer, but I can't understand how to achieve what I want...
How can I do it?
ps: My goal is to make App.Event.find() work. Currently, I get Uncaught Error: assertion failed: Your server returned a hash with the key 0 but you have no mapping for it. That's why I need to fix the JSON received.
edit: Here's how I made it work, for now:
extractMany: function(loader, json, type, records) {
var root = this.rootForType(type),
roots = this.pluralize(root);
json = reformatJSON(root, roots, json);
this._super(loader, json, type, records);
}
I am assuming that the responses contain the IDs only, and that you are trying to extract them.
You will want to subclass DS.JSONSerializer, which supplies the basic behavior for dealing with JSON payloads. In particular, you will want to override the extractHasMany hook:
// elsewhere in your file
function singularize(key) {
// remove the trailing `s`. You might want to store a hash of
// plural->singular if you deal with names that don't follow
// this pattern
return key.substr(0, key.length - 1);
}
DS.JSONSerializer.extend({
extractHasMany: function(type, hash, key) {
return hash[key][singularize(key)].id;
}
})

Ember Data not mapping relations with Padrino API

This is driving me nuts. I have a simple data model set up (using Padrino); I'm long past the stage of actually getting any error messages but adding 'App.Repo' models to an 'App.Stack' model just…doesn't work.
App.Store = DS.Store.extend({
revision: 10
adapter: DS.RESTAdapter.create({
bulkCommits: false,
mappings: {
stars: App.Stars,
stacks: App.Stacks
}
})
});
App.Stack = DS.Model.extend({
url: DS.attr('string'),
repos: DS.hasMany('App.Repo')
});
App.Repo = DS.Model.extend({
name: DS.attr('string'),
url: DS.attr('string'),
description: DS.attr('string'),
language: DS.attr('string'),
watchers: DS.attr('number'),
stack: DS.belongsTo('App.Stack'),
stackId: DS.attr('number')
});
var store = App.get('router.store');
newStack = store.createRecord(App.Stack);
console.log(newStack.serialize())
-> Object {url: null} // no mention of a repos array like I was expecting?
newStack.set('url', 'http://google.com');
console.log(newStack.serialize());
-> Object {url: "http://google.com"} // this works though
var repo = App.Repo.find().objectAt(0);
console.log(repo.serialize());
-> Object {name: "floere/james", url: "https://github.com/floere/james", description: "Voice commanded servant for OSX", language: "Ruby", watchers: 97…}
// so this exists too…
repos = newStack.get('repos');
repos.pushObject(repo);
newStack.get('repos.length'); // 1 (repos.toArray() etc etc all work too)
// but then…
console.log(newStack.serialize())
-> Object {url: null}
// and so then I try to save the relationship on the server anyway…
store.commit()
=> {"stack"=>{"url"=>nil}} // in my Ruby server logos
The store is all set up fine talking to my back end (for example submitting a POST to /repo.json sends the correct request); it just doesn't recognise that App.Stack has any relation.
No idea what's going wrong or what to look at for help :(
Also
I tried making the relations in my Ruby console and then accessing them in a view. This is what happens
// in the router
router.get('applicationController').connectOutlet('body', 'stacks', router.get('store').findAll(App.Stack));
// in the view
<script type="text/x-handlebars" data-template-name="stacks">
{{#each stack in controller }}
{{stack.id}} // this works
{{stack.url}} // this works
{{stack.repos.length}} // this returns the correct count
{{#each repo in stack.repos}}
// this loops the right number of times. so there *is* something there. somehow.
{{repo}} // prints out <App.Repo:ember490>
{{repo.id}} // prints out [object Object]
{{/each}}
{{/each}}
On that last note - maybe a clue in the [object Object]?
I'm so lost :(
More Info:
I'm using Padrino with Mongoid, using RABL to give me JSON. As I said, I can query for & template out my Stack & Repo records. Here's a JSON sample for the /stacks.json endpoint
{
"stacks": [
{
"account_id": null,
"id": "50c127ff6f094144ed000001",
"stars": [
{
"description": "Voice commanded servant for OSX",
"id": "50c128996f0941cfe8000001",
"name": "floere/james"
}
]
}
]
}
I think you'll have to add hasMany relationships to your json object manually by looping through the repos array. I'm doing this in my adapter's createRecord method.
createRecord: (store, type, record) ->
data = {}
data[root] = #toData(record, { includeId: true })
repos = []
stack.get("repos").forEach (repo) ->
repos.pushObject repo.serialize()
data[root]["repos"] = repos
...
I've found a way to get embedded related objects in the JSON to load properly. Basically you have to subclass the serializer and then in its initializer you tell it to register a map for the relationship. Here's an example for a model class called Category that has a to-many relationship 'resourceTypes':
App.WOSerializer = DS.Serializer.extend({
init: function(){
this._super();
this.map(App.Category, {
resourceTypes: { embedded: 'load' }
});
}
});
My solution is further explained here.

Categories