Express, Mongodb (mongoose) - Data comparison and count - mongodb

In my mongodb database i have datas like:
{ id: 'ran1', code: 'ABC1', createdAt: 'Sep 1 2022', count: 5 }
{ id: 'ran2', code: 'ABC1', createdAt: 'Sep 2 2022', count: 3 }
{ id: 'ran3', code: 'ABC2', createdAt: 'Sep 1 2022', count: 2 }
{ id: 'ran4', code: 'ABC1', createdAt: 'Oct 1 2022', count: 1 }
{ id: 'ran5', code: 'ABC1', createdAt: 'Oct 2 2022', count: 2 }
{ id: 'ran6', code: 'ABC2', createdAt: 'Ocr 1 2022', count: 1 }
now as an output i want all the data from October but i also want to count and compare the percentage.
So the output for October will be
{code: 'ABC1', totalCount: the sum of total count of oct (1+2) =3 , percent: (total count of oct - total count of sep)/total count of oct * 100 }
{code: 'ABC2', totalCount: 1, percent: -100}
I tried to achieve these output using two different aggregation and later map the current month aggregation with each element from previous month aggregation. But i think there are some better solution.
Here is my code
const { filterDate, shop } = req.query;
const splittedFilter = filterDate.split("-");
const query = {
shopUrl: { $regex: shop, $options: "i" },
createdAt: {
$gte: new Date(splittedFilter[0]),
$lte: new Date(splittedFilter[1]),
},
};
const currentCodes = await BlockedCode.aggregate([
{
$match: query,
},
{
$group: {
_id: "$discountCode",
totalCount: { $sum: "$count" },
},
},
]);
const prevQuery = {
shopUrl: { $regex: shop, $options: "i" },
createdAt: {
$gte: new Date(splittedFilter[2]),
$lte: new Date(splittedFilter[3]),
},
};
const previousCodes = await BlockedCode.aggregate([
{
$match: prevQuery,
},
{
$group: {
_id: "$discountCode",
totalCount: { $sum: "$count" },
},
},
]);
const result = currentCodes.map((code) => {
const foundPrevCode = previousCodes.find((i) => i._id === code._id);
if (foundPrevCode?._id) {
const prevCount = foundPrevCode?.totalCount;
const currCount = code?.totalCount;
const difference = currCount - prevCount;
const percentage = (difference / currCount) * 100;
return { ...code, percentage };
} else {
return { ...code, percentage: 100 };
}
});

#shahamar Rahman i don't understand percentage logic can you explain little more!
upto this i filtered and counted the data based October month,plz check it and let me know if it's helps you
https://mongoplayground.net/p/Fet3UUI9LDC
db.collection.aggregate([
{
$addFields: {
month: {
$month: {
$toDate: "$createdAt"
}
}
}
},
{
$match: {
month: 10
}
},
{
$group: {
_id: {
code: "$code"
},
count: {
$sum: "$count"
}
}
}
])

Related

How to get data with a cleaner way using mongoose?

I'm filtering the data based on a Boolean savedBoolean , and if that Boolean is not being inputted I'm getting all the data, this code works for now. But how to do it in a cleaner way since I'm duplicating the code.
let filteredReviews : any | undefined;
if (savedBoolean === true || savedBoolean === false) {
filteredReviews = await Interviewee.aggregate([{
$project: {
_id: 0,
userId: 1,
'interviews.review': 1,
},
},
{
$unwind: '$interviews',
},
{
$match: {
userId: '4',
'interviews.review.saved': savedBoolean,
},
},
{
$group: {
_id: '$interviews.review._id',
review: {
$first: '$interviews.review',
},
},
},
]).skip((Number(page) - 1) * 3).limit(3);
}
if (savedBoolean === undefined) {
filteredReviews = await Interviewee.aggregate([{
$project: {
_id: 0,
userId: 1,
'interviews.review': 1,
},
},
{
$match: {
userId: '4',
},
},
{
$unwind: '$interviews',
},
]).skip((Number(page) - 1) * 3).limit(3);
}
In MongoDB, the db.collection.remove() method removes documents from a collection. You can remove all documents from a collection, remove all documents that match a condition, or limit the operation to remove just a single document.

Mongoose: Filtering documents by date range returns 0 documents

I have this model:
const HistorySchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: "users",
},
category: {
type: String,
enum: category_enum,
required: true,
},
date: {
type: Date,
default: Date.now,
},
});
So a document could be:
{
"_id": "60ddad447b0e9d3c4d4fd1f1",
"user": "60dc8118118ea36a4f3cab7d",
"category": "LOGIN",
"date": "2021-03-02T00:00:00.000Z",
"__v": 0
},
I am trying to get events that happen in a given year with this:
const getEventsOfYear = async (year) => {
let response = {
events_of_year_per_user: null,
};
const start_date_of_the_year = moment(year);
const end_date_of_the_year = moment(year).endOf("year");
const filter_stage = {
$match: {
date: {
$gte: start_date_of_the_year,
$lte: end_date_of_the_year,
},
},
};
const pipeline = [filter_stage];
const history_events_with_aggregate = await History.aggregate(pipeline);
response.events_of_year_per_user = history_events_with_aggregate;
return response;
};
The problem is that this always returns an empty array:
{
"events_of_year_per_user": []
}
Any idea what I'm doing wrong?
EDIT 1:
I even tried with another model and direct date input instead of using moment and it's still the same result:
const filter_stage = {
$match: {
date: {
$gte: "2022-01-01",
$lte: "2022-12-30",
},
},
};
const pipeline = [filter_stage];
const history_events_with_aggregate = await userModel.aggregate(pipeline);
But, using find works:
const history_events_with_aggregate = await userModel.find({
date: { $gte: "2022-01-01", $lte: "2022-12-30" },
});
This is how I solved the issue:
const filter_stage = {
$match: {
date: {
$gte: new Date("2022-01-01"),
$lte: new Date("2022-12-30"),
},
},
};
And if you want to use moment:
const start_date_of_the_year = moment(year);
const end_date_of_the_year = moment(year).endOf("year");
const filter_stage = {
$match: {
date: {
$gte: new Date(start_date_of_the_year),
$lte: new Date(end_date_of_the_year),
},
},
};
You can also do this:
const today = moment().startOf("day");
const filter_stage = {
$match: {
user: ObjectId(user_id),
date: {
$gte: today.toDate(),
$lte: moment(today).endOf("day").toDate(),
},
},
};

Deleting an item with condition in MongoDB?

I want to remove a product from the Cart by checking its quantity. Its quantity should be decremented by 1 unless it reaches zero, and after that, it should pull out from the product array of the Cart.
here is my Logic : (I want to perform the pull and decrement operation inside the single query. But I m stuck on how to perform these two operations together by a simple condition in MongoDb)
const cart = await Cart.findOneAndUpdate({"products.productId": req.body.productId}, {$inc: {"products.$.quantity": -1}}, {new: true})
await Cart.update({"products.productId": req.body.productId}, {$pull: {quantity: 0}})
here is the model for clarification:
import mongoose from 'mongoose';
const cartSchema = new mongoose.Schema({
userId: {
type: String,
required: true,
},
products: [
{
productId: {
type: String,
},
quantity: {
type: Number,
default: 1
}
}
]
}, {timestamps: true});
const Cart = new mongoose.model('Cart', cartSchema);
export default Cart;
Thanks :)
There is no straight way to do this in single regular update query.
To improve your approach you can try this,
first query to check productId and quantity should greater than 1
const cart = await Cart.updateOne(
{
products: {
$elemMatch: {
productId: req.body.productId,
quantity: { $gt: 1 }
}
}
},
{ $inc: { "products.$.quantity": -1 } }
);
Playground
second query if the first query's result is nModified is 0 then pull the product, by checking condition productId and quantity equal-to 1
if (cart.nModified === 0) {
await Cart.updateOne(
{
products: {
$elemMatch: {
productId: req.body.productId,
quantity: { $eq: 1 }
}
}
},
{ $pull: { products: { productId: req.body.productId } } }
)
}
Playground
If you really want to do using single query you can try update with aggregation pipeline starting from MongoDB 4.2,
$map to iterate loop of products array and check condition, if the productId matches then increment/decrement quantity by $add operator otherwise return current quantity
$filter to iterate loop of above result and check condition if productId and quantity is not zero
await Cart.updateOne(
{ "products.productId": req.body.productId },
[{
$set: {
products: {
$filter: {
input: {
$map: {
input: "$products",
in: {
productId: "$$this.productId",
quantity: {
$cond: [
{ $eq: ["$$this.productId", req.body.productId] },
{ $add: ["$$this.quantity", -1] },
"$$this.quantity"
]
}
}
}
},
cond: {
$and: [
{ $eq: ["$$this.productId", req.body.productId] },
{ $ne: ["$$this.quantity", 0] }
]
}
}
}
}
}
])
Playground

Find MongoDB document with the latest date according to different fields

We have data stored in MongoDB by country code. Our document looks like the following,
[
{
title: '1',
US: {
data: { lastReportDate: '2021-09-09' } // will be fetched
},
GB: {
data: { lastReportDate: '2021-09-04' }
}
},
{
title: '2',
US: {
data: { lastReportDate: '2021-09-07' } // will NOT be fetched
}
},
{
title: '3',
US: {
data: null // will NOT be fetched
}
},
{
title: '4',
US: {
data: null
}
GB: {
data: { lastReportDate: '2021-09-08' } // will be fetched
},
NZ: {
data: { lastReportDate: '2021-09-04' }
}
},
{
title: '5',
GB: {
data: null
},
NZ: {
data: { lastReportDate: '2021-09-06' } // will be fetched
}
}
]
I want to fetch the titles which have the latest dates according to the countries.
For EX: in the above DB, we have the latest date for US as '2021-09-09', so I want to fetch all the titles which match this date in lastReportDate. For GB, the latest date is '2021-09-08' and for NZ, its '2021-09-06'.
We have around 180 countries in one document and I want to hit the DB minimum times. So can we build a query that can us latest dates for different countries and then query the Database according to that.
You can try below aggregation:
db.collection.aggregate([
{
$project: {
doc: {
$objectToArray:"$$ROOT"
},
title: "$title"
}
},
{
$unwind: "$doc"
},
{
$match: {
"doc.k": { $nin: [ "_id", "title" ] }
}
},
{
$group: {
_id: "$doc.k",
maxDate: { $max: "$doc.v.data.lastReportDate" },
titles: { $push: { date: "$doc.v.data.lastReportDate", title: "$title" } }
}
},
{
$project: {
_id: 0,
country: "$_id",
maxTitles: { $filter: { input: "$titles", cond: { $eq: [ "$$this.date", "$maxDate" ] } } }
}
}
])
The challenge here is that your countries are represented as keys of your document so you need to start with $obectToArray operator which in conjunction with $unwind will give you a list of countries with corresponding dates and titles.
Once you have them you can use $group to get $max date and then use $filter to get titles related to max date.
Mongo Playground

How can I retrieve current week's data on Mongoose?

I have a Model for all requests that I get for my business (real estate).
I want to create an aggregation middleware based on the "createdAt" field which returns only models created between monday at midnight and the present time.
I have looked through all previous questions but could not find anything and the aggregation docs in mongo are so huge I cannot wrap my head around!
Do you have any tips?
I have tried this but it's returning an empty array:
getGestionaleStats = async (req, res) => {
try {
const richieste = await Richiesta.aggregate([
{
$match: {
createdAt: { $lt: new Date() }
}
},
{
$group: {
_id: { $week: '$createdAt' }
}
}
]);
res.status(200).json({
status: 'success',
data: { richieste }
});
} catch (err) {
console.error(err.message);
res.status(500).json({
status: 'error',
data: err.message
});
}
Of course the {$lt: new Date()} is probably where the problem lies.
Any suggestions?
This solution uses Aggregation query and a custom JavaScript function. The function takes a date object and returns the first Monday's date before today. This is used to get all the documents with createdAt date after the calculated date.
// Function returns the date of the "last Monday" from
// the given input date.
function getLastMonday(dt) {
let n = null; // last Monday conversion
switch (dt.getDay()) {
case 0: n = -5; break;
case 1: n = -6; break;
case 2: n = 0; break;
case 3: n = -1; break;
case 4: n = -2; break;
case 5: n = -3; break;
case 6: n = -4; break;
default: "This never happens";
}
let today_date = new Date(dt.getFullYear(), dt.getMonth(), dt.getDate());
let last_monday_date = today_date.setDate(today_date.getDate() + n );
return last_monday_date;
}
var d = ISODate(); // -or- any date like ISODate("2019-11-26T00:00:00Z")
var LAST_MONDAY = getLastMonday(d);
db.test.aggregate( [
{
$addFields: {
last_monday: {
$dateFromParts : {
year: { $year: new Date(LAST_MONDAY) },
month: { $month: new Date(LAST_MONDAY) },
day: { $dayOfMonth: new Date(LAST_MONDAY) }
}
},
created_at: {
$dateFromParts : {
year: { $year: "$createdAt" },
month: { $month: "$createdAt" },
day: { $dayOfMonth: "$createdAt" }
}
}
}
},
{
$match: { $expr: { $gt: [ "$created_at", "$last_monday" ] } }
},
{
$project: { created_at: 0, last_monday: 0 }
}
] )
For a set of input documents like this:
{ _id : 1, createdAt : ISODate("2019-12-03T00:00:00Z") }
{ _id : 2, createdAt : ISODate("2019-11-12T02:00:00Z") }
{ _id : 3, createdAt : ISODate("2019-11-25T05:00:00Z") }
{ _id : 4, createdAt : ISODate("2019-11-26T00:00:00Z") }
{ _id : 9, createdAt : ISODate("2019-12-02T23:45:00Z") }
And, LAST_MONDAY = getLastMonday(ISODate("2019-12-04T05:40:20Z")), the aggregation query returns the document with _id : 1.
I'm using momentJS for this:
const result = Collection.aggregate([
{
$match: {
createdAt: {
$gte: moment().startOf('isoweek').toDate(),
$lt: moment().endOf('isoweek').toDate()
},
}
}
]);
I've found an answer with vanillaJS:
const richieste = await Richiesta.aggregate([
{
$match: {
createdAt: { $gte: getBeginningOfTheWeek(new Date()), $lt: new Date() }
}
},
{
$group: {
_id: null,
count: { $sum: 1 }
}
},
]}
where getBeginningOfTheWeek is as such:
exports.getBeginningOfTheWeek = (now) => {
const days = (now.getDay() + 7 - 1) % 7;
now.setDate(now.getDate() - days);
now.setHours(0, 0, 0, 0);
return now;
};
The latter function is from T.J. Crowder: get current week moday javascript