I am trying to config an ember app using ember-data to connect and read data from one api.
My model is:
App.Movie = DS.Model.extend
title: DS.attr 'string'
rating_average: DS.attr 'string'
short_plot: DS.attr 'string'
free: DS.attr 'boolean'
My api returns:
{
"pagination": {
"count":xx,
"page": x,
"total_pages": x
},
"movies": [
{
"id": xxxx,
"title": "xxx",
"rating_average": "x",
"short_plot": "xxxx",
"already_seen": x,
....
....
When ember try lo load data it throws:
Assertion failed: Your server returned a hash with the key **pagination** but you have no mapping for it
Ember does not expect the "pagination" keys in the Json. How can I specify that only try to read from the key 'movies' ?
While it is possible to customize ember-data to handle this, consider using ember-model instead. ember-data works best when you have control over your API and can ensure that it follows a set of conventions. If that is not the case you will find yourself fighting to make ember-data work with your API and it was simply not designed for this use case. ember-model was designed for this use case and while it does less for you out-of-box it will be much easier to customize. See ember-model-introduction to learn more.
That said, to make things work with ember-data you'll need to extend the rest adapter and customize it's ajax function.
App.Store = DS.Store.extend({
adapter: 'App.Adapter'
});
App.Adapter = DS.RESTAdapter.extend({
ajax: function(url, type, hash) {
var promise = this._super(url, type, hash);
return promise.then(function(json) {
delete json.pagination;
return json;
});
}
});
In this custom adapter we are calling the rest adapter's ajax function - see source - which returns a promise. The promise's then() function can be used to strip the key pagination from the result. Of course that means the pagination key will be removed from every json response.
Related
I am trying to learn Ember by developing a simple TODO manager application.
I am using ember-data and JSONAPISerializer with a hand rolled REST JSON API backend for this application.
I have the following model which represents a task
app/model/task.js
export default DS.Model.extend({
title: DS.attr ('string'),
description: DS.attr ('string'),
isComplete: DS.attr ('boolean')
});
The corresponding JSON data from backend looks like this
{
"data": [{
"id": "1",
"type": "task",
"attributes": {
"title": "Complete Ember TODO manager application",
"description": "Build a simple Ember application for easily managing tasks",
"is-complete": "false"
}
}]
}
As per the convention, the Ember model uses camel cased names and the JSON API backend uses dasherized names.
A basic feature in this application is to filter tasks by their status, so basically one can see either ongoing or completed or all tasks.
For fetching only the ongoing tasks, I have the following query in the model hook of the corresponding route
app/routes/tasks/ongoing.js
export default Ember.Route.extend({
model () {
return this.get('store').query('task', {
filter: {
isComplete: 'false'
}
});
}
});
So when the query is sent to the backend it goes as
restjsonapi/tasks?filter[isComplete]=false
The problem is that the backend expects "is-completed" and does not understand "isComplete".
Is there a way to dasherize the URL emitted by Ember query?
Edit (possible workaround):
I might have been trying to solve this the wrong way I think. I have changed the JSON data to have underscores instead of dashes and worked around this problem.
When using store.query, Ember then finds the relevant adapter for your model type and calls adapter.query which in turn seems to pass your query to jQuery.ajax as a value for the data attribute. See here:
https://github.com/emberjs/data/blob/v2.7.0/addon/adapters/rest.js#L504
The conversion from Object to Query String is then done by jQuery using encodeURIComponent as you can see there: https://github.com/jquery/jquery/blob/e4fd41f8fa4190fbbb6cb98cf7ace64f6e00685d/src/serialize.js#L68
So you won't be able to change this behaviour by passing an option to Ember.
However, you could try something like:
export default Ember.Route.extend({
model () {
return this.get('store').query('task', {
filter: {
"is-complete": 'false'
}
});
}
});
And it should work.
Ember : 1.13.3
Ember Data : 1.13.5
jQuery : 1.11.3
I am trying to send a JSON payload using ember-data from my EmberJS client to my server. I want to send the entire object graph to the server on saving the project, as I don't want to send multiple requests. I wouldn't mind sending multiple requests, but I am worried about what happens if one of the requests fails in the middle and the data on the server will not be correct.
I wanted to use JSONAPI (http://jsonapi.org/format/#document-compound-documents) as that is becoming the default adapter in Ember. Also, there is a few C# libraries that handle this format, so I thought it would be quite straightforward. However, after reading the spec, it seems that I cannot embed objects if they do not have an id. EmberJS also does not attach the child objects to the JSON either even though I have specified { async: false, embedded: 'always' }) on the DS.attr.
My question is: If an application is used in such a way that an object graph is created on the client side, how do you use JSONAPI format to send the entire object graph to the server? Do I have to generate ids on the client side to satisfy the JSONAPI standard? Then once they get to the server just ignore them so they get saved with an id generated by the ORM?
Here is my labelGroup model:
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string'),
labels: DS.hasMany('label-model', { async: false, embedded: 'always' })
});
Here is my project model:
import DS from 'ember-data';
export default DS.Model.extend(DS.EmbeddedRecordsMixin, {
name: DS.attr('string'),
labelGroups: DS.hasMany('labelGroup', { async: false, embedded: 'always'})
});
Here is the POST that I get after doing a save() on the project:
{
"data":{
"attributes":{"name":"Project"},
"relationships":{
"label-groups":{
"data":[
{"type":"label-groups","id":null},
{"type":"label-groups","id":null},
{"type":"label-groups","id":null},
{"type":"label-groups","id":null},
{"type":"label-groups","id":null},
{"type":"label-groups","id":null},
{"type":"label-groups","id":null}
]
}
},
"type":"label-projects"
}
}
UPDATE: I tried using https://www.npmjs.com/package/ember-cli-uuid to generate client side ids which it has. However the data getting output does not include the extra objects, only a reference to their ids. I expected to see an "included" property as specified here:http://jsonapi.org/format/#document-compound-documents, but it is not there.
{
"data":{
"id":"7b4544ee-91cd-493d-8b10-52040e68c283",
"attributes":{"name":"Project"},
"relationships":{
"label-groups":{
"data":[
{"type":"label-groups","id":"08115273-e82a-4d46-93ea-232ce071fb78"},
{"type":"label-groups","id":"9ca94fe9-8077-411e-98d2-1694c6fecce4"},
{"type":"label-groups","id":"d629f1e8-7962-404d-8034-38229ab21f77"},
{"type":"label-groups","id":"c6bda655-5489-4760-847b-bf02239bb2c5"},
{"type":"label-groups","id":"f6fef249-2d1d-43f0-ba64-24b7ff8b5637"},
{"type":"label-groups","id":"a7db25bf-52c8-477b-83e4-64e7c76b072e"},
{"type":"label-groups","id":"f3b5fbb3-261a-4b3d-b481-b9352f8ce2d6"}
]
}
},
"type":"label-projects"
}
}
Ember-data has no support for what you want at the moment. So ember-data will not save your relationships data in a save payload.
But its possible to do this your own by using a custom adapter and serializer.
I strongly recommend you to checkout the API and then look into the source.
If you call .save() on your Model the createRecord method is called on your adapter.
Here serializeIntoHash on the serializer is called to serialize the Model.
serializeIntoHash calls serialize, where serializeBelongsTo and serializeHasMany is called.
Now you can just override serializeHasMany and modify the hasMany before the line:
json[payloadKey] = hasMany;
Here you have the type and the ids as they are sent by ember-data. You could just .forEach the data on the hasMany and then fetch the store for the data and build your included array.
I hope this helps you to understand the serializer and the adapter so you can modify it to do whatever you want pretty easy. Actually this is the best part about ember-data. The structure of the adapter and the serializer, which allows easy modifications.
I want to use Ember Data but the API I am coding against does not respect all the conventions.
My problem is that the API does not return sideloaded relationships but relationships should be loaded using a REST structure: /model/id/relationship.
I have the following model:
Library = DS.Model.extend
name: DS.attr 'string'
groups: DS.hasMany 'group', {async: true}
groupsCount: DS.attr 'number'
The payload looks like:
{
library: {
groups_count: 44,
name: "demo",
id: "545262a063726d2514390100"
}
}
When I attempt to load groups using library.get('groups') nothing happens. It should make a call to libraries/545262a063726d2514390100/groups
Can I change my RESTAdapter to make this work?
After some research I found a way that works for me:
LibrarySerializer = DS.RESTSerializer.extend
normalize: (type, hash, prop)->
hash.links =
groups: "groups"
#_super(type, hash, prop)
This essentially adds a links object to the response, making the rest adapter follow that path to retrieve the relationship.
EDIT:
I've gotten around this by upgrading to EmberJS RC4. This version doesn't automatically call the model hook on routes, which allows the following:
App.LiftsRoute = Ember.Route.extend({
setupController: function(controller, model) {
controller.set('content', App.Lift.find({
county: model.county || model.id
}));
}
});
EDIT END
I'm attempting to add a route with a dynamic segment in EmberJS & Ember Data w/ RESTful Adapter which returns an array but I failing.
App.Router.map(function() {
this.route('lifts', { path: '/lifts/:county' });
});
App.LiftsRoute = Ember.Route.extend({
model: function(params) {
return App.Lift.find(params.county);
}
});
App.Lift = DS.Model.extend({
name: DS.attr('string'),
date: DS.attr('number'),
description: DS.attr('string'),
destination: DS.attr('string')
});
This is returning the following error:
Uncaught Error: assertion failed: Your server returned a hash with the key lifts but you have no mapping for it.
From JSON in the form {lifts: [{id: 1, name: "xyz", ...}, {id: 2, name: "abc", ...]}
Any ideas?
EDIT: Setting up a route with a single dynamic segment to return an array of objects
You can still keep the same route structure:
this.route('lifts', { path: '/lifts/:county_ids' });
And then override the model hook to parse params.county_ids into a query string:
model: function(params) {
ids = parseQueryIds(params.county_ids) // you have to parse this in a format that your server will accept
App.Lift.find({query: ids}) // returns an record array
}
This will preserve the url structure (if you go to /lifts/1,2,3, that url will be saved) but also returns an array of items.
END EDIT
This is happening because App.Lift.find, when passed a string, will try to query by id for a single object, but your response from the server is returning multiple objects (id 1, id 2, etc).
When you do App.Lift.find(params.county) (let's say params.county is "1"), Ember will make a GET '/lifts/1'. But for whatever reason, your server is returning JSON with a key that has an array.
Can you check that
the GET request ember is making is indeed for a single id? If you're using chrome, check the network requests -- what is the resource that App.Lift.find(params.county) asks for?
that params.county is defined? If it's undefined, you'll be calling App.Lift.find(undefined), which makes the GET to /lifts, and that might cause your server to return the array of objects.
that your server is responding to requests properly when a single id is requested?
the error message occurs because the root id of your JSON object is plural, and should be singular. Your server should return:
{lift: [
{id: 1, name: "xyz", ...},
{id: 2, name: "abc", ...}
]
}
You will most likely subsequently run into the issue Sherwin describes, as find() for the RESTAdapter assumes a singleton to be returned.
I am using the Ember-Data Rest-Adapter and the JSON returned from my server looks basically like the one in the Active Model Serializers Documentation
{
"meta": { "total": 10 },
"posts": [
{ "title": "Post 1", "body": "Hello!" },
{ "title": "Post 2", "body": "Goodbye!" }
]
}
Fetching the data from the server works but unfortunately I am not able to figure out where I can access the meta information from my JSON response.
Based on my research in ember-data's github issue, support for meta information seems to be implemented with commit 1787bff.
But even with the test cases I was not able to figure out how to access the meta information.
App.PostController = Ember.ArrayController.extend({
....
requestSearchData: function(searchParams){
posts = App.Post.find(searchParams);
this.set('content', posts);
// don't know how to access meta["total"]
// but I want to do something like this:
// this.set('totalCount', meta["total"])
}
})
Can anybody of you shed some light on this for me, please? I am aware that the Ember api is moving fast but I am sure I am just missing a small part and that this is actually possible.
I found a cleaner approach for extracting meta information from the server response with ember-data.
We have to tell the serializer which meta-information to expect (in this case pagination):
App.serializer = DS.RESTSerializer.create();
App.serializer.configure({ pagination: 'pagination' });
App.CustomAdapter = DS.RESTAdapter.extend({
serializer: App.serializer
});
App.Store = DS.Store.extend({
adapter: 'App.CustomAdapter'
});
After that every time the server sends a meta-property with a pagination object this object will be added to the store's TypeMaps property for the requested Model-Class.
For example with the following response:
{
'meta': {'pagination': { 'page': 1, 'total': 10 } },
'posts':[
...
]
}
The TypeMap for the App.Post-Model would include the pagination object after the posts have loaded.
You can't observe the TypeMaps-property of the store directly so I added an computed property to the PostsController to have access to the requests pagination meta information:
App.PostsController = Ember.ArrayController.extend({
pagination: function () {
if (this.get('model.isLoaded')) {
modelType = this.get('model.type');
this.get('store').typeMapFor(modelType).metadata.pagination
}
}.property('model.isLoaded')
});
I really don't think that's a great solution to the meta information problem but this is the best solution I was able to come up with yet with Ember-Data. Maybe this will be easier in the future.
I figured out a way to access the meta information passed in a response. But unfortunately it really does not seem to be supported by ember-data out of the box and I am writing the meta information to a global variable that I am then accessing via bindings in the requesting controller.
I ended up customizing the serializer the RESTAdapter is using:
App.CustomRESTSerializer = DS.RESTSerializer.extend({
extractMeta: function(loader, type, json) {
var meta;
meta = json[this.configOption(type, 'meta')];
if (!meta) { return; }
Ember.set('App.metaDataForLastRequest', meta);
this._super(loader, type, json);
}
});
App.Store = DS.Store.extend({
revision: 11,
adapter: DS.RESTAdapter.create({
bulkCommit: false,
serializer: App.CustomRESTSerializer
})
});
I am aware that this is not particularly pretty and actually think that this is against what ember-data expects us to do but fot the time being it's working correctly.
I will try to get this working with ember-data in a better way and submit a pull request when it is working or open an issue on github when anybody else is interested in getting this to work.
If anybody finds a more clean solution to this please let me know.
I think the immediate fix for you would be to attach totalCount to your model(recordArray), see this thread.
Another way to go would be to create your own adapter:
DS.Adapter.create({
find: function (store, type, id) {
$.ajax({
url: type.url,
dataType: 'jsonp',
context: store,
success: function(response){
this.load(type, id, response.data);
}
});
},
findAll: function(store, type) {
$.ajax({
url: type.url,
dataType: 'jsonp',
context: store,
success: function(response){
this.loadMany(type, response.data);
}
});
}
});
Response parameter in success callback in findAll method, should be an object that you need:
response: {
meta: {
totalCount: 10
},
posts: [{}, {}]
}
Hope this helps.