Is something missing in these positional arguments for nested array updates? - mongodb

I have a document that has 3 nested arrays. I'm updating the second nested array with this function:
// Append $set key
for(var key in u)
updates["scopes.$[].sections.$." + key] = u[key];
Proposal.findOneAndUpdate({
"scopes.sections._id": req.body.id // sectionId
}, {
$set: updates
}, { new: true })
.then(response => {
console.log(response);
})
.catch(error => {
console.log(error);
});
I use a similar function to update the first nested array- scopes. That is working properly and updates the scope that matches. But for the second nested array only the first element of the array is being updated. I logged the id and the correct param is being passed in the req.body.
Is there something I'm missing in the update key- scopes.$[].sections.$.key ?
Edit with sample document and logs-
_id: 6079c199c5464b6296b113f6
name: ""
status: "outstanding"
hasAutomaticThreshold:false
isDiscount:true
discount: 0
discountPercentage: 0
taxRate: 9
companyId: 606f5e179cc0382ad6aacd84
clientId: 6070fa06dd505146ccfac9ec
projectId: 60736ed48fb2c869e0c9b33d
author: 606f5e259cc0382ad6aacd86
scopes: Array
0:Object
title: ""
isExpanded: true
_id: 6079c199c5464b6296b113f7
sections:Array
0:Object
title:"Section One"
description:""
isExpanded:false
_id: 6079c199c5464b6296b113f8
items: Array
1:Object
title:""
description:""
isExpanded:false
_id: 6079c1f8d3176462c0840388
items: Array
And this is what the logged req.body.id and updates object looks like:
6079c1f8d3176462c0840388 // ID
{ 'scopes.$[].sections.$.title': 'Section One' }

The positional $ operator will update single position, you need to use arrayFilters $[<identifier>],
// Append $set key
for(var key in u)
updates["scopes.$[].sections.$[s]." + key] = u[key];
Proposal.findOneAndUpdate(
{ "scopes.sections._id": req.body.id },
{ $set: updates },
{
arrayFilters: [{ "s._id": req.body.id }],
new: true
}
)
.then(response => {
console.log(response);
})
.catch(error => {
console.log(error);
});
Playground

Related

Editing specific element of an array of a particular collection - mongoDb

I am new to mongoDb. I have created a collection named task that has comments field which is array along with other fields. I need to edit specific comment of the task. There is a edit button in each comment. Both task id and comment id are available. Now how to edit specific comment of the task?
Thanks in advance
task api
{
"status":true,
"task":[
{
"_id":"61dfef323a6ee474c4eba926",
"description":"hello there",
"title":"hello",
"comments":[
{
"comment_id":1,
"username":"test",
"comment":"abcd",
"status":true,
},
{
"comment_id":2,
"username":"test",
"comment":"abcdsdfsdf",
"status":true,
}
],
"createdAt":"2022-01-13T09:21:54.795Z",
"updatedAt":"2022-01-13T09:21:54.795Z",
"__v":0
}
]
}
Task model schema
const taskSchema = new Schema({
title: { type: String, required: true },
description: { type: String, required: true },
comments: [Object],
}, {
timestamps: true,
});
I tried using $set but I don't know how to use it in the inner array.
router.route('./comments/edit').post((req, res) => {
const commentId = req.body.commentId;
const taskId = req.body.postId;
const comment = req.body.editedComment;
const updatedAt = new Date();
Task.updateOne(
{ _id: taskId},
{
//what to do here?
// $set: { comments: [ {'comment_id': commentId} ]},
}
)
.then((response) => res.json({ status: true, msg: 'Comment Edited!' }))
.catch(err => res.json({ status: false, msg: err }));
});
Thanks in advance.
This is how to do best:
db.collection.update({
status: true
},
{
$set: {
"task.$[x].comments.$[y].username": "New Name"
}
},
{
arrayFilters: [
{
"x._id": "61dfef323a6ee474c4eba926"
},
{
"y.comment_id": 2
}
]
})
Explained:
Define x and y as arrayFIlters in the update statement.
In the $set statement provide the x & y filters to identify the specific comment for update.
In the example I update the username , but you can update any other value from the targeted array subelement addressed by x & y.
playground
And here is how to update two values at same time in the same nested array element.

Validate array of strings in mongoose

I wanted to validate each value from the request which is array of strings. Something like
emails: [ 'johndoe#gmail.com', 'jandoe#gmail.com' ]
Here is my schema
const UserSchema = mongoose.Schema({
name: {
type: String,
index: true,
required: true,
},
emails: [String],
});
In my validation I wanted to make sure that each email is not already exists in the database. I've tried the following
body("emails").custom((value, { req }) => {
return User.findOne({
emails: { $all: value },
_id: { $ne: req.params.id },
}).then((exists) => {
if (exists) {
return Promise.reject("Email already exists!");
}
});
});
But the problem is if I tried to post multiple emails in array the validation fails and the data will be inserted to db. How can I check if one of the emails already exists and reject the request?
In the docs of $in, it mentioned that:
If the field holds an array, then the $in operator selects the documents whose field holds an array that contains at least one element that matches a value in the specified array...
So you can solve it by:
User.findOne({
emails: { $in: value },
_id: { $ne: req.params.id },
})...

Update query adding ObjectIDs to array twice

I am working on a table planner application where guests can be assigned to tables. The table model has the following Schema:
const mongoose = require('mongoose');
mongoose.Promise = global.Promise;
const tableSchema = new mongoose.Schema({
name: {
type: String,
required: 'Please provide the name of the table',
trim: true,
},
capacity: {
type: Number,
required: 'Please provide the capacity of the table',
},
guests: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Guest',
}],
});
module.exports = mongoose.model('Table', tableSchema);
Guests can be dragged and dropped in the App (using React DND) to "Table" React components. Upon being dropped on a table, an Axios POST request is made to a Node.js method to update the Database and add the guest's Object ID to an array within the Table model:
exports.updateTableGuests = async (req, res) => {
console.log(req.body.guestId);
await Table.findOneAndUpdate(
{ name: req.body.tablename },
{ $push: { guests: req.body.guestId } },
{ safe: true, upsert: true },
(err) => {
if (err) {
console.log(err);
} else {
// do stuff
}
},
);
res.send('back');
};
This is working as expected, except that with each dropped guest, the Table model's guests array is updated with the same guest Object ID twice? Does anyone know why this would be?
I have tried logging the req.body.guestID to ensure that it is a single value and also to check that this function is not being called twice. But neither of those tests brought unexpected results. I therefore suspect something is wrong with my findOneAndUpdate query?
Don't use $push operator here, you need to use $addToSet operator instead...
The $push operator can update the array with same value many times
where as The $addToSet operator adds a value to an array unless the
value is already present.
exports.updateTableGuests = async (req, res) => {
console.log(req.body.guestId);
await Table.findOneAndUpdate(
{ name: req.body.tablename },
{ $addToSet : { guests: req.body.guestId } },
{ safe: true, upsert: true },
(err) => {
if (err) {
console.log(err);
} else {
// do stuff
}
},
);
res.send('back');
};
I am not sure if addToSet is the best solution because the query being executed twice.
If you used a callback and a promise simultaneously, it would make the query executes twice.
So choosing one of them would make it works fine.
Like below:
async updateField({ fieldName, shop_id, item }) {
return Shop.findByIdAndUpdate(
shop_id,
{ $push: { menuItems: item } },
{ upsert: true, new: true }
);
}

Mongoose pull ObjectId from array

i'm trying to do a pretty simple operation, pull an item from an array with Mongoose on a Mongo database like so:
User.update({ _id: fromUserId }, { $pull: { linkedUsers: [idToDelete] } });
fromUserId & idToDelete are both Objects Ids.
The schema for Users goes like this:
var UserSchema = new Schema({
groups: [],
linkedUsers: [],
name: { type: String, required: true, index: { unique: true } }
});
linkedUsers is an array that only receives Ids of other users.
I've tried this as well:
User.findOne({ _id: fromUserId }, function(err, user) {
user.linkedUsers.pull(idToDelete);
user.save();
});
But with no luck.
The second option seem to almost work when i console the lenghts of the array at different positions but after calling save and checking, the length is still at 36:
User.findOne({ _id: fromUserId }, function(err, user) {
console.log(user.linkedUsers.length); // returns 36
user.linkedUsers.pull(idToDelete);
console.log(user.linkedUsers.length); // returns 35
user.save();
});
So it looks like i'm close but still, no luck. Both Ids are sent via the frontend side of the app.
I'm running those versions:
"mongodb": "^2.2.29",
"mongoose": "^5.0.7",
Thanks in advance.
You need to explicitly define the types in your schema definition i.e.
groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }],
linkedUsers: [{ type: Schema.Types.ObjectId, ref: 'User' }]
and then use either
User.findOneAndUpdate(
{ _id: fromUserId },
{ $pullAll: { linkedUsers: [idToDelete] } },
{ new: true },
function(err, data) {}
);
or
User.findByIdAndUpdate(fromUserId,
{ $pullAll: { linkedUsers: [idToDelete] } },
{ new: true },
function(err, data) {}
);
I had a similar issue. I wanted to delete an object from an array, using the default _id from mongo, but my query was wrong:
const update = { $pull: { cities: cityId }};
It should be:
const update = { $pull: { cities: {_id: cityId} }};

Mongoose returning NULL on findOneAndUpdate()

To learn the MEAN stack (using Mongoose), I'm creating a StackOverflow type application. I have Questions that are stored in Mongo(v3.0.7) and they have Answer sub-documents.
I am trying to increment the Vote of an Answer, but when the question is returned it is null. I'm pretty sure there's something wrong with the query, specifically where I'm trying to get the answer with the ID I need to modify.
Question Schema:
var questionsSchema = new mongoose.Schema({
answers: [ answerSchema ],
});
Answer Schema:
var answerSchema = new mongoose.Schema({
votes: { type: Number, default: 0 },
});
Querying for _id returns null:
Question.findOneAndUpdate(
{_id: req.params.questionId, 'answers._id': req.params.answerId },
{ $inc: { 'answers.$.votes': 1 } },
{ new: true },
function(err, question){
if (err) { return next(err); }
//question is returned as NULL
res.json(question);
});
Querying for 0 votes works:
Question.findOneAndUpdate(
{_id: req.params.questionId, 'answers.votes': 0 },
{ $inc: { 'answers.$.votes': 1 } },
{ new: true },
function(err, question){
if (err) { return next(err); }
//question is returned as NULL
res.json(question);
});
UPDATE:
Query through Mongo the result is returned:
db.questions.find({_id: ObjectId('562e635b9f4d61ec1e0ed953'), 'answers._id': ObjectId('562e63719f4d61ec1e0ed954') })
BUT, through Mongoose, NULL is returned:
Question.find(
{_id: Schema.ObjectId('562e635b9f4d61ec1e0ed953'), 'answers._id': Schema.ObjectId('562e63719f4d61ec1e0ed954') },
Try to use mongoose Types ObjectID
http://mongoosejs.com/docs/api.html#types-objectid-js:
var ObjectId = mongoose.Types.ObjectId;
Question.find({
_id: '562e635b9f4d61ec1e0ed953',
'answers._id': new ObjectId('562e63719f4d61ec1e0ed954')
})
Final answer to the original update question:
Question.findOneAndUpdate(
{_id: req.params.questionId,
'answers._id': new ObjectId(req.params.answerId) },
{ $inc: { 'answers.$.votes': 1 } },
{ new: true },
function(err, question){
if (err) { return next(err); }
res.json(question);
});
You don't need ObjectId at all:
Question.findOne({_id: "562e635b9f4d61ec1e0ed953"}, callback)
Mongoose handles the string for you.
In addition, using find() and querying by _id will result in an array of length 0 or 1. Using findOne() will return the document object.