How to implement an upvote/downvote system in MongoDB/Mongoose? - node.js

I am new to MongoDB, and I'm trying to implement an upvote/downvote system so that users can vote on reviews in my application.
How I've set up my system is that the user sends an POST request via AJAX by pressing an upvote or downvote button in the application, which contains a boolean "upvote" which is an upvote if true, and a downvote if false (this code works so I didn't include it). Once the request reaches the server, the server checks if the review the user voted on contains a vote already. If not, the server adds a vote to the review's "votes" array and increments or decrements the voteBalance attribute of that review. If there already exists a vote in that review's "votes" array then it should either:
1) Modify it if the existing vote's upvote attribute is different from the new vote and then modify voteBalance accordingly, or
2) Delete the existing vote if its upvote attribute is the same as the new one and then modify voteBalance accordingly
My code for inserting a new vote works fine, but the issue I'm having is that I can't figure out how to make it work when a vote already exists. In the server-side code below, the else statement near the bottom is what I tried to handle case 1) from above, but it doesn't work. So how can I get both these cases to work?
Here is my Review schema:
var ReviewSchema = new mongoose.Schema({
authorID: {
type: String,
required: true,
},
movieID: {
type: Number,
required: true,
},
date: {
type: Number,
required: true
},
username: {
type: String,
required: true
},
score: {
type: Number,
required: true
},
text: {
type: String
},
voteBalance: {
type: Number,
required: true,
default: 0
},
votes: [{
voterID: {
type: String,
required: true,
},
upvote: {
type: Boolean,
required: true
}
}],
comments: [{
commenterID: {
type: String,
required: true,
},
text: {
type: String,
required: true
},
date: {
type: Number,
required: true
}
}]
},{collection: 'reviews'});
Here is the code I'm using to create and update votes on the server:
Review.findOne({_id: new ObjectID(reviewID), votes: { $elemMatch: { voterID: req.session._id }}}, function(err, review) {
if (err) {
console.log(err);
res.status(500).send();
}
//If vote was not found (or if review was not found), create vote
if (!review) {
if (upvote) {
var update = {$addToSet: {votes: {voterID: req.session._id, upvote}}, $inc : {voteBalance : 1}};
}
else {
var update = {$addToSet: {votes: {voterID: req.session._id, upvote}}, $inc : {voteBalance : -1}};
}
Review.findOneAndUpdate({_id: new ObjectID(reviewID)}, update, function(err, review) {
if (err) {
console.log(err);
return res.status(500).send();
}
res.status(200).send();
});
}
//If vote was found, update
else {
if (upvote) {
var update = {$set: { 'votes.$.upvote': upvote }, $inc : {voteBalance : 1}};
}
else {
var update = {$set: { 'votes.$.upvote': upvote }, $inc : {voteBalance : -1}};
}
Review.findOneAndUpdate({_id: new ObjectID(reviewID), 'votes.$.voterID': req.session._id}, update, function(err) {
if (err) {
console.log(err);
return res.status(500).send();
}
res.status(200).send();
});
}
});
Also, I recognize that this code is probably not as efficient as it could be, and I would appreciate any tips on that front as well.

Instead of doing findOne() and then findOneAndUpdate(), you would be better off using findOneAndUpdate() with the upsert option. That way you don't need that extra if statement in the callback.
I'd also recommend not storing votes as an array in the ReviewSchema. That array can grow without bound because any number of users can vote on a Review, which means a review document might become huge and unwieldy. I'd recommend using a mapping collection instead.

Related

how to add comments to mongoose array with node and reactjs?

So I am running into an issue trying to add comments to some data that is already in my mongoDB database. I want to make it so I can have comments be added and removed and updated for each account saved in my database and I am not sure what I am doing wrong necessarily. I have set up my front end to send in a new comment and all the data that needs to come along with it. it successfully gets to my back end, and at my backend it says it runs through and it gets saved but it doesnt, and when i reload the page the comments array in my data is still 0.
Account.findByIdAndUpdate(
{ _id: comment.accountId },
{
$push: {Account: { comments: comment }},
},
{ new: true, upsert: true }
).exec(function (err, task) {
if (err) {
return res
.status(400)
.send({ message: "Failed to add comment due to invalid params" });
}
console.log("successfully uploaded comment");
return res.status(200).send(task);
});
so here we are loading the specific account and then pushing that new comment to the comments array that is in my mongoose schema. when I take out the "Account: object and just ahve the object with comments:comment, it says there is an internal error on the back end not finding the parameter i guess, which would be comments array.
I dont know why that would be an issue. below is my mongoose schema as well.
const AccountSchema = new Schema({
username: {
type: String,
},
name: {
type: String,
},
date: {
type: Date,
default: Date.now,
},
description: {
type: String,
},
latitude: {
type: Number,
},
longitude: {
type: Number,
},
comments: [
{
id: Number,
text: String,
type: String,
date: Date,
replies: [{ id: Number, text: String, type: String, date: Date }],
},
],
});
Am I not setting up my schema correctly? or am I forgetting to add something somewhere. any help would be great, thank you!
It looks like your update query's $push specification is to blame. In the code, it says:
$push: {Account: { comments: comment }}
But your Account model does not have an "Account" field to push an object into. To insert a new element into the "comments" array, do this instead:
$push: { comments: comment }
Just wanted to post an update, I changed around the options on the findbyidandupdate, i added new and safe and upsert all true, and then low and behold I realized that my main issue was that my comments array in my schema was set to type: string, and not array. lol thanks for the help guys.

Mongoose aggregation with $sum returning 0?

I know several question have been asked in here but no one give me the correct answer. So basically I have Schema like here
const cashFlowSchema = new Schema({
date: { type: Date, default: Date.now },
type: { type: String, required: true },
category: { type: String, required: true },
amount: { type: Number, required: true },
description: String
});
And then I tried to create get sum for amount with aggregate like this
CashFlow.aggregate([{
$group: {
_id: null,
balance: { $sum: "$amount" }
}
}], function(err, result) {
if (err) {
console.log(err);
return;
}
console.log(result);
});
And give the result like this
[ { _id: null, balance: 0 } ]
But if I try in Robo3T (previously Robomongo), it give me correct answer. I use mongodb version 3.4. Thank you before.
Update
I found the answer, so it's my silly mistake. After I enable debug mode in mongoose, it show I'm make wrong connection to the collection. After fix it, I get the correct result. I will delete this question soon, thank's all before.

Need some clarification on mongoose/mongodb populate command

Hello so I am making a basic app with users and posts.
I followed the mongoose documentation on population (http://mongoosejs.com/docs/2.7.x/docs/populate.html) and setup my Schemas so that the users and be connected to posts
var userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
password: { type: String, required: true },
email: String,
created_at: Date,
updated_at: Date,
admin: Boolean,
posts: [{ type: mongoose.Schema.ObjectId, ref: 'Post' }]
});
var postSchema = new mongoose.Schema({
_user : [{ type: mongoose.Schema.ObjectId, ref: 'User' }],
audioFile: { type: String, required: true },
imageFile: { type: String },
title: { type: String, required: true },
artist: { type: String, required: true },
start: { type: String, required: true },
stop: { type: String, required: true },
genre: { type: String, required: true },
tags: [{ type: String }]
});
app.get('/', function (req, res){
Post.find({}, function(err, allPosts){
if(!err){
res.render('main.njk', {
posts : allPosts,
title : 'Title',
isLogged : req.session.isLogged,
user : req.session.user,
messages : req.flash('alert')
});
} else { return done(err); }
});
});
Thats all fine and gravy and I can run a foreach loop on allPosts to pull each one in my HTML, but when I try to think of how I am going to display all the posts with their respective users attached to each post I am unsure of how to connect the two since all the examples in the mongoose doc is just mainly for findOne.
I was thinking something like this
app.get('/', function (req, res){
Post.find({}, function(err, allPosts){
if(!err){
allPosts.populate('_user', ['username']);
allPosts.exec(function (err, users){
if(err) console.log(err);
console.log(users);
});
res.render('main.njk', {
posts : allPosts,
title : 'Spaurk.net',
isLogged : req.session.isLogged,
user : req.session.user,
messages : req.flash('alert')
});
} else { return done(err); }
});
});
but that doesn't work of course.
So I was wondering if anyone with experience with this situation would be able to help me solve this.
Thanks a lot for any input.
EDIT, thanks to Daves help I was able to get the populate to work properly, I just cant pull the fields I want correctly with
Post.find({}).populate('_user').exec(function(err, allPosts){
In my loop {% for post in posts %}
, when I do post._user it shows the whole user schema, but when I do post._user.username it doesn't return anything. I am unsure as to why this is.
The proper way to structure a populate on a query is like this:
Post.find({})
.populate('_user')
.exec((err, allposts){...})
Then you will have an array of your Posts with the _user array populated. If you need to access a property of a user, you will need to do another loop through the _user array or specify with use you want to use _user[0].<property>

Insert into embedded document

I have the following schema:
var UserSchema = new Schema({
username: { type: String, required: true },
password: { type: String, required: true },
userType: { type: String, default: 'user'},
quizzHistory: [{
quizzName: String,
quizzScore: Number
}]
});
my goal is to change document into embedded quizzHistory or insert new one if not exists document in embedded quizzeHistory
I try to set document into embedded quizzHistory :
User.findOneAndUpdate({ _id: req.session.user['_id'], 'quizzHistory.quizzName': testName},
{
'$set': {
'quizzHistory.$.quizzName': testName,
'quizzHistory.$.quizzScore': finalScore
}
}, {upsert: true},
function(err, upd) {
console.log("added");
})
code above works if there is document in quizzHistory with required _id and quizzHistory.quizzName,but don't pushed new one if there isn't any document.
Is there any way in Mongodb to change document into embedded collection or insert new one if not exists ?
the reason is because you are using "find and update" you are not handling the condition when the row hasn't been found and create a new document, being said that you need manage the in a different way like
User.update({ _id: req.session.user['_id'], 'quizzHistory.quizzName': testName},
{
'$push': {
'quizzHistory.quizzName': testName,
'quizzHistory.quizzScore': finalScore
}
}, {upsert: true},
function(err, upd) {
console.log("added");
})
this worked for me
User.update({ _id: req.session.user['_id'],'quizzHistory.quizzName':testName},
{
$set: {
"quizzHistory.$.quizzName":testName,
"quizzHistory.$.quizzScore":finalScore
}
},
function(err, upd) {
if(!upd){
User.update({ _id: req.session.user['_id']},
{ "$addToSet": { quizzHistory: newScoreData }},function(err,data){
});
}
});
If you want to benefit for all possible plugins and methods added to model and don't want to fight with actual Mongo queries you should first retrieve user object, then push new element to quizzHistory array and do save. Something like below (you need to align that code to your needs).
var entry = {
quizzName : 'abc',
quizzScore : 123
};
User.findOne({_id:id, function(err, object) {
if(err) {
return someErrorCallback();
}
var
object.quizzHistory.push(entry);
object.save(function(err) {
if(err) {
return someErrorCallback();
}
someSuccessCallback();
});
});
Updating directly may be efficient, but questions usage of mongoose.
Hope it makes sense.

_id not being generated for subdocs when It's supposed to

Here are my two schemas
var reviews = new Schema({
scenarioId: { type: Schema.Types.ObjectId, required: true},
authorId: { type: Schema.Types.ObjectId , required:true },
reviewNote: String,
subReviews: [subReviewSchema],
active: {type: Boolean, default: true},
display: {type: Boolean, default: true},
date : {type: Date, default: Date.now()}
});
and the subscheA for subreviews
var subReviews = new Schema({
authorId: { type: Schema.Types.ObjectId, required:true },
subReviewNote: String,
date: {type: Date, default: Date.now()},
active: {type: Boolean, default: true},
display: {type: Boolean, default: true}
});
and here is my code that updates the document
exports.addSubReview = function (req, res) {
var id = req.params.id;
var update = req.body;//scenario Id
Review.findById(id, function (err, obj) {
if (err || !obj) { return res.send(404, { error: err.message }); }
obj.subReviews.push(update);
obj.save(function (err) {
if (err) { return res.send(404, { error: err.message }); }
return res.send(200, obj);
});
});
};
For some reason though whenever I send an http post to this code the results only adds what i send in the post request not _id _v or any other things that I would expect mongoose/mongodb to add as boilerplate. Here is an example document in my database
{
"__v": 2,
"_id": "531e3214a30f5f8427830a97",
"authorId": "52fd0e6df8352c184b000004",
"reviewNote": "aaaaaaaaaaaaaaaaa",
"scenarioId": "531a5b80af15cffc051cea67",
"date": "2014-03-10T21:37:05.230Z",
"display": true,
"active": true,
"subReviews": [
{
"subReviewNote": "This is a subReview",
"authorId": "52fd0e6df8352c184b000004"
},
{
"subReviewNote": "This is a subReview",
"authorId": "52fd0e6df8352c184b000004"
}
]
}
Any ideas on why _id is not being added to my subDocs in subReviews?.
My guess is that the problem is in the parent doc where you say:
subReviews: [subReviewSchema]
But you named the child schema variable
subReviews
Not subReviewSchema. But that's a guess, from what you've posted. I'd have to see the code together to get a better picture, unless this is it.
But this would explain it, since subReviews: is just expecting an Object because of this naming issue - and an object is exactly what it gets when you POST,so it just pushes it into the array as it expects.
EDIT
I poked around in the mongoose code on github and I am less confident in my answer above, although I guess it could still be possible. However, I did stumble upon this comment, when declaring schemas:
When nesting schemas, (children in the example above), always declare the child schema first before passing it into is parent.
I have less confidence in my original answer, because it looks like if you named the variable incorrectly, mongoose is going to throw a TypeError

Resources