I'm updating a nested array and it updates the wrong element - mongodb

I have a list of apiKeys stored as a nested array of objects below my user. I'm looking to revoke the key using an update query, however, the query is updating the wrong record (seems to be the first isActive:true array element).
db.users.update({
$and: [
{ _id: <TheUserId> },
{ 'apiKeys._id': <TheKeyId> },
{ 'apiKeys.isActive': true } // This means the revoked date can't be changed
]
},
{
$set: {
'apiKeys.$.isActive': false,
'apiKeys.$.revokedAt': new Date()
}
})
Strangely, it seems to work fine if I remove the { _id: <TheUserId> } (though this could be a false impression on my part).
Edit: I was incorrect about this

This approach is wrong, both properties will search its own condition in any of the object, it will not rely on second property's condition, We need to specify that both properties should be in same object,
{ 'apiKeys._id': <TheKeyId> },
{ 'apiKeys.isActive': true }
We need to specify that both fields should be in same object using $elemMatch,
db.collection.update({
_id: <TheUserId>,
apiKeys: {
$elemMatch: {
_id: <TheKeyId>,
isActive: true
}
}
},
{
$set: {
"apiKeys.$.isActive": false,
"apiKeys.$.revokedAt": new Date()
}
})
Playground

Related

MongoDB: How to update document fields based on document field calculations?

I'm wondering how I can perform calculations on document fields, and then update an existing field in that document based on those calculations?
I'm currently using a roundabout way of doing it (below), but I'm wondering if there's a more performant or straight-forward way?
Or if possible, have a document field ("cumulativeField") that is dynamic and updates in the following way:
db.collection
.aggregate([
{ $match: { arrayField: { $exists: true } } },
{ $addFields: { cumulativeField: { $sum: "$arrayField.number" } } }
])
.forEach(function (x){
db.collection.updateOne(
{ id: x.id },
{ $set: { cumulativeField: NumberInt(x.cumulativeField) } }
)})
Note: arrayField = an "array of objects" field, with each object in the array having a key "number" whose value(s) I am summing up to then put as a single value into the "cumulativeField" field.
MongoDB >= 4.2 supports pipeline updates, and updates can be like aggregation, aggregation result is the new value of the document.
In you case i think you only need to write the code as
updateOne({},
[{ $match: { arrayField: { $exists: true } } },
{ $addFields: { cumulativeField: { $toInt: { $sum: "$arrayField.number" } } } }])

How to migrate value of nested subdocument in array in Mongoose

I have a Mongoose collection called Track that has an array of fitnessPlan subdocuments, each of which currently has a month field that needs to be changed to week in production. I am using mongoose-migrate to migrate these values from the old month field to a new week field. Here's what I have got at the moment:
async function up () {
await Track.updateMany({},
{
$set: {
'fitnessPlans.$[elem].month': '$fitnessPlans.$[elem].week',
},
},
{ arrayFilters: [{ "elem.week": { $gte: 0 } }], strict: false, });
await Track.updateMany({},
{
$unset: {
'fitnessPlans.$[elem].week': '',
},
},
{ arrayFilters: [{ "elem.week": { $gte: 0 } }], strict: false, });
}
However, mongoose-migrate is throwing the following error:
Cast to number failed for value "$fitnessPlans.$[elem].week" at path "month"
I'm guessing this is because the string isn't evaluating correctly, but I'm not sure how else to reference that field's value in this setting.
Try update with aggregation pipeline starting from MongoDB 4.2,
$map to iterate loop of fitnessPlans array merge objects with current and new created week field using $mergeObjects
$unset month field
async function up () {
await Track.updateMany({},
[
{
$set: {
fitnessPlans: {
$map: {
input: "$fitnessPlans",
in: {
$mergeObjects: ["$$this", { week: "$$this.month" }]
}
}
}
}
},
{ $unset: "fitnessPlans.month" }
],
{ strict: false });
}
Playground

Conditional array update MongoDB

I have a collection like this one:
{
"citizen":[
{
"country":"ITA",
"language":"Italian"
},
{
"country":"UK",
"language":"English"
},
{
"country":"CANADA",
"language":"French"
}]
}
I'm trying to update the collection but with a certain condition. I need to update remove an element of the array citizen if the value in the field country is longer than 3 characters.
I got that to remove an element i have to use $pull, to check the size of a string I have to use $strLenCP and $gt is greater than, but I'm struggling to put them together.
The result of the update should be:
{
"citizen":[
{
"country":"ITA",
"language":"Italian"
},
{
"country":"UK",
"language":"English"
}]
}
Any suggestions?
EDIT:
with the collection, as it is, the command:
db.getCollection('COLLECTION').update( { }, { $pull: {"citizen": {"country": /^[\s\S]{4,}$/}}}, { multi: true })
works perfectly.
I tried it on another collection as this one:
{
"cittadino":{
"citizen":[
{
"country":"ITA",
"language":"Italian"
},
{
"country":"UK",
"language":"English"
},
{
"country":"CANADA",
"language":"French"
}]
}
}
and it doesn't update anymore. What should i do?
You're on the right path:
db.getCollection('COLLECTION').update( { }, { $pull: {"citizen": {"country": /^[\s\S]{4,}$/}}}, { multi: true })
$pull operator takes a value or a condition. You need to use this to filter the array based on the "country" property
EDIT:
Use the dot-notation to access the nested document
db.getCollection('COLLECTION').update( { }, { $pull: {"cittadino.citizen": {"country": /^[\s\S]{4,}$/}}}, { multi: true })

How to query all instances of a nested field with variable paths in a MongoDb collection

I have a collection that has documents with a field called overridden. I need to find the number of times overridden: true.
So for example, I have a collection as follows:
[ // assume this is my collection / array of documents
{ // this is a document
textfield1: {
overridden: true,
},
page1: {
textfield2: {
overridden: true,
},
textfield3: {
overridden: false,
}
},
page2: {
section1: {
textfield4: {
overridden: true,
}
}
}
},
{ // this is a different document
page1: {
section1: {
textfield1: {
overridden: false,
},
textfield2: {
overridden: false,
}
},
section2: {
textfield3: {
overridden: true,
}
}
}
}
}
So from the above, I need to get # of fields overridden = 4.
This is a simplified example of how the documents in the collection are structured. I'm attempting to show that:
There is no guaranteed structure to find the path to each overridden field in the document.
I need to aggregate across documents in the collection.
From research online, I did the following:
db.reports.aggregate()
.group({
_id: "overridden",
totalOverridden: {
"$sum": {
$cond: [ { "overridden": true }, 1, 0]
}
}
})
That gave me a value of 2472 in the actual collection, which looks like the total number of times that field occurs because if I remove the entire $cond I still get the same value. From the looks of it, { "overridden": true } always returns true, because if I flip the if-else return values (or just do $sum: 1), I get 0.
If it's any help, I do actually have mongoose schemas with defined paths for each overridden field but the schema is a bit large and heavily nested, therefore tracking each path would be quite tedious. That being said, if the above is not possible, I'm open to suggestions for analyzing the schema/document JSON as well to get the different paths and somehow using those to query all the fields.
Thanks a ton! I'd appreciate any help. :-)
This is possible, but not very pretty. You can repeatedly convert the object to any array, eliminate all fields that do not contain objects or have the name "overridden", and repeat.
There is no flow control in an aggregation pipeline, so you won't be able to have it automatically detect when to stop. Instead, you'll need to repeat the extraction for the number of levels of embedding that you want to support.
Perhaps something like:
reapeating = [
{$unwind: {
path: "$root",
preserveNullAndEmptyArrays: true,
}},
{$unwind: {
path: "$v",
preserveNullAndEmptyArrays: true
}},
{$match: {
$or: [
{"root.k": "overridden", "root.v":true},
{"root.v": {$type: 3}}
]
}},
{$project: {
root: {
$cond: {
if: {
$eq: [
"object",
{$type: "$root.v"}
]
},
then: {$objectToArray: "$root.v"},
else: "$root"
}
}
}}
]
Then to find all "overridden" fields down to 5 levels deep:
pipeline = [
{$project: {
_id: 0,
root: {$objectToArray: "$$ROOT"}
}}
];
final = [
{$match: {
"root.k": "overridden",
"root.v": true
}},
{$count: "overridden"}
];
for(i=0;i<5;i++){
pipeline = pipeline.concat(repeating)
}
pipeline = pipeline.concat(final);
db.reports.aggregate(pipeline)
Playground

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