Mongoose aggregate by date and values - node.js

I cannot solve a problem I am having on a project.
First of all, I have a Schema called Emotions, shown below:
import mongoose from 'mongoose';
const EmotionSchema = new mongoose.Schema({
classification: {
type: String,
required: true,
},
probability: {
type: Number,
required: true,
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
}
}, {
timestamps: true,
});
export default mongoose.model('Emotion', EmotionSchema);
And some data like this:
[
{
"_id": "61144a393c532f066725bd24",
"classification": "Feliz",
"probability": 0.98,
"user": "60eecfeba0810013e750cdbc",
"createdAt": "2021-08-11T22:07:53.331Z",
"updatedAt": "2021-08-11T22:07:53.331Z",
"__v": 0
},
{
"_id": "61144a46d30bd006fa2be702",
"classification": "Feliz",
"probability": 0.98,
"user": "60eecfeba0810013e750cdbc",
"createdAt": "2021-08-11T22:08:06.618Z",
"updatedAt": "2021-08-11T22:08:06.618Z",
"__v": 0
},
{
"_id": "611541dd62a7f214afab3ceb",
"classification": "Triste",
"probability": 0.9,
"user": "60eecfeba0810013e750cdbc",
"createdAt": "2021-08-12T15:44:29.150Z",
"updatedAt": "2021-08-12T15:44:29.150Z",
"__v": 0
},
{
"_id": "611541f762a7f214afab3cf5",
"classification": "Raiva",
"probability": 0.86,
"user": "60eecfeba0810013e750cdbc",
"createdAt": "2021-08-12T15:44:55.909Z",
"updatedAt": "2021-08-12T15:44:55.909Z",
"__v": 0
},
{
"_id": "6115420362a7f214afab3cf7",
"classification": "Neutro",
"probability": 0.99,
"user": "60eecfeba0810013e750cdbc",
"createdAt": "2021-08-12T15:45:07.297Z",
"updatedAt": "2021-08-12T15:45:07.297Z",
"__v": 0
},
{
"_id": "6115420462a7f214afab3cf9",
"classification": "Neutro",
"probability": 0.99,
"user": "60eecfeba0810013e750cdbc",
"createdAt": "2021-08-12T15:45:08.002Z",
"updatedAt": "2021-08-12T15:45:08.002Z",
"__v": 0
},
{
"_id": "611543d252be9a187c380e5d",
"classification": "Feliz",
"probability": 0.91,
"user": "60eecfeba0810013e750cdbc",
"createdAt": "2021-08-12T15:52:50.599Z",
"updatedAt": "2021-08-12T15:52:50.599Z",
"__v": 0
}
]
What I want is:
First group each of these records by day.
Afterwards, count the values ​​by classification, but separating these classifications.
Something like that:
[
{
"_id": "12/08/2021",
"feliz": 1,
"triste": 1,
"raiva": 1,
"neutro": 2,
"surpreso": 0,
},
{
"_id": "11/08/2021",
"feliz": 2,
"triste": 0,
"raiva": 0,
"neutro": 0,
"surpreso": 0,
}
]
I did something like that, but it's only working for a value, eg "Feliz".
In code:
Emotion.aggregate([
{ $match:
{
user: user._id,
classification: "Feliz"
}
},
{ $group: {
_id : { $dateToString: { format: "%d/%m/%Y", date: "$createdAt" } },
feliz: { $sum: 1 }
}},
], function(err, results) {
if (err) throw err;
return res.json(results);
})
And it returns:
[
{
"_id": "12/08/2021",
"feliz": 1
},
{
"_id": "11/08/2021",
"feliz": 4
}
]
One more question:
From the Client-side, I always get values ​​for "classification" like:
"Feliz", "Triste", "Surpreso", "Raiva", "Neutro".
So is it better to add an enum to my schema "Emotion"?
(Sorry for my English, i hope you understand).

$group by createdAt date and classification, count sum
$group by createdAt only and construct the array of classification and count in key-value pair
$arrayToObject to convert above key-value pair array of object to an object format
Emotion.aggregate([
{
$match: {
user: user._id,
classification: "Feliz"
}
},
{
$group: {
_id: {
createdAt: {
$dateToString: {
format: "%d/%m/%Y",
date: "$createdAt"
}
},
classification: "$classification"
},
count: { $sum: 1 }
}
},
{
$group: {
_id: "$_id.createdAt",
classifications: {
$push: {
k: "$_id.classification",
v: "$count"
}
}
}
},
{
$addFields: {
classifications: {
$arrayToObject: "$classifications"
}
}
}
],
function(err, results) {
if (err) throw err;
return res.json(results);
})
Playground
Note: this will not return classification when count is 0! You need to do this after query in js.
One more question: From the Client-side, I always get values ​​for "classification" like: "Feliz", "Triste", "Surpreso", "Raiva", "Neutro". So is it better to add an enum to my schema "Emotion"?
It is up to your project requirement, you can set these values in enum to restrict the input.

Related

mongodb $addfields conditions is not working

i have used lookup for join 2 tables and i need to create 2 custom column so i have used the $addfields.
In $addfields, Member is not working but pqr is working.
I have a array of objects as a given below :
await mongoose.models.abc.aggregate([
{
$lookup:
{`enter code here`
from: 'xyz',
localField: 'id',
foreignField: '_id',
as: 'my'
}
},
{
$match: {
"my": { $ne: [] }
}
},
{
$addFields: {
pqr: { $cond: [{ $gte: ["$my.request", 1] }, { $sum: "$my.request" }, 0] },
Member: { $cond: [{ $eq: ["$myClub.request", 0] }, { $sum: "$myClub.request" }, 0] },
}
},
]);
[
{
"_id": "5ef05f650e26a40cb44a68d6",
"createdAt": "2020-06-22T07:36:05.640Z",
"updatedAt": "2020-06-22T07:36:05.640Z",
"__v": 0,
"my": [
{
"_id": "5eecc3c961767f21c8d694e0",
"request": 1,
"createdAt": "2020-06-19T13:55:21.573Z",
"updatedAt": "2020-06-19T13:55:21.573Z",
"__v": 0
},
{
"_id": "5ef1b8f2c3d54e21f4d6b332",
"request": 0,
"createdAt": "2020-06-23T08:10:26.036Z",
"updatedAt": "2020-06-23T08:10:26.036Z",
"__v": 0
}
]
}
]
i need a output like this:
[
{
"_id": "5ef05f650e26a40cb44a68d6",
"createdAt": "2020-06-22T07:36:05.640Z",
"updatedAt": "2020-06-22T07:36:05.640Z",
"__v": 0,
"my": [
{
"_id": "5eecc3c961767f21c8d694e0",
"request": 1,
"createdAt": "2020-06-19T13:55:21.573Z",
"updatedAt": "2020-06-19T13:55:21.573Z",
"__v": 0
},
{
"_id": "5ef1b8f2c3d54e21f4d6b332",
"request": 0,
"createdAt": "2020-06-23T08:10:26.036Z",
"updatedAt": "2020-06-23T08:10:26.036Z",
"__v": 0
}
],
"pqr":1, // for greater than eqaul to request=1
"Member":1 // // for eqaul to request=0
}
]

Mongoose aggregate

I need some help with Mongo, Mongoose and Node.js.
In the code below, I'd like to join carrinho and produtos collection to retrieve produtos _id, price and description in the same array/object.
My Carrinho Schema
const Carrinho = new mongoose.Schema(
{
title: {
type: String,
},
produtos: [{
price: Number,
produto: { type: mongoose.Schema.Types.ObjectId, ref:
"Produtos" }
}
],
total: {
type: Number,
},
},
{
timestamps: true
})
My Produtos Schema
const Produtos = new mongoose.Schema(
{
description: {
type: String,
required: true,
},
gtin: {
type: String,
required: true,
unique: true,
},
thumbnail: {
type: String,
},
price: {
type: Number,
}
},
{
timestamps: true
}
)
After reading aggregate documentation this is the best I've got:
Carrinho.aggregate([
{ "$match": { "_id": mongoose.Types.ObjectId(req.params.id) } },
{
"$lookup": {
"from": "produtos",
"localField": "produtos._id",
"foreignField": "_id",
"as": "produtosnocarrinho"
}
},
{
"$addFields": {
"total": {
"$reduce": {
"input": "$produtos",
"initialValue": 0,
"in": { "$add": ["$$value", "$$this.price"] }
}
}
}
}
]).exec((err, data) => {
if (err) res.json(err)
res.json(data)
});
And this is the result:
[
{
"_id": "5cb76d7d99c3f4062f512537",
"title": "Carrinho do Lucas",
"produtos": [
{
"_id": "5cafead2bc648978100d7698",
"price": 20.1
},
{
"_id": "5cae911adf75ac4d3ca4bcb6",
"price": 20.1
},
{
"_id": "5cb0f0adc5fb29105d271499",
"price": 20.1
}
],
"createdAt": "2019-04-17T18:16:29.833Z",
"updatedAt": "2019-04-19T00:50:43.316Z",
"__v": 3,
"produtosnocarrinho": [
{
"_id": "5cae911adf75ac4d3ca4bcb6",
"description": "AÇÚCAR REFINADO UNIÃO 1KGS",
"gtin": "7891910000197",
"thumbnail": "7891910000197",
"createdAt": "2019-04-11T00:58:02.296Z",
"updatedAt": "2019-04-11T00:58:02.296Z",
"__v": 0
},
{
"_id": "5cafead2bc648978100d7698",
"description": "HASBRO MR. POTATO HEAD MALETA DE PEÇAS",
"gtin": "5010994598815",
"thumbnail": "pecas_300x300-PU3435f_1.jpg",
"createdAt": "2019-04-12T01:33:06.628Z",
"updatedAt": "2019-04-12T01:33:06.628Z",
"__v": 0
},
{
"_id": "5cb0f0adc5fb29105d271499",
"description": "REPELENTE EXPOSIS INFANTIL SPRAY",
"gtin": "7898392800055",
"thumbnail": "PU28bb9_1.jpg",
"createdAt": "2019-04-12T20:10:21.363Z",
"updatedAt": "2019-04-12T20:10:21.363Z",
"__v": 0
}
],
"total": 60.300000000000004
}
]
The following Query will be help:
models.Carrinho.aggregate(
[
{ "$match": { "_id": mongoose.Types.ObjectId(req.params.id) } },
{
"$lookup": {
"from": "produtos",
"localField": "produtos._id",
"foreignField": "_id",
"as": "produtosnocarrinho"
}
},
{
"$addFields": {
"total": {
"$reduce": {
"input": "$produtos",
"initialValue": 0,
"in": { "$add": ["$$value", "$$this.price"] }
}
}
}
},
{$unwind : '$produtos'},
{$unwind : '$produtosnocarrinho'},
{$redact: { $cond: [{
$eq: [
"$produtos._id",
"$produtosnocarrinho._id"
]
},
"$$KEEP",
"$$PRUNE"
]
}
},
{ $project: {
_id : 1,
title : 1,
produtosData : {
_id : "$produtos._id",
price : "$produtos.price",
description : "$produtosnocarrinho.description"
},
total : 1,
createdAt: 1,
updatedAt : 1
}
},
{
$group : {
_id : {
_id : '$_id',
title : '$title',
total : '$total',
createdAt : '$createdAt',
updatedAt : '$updatedAt'
},
produtosData: {$push: "$produtosData" }
}
},
{ $project: {
_id : '$_id._id',
title : '$_id.title',
total : '$_id.total',
createdAt : '$_id.createdAt',
updatedAt : '$_id.updatedAt',
produtosData: '$produtosData'
}
}
]).exec((err, data) => {
if (err) res.json(err)
res.json(data)
});
Output :
[{
"_id": "5cbc42c24502a7318952d7b2",
"title": "Carrinho do Lucas",
"total": 60.300000000000004,
"createdAt": "2019-04-21T10:15:30.629Z",
"updatedAt": "2019-04-21T10:15:30.629Z",
"produtosData": [{
"_id": "5cafead2bc648978100d7698",
"price": 20.1,
"description": "HASBRO MR. POTATO HEAD MALETA DE PEÇAS"
}, {
"_id": "5cae911adf75ac4d3ca4bcb6",
"price": 20.1,
"description": "AÇÚCAR REFINADO UNIÃO 1KGS"
}, {
"_id": "5cb0f0adc5fb29105d271499",
"price": 20.1,
"description": "REPELENTE EXPOSIS INFANTIL SPRAY"
}]
}]
performance depends on produtos matching data from Lookup Query As we are doing double Unwind.

MongoDB group by ID and then by date

I have a collection in my MongoDB database that stores durations for people who are in groups, it looks a like this:
[{
"_id": "5c378eecd11e570240a9b0ac",
"state": "DRAFT",
"groupId": "5c378eebd11e570240a9ae49",
"personId": "5c378eebd11e570240a9aee1",
"date": "2019-01-07T00:00:00.000Z",
"duration": 480,
"__v": 0
},
{
"_id": "5c378eecd11e570240a9b0bb",
"state": "DRAFT",
"groupId": "5c378eebd11e570240a9ae58",
"personId": "5c378eebd11e570240a9aeac",
"date": "2019-01-07T00:00:00.000Z",
"duration": 480,
"__v": 0
},
{
"_id": "5c378eecd11e570240a9b0c5",
"state": "DRAFT",
"groupId": "5c378eebd11e570240a9ae3e",
"personId": "5c378eebd11e570240a9aef6",
"date": "2019-01-07T00:00:00.000Z",
"duration": 480,
"__v": 0
}]
I would like to be able to run an aggregate query which returns a collection of personIds and the duration grouped per day with the corresponding groupId, which would look like this:
[{
"personId": "5c378eebd11e570240a9aee1",
"time": [{
"date": "2019-01-07T00:00:00.000Z",
"entries": [{
"groupId": "5c378eebd11e570240a9ae49",
"duration": 480,
"state": "DRAFT"
}]
}]
}, {
"personId": "5c378eebd11e570240a9aeac",
"time": [{
"date": "2019-01-07T00:00:00.000Z",
"entries": [{
"groupId": "5c378eebd11e570240a9ae58",
"duration": 480,
"state": "DRAFT"
}]
}]
}, {
"personId": "5c378eebd11e570240a9aef6",
"time": [{
"date": "2019-01-07T00:00:00.000Z",
"entries": [{
"groupId": "5c378eebd11e570240a9ae3e",
"duration": 480,
"state": "DRAFT"
}]
}]
}]
So far, I have written the following aggregation (I'm using Mongoose, hence the syntax):
Time.aggregate()
.match({ date: { $gte: new Date(start), $lte: new Date(end) } })
.group({
_id: '$personId',
time: { $push: { date: '$date', duration: '$duration', state: '$state' } },
})
.project({ _id: false, personId: '$_id', time: '$time' })
Which returns the following:
[{
"personId": "5c378eebd11e570240a9aed1",
"time": [{
"date": "2019-01-11T00:00:00.000Z",
"duration": 480,
"state": "DRAFT"
}, {
"date": "2019-01-11T00:00:00.000Z",
"duration": 480,
"state": "DRAFT"
}
// ...
}]
Hopefully you can see that the durations are being grouped by personId but I've not been able to figure out how to apply another grouping to the time array as the dates are duplicated if a personId has more than one duration for a given date.
Is it possible to group by and ID, push to an array and then group the values in that array as an aggregation or will my application need to map/reduce the results instead?
I would suggest running two $group operations in a row:
db.time.aggregate({
// first, group all entries by personId and date
$group: {
_id: {
personId: "$personId",
date: "$date"
},
entries: {
$push: {
groupId: "$groupId",
duration: "$duration",
state: "$state"
}
}
}
}, {
// then, group previously aggregated entries by personId
$group: {
_id: "$_id.personId",
time: {
$push: {
date: "$_id.date",
entries: "$entries"
}
}
}
}, {
// finally, rename _id to personId
$project: {
_id: 0,
personId: "$_id",
time: "$time"
}
})
In Mongoose it should be something like that:
Time.aggregate()
.match({
date: {
$gte: new Date(start),
$lte: new Date(end)
}
})
.group({
_id: {
personId: '$personId',
date: '$date'
},
entries: {
$push: {
groupId: '$groupId',
duration: '$duration',
state: '$state'
}
}
})
.group({
_id: '$_id.personId',
time: {
$push: {
date: '$_id.date',
entries: '$entries'
}
}
})
.project({
_id: false,
personId: '$_id',
time: '$time'
})
db.getCollection("dummyCollection").aggregate(
[
{
"$group" : {
"_id" : "$personId",
"time" : {
"$push" : {
"date" : "$date",
"duration" : "$duration",
"state" : "$state"
}
}
}
},
{
"$project" : {
"_id" : false,
"personId" : "$_id",
"time" : "$time"
}
},
{
"$unwind" : "$time"
},
{
"$group" : {
"_id" : "$time.date",
"time" : {
"$addToSet" : "$time"
}
}
}
]
);
Use $addToSet which returns an array of all unique values that results from applying an expression to each document in a group of documents that share the same group by key.

Issue with Mongoose aggregation

My requirement is for a particular user get all the expenses which are grouped based on the month and return the Month and the total expenses for that month.
I have collection something like below.
expenses = {{
"_id": {
"$oid": "5bc0f74df46dae0bf4ffdc39"
},
"user": {
"$oid": "5bab847a5b0d2e2ce8b4cbe5"
},
"date": "15-Mar-2018",
"expenseInfo": "Clothes",
"category": "Shopping",
"amount": 100,
"__v": 0
},
{
"_id": {
"$oid": "5bc0f78bf46dae0bf4ffdc3b"
},
"user": {
"$oid": "5bab847a5b0d2e2ce8b4cbe5"
},
"date": "11-Apr-2018",
"expenseInfo": "mobile",
"category": "Bills",
"amount": 100,
"__v": 0
},
{
"_id": {
"$oid": "5bc0f76cf46dae0bf4ffdc3a"
},
"user": {
"$oid": "5bab847a5b0d2e2ce8b4cbe5"
},
"date": "18-Apr-2018",
"expenseInfo": "Petrol",
"category": "Fuel",
"amount": 20,
"__v": 0
}
}
Here is my Expense schema.
Also, Note that user object(user is a different schema which is referred in Expense schema)
const ExpenseSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'user'
},
date: {
type: String
},
expenseInfo: {
type: String,
required: true
},
category: {
type: String,
required: true
},
amount: {
type: Number,
required: true
}
});
I tried something like this, but I am not getting any data. What is the issue here?
Expense.aggregate([{
$match: {
"_id": request.user.id
}
},
{
"$group": {
"_id": {
"$arrayElemAt": [{
"$split": ["$date", "-"]
}, 1]
},
"Total": {
"$sum": "$amount"
}
}
}
])

Mongoose Aggregate with $match on date range or local time zone

I have invoice Model as following
{
...
"itemDetails": [
{
"item": "593a1a01bbb00000043d9a4c",
"purchasingPrice": 100,
"sellingPrice": 150,
"qty": 200,
"_id": "59c39c2a5149560004173a05",
"discount": 0
}
],
"payments": [],
"status": "PENDING",
"created": {
"$date": "2017-09-21T11:02:02.675Z"
},
...
}
Sample Invoice Document is as follows.
{
"_id": {
"$oid": "59c39c2a5149560004173a04"
},
"customer": {
"$oid": "5935013832f9fc0004fa9a16"
},
"order": {
"$oid": "59c1df8393cbba0004a0e956"
},
"employee": {
"$oid": "592d0a6238880f0004637e84"
},
"status": "PENDING",
"deliveryStatus": "PROCESSING",
"created": {
"$date": "2017-09-21T11:02:02.675Z"
},
"discount": 0,
"payments": [],
"itemDetails": [
{
"item": {
"$oid": "593a1a01bbb00000043d9a4c"
},
"purchasingPrice": 100,
"sellingPrice": 150,
"qty": 200,
"_id": {
"$oid": "59c39c2a5149560004173a05"
},
"discount": 0
}
],
"__v": 0
}
Item details Item is an object Id which refers to Item collection.
I'm writing a mongoose Aggregate query to get the sale by Item. for that I need to filter the invoice from a given date range and status does not equal to "CANCELED". for that, I have written following code
module.exports.saleByItem = (req, res) => {
let fromDate;
let toDate;
if ((req.query.fromDate && moment(req.query.fromDate, config.dateFormat, true).isValid()) && (req.query.toDate && moment(req.query.toDate, config.dateFormat, true).isValid())) {
fromDate = moment(req.query.fromDate, config.dateFormat).startOf('day');
toDate = moment(req.query.toDate, config.dateFormat).endOf('day');
}
Invoice.aggregate([
{
"$match": {
"created": {
"$gte": fromDate
? fromDate.toDate()
: undefined,
"$lte": toDate
? toDate.toDate()
: undefined
},
"status": {
"$ne": "CANCELED"
}
}
}, {
"$unwind": "$itemDetails"
}, {
"$group": {
"_id": "$itemDetails.item",
"qty": {
"$sum": "$itemDetails.qty"
},
"value": {
"$sum": {
"$multiply": [
"$itemDetails.qty", {
"$subtract": ["$itemDetails.sellingPrice", "$itemDetails.discount"]
}
]
}
},
"avarageSellingPrice": {
"$avg": {
"$subtract": ["$itemDetails.sellingPrice", "$itemDetails.discount"]
}
}
}
}, {
"$sort": {
"value": -1
}
}, {
"$lookup": {
from: "items",
localField: "_id",
foreignField: "_id",
as: "item"
}
}, {
"$unwind": "$item"
}, {
"$project": {
_id: 1,
itemName: "$item.itemName",
qty: 1,
value: 1,
avarageSellingPrice: 1
}
}
]).then(salesFigures => {
res.status(200).json(salesFigures);
}).catch((err) => {
res.status(422).json(err);
});
};
The issue is when I put today date to both dates it returns sale of today. Gives []
How to handle date ranges in $match with local time-zone?

Resources