Mongodb aggregate $push with $cond and $each - node.js

I'm trying to use $cond to conditionally $push multiple integers onto a numbers array during an aggregate $group without any success. Here is my code:
Item.aggregate(
[
{
$group: {
_id: "$_id",
numbers: {
$push: {
$cond: {
if: { $gt: [ "$price.percent", 70 ] },
then: { $each: [10,25,50,70] },
else: null,
}
}
}
}
},
]
)
...
Is Mongo DB just not set up for this right now, or am I looking at this all wrong?

Please try it without $each as below
Item.aggregate(
[{
$group: {
_id: "$_id",
numbers: {
$push: {
$cond: {
if: { $gt: [ "$price.percent", 70 ] },
then: [10,25,50,70] ,
else: null,
}
}
}
}
}]);

Provided answers will work but they'll add null to the array whenever else block gets executed & at the end you need to filter out the null values from the actual array (numbers) which is an additional step to do!
You can use $$REMOVE to conditionally exclude fields in MongoDB's $project.
Item.aggregate(
[{
$group: {
_id: "$_id",
numbers: { $push: { $cond: [{ $gt: ["$price.percent", 70] }, [10, 25, 50, 70], '$$REMOVE'] } } // With $$REMOVE nothing happens on else
}
}]);
REF: $cond

Have you tried:
Item.aggregate(
[
{
$group: {
_id: "$_id",
numbers: {
$push: {
$each: {
$cond: {
if: { $gt: [ "$price.percent", 70 ] },
then: [10,25,50,70],
else: null
}
}
}
}
}
},
]
)

Related

how to calculate total for each enum of a field in aggregate?

hello I have this function where I want to calculate the number of orders for each status in one array, the code is
let statusEnum = ["pending", "canceled", "completed"];
let userOrders = await Orders.aggregate([
{
$match: {
$or: [
{ senderId: new mongoose.Types.ObjectId(req.user._id) },
{ driverId: new mongoose.Types.ObjectId(req.user._id) },
{ reciverId: new mongoose.Types.ObjectId(req.user._id) },
],
},
},
{
$group: {
_id: null,
totalOrders: { $sum: 1 },
totalPendingOrders: "??", //I want to determine this for each order status
totalCompletedOrders: "??",
totalCanceledOrders: "??",
},
},
]);
so I could add add a $match and use {status : "pending"} but this will filter only the pending orders, I could also map the status enum and replace each element instead of the "pending" above and then push each iteration in another array , but that just seems so messy, is there any other way to calculate total for each order status with using only one aggregate?
thanks
You can use group as you used, but with condition
db.collection.aggregate([
{
$group: {
_id: null,
totalPendingOrders: {
$sum: { $cond: [ { $eq: [ "$status", "pending" ] }, 1, 0 ] }
},
totalCompletedOrders: {
$sum: { $cond: [ { $eq: [ "$status", "completed" ] }, 1, 0 ] }
},
totalCanceledOrders: {
$sum: { $cond: [ { $eq: [ "$status", "canceled" ] }, 1, 0 ] }
}
}
}
])
Working Mongo playground

Last value by type with _id relation

I have a collection of Event containing a type that can be 0, 1, 2, 3, 4, 5 and a createdAt date. Each event is related to an other collection called RTS.
I want to gather for each Rts, the last event of each type.
Problem using my soluce :
The problem is, I have to describe each types one by one in order to make it work. Is there any solution to have dynamical key creation induced by the type value ?
Here is what I get now :
I sort the data
Group by idRTS whch contains the link to the second collection. For each type, push the values inside of a specific array.
Remove the null values from the types arrays.
Keep the first value only (the most updated).
Makes the data presentable.
[
{
$sort: {
idRTS: -1,
createdAt: -1
}
},
{
$group: {
_id: '$idRTS',
type0: {
$push: {
$cond: {
if: {
$eq: [
'$type', 0
]
},
then: '$$ROOT',
else: null
}
}
},
type5: {
$push: {
$cond: {
if: {
$eq: [
'$type', 5
]
},
then: '$$ROOT',
else: null
}
}
}
}
},
{
$project: {
_id: '$_id',
type0: {
'$filter': {
'input': '$type0',
'as': 'd',
'cond': {
'$ne': [
'$$d', null
]
}
}
},
type5: {
$filter: {
input: '$type5',
as: 'd',
cond: {
$ne: [
'$$d', null
]
}
}
}
}
},
{
$project: {
_id: '$_id',
type0: {
$arrayElemAt: [
'$type0', 0
]
},
type5: {
'$arrayElemAt': [
'$type5', 0
]
}
}
}
]
$match type in 0 or 5
$sort by idRTS and createdAt in descending order
$group by both idRTS and createdAt field and get first object, this will get first document of both type
$group by idRTS and make array of both types, in k(key) and v(value) format
$project to convert type array to object using $objectToArray
db.collection.aggregate([
{ $match: { type: { $in: [0, 5] } } },
{
$sort: {
idRTS: -1,
createdAt: -1
}
},
{
$group: {
_id: {
idRTS: "$idRTS",
type: "$type"
},
type: { $first: "$$ROOT" }
}
},
{
$group: {
_id: "$_id.idRTS",
type: {
$push: {
k: { $toString: "$type.type" },
v: "$type"
}
}
}
},
{ $project: { type: { $arrayToObject: "$type" } } }
])
Playground

How to find array greater than document size in MongoDB

I have schema like below
[
{
id:"111"
tags:[222,333,444,555]
},
{
id: "222"
tags:[312,345,534]
},
{
id:"333"
tags:[111,222,333,444,555]
},
]
I want to find all documents where tags array size is greater than document size returned by $match in aggregation pipeline, so in above Ex. the number of documents are 3 so i want to return all documents having tags array size greater that 3
[
{
id:"111"
tags:[222,333,444,555]
},
{
id:"333"
tags:[111,222,333,444,555]
},
]
I am using aggregation pipeline to process other info, I am stuck at how to have store document size so that i can find all tags greater than document size
below is query which i am using, i want to do it in aggregation and in one call
.aggregate([
{
"$match":{
"ids":{
"$in":[
"111",
"222",
"333"
]
}
}
})]
Facet helps you to solve this problem.
$facet helps to categorize the incoming documents. We use totalDoc for counting the document and allDocuments for getting all the documents
$arrayElemAt helps to get the first object from totalDoc where we already know that only one object should be inside the totalDoc. Because when we group it, we use _id:null
$unwind helps to de-structure the allDocuments array
Here is the code
db.collection.aggregate([
{
$facet: {
totalDoc: [
{
$group: {
_id: null,
count: {
$sum: 1
}
}
}
],
allDocuments: [
{
$project: {
tags: 1
}
}
]
}
},
{
$addFields: {
totalDoc: {
"$arrayElemAt": [
"$totalDoc",
0
]
}
}
},
{
$unwind: "$allDocuments"
},
{
$addFields: {
sizeGtDoc: {
$gt: [
{
$size: "$allDocuments.tags"
},
"$totalDoc.count"
]
}
}
},
{
$match: {
sizeGtDoc: true
}
},
{
"$replaceRoot": {
"newRoot": "$allDocuments"
}
}
])
Working Mongo playground
You can try,
$match you condition
$group by null and make root array of documents and get count of root documents in count
$unwind deconstruct root array
$match tags size and count greater than or not using $expr expression match
$replaceRoot to replace root object in root
db.collection.aggregate([
{ $match: { id: { $in: ["111", "222", "333"] } } },
{
$group: {
_id: null,
root: { $push: "$$ROOT" },
count: { $sum: 1 }
}
},
{ $unwind: "$root" },
{ $match: { $expr: { $gt: [{ $size: "$root.tags" }, "$count"] } } },
{ $replaceRoot: { newRoot: "$root" } }
])
Playground
Second option:
first 2 stages $match and $group both are same as like above query,
$project to filter root array match condition if tags size and count greater than or not, this will return filtered root array
$unwind deconstruct root array
$replaceRoot replace root object to root
db.collection.aggregate([
{ $match: { id: { $in: ["111", "222", "333"] } } },
{
$group: {
_id: null,
root: { $push: "$$ROOT" },
count: { $sum: 1 }
}
},
{
$project: {
root: {
$filter: {
input: "$root",
cond: { $gt: [{ $size: "$$this.tags" }, "$count"] }
}
}
}
},
{ $unwind: "$root" },
{ $replaceRoot: { newRoot: "$root" } }
])
Playground
You can skip $unwind and $replaceRoot stages if you want because this query always return one document in root, so you can easily access like this result[0]['root'], you can save 2 stages processing and execution time.
You could use $facet to get two streams i.e. one with the filtered documents and the counts using $count. The resulting streams can then
be aggregated further with a $filter as follows to get the desired result
db.getCollection('collection').aggregate([
{ '$facet': {
'counts': [
{ '$match': { 'id': { '$in': ['111', '222', '333'] } } },
{ '$count': "numberOfMatches" }
],
'docs': [
{ '$match': { 'id': { '$in': ['111', '222', '333'] } } },
]
} },
{ '$project': {
'result': {
'$filter': {
'input': '$docs',
'cond': {
'$gt': [
{ '$size': '$$this.tags' },
{ '$arrayElemAt': ['$counts.numberOfMatches', 0] }
]
}
}
}
} }
])

mongodb match and group 2 query in one

var oneWeekAgo = new Date();
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
User.aggregate([
{ $match: { isAdmin: false, isActive: true } },
{
$group: {
_id: null,
totalCount: {
$sum: 1
}
}
},
])
User.aggregate([
{ $match: { isAdmin: false, dateCreated: { $gte: oneWeekAgo }, isActive: true } },
{
$group: {
_id: null,
lastWeekTotal: {
$sum: 1
}
}
},
])
is there a way to combine 2 aggregation queries above?
I want to count all the entries in the collection and also entries that are created within a week.
Expected result:
[ { _id: null, totalCount: 100 , lastWeekTotal: 10 } ]
You can combine inside $group together like this,
The $cond operator, there are three arguments ($cond: [if check condition, then, else])
first part if condition checks your conditions using $and operator, if conditions is true then return 1 otherwise 0
User.aggregate([
{
$group: {
_id: null,
totalCount: {
$sum: {
$cond: [
{
$and: [
{ $eq: ["$isAdmin", false] },
{ $eq: ["$isActive", true] }
]
},
1,
0
]
}
},
lastWeekTotal: {
$sum: {
$cond: [
{
$and: [
{ $gte: ["$dateCreated", oneWeekAgo] },
{ $eq: ["$isAdmin", false] },
{ $eq: ["$isActive", true] }
]
},
1,
0
]
}
}
}
}
])
Playground

mongodb $sum only postive Numbers

is their a way i can sum only positive numbers and avoid those with negative in my MongoDB aggregate query i.e
aggregate([
{
$match: { },
},
{ $group: { _id: '$id', total: { $sum: '$amount' } } },
])
You can use $cond operator and simply $sum 0 when the number is negative:
{ $group: { _id: '$id', total: { $sum: { $cond: [ { $gt: [ '$amount', 0 ] }, '$amount', 0 ] } } } }

Resources