Mongoose aggreagate with group not working - node.js

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!

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"
}
}
}
])

I wanna fetch all users who have most sparks in last 7 days in mongodb and nodejs

I wanna fetch all users who has most sparks in last 7 days
Sparks are something like friends
can anyone help in this problem
Thanks in advance
//create schema for users
const UserSchema = new Schema({
password: {
type: String,
default: "",
},
account_type: {
type: String,
default: "",
},
account_name: {
type: String,
default: "",
},
firstName: {
type: String,
default: "",
},
lastName: {
type: String,
default: "",
},
image: {
type: String,
default: "",
},
sparks: [
{
user: {
type: Schema.Types.ObjectId,
ref: "users",
},
status: {
type: Number,
required: true,
enum: STATUS,
},
createdAt: {
type: Date,
default: Date.now,
},
},
],
createdAt: {
type: Date,
default: Date.now,
},
});
This is my schema
router.get("/leaderboard/get/sparks", async (req, res) => {
let newDate = moment().subtract(7, "day");
console.log(
Date("2020-06-26T12:49:29.324Z") < Date("2020-06-24T12:49:29.563+00:00")
);
let topUsers = await User.aggregate([
{
$match: {
"sparks.createdAt": { $eq: new Date("2020-06-24T12:49:29.563+00:00") },
},
},
]);
res.status(200).json({ date: topUsers });
});
This is what I tried but did not work for me
I tried so many ways but faild
I was searching but not found anthing useful for my problem
You can do the following aggreagtion
User.aggregate([
{
'$project': {
'_id': 1,
'numberOfSparks': {
'$cond': {
'if': {
'$isArray': '$sparks'
},
'then': {
'$size': '$sparks'
},
'else': 'NA'
}
}
}
}, {
'$sort': {
'numberOfSparks': -1 //sort in descending order
}
}, {
'$limit': 10 // get top 10 users
}
])
Now this will work fine but if this aggregation is used frequently I would advice you a different approach. Aggregation is quite compute intensive it has to check all the users, then generate all the computed field, numberOfSparks, then sort. So as the number of users and sparks grow it could slow your server down.
Instead you could make a sparkCount field in your user schema. Then every time a spark is pushed to your User, you could increment a sparkCount field. Simply index the sparkCount field. Now you can easily query users with top sparkCount with a single query;
User.find({}).sort({sparkCount:-1}).limit(10)
This query will only read the 10 user documents with top sparkCount. No additional computation. And its also very scalable. Number of users and sparks will not effect performance.
Hope it helped

get count of conditionally matched elements from an array in MongoDB

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

updateMany and elemMatch in with nested schemas in mongoose (Node.js)

I'm trying to query a MongoDB database via mongoose to updateMany the fields of my database. I suppose that the first request is correct because mongoose doesn't fire any error, but for the nested schemas, I'm getting the following error.
My goal is to delete the occurences of the userTag in friends and remove the friendRequestsSent when userTarget equals userTag, friendRequestsReceived when userRequest equals userTag and notification when data equals userTag.
Here are the schemas of my Model
const NotificationSchema = new Schema({
title: String,
type: Number,
icon: String,
data: String,
createdAt: { type: Date, default: Date.now },
})
const FriendRequestSchema = new Schema({
userRequest: { type: String, required: true },
userTarget: { type: String, required: true },
createdAt: { type: Date, default: Date.now },
})
const UserSchema = new Schema({
tag: { type: String, required: true, unique: true },
friendRequestsSent: { type: [FriendRequestSchema] },
friendRequestsReceived: { type: [FriendRequestSchema] },
friends: { type: [String] },
notifications: { type: [NotificationSchema] },
})
The request
const updateResponse = await User.updateMany(
{
friends: { $elemMatch: { $eq: userTag } },
friendRequestsSent: {
userTarget: {
$elemMatch: { $eq: userTag },
},
},
friendRequestsReceived: {
userRequest: {
$elemMatch: { $eq: userTag },
},
},
notifications: {
data: {
$elemMatch: { $eq: userTag },
},
},
},
{
$pull: {
friends: userTag,
friendRequestsSent: { userTarget: userTag },
friendRequestsReceived: { userRequest: userTag },
notifications: { data: userTag },
},
}
)
The error
Error while deleting the user account: Cast to String failed for value "{ '$elemMatch': { '$eq': '0eQzaAwpt' } }" at path "userRequest" for model "User"
The userRequest field in friendRequestsReceived is type String, not array so $elemMatch will not work. Also, you don't need to use $elemMatch because you specify only a single condition in the $elemMatch expression as it says in the docs:
If you specify only a single condition in the $elemMatch expression, you do not need to use $elemMatch.
In your case, you just need to do something like (details here):
await User.updateMany({
friends: userTag,
"friendRequestsSent.userTarget" : userTag,
"friendRequestsReceived.userRequest": userTag,
"notifications.data": userTag
}...

Mongo DB / Mongoose Conditional Inclusion Search

I have a simple feature that may involve a complicated query. I want to allow users to only be shown to members they've already given approval to find them in my site.
The trouble is I have no idea how to add a conditional and then query within the queried users.
A user calls find.
If the searched users have "Approval Only" selected.
Check to see if the searching user is in their approved list.
I am using NodeJS with Mongoose for my finds.
Please help.
Schema below:
const ProfileSchema = new mongoose.Schema({
userDOBs: {
type: [Date],
required: true
},
gender: {
type: String,
required: true
},
active: {
type: Boolean,
default: true
},
discoverySettings: {
approvedOnly: {
type: Boolean,
default: false
}
},
blockedProfileIDs: {
type: [mongoose.Schema.Types.ObjectId],
ref: "Profile"
},
approvedIDs: [
{
type: [mongoose.Schema.Types.ObjectId],
ref: "Profile"
}
]
});
Query Below:
const profiles = await Profile.find({
$and: [
{
_id: {
$ne: req.user.profileID,
$nin: doNotDisplayList
},
"loc.loc": {
$nearSphere: [long, lat],
$maxDistance: maxDistance
},
userDOBs: {
$lt: moment().subtract(filter.searchParams.ageRange[0], "years"),
$gt: moment().subtract(filter.searchParams.ageRange[1], "years")
},
active: true,
"discoverySettings.visible": true,
blockedProfiles: {
$ne: req.user.profileID
}
}
]
})
.sort({
updatedAt: -1
})
.limit(limit)
.skip(skip);

Resources