Mongoose aggregate throws error when zero match - mongodb

I'm trying to aggregate a field in mongodb based on some conditions. This works fine when there's at least single match, but it throws error otherwise. I want to the aggregated value as 0 when there's no match. What need to be modified in below code?
Note: fromDate and toDate passed through API Payload.
"errorMesssage": "Cannot read property 'amount' of undefined"
DTO:
export class MetricsDTO {
fromDate: Date;
toDate: Date;
}
Service
async getRevenue(metricClause: Partial<MetricsDTO>) {
if (Object.keys(metricClause).length == 0) {
var rev = await this.bookingsModel.aggregate([
{ $group: { _id: null, amount: { $sum: '$GrossAmount' } } }
])
} else {
var rev = await this.bookingsModel.aggregate([
{ $match: { TxnDate: { $gte: metricClause.fromDate, $lte: metricClause.toDate } } },
{ $group: { _id: null, amount: { $sum: '$GrossAmount' } } }
])
}
return rev[0].amount;
}
Payload
{
"fromDate": "2019-10-24",
"toDate": "2019-10-25"
}

It is not the aggreage causing the exception, this line causes the error, because you didn't check if the rev is null or not.
return rev[0].amount;
So to handle null, you can:
if (rev && rev[0]) {
return rev[0].amount;
} else {
return 0;
}

Related

Statistics with Mongo DB

I have the following DB structure :
{
"uploadedAt": "2021-09-22T22:09:12.133Z",
"paidAt: "2021-09-30T22:09:12.133Z",
"amount": {
"currency": "EUR",
"expected": 70253,
"paid": 0
},
}
I would like to know how do I calculate the total amount that still need to be paid (expected - paid), and the average date between uploadedAt and paidAt. This for multiple records.
My function for getting the data is (the criteria should be updated to get this data).
const invoiceParams = new FindParams();
invoiceParams.criteria = { company: company._id }
const invoices = await this.findAll(invoiceParams);
FindAll function looks like:
async findAll(
params: FindParams,
ability?: Ability,
includeDeleted: boolean = false,
): Promise<Entity[]> {
let queryCriteria: Criteria = params.criteria;
let query: DocumentQuery<Entity[], Entity> = null;
if (!includeDeleted) {
queryCriteria = {
...queryCriteria,
deleted: { $ne: true },
};
}
try {
if (ability) {
ability.throwUnlessCan('read', this.entityModel.modelName);
queryCriteria = {
...toMongoQuery(ability, this.entityModel.modelName),
...queryCriteria,
};
}
query = this.entityModel.find(queryCriteria);
if (params.populate) {
query = query.populate(params.populate);
}
if (params.sort) {
query = query.sort(params.sort);
}
if (params.select) {
query = query.select(params.select);
}
return query.exec();
} catch (error) {
if (error instanceof ForbiddenError) {
throw new ForbiddenException(error.message);
}
throw error;
}
}
Update:
const paymentTime = await this.invoiceModel.aggregate([
{
$group: {
_id: "$account",
averageSpread: { $avg: { $subtract: ["$paidAt", "$uploadedAt"] } },
count: { $sum: 1 }
}
}
]);
Try this aggregation pipeline:
db.invoiceParams.aggregate([
{
$set: {
expectedPaid: { $subtract: ["$amount.expected", "$amount.paid"] },
averageDate: { $toDate: { $avg: [{ $toLong: "$uploadedAt" }, { $toLong: "$paidAt" }] } }
}
}
])

mongoose - update many from an array of Ids

im trying to update all keys ('indexOrder') in an array of objects in a document.
The values for the update are recived as an array from the client:
[{_id:'1s284hd72hdd', indexOrder: 1}, {_id:'543543531', indexOrder: 2}, etc..]
im trying to match the _id of the array from the client with the _id of the objects in the document. When a match is found -> it needs to update the 'indexOrder' in the document to its value from the array from the client.
currently im doing it with looping on the client array, and updating for each iteration.
async updateIndexOrder(orderList) {
try {
orderList.forEach(async ({_id, indexOrder}) => {
await Model.findOneAndUpdate({_id}, {$set:{indexOrder}})
})
return true;
} catch (err) {
throw new Error(err);
}
}
How can I update in one call instead of so many server calls?
something like:
async updateIndexOrder(orderList) {
const idList= orderList.map(x => x._id)
try {
await Model.updatMany(
{_id: {$in:{idList}},
{$set: {indexOrder: orderList[FIND INDEX OF THE ITERATING _id].indexOrder}}
)
} catch (err) {
throw new Error(err);
}
}
Thank you.
you can do it in mongodb 4.2 and onwards
exports.updateDIndexOrder = async (keyValPairArr) => {
try {
let data = await Model.collection.update(
{ _id: { $in: keyValPairArr.map(o => o._id) } },
[{
$set: {
indexOrder: {
$let: {
vars: { obj: { $arrayElemAt: [{ $filter: { input: keyValPairArr, as: "kvpa", cond: { $eq: ["$$kvpa.id", "$_id"] } } }, 0] } },
in: "$$obj.indexOrder"
}
}
}
}],
{ runValidators: true, multi: true }
)
return data;
} catch (error) {
throw error;
}
}

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

MongoDB + Mongoose Aggregate w/ Asnyc

I've got the following route in my express file, which takes parameters passed in from a middleware function and queries my backend MongoDB database. But for some reason, it only ever returns an empty array.
I'd like to convert the Mongoose model that allows me to use aggregate functions into async/await to conform with the rest of my code. It's online here.
module.exports = {
search: asyncWrapper(async(req, res, next) => { // Retrieve and return documents from the database.
const {
filterTarget,
filter,
source,
minDate,
maxDate,
skip,
limit,
sortBy,
sortOrder
} = req.search;
try {
const mongoData = await Model.aggregate([
{
$match: {
date: {
$gt: minDate, // Filter out by time frame...
$lt: maxDate
}
}
},
{
$match: {
[filterTarget]: filter // Match search query....
}
},
{
$set: {
[filterTarget]: { $toLower: `$${filterTarget}` } // Necessary to ensure that sort works properly...
}
},
{
$sort: {
[sortBy]: sortOrder // Sort by date...
}
},
{
$group: {
_id: null,
data: { $push: "$$ROOT" }, // Push each document into the data array.
count: { $sum: 1 }
}
},
{
$project: {
_id: 0,
count: 1,
data: {
$slice: ["$data", skip, limit]
},
}
}
])
return res.status(200).json({ data: mongoData.data || [], count: mongoData.count || 0 });
} catch (err) {
next(err);
}
})
};
For some reason, the route is only returning an empty array every time. I've double and triple checked my variables, they are not the problem.
How can I use the Mongoose.aggregate() function in an async await route?

Mongoose loop through findOneAndUpdate condition statement

I want to know if this part of code can be written differently, only with Mongoose helper methods of models ? Can I return a success and error if no stock are greater then 0 ?
ProductSchema.statics.substractStock = function (products) {
_.map(products, updateStock)
function updateStock(o) {
mongoose.model('Product').findById(o._id, function (err, product) {
return product
}).then(function(productDB){
if(productDB.stock > o.stock && productDB.stock > 0){
mongoose.model('Product').findOneAndUpdate(o._id, {$inc: {stock: -(o.stock)}}, {},
function (err, doc) {
//return success ??
}
);
} else {
//return 'no update'
}
});
}
};
This could be done with an atomic update where you can ditch the initial findById() call and include the comparison logic
if (productDB.stock > o.stock && productDB.stock > 0) { ... }
within the query as in the following:
function updateStock(o) {
mongoose.model('Product').findOneAndUpdate(
{
"_id": o._id,
"$and": [
{ "stock": { "$gt": o.stock } } ,
{ "stock": { "$gt": 0 } }
]
},
{ "$inc": { "stock": -(o.stock) } },
{ "new": true }, // <-- returns modified document
function (err, doc) {
// check whether there was an update
}
);
}