mongoose/mongodb subdocument deep query and update - node.js

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.

Related

I have problem updating a subdocument in an array of subdocuments in MongoDB

I have problems updating a subdocument in an array of subdocuments.
Here is my data structure in the users collection:
{
favorites: [
{
id: new ObjectId("639707f36bf9468265d91810"),
expiresAt: 1671361200000,
reminder: false
},
{
id: new ObjectId("637cc4c986b4fbec43579e1f"),
expiresAt: 1672603200000,
reminder: false
}
],
_id: new ObjectId("637e8af40e43f40373686da2"),
email: 'something#something.com',
forename: 'something',
surname: 'something',
role: 'user',
password: 'something',
__v: 0
}
My Schema is:
const userSchema = new Schema({
email: String,
forename: String,
surname: String,
role: String,
password: String,
favorites: {
id: { type: Schema.Types.ObjectId, ref: "Event" },
expiresAt: Number,
reminder: Boolean,
},
});
I want to update the reminder field in a subdocument based on the subdocument’s id.
I’ve tried following approaches:
1.
User.findOneAndUpdate(
{ _id: req.body.user, "favorites.id": { $eq: BSON.ObjectId(req.body.id) } },
{ $set: { "favorites.$.reminder": true } },
).setOptions({ sanitizeFilter: true });
Here nothing happens. It finds the document but does not update it.
2.
User.findOneAndUpdate(
{ _id: req.body.user },
{ $set: { "favorites.$[elem].reminder": true } },
{
arrayFilters: [{ "elem.id": { $eq: BSON.ObjectId(req.body.id) } }],
returnNewDocument: true,
}
).setOptions({ sanitizeFilter: true });
Here it returns an error: “Error: Could not find path “favorites.0.id” in schema”
I cannot find where is my mistake? Any help is much appreciated!
P.S.
Mongo version is 5.0.14
Try to use updateMany instead.
User.updateMany(
{
_id: userId,
"favorites.id": eventId
},
{
$set: {
"favorites.$.reminder": true
}
},
function(err, res) {
if (err) {
// Handle error
} else {
// Handle success
}
}
);
I think you can adapt the query to your calling method findOneAndUpdate. But it's enough to you.

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

Problem with the relationship in express/mongoose

Im creating a document named 'Sells', each sell has properties such as date, name, total, the id of the user who created it ect. and I have another document named 'Sell_details' where I store the details of each sell with their sell _id. So how do i retrieve all the sells i have in the database with an array of the details which that sell has? I've tried to do it but it doesn't retrieve all the details and among other problems. Here's what i have.
this is the sell model
const mongoose = require('mongoose');
const validator = require('validator');
const uniqueValidator = require('mongoose-unique-validator');
const ventaSchema = new mongoose.Schema({
usuarioId: {
type: mongoose.Schema.Types.ObjectId,
required: true
},
clienteId: {
type: mongoose.Schema.Types.ObjectId,
required: true
},
tipo_comprobante: {
type: String,
required: true
},
num_comprobante: {
type: Number,
required: true
},
serie_comprobante: {
type: Number,
required: true
},
fecha: {
type: Date,
default: Date.now()
},
impuesto: {
type: Number,
required: true
},
total: {
type: Number,
required: true
},
estado: {
type: String,
default: 'Activo'
},
owner: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'User'
},
ventas: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Detalle_Venta'
},
detalles: {
type: Array
}
},{ toJSON: { virtuals: true } });
ventaSchema.plugin(uniqueValidator, { message: 'Expected {PATH} to be unique' });
const Venta = mongoose.model('Venta', ventaSchema);
module.exports = Venta;
his is the sell router:
router.get('/ventas', auth, async(req, res) => {
try {
await req.user.populate({
path: 'ventas',
options: {
limit: req.query.limit
}
}).execPopulate();
req.user.ventas.forEach(element => {
const detalles = Detalle_Venta.find({ventaId: element._id});
detalles.then(el => {
el.forEach(detalle => {
if (element.detalles.length > 1) {
element.detalles.forEach(el => {
const isAdded = el._id === detalle._id;
if (isAdded) {
element.detalles.push(detalle);
}
})
} else {
element.detalles.push(detalle);
}
});
});
element.save();
});
res.send(req.user.ventas);
} catch (error) {
res.status(400).send(error);
}
});
and this is the output from postman:
[
{
"fecha": "2020-06-22T18:16:44.175Z",
"estado": "Activo",
"detalles": [
{
"_id": "5ef0fa4e49de0641c46eca0b",
"idArticulo": "5ee825bfb3da101df49822ae",
"cantidad": 30,
"precio": 2,
"descuento": 0,
"ventaId": "5ef0fa4e49de0641c46eca0a",
"__v": 0
},
{
"_id": "5ef0fa4e49de0641c46eca0b",
"idArticulo": "5ee825bfb3da101df49822ae",
"cantidad": 30,
"precio": 2,
"descuento": 0,
"ventaId": "5ef0fa4e49de0641c46eca0a",
"__v": 0
}
],
"_id": "5ef0fa4e49de0641c46eca0a",
"usuarioId": "5ee3b6b50376d7143c476834",
"clienteId": "5ee6b115e43839274cc50ddb",
"tipo_comprobante": "RNC",
"num_comprobante": 1000000,
"serie_comprobante": 20001,
"impuesto": 18,
"total": 10500,
"owner": "5ef0e64083f8c815cc67cd7c",
"__v": 2,
"id": "5ef0fa4e49de0641c46eca0a"
},
{
"fecha": "2020-06-22T18:16:44.175Z",
"estado": "Activo",
"detalles": [
{
"_id": "5ef0fa5a49de0641c46eca0d",
"idArticulo": "5ee825bfb3da101df49822ae",
"cantidad": 30,
"precio": 2,
"descuento": 0,
"ventaId": "5ef0fa5a49de0641c46eca0c",
"__v": 0
},
{
"_id": "5ef0fa5a49de0641c46eca0e",
"idArticulo": "5ee825bfb3da101df49822ae",
"cantidad": 303,
"precio": 2,
"descuento": 0,
"ventaId": "5ef0fa5a49de0641c46eca0c",
"__v": 0
}
],
"_id": "5ef0fa5a49de0641c46eca0c",
"usuarioId": "5ee3b6b50376d7143c476834",
"clienteId": "5ee6b115e43839274cc50ddb",
"tipo_comprobante": "RNC",
"num_comprobante": 1000000,
"serie_comprobante": 20001,
"impuesto": 18,
"total": 10500,
"owner": "5ef0e64083f8c815cc67cd7c",
"__v": 1,
"id": "5ef0fa5a49de0641c46eca0c"
}
]
Before you say i did it, the first array of the 'detalles' array (which stores the sell details of that sell), i added one sell detail and i'm getting two, and the second array of the other sell, i'm getting two sell details but i added 3 and if i add 4 i'm only getting 2 and i don't know why. is there a way to fix it and another best way to do it ?
mongoose has a powerful aggregation operator called populate which you have used.
You should use type ObjectId to the property sell details.
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const personSchema = Schema({
_id: Schema.Types.ObjectId,
name: String,
age: Number,
stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});
const storySchema = Schema({
author: { type: Schema.Types.ObjectId, ref: 'Person' },
title: String,
fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
});
Then you create the author and story linking them up
const author = new Person({
_id: new mongoose.Types.ObjectId(),
name: 'Ian Fleming',
age: 50
});
author.save(function (err) {
if (err) return handleError(err);
const story1 = new Story({
title: 'Casino Royale',
author: author._id // assign the _id from the person
});
story1.save(function (err) {
if (err) return handleError(err);
// that's it!
});
});
You can thereafter populate
Story.
findOne({ title: 'Casino Royale' }).
populate('author').
exec(function (err, story) {
if (err) return handleError(err);
console.log('The author is %s', story.author.name);
// prints "The author is Ian Fleming"
});
This will ensure you don't get an entry twice..

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

How to update a document having an array of arrays in MongoDB with Mongoose?

Given the following schema:
const item = {
_id: false,
amount: { type: Number, required: true },
};
const item_schema = new Schema({ item_history: [item] });
const parent_schema = new Schema({
...
items: [item_schema],
...
})
and this document in the database
{
...
items: [{ _id: 1, item_history: [{ amount: 10 }] }]
...
}
Let's say I want to update this document with these items:
const changed_or_new_items = [{ _id: 1, amount: 20 }, { amount: 30 }];
Which should result in this object in the database:
{
...
items: [{ _id: 1, item_history: [{ amount: 10 }, { amount: 20}] },
{ _id: 2, item_history: [{ amount: 30 }] }]
...
}
This is how I currently update the document:
const parent = await Parent.findOne(some_query).exec();
changed_or_new_items.forEach(item => {
if (!item._id) {
parent.items.push({ item_history: [item] });
}
else {
const item_doc = parent.items.id(item._id);
item_doc.item_history.push(_.omit(item, '_id'));
}
});
await parent.save();
Would the above be possible to achieve using an update operation e.g. findOneAndUpdate and if so, how?
You can use findOneAndUpdate with arrayFilters as:
Parent.findOneAndUpdate(
{ 'items._id': 1 },
{ '$set': { 'items.$.item_history.$[element].amount': 30 } },
{
'arrayFilters': [ {'element.amount': 20} ],
'new': true,
'upsert': true
}, (err, updatedParent ) => {
if (err) res.status(400).json(err);
res.status(200).json(updatedParent);
}
);

Resources