$addToSet in nested array - mongodb

I need to perform a $addToSet on an nested array, here an example:
{
_id: 123,
posts:[
{
_id: 234,
userid: 123 //its same id like the documents id,
likesUser: []
}
]
}
My API requires 2 parameters:
The ID of the document, and the Post ID in witch post i want to add.
The ID of the user that liked that post.
If a user likes an Post his User ID should be stored in likesUser.
Lets say a user likes a post. The document have the id 123, first i want to search the document with that ID. After that i want to search the post with the id 234. Now i want to store the ID of the users that liked the post in likesUser, in this case the user have the ID 5d345
{
_id: 123,
posts:[
{
_id: 234,
userid: 123 //its same id like the documents id,
likesUser: ["5d345"]
}
]
}
How do i archieve it? I have tried it with $elemMatch but it doesnt work it throws me error over error.

I have found out the result:
let result = await Post.findOneAndUpdate(
{
_id: mongoose.Types.ObjectId(userId),
"posts._id": mongoose.Types.ObjectId(postId)
},
{
$addToSet: {
"posts.$.likesUser": USER
}
}
);

Related

Using MongoDB .findOne() function with nested document value

Consider I have this document in my MongoDB collection, Workout:
{
_id: ObjectId("60383b491e2a11272c845749") <--- Workout ID
user: ObjectId("5fc7d6b9bbd9473a24d3ab3e") <--- User ID
exercises: [
{
_id: ObjectId("...") <--- Exercise ID
exerciseName: "Bench Press",
sets: [
{
_id: ObjectId("...") <--- Set ID
},
{
_id: ObjectId("...") <--- Set ID
}
]
}
]
}
The Workout object can include many exercise objects in the exercises array and each exercise object can have many set objects in the sets array. I am trying to implement a delete functionality for a certain set. I need to retrieve the workout that the set I want to delete is stored in. I have access to the user's ID (stored in a context), exercise ID and the set ID that I want to delete as parameters for the .findOne() function. However, I'm not sure whether I can traverse through the different levels of arrays and objects within the workout object. This is what I have tried:
const user = checkAuth(context) // Gets logged in user details (id, username)
const exerciseID, setID // Both of these are passed in already and are set to the appropriate values
const workoutLog = Workout.findOne({
user: user.id,
exercises: { _id: exerciseID }
});
This returns an empty array but I am expecting the whole Workout object that contains the set that I want to delete. I would like to omit the exerciseID from this function's parameters and just use the setID but I'm not sure how to traverse through the array of objects to access it's value. Is this possible or should I be going about this another way? Thanks.
When matching against an array, if you specify the query like this:
{ exercises: { _id: exerciseID } }
MongoDB tries to do an exact match on the document. So in this case, MongoDB would only match documents in the exercises array of the exact form { _id: ObjectId("...") }. Because documents in the exercises have other fields, this will never produce a match, even if the _ids are the same.
What you want to do instead is query a field of the documents in the array. The complete query document would then look like this:
{
user: user.id,
"exercises._id": exerciseID
}
You can perform both find and update in one step. Try this:
db.Workout.updateOne(
{
"user": ObjectId("5fc7d6b9bbd9473a24d3ab3e"),
},
{
$pull: {
"exercises.$[exercise].sets": {
"_id": ObjectId("6039709fe0c7d52970d3fa30") // <--- Set ID
}
}
},
{
arrayFilters: [
{
"exercise._id" : ObjectId("6039709fe0c7d52970d3fa2e") // <--- Exercise ID
}
]
}
);

Mongoose populate with a defualt value

I'm trying to populate a model with related details.
Each product has a user reference.
Currently i'm populating my products with user details:
Product document:
{
_id: 'object_product_id`,
title: 'Product1',
user: 'object_user_id'
}
User document:
{
_id: 'object_user_id`,
username: 'User1'
}
To populate a product i use:
Product.find().populate('user').exec()
The expected is:
{
_id: 'object_product_id`,
title: 'Product1',
user: {
_id: 'object_user_id`,
username: 'User1'
}
}
But if a user document is not present it will be:
{
_id: 'object_product_id`,
title: 'Product1',
user: null
}
Is there a way to populate products with a default value when a user document is not present or to leave the default value of user key that was in product document? Only in context of populate().
I've read the mongoose documentation it is a expected behavior to have a null if a document was not found, also i know that i can do that specific population within the callback of exec()
Something like:
{
user: 'my_custom_default_value'
}

How to query a document using mongoose and send the document to the client with only one relevant element from an array field to the client?

I have the following schema:
var lessonSchema = mongoose.Schema({
_id: mongoose.Schema.Types.ObjectId,
name: String,
students: [{
_id: mongoose.Schema.Types.ObjectId,
attendance: {
type: Boolean,
default: false,
},
}],
});
The students array is an array of students who attended the particular lesson. I want to find a lesson using whether a particular user is present in the students array and then sent only that element of the students array which corresponds to the user making the request, along with all other fields as it is. For example, the query should return:
{
_id: 'objectid',
name: 'lesson-name'
students: [details of just the one student corresponding to req.user._id]
}
I tried using:
Lesson.find({'students._id': String(req.user._id)}, {"students.$": 1})
The query returns the document with just the id and the relevant element from the students array:
{
_id: 'objectid'
students: [details of the one student corresponding to req.user._id]
}
I tried using:
Lesson.find({'students._id': mongoose.Types.ObjectId(req.user._id)})
This returns the document with the details of all the students:
{
_id: 'objectid',
name: 'lesson-name'
students: [array containing details of all the students who attended the lesson]
}
How can I modify the query to return it the way I want?
You can return the name field by adding it to the projection object like this:
Lesson.find({ "students._id": String(req.user._id) }, { "name": 1, "students.$": 1 })
When you add a projection object (2nd parameter to find), the _id field is returned by default, plus whichever fields you set to 1.
Therefore, you were returning just the _id and the desired student but not the name field.
If you want to return all other fields and just limit the array to the matched item then you can make use of $slice in your projection:
Lesson.find({ "students._id": String(req.user._id) }, { "students.$": { $slice: 1 } })

How to check whether each item in an array exists or not

I'm trying to create a watch list where users can watch items. I was trying to create it by adding a watchlist field to my users collection. The watchlist would be an array of IDs corresponding to other items.
Users Collection:
id: ObjectId
name: string
watchlist: array i.e. [9872, 342, 4545, 234, 8745]
The question I have is related to querying this structure. I want to be able to write a query where I pass in a user id and an array of ~20 IDs and check which of those IDs the user watches (i.e. which of them exists in the watchlist field for that user).
I tried this initially:
db.users.find({
_id: 507c35dd8fada716c89d0013,
watchlist: { $in: [342, 999, 8745, etc...] }
});
But this gives me the list of users that contain any of those watchlist items, which is not what I want. What I actually want is a response containing an array like this:
{
id: 342,
exists: true
},
{
id: 999,
exists: false
},
{
id: 8745,
exists: true
}
I'd even be ok just getting an array of items that match:
{
_id: 507c35dd8fada716c89d0013,
watching: [342, 8745]
}
Is this doable, or would I be better off moving the watchlist to a separate collection with users as an array? (My concern with the latter approach is that a user will only watch a few hundred items, but tens of thousands of users could potentially watch the same item.)
You can easily achieve the second output using $setIntersection operator.
db.users.aggregate(
[ {$match:{"_id": 507c35dd8fada716c89d0013}},
{ $project: { "watching": { $setIntersection: [ "$watchlist", [ 342, 999, 8745 ] ] } } }
]
)

MongoDB - Meteor : Update, add and delete an element from an array

I am trying to select a specific element in an nested array in a hash like so.
post:{
_id,
comments:[{
comment_id:post.comments.length + 1,
comment: "hello world",
user_id:Meteor.userId()}]
}
My ultimate goal is to be able to add/edit/delete the nested comment by comment_id but I am having trouble trying to select the element I need in the first place.
If you only have comments in your post object you should only have a comments array like this:
Posts {
_id: String;
comments: [];
}
And to delete a comment by his id :
Posts.update({_id: postId, "comments.comment_id" : "commentIdToDelete"},
{ $pull:{"comments": {"comment_id": "commentIdToDelete"}}})
To update a comment by his id :
Posts.update({_id: postId, "comments.comment_id" : "commentIdToUpdate"},
{ $set:{"comments.$.comment": "A new comment"}})
UPDATE
In order to add a comment in the comments array we have to initialiaze the post document (if it's not already done):
Posts.insert({
comments: []
});
Now we have to retrieve the comments array size of the post to update:
let lengthComments = Posts.findOne({_id: postIdToUpdate}).comments.length;
And finally we can add a comment in the comments array:
Posts.update({_id: postIdToUpdate}, {
$push: {
"comments": {
"comment_id": lengthComments + 1,
"comment": "My comment",
"user_id": Meteor.userId(),
}
}
});