Customizing what Ember Data sends to the server? - ember.js

Two of the models in my application are projects and users. Projects can have many users assigned to them and vice versa. I'm using Ember Data, so my model looks like this:
App.Project = DS.Model.extend({
name: DS.attr('string'),
created: DS.attr('date'),
users: DS.hasMany('App.User')
});
When creating a project on the server, the API expects to receive the project's name AND an array of IDs corresponding to the project's users. So, basically, something like this:
POST /projects
{
project: {
name: 'My Project',
users: [1, 10, 14]
}
}
However, Ember Data isn't including the array of user IDs when sending a POST or PUT request. By default, it only includes the name attribute. How can I modify Ember Data to include what I need? Is it even worth doing, or should I go the Discourse route and abandon Ember Data for now?

Assuming you are using the latest version and the RESTAdapter/RESTSerializer, you can override the addHasMany method of the serializer.
So, here is an example of how to do this:
App.CustomSerializer = DS.RESTSerializer.extend({
addHasMany: function(hash, record, key, relationship) {
var ids = record.get(relationship.key).map(function(item) {
return item.get('id');
});
hash[relationship.key] = ids;
},
});
App.Store = DS.Store.extend({
adapter: DS.RESTAdapter.extend({
serializer: App.CustomSerializer.create()
})
});
Note that the addHasMany implementation is taken from https://github.com/emberjs/data/blob/master/packages/ember-data/lib/serializers/fixture_serializer.js#L41

Related

Accessing store in another route

I have this model:
App.Game = DS.Model.extend({
name: attr(),
uri: attr()
});
and this route:
App.GamesRoute = Ember.Route.extend({
model: function() {
return this.store.find('game');
}
});
This works fine, calls the backend server, and stores elements in the store (I've checked with Ember inspector). This is the json I return:
{"games":[{"id":"TicTacToe","name":"TicTacToe","uri":"http://localhost:10000/games/TicTacToe"}]}
Now I have this template for 'games' (snipped):
{{#each game in model}}
{{#link-to 'games.matchlist' game.id}}{{game.uri}}{{/link-to}}
This shows the URI for each game. Now in the games.matchlist route what I would like to do is to search in the store by the game_id received param and get the game URI. The reason is that the server doesn't follow RESTAdapter conventions, and I would like to make a custom AJAX query to that URI myself.
This doesn't work:
App.GamesMatchlistRoute = Ember.Route.extend({model: function(params) {
var store = this.store;
var game = store.find('game', params.game_id)
console.log(game);
console.log("URI: " + game.uri);
at this point, game is an object but it's not an instance of my model. It doesn't have a uri attribute. What am I doing wrong? I'm feeling that I'm missing something obvious.
If you want to get records without hitting the server and you know you already have it in the store, use this.store.getById('game', ID).
I'm on my mobile, but you need to create a GameAdapter and customize I believe the fetch function. Checkout the docs for adapters on the ember site and you should have your answer.
Your other option is to fetch the data from your server and use this.store.pushPayload(data).
Docs here: http://emberjs.com/api/data/classes/DS.Store.html#method_pushPayload
And the adapter docs here: http://emberjs.com/guides/models/customizing-adapters/

Ember Data Endpoint Issue

I am experiencing a weird issue while using ember data. With the following user model everything works great.
App.User= DS.Model.extend({
firstName: attr(),
lastName: attr()
});
I call user.save() and is posts to /users with the correct data. However when i try and use a user model that has relationships on it
App.User= DS.Model.extend({
firstName: DS.attr('string'),
lastName: DS.attr('string'),
friends: DS.hasMany('user'),
followers: DS.hasMany('user'),
});
For some reason with that model when i call user.save() it posts to /Users (note the capitalization. Also, in the response it expects it formatted {"User": {...}} instead of {"user": {...}}
Anyone run into this before? I could always add the additional endpoints to my api however I would like it to work uniform if possible.
I did a little more digging and it seems when you add a relationship to the model there is a computed property called relationshipsByName. This property, in my example, will set the meta.type property to 'User'. It works without relationships because I called the createRecord method with 'user' so i assume it uses this as the type. When the relationship is added it uses 'User'
I found that modelFor calls the resolvers normalize on the keys. So the solution is to add a custom resolver like below.
App = Ember.Application.create({
Resolver: Ember.DefaultResolver.extend({
normalize: function(fullName) {
var n = this._super(fullName);
if(fullName.startsWith('model')){
n = n.replaceAt(6, n[6].toLowerCase());
}
return n;
}
})
});
*note i have string extensions for startsWith and replaceAt

Ember Data 1.0.0: what is expected format for belongsTo relationship

I have the following models:
App.Publication = DS.Model.extend({
title: DS.attr('string'),
bodytext: DS.attr('string'),
author: DS.belongsTo('author')
});
App.Author = DS.Model.extend({
name: DS.attr('string')
});
And the folowing json data:
{
"publications": [
{
id: '1',
title: 'first title',
bodytext: 'first body',
author_id: 100
},
{
id: '2',
title: 'second title',
bodytext: 'second post',
author_id: 200
}
];
}
In Ember Data RC12 this worked (you could specify author_id OR author in the json and the publication would always have the correct author linked).
In Ember Data 1.0.0 this no longer works; author is always null.
In some documents I found that - since I am using "author_id" in the json data (and not simply author) - I need to specify the key in the model; thus:
author: DS.belongsTo('author', { key: 'author_id' })
This however does not work; the author in the publication remains null.
The only solution I see for now is to implement a custom serializer and override the author_id to author (via normailzeId); I cannot change my backend data structure ... thus:
App.MySerializer = DS.RESTSerializer.extend({
//Custom serializer used for all models
normalizeId: function (hash) {
hash.author = hash.author_id;
delete hash.author_id;
return hash;
}
});
Is the above the correct way ?
Ember Data 1.0 no longer does any payload normalization by default. The key configuration for DS.belongsTo has been removed as well so you will have to implement a custom serializer.
normalizeId is an internal serializer function used for converting primary keys to always be available at id. You shouldn't override this.
Instead, you can override the keyForRelationship method which is provided for this purpose.
You could use something like the following:
App.ApplicationSerializer = DS.RESTSerializer.extend({
keyForRelationship: function(rel, kind) {
if (kind === 'belongsTo') {
var underscored = rel.underscore();
return underscored + "_id";
} else {
var singular = rel.singularize();
var underscored = singular.underscore();
return underscored + "_ids";
}
}
});
Note: I've also renamed the serializer to App.ApplicationSerializer so that it will be used as the default serializer for your application.
Finally, if you haven't already found it, please take a look at the transition notes here: https://github.com/emberjs/data/blob/master/TRANSITION.md
If you read through the transition document shortly after the initial 1.0.0.beta.1 release I would recommend taking a look again as there have been a number of additions, particularly regarding serialization.
From the Ember 1.0.0 Transition Guide:
Underscored Keys, _id and _ids
In 0.13, the REST Adapter automatically camelized incoming keys for you. It also expected belongsTo relationships to be listed under name_id and hasMany relationships to be listed under name_ids.
If your application returns json with underscored attributes and _id or _ids for relation, you can extend ActiveModelSerializer and all will work out of the box.
App.ApplicationSerializer = DS.ActiveModelSerializer.extend({});
Note: DS.ActiveModelSerializer is not to be confused with the ActiveModelSerializer gem that is part of Rails API project. A conventional Rails API project with produce underscored output and the DS.ActiveModelSerializer will perform the expected normalization behavior such as camelizing property keys in your JSON.

How to add in hasMany relationship with the Ember Data local storage adapter/serializer?

I am using the Ember local storage adapter for an app that has two models: Organizations and Members. The relationship is that Organizations "hasMany" Members, and Members "belongsTo" Organizations, as follows:
App.Org = DS.Model.extend({
name: DS.attr('string'),
description: DS.attr('string'),
members: DS.hasMany('App.Member')
});
App.Member = DS.Model.extend({
name: DS.attr('string'),
org: DS.belongsTo('App.Org')
});
In the jsbin of the app, you can add members and the organizations to the local storage, which works perfectly (meaning that when I add a new organization, its selected members are added in the "members" hasMany relationship).
In addition to adding the new organization information, I would like to serialize the newly created organization record to be sent over to a server via AJAX (no I not want to use the RESTAdapter).
However, the "members" hasMany relationship is completely missing from the serialization, so this is the serialization that I'm getting (you can see for yourself if you add some members and then open up the console):
{
name: "Organization name",
description: "description of the org"
}
and this is the serialization that I would like:
{
name: "Organization name",
description: "description of the org",
members: ["id_of_member_1", "id_of_member_2", "id_of_member_3"]
}
This is how I am doing the serialization:
App.OrganizationController = Ember.ArrayController.extend({
createOrg: function(){
var neworg = App.Org.createRecord({
name: this.get('newname'),
description: this.get('newdescription')
});
neworg.get('members').pushObjects(this.get('newmembers'));
neworg.save();
var serializer = DS.RESTSerializer.create({});
console.log(serializer.serialize(neworg));
}
...
If serialization is not the way to go, another way might be making "members" a string array attribute, but so far this path has failed me.
In addition to suggestions, please provide a working jsbin based on the current jsbin of the app. Because I've tried things that don't work.
Keep in mind this app is set up with an Ember Data local storage adapter.
You can do it with the DS.EmbeddedRecordsMixin. org-serializer.js would look like:
import DS from 'ember-data';
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs: {
members: { serialize: 'ids' }
}
});
See the documentation here.

Non-persistent attributes in EmberJS

Does anyone know of a way to specify for an Ember model an attribute which is not persisted?
Basically, we're loading some metadata related to each model and sending that data to Ember via the RESTAdapter within the model. This metadata can be changed in our app, but is done via using an AJAX call. Once the call succeeds, I want to be able to update this value within the model without Ember sticking its nose in this business by changing the model to the uncommitted and doing whatever it does with transactions behind the scenes.
I also have the problem that this metadata, which is not data from the model's database record, is passed by the RESTAdapter back to the server, which doesn't expect these values. I am using a RoR backend, so the server errors out trying to mass-assign protected attributes which aren't meant to be attributes at all. I know I can scrub the data received on the server, but I would prefer the client to be able to distinguish between persistent data and auxiliary data.
So, to the original question: is there any alternative to Ember-Data's DS.attr('...') which will specify a non-persistent attribute?
The other answers to this question work with Ember data versions up to 0.13, and no longer work.
For Ember data 1.0 beta 3 you can do:
App.ApplicationSerializer = DS.RESTSerializer.extend({
serializeAttribute: function(record, json, key, attribute) {
if (attribute.options.transient) {
return;
}
return this._super(record, json, key, attribute);
}
});
Now you can use transient attributes:
App.User = DS.Model.extend({
name: DS.attr('string', {transient: true})
});
These attributes won't be sent to the server when saving records.
When this PR get's merged it will be possible to flag properties as readOnly. But till then there are some workarounds to this, e.g. overriding your addAttributes method in the Adapter and deal with your special properties, here an example how this could look like:
Define your Model by adding the new option readOnly:
App.MyModel = DS.Model.extend({
myMetaProperty: DS.attr('metaProperty', {readOnly: true})
});
and then on the Adapter:
App.Serializer = DS.RESTSerializer.extend({
addAttributes: function(data, record) {
record.eachAttribute(function(name, attribute) {
if (!attribute.options.readOnly) {
this._addAttribute(data, record, name, attribute.type);
}
}, this);
}
});
what this does is to loop over the attributes of your model and when it find's an attribute with the readOnly flag set it skips the property.
I hope this mechanism works for your use case.
Following this answer, to prevent a field from being serialized, override the default serializer for your model:
In app/serializers/person.js:
export default DS.JSONSerializer.extend({
attrs: {
admin: { serialize: false }
}
});
See here for the source PR. This solution works in Ember Data 2, and should work in older versions as well.
Update
This answer is most likely out of date with the current releases of Ember Data. I wouldn't use anything in my answer.
I'm answering this question for reference, and because your comment indicated that the record remains isDirty, but here is my solution for read-only, non-persistent, non-dirty attributes.
Overriding the addAtributes method in your Serializer prevents readOnly attributes from being sent to the server, which is probably exactly what you want, but you need to extend (or reopen) your adapter to override the dirtyRecordsForAttributeChange to prevent the record from becoming dirty:
App.CustomAdapter = DS.RESTAdapter.extend({
dirtyRecordsForAttributeChange: function(dirtySet, record, attrName, newValue, oldValue) {
meta = record.constructor.metaForProperty(attrName);
if (meta && meta.options.readOnly) { return; }
this._super.apply(this, arguments);
};
});
Then you can use readOnly attributes like so:
App.User = DS.Model.extend({
name: DS.attr('string', {readOnly: true})
});
user = App.User.find(1); # => {id: 1, name: 'John Doe'}
user.set('name', 'Jane Doe'); #
user.get('isDirty') # => false
This setup is working for me.