Update a document and upsert a subdocument in a single query - mongodb

How do I update an item in the parent document and upsert a subdocument in a single query?
This is my example schema.
const ExampleSchema = new Schema({
user_count: {
type: String,
default: 0
},
users: [
{
id: {
type: Schema.Types.ObjectId,
ref: "users",
unique: true
},
action: {
type: Boolean
}
}
],
});
I am trying to add +1 to user_count and upsert a document to the users array in a single query.
const result = await Example.updateOne(
{
_id: id,
},
{
$set: {
"user_count": user_count++,
"users.$.id": req.user.id,
"users.$.action": true
}
},
{ upsert: true }
);
I have tried the above code, but got the following error.
[0] 'The positional operator did not find the match needed from the query.',
[0] [Symbol(mongoErrorContextSymbol)]: {} }

I'm not familiar with mongoose, so I will take for granted that "user_count": user_count++ works.
For the rest, there are two things that won't work:
the $ operator in "users.$.id": req.user.id, is known as the positional operator, and that's not what you want, it's used to update a specific element in an array. Further reading here: https://docs.mongodb.com/manual/reference/operator/update/positional/
the upsert is about inserting a full document if the update does not match anything in the collection. In your case you just want to push an element in the array right?
In this case I guess something like this might work:
const result = await Example.updateOne(
{
_id: id,
},
{
$set: {
"user_count": user_count++
},
$addToSet: {
"users": {
"id": req.user.id,
"action": true
}
}
}
);
Please note that $push might also do the trick instead of $addToSet. But $addToSet takes care of keeping stuff unique in your array.

db.collection.findOneAndUpdate({_id: id}, {$set: {"user_count": user_count++},$addToSet: {"users": {"id": req.user.id,"action": true}}}, {returnOriginal:false}, (err, doc) => {
if (err) {
console.log("Something wrong when updating data!");
}
console.log(doc);
});

Related

Return all documents only if all matches conditions

Looking for help with a query that returns either true or false (or an empty array when false or similar) I need to query a couple of documents by id in a collection and only return true if all documents match the query, if one or more documents don't match I need a false value returned.
If the documents looks like below with the checked value both true and false I would like a false/empty array value back from the query but if the checked are true in all I want a true or the whole array back.
If a regular find is more suitable i could use that.
I've tried with a regular $match but it only return the matched documents.
I do like this now but feels it could be done in a better way?
const coupons = await CouponModel.find({ id }, { checked: 1, _id: 0 });
const everyCouponIsChecked = coupons.every(data => data.checked === true);
Thanks.
Sample data:
[ { _id: 5e43e7831bc81503efa54c61,
id: 'foo',
checked: true,
},
{ _id: 5e43e7831bc81503efa54c61,
id: 'foo',
checked: true,
},{ _id: 5e43e7831bc81503efa54c61,
id: 'foo',
checked: false,
}]
const result = await MyModel.aggregate([
{
$match: {
id: 'foo',
checked: true,
},
},
]);
You can use a $group stage with null _id, then check if all elements checked field are true with $allElementsTrue operator.
Here's the query :
db.collection.aggregate([
{
$group: {
_id: null,
docs: {
$push: "$$ROOT"
}
}
},
{
$addFields: {
allTrue: {
$allElementsTrue: "$docs.checked"
}
}
},
{
$project: {
result: {
$cond: {
if: {
$eq: [
true,
"$allTrue"
]
},
then: "$docs",
else: "$allTrue"
}
}
}
}
])
If any checked field is false, result will equal to false, else result will be equal to the array of documents.
You can test it here
You could do with a query like this
const coupons = await CouponModel.find({ _id:id, checked:true });
const everyCouponIsChecked = coupons.length > 0;

MongoDb remove element from array as sub property

I am trying to remove an entry in an array that is a sub property of a document field.
The data for a document looks like this:
{
_id: 'user1',
feature: {
enabled: true,
history: [
{
_id: 'abc123'
...
}
]
},
...
}
For some reason I have not been able to remove the element using $pull and I'm not sure what is wrong.
I've looked at the official docs for $pull, this well-known answer, as well this one and another.
I have tried the following query
db.getCollection('userData').update({ _id:'user1' }, {
$pull: {
'feature.history': { _id: 'abc123' }
}
})
and it has no effect. I've double-checked _id and it is a proper match. I've also tried filtering based on the same entry, thinking I need to target the data I'm trying to remove:
db.getCollection('userData')
.update({ _id: 'user1', 'feature.history': { _id: 'abc123' }, { ... })
So far no luck
You need to cast your id to mongoose ObjectId
db.getCollection('userData').update(
{ "_id": "user1" },
{ "$pull": { "feature.history": { "_id": mongoose.Types.ObjectId(your_id) } }
})
db.getCollection('userData').update({ _id:'user1', "feature.history._id" : "abc123" }, {
$pull: {
'feature.history.$._id': 'abc123'
}
})

MongoError: cannot infer query fields to set, path 'users' is matched twice

I am using mongoose. I want to create a document chat with an array users (including userId1, userId2), if I do not find it:
This is how I do:
ChatModel.findOneAndUpdate(
{ users: { $all: [userId1, userId2] }},
{ $setOnInsert: {
users: [userId1, userId2]
}},
{ upsert: true })
.exec()
.catch(err => console.log(err));
But I got the error:
MongoError: cannot infer query fields to set, path 'users' is matched
twice
This is Chat Schema:
{
users: [{ type: Schema.Types.ObjectId, ref: 'User' }],
createdAt: { type: Date, default: Date.now }
}
How can I do it correctly? Thanks
I use this as the condition
{
"users": {
$all: [
{"$elemMatch": userId1},
{"$elemMatch": userId2}
]
}......
}
I know this already has an answer but to hopefully save someone else some time, I had to do this:
{
"users": {
$all: [
{ $elemMatch: { $eq: mongoose.Types.ObjectId(userId1) }},
{ $elemMatch: { $eq: mongoose.Types.ObjectId(userId2) }}
]
}......
}
Modifications from accepted answer:
The $eq was needed just like Dave Howson said in his comment on the accepted answer.
mongoose.Types.ObjectId was needed because I guess the _id property on my schema instance was a string.
There is a workaround for this issue:
db.foo.update({a:{$all:[{$elemMatch:{$eq:0}},{$elemMatch:{$eq:1}}]}},{$set:{b:1}},{upsert:true})
This will match when a is an array with both 0 and 1 in it and it will upsert otherwise.
From: https://jira.mongodb.org/browse/SERVER-13843?focusedCommentId=2305903&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-2305903

How can I find all records where an id is in one array, or another id is in another array?

I need to perform a query that returns all results where an id, or array of ids in an array of ids AND another id, or array of ids, is in another array of ids. Perhaps an example will better explain what I'm trying to do:
Schema:
var somethingSchema = mongoose.Schema({
space_id : String,
title : String,
created : {
type: Date,
default: Date.now
},
visibility : {
groups : [{
type : String,
ref : 'Groups'
}],
users : [{
type : String,
ref : 'User'
}]
}
});
Query:
something.find({
space_id: req.user.space_id,
$and: [
{ $or: [{ "visibility.groups": { $in: groups } }] },
{ $or: [{ "visibility.users": { $in: users } }] }
]
}, function (err, data) {
return res.json(data);
});
In this example, both groups and users are arrays of ids. The query above isn't working. It always returns an empty array. What am I doing wrong?
You should be including all clauses to OR together in a single $or array:
something.find({
space_id: req.user.space_id,
$or: [{ "visibility.groups": { $in: groups } },
{ "visibility.users": { $in: users } }]
}, function (err, data) {
return res.json(data);
});
Which translates to: find all docs with a matching space_id AND that have a visibility.groups value in groups OR a visibility.users value in users.

Check if document exists in mongodb

This is how I check if a document exists:
var query = {};
if (req.body.id) {
query._id = {
$ne: new require('mongodb').ObjectID.createFromHexString(req.body.id)
};
}
Creditor.native(function(err, collection) {
collection.find({
$or: [{
name: req.body.name
}, {
sapId: req.body.sapId
},
query
]
}).limit(-1).toArray(function(err, creditors) {
if (creditors.length > 0) {
return res.send(JSON.stringify({
'message': 'creditor_exists'
}), 409);
} else {
return next();
}
})
});
To avoid that multiple documents exist with the same name or/and the same sapID I do this check on every creation/update of a document.
E.g. I want to update this document and give it a different name
{
name: 'Foo',
sapId: 123456,
id: '541ab60f07a955f447a315e4'
}
But when I log the creditors variable I get this:
[{
_id: 541a89a9bcf55f5a45c6b648,
name: 'Bar',
sapId: 3454345
}]
But the query should only match the same sapID/name. However there totally not the same. Is my query wrong?
You're currently finding docs where name matches OR sapId matches OR _id doesn't match. So that last clause is what's pulling in the doc you're seeing.
You probably mean to find docs where (name matches OR sapId matches) AND _id doesn't match.
collection.find({ $and: [
query,
{ $or: [{
name: req.body.name
}, {
sapId: req.body.sapId
}
] } ]
})
Or more simply:
collection.find({
_id: { $ne: require('mongodb').ObjectID.createFromHexString(req.body.id) },
$or: [{
name: req.body.name
}, {
sapId: req.body.sapId
}
]
})