MongoDB - Comment Upvoting/Downvoting with Aggregation Pipeline - javascript

I'm trying to implement an upvote/downvote mechanism for comments (similar to the upvoting/downvoting mechanism found on reddit). I have a separate collection called commentReputation and the documents inside can look like this:
{
"_id" : ObjectId("5e5acb6d6034a879655c8819"),
"commentId" : ObjectId("5e5983102328a83d1a4b541f"),
"creationDate" : ISODate("2020-02-29T20:37:01.509Z"),
"upvotes" : [
ObjectId("5e5983102328a83d1a4b53e7"),
ObjectId("5e5983102328a83d1a4b53e4")
],
"downvotes" : [
ObjectId("5e5983102328a83d1a4b53e5")
]
}
In short: every comment will eventually have it's own CommentReputation document (the CommentReputation document should be created as soon as someone upvotes/downvotes a comment)
There are 2 case scenarios:
The collection is empty meaning that I need to create my very first CommentReputation document with a given commentId x. In some other part of the project I was using $setOnInsert with { upsert: true } but it seems (looking at the documentation) that the aggregation pipeline does not support $setOnInsert as for now. Is there another way to deal with this problem?
The document is there and the actuall upvoting should occur.
a) Both upvotes and downvotes arrays do not contain the userId that is trying to upvote thus it gets added to the upvotes array without any further actions
b) The upvotes array contains the userId that is trying to upvote the comment as a result the userId should be REMOVED from the upvotes array. (the user already had this comment upvoted and clicked a second time the upvote button which cancels out the upvote)
c) The downvotes array contains the userId. In this case the userId should be removed from downvotes and added to upvotes
I'm trying to accomplish the above logic with the updateOne method and a aggreagtion pipeline however I'm not sure if this is even possible.
What I currently have is returning a "Unrecognized pipeline stage name: '$cond'"
const updateUpvotes = {
$cond: {
if: { $elemMatch: { upvotes: ObjectID(userId) } },
then: { $pull: { upvotes: ObjectID(userId) } },
else: { $addToSet: { upvotes: ObjectID(userId) } }
}
};
db.collection(collectionName).updateOne({
commentId: ObjectID('5e5983102328a83d1a4b541f')
}, [updateUpvotes])
Am I overthinking the whole feature? I guess the 1. problem can be solved by simply creating a CommentReputation document (with empty upvotes and downvotes at the same time the Comment document is being created.
Is there a better way of doing this? I would love to have it working inside a single query request. Maybe someone of You guys implemented a similar feature and can give me some hints on this one.

you can do it with the following pipeline update but it requires that the upvotes and downvotes arrays exist. even if it's just empty.
var comment_id = ObjectId("5e5983102328a83d1a4b541f");
var user_id = ObjectId("5e5983102328a83d1a4b53e5");
db.commentReputation.update(
{
commentId: comment_id
},
[
{
$set: {
upvotes: {
$cond: [
{ $in: [user_id, '$upvotes'] },
{ $setDifference: ['$upvotes', [user_id]] },
{ $setUnion: ['$upvotes', [user_id]] }
]
}
}
},
{
$set: {
downvotes: {
$cond: [
{ $in: [user_id, '$downvotes'] },
{ $setDifference: ['$downvotes', [user_id]] },
'$downvotes'
]
}
}
}
]
);

Related

Insert to array if value is not present in more arrays

I am building a forum web app where user can like/dislike post of other users.
I have the following model, representing a Post:
[
{
_id:"0002253,
title:"My first post",
likes:[],
dislikes:[]
},
{
_id:"0002254,
title:"My second post",
likes:[],
dislikes:[]
},
{
_id:"0002255,
title:"My third post",
likes:[],
dislikes:[]
},
]
When a user sends a like or dislike, the latter's ID will be inserted into the corresponding array (like or dislike).
Each user can, for each post, insert only a like or a dislike (not both).
The query I used for the insert is the following:
let vote_result = await Poll.updateOne(
{
_id: poll_id,
$and: [
{ "likes": { $nin: [MY_ID] } },
{ "dislikes": { $nin: [MY_ID] }},
],
},
{
$addToSet: {
"likes": MY_ID,
},
},
So, theoretically, query should insert MY_ID in likes or dislikes array if and only if MY_ID is not present in either array.
The problem is that query still writes MY_ID in likes array (even if MY_ID is present in dislikes) and vice versa.
Let's suppose now I want to leave a 'Like' to a post:
Phase 1:
likes:[]
dislikes:[]
(I am able to like or dislike, it works.)
Phase 2:
likes: [MY_ID]
dislikes:[]
(I am not able send like again, it works, but I am still able to dislike - it should not allow me to vote again.)

I can't get the $set operator to work on a specific field

I am writing a database backend for our game. I have been wrestling for the past 3 days with this issue. No matter what I try, I cannot get the $set operator to work on one field. Just one.
I have a GlobalLeaderboard collection, with each document containing this structure:
{
"_id" : { "$oid" : "5e7d445f5010bb548850d2ee" },
"PlayerName" : "Regen_erate",
"Rating" : 38.24,
"TotalMapsPlayed" : 372,
"UserId" : "P526993347"
}
The Node.js code that I am running to edit the database is as follows:
rating = await getRating(Plays.find({"UserId": newPlayData.UserId}).sort({"Rating": -1}));
console.log(rating);
Global.findOneAndUpdate({"UserId": newPlayData.UserId},
{
$inc: {"TotalMapsPlayed": 1},
$set: {"PlayerName": newPlayData.PlayerName},
$set: {"Rating": rating.toFixed(2)},
$set: {"UserId": newPlayData.UserId}
},
{
upsert: true,
bypassDocumentValidation: true,
ignoreUndefined: true
}).catch(err => {
console.log("ERR: " + err);
res.status(500).send("Whoops! Something went horribly wrong! Here's some info: " + err);
});
Even if I stick a random number (double) into the $set operation it still won't update. This seems to be happening for no reason at all...
I am able to run $set on all other fields except the Rating field. Out of curiosity, I tried to use $inc on the field in question, and, surprisingly, I was able to get it to work. What is going on?
The second argument to findOneAndUpdate is an object. An object can only have 1 instance of a specific field name, when you specify the same field multiple times, the last one is the value that remains. So your update document:
{
$inc: {"TotalMapsPlayed": 1},
$set: {"PlayerName": newPlayData.PlayerName},
$set: {"Rating": rating.toFixed(2)},
$set: {"UserId": newPlayData.UserId}
}
Is equivalent to
{
$inc: {"TotalMapsPlayed": 1},
$set: {"UserId": newPlayData.UserId}
}
To set multiple fields in the same update, list all of the fields inside a single $set like:
{
$inc: {"TotalMapsPlayed": 1},
$set: {"PlayerName": newPlayData.PlayerName,
"Rating": rating.toFixed(2),
"UserId": newPlayData.UserId}
}

MongoDB Aggregate not working as expected

I am trying to find all votes associated with a comment after a graphlookup but it isn't working.
I'm trying to get the comments/votes recursively of a thread.
I have 3 schemas:
comments
- _id
- points
- content
- userId
- parentId
- parentThreadId
threads
- _id
- upvotes
- downvotes
- title
- content
- userName
votes
- _id
- vote
- commentId
- userId
The problem is that a comment has two keys. One being 'parentId' and the other one 'parentThreadId'. parentId is only set if the comment is a child of another comment, and parentThreadId is set whenever it is a top-level comment under a thread.
Using this code, I only receive the votes associated with top-level comment (thus with a parentThreadId instead of the votes of all comments).
const threadId = req.params.id;
const ObjectId = mongoose.Types.ObjectId;
Thread.aggregate([
{
$match: {_id : ObjectId(threadId)}
},
{
$lookup:
{
from: 'comments',
as: 'comments',
pipeline: [
{
$match: {parentThreadId : ObjectId(threadId)}
},
{
$graphLookup: {
from: "comments",
startWith: "$_id",
connectFromField: "_id",
connectToField: "parentId",
as: "children",
depthField: "level",
}
},
{
$lookup :
{
from: 'votes',
localField: '_id',
foreignField: 'commentId',
as: 'votes'
}
},
]
}
}
]).
Does anybody have a clue as to how to achieve this?
I get into similiar problem not a long ago, you have here recursive relationship between documents in one collection, and sadly, only way to get all tree of comments is recursievly make requests for each child level of comments.
Correct way to handle such stuff is to save all tree in topmost document, so you have thread, you need to save there in one property whole tree of comments, this way moving responsibility to manage it to js (where it is way easier to walk over such tree object, update it etc.)

get all users who is not inside group

I'm trying to get all users from a m:n association who is not a member of a specific group.
After a couple hours of trying and searching for something related this is what I came up with. But it doesn't solve my problem 100%
router.get('/:group_id/notmembers', function(req, res) {
models.User.findAll({
include: [
{ model: models.Group,
through: {
model: models.User_Group,
where: {
GroupId: {[Op.notIn] : [req.params.group_id] },
},
},
required: true,
}
],
}).then(function(groups) {
res.send(groups);
});
});
The code I've written so far returns all users who are part of another group instead of the specific group_id, this would work in a scenario where a user could only be member of one group. In my scenario users can be a member of multiple groups, therefore this doesn't work for me.
any help appreciated!
edit:
No one knows?
After blood sweat and tears I finally figured it out.
by setting the required variable to false I can include all db entries for the specific group_id, even if the users don't have it. From there I just select all entries where there is not a Group.id leaving only the users who aren't part of that specific group left.
router.get('/:group_id/notmembers', function(req, res) {
models.User.findAll({
include: [
{ model: models.Group,
through: {
model: models.User_Group,
where: {
GroupId: req.params.group_id,
},
},
required: false,
}
],
where: {
'$Groups.id$' : null,
}
}).then(function(groups) {
res.send(groups);
});
});

How bind search values in mongodb with mongoose

I have the following code in my /search/:query route:
var param = {
query: req.query['query']
}
MyModel.find({
"$or": [
{ 'name': req.param.query },
{ 'age': req.param.query },
{ 'event': req.param.query },
]
}, function (err, results) {
if (err) {
console.log(err)
}
else {
res.render('index', {
data: results
});
}
}
);
And is good, i can search for pretty much every data that i want, but only individually. What if i want search name + age, can i? Example: 'Leo 22'.
There is any way that mongoose help me with this?
UPDATE:
My problem is:
I have tables lists it titles, this title is the concatenation of 'eventName' and 'eventDate'.
Real examples of this fields:
'Special Event - 20/12/2015'
'Classic Event - 12/03/2015'
'Hot Summer Event - 05/07/2005'
Every week will be create 4 events. In some point, a user will search for an old event, and i believe that the user will search in this format:'EVENT NAME - EVENT DATE'..
So i need a way to bind this values in my controllers.
I'm no familiar with mongoose but in order to do that, you must have a way to bind your query param to the attribute you want to search. Otherwise, they wouldn't know Leo is name and 22 is age.
Ur path would be like search?name=:name&age=:age&event=:event and in your code, you will have to process like if the param is not null, add and condition to it.
It seems you are using only one parameter (req.param.query) to filter all attributes. That's not mongoose related: you could create distinct parameters for each attribute and pass them along the query string.
For instance:
"$or": [
{ 'name': req.param.name },
{ 'age': req.param.age },
{ 'event': req.param.event },
]
And your HTTP request will be like this:
http://youraddress/expressRoute?name=Leo&age=22

Categories

Resources