I am using Ember data with the REST adapter. When you save a record using this.transaction.commit() and the server responds with a 422 validation error, then this case can be captured using the "becameError" event.
However, after changing the data in teh form field and clicking save again (thus doing a second this.transaction.commit(), nothing happens. The transaction is not committed because we are in a Invalid state ...
How can I solve this ?
You can transition the model back to uncommitted state via it's stateManager. If it is an existing record, transition to loaded.updated.committed:
model.get('stateManager').transitionTo('loaded.updated.uncommitted')
and for new records, transition to loaded.created.uncommitted
model.get('stateManager').transitionTo('loaded.created.uncommitted')
Consider this to be a workaround until the ember-data API has a better way.
See What can you do with Ember Data Models when in the error state? and https://gist.github.com/intrica/4773420 for more detail
After transition to ('loaded.created.uncommitted') state in case of a becameInvalid state, you need to use the store defaultTransaction to recommit.
See code below - very dirty check to know whether to use transaction.commit() or defaultTransaction.commit()
save: function () {
//Local commit - author record goes in Flight state
this.transaction.commit();
//After a becameInvalid state, transaction.commit() does not work; use defaultTransaction in that case
//Is this the only way to do this ?
if (this.get('stateManager.currentState.name') == "uncommitted") {
this.get('store').get("defaultTransaction").commit();
}
var author = this.get('model');
author.one('didCreate', this, function () {
this.transitionToRoute('author.edit', author);
});
//If response is error (e.g. REST API not accessible): becameError fires
author.one('becameError', this, function () {
this.get('stateManager').transitionTo('loaded.created.uncommitted');
});
//If response is 422 (validation problem at server side): becameInvalid fires
author.one('becameInvalid', this, function () {
this.set('errors', this.get('content.errors'));
//Does set stateManager.currentState.name to uncommitted, but when committing again, nothing happens.
this.get('stateManager').transitionTo('loaded.created.uncommitted')
});
},
As a horrible workaround, you can try saving a clone of the record instead. This will leave your original record pristine.
If the save succeeds, delete the original record.
Else delete the clone and try again with a new clone.
Related
Will doing partialupdate() cause code in a data class' onUpdate Handler to run?
I have this setup in the data class:
exports.onUpdate = function(db, obj) {
DB.log.info(obj.ShiftID);
db.Shifts.load(obj.ShiftID)
.then((Shift) => {
DB.log.info(Shift);
if (Shift.User == db.User.me) {
Shift.User = null;
Shift.status = 0;
return Shift.update();
}
})
};
(yes, role 2 for node has permissions to query and update the Shifts data class)
But I am getting zero logs when I make a partialupdate(). Do I need to do a real update query...load the object, modify the data, update()?
Also it seems that this code causes the partialupdate() to not run at all, but when I delete the handler, it starts working again.
Yes, that is currently an unimplemented feature since a partial update can't execute an onUpdate handler since there is no object which can be passed to the update handler.
On the other hand, a partial update can't be executed directly since that will result in a security issue (since your onUpdate handler can contain validation code etc.)
So we currently reject any partial update on a class which has an onUpdate handler because there doesn't exist a way how we can actually validate the partial update against your onUpdate code.
We have planned that you can define an extra onPartial handler where you can take some extra steps before the partialUpdate is executed. But that handler will only get the partial update and not the object itself.
I'm pretty sure that partialupdate() will not cause the onUpdate Handler to run.
When I put the log line in and edit the records using website data manager it does log as expected. Not a big deal, I can just rewrite the query to be a full update.
BUT having any code in there does break partialupdate() which is not good.
Here is the code I'm using that works as long as there is nothing in the onUpdateHandler:
requestShift(shiftID) {
db.ready().then((db) => {
db.Applicants.find()
.where({
"shiftID": { "$in": [shiftID] },
})
.singleResult((applicants) => {
return applicants.partialUpdate()
.add("applicants", db.User.me.id)
.add("photos", this.props.UserData.photo)
.execute()
})
Alert.alert(
'Confirmation',
'Shift has been requested.',
)
this.props.navigation.dispatch(goToFindShifts)
})
}
I want to be able to retrieve a certain conversation when its id is entered in the URL. If the conversation does not exist, I want to display an alert message with a record not found.
here is my model hook :
model: function(params){
return this.store.filter('conversation', { status : params.status}, function(rec){
if(params.status == 'all'){
return ((rec.get('status') === 'opened' || rec.get('status') === 'closed'));
}
else{
return (rec.get('status') === params.status); <--- Problem is here
}
});
}
For example, if I want to access a certain conversation directly, I could do :
dev.rails.local:3000/conversations/email.l#email.com#/convid
The problem is when I enter a conversation id which doesn't exist (like asdfasdf), ember makes call to an inexisting backend route.
It makes a call to GET conversation/asdfasdf. I'm about sure that it is only due to the record not existing. I have nested resources in my router so I'm also about sure that it tries to retrieve the conversation with a non existing id.
Basically, I want to verify the existence of the conversation before returning something from my hook. Keep in mind that my model hook is pretty much set and won't change, except for adding a validation on the existence of the conversation with the id in the url. The reason behind this is that the project is almost complete and everything is based on this hook.
Here is my router (some people are going to tell me you can't use nested resources, but I'm doing it and it is gonna stay like that so I have to work with it because I'm working on a project and I have to integrate ember in this section only and I have to use this setup) :
App.Router.map(function(){
// Routing list to raw namespace path
this.resource('conversations', { path : '/' }, function() {
this.resource('conversation', { path : '/:conversation_id'});
});
});
This also happens when I dont specify any id and I use the hashtag in my url like this :
dev.rails.local:3000/conversations/email.l#email.com#/ would make a call to conversation/
I know it is because of my nested resource. How can I do it?
By passing a query to filter (your { status : params.status}) you are asking Ember Data to do a server query. Try removing it.
From the docs at http://emberjs.com/api/data/classes/DS.Store.html#method_filter:
Optionally you can pass a query, which is the equivalent of calling find with that same query, to fetch additional records from the server. The results returned by the server could then appear in the filter if they match the filter function.
So, remove the query:
model: function(params){
return this.store.filter('conversation', function(rec) {
if (params.status == 'all') {
return rec.get('status') === 'opened' || rec.get('status') === 'closed';
} else {
return rec.get('status') === params.status;
}
});
}
Ok so here is what I did. I removed my nested resource because I realised I wasn't using it for any good reason other than redirecting my url. I decided to manually redirect my url using javascript window.location.
This removed the unwanted call (which was caused by the nested resource).
Thanks to torazaburo, you opened my eyes on many things.
Whenever my backend replies with an error, I would like to:
Discard the record as it is in the store. I do not care what state the record is in, I just want it out of the store.
Request it again from the backend. The record should be in a normal state now.
Is it possible to do this? Can I do it in a becameError? How?
This is currently my code:
var entry = this.get('content');
this.transaction = this.get('store').transaction();
this.transaction.add(entry);
...
this.transaction.commit();
entry.on('becameError', this, function () { this.handleFailure(); });
And handleFailure:
handleFailure : function() {
console.error('handleFailure > ');
this.transaction.rollback();
this.goBack();
},
What can I do in handleFailure so that the record is forgotten and requested again?
Or, as alternative, how can I clear any flag in the record so that I can continue to use it normally, without getting problems like:
Uncaught Error: Attempted to handle event `becomeDirty` on <SettingsApp.Scvoicemail:ember1027:08b8fc66-cd90-47a1-9053-4f6fefabdfe3> while in state root.error.
record.unload() is the way to do it, but it wasn't introduced until 1.0 beta, and it's a ton easier without the unnecessary transaction stuff.
I've kinda been struggling with this for some time; let's see if somebody can help me out.
Although it's not explicitly said in the Readme, ember-data provides somewhat validations support. You can see that on some parts of the code and documentation:
https://github.com/emberjs/data/blob/master/packages/ember-data/lib/system/model/states.js#L411
https://github.com/emberjs/data/blob/master/packages/ember-data/lib/system/model/states.js#L529
The REST adapter doesn't add validations support on itself, but I found out that if I add something like this in the ajax calls, I can put the model on a "invalid" state with the errors object that came from the server side:
error: function(xhr){
var data = Ember.$.parseJSON(xhr.responseText);
store.recordWasInvalid(record, data.errors);
}
So I can easily to the following:
var transaction = App.store.transaction();
var record = transaction.createRecord(App.Post);
record.set('someProperty', 'invalid value');
transaction.commit()
// This makes the validation fail
record.set('someProperty', 'a valid value');
transaction.commit();
// This doesn't trigger the commit again.
The thing is: As you see, transactions don't try to recommit. This is explained here and here.
So the thing is: If I can't reuse a commit, how should I handle this? I kinda suspect that has something to do to the fact I'm asyncronously putting the model to the invalid state - by reading the documentation, it seems like is something meant for client-side validations. In this case, how should I use them?
I have a pending pull request that should fix this
https://github.com/emberjs/data/pull/539
I tried Javier's answer, but I get "Invalid Path" when doing any record.set(...) with the record in invalid state. What I found worked was:
// with the record in invalid state
record.send('becameValid');
record.set('someProperty', 'a valid value');
App.store.commit();
Alternatively, it seems that if I call record.get(...) first then subsequent record.set(...) calls work. This is probably a bug. But the above work-around will work in general for being able to re-commit the same record even without changing any properties. (Of course, if the properties are still invalid it will just fail again.)
this may seem to be an overly simple answer, but why not create a new transaction and add the pre-existing record to it? i'm also trying to figure out an error handling approach.
also you should probably consider writing this at the store level rather than the adapter level for the sake of re-use.
For some unknown reason, the record becomes part of the store default transaction. This code works for me:
var transaction = App.store.transaction();
var record = transaction.createRecord(App.Post);
record.set('someProperty', 'invalid value');
transaction.commit()
record.set('someProperty', 'a valid value');
App.store.commit(); // The record is created in backend
The problem is that after the first failure, you must always use the App.store.commit() with the problems it has.
Give a look at this gist. Its the pattern that i use in my projects.
https://gist.github.com/danielgatis/5550982
#josepjaume
Take a look at https://github.com/esbanarango/ember-model-validator.
Example:
import Model, { attr } from '#ember-data/model';
import { modelValidator } from 'ember-model-validator';
#modelValidator
export default class MyModel extends Model {
#attr('string') fullName;
#attr('string') fruit;
#attr('string') favoriteColor;
validations = {
fullName: {
presence: true
},
fruit: {
presence: true
},
favoriteColor: {
color: true
}
};
}
I have a DS.Store which uses the DS.RESTAdapter and a ChatMessage object defined as such:
App.ChatMessage = DS.Model.extend({
contents: DS.attr('string'),
roomId: DS.attr('string')
});
Note that a chat message exists in a room (not shown for simplicity), so in my chat messages controller (which extends Ember.ArrayController) I only want to load messages for the room the user is currently in:
loadMessages: function(){
var room_id = App.getPath("current_room.id");
this.set("content", App.store.find(App.ChatMessage, {room_id: room_id});
}
This sets the content to a DS.AdapterPopulatedModelArray and my view happily displays all the returned chat messages in an {{#each}} block.
Now it comes to adding a new message, I have the following in the same controller:
postMessage: function(contents) {
var room_id = App.getPath("current_room.id");
App.store.createRecord(App.ChatMessage, {
contents: contents,
room_id: room_id
});
App.store.commit();
}
This initiates an ajax request to save the message on the server, all good so far, but it doesn't update the view. This pretty much makes sense as it's a filtered result and if I remove the room_id filter on App.store.find then it updates as expected.
Trying this.pushObject(message) with the message record returned from App.store.createRecord raises an error.
How do I manually add the item to the results? There doesn't seem to be a way as far as I can tell as both DS.AdapterPopulatedModelArray and DS.FilteredModelArray are immutable.
so couple of thoughts:
(reference: https://github.com/emberjs/data/issues/190)
how to listen for new records in the datastore
a normal Model.find()/findQuery() will return you an AdapterPopulatedModelArray, but that array will stand on its own... it wont know that anything new has been loaded into the database
a Model.find() with no params (or store.findAll()) will return you ALL records a FilteredModelArray, and ember-data will "register" it into a list, and any new records loaded into the database will be added to this array.
calling Model.filter(func) will give you back a FilteredModelArray, which is also registered with the store... and any new records in the store will cause ember-data to "updateModelArrays", meaning it will call your filter function with the new record, and if you return true, then it will stick it into your existing array.
SO WHAT I ENDED UP DOING: was immediately after creating the store, I call store.findAll(), which gives me back an array of all models for a type... and I attach that to the store... then anywhere else in the code, I can addArrayObservers to those lists.. something like:
App.MyModel = DS.Model.extend()
App.store = DS.Store.create()
App.store.allMyModels = App.store.findAll(App.MyModel)
//some other place in the app... a list controller perhaps
App.store.allMyModels.addArrayObserver({
arrayWillChange: function(arr, start, removeCount, addCount) {}
arrayDidChange: function(arr, start, removeCount, addCount) {}
})
how to push a model into one of those "immutable" arrays:
First to note: all Ember-Data Model instances (records) have a clientId property... which is a unique integer that identifies the model in the datastore cache whether or not it has a real server-id yet (example: right after doing a Model.createRecord).
so the AdapterPopulatedModelArray itself has a "content" property... which is an array of these clientId's... and when you iterate over the AdapterPopulatedModelArray, the iterator loops over these clientId's and hands you back the full model instances (records) that map to each clientId.
SO WHAT I HAVE DONE
(this doesn't mean it's "right"!) is to watch those findAll arrays, and push new clientId's into the content property of the AdapterPopulatedModelArray... SOMETHING LIKE:
arrayDidChange:function(arr, start, removeCount, addCount){
if (addCount == 0) {return;} //only care about adds right now... not removes...
arr.slice(start, start+addCount).forEach(function(item) {
//push clientId of this item into AdapterPopulatedModelArray content list
self.getPath('list.content').pushObject(item.get('clientId'));
});
}
what I can say is: "its working for me" :) will it break on the next ember-data update? totally possible
For those still struggling with this, you can get yourself a dynamic DS.FilteredArray instead of a static DS.AdapterPopulatedRecordArray by using the store.filter method. It takes 3 parameters: type, query and finally a filter callback.
loadMessages: function() {
var self = this,
room_id = App.getPath('current_room.id');
this.store.filter(App.ChatMessage, {room_id: room_id}, function (msg) {
return msg.get('roomId') === room_id;
})
// set content only after promise has resolved
.then(function (messages) {
self.set('content', messages);
});
}
You could also do this in the model hook without the extra clutter, because the model hook will accept a promise directly:
model: function() {
var self = this,
room_id = App.getPath("current_room.id");
return this.store.filter(App.ChatMessage, {room_id: room_id}, function (msg) {
return msg.get('roomId') === room_id;
});
}
My reading of the source (DS.Store.find) shows that what you'd actually be receiving in this instance is an AdapterPopulatedModelArray. A FilteredModelArray would auto-update as you create records. There are passing tests for this behaviour.
As of ember.data 1.13 store.filter was marked for removal, see the following ember blog post.
The feature was made available as a mixin. The GitHub page contains the following note
We recommend that you refactor away from using this addon. Below is a short guide for the three filter use scenarios and how to best refactor each.
Why? Simply put, it's far more performant (and not a memory leak) for you to manage filtering yourself via a specialized computed property tailored specifically for your needs