Javascript
Parse Cloud Code
In my app a User can request to join another Users account. The pseudo code for this is as follows:
send up the username of the account we want to join
search to see if the username exist in the database
if no return
if yes create a new AccountRequest object
add the newly created AccountRequest object to the user we were searching for.
I'm able to do steps 1-4 however I'm having trouble accomplishing #5.
Here is my code that I'm working with.
Parse.Cloud.define("sendAccountAdditionRequest", function(request, response) {
console.log("-sendAccountAdditionRequest");
// Create the query on the User class
var query = new Parse.Query(Parse.User);
// Set our parameters to search on
query.equalTo("username", request.params.adminUsername);
// Perform search
query.find({
// We found a matching user
success: function(results) {
Parse.Cloud.useMasterKey();
var fetchedUser = results[0]
console.log("--found user");
console.log("--creating new AccountRequest");
// Create a new instance of AccountRequest
var AccountRequestClass = Parse.Object.extend("AccountRequest");
var accountRequest = new AccountRequestClass();
// Set the User it is related to. Our User class has a 1..n relationship to AccountRequest
accountRequest.set("user", fetchedUser);
// Set out other values for the AccountRequest
accountRequest.set("phone", request.params.adminUsername);
accountRequest.set("dateSent", Date.now());
// Save the new AccountRequest
accountRequest.save(null,{
// Once the new AccountRequest has been saved we need to add it to our fetched User
success:function(savedRequest) {
console.log("---adding AccountRequest to fetched User");
//
// === This is where stuff breaks
//
var requestRelation = fetchedUser.relation("accountRequest");
// Now we need to add the new AccountRequest to the fetched User. The accountRequest property for a User is array...I'm not sure how I'm suppose to append a new item to that. I think I need to somehow cast results[0] to a User object? Maybe?
requestRelation.add(savedRequest);
// We perform a save on the User now that the accountRequest has been added.
fetchedUser.save(null, {
success:function(response) {
console.log("----AccountRequest complete");
response.success("A request has been sent!");
},
error:function(error) {
// This is printing out: ParseUser { _objCount: 2, className: '_User', id: 'QjhQxWCFWs' }
console.log(error);
response.error(error);
}
});
//
// ================================
//
//response.success("A request has been sent!");
},
// There was an error saving the new AccountRequest
error:function(error) {
response.error(error);
}
});
},
// We were not able to find an account with the supplied username
error: function() {
response.error("An account with that number does not exist. Please tell your administrator to sign up before you are added.");
}
});
});
I believe my problem is fetching the accountRequest relation from the returned search results from the initial query. Another thing to note is that I have not created the accountRequest property of my PFUser, my understanding is that this will automatically be done by Parse when I perform the save function.
Whether the accountRequest will get created for you depends on whether class level permissions are set to allow the client application to add fields. This is probably set to NO for your Parse.User.
But the relation on Parse.User isn't needed anyway, since you've already established it on the AccountRequest class. Given a user, you can get it's accountRequests with:
PFQuery *query = [PFQuery queryWithClassName:#"AccountRequest"];
[query whereKey:#"user" equalTo:aUser];
query.find()...
This is equivalent to getting the relation on User, getting its query and running it.
A couple notes about your code: (a) findOne will save you a line when you know there's just one result, (b) using Parse.Promise would really tidy things up.
Related
I have a function that creates a new Comment object and saves it to the database. Before saving it the comment text, the id of the object it is attached to, as well as a pointer to the user object that posted the comment are set on the object before it is saved. However when run I get the error:
Cannot create pointer to unsaved parse object
Here is the code in question:
this.postComment = function(comment, objectId) {
// Get logged in user
var currentUser = Parse.User.current();
// extend and create new Comment object
var Comment = Parse.Object.extend('Comment');
var comment = new Comment();
// Set fields to be saved
comment.set('object', objectId);
comment.set('commentText', comment);
comment.set('user', currentUser);
// Save new comment to db
comment.save(null, {
success: function(comment) {
console.log("success");
},
error: function(comment, error) {
console.log(error);
}
});
}
This leads me to believe that there is something wrong with the User object that I am pointing to but the User exists in the database and is even retrieved using Parse.User.current()
I've looked at other posts with the same error but I have yet to find a solution to this problem. Please let me know if there is any more information I can provide.
Just change the function parameter name or handle local comment object assignment. Check if it works after.
To use an example that demonstrates the question, assume I have a User model defined by the following schema:
var UserSchema = new Schema({
username: String,
email: String
}
mongoose.model('User', UserSchema);
I know that to update a user using the save method, I could query the user and then save changes like so:
User.findOne({username: usernameTofind}, function(err, user) {
//ignore errors for brevity
user.email = newEmail;
user.save(function(err) { console.log('User email updated') });
});
But if I try to create a new User object with the exact same field values (including the _id) is there any possibility of overwriting the database document? I would assume not, because in theory this would mean that a malicious user could exploit an insecure api and overwrite existing documents (for instance using a 'Create a New Account' request, which wouldn't/couldn't rely on the user already being authenticated) , but more importantly, when I try to do this using a request tool (I'm using Postman, but I'm sure a similar curl command would suffice), I get a duplicate _id error
MongoError: insertDocument :: caused by :: 11000 E11000 duplicate key error index
So I just want to clarify that the only way to update an existing document is to query for the document, modify the returned instance, then call the save method on that instance, OR use the static update(). Both of these could be secured by requiring authentication.
If it helps, my motivation for this question is mentioned above, in that I want to make sure a user is not able to overwrite an existing document if a method such as the following is exposed publicly:
userCtrl.create = function(req, res, next) {
var user = new User(req.body);
user.save(function(err) {
if (err) {
return next(err);
} else {
res.json(user);
}
});
};
Quick Edit: I just realized, if this is the case, then how does the database know the difference between the queried instance and a new User object with the exact same keys and properties?
Does modelObject.save() only update an existing database document when
the modelObject was obtained from the database itself?
Yes, it does. There is a flag that indicates if the document is new or not. If it is new, Mongoose will insert the document. If not, then it will update the document. The flag is Document#isNew.
When you find a document:
User.findOne({username: usernameTofind}, function(err, user) {
//ignore errors for brevity
console.log(user.isNew) // => will return false
});
When you create a new instance:
var user = new User(req.body);
console.log(user.isNew) // => will return true
So I just want to clarify that the only way to update an existing
document is to query for the document, modify the returned instance,
then call the save method on that instance, OR use the static
update(). Both of these could be secured by requiring authentication.
There are other ways you can update documents, using Model#update, Model.findOneAndUpdate and others.
However, you can't update an _id field. MongoDB won't allow it even if Mongoose didn't already issue the proper database command. If you try it you will get something like this error:
The _id field cannot be changed from {_id: ObjectId('550d93cbaf1e9abd03bf0ad2')} to {_id: ObjectId('550d93cbaf1e9abd03bf0ad3')}.
But assuming you are using the last piece of code in your question to create new users, Mongoose will issue an insert command, so there is no way someone could overwrite an existing document. Even if it passes an _id field in the request body, MongoDB will throw a E11000 duplicate key error index error.
Also, you should filter the fields a user can pass as payload before you use them to create the user. For example, you could create a generic function that receives an object and an array of allowed parameters:
module.exports = function(object, allowedParams) {
return Object.keys(object).reduce(function(newObject, param) {
if (allowedParams.indexOf(param) !== -1)
newObject[param] = object[param];
return newObject;
}, {});
}
And then you only require and use the function to filter the request body:
var allow = require('./parameter-filter');
function filter(params) {
return allow(params, ["username", "email"]);
}
var user = new User(filter(req.body));
I have Parse table with entities.
User - default class
Commets - class with pointer to _User entity.
I need to delete user from entity User together with all its comments, located in Comments entity:
Right now I have JS Cloud code:
Parse.Cloud.define("deleteUser", function(request, response) {
var User = Parse.Object.extend("User");
var query = new Parse.Query(User);
var userID = request.params.userID;
query.get(userID,{
success: function(User) {
var message = 'success';
User.destroy({
useMasterKey: true ,
success:function() {
response.success(message);
return;
},
error:function(error) {
response.error('Could not delete object '+ User.id);
return;
}
});
},
error: function(object, error) {
var message = 'User could not found';
response.error(message);
}
});
});
It's removing user only.
How I can combine to remove also records from other entity by user?
Thanks for advance
As eth3lbert pointed out in the comments, you should use an afterDelete hook that gets called after the User object has been deleted. You can kick off any other delete operations you want.
However, there is a little gotcha, the before* and after* methods get killed after 3 seconds of wall clock time which might lead to unwanted results depending on the amount of data that you need to delete.
The best solution for you is to setup a background job (they can run for up to 15 minutes), schedule it to run, lets say once every day, and do any cleanup work in that job.
You could create a simple table for that, that contains the objectIds of deleted users, whenever your afterDelete method gets called, you add the deleted users id into that table, your background job then queries that table on run and deletes the content that was associated with it.
You can delete user easily from user request as below.
Parse.Cloud.define('deleteUser', async (req) => {
const user = req.user;
return user.destroy({ useMasterKey: true});
})
and add additional deleting logic related to User.
I am very new to mongodb and have a basic question that I am having trouble with. How do I get the ID field of a document that has already been created? I need the ID so i can update/add a new field to the document.
//newProfile is an object, one string it holds is called school
if(Schools.find({name: newProfile.school}).fetch().length != 1){
var school = {
name: newProfile.school
}
Meteor.call('newSchool', school);
//Method 1 (doesn't work)
var schoolDoc = Schools.findOne({name: newProfile.school});
Schools.update({_id: schoolDoc._id}, {$set: {enrolledStudents: Meteor.user()}});
//Method 2?
//Schools.update(_id: <what goes here?>, {$push: {enrolledStudents: Meteor.user()}});
}
else {
//Schools.update... <add users to an existing school>
}
I create a new school document if the listed school does not already exist. Schools need to hold an array/list of students (this is where i am having trouble). How do I add students to a NEW field (called enrolledStudents)?
Thanks!
I'm having some trouble understanding exactly what you're trying to do. Here's my analysis and understanding so far with a couple pointers thrown in:
if(Schools.find({name: newProfile.school}).fetch().length != 1){
this would be more efficient
if(Schools.find({name: new Profile.school}).count() != 1) {
Meteor.call('newSchool', school);
Not sure what you're doing here, unless you this will run asynchronously, meaning by the time the rest of this block of code has executed, chances are this Meteor.call() function has not completed on the server side.
//Method 1 (doesn't work)
var schoolDoc = Schools.findOne({name: newProfile.school});
Schools.update({_id: schoolDoc._id}, {$set: {enrolledStudents: Meteor.user()}});
Judging by the if statement at the top of your code, there is more than one school with this name in the database. So I'm unsure if the schoolDoc variable is the record you're after.
I believe you are having trouble because of the asynchronous nature of Meteor.call on the client.
Try doing something like this:
// include on both server and client
Meteor.methods({
newSchool: function (school) {
var newSchoolId,
currentUser = Meteor.user();
if (!currentUser) throw new Meteor.Error(403, 'Access denied');
// add some check here using the Meteor check/match function to ensure 'school'
// contains proper data
try {
school.enrolledStudents = [currentUser._id];
newSchoolId = Schools.insert(school);
return newSchoolId;
} catch (ex) {
// handle appropriately
}
}
});
// on client
var schoolExists = false;
if (Schools.findOne({name: newProfile.school})) {
schoolExists = true;
}
if (schoolExists) {
var school = {
name: newProfile.school
};
Meteor.call('newSchool', school, function (err, result) {
if (err) {
alert('An error occurred...');
} else {
// result is now the _id of the newly inserted record
}
})
} else {
}
Including the method on both the client and the server allows Meteor to do latency compensation and 'simulate' the insert immediately on the client without waiting for the server round-trip. But you could also just keep the method on the server-side.
You should do the enrolledStudents part on the server to prevent malicious users from messing with your data. Also, you probably don't want to actually be storing the entire user object in the enrolledStudents array, just the user _id.
For what you're trying to do, there is no need to get the _id. When you use update, just switch out the {_id: schoolDoc._id} with your query. Looks like using {name: newProfile.school} will work, assuming that the rest of your code does what you want it to do.
While that would work with the normal Mongo driver, I see that Meteor does not allow your update query to be anything but _id: Meteor throws throwIfSelectorIsNotId exception
First, make sure that you're pulling the right document, and you can try something like this:
var school_id = Schools.findOne({name: newProfile.school})._id;
Schools.update({_id: school_id}, { $push: { enrolledStudents: Meteor.user()}});
If that doesn't work, you'll have to do a little debugging to see what in particular about it isn't working.
I am working on a messaging system using node.js + cradle and couchdb.
When a user pulls a list of their messages, I need to pull the online status of the user that sent them the message. The online status is stored in the user document for each registered user, and the message info is stored in a separate document.
Here is the only way I can manage to do what I need, but its hugely inefficient
privatemessages/all key = username of the message recipient
db.view('privatemessages/all', {"key":username}, function (err, res) {
res.forEach(function (rowA) {
db.view('users/all', {"key":rowA.username}, function (err, res) {
res.forEach(function (row) {
result.push({onlinestatus:row.onlinestatus, messagedata: rowA});
});
});
});
response.end(JSON.stringify(result));
});
Can someone tell me the correct way of doing this?
Thank you
Your code could return empty result because you are calling response at the time when user statuses may not yet be fetched from DB. Other problem is that if I received multiple messages from the same user, then call for his status may be duplicit. Below is a function which first fetch messages from DB avoiding duplicity of users and then get their statuses.
function getMessages(username, callback) {
// this would be "buffer" for senders of the messages
var users = {};
// variable for a number of total users I have - it would be used to determine
// the callback call because this function is doing async jobs
var usersCount = 0;
// helpers vars
var i = 0, user, item;
// get all the messages which recipient is "username"
db.view('privatemessages/all', {"key":username}, function (errA, resA) {
// for each of the message
resA.forEach(function (rowA) {
user = users[rowA.username];
// if user doesn't exists - add him to users list with current message
// else - add current message to existing user
if(!user) {
users[rowA.username] = {
// I guess this is the name of the sender
name: rowA.username,
// here will come his current status later
status: "",
// in this case I may only need content, so there is probably
// no need to insert whole message to array
messages: [rowA]
};
usersCount++;
} else {
user.messages.push(rowA);
}
});
// I should have all the senders with their messages
// and now I need to get their statuses
for(item in users) {
// assuming that user documents have keys based on their names
db.get(item, function(err, doc) {
i++;
// assign user status
users[item].status = doc.onlineStatus;
// when I finally fetched status of the last user, it's time to
// execute callback and rerutn my results
if(i === usersCount) {
callback(users);
}
});
}
});
}
...
getMessages(username, function(result) {
response.end(JSON.stringify(result));
});
Although CouchDB is a great document database you should be careful with frequent updates of existing documents because it creates entirely new document version after each update (this is because of it's MVCC model which is used to achieve high availability and data durability). Consequence of this behavior is higher disk space consumption (more data/updates, more disk space needed - example), so you should watch it and run database consumption accordingly.
I think your system could use an in memory hashmap like memcached. Each user status entry would expire after a time limit.
Mapping would be
[user -> lasttimeseen]
If the hashmap contains the user, then the user is online.
On some certain actions, refresh the lasttimeseen.
Then instead of pinging the whole world each time, just query the map itself and return the result.
I'm reminded of this presentation:
Databases Suck for Messaging
And its quote from Tim O'Reilly:
"On monday friendfeed polled flickr nearly 3 million times for 45000 users, only 6K of whom were logged in. Architectural mismatch."
As pointed out in the other answers, updates in CouchDB are expensive and should be avoided if possible, and there's probably no need for this data to be persistent. A cache or messaging system may solve your problem more elegantly and more efficiently.