MongoDB Searching if element exist in array - node.js

So I have something like Survey Schema (I am using mongoose).
In this Schema, for each voting option, I have votes[] array that contains ObjectIds of Users.
Now I want to check if User can vote again, or if he already voted?
The simple solution is iterating thru votes with .indexOf() and checking if id exists. Now this is a blocking way for Node JS, since this operation is sync.
Is there a way to do this with Mongo aggregate or query? So for each voting option I would get additional field like:
didIVoted: true
My Schema looks like this:
const SurveySchema = new Schema({
title: {
type: String
},
options: [{
value: String,
votes: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }]
}]
}, { timestamps: true })

You can use $addFields and $map to overwrite existing options field. To check if userId exists in votes array you can use $indexOfArray
SurveySchema.aggregate([
{
$addFields: {
options: {
$map: {
input: "$options",
in: {
value: "$$this.value",
votes: "$$this.votes",
didIVote: { $ne: [ { $indexOfArray: [ "$$this.votes", userId ] }, -1 ] }
}
}
}
}
}
])

Related

MongoDB: add object to subarray if not exists

I searched many questions here and other articles on the web, but they all seem to describe somehow different cases from what I have at hand.
I have User schema:
{
username: { type: String },
lessons: [
{
lesson: { type: String },
result: { type: String }
}
]
}
I want to add new element into lessons or skip, if there is already one with same values, therefore I use addToSet:
const dbUser = await User.findOne({ username })
dbUser.lessons.addToSet({ lesson, result: JSON.stringify(result) })
await dbUser.save()
However it makes what seems to be duplicates:
// first run
[
{
_id: 60c80418f2bcfe5fb8f501c1,
lesson: '60c79d81cf1f57221c05fdac',
result: '{"correct":2,"total":2}'
}
]
// second run
[
{
_id: 60c80418f2bcfe5fb8f501c1,
lesson: '60c79d81cf1f57221c05fdac',
result: '{"correct":2,"total":2}'
},
{
_id: 60c80470f2bcfe5fb8f501c2,
lesson: '60c79d81cf1f57221c05fdac',
result: '{"correct":2,"total":2}'
}
]
At this point I see that it adds _id and thus treats them as different entries (while they are identical).
What is my mistake and what should I do in order to fix it? I can change lessons structure or change query - whatever is easier to implement.
You can create sub-documents avoid _id. Just add _id: false to your subdocument declaration.
const userSchema = new Schema({
username: { type: String },
lessons: [
{
_id: false,
lesson: { type: String },
result: { type: String }
}
]
});
This will prevent the creation of an _id field in your subdoc, and you can add a new element to the lesson or skip it with the addToSet operator as you did.

How to return field based on other fields with mongoose

I have mongoose schema that looks something like this:
{
_id: someId,
name: 'mike',
keys: {
apiKey: 'fsddsfdsfdsffds',
secretKey: 'sddfsfdsfdsfdsds'
}
}
I don't want to send back to the front the keys of course, but I want some indication, like:
{
_id: someId,
name: 'mike',
hasKeys: true
}
There is built in way to create 'field' on the way based on other fields, or do I need every time fetch the whole document, check if keys is not empty and set object property based on that?
For Mongo version 4.2+ What you're looking for is called pipelined updates, it let's you use a (restricted) aggregate pipeline as your update allowing the usage of existing field values.
Here is a toy example with your data:
db.collection.updateOne(
{ _id: someId },
[
{
"$set": {
"hasKeys": {
$cond: [
{
$ifNull: [
"$keys",
false
]
},
true,
false
]
}
}
},
])
Mongo Playground
For older Mongo versions you have to do it in code.
If you don't want to update the actual document but just populate this field when you fetch it you can use the same aggregation to fetch the document
you can use $project in mongoose aggregation like this.
$project: { hasKeys: { $cond: [{ $eq: ['$keys', null] }, false, true]}}

MongoDB - update data in array of objects within object

I have a document in mongoDB structured like that
_id: ObjectId("generatedByMongo"),
name: {
required: true,
type: String,
trim: true
},
last: {
required: true,
type: String,
trim: true
},
grades: [{
grade: {
_id: ObjectId(""),
grade: Number,
date: date
}
}]
And to server I send array of objects containing 3 fields
[
{studentId}, {gradeId}, {newGrade}
]
What I'm trying to accomplish is I want to find in within that user collection grade with given gradeId and update it's value to newGrade. As far as I tried to do that I have done this
router.patch('/students/updateGrade',async(req,res) => {
const studentId = req.body.updateGradeArray[0].studentId;
const gradeId = req.body.updateGradeArray[0].gradeId;
const newGrade = req.body.updateGradeArray[0].newGrade;
try {
const student = await Student.find({_id: studentId})
.select({'grades': {$elemMatch: {_id: gradeId}}});
} catch(e) {
console.log(e);
}
}
);
If you intend to update just grade.grade(the number value), try this:
Student.updateOne(
// Find a document with _id matching the studentId
{ "_id": studentId },
// Update the student grade
{ $set: { "grades.$[selectedGrade].grade": newGrade } },
{ arrayFilters: [{ "selectedGrade._id": gradeId }] },
)
Why this should work:
Since you are trying to update a student document, you should be using one of MongoDB update methods not find. In the query above, I'm using the updateOne method. Inside the updateOne, I am using a combination of $set and $[identifier] update operators to update the student grade.
I hope this helps✌🏾

get length of array field in mongoose _ Nodejs

suppose we have a User model that contains an array of other User objects.
let UserSchema = mongoose.Schema({
followers: [{
type: mongoose.Schema.ObjectId,
ref: 'User',
}]
})
I need a count of this objectIds.
the first solution is to get length of followers.
req.user.followers.length
but I think it's not relevant to get all the followers that contains many of objectIds. and in my query i dont need all of this objectIds.
I tried to use virtuals but in virtuals I have many unnecessary pieces of stuff.I'm looking for the best and uncosted way for this type of situations.
because of misunderstanding your question, so I update my answer: you can use $size of mongo aggregate.
db.users.aggregate(
[
{
$project: {
id: 1,
total_followers: { $size: "$followers" }
}
}
]
)
In case of you want to find any document with specific number of length (eg: 0), you can do this :
db.users.find({ followers: { $size: 0 } } )

MongoDB: use array returned from aggregation pipeline for $in query in the next stage

As the question title says, I'm trying to use an array field returned from a $match stage to query another collection in the next stage using $lookup and the $in operator to retrieve all documents that have at least one category inside this array. (I'm using Mongoose in Node, by the way)
I want to match a "configurations" collection by '_id' that have this simplified schema:
{
title: {type: String, required: true},
categories: {
allow: {type: Boolean, required: true},
list: [
{
name: {type: String, required: true},// DENORMALIZED CATEGORY NAME
_id: {type: mongoose.Schema.Types.ObjectId}
}
]
}
}
And in the next stage I want to aggregate all "partners" that belongs to at least one of those categories array. "partners" have the following schema:
{
company: {type: String, required: true},
categories: [
{type: mongoose.Schema.Types.ObjectId}
]
}
This is what I'm doing right now:
configuration.aggregate([
{$match: {_id: ObjectID(configurationId)}},
{
$lookup: {
from: "partners",
pipeline: [
{
$match: {
active: true,// MATCH ALL ACTIVE PARTNERS
categories: {
$in: {// HERE IS THE PROBLEM: I CAN'T RETRIEVE AN ARRAY FROM $map OPERATOR
$map: {// MAP CONFIGURATION CATEGORY LIST TO OUTPUT AN ARRAY ONLY WITH ID OBJECTS
input: '$categories.list',
as: 'category',
in: '$$category._id'
}
}
}
}
},
{ $project: { _id: 1, company: 1 } }
],
as: "partners"
}
},
])
The $map operator works as expected in a $project stage, but in this case I just can't use it's result as an array to be used with $in operator.
Is there any way to do this?
Thanks!
UPDATE
Doing like #Veeram suggested eliminates the need of $map operator in the $lookup stage:
{
"$lookup":{
"from":"partners",
"let":{"categories_id":"$categories.list._id"},
"pipeline":[
{"$match":{"active":true,"$expr":{"$in":["$categories","$$categories_id"]}}},
{"$project":{"_id":1,"company":1}}
],
"as":"partners"
}
}
But the problem persists with the $in operator. Like I've commented, this $in use case is the same as the 4th example in the official documentation (docs.mongodb.com/manual/reference/operator/aggregation/in), and it results in a false statement, because we are trying to check if an array ("$categories") is an element of another array ("$$categories_id"), which will fail, because the elements of "$$categories_id" are id objects and not arrays.
Does anyone know if there is any workaround for this?
Thanks!
You don't need to use $map. You can use dot notation to access the ids.
$let is required to access the values from local collection and $expr to compare the document fields.
Something like
{
"$lookup":{
"from":"partners",
"let":{"categories_id":"$categories.list._id"},
"pipeline":[
{"$match":{
"active":true,
"$expr":{
"$gt":[
{"$size":{"$setIntersection":["$categories","$$categories_id"]}},
0
]
}
}},
{"$project":{"_id":1,"company":1}}
],
"as":"partners"
}
}

Resources