Backbone.js client model save issue on server-side error - javascript

Wondering if any one can help me out here.
I have a single page on the client side running backbone.js
The server is running socket.io.
I have replaced Backbones default sync with the backbone.iobind sync to support socket.io.
I have a backbone model that on save gets an error response from the server (which is planned), and triggers the save's error function, however my model on the client side still updates to the new value. I have included wait:true which from my understanding waits for a success response from the server before updating, however this does not happen for me.
Server side socket.io code
io.sockets.on('connection', function (socket) {
socket.on('test:create', function (data, callback) {
//err - used to fake an error for testing purposes
var err = true;
if (err) {
// error
callback('serv error');
} else {
// success
// The callback accepts two parameters: error and model. If no error has occurred, send null for error.
callback(null, data);
}
});
Client Side Code
//HTML Code
//<div id="callBtns">
//<button id="runTest">Create</button>
//</div>
var CommunicationType = Backbone.Model.extend({
defaults: {
runTest: 'default'
},
urlRoot : '/test',
validate: function(attrs) {
//return 'error';
}
});
var BasicView = Backbone.View.extend({
el: $('#callBtns'),
initialize: function(){
_.bindAll(this);
},
events: {
"click #runTest": "runTestFn"
},
runTestFn: function() {
//causing the issue?
communicationType.set({runTest: 'value from set'});
communicationType.save({},{
success:function(model, serverResponse){
console.log('Success');
console.log(serverResponse);
},
error:function(model, serverResponse){
console.log('Error');
console.log(serverResponse);
console.log(JSON.stringify(communicationType));
},
wait: true
});
}
});
var communicationType = new CommunicationType();
var basicView = new BasicView();
I have noticed that this problem looks to be caused by the fact that I'm using set to update my model directly (communicationType.set({runTest: 'value from set'});)
If I don't set any values before saving, and pass the value in directly to save, like in the code below, it works correctly e.g.
communicationType.save({runTest: 'value from save'},{
success:function(....
However this then means that I can't ever set any new values/update my model without it going via the save function :/
So my question is, how can I get this working correctly (get the client model to not update if there is a server-side error) while still being able to use set?
Any answers are really appreciated! Many Thanks

This can't be done because your model has no way of knowing if it's data is valid until a request is made (model.save in this case).
If you want to prevent Backbone.Model.set from working when you pass invalid values, you must update the model's "validate" function to check for errors.
Given the following validate function:
validate: function(attrs) {
this.errors = [];
if (attrs.runTest === 'wrong_value') {
this.errors.push('error message');
}
return _.any(this.errors) ? this.errors : null;
}
on your CommunicationType model. Running this code in BasicView:
communicationType.set({runTest: 'wrong_value'});
will not change the model.
You can also use the validate function to check for server side errors by returning a errors array and checking for it:
validate: function(attrs) {
this.errors = [];
if (attrs.runTest === 'wrong_value') { // client side check
this.errors.push('error message');
}
if (attrs.errors && attrs.errors.length > 0) { // server side check
for (var key in attrs.errors) {
this.errors.push(attrs.errors[key]);
}
}
return _.any(this.errors) ? this.errors : null;
}
But this will only prevent changing the model when it is saved.
As a hack-ish alternative, you can add server requests inside the validate function that check for errors and prevent the "set" function from changing the model.

You may use set with silent: true so it won't fire change event.

Related

Proper way to findOne document in Template Event?

I am trying to findOne document in my Template.admin.events code. I have a form and onClick I want to verify if the ID of the ObjectID entered is an existing document in my collection and fetch that result to show it on the template.
My event code on the client :
Template.admin.events({
'click #btnAjouterObjet'(event) {
let objetIdInput = $('#object_id').val().toString();
Meteor.subscribe('objetsFindOne', objetIdInput, {
onReady: function () {
let obj = Objets.findOne();
if (obj) {
console.log("found");
console.log(obj);
objetsArr.push(objetIdInput);
}
else {
console.log("not found");
console.log(obj);
}
}
});
}
});
In my Objets api :
Meteor.publish('objetsFindOne', function objetsFindOne(param_id){
return Objets.find({_id : param_id});
})
I have verified and my objetIdInput always change on click when a different Id is entered but the subscribe always returns the first id entered. I also added the onReady because otherwise it returned undefined.
I am new to Meteor and I have also tried to subscribe to all the collection and doing the find on the client but I don't think it is the best idea as my collection has about 22000 documents.
Just to elaborate a little bit on the first answer, as to how to change this pattern:
(1) you should place your Meteor.subscribe() call in your Template.admin.onCreated() function.
(2) the subscription reads from a reactive value, for example, new ReactiveVar().
(3) now, anytime the reactive value changes, the subscription will re-run. So, in your template event, you set the reactive value to the id and let the subscription handle the rest.
Discover Meteor and other resources should be helpful on any details.
You are going about this all wrong. I suggest you take a look at Template-Level Subscriptions
I opted for the use of a method :
Client side:
'click #btnAjouterObjet'(event) {
let objetIdInput = $('#object_id').val().toString();
let result = Meteor.call('findObj', objetIdInput, function (error, result) {
if (error) {
console.log(error.reason);
return;
}
console.log(result);
});
}
On the server side :
Meteor.methods({
findObj: function (param_id) {
console.log(Objets.find({ _id: param_id }).fetch());
return Objets.find({ _id: param_id }).fetch();
},
});

Parse Pointer Permissions don't allow create

I've followed every step of this walkthrough, but when I try to create a new row, I get a 403:
code: 119
message: "This user is not allowed to perform the create
operation on Messages. You can change this setting in the Data Browser."
My code:
Messages = Parse.Object.extend("Messages")
var message = new Messages();
message.set("sender", Parse.User.current());
message.set("receiver", *anotherUser*);
message.set("subject", "foo")
message.set("body", "bar")
message.save()
.then(
function(message){
console.log("success!")
},function(error){
console.log("error: ", error);
});
My CLPs are set as follows:
It looks like someone else posted the same issue in a google group. What are we missing?
I've submitted this as a bug to Parse (Facebook), and they replied:
We have managed to reproduce this issue and it appears to be a valid bug. We are assigning this to the appropriate team.
I will update this answer once the issue has been resolved. If this issue is impacting you, please subscribe to the bug, as this will help prioritize the fix.
UPDATE
Facebook replied:
Turns out that this is actually by design. To create an object, the class should have public create permissions on it
Unfortunately, with this solution, I can create a message "from" any other user (another user set as the sender). This is unacceptable and unusable IMHO.
That has been a bug since the launch of Pointer Permissions, which effectively makes them useless. My impression is they built this with the idea of letting developers secure existing schemas in one go, but of course you need it to work for future creation.
One workaround would involve combining the older Class Level Permissions and per-row ACL's while being careful to not disable your Data Browser. Let's assume you have classes "Puppy" and "Cat" and both have a field called "owner".
In your Data Browser, for each class where it makes sense to have an owner field, you set its Class Level Permissions for Puppy and Cat each to:
Public - Read: Yes or No, depends on your use case, Write: Yes
Add a Pointer Permission for "owner" - Read: Yes, Write: Yes (can skip this for now, see below)
Then in your cloud/main.js, you can use the following as a starting point (which I often call "types" below, sorry).
When Parse fixes the creation issue, you remove the Public Write Class Level permission (above), leave the Pointer Permission one, and get rid of the workaround code below.
--
var validateAndUpdateOwnerWritePerms = function(request){
var object = request.object;
var error = null;
var owner = object.get('owner');
if (!Parse.User.current()) {
error = 'User session required to create or modify object.';
} else if (!owner) {
error = 'Owner expected, but not found.';
} else if (owner && owner.id != Parse.User.current().id && !object.existed()) {
error = 'User session must match the owner field in the new object.';
}
if (request.master) {
error = null;
}
if (error) {
return error;
}
if (object.existed()) {
return null;
}
var acl = new Parse.ACL();
acl.setReadAccess(owner, true);
acl.setWriteAccess(owner, true);
object.setACL(acl);
return null;
}
// Wrapper that makes beforeSave, beforeDelete, etc. respect master-key calls.
// If you use one of those hooks directly, your tests or admin
// console may not work.
var adminWriteHook = function(cloudHook, dataType, callback) {
cloudHook(dataType, function(request, response) {
if (request.master) {
Parse.Cloud.useMasterKey();
} else {
var noUserAllowed = false;
if (cloudHook == Parse.Cloud.beforeSave &&
(dataType == Parse.Installation || dataType == Parse.User)) {
noUserAllowed = true;
}
if (!noUserAllowed && !Parse.User.current()) {
response.error('Neither user session, nor master key was found.');
return null;
}
}
return callback(request, response);
});
};
// Set hooks for permission checks to run on delete and save.
var beforeOwnedTypeWriteHook = function(type) {
var callback = function (request, response) {
var error = validateAndUpdateOwnerWritePerms(request);
if (error) {
response.error(error);
return;
}
response.success();
};
return adminWriteHook(Parse.Cloud.beforeSave, type, callback);
return adminWriteHook(Parse.Cloud.beforeDelete, type, callback);
};
beforeOwnedTypeWriteHook('Puppy');
beforeOwnedTypeWriteHook('Cat');
Unfortunately it seems that Parse Pointer Permissions do not work as you expect it on Create. The quick fix would be to allow Create permission to Public. Then to ensure that the user who is creating a record is the same as the sender. So you need to perform a manual check in the beforeSave trigger for Messages class in cloud code and if that check fails, reject the record being created.

Waiting on collection data prior to rendering a subview

I am building what should be a fairly simple project which is heavily based on Ampersand's starter project (when you first run ampersand). My Add page has a <select> element that should to be populated with data from another collection. I have been comparing this view with the Edit page view because I think they are quite similar but I cannot figure it out.
The form subview has a waitFor attribute but I do not know what type of value it is expecting - I know it should be a string - but what does that string represent?
Below you can see that I am trying to fetch the app.brandCollection and set its value to this.model, is this correct? I will need to modify the output and pass through the data to an ampersand-select-view element with the correct formatting; that is my next problem. If anyone has suggestions for that I would also appreciate it.
var PageView = require('./base');
var templates = require('../templates');
var ProjectForm = require('../forms/addProjectForm');
module.exports = PageView.extend({
pageTitle: 'add project',
template: templates.pages.projectAdd,
initialize: function () {
var self = this;
app.brandCollection.fetch({
success : function(collection, resp) {
console.log('SUCCESS: resp', resp);
self.brands = resp;
},
error: function(collection, resp) {
console.log('ERROR: resp', resp, options);
}
});
},
subviews: {
form: {
container: 'form',
waitFor: 'brands',
prepareView: function (el) {
return new ProjectForm({
el: el,
submitCallback: function (data) {
app.projectCollection.create(data, {
wait: true,
success: function () {
app.navigate('/');
app.projectCollection.fetch();
}
});
}
});
}
}
}
});
This is only the add page view but I think that is all that's needed.
The form subview has a waitFor attribute but I do not know what type of value it is expecting - I know it should be a string - but what does that string represent?
This string represents path in a current object with fixed this context. In your example you've waitFor: 'brands' which is nothing more than PageView.brands here, as PageView is this context. If you'd have model.some.attribute, then it'd mean that this string represents PageView.model.some.attribute. It's just convenient way to traverse through objects.
There's to few informations to answer your latter question. In what form you retrieve your data? What do you want to do with it later on?
It'd be much quicker if you could ping us on https://gitter.im/AmpersandJS/AmpersandJS :)

How do I get a hold of a Strongloop loopback model?

This is maddening, how do I get a hold of a loopback model so I can programmatically work with it ? I have a Persisted model named "Notification". I can interact with it using the REST explorer. I want to be able to work with it within the server, i.e. Notification.find(...). I execute app.models() and can see it listed. I have done this:
var Notification = app.models.Notification;
and get a big fat "undefined". I have done this:
var Notification = loopback.Notification;
app.model(Notification);
var Notification = app.models.Notification;
and another big fat "undefined".
Please explain all I have to do to get a hold of a model I have defined using:
slc loopback:model
Thanks in advance
You can use ModelCtor.app.models.OtherModelName to access other models from you custom methods.
/** common/models/product.js **/
module.exports = function(Product) {
Product.createRandomName = function(cb) {
var Randomizer = Product.app.models.Randomizer;
Randomizer.createName(cb);
}
// this will not work as `Product.app` is not set yet
var Randomizer = Product.app.models.Randomizer;
}
/** common/models/randomizer.js **/
module.exports = function(Randomizer) {
Randomizer.createName = function(cb) {
process.nextTick(function() {
cb(null, 'random name');
});
};
}
/** server/model-config.js **/
{
"Product": {
"dataSource": "db"
},
"Randomizer": {
"dataSource": null
}
}
I know this post was here a long time ago. But since I got the same question recent days, here's what I figured out with the latest loopback api:
Loopback 2.19.0(the latest for 12th, July)
API, Get the Application object to which the Model is attached.: http://apidocs.strongloop.com/loopback/#model-getapp
You can get the application which your model was attached as following:
ModelX.js
module.exports = function(ModelX) {
//Example of disable the parent 'find' REST api, and creat a remote method called 'findA'
var isStatic = true;
ModelX.disableRemoteMethod('find', isStatic);
ModelX.findA = function (filter, cb) {
//Get the Application object which the model attached to, and we do what ever we want
ModelX.getApp(function(err, app){
if(err) throw err;
//App object returned in the callback
app.models.OtherModel.OtherMethod({}, function(){
if(err) throw err;
//Do whatever you what with the OtherModel.OtherMethod
//This give you the ability to access OtherModel within ModelX.
//...
});
});
}
//Expose the remote method with settings.
ModelX.remoteMethod(
'findA',
{
description: ["Remote method instaed of parent method from the PersistedModel",
"Can help you to impliment your own business logic"],
http:{path: '/finda', verb: 'get'},
accepts: {arg:'filter',
type:'object',
description: 'Filter defining fields, where, include, order, offset, and limit',
http:{source:'query'}},
returns: {type:'array', root:true}
}
);
};
Looks like I'm not doing well with the code block format here...
Also you should be careful about the timing when this 'getApp' get called, it matters because if you call this method very early when initializing the model, something like 'undefined' error will occur.

Backbone.js - problem when saving a model before previous save issues POST(create) instead of PUT(update) request

I've developed a nice rich application interface using Backbone.js where users can add objects very quickly, and then start updating properties of those objects by simply tabbing to the relevant fields. The problem I am having is that sometimes the user beats the server to its initial save and we end up saving two objects.
An example of how to recreate this problem is as follows:
User clicks the Add person button, we add this to the DOM but don't save anything yet as we don't have any data yet.
person = new Person();
User enters a value into the Name field, and tabs out of it (name field loses focus). This triggers a save so that we update the model on the server. As the model is new, Backbone.js will automatically issue a POST (create) request to the server.
person.set ({ name: 'John' });
person.save(); // create new model
User then very quickly types into the age field they have tabbed into, enters 20 and tabs to the next field (age therefore loses focus). This again triggers a save so that we update the model on the server.
person.set ({ age: 20 });
person.save(); // update the model
So we would expect in this scenario one POST request to create the model, and one PUT requests to update the model.
However, if the first request is still being processed and we have not had a response before the code in point 3 above has run, then what we actually get is two POST requests and thus two objects created instead of one.
So my question is whether there is some best practice way of dealing with this problem and Backbone.js? Or, should Backbone.js have a queuing system for save actions so that one request is not sent until the previous request on that object has succeeded/failed? Or, alternatively should I build something to handle this gracefully by either sending only one create request instead of multiple update requests, perhaps use throttling of some sort, or check if the Backbone model is in the middle of a request and wait until that request is completed.
Your advice on how to deal with this issue would be appreciated.
And I'm happy to take a stab at implementing some sort of queuing system, although you may need to put up with my code which just won't be as well formed as the existing code base!
I have tested and devised a patch solution, inspired by both #Paul and #Julien who posted in this thread. Here is the code:
(function() {
function proxyAjaxEvent(event, options, dit) {
var eventCallback = options[event];
options[event] = function() {
// check if callback for event exists and if so pass on request
if (eventCallback) { eventCallback(arguments) }
dit.processQueue(); // move onto next save request in the queue
}
}
Backbone.Model.prototype._save = Backbone.Model.prototype.save;
Backbone.Model.prototype.save = function( attrs, options ) {
if (!options) { options = {}; }
if (this.saving) {
this.saveQueue = this.saveQueue || new Array();
this.saveQueue.push({ attrs: _.extend({}, this.attributes, attrs), options: options });
} else {
this.saving = true;
proxyAjaxEvent('success', options, this);
proxyAjaxEvent('error', options, this);
Backbone.Model.prototype._save.call( this, attrs, options );
}
}
Backbone.Model.prototype.processQueue = function() {
if (this.saveQueue && this.saveQueue.length) {
var saveArgs = this.saveQueue.shift();
proxyAjaxEvent('success', saveArgs.options, this);
proxyAjaxEvent('error', saveArgs.options, this);
Backbone.Model.prototype._save.call( this, saveArgs.attrs, saveArgs.options );
} else {
this.saving = false;
}
}
})();
The reason this works is as follows:
When an update or create request method on a model is still being executed, the next request is simply put in a queue to be processed when one of the callbacks for error or success are called.
The attributes at the time of the request are stored in an attribute array and passed to the next save request. This therefore means that when the server responds with an updated model for the first request, the updated attributes from the queued request are not lost.
I have uploaded a Gist which can be forked here.
A light-weight solution would be to monkey-patch Backbone.Model.save, so you'll only try to create the model once; any further saves should be deferred until the model has an id. Something like this should work?
Backbone.Model.prototype._save = Backbone.Model.prototype.save;
Backbone.Model.prototype.save = function( attrs, options ) {
if ( this.isNew() && this.request ) {
var dit = this, args = arguments;
$.when( this.request ).always( function() {
Backbone.Model.prototype._save.apply( dit, args );
} );
}
else {
this.request = Backbone.Model.prototype._save.apply( this, arguments );
}
};
I have some code I call EventedModel:
EventedModel = Backbone.Model.extend({
save: function(attrs, options) {
var complete, self, success, value;
self = this;
options || (options = {});
success = options.success;
options.success = function(resp) {
self.trigger("save:success", self);
if (success) {
return success(self, resp);
}
};
complete = options.complete;
options.complete = function(resp) {
self.trigger("save:complete", self);
if (complete) {
return complete(self, resp);
}
};
this.trigger("save", this);
value = Backbone.Model.prototype.save.call(this, attrs, options);
return value;
}
});
You can use it as a backbone model. But it will trigger save and save:complete. You can boost this a little:
EventedSynchroneModel = Backbone.Model.extend({
save: function(attrs, options) {
var complete, self, success, value;
if(this.saving){
if(this.needsUpdate){
this.needsUpdate = {
attrs: _.extend(this.needsUpdate, attrs),
options: _.extend(this.needsUpdate, options)};
}else {
this.needsUpdate = { attrs: attrs, options: options };
}
return;
}
self = this;
options || (options = {});
success = options.success;
options.success = function(resp) {
self.trigger("save:success", self);
if (success) {
return success(self, resp);
}
};
complete = options.complete;
options.complete = function(resp) {
self.trigger("save:complete", self);
//call previous callback if any
if (complete) {
complete(self, resp);
}
this.saving = false;
if(self.needsUpdate){
self.save(self.needsUpdate.attrs, self.needsUpdate.options);
self.needsUpdate = null;
}
};
this.trigger("save", this);
// we are saving
this.saving = true;
value = Backbone.Model.prototype.save.call(this, attrs, options);
return value;
}
});
(untested code)
Upon the first save call it will save the record normally. If you quickly do a new save it will buffer that call (merging the different attributes and options into a single call). Once the first save succeed, you go forward with the second save.
As an alternative to the above answer, you could achieve the same affect by overloading the backbone.sync method to be synchronous for this model. Doing so would force each call to wait for the previous to finish.
Another option would be to just do the sets when the user is filing things out and do one save at the end. That well also reduce the amount of requests the app makes as well

Categories

Resources