Aggregate results, sum only if condition is true - node.js

I'm trying to get a sum of all values of $revenue, and a count of only where $user is equal to the user param I pass when calling this function.
this.aggregate([
{ $match: { createdAt: { $gte: start, $lte: end }, 'status.verified': true } },
{
$group: {
_id: null,
balance: {
$sum: "$revenue"
},
count: {
$cond: {
if: { $eq: [ "$user", user ] },
then: { $sum: 1 },
else: { $sum: 0 }
}
}
}
}
], next);
I'm expecting the data to look like this:
[ { _id: null, balance: 1287, count: 10 ] }
Where balance is the sum of all revenue fields in the match query, and count is the count of that users contributions to the data.
It works fine if I sum the count unconditionally (e.g. like this)
this.aggregate([
{ $match: { createdAt: { $gte: start, $lte: end }, 'status.verified': true } },
{
$group: {
_id: null,
balance: {
$sum: "$revenue"
},
count: { $sum: 1 }
}
}
], next);
Which suggests the error is with my conditional sum. The error thrown by MongoDB is
TypeError: Cannot read property '0' of undefined
at redacted:10:20
at redacted/node_modules/mongoose/lib/aggregate.js:529:13
My schema is
var schema = new Schema({
user: { type: Schema.Types.ObjectId, ref: 'User' },
status: {
verified: { type: Boolean, default: false },
completed: { type: Boolean, default: false },
canceled: { type: Boolean, default: false },
refused: { type: Boolean, default: false }
},
meta: { type: Schema.Types.Mixed, default: {} },
revenue: { type: Number, default: 0 }
});
Note: the createdAt value used in $match is inserted automatically by a plugin.

The $cond operator should essentially be an expression of the $sum operator, like the following:
this.aggregate([
{ "$match": {
"createdAt": { "$gte": start, "$lte": end },
"status.verified": true
} },
{
"$group": {
"_id": null,
"balance": { "$sum": "$revenue" },
"count": {
"$sum": {
"$cond": [
{ "$eq": [ "$user", user ] },
1, 0
]
}
}
}
}
], next);

Related

How to create mongodb aggregation pipeline between two collections?

I want to create a Mongodb aggregation pipeline for a collection named Transaction.
The Transaction collection has values amount, categoryID, description and I also have a Category collection with values type, icon and color.
I want the pipeline to show the top3 categories with their percentage values and a others category with its percentage value.
the transaction type should be Expense which it should get from the Category collection and it should show all transactions having category type Expense. The top3 should then give the results as transaction with category (example)
type : Rent
percentage:45
type: Entertainment
percentage: 30
type: Food
percentage: 20
type: Others
percentage: 5
I tried it with Category collection but I don't want category to store amount, but Transaction should store amount.
Category.aggregate([
{
$match: {
type: 'expense'
}
},
{
$group: {
_id: "$name",
amount: { $sum: "$amount" }
}
},
{
$group: {
_id: null,
totalExpense: { $sum: "$amount" },
categories: {
$push: {
name: "$_id",
amount: "$amount"
}
}
}
},
{
$project: {
_id: 0,
categories: {
$map: {
input: "$categories",
as: "category",
in: {
name: "$$category.name",
percent: { $multiply: [{ $divide: ["$$category.amount", "$totalExpense"] }, 100] }
}
}
}
}
},
{
$unwind: "$categories"
},
{
$sort: { "categories.percent": -1 }
},
{
$limit: 3
}
])
This was the pipeline I used for it.
//edit
Tried the method suggested by Joe
Transaction.aggregate([
// Join the Transaction collection with the Category collection
{
$lookup: {
from: 'Category',
localField: 'categoryID',
foreignField: '_id',
as: 'category',
},
},
// Unwind the category array to separate documents
{
$unwind: '$category',
},
// Filter for transactions where the category type is "Expense"
{
$match: {
'category.type': 'Expense',
},
},
// Group transactions by category type and calculate the percentage
{
$group: {
_id: '$category.type',
total: { $sum: '$amount' },
count: { $sum: 1 },
},
},
{
$project: {
_id: 0,
category: '$_id',
percentage: {
$multiply: [{ $divide: ['$count', { $sum: '$count' }] }, 100],
},
},
},
// Sort the categories by percentage in descending order
{
$sort: { percentage: -1 },
},
// Limit the result to top 3 categories
{
$limit: 3,
},
// group the rest of the categories as others
{
$group: {
_id: null,
top3: { $push: '$$ROOT' },
others: { $sum: { $subtract: [100, { $sum: '$top3.percentage' }] } },
},
},
{
$project: {
top3: 1,
others: { category: 'Others', percentage: '$others' },
},
},
]);
I am getting an empty array rather than the values. I have data in the collections with the correct ID's. What might be the issue?
//Answer
This aggregation worked for me
Transaction.aggregate([
{
$match: {
userID: { $eq: UserID },
type: 'Expense',
},
},
{
$addFields: { categoryID: { $toObjectId: '$categoryID' } },
},
{
$lookup: {
from: 'categories',
localField: 'categoryID',
foreignField: '_id',
as: 'category_info',
},
},
{
$unwind: '$category_info',
},
{
$group: {
_id: '$category_info.name',
amount: { $sum: '$amount' },
},
},
{
$sort: {
amount: -1,
},
},
{
$group: {
_id: null,
total: { $sum: '$amount' },
data: { $push: '$$ROOT' },
},
},
{
$project: {
results: {
$map: {
input: {
$slice: ['$data', 3],
},
in: {
category: '$$this._id',
percentage: {
$round: {
$multiply: [{ $divide: ['$$this.amount', '$total'] }, 100],
},
},
},
},
},
others: {
$cond: {
if: { $gt: [{ $size: '$data' }, 3] },
then: {
amount: {
$subtract: [
'$total',
{
$sum: {
$slice: ['$data.amount', 3],
},
},
],
},
percentage: {
$round: {
$multiply: [
{
$divide: [
{
$subtract: [
'$total',
{ $sum: { $slice: ['$data.amount', 3] } },
],
},
'$total',
],
},
100,
],
},
},
},
else: {
amount: null,
percentage: null,
},
},
},
},
},
]);

MongoDB Aggregate field between two dates

I want to aggregate complaints based on status between two dates.
Sample Data :
In JSON format
Code :
const result = await Complaint.aggregate([
{
$match: {
createdAt: {
$gte: "2021-08-31T18:30:00.000Z",
$lt: "2021-09-30T18:29:59.999Z",
},
},
},
{ $group: { _id: "$status", count: { $sum: 1 } } },
]);
Expected result :
[ { _id: 'progress', count: 1 }, { _id: 'raised', count: 4 } ]
but result is always coming as empty []
try it
const result = await Complaint.aggregate([
{
$match: {
createdAt: {
$gte: ISODate("2021-08-31T18:30:00.000Z"),
$lt: ISODate("2021-09-30T18:29:59.999Z"),
},
},
},
{ $group: { _id: "$status", count: { $sum: 1 } } },
]);
or
const result = await Complaint.aggregate([
{
$match: {
createdAt: {
$gte: new Date("2021-08-31T18:30:00.000Z"),
$lt: new Date("2021-09-30T18:29:59.999Z"),
},
},
},
{ $group: { _id: "$status", count: { $sum: 1 } } },
]);

Count by category and sum it up in MongoDB

I have a product collection in MongoDb which sole fields like _id, category, user_id.
I want to check and count the sum number of each category in collection given the matching the user_id and then sum up all the count again at the end.
my solution is :
return Product.aggregate([
{ $match: { "user_id": "id if user that added the product" } },
{ "$unwind": "$category" },
{
"$group": {
"_id": {
'category': '$category',
},
"count": { "$sum": 1 }
}
},
{ "$sort": { "_id.category": 1 } },
{
"$group": {
"_id": "$_id.category",
"count": { "$first": "$count" }
}
}
])
the code gives me the count of each category without matching the condition of user_id. But when I add the $match it fails.
Product Schema:
const ProductSchema = new Schema({
title: {
type: String,
required: true
},
description: {
type: String,
required: true
},
quantity: {
type: Number,
default: -1
},
category:
{
type: String,
required: true
},
manufactured_by: {
type: String,
required: true
},
user_id: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true
}
})
my result if I dont add the condition:
[
{
"_id": "A Tables",
"count": 1
},
{
"_id": "C Tables",
"count": 4
},
{
"_id": "B Tables",
"count": 2
}
]
Not sure what you are trying to achieve from the last stage in your pipeline.
But the following should give you desired output (without any complications that you added)
async getSellerStatistics(seller_id) {
return await Product.aggregate([
{ $match: { user_id: seller_id } }
{ $unwind: "$category" },
{
$group: {
_id: "$category",
count: { $sum: 1 },
},
},
{ $sort: { _id: 1 } },
])
}

How to convert string to ObjectId type and use in $lookup for mongodb?

I want to get the business information with businessId as a reference. However, I can't get the correct data because the previous developer did not use ObjectId type on the model. Now what I want to do is convert the businessId type to objectId withough altering the model, it would be easy if I do it but the old data will be affected, which is not good. Please see below for the model
const scanHistory = new Schema({
businessId: { type: String },
domains: [
{
domainId: { type: String },
scanType: { type: String },
status: { type: String },
reportUrl: { type: String },
scanStart: { type: Date, default: Date.now },
scanFinish: { type: Date, default: Date.now }
}
],
scanStart: { type: Date, default: Date.now },
scanStatus: { type: String },
scanType: { type: String }
});
This is my aggregate query
.collection("scanhistories")
.aggregate([
{
$addFields: {
businessObjId: {
$convert: {
input: "businessId",
to: "objectId",
onError: "Could not convert to type ObjectId."
}
}
}
},
{
$group: {
_id: { $max: "$businessId" },
businessObjId: { $max: "$businessId" },
scanStatus: { $max: "$scanStatus" },
scanStart: { $max: "$scanStart" },
domains: { $max: "$domains" }
}
},
{
$lookup: {
from: "businesses",
as: "businessInfo",
localField: "businessObjId",
foreignField: "_id"
}
},
{
$project: {
_id: 1,
businessObjId: 1,
primaryDomain: { $arrayElemAt: ["$businessInfo.primaryDomain", 0] },
businessName: { $arrayElemAt: ["$businessInfo.businessName", 0] },
frequency: { $arrayElemAt: ["$businessInfo.scanFrequency", 0] },
scanStatus: 1,
domains: 1
}
},
{
$match: {
scanStatus: { $in: ["running", "undef"] },
domains: { $exists: true }
}
}
])
.toArray();
for (let x = 0; x < history.length; x++) {
console.log(history[x]);
}
Now the output is like this which is not the one I expected.
{ _id: 5de09321bdb7cc07b7595de4,
businessObjId: 5de09321bdb7cc07b7595de4,
scanStatus: 'undef',
domains:
[ { _id: 5dfa626300007c243c1528b3,
domainId: '5de09321bdb7cc07b7595de5',
scanType: 'scheduled',
status: 'running',
reportUrl: '',
scanStart: 2019-12-18T17:31:14.754Z,
scanFinish: 2019-12-18T17:31:14.754Z } ] }
The expected result should have been with the lookup businessInfo that I wanted
{ _id: 5de09321bdb7cc07b7595de4,
businessObjId: 5de09321bdb7cc07b7595de4,
scanStatus: 'undef',
domains:
[ { _id: 5dfa626300007c243c1528b3,
domainId: '5de09321bdb7cc07b7595de5',
scanType: 'scheduled',
status: 'running',
reportUrl: '',
scanStart: 2019-12-18T17:31:14.754Z,
scanFinish: 2019-12-18T17:31:14.754Z } ],
primaryDomain: "mydomainxxx.xy",
businessName: "The biz",
scanFrequency: "daily"
}
Can you help me? I am really new to MongoDB and my background is PHP/SQL so all advises will be much appreciated. Thank you!
So I have found the solution for this. It was just a single mistake. :(
So on the aggregate code above where the group pipeline is. I made a mistake here.
Previous code above
{
$group: {
_id: { $max: "$businessId" },
businessObjId: { $max: "$businessId" },
scanStatus: { $max: "$scanStatus" },
scanStart: { $max: "$scanStart" },
domains: { $max: "$domains" }
}
},
Correct one
I change this part here:
businessObjId: { $first: "$businessObjId" }
{
$group: {
_id: { $max: "$businessId" },
businessObjId: { $first: "$businessObjId" },
scanStatus: { $max: "$scanStatus" },
scanStart: { $max: "$scanStart" },
domains: { $max: "$domains" }
}
},

Get conversation's last message with unread count

I'm making an in app messaging system in which I have to show the list of conversations with their last message and the unread count. My schema is as follows--
var schema = new Schema({
senderID: {
type: Schema.Types.ObjectId,
ref: 'Member'
},
receiversID: [{
type: Schema.Types.ObjectId,
ref: 'Member'
}],
content: {
type: String,
default: ''
},
isRead: {
type: Boolean,
default: false,
},
createdAt: {
type: Number,
default: Date.now
}
});
I did this initially to get all the conversations with their last message --
messageModel.aggregate(
[{ $match: { senderID: userId } },
{ $unwind: '$receiversID' },
{ $sort: { createdAt: -1 } },
{ $group: { _id: '$receiversID', unreadCount: { $sum: { $cond: [{ $eq: ["$isRead", false] }, 1, 0] } }, senderID: { $first: '$senderID' }, receiversID: { $first: '$receiversID' }, content: { $first: '$content' } } },
{ $skip: pagingData.pageSize * (pagingData.pageIndex - 1) },
{ $limit: pagingData.pageSize }
], function (err, docs) {
resolve(docs);
}
);
But it doesn't shows the messages if you are a receiver. I want to show the conversation whether you are receiver or sender.
i use something like this:
{
'$or': [
{
'$and': [
{
'receiversID': userId
}, {
'senderID': toUserId
}
]
}, {
'$and': [
{
'receiversID': toUserId
}, {
'senderID': userId
}
]
},
],
}

Resources