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.
Related
I'm working on an Ember app that is using Ember Data and the now default json-api adapter.
According to the json-api spec (http://jsonapi.org/format/#crud-deleting) when deleting a record your server should return a 200 response if the deletion is successful and the server responds with just a top level meta key.
My current server does just this and I'm trying to figure out how to access the data in the top level meta object when using Ember Data's model.destroyRecord() method.
myModel.destroyRecord().then(function(model){
// the returned value is the model. How can I get the actual metadata
// returned by the server?
});
The server response contains information about what exactly was deleted and looks like this:
{
"meta": {
num-deleted-a: 10,
num-deleted-b: 100,
num-deleted-c: 200
}
}
I'd like to get this information so I can display it to the user.
Thank you!
I am using the following versions:
Ember : 2.2.0
Ember Data : 2.3.3
jQuery : 1.11.3
After upgrading to Ember 2.6.1 and Ember 2.6.1 I was no longer able to access the store._metadataFor property.
To get access to the metadata from a particular call I now override the serializer for the model and add a meta property to the model itself that simply passes through the metadata.
As an example I have a record type called vote which when saved returns some metadata.
For the model I do the following:
// Vote Model (/app/models/vote)
export default DS.Model.extend({
vote: DS.attr('number'),
// Since i don't provide a transform the values for meta are passed through in
// raw form
meta: DS.attr()
});
Then in the serializer for the vote model I do the following:
// Vote serializer (/app/serializers/vote)
import DS from "ember-data";
export default DS.JSONAPISerializer.extend({
normalizeSaveResponse(store, primaryModelClass, payload, id, requestType) {
// The metadata in the payload does get processed by default and will be
// placed into a top level `meta` key on the returned documentHash
let documentHash = this._super(store, primaryModelClass, payload, id, requestType);
// Make sure we always have an empty object assigned to the meta attribute
if(typeof(payload.meta) !== 'object'){
payload.meta = {};
}
// Move the metadata into the attributes hash for the model
documentHash.data.attributes.meta = payload.meta;
return documentHash;
}
});
Note that in the above example I'm only adding in metadata to the vote model when making a save call to the store. If you wanted to always add in the metadata then you would override the normalize method instead of the normalizeSaveResponse method.
Then you can access a meta field in the results of your save call.
let vote = self.store.createRecord('vote', {
vote: voteValue
});
vote.save().then(function(result){
// this will now contain your metadata
console.info(result.get('meta'));
});
Ember does not support meta for single model requests (find,save and destroyRecord) at the moment!
If you want this you have to hook into ember internals.
The following code uses ember internals from ember 2.3 and may break in future versions!
There is the undocumented _metadataFor function on the store that gives you the last metadata for a given type. I use a custom initializer to always save it to the Model:
import Ember from 'ember';
import DS from 'ember-data';
const {set} = Ember;
export function initialize(application) {
DS.Model.reopen({
meta: null,
didCommit() {
this._super(...arguments);
set(this, 'meta', this.store._metadataFor(this.constructor.modelName));
}
});
};
export default {
name: 'meta',
initialize: initialize
};
After this you can do model.save().then(() => console.log(model.get('meta'))) or model.destroyRecord.then(() => console.log(model.get('meta'))).
Maybe checkout this ember-twiddle.
So let's say you have a User model which is set up to a fairly standard API. On the front end you have an Ember project which also has that User model. The normal creation call would be something like the following:
store.createRecord('user', {email: 'test#gmail.com'}).save();
This would send off a POST request to something like /api/users. However, something fairly extensive API's support is the creation of multiple models at once. So for instance, instead of the POST call just sending a single object under user: {email: 'test#gmail.com'} it would send an array of objects like users: [{email: 'test#gmail.com'}, {email: 'test2#gmail.com'}, ...].
How I have seen this handled in ember is to just do multiple creation calls at runtime. However, this is terribly inefficient and I am wondering if Ember supports saving multiple models at the same time? How would you achieve this in Ember?
You cannot save an array of models in a single POST request Ember Data as you describe it, however there is a way.
You can save a parent model which hasMany 'user' with the EmbeddedRecordsMixin, which will include either relationship ids or full records. Your serializer would look like -
import DS from 'ember-data';
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs: {
users: { embedded: 'always' },
}
});
Depending on your use case it may make sense to create a parent model only for this purpose which hasMany 'user'. If you want to use an existing model and don't always want to embed its user records there is an answer here.
If you do decide to save the models individually, you would want to do users.invoke('save'), which will trigger a POST for each model.
If you're asking specifically about Ember Data, I don't know of any way of doing that (I don't think it's possible to use any equivalent of save() on a collection/array). There could be alternative Data libraries that may work (for instance you could check Orbit.JS - which is something I haven't done yet)
The way I've done it it to have a custom endpoint on my backend that receives a certain JSON payload and creates the resources. You do it by issuing a regular ajax call, see this example (from a project of mine).
let content = //get content that you want to post
let accessToken = this.get('session.session.authenticated.token');
Ember.$.ajax({
data: JSON.stringify(content),
dataType: 'json',
method: 'POST',
url: 'path/to/my/custom/end/point',
headers: {
'Content-Type': 'application/json',
'Authorization': `Beader ${accessToken}`
}
}).then((result) => {
// Code for success
}, (jqXHR) => {
// Code for error
}).always(() => {
// Code for always/finally
});
As you can see this is all custom code, not leveraging Ember Data store or models. So far I haven't found a better answer.
EDIT: After seeing andorov's answer. I forgot to mention something. I'm using Ember Data 2.0 (JSONAPI by default) and EmbeddedRecordsMixin does not work property with JSON API
I was reading through Ember docs and some examples on working with Embedded Object like JSON in Ember.
I came across the EmbeddedRecordsMixin feature and saw that we can write code like below to tell it is embedded record.
import DS from 'ember-data';
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs: {
author: { embedded: 'always' },
}
});
Qouting the below from Ember page
Note that this use of { embedded: 'always' } is unrelated to the { embedded: 'always' } that is defined as an option on DS.attr as part of defining a model while working with the ActiveModelSerializer. Nevertheless, using { embedded: 'always' } as an option to DS.attr is not a valid way to setup embedded records.
And i have also seen model written like this.
App.Child = DS.Model.extend({
name: DS.attr('string'),
toys: DS.hasMany('toy', {embedded: 'always'}),
});
Where child object has toys object embedded.
Going by the first example, can i write the child serailizer as below?
App.ChildSerializer = DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs: {
toys: {embedded: 'always'}
}
});
Can someone help me understand the difference between these two {embedded: 'always'} and what to use when?
Thanks
Short answer: yes you can and you should.
Well, as far as I know, ember (especialy ember-data) is build to work perfectly with a Rails backEnd.
Rails have a module called ActiveModelSerializer to serialize resources and their related attributes and relationships. Into this module, you can use an option embedded: 'always' to serialize the whole targeted relationship and not only the ids when your client ask for a ressource.
If you use it Rails side (server), you can handle it Ember side (client) by putting this option into your model if you want your ember-data store to handle it easily. It's just an 'echo' to this ActiveModelSerializer module functionality.
On the other side, if for example you create/update an object with many relationships, there is 2 ways to deal with it. The first is to first save object's relationships and then, on success, save the object itself. The second is to send it at once to your server with the option {embedded: 'always'} into your model's serializer, into the relationship you want to send at the same time (embedded) at the object itself.
Ember probably encourage to use this into the serializer, because putting this into model seems only related to a specific Rails option, and it's not straightforward at all. Moreover, putting this into the serializer fulfill this role, with or without ActiveModelSerializer.
Hope it's clear for you, if not, let me know so I can edit this.
In my Ember app, I have a nested hasMany relationships using ember-data like so workout->exercise->set.
My API has nested JSON instead of sideloaded JSON, so to fetch an existing workout I use store.find('workout', id) and override extractSingle.
My problem is when building a new workout I need to prepopulate it with exercises and sets based on a workout plan that a user is following. On the server side, I just have a /new controller action that prepopulates everything and renders a template.
Now that I'm moving to Ember I need the same functionality, but can't seem to make it work. The first thing I tried was to use Ember.$.getJSON to call a custom API endpoint in conjunction with pushPayload. This doesn't work however, because pushPayload bypasses extractSingle which means I can't convert my nested JSON into side-loaded JSON.
The prepopulation logic is very complicated so I'd prefer not to duplicated it client side and retrieve it from the API. Any other ideas on how I could accomplish this using ember-data?
For anyone trying to load embedded relationships with EmberData, check out EmberData's EmbeddedRecordsMixin.
Using Ember-CLI, you will need to create a serializer for the model with embedded relationships and add the mixin in the serializer like this:
// app/serializers/my-example-model.js
import DS from 'ember-data';
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
Then, declare an attrs hash and include each embedded relationship with a hash for each, like this:
attrs: {
conference: { embedded: 'always' },
organizationType: { embedded: 'always' }
}
});
In my example above, conference and organizationType are both embedded in the myExampleModel model for both serializing and deserializing. That means that when I use this serializer over a RESTful API, I will get the embedded objects and I need to put the embedded objects.
The EmbeddedRecordsMixin has several options regarding each relationship, as in you have separate settings for serializing and deserializing. you can specify ids, records, or neither. embedded: always is shorthand for:
{
serialize: 'records',
deserialize: 'records'
}
It's all documented here with better examples here:
http://emberjs.com/api/data/classes/DS.EmbeddedRecordsMixin.html.
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.