Implementing post('updateOne') hook middleware with Mongoose(MongoDB) - node.js

I am trying to calculate the average value of the ratings for a Product per Document that is being rated. Instead of calculating the average rating of the Product every time when we need the value. I calculate it every time someone rates it. To accomplish this task I am implementing a post(‘updateOne’) hook middleware. Though not sure I can accomplish this by implementing this hook. Let me know if I am going into the wrong direction. Here is the error I am getting avgRating error from post updateOne hookTypeError: Cannot read property 'aggregate' of undefined.
file - Product.js
import mongoose from 'mongoose';
const { ObjectId } = mongoose.Schema;
const ParentSchema = new mongoose.Schema(
{
title: {
type: String,
trim: true,
required: true,
maxlength: 32,
text: true,
},
slug: {
type: String,
unique: true,
lowercase: true,
index: true,
},
price: {
type: Number,
required: true,
trim: true,
maxlength: 32,
},
quantity: Number,
ratings: [
{
star: Number,
postedBy: { type: ObjectId, ref: 'User' },
},
],
},
{
timestamps: true,
}
);
const avgRating = async function () {
try {
const stats = await this.aggregate([
{
$addFields: {
avgRating: { $avg: '$ratings.star' },
nRatings: { $size: '$ratings' },
},
},
]);
console.log('stats: ', stats);
} catch (err) {
console.log(`avgRating error from post updateOne hook${err}`);
}
};
// Call avgRating after updateOne
ParentSchema.post(
'updateOne',
{ document: true, query: false },
async function () {
await avgRating();
}
);
export default mongoose.models.Product ||
mongoose.model('Product', ParentSchema);
file - productServices.js
import Product from './Product.js';
const updateRating = async (existingRatingObject, star) => {
const query = {
ratings: { $elemMatch: existingRatingObject },
};
const update = { $set: { 'ratings.$.star': star } };
const option = { new: true };
try {
const doc = new Product();
const ratingUpdated = await doc.updateOne(query, update, option);
return ratingUpdated;
} catch (error) {
console.log('product model updateRating error: ', error);
}
};

Your avgRating function does not have access to the mongoose document under this. That is why you are getting the error, Cannot read property 'aggregate' of undefined, because this (the document) which you want to modify does not exists in the function.
You need to use an instance method available under mongoose. With instance method, you can call this to reference the document.
Check out:
https://www.freecodecamp.org/news/introduction-to-mongoose-for-mongodb-d2a7aa593c57/#instace-methods
https://mongoosejs.com/docs/guide.html#methods

Related

Mongoose - findByIdAndUpdate

All,
I can seem to figure out why the record in the database will not update. I am not 100% sure where my error is but this isn't really providing me a great error message. Can someone please take a look at this for me?
I believe that I am calling the mongoose request properly. Thank you in advance!
$ npm mongoose -v
8.15.0
const mongoose = require("mongoose");
const CartSchema = new mongoose.Schema(
{
owner: {
type: String,
unique: true,
required: true,
},
discount: {
type: Number,
},
total: {
type: Number,
},
items: [
{
itemId: {
type: Number,
},
sku: {
type: Number,
},
quantity: {
type: Number,
},
price: {
type: Number,
},
},
],
},
{ timestamps: true }
);
const Cart = mongoose.model("Cart", CartSchema);
module.exports = Cart;
Record in Database
{"_id":{"$oid":"630689708997a6589635986c"},
"owner":"611afa8b9069c9126cff3357",
"total":{"$numberInt":"0"},
"items":[],
"createdAt":{"$date":{"$numberLong":"1661372784844"}},
"updatedAt":{"$date":{"$numberLong":"1661372784844"}},
"__v":{"$numberInt":"0"}}
exports.add = async (req, res, next) => {
const { id, product } = req.body;
const addItem = { itemId: product._id, sku: product.sku, quantity: 1, price: product.price };
console.log(addItem);
try {
const updateCart = Cart.findByIdAndUpdate(id, { $addToSet: { items: addItem } }, { new: true, returnDocument: "after" });
if (!updateCart) return next(new ErrorResponse("Unable to update the cart record", 404));
console.log(updateCart);
if (updateCart) {
return sendRes(updateCart, 200, res);
} else {
return sendRes(updateCart, 201, res);
}
} catch (error) {
console.log(error);
next(error);
}
};
This issue was caused by me using an ASYNC Function without the AWAIT on the DB Call.
Please try once with this:
Cart.findByIdAndUpdate(id, { $addToSet: { items: addItem } }, { new: true, returnDocument: "after" });
The first obvious mistake is that you're searching for a document with the wrong field:"id", Kindly change that to "_id: id"
Also you might need to convert the _id string you have to MongoDB Object ID, like this:
const ObjectId = require('mongodb').ObjectId;
Cart.updateOne({_id: new ObjectId(id)}, { $addToSet: { items: addItem } }, { new: true, returnDocument: "after" });
For other update method, you need to specify the field, and also convert it to a MongoDB ID
OR
Cart.findByIdAndUpdate(id, { $addToSet: { items: addItem } }, { new: true, returnDocument: "after" })
You do not to specify the field in findByIdAndUpdate, just pass the id to it.

In Mongoose findOneAndUpdate, how can I make my post request work?

Hi all so I am trying to make a post request that increments a value if it already exists and if not it should create a new item.
router.post('/', auth, async (req, res) => {
try {
const { name, price, image } = req.body;
var query = { name },
update = { $inc: { count: 1 } },
options = { upsert: true, new: true,};
await CartItem.findOneAndUpdate(query, update, options, function (
err,
data
) {
if (err) {
const newItem = new CartItem({
user: req.user.id,
name: name,
price: price,
image: image,
});
const item = newItem.save();
res.json(item);
} else {
res.json(data);
}
});
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const CartItemSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'user',
},
name: {
type: String,
required: true,
},
price: {
type: Number,
required: true,
},
count: {
type: Number,
},
image: {
type: String,
required: true,
},
});
module.exports = CartItem = mongoose.model('cartItem', CartItemSchema);
So there are two problems here that I cannot wrap my head around(Pretty new with MongoDb, did do my research).
I can get the count to increment, but it increments with 2 or even more instead of 1. (I know other users also experienced this)
If the item is already in the cart(name matches) I want a new item to be added which does happen, but it only adds the name, count and Id. I want it to add the user, name, price and image.
Would appreciate some assistance.
you should create your document with a default value equals to 0.
define count at your schema like the following:
count: {
type: Number,
default: 0
}
then use { $inc: { <field1>: <amount1>, <field2>: <amount2>, ... } }.
link to docs: https://docs.mongodb.com/manual/reference/operator/update/inc/

Is there a way i could keep track of the Time and the entity that was changed in a model

Basically I'm trying to get the time and the entity changed in a particular model when ever the update method is called.
This is my model I want to keep track of:
const mongoose = require("mongoose");
const modelSchema = mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
name: {
type: String,
required: true,
},
note1: String,
note2: String,
note3: String,
images: {
type: Array,
required: true
},
status: {
enum: ['draft', 'pending_quote', 'pendong_payment', 'in_production', 'in_repair', 'pemding_my_review', 'fulfilled'],
type: String,
default: "draft"
},
price: {
type: mongoose.Schema.Types.ObjectId,
ref: "Price",
}
}, {
timestamps: true,
})
module.exports = mongoose.model("Model", modelSchema)
And this is the method I call to update the status:
exports.updateModel = async (req, res) => {
try {
let id = req.params.id;
let response = await Model.findByIdAndUpdate(id, req.body, {
new: true
})
res.status(200).json({
status: "Success",
data: response
})
} catch (err) {
res.status(500).json({
error: err,
msg: "Something Went Wrong"
})
}
}
you can add a new field in your schema like:
logs:[{
entity: String,
timeStamp: Date
}]
Then updating it basing on your current code:
let id = req.params.id;
// I don't know whats in the req.body but assuming that it
// has the correct structure when passed from the front end
let response = await Model.findByIdAndUpdate(id,
{
$set:req.body,
$push:{logs:{entity:'your entity name here',timeStamp:new Date()}}
}, {
new: true
})

In mongoose set a field based on the value of another field in findOneAndUpdate

I'm working on a project where in one model I need to set the value of a field based on another fields value. Let me explain with some code.
Destination model
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const DestinationSchema = new Schema({
name: {
type: String,
required: true
},
priority: {
type: Number,
default: 0,
max: 10,
required: true
}
})
DestinationSchema.statics.getPriority = function(value) {
return this.findOne({ _id: value })
}
const Destination = mongoose.model('Destination', DestinationSchema)
exports.Destination = Destination
Task model
const mongoose = require('mongoose')
const { Destination } = require('../_models/destination.model')
const Schema = mongoose.Schema;
const TaskSchema = new Schema({
priority: {
type: Number,
required: true,
min: 0,
max: 25
},
from: {
type: Schema.Types.ObjectId,
ref: 'Destination',
required: true
},
to: {
type: Schema.Types.ObjectId,
ref: 'Destination',
required: true
},
type: {
type: Number,
required: true,
min: 0,
max: 3
}
}, {
timestamps: true
})
TaskSchema.pre('save', async function () {
this.priority = await Destination.getPriority(this.from).then(doc => {
return doc.priority
})
this.priority += await Destination.getPriority(this.to).then(doc => {
return doc.priority
})
this.priority += this.type
})
Task Controller update function
exports.update = async function (req, res) {
try {
await Task.findOneAndUpdate({
_id: req.task._id
}, { $set: req.body }, {
new: true,
context: 'query'
})
.then(task =>
sendSuccess(res, 201, 'Task updated.')({
task
}),
throwError(500, 'sequelize error')
)
} catch (e) {
sendError(res)(e)
}
}
When I create a new Task, the priority gets set in the pre save hook just fine as expected. But I'm hitting a wall when I need to change Task.from or Task.to to another destination, then I need to recalculate the tasks priority again. I could do it on the client side, but this would lead to a concern where one could just simply send a priority in an update query to the server.
My question here is, how can I calculate the priority of a Task when it gets updated with new values for from and to? Do I have to query for the document which is about to get updated to get a reference to it or is there another cleaner way to do it, since this would lead to one additional hit to the database, and I'm trying to avoid it as much as possible.
In your task schema.
you have to use pre("findOneAndUpdate") mongoose middleware. It allows you to modify the update query before it is executed
Try This code:
TaskSchema.pre('findOneAndUpdate', async function(next) {
if(this._update.from || this._update.to) {
if(this._update.from) {
this._update.priority = await Destination.getPriority(this._update.from).then(doc => {
return doc.priority
});
}
if(this._update.to) {
this._update.priority += await Destination.getPriority(this._update.to).then(doc => {
return doc.priority
});
}
}
next();
});

Mongoose: findByIdAndUpdate method is not updating nor inserting

The findByIdAndUpdate method should update or insert an object in the DB, but it does nothing. Nor it does throw an error message or something.
I also tried it with the original ObjectId as _id field, but doesn't work either.
Has anybody a clue what is missing to update or insert the object into the DB?
const schema = new Schema({
_id: {
type: Number,
required: true
},
name: {
type: String,
required: true
}
}, { toJSON: { virtuals: true } });
const myJson = {
"myobject": {
"_id": 781586495786495,
"name": "MyName"
}
}
const MyModel = mongoose.model('MyModel', schema);
MyModel.findByIdAndUpdate(myJson.myobject._id, myJson.myobject, { upsert: true });
I used your code and it actually works as intended. The reason why it doesn't return any errors nor does anything might be that you're not connected to the database at the time you're executing the operation on the model.
Please consider the following snippet, it worked for me.
mongoose.connect('mongodb://localhost:27017/testingcollection').then(() => {
const schema = new mongoose.Schema({
_id: {
type: Number,
required: true
},
name: {
type: String,
required: true
}
}, { toJSON: { virtuals: true } });
const myJson = {
"myobject": {
"_id": 781586495786495,
"name": "MyName"
}
};
const MyModel = mongoose.model('MyModel', schema);
MyModel.findByIdAndUpdate(myJson.myobject._id, myJson.myobject, { upsert: true }).then(obj => {
mongoose.connection.close();
});
});

Resources