Can I POST multiple models using Ember Data? - ember.js

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

Related

How does Ember Data write over instances of stored model data

Ok, so I'm new to Ember so bear with me.
I have an Ember application that is communicating with an API that does not adhere to JSONAPI standards, thus I have begun writing my own serializers in order to use Ember Data. However I am finding that when I make multiple requests to the same resource, the data is having trouble writing to the store. Consecutive requests to the same resource always responds with the following error:
TypeError: Cannot convert object to primitive value
Which from my limited understanding, implies that the data I am sending to the store is being treated like a string.
In my Application route I have written a findAll to my model 'listing-item' like so:
model: function() {
return this.store.findAll('listing-item');
},
In a nested 'user' route, when I do any type of request for the listing-item data that returns an array response (query, findAll) for the listing-item data, I get:
TypeError: Cannot convert object to primitive value
at EmptyObject.SETTER_FUNCTION [as title] (ember.debug.js:20672)
at assign (<anonymous>)
at InternalModel.setupData (internal-model.js:244)
at Class._load (store.js:1728)
at Class._pushInternalModel (store.js:2055)
at Class._push (store.js:1995)
at finders.js:141
at Backburner.run (ember.debug.js:720)
at Class._adapterRun (store.js:2253)
at finders.js:139
(Title is a field in my listing item model).
As I mentioned earlier, my API does not adhere to JSONAPI standards, so I've written a listing-item serializer like so:
import DS from 'ember-data';
export default DS.RESTSerializer.extend({
normalizeArrayResponse(store, primaryModelClass, payload) {
payload.data = [];
payload.listing_item._data.forEach(this.formatListingItemArray, payload.data);
delete payload.listing_item;
return payload;
},
formatListingItemArray(listingItem) {
this.push({
type: "listing-item",
id: listingItem.id,
attributes: {
title: listingItem.title,
description: listingItem.description,
buy_now_price: listingItem.buy_now_price,
created_at: listingItem.created_at,
category_id: listingItem.category_id,
subcategory_id: listingItem.subcategory_id,
identity_id: listingItem.identity_id,
listing_number: listingItem.listing_number,
brand_new: listingItem.brand_new,
sold: listingItem.sold,
},
});
},
});
So I suppose my question is, what is Ember Data doing with my data object for this error to occur, and what might I be doing wrong in formatting my data for Ember data to consume.
UPDATES:
It appears as though only the top 3 fields are causing this error to occur. If I comment out the attributes 'title', 'description' and 'buy_now_price' in my serializer, I don't get this error. Also, it appears this only occurs when I navigate to the route, If I am in the /user route when the application loads, both requests work as expected.
Ok, so I've been crawling through ember-data code and found that in the internal-model.js file there is a setup function that looks at the current attributes in the store and compares them to the data being passed from the serializer. It then does an assign() to copy the serializers new data over to the stores object. However for some reason it seems that my stores object has a set of 'getter' and 'setter' function that come back from the store for the problematic fields (title, description and buy_now_price). What I need to know now is why are these functions coming along for the ride and what have I done to cause this?
Picture of getters/setters on ember-data object
Thanks in advance, let me know if there's any more information I need to provide in order to give better context.
So I found the solution to my problem for anyone that experiences a similar issue.
The symptom was as stated above, it seemed as though ember-data was returning getter and setter functions back from the store and having problems placing my new values in their stead. However the clue was that it was only the fields that I was rendering in my template.
The problem seems to be that I was using an Ember Concurrency task to perform the loading of my data, then passing the task straight in to an internal component from the model template. Something like this (not tested code):
The WRONG pattern (one I was using to experience the issue).
//route file - application.js
export default Ember.Route.extend({
model: function() {
return {
listings: this.get('getListings').perform(),
};
},
getListings: task(function * () {
return yield this.store.findAll('listing-item');
}),
})
//application template - application.hbs
{{listing-index listings=model.listings}}
//listing index component template.hbs
{{#if listings.value}}
{{#each listings.value.content as |listing|}}
{{listing._data.title}}
{{listing._data.description}}
{{listing._data.buy_now_price}}
{{/each}}
{{/if}}
The CORRECT pattern seems to be as thus:
//route file - application.js
export default Ember.Route.extend({
model: function() {
return {
listings: this.get('getListings').perform(),
};
},
getListings: task(function * () {
return yield this.store.findAll('listing-item');
}),
})
//application template - application.hbs
{{#if listings.value}}
{{listing-index listings=model.listings.value}}
{{/if}}
//listing index component template.hbs
{{#each listings as |listing|}}
{{listing.title}}
{{listing.description}}
{{listing.buy_now_price}}
{{/each}}
So the problem seems to lie in this pattern of passing the task rather than the content of the task in to my component. When I looped over the listings in the model template then passed the listings straight in to the component, it seemed to have solved my issues. I gather this is something to do with the use of these _data properties. A further explanation would be appreciated but i'll mark this as resolved for now.

EmberJS embedded items in payload JSONAPI

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.

getting data from non-standard endpoints using ember-data

I have an endpoint /activities which behaves in a typical fashion for a RESTful API using JSON. I can of course do things like the following:
model: function() {
return this.store.find('activity');
}
to return a list of activities. Or I can:
model: function(params) {
return this.store.find('activity', params.id);
}
get a specific Activity record. This works fine. But the API also allows certain filtering to be done using URL parameters. So for instance,
GET /activities/type/exercise
Unlike a full list it would only bring back activities which are associated with "exercise". What is the best way for one to pull back data using ember-data? At a bare minimum I want the data to come back, be associated to the correct model, and be iterable via a `DS.RecordArray or similar object.
Ideally I would also like to avoid making network requests every time I hit this route although possibly this is something that I can't get "out of the box". Still interested if there are any best practices.
Ember can already query for data, by default using query params i.e.
store.find(activities, {type: 'exercise'}); // /activities?type=exercise
you could override findQuery on your activities adapter so that if conforms to your api:
// /adapters/activity.js
import DS form 'ember-data';
export default DS.RESTAdapter.extend({
findQuery: function(store, type, query) {
for(var key in query) break; // get first key in the example 'type'
var path = ['', key, query[key]].join('/');
return this.ajax(this.buildURL(type.typeKey) + path, 'GET');
},
});
I haven't tested this but it should work.

Proper way to set multiple models on route; depending on user authentication?

I'm currently working on an Ember app and it is coming along fine but since I am new to MVC applications in general there are a lot of concepts that don't come naturally to me.
I am currently trying to return two models for my index route. I referred to another SO question (EmberJS: How to load multiple models on the same route?) for the correct method and it has worked great.
My problem is now that I need to only set one of the two models only if the user is authenticated. I am using ember-simple-auth, and currently this is what I've got:
// app/routes/index.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
if (this.get('session.isAuthenticated')) {
var _this = this;
this.get('store').find('user', this.get('session.uid')).then(function(user) {
_this.set('model.entries', user.get('entries'));
});
}
return Ember.RSVP.hash({
newEntry: this.get('store').createRecord('entry', {
body: 'Write here ...'
})
});
}
});
For some reason, this does not work. After my route is loaded, the model only has the 'newEntry' property and not an 'entries' property, although the promise does get fulfilled (I put console.logs inside to prove it).
What could be happening? And is this the best way to accomplish this?
There is a set of data that you always want to load, for every user. Do that in the model hook, that is actually the data for the route.
There is another piece of info that you want to add only if a condition is met (authentication). Do that in the afterModel hook.
...is provided the route's resolved model...
http://emberjs.com/api/classes/Ember.Route.html#method_afterModel
So, now you can append or remove data from the model. Or take any relevant action depending on the data that you received.

Preload nested data with ember-data

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.