MongoDB Aggregate Query Returning Empty Array - mongodb

I'm new to Mongo and am trying to run an aggregate command on my model in my Node.js application (using Express). I'm trying to run the query for finding all users registered within the last month.
When I run User.find(), it returns all 2 users in the DB, so I know the users are definitely there.
However, when I run this, data is just an empty array. Is there a solution I'm missing here?
router.get("/stats", verifyTokenAndAdmin, async (req, res) => {
const date = new Date();
const lastYear = new Date(date.setFullYear(date.getFullYear() - 1));
try {
const data = await User.aggregate([
{ $match: { createdAt: { $gte: lastYear } } },
{
$project: {
month: { $month: "$createdAt" },
},
},
{
$group: {
_id: "$month",
total: { $sum: 1 },
},
},
]);
res.status(200).json(data)
} catch (err) {
res.status(500).json(err);
}
});
Also, the response is a 200 so no errors there.

Related

mongodb aggregation where document field is less than another field

Using mongoose, I'm trying to make a query that searches for tasks where timeSpent is greater than timeBilled.
Task schema:
const myTaskSchema = new Schema({
date: { type: Date, default: Date.now },
timeSpent: { type: Number },
timeBilled: { type: Number }
})
The query I've tried:
myTaskSchema.aggregate([
{
$match: {
timeSpent: { $gt: '$timeBilled' }
}
}
])
.then(data => {
console.log(data)
})
But I'm getting zero results (I know there should be results)
NOTE: Not every task has a timeSpent or timeBilled.field if that matters.
here is my dirty solution. It'd be nice if I didnt have to add a field but this gets me where I want to be.
myTaskSchema.aggregate([
{
$addFields: {
needToBill: { $gt: ['$timeSpent', '$timeBilled'] }
}
},
{
$match: {
needToBill: true
}
},
{
$project: {
timeSpent: 1,
timeBilled: 1
}
}
])

How do I count all the documents in a collection and use the cont in a controller, with MongoDB and Express.js?

I am working on a blogging application (click the link to see the GitHub repo) with Express (version 4.17.1), EJS and MongoDB (version 4.0.10).
Trying to paginate the posts I did the following, in the controller:
exports.getPosts = (req, res, next) => {
const perPage = 5;
const currPage = req.query.page ? parseInt(req.query.page) : 1;
let postsCount = 0;
const posts = Post.find({}, (err, posts) => {
postsCount = posts.length;
let pageDecrement = currPage > 1 ? 1 : 0;
let pageIncrement = postsCount >= perPage ? 1 : 0;
if (err) {
console.log('Error: ', err);
} else {
res.render('default/index', {
moment: moment,
layout: 'default/layout',
website_name: 'MEAN Blog',
page_heading: 'XPress News',
page_subheading: 'A MEAN Stack Blogging Application',
currPage: currPage,
posts: posts,
pageDecrement: pageDecrement,
pageIncrement: pageIncrement
});
}
})
.sort({
created_at: -1
})
.populate('category')
.limit(perPage)
.skip((currPage - 1) * perPage);
};
And in the view:
<a class="btn btn-primary <%= pageDecrement == 0 ? 'disabled' : '' %>" href="/?page=<%= currPage - pageDecrement %>">← Newer Posts</a>
and
<a class="btn btn-primary <%= pageIncrement == 0 ? 'disabled' : '' %>" href="/?page=<%= currPage + pageIncrement %>">Older Posts →</a>
That works fine unless there are is a number of posts equal to perPage x N, where N is an integer, in which case the "Older Posts" button becomes disabled one page too late.
That is because postsCount = posts.length counts the posts after they are limited by .skip((currPage - 1) * perPage).
So I need to count the posts from the model/collection and bring that count variable in the controller.
My model:
const mongoose = require('mongoose');
const postSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
short_description: {
type: String,
required: true
},
full_text: {
type: String,
required: true
},
category: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Category'
},
post_image: {
type: String,
required: false
},
updated_at: {
type: Date,
default: Date.now()
},
created_at: {
type: Date,
default: Date.now()
}
});
module.exports = mongoose.model('Post', postSchema);
How do I count all the documents in the posts collection and use that number in the posts controller?
This can be done easier with mongodb aggregation framework.
We use $facet aggregation to get the paginated data along with the total number of documents.
In aggregation framework we use $lookup instead of mongoose populate. $lookup returns an array, to get the first item in array we use $arrayElemAt operator inside $addFields.
Playground
And here is the code to apply to your app:
(The first $match aggregation is unnecessary here, but I put in in case you may need it in the future)
exports.getPosts = async (req, res, next) => {
const perPage = 5;
const currPage = req.query.page ? parseInt(req.query.page) : 1;
const skip = (currPage - 1) * perPage;
try {
const result = await Post.aggregate([{
$match: {},
},
{
$sort: {
created_at: -1,
},
},
{
$lookup: {
from: "categories",
localField: "category",
foreignField: "_id",
as: "category",
},
},
{
$addFields: {
category: {
$arrayElemAt: ["$category", 0],
},
},
},
{
$facet: {
totalRecords: [{
$count: "total",
}, ],
data: [{
$skip: skip,
},
{
$limit: perPage,
},
],
},
},
]);
let postsCount = result[0].totalRecords[0].total;
const pageCount = Math.ceil(postsCount / perPage);
const pageDecrement = currPage > 1 ? 1 : 0;
const pageIncrement = currPage < pageCount ? 1 : 0;
const posts = result[0].data;
res.render("default/index", {
moment: moment,
layout: "default/layout",
website_name: "MEAN Blog",
page_heading: "XPress News",
page_subheading: "A MEAN Stack Blogging Application",
currPage,
posts,
pageDecrement,
pageIncrement,
});
} catch (err) {
console.log("Error: ", err);
res.status(500).send("something went wrong");
}
};
By the way, in the post schema, for date fields you use default: Date.now(), this will cause the date value always the same value, it should be in this format: default: Date.now
Read $facet.
New in version 3.4.
Processes multiple aggregation pipelines within a single stage on the
same set of input documents. Each sub-pipeline has its own field in
the output document where its results are stored as an array of
documents.
Example: See here
db.collection.aggregate([
{
$facet: {
"count": [
{ $match: {} },
{ $count: "totalCount" }
],
"data": [
{ $match: {} },
{ $sort: { _id: -1 } },
{ $skip: 1 },
{ $limit: 2 }
]
}
}
])
Mongoose Version:
Model.aggregate([
{
$facet: {
"count": [
{ $match: {} },
{ $count: "totalCount" }
],
"data": [
{ $match: {} },
{ $sort: { _id: -1 } },
{ $skip: 1 },
{ $limit: 2 }
]
}
}
]).
then(res => console.log(res)).
catch(error => console.error('error', error));
In case of Mongoose you should use this:
https://mongoosejs.com/docs/api.html#aggregate_Aggregate-facet
Official Mongodb docs:
https://docs.mongodb.com/manual/reference/operator/aggregation/facet
General idea is to perform aggregation instead of multiple calls (1 for getting needed info + 1 to get the total count of documents)
You can perform 2 separate calls of course but it will hit your performance (not much for small data volumes but still...)
So you can get all needed data with .find() and then get count like this:
https://mongoosejs.com/docs/api.html#model_Model.count
PS. btw, use async/await instead of callbacks to avoid callback hell

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?

MongoDB: aggregate error when use Match and Group

I've model
var LogSchema = mongoose.Schema({
userId: String,
pageId: String,
tagId: String
}, {
timestamps: true
});
In code,
Log.aggregate([
{
$match: {
createdAt: {
$gte: new Date(strFrom),
$lte: new Date(strTo),
}
},
//$group: { _id: "$userId" },
}
], function (err, logs) {
if (err) {
res.status(500).send({ message: "error retrieving logs." });
} else {
res.send(logs);
}
});
When I execute code use $match, that's ok. Then, I add $group, I receive error
Error: Arguments must be aggregate pipeline operators
So, I remove $match, only use $grooup, code run ok. So, when I use both $match and $group, receive errors.
Please, give me ideas
Thank so much
You have a missing brace.
Log.aggregate([{
$match: {
createdAt: {
$gte: new Date(strFrom),
$lte: new Date(strTo),
}
},
{ <-- missing brace around your group
$group: { _id: "$userId" },
}],
function (err, logs) {
if (err) {
res.status(500).send({ message: "error retrieving logs." });
} else {
res.send(logs);
}
});

MongoError when trying to aggregate with collection relationship

I am new to mongodb, using Mean stack from meanjs.org.
I have a model with a user collection relationship:
var MealSchema = new Schema({
mealDate: {
type: Date,
default: Date.now
},
user: {
type: Schema.ObjectId,
ref: 'User'
}
});
mongoose.model('Meal', MealSchema);
I had find an implementation that works:
var user = req.user;
Meal.find({
mealDate: {
$gte: minus8days.toDate(),
$lt: tomorrow.toDate()
},
user : user
}).exec(function (err, meals) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(meals);
}
});
But I need some grouping and aggregation so I am trying to change this code to use aggregate instead of a find, I've tried many combinations and I keep getting an error, basically the following implementation throws an error MongoError: Maximum call stack size exceeded
Meal.aggregate([
{
$match: {
$and: [
{
mealDate: {
$gte: minus8days.toDate(),
$lt: tomorrow.toDate()
}
}, { user: user }
]
}
}]...
Why user:user works with find and not with aggregate?
How can the aggregate approach be fixed?
Try this :
Meal.aggregate([
{
$match: {
mealDate: { $gte: minus8days.toDate(), $lt: tomorrow.toDate()},
user: user._id
}
}
], function(err, docs){
if(err) console.log("Error : " + JSON.stringify(err));
if(docs) console.log("Got the docs successfully : " + JSON.stringify(docs));
});