Concurrent beforeSave calls allowing duplicates - javascript

In an effort to prevent certain objects from being created, I set a conditional in that type of object's beforeSave cloud function.
However, when two objects are created simultaneously, the conditional does not work accordingly.
Here is my code:
Parse.Cloud.beforeSave("Entry", function(request, response) {
var theContest = request.object.get("contest");
theContest.fetch().then(function(contest){
if (contest.get("isFilled") == true) {
response.error('This contest is full.');
} else {
response.success();
});
});
Basically, I don't want an Entry object to be created if a Contest is full. However, if there is 1 spot in the Contest remaining and two entries are saved simultaneously, they both get added.
I know it is an edge-case, but a legitimate concern.

Parse is using Mongodb which is a NoSQL database designed to be very scalable and therefore provides limited synchronisation features. What you really need here is mutual exclusion which is unfortunately not supported on a Boolean field. However Parse provides atomicity for counters and array fields which you can use to enforce some control.
See http://blog.parse.com/announcements/new-atomic-operations-for-arrays/
and https://parse.com/docs/js/guide#objects-updating-objects

Solved this by using increment and then doing the check in the save callback (instead of fetching the object and checking a Boolean on it).
Looks something like this:
Parse.Cloud.beforeSave("Entry", function(request, response) {
var theContest = request.object.get("contest");
theContest.increment("entries");
theContest.save().then(function(contest) {
if (contest.get("entries") > contest.get("maxEntries")) {
response.error('The contest is full.');
} else {
response.success();
}
});
}

Related

Using permissions without repeating code

I have a function that requires the user to be a owner or an admin. Some commands are only available to owners and not admins but owners can also run admin commands.
I find that I'm just repeating the same code for both parts. Is there a way to consolidate this so I don't violate the rule of repeating myself?
Here's a general scheme of what I have:
request(checkPermissionRequest, function (error, response, body) {
// Determine who is allowed to run the script
switch (body[0].permission) {
// Owner
case 0:
if (isOwner(user)) {
// Script code
}
break;
// Admin
case 1:
if (isOwner(user) || isAdmin(user)) {
// Script code
}
break;
}
});
UPDATE
I can see how my above explanation has caused some confusion so I'll try to attempt to clear it up. There are multiple available scripts and some are for owner and some for admins (which can also be run by owner). These permissions are saved in a database and I get it by making a request. This is why I can't just set things for owners and things for admins and owners because I don't know what permission the script is until I get the result from the database. These permissions are set by the owners and I cannot keep track of them. I modified the code to try to show more clarity.
The way that you've designed your scripts, your permissions levels are represented by integers. Let's use that to our advantage.
Let's define a helper function like this:
function getUserPermissions(user) {
if(isOwner(user) return 0;
if(isAdmin(user) return 1;
return 2; // or some arbitrarily high value- heck, even Number.MAX_SAFE_INTEGER
}
Then, instead of needing a switch and a bunch of if statements, you can simply do something like this:
request(checkPermissionRequest, function (error, response, body) {
// Determine who is allowed to run the script
if(getUserPermissions(user) < parseInt(body[0].permission) {
// do the script
}
});
If following this pattern, you could also consider switching the 0 and 1, if you're able, so that a higher numeric value indicates a higher level of permissions -- if only for the sake of readability, as it would allow a -1 value to indicate someone with no permissions. In that case, you would want to change the if condition to check for getUserPermissions(user) > parseInt(body[0].permission rather than <.
On the other hand, if you stay with the slightly less readable < option, that allows for adding other levels of permission with less access than admin as 3 and so on, without having to increment the existing Owner and Admin levels.
if (isOwner(user) || (isAdmin(user) && script.permission == 1)) {
// Script code
}
You could nest the if, and just exclude 'admin' from the first chunk of data.
if (isOwner(user) || isAdmin(user)) {
//some permissions for both
if(isOwner(user)) {
//more permissions just for owner
}
}

Reading a subset of documents using Couchbase View

I have around 25M documents in my cluster. I need to read 1M documents at a time without any specific criterion. I don't have the access to the keys. So I need to create a view which will emit documents till I reach a counter which goes up to 1M.
I have written a Map function inside which I am trying to create a static variable, but JS doesn't support static variables. I am not sure how to do this operation. The map function which I have written is just to return 1000 documents and it is full of errors. Can someone help me with this functionality?
function (doc, meta) {
value = foo();
if(value < 1000)
{
emit(meta.id, null);
}else{
return;
}
}
function incrementor(){
if(typeof incrementor.counter == 'undefined'){
incrementor.counter = 0;
}
return ++incrementor.counter;
}
Reading a subset of documents with Views can be done using pagination: http://blog.couchbase.com/pagination-couchbase
The Map function is called for each mutation stored in the bucket so a counter approach like this does not make sense. If you want to split your indexes, you need to do that based on the content of the document. But you should really use pagination. This can also be achieved with N1Ql by the way.

Many request to parse at once

I have a list of request to make to Parse.com using the swif API for tasks acumulated once the application was offine. Some tests show that if I dowload it all at once the overall time is slow that is I use multiple requests. However I couldn't figureout how can I request many "random" objectId's from Pase.com (I have a list of course, by random I mean out of order and not a fix number)
At the moment I am using a loop and calling many:
let pred = NSPredicate(format: "newDataID = %#, dataID[i])
query.findObjectsInBackgroundWithBlock { (result:[AnyObject]?, error:NSError?)
I was thinking in auto generate the string for the predicate but it can get very long what I image would make the query very slow.
Any ideas?
Under any circumstances, initiating many requests in a tight loop is ill-advised. Instead, send the dataID array to a cloud function. Also, if its really an array of object ids, then find is the wrong method, use get() instead...
var _ = require('underscore'); // underscore includes many handy functions, including map and toArray
Parse.Cloud.define("getManyObjectsById", function(request, response) {
var dataID = request.params.dataID;
var promises = _.map(dataID, function(anID) {
var query = new Parse.Query("MyCustomClassName");
return query.get(anID);
});
Parse.Promise.when(promises).then(function() {
response.success(_.toArray(arguments));
}, function(error) {
response.error(error);
});
});
Call it...
PFCloud.callFunctionInBackground("getManyObjectsById", withParameters: dataID) {
(objects: [AnyObject]?, error: NSError?) -> Void in
// objects should be an array of objects corresponding to the ids
}

Meteor Leaderboard example: resetting the scores

I've been trying to do Meteor's leaderboard example, and I'm stuck at the second exercise, resetting the scores. So far, the furthest I've got is this:
// On server startup, create some players if the database is empty.
if (Meteor.isServer) {
Meteor.startup(function () {
if (Players.find().count() === 0) {
var names = ["Ada Lovelace",
"Grace Hopper",
"Marie Curie",
"Carl Friedrich Gauss",
"Nikola Tesla",
"Claude Shannon"];
for (var i = 0; i < names.length; i++)
Players.insert({name: names[i]}, {score: Math.floor(Random.fraction()*10)*5});
}
});
Meteor.methods({
whymanwhy: function(){
Players.update({},{score: Math.floor(Random.fraction()*10)*5});
},
}
)};
And then to use the whymanwhy method I have a section like this in if(Meteor.isClient)
Template.leaderboard.events({
'click input#resetscore': function(){Meteor.call("whymanwhy"); }
});
The problem with this is that {} is supposed to select all the documents in MongoDB collection, but instead it creates a new blank scientist with a random score. Why? {} is supposed to select everything. I tried "_id" : { $exists : true }, but it's a kludge, I think. Plus it behaved the same as {}.
Is there a more elegant way to do this? The meteor webpage says:
Make a button that resets everyone's score to a random number. (There
is already code to do this in the server startup code. Can you factor
some of this code out and have it run on both the client and the
server?)
Well, to run this on the client first, instead of using a method to the server and having the results pushed back to the client, I would need to explicitly specify the _ids of each document in the collection, otherwise I will run into the "Error: Not permitted. Untrusted code may only update documents by ID. [403]". But how can I get that? Or should I just make it easy and use collection.allow()? Or is that the only way?
I think you are missing two things:
you need to pass the option, {multi: true}, to update or it will only ever change one record.
if you only want to change some fields of a document you need to use $set. Otherwise update assumes you are providing the complete new document you want and replaces the original.
So I think the correct function is:
Players.update({},{$set: {score: Math.floor(Random.fraction()*10)*5}}, {multi:true});
The documentation on this is pretty thorough.

Subscribe to a count of an existing collection

I need to keep track of a counter of a collection with a huge number of documents that's constantly being updated. (Think a giant list of logs). What I don't want to do is to have the server send me a list of 250k documents. I just want to see a counter rising.
I found a very similar question here, and I've also looked into the .observeChanges() in the docs but once again, it seems that .observe() as well as .observeChanges() actually return the whole set before tracking what's been added, changed or deleted.
In the above example, the "added" function will fire once per every document returned to increment a counter.
This is unacceptable with a large set - I only want to keep track of a change in the count as I understand .count() bypasses the fetching of the entire set of documents. The former example involves counting only documents related to a room, which isn't something I want (or was able to reproduce and get working, for that matter)
I've gotta be missing something simple, I've been stumped for hours.
Would really appreciate any feedback.
You could accomplish this with the meteor-streams smart package by Arunoda. It lets you do pub/sub without needing the database, so one thing you could send over is a reactive number, for instance.
Alternatively, and this is slightly more hacky but useful if you've got a number of things you need to count or something similar, you could have a separate "Statistics" collection (name it whatever) with a document containing that count.
There is an example in the documentation about this use case. I've modified it to your particular question:
// server: publish the current size of a collection
Meteor.publish("nbLogs", function () {
var self = this;
var count = 0;
var initializing = true;
var handle = Messages.find({}).observeChanges({
added: function (id) {
count++;
if (!initializing)
self.changed("counts", roomId, {nbLogs: count});
},
removed: function (id) {
count--;
self.changed("counts", roomId, {nbLogs: count});
}
// don't care about moved or changed
});
// Observe only returns after the initial added callbacks have
// run. Now return an initial value and mark the subscription
// as ready.
initializing = false;
self.added("counts", roomId, {nbLogs: count});
self.ready();
// Stop observing the cursor when client unsubs.
// Stopping a subscription automatically takes
// care of sending the client any removed messages.
self.onStop(function () {
handle.stop();
});
});
// client: declare collection to hold count object
Counts = new Meteor.Collection("counts");
// client: subscribe to the count for the current room
Meteor.subscribe("nbLogs");
// client: use the new collection
Deps.autorun(function() {
console.log("nbLogs: " + Counts.findOne().nbLogs);
});
There might be some higher level ways to do this in the future.

Categories