Mongoose updateMany based on result of previous query - mongodb

I have two collections energyOffers and energyOfferLogs. When a user deactivated their account I'm looking for all the remaining active energyOffers where the entity of the user is in the assignees array, not in the declinedEntities array and the offerValidTill date is less than the current timestamp.
const [energyOffers] = await EnergyOffer.find([{
'assignees.id': entityID,
declinedEntities: {
$ne: leadID
},
offerValidTill: { $gt: Date.now() }
}], { session });
Based on these energyOffers I need to update the corresponding energyOfferLogs. I can find these with { entityID: entityID, 'offer.offerID': offer._id } but how can I look for all these offers in the same query?
If I loop through the energyOffers I will have to perform multiple updates while my guess is that this can be done in one updateMany. I was looking into the $lookup aggregate operator (https://www.mongodb.com/docs/v6.0/reference/operator/aggregation/lookup/) but it seems that the EnergyOffer find query is too complex to perform in this.
await EnergyOfferLog.updateMany({ ??? }, {
$set: {
'offer.action': 'declined',
'offer.action_date': Math.floor(Date.now()),
'offer.action_user': user.first_name,
'offer.action_user_id': userID
}
});

Get all offer ids from the first query, e.g.
let ids = energyOffers.map(o => o._id)
Use $in to match logs for all matching offers:
await EnergyOfferLog.updateMany({ entityID: entityID, 'offer.offerID': {$in: ids} }, {
$set: {
'offer.action': 'declined',
'offer.action_date': Math.floor(Date.now()),
'offer.action_user': user.first_name,
'offer.action_user_id': userID
}
});

If you want to do it with one query only, it is not complex. You can use $lookup with a pipeline for this:
Start with your $match query on the energyOffers collection
Use '$lookupto get the matchingenergyOfferLogs`
Clean the pipeline to contain only the energyOfferLogs docs
Perform the $set
Use $merge to save it back to energyOfferLogs collection
db.energyOffers.aggregate([
{$match: {
"assignees.id": entityID,
declinedEntities: {$ne: leadID},
offerValidTill: {$gt: Date.now()}
}
},
{$lookup: {
from: "energyOfferLogs",
let: {offerId: "$_id"},
pipeline: [
{$match: {
$and: [
{entityID: entityID},
{$expr: {$eq: ["$offer.offerID", "$$offerId"]}}
]
}
}
],
as: "energyOfferLogs"
}
},
{$unwind: "$energyOfferLogs"},
{$replaceRoot: {newRoot: "$energyOfferLogs"}},
{$set: {
"offer.action": "declined",
"offer.action_date": Math.floor(Date.now()),
"offer.action_user": user.first_name,
"offer.action_user_id": userID
}
},
{$merge: {into: "$energyOfferLogs"}}
])
See how it works on the playground example
Answer was updated according to a remark by #Alex_Blex

Related

How to use two MongoDB aggregations to perform an updateMany

I am trying to write a script that uses 2 aggregates and saves the results as an array to be used for an updateMany.
The first aggregate finds any documents that has a firstTrackingId and a secondTrackingId on it. I save this into an array. This aggregate is working correctly when tested alone.
The second aggregate will use the first aggregate's result array, pulling all documents that have a firstTrackingId from the first aggregate's results. This one will pull any documents that do NOT have a secondTrackingId on it, and save the unique mongo _id/ObjectId to an array.
The updateMany will use all of the results from the second aggregation to update all relevant documents with a status of void.
All these functions are working when I give them hard-coded data, but I can't figure out how to pull the data from the arrays. I am not even sure if I'm "saving" it correctly, or if there is something else I should be doing aside from just initializing the aggregation as an array.
var ids = db.getCollection('Test').aggregate([
{
$match: {
"firstTrackingId": { "$ne": "" },
"secondTrackingId": { "$exists": true }
}
},
{
$group: {
_id: "$firstTrackingId",
}
},
])
var secondIds = db.getCollection('Test').aggregate([
{
$match: {
"firstTrackingId": { $in: ids },
"secondTrackingId": { $exists: false }
}
},
{
$group: {
"_id": "$_id",
}
},
])
db.getCollection('Test').updateMany({
"_id": {
"$in": secondIds
},
}, { $set: {
"status": "VOID"
} })
I tried printing the first aggregation's results out... can't really figure out how... so for the first one if I do:
print(ids.next(ids._id))
I get:
[object BSON]
Which leads me to believe I need to somehow perform an $objectToArray. If anyone has any insight, that'd be awesome. Thank you!
If you are using MongoDB 4.4+, you can do that with a single aggregation pipeline:
match documents with both first and second tracking ID
lookup an array of all documents with the same first tracking ID
unwind the array
consider the array elements as the root document
match to eliminate any that have a second tracking ID
set the desired status field
merge the results with the original collection
{$match: {
firstTrackingId: { $ne: "" },
secondTrackingId: { $exists: true }
}},
{$lookup:{
from: "Test",
localField:"firstTrackingId",
foreignField:"firstTrackingId",
as:"matched"
}},
{$unwind:"$matched"},
{$replaceRoot:{newRoot:"$matched"}},
{$match:{secondTrackingId:{"$exists":false}}},
{$addFields:{status:"VOID"}},
{$merge: {into: "Test"}}

How to set one data field to another date field in the same object of the collection mongodb

I am trying to edit the fields of entries in a collection. I am checking if the lastUpdated date is less then published date. If it is, then the entry is probably faulty and I need to make the lastUpdated date same as published date. I have created the following mongo query for it :-
db.runCommand({ aggregate: "collectionNameHere",pipeline: [
{
$project: {
isFaulty: {$lt: ["$lastUpdated","$published"]}
}
},{
$match: {
isFaulty: true
}
},{
$addFields: {
lastUpdated: "$published"
}
}]
})
I am able to get the list of documents which have this fault, but I am not able to update the field. The last $addFields does not seem to be working. There is no error as well. Can someone help me with this or if they can provide me a better query fro my use case.
Thanks a lot.
You're doing a mistake by trying to update with aggreggation, what is not possible. You have to use update command to achieve your goal.
Cannot test it right now, but this should do the job :
db.collection.update({
$expr: {
$lt: [
"$lastUpdated",
"$published"
]
}
},
{$set:{lastUpdated:"$published"}}
)
It is not possible to update the document with the same field. You can use $out aggregation
db.collection.aggregate([
{ "$match": { "$expr": { "$lt": ["$lastUpdated", "$published"] }}},
{ "$addFields": { "lastUpdated": "$published" }}
])
here but it always creates a new collection as the "output" which is also not a solution here.
So, at last You have to use some iteration here to first find the document using find query and then need to update it. And with the async await now it quite easy to work this type of nested asynchronous looping.
const data = await db.collection
.find({ "$expr": { "$lt": ["$lastUpdated", "$published"] }})
.project({ lastUpdated: 1 })
.toArray()
await Promise.all(data.map(async(d) => {
await db.collection.updateOne({ _id: d._id }, { $set: { lastUpdated: d.published }})
}))

Compare document array size to other document field

The document might look like:
{
_id: 'abc',
programId: 'xyz',
enrollment: 'open',
people: ['a', 'b', 'c'],
maxPeople: 5
}
I need to return all documents where enrollment is open and the length of people is less than maxPeople
I got this to work with $where:
const
exists = ['enrollment', 'maxPeople', 'people'],
query = _.reduce(exists, (existsQuery, field) => {
existsQuery[field] = {'$exists': true}; return existsQuery;
}, {});
query['$and'] = [{enrollment: 'open'}];
query['$where'] = 'this.people.length<this.maxPeople';
return db.coll.find(query, {fields: {programId: 1, maxPeople: 1, people: 1}});
But could I do this with aggregation, and why would it be better?
Also, if aggregation is better/faster, I don't understand how I could convert the above query to use aggregation. I'm stuck at:
db.coll.aggregate([
{$project: {ab: {$cmp: ['$maxPeople','$someHowComputePeopleLength']}}},
{$match: {ab:{$gt:0}}}
]);
UPDATE:
Based on #chridam answer, I was able to implement a solution like so, note the $and in the $match, for those of you that need a similar query:
return Coll.aggregate([
{
$match: {
$and: [
{"enrollment": "open"},
{"times.start.dateTime": {$gte: new Date()}}
]
}
},
{
"$redact": {
"$cond": [
{"$lt": [{"$size": "$students" }, "$maxStudents" ] },
"$$KEEP",
"$$PRUNE"
]
}
}
]);
The $redact pipeline operator in the aggregation framework should work for you in this case. This will recursively descend through the document structure and do some actions based on an evaluation of specified conditions at each level. The concept can be a bit tricky to grasp but basically the operator allows you to proccess the logical condition with the $cond operator and uses the special operations $$KEEP to "keep" the document where the logical condition is true or $$PRUNE to "remove" the document where the condition was false.
This operation is similar to having a $project pipeline that selects the fields in the collection and creates a new field that holds the result from the logical condition query and then a subsequent $match, except that $redact uses a single pipeline stage which restricts contents of the result set based on the access required to view the data and is more efficient.
To run a query on all documents where enrollment is open and the length of people is less than maxPeople, include a $redact stage as in the following::
db.coll.aggregate([
{ "$match": { "enrollment": "open" } },
{
"$redact": {
"$cond": [
{ "$lt": [ { "$size": "$people" }, "$maxPeople" ] },
"$$KEEP",
"$$PRUNE"
]
}
}
])
You can do :
1 $project that create a new field featuring the result of the comparison for the array size of people to maxPeople
1 $match that match the previous comparison result & enrollment to open
Query is :
db.coll.aggregate([{
$project: {
_id: 1,
programId: 1,
enrollment: 1,
cmp: {
$cmp: ["$maxPeople", { $size: "$people" }]
}
}
}, {
$match: {
$and: [
{ cmp: { $gt: 0 } },
{ enrollment: "open" }
]
}
}])

Mongodb - aggregation of subdocument and update with the result

I have the following problem. I have found and summarized each value in a subdocument.
It gives the following [ { _id: 551fb140e4b04589d8997213, sumOfpeople: 342 } ]
I want to take the sumOfpeople and insert it to the same House( the same req.params.house_id)
House.aggregate([
{ $match: {
id: req.params.house_id
}},
{ $unwind: '$people' }, // unwind creates a doc for every array element
{ $group: {
_id: '$_id',
sumOfpeople: { $sum: '$people.nr'}
}}
], function (err, result) {
if (err) {
console.log(err);
return;
}
console.log(result);
});
This is the model that I want insert the result after the aggregation into.
module.exports = mongoose.model('House', {
id: String,
people: [{
id: String,
nr: Number
}],
sumOfpeople: Number //this is the field that I want to update after the aggregation
});
I have tried to use $set : {sumOfpeople: { $sum: '$people.nr'}}.
Is it possible to use $set inside an aggregation, or how can it be solved otherwise?
There's no way in MongoDB to write results directly into an existing document while doing an aggregation.
You've got 2 options:
retrieve the results in your application code, and then in a second query update the document.
use the $out operator, that will write the results of the aggregation into a new collection. This operation will delete all documents in the results collection and insert the new one. ( http://docs.mongodb.org/manual/reference/operator/aggregation/out/ )

How to aggregate queries in mongodb

I have a document collection that look like the following:
{
name : "tester"
, activity: [
{
gear: "glasses"
where: "outside"
}
, {
gear: "hat"
, where: "inside"
}
, {
gear: "glasses"
, where: "car"
}
]
}
How do I query the collection to return only documents with multiple activities that contain the value of "gear":"glasses"?
Thanks!
I think it's possible to do without aggregation framework, if you need full document filtered by your condition:
db.collection.find({
"activity": {$elemMatch: {gear:"glasses"}},
"activity.1" : {$exists: 1}
})
This is going to be ugly with aggregation framework, but it can be done:
db.collection.aggregate(
{$match: {"activity.gear": "glasses"}},
{$unwind: "$activity"},
{$group: {
_id: {_id: "$_id", name: "$name"},
_count: {$sum: {$cond: [{$eq: ["glasses", "$activity.gear"]}, 1, 0]}}
}},
{$match: {_count: {$gt: 1}}}
)
When analyzing the above query, I would recommend walking through step. Start with just the "$match", the the "$match" and "$unwind". And so one. You will see how each step works.
The response is not the full document. If you are looking for the full document, include a $project step that passes through a dummy activity, and reconstruct the full document on the output.
You can also try this:
db.collection.find( { activity: { $elemMatch: { gear: "glasses" } } )