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

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

Related

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.

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
}...

Implementing joins on multiple collections

I am trying to create a social networking application which can have connect (followers, following), posts, comments, likes, shares, etc. This is a MVP project, but still i wanted to explore mongoDB for this use case. I am having some doubt regarding the performance of this application.
I have three collection:
Posts: This is where a new post shall be added. This collection contains all the details related to a post.
Schema:
const postSchema = new mongoose.Schema({
user_id: String,
title: String,
text: String,
type: { type: String, enum: ["music", "movie", "tv"] },
mediaUrl: String,
thumbnailUrl: String,
accessType: { type: Number, enum: [1, 2, 3] },
tags: [String],
like_count: Number,
comment_count: Number,
share_count: Number,
insertDate: {
type: Date,
default: () => {
return new Date();
}
}
});
Feeds: This collection just add a metadata of user and post including tags. This i intend to use to get the relevant feed for a particular user.
Schema:
const feedSchema = new mongoose.Schema({
post_id: String,
user_id: String,
isTag: Boolean,
isPublic: Boolean,
insertDate: {
type: Date,
default: () => {
return new Date();
}
},
modifiedDate: {
type: Date,
default: () => {
return new Date();
}
}
});
Connects: This collection is for the relationship of users.
Schema:
const connectSchema = new mongoose.Schema({
followed_by: String,
user_id: String,
insertDate: {
type: Date,
default: () => {
return new Date();
}
}
});
My approach was to first find the posts from feeds collection basis users whom I am following, then fetching the posts from post collection.
Here goes my attempted query:
db.connects.aggregate([
{ $match: { followed_by: "5cbefd61d3b53a4aaa9a2b16" } },
{
$lookup: {
from: "feeds",
let: { user_id: "$user_id" },
pipeline: [{ $match: { $expr: { $or: [{ $eq: ["$user_id", "$$user_id"] }, { isPublic: true }] } } }],
as: "feedData"
}
},
{ $unwind: "$feedData" },
{ $replaceRoot: { newRoot: "$feedData" } },
{ $group: { _id: { post_id: { $toObjectId: "$post_id" }, modifiedDate: { $toLong: "$modifiedDate" } } } },
{ $replaceRoot: { newRoot: "$_id" } },
{ $sort: { modifiedDate: -1 } },
{ $skip: 0 },
{ $limit: 10 },
{
$lookup: { from: "posts", localField: "post_id", foreignField: "_id", as: "postData" }
},
{ $unwind: "$postData" },
{ $replaceRoot: { newRoot: "$postData" } },
{ $addFields: { userId: { $toObjectId: "$user_id" } } },
{
$lookup: { from: "users", localField: "userId", foreignField: "_id", as: "userData" }
},
{
$project: {
post_id: "$_id",
user_id: 1,
title: 1,
text: 1,
typeaccessType: 1,
mediaUrl: 1,
thumbnailUrl: 1,
insertDate: { $toLong: "$insertDate" },
like_count: 1,
comment_count: 1,
share_count: 1,
user_email: { $arrayElemAt: ["$userData.email", 0] },
user_profile_pic: { $arrayElemAt: ["$userData.profile_pic", 0] },
username: { $arrayElemAt: ["$userData.firstname", 0] }
}
}
]).pretty();
Please share your feedback on:
Which index should I use to boost up the performance?
Is there is a better way of doing the same in mongoDB. Also if any part of the query can be optimised?

Unable to push object into array in Mongodb collection but Id is inserted

I am using comment array in my schema as fallows. I want to push comments data into that comment array using nodejs api
var Schema = mongoose.Schema;
var myfeeds = new Schema({
title: {
type: String,
required: true
},
feed: {
type: String,
required: true
},
createdBy: {
type: String,
required: true,
unique: true
},
createdDate: {
type: Date,
required: true,
default: Date.now()
},
comment: [
{
commentBy: {
type: String
},
commentText: {
type: String
},
createdDate: {
type: Date
}
}
],
likes: [
{
likesCount: {
type: Number,
required: false
},
likeBy: {
type: String,
required: false
}
}
]
});
I want to push object to this comment array. so, for that I did in this way please tell me if anything wrong in this
let _id = req.body.id;
let commentBy = req.body.commentedBy;
let commentedText = req.body.commentedText;
let commentedDate = req.body.commentedDate;
let data = {
commentBy: commentBy,
commentText: commentedText,
createdDate: commentedDate
};
MyFeeds.findByIdAndUpdate(
{ _id: _id },
{
$push: {
comment: data
}
}
)
.then((result) => {
res.status(200).json({
status: result
});
})
.catch((err) => {
res.status(500).json({
status: 'invalid',
err: err
});
});
but only id are inserted into that comment array but not the required content
"comment": [
{
"_id": "5badfd092b73fa14f4f0aa7c"
},
{
"_id": "5badfd102b73fa14f4f0aa7d"
},
{
"_id": "5badfd142b73fa14f4f0aa7e"
},
{
"_id": "5badfd31500fb11bb06b4c8a"
},
{
"_id": "5badfd35500fb11bb06b4c8b"
},
{
"_id": "5badff3d439a151190d62961"
}
],

mongoose/mongodb subdocument deep query and update

Schema
var chapterSchema = new Schema({_id: Number, completed: Boolean})
var bookSchema = new Schema({ date: { type: Date, default: Date.now }, author: String, chapter: chapterSchema })
var personSchema = new Schema({ _id: Number, name: String, books: [bookSchema] })
Sample object (person object)
{
_id: 1,
name: Bond,
books: [{
date: ISODate("2017-10-24T19:01:18.362Z”),
author: Martin,
chapter: {}
}]
}
Subdocument (chapter object)
var chapter = new chapterSchema({_id: 1, completed: true})
Requirement
I would want to add the chapter object into the sample object (person.books)
Expected
{
_id: 1,
name: Bond,
books: [{
date: ISODate("2017-10-24T19:01:18.362Z”),
author: Martin,
chapter: {
_id: 1,
completed: true
}
}]
Tried
let todayStart = new Date()
todayStart.setHours(0,0,0,0)
Patient.findOneAndUpdate({'_id': 1, ‘books.date': { $not: { $gt: todayStart } } }, {$set: {‘books.$.chapter’: chapter}}, {new: true, upsert: true}, (err, consultation) => {
if (err) {
console.error(‘[UPDATE] Failed to add chapter')
} else {
console.log(‘[UPDATE] chapter is added)
}
})
I got 'Failed to add chapter' error
So, I tried to findOne to get any of fields in books subdocument.
Patient.findOne({'_id': 1, ‘books.date': { $gt: todayStart } }, {“books.$.author”: 1}, (err, data) => {
if (err) console.log('[find] data not found')
console.log('[FIND]' + JSON.stringify(data)
})
It gave me the following result,
{_id: 1, books: [{ date: ISODate("2017-10-24T19:01:18.362Z”), author: Martin, chapter: {} }]}
But, I was expecting only author here.
Eventually, I would like to know how to insert an object into subdocument in Mongodb/Mongoose.
P.S: new Date() I got from express.js, which does not matter here though.
This can help you:
db.yourDb.update(
{
"_id": 1,
"books": {
"$elemMatch": {
"date": {
"$lte": ISODate("2017-10-24T20:01:18.362+0000")
}
}
}
},
{
"$set": {
"books.$.chapter": { "_id": 1, "completed": true }
}
}
)
Remember though that if more than one book matches the query, the first one is updated.

Resources