I have a model that have to be updated (reloaded) on certain user actions. If two of those actions happen in quick succession, I'd like the first update request to be aborted because a) it's result will be outdated, and b) the request takes some time (so queueing those requests is not an option either).
Is there a way to easily achieve this with Ember Data?
I overrode the application adapter's ajax method to add the XMLHttpRequest object to an array.
App.ApplicationAdapter = DS.RESTAdapter.extend({
xhr: [],
ajax: function(url, type, hash) {
var adapter = this;
return new Ember.RSVP.Promise(function(resolve, reject) {
hash = adapter.ajaxOptions(url, type, hash);
hash.success = function(json) {
Ember.run(null, resolve, json);
};
hash.error = function(jqXHR, textStatus, errorThrown) {
Ember.run(null, reject, adapter.ajaxError(jqXHR));
};
adapter.xhr.push(Ember.$.ajax(hash));
}, "DS: RestAdapter#ajax " + type + " to " + url);
},
});
Then, when I want to abort a request (in this case, from a route):
this.store.adapterFor('discussion').xhr.forEach(function(xhr) {xhr.abort();});
Related
I'm using ember-simple-auth and a custom authenticator for an HTTP basic login with CSRF protection. Everything is working fine except sometimes my restore method resolves when it should be failing, like when the session expires.
When authentication succeeds I resolve with the csrf token, but then when the token or session expires and I refresh the page, the resolve method still succeeds because all I'm doing is checking if the token is still there (not if it's valid). I know this is wrong, so I guess my question would be what is the proper way to handle this? Should I also be resolving with the session id? Should I be sending an AJAX request in the restore method with the stored token to see if it is still valid and returns success? I'm interested in hearing about any other improvements I could make as well.
Here is my authenticator code:
import Ember from 'ember';
import ENV from 'criteria-manager/config/environment';
import Base from 'ember-simple-auth/authenticators/base';
export default Base.extend({
restore(data) {
return new Ember.RSVP.Promise((resolve, reject) => {
if (data.token) {
Ember.$.ajaxSetup({
headers: {
'X-XSRF-TOKEN': data.token
}
});
resolve(data);
}
else {
reject();
}
});
},
authenticate(credentials) {
let csrfToken = this.getCookie('XSRF-TOKEN');
return new Ember.RSVP.Promise((resolve, reject) => {
Ember.$.ajax({
beforeSend: function(xhr) {
xhr.setRequestHeader("Authorization", "Basic " + btoa(credentials.username + ":" + credentials.password));
xhr.setRequestHeader("X-XSRF-TOKEN", csrfToken);
},
url: ENV.host + "/api/users/login",
method: 'POST'
}).done(() => {
//A new CSRF token is issued after login, add it to future AJAX requests
Ember.$.ajaxSetup({
headers: {
'X-XSRF-TOKEN': this.getCookie('XSRF-TOKEN')
}
});
Ember.run(() => {
resolve({
token: this.getCookie('XSRF-TOKEN')
});
});
}).fail((xhr) => {
Ember.run(() => {
if(xhr.status === 0) {
reject("Please check your internet connection!");
}
else if (xhr.status === 401) {
reject("Invalid username and/or password.");
}
else {
reject("Error: Http Status Code " + xhr.status);
}
});
});
});
},
invalidate() {
return new Ember.RSVP.Promise((resolve, reject) => {
let csrfToken = this.getCookie('XSRF-TOKEN');
Ember.$.ajax({
beforeSend: function(xhr) {
xhr.setRequestHeader("X-XSRF-TOKEN", csrfToken);
},
url: ENV.host + '/logout',
method: 'POST'
}).done(() => {
Ember.run(() => {
resolve();
});
}).fail(() => {
Ember.run(() => {
reject();
});
});
});
},
getCookie(name) {
let alLCookies = "; " + document.cookie;
let cookieArray = alLCookies.split("; " + name + "=");
if (cookieArray.length === 2) {
return cookieArray.pop().split(";").shift();
}
}
});
Should I also be resolving with the session id? Should I be sending an
AJAX request in the restore method with the stored token to see if it
is still valid and returns success?
It all depends on your project's needs. In my opinion it's good to check if token is still valid. For example, oauth2-password-grant stores expiring date in session and when restoring simply compares it with current time. You may do this too. Or, if your backend has some token validation endpoint, you may send request to be sure if token is valid.
Im stuck trying to.save() a record using Ember-Data 1.3 😵
When I perform a .save() nothing goes wrong but the request payload is empty:
I'm pretty convinced it's an issue with the ember-data request because from the back-end side the only data I got it's "token=blahblahblah". Also I took the request (copy as cURL) and I confirm it's empty:
Here's the .save() code:
var self = this;
this.set('isLoading',true);
return this.store.find('feed', feed_id).then(function(feed) {
//Setting the system_status of the feed to either 4 (archived) or 1 (normal)
feed.set('system_status',param);
//Persist to change to store (and server)
console.log(feed);
feed.save().then(function(){
//success
self.set('isLoading',false);
alert('ok');
},function(){
//Error
self.set('isLoading',false);
alert('error');
}) // => PUT to /feeds/id
});
RESTAdapter:
export default DS.RESTAdapter.extend({
host: 'http://localhost:8000/',
namespace: 'ed',
headers: {
"Content-type": "x-www-form-urlencoded" // workaround for laravel
}});
Model console.log before .save()
Any ideas?
The solution is indeed the Serializer, you can change it to send a POST request instead of a PUT request.
Update the DS.RESTAdapter with this code:
updateRecord: function(store, type, snapshot) {
var data = this.serialize(snapshot, { includeId: true });
var id = snapshot.id;
console.log(snapshot);
var url = ENV.apiUrl + "ed/" + snapshot.typeKey + "/" + snapshot.id;
return new Ember.RSVP.Promise(function(resolve, reject) {
jQuery.ajax({
type: 'POST',
url: url,
dataType: 'json',
data: data
}).then(function(data) {
Ember.run(null, resolve, data);
}, function(jqXHR) {
jqXHR.then = null; // tame jQuery's ill mannered promises
Ember.run(null, reject, jqXHR);
});
});
//Route
url: "https://xxxxxx.com/api/entries",
users: "https://xxxxxx.com/api/users/",
model: function(){
var localData = JSON.parse(localStorage.getItem("user"));
var data = { auth_token: localData.user_token };
return new Ember.RSVP.hash({
logs: Ember.$.ajax({ url: this.get('url'), headers: { "X-Api-Token": data.auth_token } }),
team: Ember.$.ajax({ url: this.get('users'), headers: { "X-Api-Token": data.auth_token } })
});
}
//controller
actions:{
deleteWorklog: function( worklogId ){
var model = this.get('model.logs');
var data = { auth_token: this.get('local_data').user_token };
Ember.$.ajax({
method: "DELETE",
url: this.get('url') + "/" + worklogId,
headers: { 'X-Api-Token': data.auth_token }
}).then(function(data){
//how do i do it?
})
},
loadMore: function(){
var model = this.get('model.logs');
var url = this.get('url');
var today = new Date();
today.setDate(today.getDate() - this.get('from'));
console.log(today);
url += "?from="+ today.toISOString();
Ember.$.ajax({
url: url,
headers: { "X-Api-Token": data.auth_token }
}).then(function(data) {
model.replace(0, model.length, data);
});
var initial = this.get('from') + 10;
this.set('from', initial);
}
}
}
I'm blocked after the request, i need to refresh my model but i've to do cmd + R to see the change, is there a method re-call the model or something like that?
I've added another things maybe help
There's two ways that I can think of. The first is just to call the refresh method on the route. This is by far the simplest. Just call it and Ember will re-call the model hook for that route and any child routes. In your case, I would send an action from your controller to your route, then have your route refresh itself in that action handler.
The second way would be to manually re-get your data and set it on the controller. Perhaps something like this:
// route.js
actions: {
refreshModel: function() {
var route = this;
Ember.$.ajax({ ... }).then(function(data) {
route.get('controller').set('model', data);
});
}
}
However, I wouldn't recommend this method over the first. There are too many variables when dealing with Ember routing and it's easier just to let Ember handle it all.
You either want to use model.reload to refresh the record from the server:
http://emberjs.com/api/data/classes/DS.Model.html#method_reload
Or you may want to delete the record local:
http://emberjs.com/api/data/classes/DS.Model.html#method_deleteRecord
Or you may want to use model.destroyRecord to let Ember do the delete request (remote + local):
http://emberjs.com/api/data/classes/DS.Model.html#method_destroyRecord
I think it would be the best to use model.destroyRecord, but I assume there's a reason you do this manually?
I'm loading a route. Its model hook loads some models. Some are fetch from ember store and some are promises requested through AJAX:
model: function () {
return Em.RSVP.hash({
//the server data might not be loaded if user is offline (application runs using appcache, but it's nice to have)
someServerData: App.DataService.get(),
users: this.store.find('user')
});
}
The App.DataService.get() is defined as:
get: function () {
return new Ember.RSVP.Promise(function(resolve, reject) {
//ajax request here
});
}
Obviously if the request is rejected, the flow is interrupted and I cannot display the page at all.
Is there a way to overcome this?
Ember.RSVP.hashSettled is exactly meant for this purpose.
From tildeio/rsvp.js Github repository:
hashSettled() work exactly like hash(), except that it fulfill with a hash of the constituent promises' result states. Each state object will either indicate fulfillment or rejection, and provide the corresponding value or reason. The states will take one of the following formats:
{ state: 'fulfilled', value: value }
or
{ state: 'rejected', reason: reason }
Here is an example for using it (working JS Bin example):
App.IndexRoute = Ember.Route.extend({
fallbackValues: {
firstProperty: null,
secondProperty: null
},
model: function() {
var fallbackValues = this.get('fallbackValues');
return new Ember.RSVP.Promise(function(resolve, reject) {
Ember.RSVP.hashSettled({
firstProperty: Ember.RSVP.Promise.resolve('Resolved data despite error'),
secondProperty: (function() {
var doomedToBeRejected = $.Deferred();
doomedToBeRejected.reject({
error: 'some error message'
});
return doomedToBeRejected.promise();
})()
}).then(function(result) {
var objectToResolve = {};
Ember.keys(result).forEach(function(key) {
objectToResolve[key] = result[key].state === 'fulfilled' ? result[key].value : fallbackValues[key];
});
resolve(objectToResolve);
}).catch(function(error) {
reject(error);
});
});
}
});
fallbackValues can be useful for managing resolved hash's properties' fallback values without using conditions inside the promise function.
Taking into account that Ember.RSVP.hashSettled is not available in my Ember version. I come up with the following solution:
model: function(params) {
var self = this;
return new Em.RSVP.Promise(function(resolve, reject){
// get data from server
App.DataService.get().then(function(serverData) { //if server responds set it to the promise
resolve({
serverData: serverData,
users: self.store.find('user')
});
}, function(reason){ //if not ignore it, and send the rest of the data
resolve({
users: self.store.find('user')
});
});
});
}
I have an Ember.js app backed by a RESTful API. Session control is done through an authentication token: once a user logs in, he appends his authentication token to each request he makes to the server. I do this by adding the authentication to the data in $.ajaxSetup.
$.ajaxSetup({
data: { auth_token: this.get('authToken') }
});
Now, this works fine for GET requests. However, when saving models to the server through a POST or PUT request, the Ember Data RESTAdapter stringifies the data object. In DS.RESTAdapter.ajax it does
....
if (hash.data && type !== 'GET') {
hash.contentType = 'application/json; charset=utf-8';
hash.data = JSON.stringify(hash.data);
}
...
Because of this, the authentication token is not merged into the data. In this jQuery ticket they say that it's something they are never going to support.
What's the most elegant way of solving this? I'd rather not override the Ember's RESTAdapter.ajax function because the code is changing so quickly so my overridden function might not be compatible with the rest of the codebase at the next release.
In the end, I couldn't find another solution besides overriding RESTAdapter.ajax. I ended up adding three parameters: auth[token], auth[school] and auth[name].
DS.RESTAdapter.reopen({
/* Override to add the authToken, school and name */
ajax: function(url, type, hash) {
var adapter = this;
return new Ember.RSVP.Promise(function(resolve, reject) {
hash = hash || {};
hash.url = url;
hash.type = type;
hash.dataType = 'json';
hash.context = adapter;
if (hash.data && type !== 'GET') {
hash.contentType = 'application/json; charset=utf-8';
/* Add the data to the hash before it's stringified. */
if (HstryEd.Session.get('isLoggedIn')) {
hash.data.auth = {};
hash.data.auth.token = HstryEd.Session.get('authToken');
hash.data.auth.school = HstryEd.Session.get('currentUser').get('school');
hash.data.auth.name = HstryEd.Session.get('currentUser').get('name');
}
hash.data = JSON.stringify(hash.data);
}
if (adapter.headers !== undefined) {
var headers = adapter.headers;
hash.beforeSend = function (xhr) {
forEach.call(Ember.keys(headers), function(key) {
xhr.setRequestHeader(key, headers[key]);
});
};
}
hash.success = function(json) {
Ember.run(null, resolve, json);
};
hash.error = function(jqXHR, textStatus, errorThrown) {
if (jqXHR) {
jqXHR.then = null;
}
Ember.run(null, reject, jqXHR);
};
Ember.$.ajax(hash);
});
}
});