MongoDB: add object to subarray if not exists - node.js

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.

Related

$push into deeply nested array mongoose

my userSchema is as follows...
const userSchema = new mongoose.Schema({
firstName: { type: String },
lastName: { type: String },
movies: [
{
name: String,
duration: Number,
actors: [{ name: String, age: Number }]
}
],
});
In my NodeJS app with express I am trying to update my actors array to have another actor with value stroking from req.body.
I thought that I could do something like this...
await User.updateOne(
{
_id: req.user.id
movies._id: req.params.id
},
{
$push: { 'movies.actors': req.body }
}
)
I thought this would push an actor into the specified movie req.params.id but does nothing instead.
Try using positional operator $ in this way:
db.collection.update({
"_id": 1,
"movies._id": 1
},
{
"$push": {
"movies.$.actors": req.body
}
})
Note the only diference is the use of $. But with positional operator you are saying mongo where push the new data.
As docs says:
The positional $ operator identifies an element in an array to update without explicitly specifying the position of the element in the array.
So you can update the element movies.actors pushing new data without knowin the position of the element.
Example here
Try this:
await user.updateOne(
{$and:[{_id: req.user.id},{movie.name: 'I am Legend'}]},
{$set: { movies.actors:req.body}},
);

remove a particular array element from mongodb on clicking the element

I'm using this code for removing a particular array element stored in MongoDB when clicked that from the app. But this code is not working.
schema structure looks like this -
const tagsSchema = new Schema({
category: {
type: String,
required: true
},
tname: { type: Array }
}, { _id: true });
Below is the code I'm using for removing array element from db -
Tags.updateOne({ tname: req.params.name }, { $pull: { _id: [req.params.id] } })
For example - "tname": "technical", "nontechnical"
Now, technical is being clicked in the app to remove, but with the code I'm using it's not getting removed.
you can use directly your element_value without [] after your array field, like below..
Tags.updateOne({tname: req.params.name}, { $pull: { your_array_field: req.params.id } } )
You have to find the specific tag by its "_id" and then remove the particular name from the "tname" array.
Tags.updateOne({ _id: req.params.id }, { $pull: { tname: req.params.name } })

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✌🏾

MongoDB Searching if element exist in array

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 ] }
}
}
}
}
}
])

mongoose subdocument sorting

I have an article schema that has a subdocument comments which contains all the comments i got for this particular article.
What i want to do is select an article by id, populate its author field and also the author field in comments. Then sort the comments subdocument by date.
the article schema:
var articleSchema = new Schema({
title: { type: String, default: '', trim: true },
body: { type: String, default: '', trim: true },
author: { type: Schema.ObjectId, ref: 'User' },
comments: [{
body: { type: String, default: '' },
author: { type: Schema.ObjectId, ref: 'User' },
created_at: { type : Date, default : Date.now, get: getCreatedAtDate }
}],
tags: { type: [], get: getTags, set: setTags },
image: {
cdnUri: String,
files: []
},
created_at: { type : Date, default : Date.now, get: getCreatedAtDate }
});
static method on article schema: (i would love to sort the comments here, can i do that?)
load: function (id, cb) {
this.findOne({ _id: id })
.populate('author', 'email profile')
.populate('comments.author')
.exec(cb);
},
I have to sort it elsewhere:
exports.load = function (req, res, next, id) {
var User = require('../models/User');
Article.load(id, function (err, article) {
var sorted = article.toObject({ getters: true });
sorted.comments = _.sortBy(sorted.comments, 'created_at').reverse();
req.article = sorted;
next();
});
};
I call toObject to convert the document to javascript object, i can keep my getters / virtuals, but what about methods??
Anyways, i do the sorting logic on the plain object and done.
I am quite sure there is a lot better way of doing this, please let me know.
I could have written this out as a few things, but on consideration "getting the mongoose objects back" seems to be the main consideration.
So there are various things you "could" do. But since you are "populating references" into an Object and then wanting to alter the order of objects in an array there really is only one way to fix this once and for all.
Fix the data in order as you create it
If you want your "comments" array sorted by the date they are "created_at" this even breaks down into multiple possibilities:
It "should" have been added to in "insertion" order, so the "latest" is last as you note, but you can also "modify" this in recent ( past couple of years now ) versions of MongoDB with $position as a modifier to $push :
Article.update(
{ "_id": articleId },
{
"$push": { "comments": { "$each": [newComment], "$position": 0 } }
},
function(err,result) {
// other work in here
}
);
This "prepends" the array element to the existing array at the "first" (0) index so it is always at the front.
Failing using "positional" updates for logical reasons or just where you "want to be sure", then there has been around for an even "longer" time the $sort modifier to $push :
Article.update(
{ "_id": articleId },
{
"$push": {
"comments": {
"$each": [newComment],
"$sort": { "$created_at": -1 }
}
}
},
function(err,result) {
// other work in here
}
);
And that will "sort" on the property of the array elements documents that contains the specified value on each modification. You can even do:
Article.update(
{ },
{
"$push": {
"comments": {
"$each": [],
"$sort": { "$created_at": -1 }
}
}
},
{ "multi": true },
function(err,result) {
// other work in here
}
);
And that will sort every "comments" array in your entire collection by the specified field in one hit.
Other solutions are possible using either .aggregate() to sort the array and/or "re-casting" to mongoose objects after you have done that operation or after doing your own .sort() on the plain object.
Both of these really involve creating a separate model object and "schema" with the embedded items including the "referenced" information. So you could work upon those lines, but it seems to be unnecessary overhead when you could just sort the data to you "most needed" means in the first place.
The alternate is to make sure that fields like "virtuals" always "serialize" into an object format with .toObject() on call and just live with the fact that all the methods are gone now and work with the properties as presented.
The last is a "sane" approach, but if what you typically use is "created_at" order, then it makes much more sense to "store" your data that way with every operation so when you "retrieve" it, it stays in the order that you are going to use.
You could also use JavaScript's native Array sort method after you've retrieved and populated the results:
// Convert the mongoose doc into a 'vanilla' Array:
const articles = yourArticleDocs.toObject();
articles.comments.sort((a, b) => {
const aDate = new Date(a.updated_at);
const bDate = new Date(b.updated_at);
if (aDate < bDate) return -1;
if (aDate > bDate) return 1;
return 0;
});
As of the current release of MongoDB you must sort the array after database retrieval. But this is easy to do in one line using _.sortBy() from Lodash.
https://lodash.com/docs/4.17.15#sortBy
comments = _.sortBy(sorted.comments, 'created_at').reverse();

Resources