findOneAndUpdate doesn't update object's field (array of objects) - node.js

I'm trying to update multiple fields of a object in an array but it doesn't work.
what am i doing wrong?
Data Sample:
{
_id: 'mongodbid',
name: 'something',
employees: [
{
age: 25,
name: 'name',
salary: 500
},
{
age: 28,
name: 'name2',
salary: 700
}
],
}
Query:
await this.somethingModel
.findOneAndUpdate(
{
_id: id,
'employees.age': 25,
},
{
$set: {
'employees.$.salary': 600,
'employees.$.name': 'name4',
}
},
)
.exec();

I've added a arrayFilter in findOneAndUpdate. Try with this one, I hope, it works.
await this.somethingModel.findOneAndUpdate(
{
_id: id,
},
{
$set: {
"employees.$[elem].salary": 600,
"employees.$[elem].name": "name4"
}
},
{
$arrayFilters: [
{
"elem.age": 25,
},
],
});

Turns out problem wasn't with the query itself, rather the schema.
I forgot to add [ ] to the employees field in the schema.
I was using 'raw' from '#nestjs/mongoose' package, I mistakenly wrote this:
#Prop(raw({ age: Number, salary: Number, name: String }))
employees: { age: number; salary: number; name: string}[]
Instead of this:
#Prop(raw([{ age: Number, salary: Number, name: String }]))
employees: { age: number; salary: number; name: string}[]

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

Remove all array element in mongodb which has id match in array

I have an array of the id which I get from post request in nodejs. I want to remove all the elements in the orders array which match receive id array.
eg remove_id = ['60f1ab20891ced4818b5ea88','60f1ab20891ced4818b5ea87']
so I want to remove all elements from orders array whoose id match match remove_id array.
orderdetail = {
_id: 60f1ab20891ced4818b5ea86,
totalPrice: 400,
orders: [
{
_id: 60f1ab20891ced4818b5ea87,
quantity: 1,
price: 200,
name: 'Caramel Latte',
category: 'Steaming Cups'
},
{
_id: 60f1ab20891ced4818b5ea88,
quantity: 1,
price: 200,
name: 'Cinnamon Latte',
category: 'Steaming Cups'
},
{
_id: 60f1ab20891ced4818b5ea89,
quantity: 1,
price: 200,
name: 'Cinnamon Latte',
category: 'Steaming Cups'
}
],
timestamp: 1626450720332,
name: 'xxx',
email: 'xxx',
}
what I have tried but not working,
Orderdetail.updateOne( { _id: '60f1ab20891ced4818b5ea86' }, {
$pull: { order: { _id: { $in: remove_id } } } } )
Rename object name order to orders !
Try this code !
let remove_id = ['60f1ab20891ced4818b5ea87','60f1ab20891ced4818b5ea88']
db.getCollection('Orderdetail').updateOne({ _id: '60f1ab20891ced4818b5ea86' }, {
$pull: {
orders: { // rename this to orders
_id: { $in: remove_id }
}
}
})

find array of objects with only matched object in subarray mongoose?

I'm trying to get an array of objects with the matching id's and with only sub-array product_price object with matching attributes size and model?
product_name: {
type: String,
required: true,
},
service_hourly_price: {
type: Number,
required: true
},
product_price: [{
model:{
type: String,
enum:['Euro','Japanese']
},
size:{
type: String,
enum: ['S','M','L','XL']
},
price:{
type: Number,
required: true,
}
}],
Trying to query like this:
ProductSchema.aggregate( [
{$match: { _id: {
$in: _id.map(function(_id){ return new mongoose.Types.ObjectId(_id) })
}}},
{ $match : { product_price : {model : 'Euro' , size: 'S'}}}
])
how can I achieve result like this:
products:{
_id: new ObjectId("61b3ab3ceba5bc724d754929"),
product_name: 'Basic Service',
service_hourly_price: 25,
product_price: [
{
_id: new ObjectId("61b3ab3ceba5bc724d75492a"),
size: 'S',
model: 'Euro',
price: 100
}
]
},
{
_id: new ObjectId("61b3aa88eba5bc724d7548fb"),
product_name: 'Horn',
service_hourly_price: 5,
product_price: [
{
_id: new ObjectId("61b3aa88eba5bc724d7548fc"),
size: 'S',
model: 'Euro',
price: 110
}
]
}
product_price must contain only one matching object in it.
I am uncertain why your product_prize is an array with only one object, you could remove the array there, and thus remove the $unwind in the aggregation, but nontheless this works for you right now:
ProductSchema.aggregate( [
{$match: {
_id: {
$in: _id.map(function(_id){ return new mongoose.Types.ObjectId(_id) })
}}}, {
'$unwind': {
'path': '$product_price'
}
}, {
'$match': {
'product_price.model': 'Euro',
'product_price.size': 'S'
}
}
])
Here try it out
Here the proof with the mongo Compass:

Iterate Through mongodb using mongoose to find nested-like data

Let's say i have 5 documents in members collection as follow:
[
{
_id: ObjectId("60e6afadfb6fe6510155e9b2"),
name: "James",
parentId: ObjectId("60e6afadfb6fe6510155e9b1")
},
{
_id: ObjectId("60e6afadfb6fe6510155e9b1"),
name: "Michael",
parentId: ObjectId("60e6afadfb6fe6510155e9b0")
},
{
_id: ObjectId("60e6afadfb6fe6510155e9b0"),
name: "Josh",
parentId: ObjectId("60e6afadfb6fe6510155e9af")
},
{
_id: ObjectId("60e6afadfb6fe6510155e9af"),
name: "Robert",
parentId: ObjectId("60e6afadfb6fe6510155e9ad")
},
{
_id: ObjectId("60e6afadfb6fe6510155e9ad"),
name: "William",
parentId: null
},
]
All I want is to iterate through those documents to get the full name of James
My output should look like 'James Michael Josh Robert William' consider that the parentId in the child document is the _id of the parent document
You can use the concept of the linked list to traverse through an array. To my knowledge, it is not possible on the DB level, and since you have mentioned the nodejs tag. So here is a nodejs solution
const {Types: {ObjectId}} = require("mongoose")
let data = [
{
_id: ObjectId("60e6afadfb6fe6510155e9b2"),
name: "James",
parentId: ObjectId("60e6afadfb6fe6510155e9b1")
},
{
_id: ObjectId("60e6afadfb6fe6510155e9b1"),
name: "Michael",
parentId: ObjectId("60e6afadfb6fe6510155e9b0")
},
{
_id: ObjectId("60e6afadfb6fe6510155e9b0"),
name: "Josh",
parentId: ObjectId("60e6afadfb6fe6510155e9af")
},
{
_id: ObjectId("60e6afadfb6fe6510155e9af"),
name: "Robert",
parentId: ObjectId("60e6afadfb6fe6510155e9ad")
},
{
_id: ObjectId("60e6afadfb6fe6510155e9ad"),
name: "William",
parentId: null
},
]
let names = []
let current = data[0]
// handle null / undefined current value
while(current) {
names.push(current.name)
current = data.find(v=>v._id.equals(current.parentId))
}
console.log(names.join(' ')) // James Michael Josh Robert William

Get average rate for product comments in MongoDB and Express

I want to calculate average rating for products based on comments rate,
exactly I want to return products object with one more property in response when client call get all product api
my product schema is like below, thank you in advance.
const mongoose = require("mongoose");
const { s, rs, n, rn, ref, rref } = require("../utils/mongo");
let commentSchema = new mongoose.Schema(
{
user: rref("user"),
body: rs,
rate: {
...n,
default: 0,
enum: [0, 1, 2, 3, 4, 5],
},
},
{ timestamps: true }
);
let schema = new mongoose.Schema(
{
user: rref("user"),
name: rs,
description: s,
images: [s],
price: rn,
discount: n,
sizes: [sizeSchema],
categories: [ref("category")],
tags: [s],
comments: [commentSchema],
rating: n,
inStock: {
type: Boolean,
default: true,
},
stock: n,
sold: {
...n,
default: 0,
},
view: {
...n,
default: 0,
},
},
{ timestamps: true }
);
module.exports = mongoose.model("product", schema);
I want to return in response something like this:
let result = [
{
name: 'product name',
comments: [
{
user: "2a2sdw22das5262123223",
body: "good product",
rate: 4
},
{
user: "2a2sdw22das5262123223",
body: "good product",
rate: 2
}
],
avgRate: 2, // how to calculate this?
price: 20
...
}
]
You can use aggregation pipeline to do that, it will add the avgRating field avgRate which you can use for future ref:
db.collectionName.aggregate([{ $addFields : { avgRate: { $avg: "$comments.rate"} } }])

Resources