get count of conditionally matched elements from an array in MongoDB - node.js

I want comments with today's date and it should be non-empty and how much comments it has via using mongoose. I have tried a lot. Currently, I am trying to achieve with two methods. both have some problems let me explain. please consider I have only two posts in DB one has no comments like: [], and the other has 2 comments two inside it with today date and the 3 is old.
Method 1 :
in this method, it returns me today comment but it only returns single comment added on today.
and also returning me another object which has no comments
Post.find({ })
.select({
comments: { $elemMatch: { date: { $gt: startOfToday } } },
title: 1,
})
.exec((err, doc) => {
if (err) return res.status(400).send(err);
res.send(doc);
});
the output of above code is :
[{"_id":"5e9c67f0dd8479634ca255b1","title":"sdasd","comments":[]},{"_id":"5e9d90b4a7008d7bf0c4c96a","title":"sdsd","comments":[{"date":"2020-04-21T04:04:11.058Z","votes":
[{"user":"hhhh","vote":1}],"_id":"5e9e70bbece9c31b33f55041","author":"hhhh","body":"xvxgdggd"}]}]
Method 2 :
In this method I am using the same thing above inside the found object like this:
Post.find({ comments: { $elemMatch: { date: { $gt: startOfToday } } } })
.exec((err, doc) => {
if (err) return res.status(400).send(err);
res.send(doc);
});
And it returns me first post with all comments (3 comments) but not second post(that is good) that have empty comment array.
here is the output :
[{"author":{"id":"5e85b42f5e4cb472beedbebb","nickname":"hhhh"},"hidden":false,"_id":"5e9d90b4a7008d7bf0c4c96a","title":"sdsd","body":"dsfdsfdsf","votes":[{"user":"5e85b42f5e4cb472beedbebb","vote":1}],"comments":[{"date":"2020-04-20T12:08:32.585Z","votes":[],"_id":"5e9d90c0a7008d7bf0c4c96b","author":"hhhh","body":"zcxzczxc z zxc"},
{"date":"2020-04-21T04:04:11.058Z","votes":[{"user":"hhhh","vote":1}],"_id":"5e9e70bbece9c31b33f55041","author":"hhhh","body":"xvxgdggd"},
{"date":"2020-04-21T04:56:25.992Z","votes":[],"_id":"5e9e7cf96095882e11dc510c","author":"hhhh","body":"new should appear in feeds"}],"date":"2020-04-20T12:08:20.687Z","createdAt":"2020-04-20T12:08:20.692Z","updatedAt":"2020-04-21T04:56:26.003Z","__v":3}]
This is my post schema :
const postSchema = new Schema(
{
title: {
type: String,
required: true,
unique: 1,
index: true,
},
author: {
id: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
nickname: String,
},
body: {
type: String,
required: true,
},
comments: [
{
author: {
type: String,
required: true,
},
body: {
type: String,
required: true,
},
date: { type: Date, default: Date.now },
votes: [{ user: String, vote: Number, _id: false }],
},
],
date: { type: Date, default: Date.now },
hidden: {
type: Boolean,
default: false,
},
votes: [{ user: Schema.Types.ObjectId, vote: Number, _id: false }],
},
{ timestamps: true }
);
So, if I have SUM up the things I need today comments and today is 21st April (Two comments) and another comment date is 20. I only need today's comments with its count.
If I forgot something to add please let me know. Thanks

There are couple of changes as $elemMatch would return only the first matching element from array but not all the matching elements in comments array. So it's not useful here, additionally if you want comments for today you need to use $gte instead of $gt for input startOfToday. Finally, You need to use aggregation-pipeline to do this :
db.collection.aggregate([
/** Lessen the no.of docs for further stages by filter with condition */
{
$match: { "comments.date": { $gte: ISODate("2020-04-21T00:00:00.000Z") } }
},
/** Re-create `comments` array by using filter operator with condition to retain only matched elements */
{
$addFields: {
comments: {
$filter: {
input: "$comments",
cond: { $gte: ["$$this.date", ISODate("2020-04-21T00:00:00.000Z")] }
}
}
}
},
{
$addFields: { count: { $size: "$comments" } } // Add count field which is size of newly created `comments` array(Which has only matched elements)
}
]);
Test : mongoplayground

Related

Mongoose: find and aggregate together in db.collection()

I have a collection in mongodb, called recipe, where I have a document, called comments, which is an array, and in each recipe is saving the comments. Inside the comments array I have a ratings, which type is Number. So I want to calculate the average the ratings, but don't know, how can I use the db.collection().aggregate code to work in the recipe collection, in the comments document with the ratings variable.
Here is the recipe collection in mongodb:
const { Double } = require('bson');
const { timeStamp } = require('console');
const mongoose = require('mongoose');
const recipeSchema = new mongoose.Schema({
name: {
type: String,
required: 'This field is required.'
},
description: {
type: String,
required: 'This field is required.'
},
quantity: {
type: Array,
required: 'This field is required.'
},
ingredients: {
type: Array,
required: 'This field is required.'
},
categoryByServing: {
type: String,
enum: ['Reggeli', 'Ebéd', 'Vacsora', 'Desszert', 'Levesek', 'Egyéb'],
required: 'This field is required.'
},
categoryByNationality: {
type: String,
enum: ['Thai', 'Kínai', 'Indiai', 'Olasz', 'Angol', 'Magyar', 'Egyéb'],
required: 'This field is required.'
},
image: {
type: Array,
required: 'This field is required.'
},
comments: [
{
username: String,
comment: String,
date: {
type: Date,
default: Date.now
},
rating: Number
},{
timestamps: true
}
],
count: {
type: Number
},
likes: {
type: Number
},
recipe_id: {
type: String
}
});
recipeSchema.index({ name: 'text', description: 'text' });
const Recipe = module.exports = mongoose.model('Recipe', recipeSchema);
Here is the code, where I implemented the rating avg calculation, which is inside the commenting post method:
/**
* POST /comment-recipe
* Comment Recipe
*/
module.exports.CommentRecipeOnPost = async(req, res) => {
let recipeId = req.params.id
const comment = new Comment({
username: req.body.username,
comment: req.body.comment,
date: req.body.date,
rating: req.body.rating
});
comment.save((err, result) => {
if (err){
console.log(err)
}else {
Recipe.findById(req.params.id, (err, post) =>{
if(err){
console.log(err);
}else{
post.comments.push(result);
post.save();
db.collection('recipes').aggregate([
{
$unwind: "$comments"
},
{
$group: {
_id: null,
avgrating: {
$avg: "$rating"
}
}
}
]).toArray()
.then(results => {
console.log({ rating: results[0].avgrating })
})
.catch(error => console.error(error))
console.log('====comments=====')
console.log(post.comments);
res.redirect('/recipe/' + recipeId);
}
})
}
})
}
UPDATE
There is a simpler way pointed out by chridam in the comments using only a $project which I didn't figure out at first demo
db.collection.aggregate([
{
$project: {
_id: 0,
name: 1,
avgRating: {
$avg: "$comments.rating"
}
}
}
])
..or using $addFields which will give the average of ratings as a new field avgRating for each record demo . you can use a project step after if need to get only certain fields
db.collection.aggregate([
{
$addFields: {
avgRating: {
$avg: "$comments.rating"
}
}
}
])
You have done the $unwind step correctly and now you will get a record for each comment.
{
"_id": "1",
"comments": {
"comment": "commment1-1",
"rating": 4
},
"name": "recipe 1"
},
{
"_id": "1",
"comments": {
"comment": "comment1-2",
"rating": 3
},
"name": "recipe 1"
},
...
In the $group stage group by something unique like the _id or the name and the $avg should be of $comments.rating instead of $rating.
In the end the pipeline should look something like this. demo
db.collection.aggregate([
{
$unwind: "$comments"
},
{
$group: {
_id: "$name", //group by something unique for that document containing comments
avgRating: {
$avg: "$comments.rating"
}
}
}
])

How to query for sub-document in an array with Mongoose

I have a Schema of Project that looks like this:
const ProjectSchema = new mongoose.Schema({
name: {
type: String,
Required: true,
trim: true
},
description: {
type: String,
},
devices: [{
name: {type: String, Required: true},
number: {type: String, trim: true},
deck: {type: String},
room: {type: String},
frame: {type: String}
}],
cables: {
type: Array
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
adminsID: {
type: Array
},
createdAt: {
type: Date,
default: Date.now
}
I want to query an object from array of "devices".
I was able to add, delete and display all sub-documents from this array but I found it really difficult to get single object that matches _id criteria in the array.
The closest I got is this (I'm requesting: '/:id/:deviceID/edit' where ":id" is Project ObjectId.
let device = await Project.find("devices._id": req.params.deviceID).lean()
console.log(device)
which provides me with below info:
[
{
_id: 6009cfb3728ec23034187d3b,
cables: [],
adminsID: [],
name: 'Test project',
description: 'Test project description',
user: 5fff69af08fc5e47a0ce7944,
devices: [ [Object], [Object] ],
createdAt: 2021-01-21T19:02:11.352Z,
__v: 0
}
]
I know this might be really trivial problem, but I have tested for different solutions and nothing seemed to work with me. Thanks for understanding
This is how you can filter only single object from the devices array:
Project.find({"devices._id":req.params.deviceID },{ name:1, devices: { $elemMatch:{ _id:req.params.deviceID } }})
You can use $elemMatch into projection or query stage into find, whatever you want it works:
db.collection.find({
"id": 1,
"devices": { "$elemMatch": { "id": 1 } }
},{
"devices.$": 1
})
or
db.collection.find({
"id": 1
},
{
"devices": { "$elemMatch": { "id": 1 } }
})
Examples here and here
Using mongoose is the same query.
yourModel.findOne({
"id": req.params.id
},
{
"devices": { "$elemMatch": { "id": req.params.deviceID } }
}).then(result => {
console.log("result = ",result.name)
}).catch(e => {
// error
})
You'll need to use aggregate if you wish to get the device alone. This will return an array
Project.aggregate([
{ "$unwind": "$devices" },
{ "$match": { "devices._id": req.params.deviceID } },
{
"$project": {
name: "$devices.name",
// Other fields
}
}
])
You either await this or use .then() at the end.
Or you could use findOne() which will give you the Project + devices with only a single element
Or find, which will give you an array of object with the _id of the project and a single element in devices
Project.findOne({"devices._id": req.params.deviceID}, 'devices.$'})
.then(project => {
console.log(project.devices[0])
})
For now I worked it around with:
let project = await Project.findById(req.params.id).lean()
let device = project.devices.find( _id => req.params.deviceID)
It provides me with what I wanted but I as you can see I request whole project. Hopefuly it won't give me any long lasting troubles in the future.

Mongoose aggreagate with group not working

In my Node.JS API, it is possible to order and get menus. The structure of an ordered menu looks like the following (the main schema is the orderMenuSchema; menuItemSchema is for the subdocument-array with ordered items):
var menuItemSchema = mongoose.Schema({
itemId: {
type: mongoose.Schema.Types.ObjectId,
required: true
},
prepared: {
type: Boolean,
default: false
},
finished: {
type: Boolean,
default: false
},
timestamp: {
type: Date,
default: Date()
}
}, {_id: false})
var orderMenuSchema = mongoose.Schema({
orderId: {
type: mongoose.Schema.Types.ObjectId,
required: true
},
menuId: {
type: mongoose.Schema.Types.ObjectId,
required: true
},
items: {
type: [menuItemSchema],
required: true,
validate: menuItemsCheck
},
finished: {
type: Boolean,
default: false
},
timestamp: {
type: Date,
default: Date()
}
})
Example Data:
{
"_id":"5d2333a1841a0e4ef05873d0",
"finished":false,
"timestamp":"2019-07-08T12:14:04.000Z",
"menuId":"5d189ffdbe02ef0b00b22370",
"items":[
{
"prepared":false,
"finished":false,
"timestamp":"2019-07-08T12:14:04.000Z",
"itemId":"5d189ffdbe02ef0b00b2236d"
},
{
"prepared":false,
"finished":false,
"timestamp":"2019-07-08T12:14:04.000Z",
"itemId":"5d189ffdbe02ef0b00b2236e"
},
{
"prepared":false,
"finished":false,
"timestamp":"2019-07-08T12:14:04.000Z",
"itemId":"5d189ffdbe02ef0b00b2236f"
}
],
"orderId":"5d2333a1841a0e4ef05873c3",
"__v":0
}
Whether an item is prepared or not is stored in the prepared field of the menuItem.
Each menu has multiple items to choose from, and the user is able to have only some items - that's why the orderMenuSchema has an array of subdocuments called "items" in which only the ordered items are stored.
Now I would like to get all unprepared menus, group them by the menuID
and then group them by the itemID - everything with a Mongoose
aggregation.
So, I think I need two groupings: The first one by the menuId, the second one by the itemId.
Furthermore, I would like to know how many of each item are unprepared - so after grouping by the menuId, I need to get a count of all unprepared items
Expected Output:
I thought of something like this:
{
"result":[
{
"menuID":"tastefulMenu123",
"items":[
{
"itemId":"noodlesoop123",
"unpreparedCount":13
},
{
"itemId":"tastyBurger123",
"unpreparedCount":2
},
{
"itemId":"icecoldIce123",
"unpreparedCount":20
}
]
}
]
}
There will be an array of subdocuments, one subdocument for each menuId. Each subdocument than has an array of items in which the itemID as well as the unpreparedCount are stored.
What I already tried (not working):
OrderMenu.aggregate([
{$unwind: "$items"},
{ $project: { prepared: 1, itemId: 1} },
{ $match: {
prepared: false,
timestamp: {
$gte: today,
$lt: tomorrow
}
}},
{ $group: {
_id: {menuId: '$menuId', itemId: '$itemId'},
count: { $sum: 1 }
}}
]).then(result => {
console.log(result)
return Promise.resolve(result)
}).catch(error => {
console.log(error)
return Promise.reject(500)
})
Any help would be appreciated!

Mongoose aggregation with $sum returning 0?

I know several question have been asked in here but no one give me the correct answer. So basically I have Schema like here
const cashFlowSchema = new Schema({
date: { type: Date, default: Date.now },
type: { type: String, required: true },
category: { type: String, required: true },
amount: { type: Number, required: true },
description: String
});
And then I tried to create get sum for amount with aggregate like this
CashFlow.aggregate([{
$group: {
_id: null,
balance: { $sum: "$amount" }
}
}], function(err, result) {
if (err) {
console.log(err);
return;
}
console.log(result);
});
And give the result like this
[ { _id: null, balance: 0 } ]
But if I try in Robo3T (previously Robomongo), it give me correct answer. I use mongodb version 3.4. Thank you before.
Update
I found the answer, so it's my silly mistake. After I enable debug mode in mongoose, it show I'm make wrong connection to the collection. After fix it, I get the correct result. I will delete this question soon, thank's all before.

Remove array entries containing an empty array

I am trying to remove all products from the products array where the product has no rates. In my queries below I tried to check the length of the rates array, but
none of them seem to work. Any help is appreciated.
Thanks in advance
var ProductRateSchema = new Schema({
product: {
type: Schema.ObjectId,
ref: 'products'
},
user: {
type: Schema.ObjectId,
ref: 'User'
},
rates: [{
type: Schema.ObjectId,
ref: 'rates'
}]
});
var InventorySchema = new Schema({
name: {
type: String,
default: '',
required: 'Please enter in a name',
trim: true
},
created: {
type: Date,
default: Date.now
},
user: {
type: Schema.ObjectId,
ref: 'User'
},
products: [productRateSchema]
});
var inventoryId = req.body.inventoryId;
var productId = req.body.productId;
// none of these queries work
db.inventory.findOneAndUpdate({ '_id': inventoryId,
{$pull: {'products': { product: productId, 'rates': { $eq:[] }}}}, function (err, result) {
});
db.inventory.findOneAndUpdate({ '_id': inventoryId,
{$pull: {'products': { product: productId, 'rates': {$size: {$lt: 1}}}}}, function (err, result) {
});
db.inventory.findOneAndUpdate({ '_id': inventoryId,
{$pull: {'products': { product: productId, 'rates': null }}}, function (err, result) {
});
Don't know what you tried since it is simply not included in your question, but the best way to check for an empty array is to basically look where the 0 index does not match $exists:
Inventory.update(
{ "products.rates.0": { "$exists": false } },
{
"$pull": {
"products": { "rates.0": { "$exists": false } }
}
},
{ "multi": true },
function(err,numAffected) {
}
)
The "query" portion of the .update() statement is making sure that we only even attempt to touch documents which have an empty array in "products.rates". That isn't required, but it does avoid testing the following "update" statement condition on documents where that condition is not true for any array element, and thus makes things a bit faster.
The actual "update" portion applies $pull on the "products" array to remove any of those items where the "inner" "rates" is an empty array. So the "path" within the $pull is actually looking inside the "products" content anyway, so it is relative to that and not to the whole document.
Naturally $pull will remove all elements that match in a single operation. The "multi" is only needed when you really want to update more than one document with the statement

Resources