In Mongoose findOneAndUpdate, how can I make my post request work? - node.js

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/

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.

How do I update nested array values in mongoose?

I am fairly new to nodejs/express and I'm practicing full stack development with devchallenges.io, i'm doing the shoppingify challenge. I'm trying to update the quantity of an item I am targeting inside of the items array. I understand my attempt below was terrible, I'm really struggling to understand the logic to be able to do so.
// #route PUT api/list/item/quantity/:id
// #desc Increase or decrease quantity
// #access Private
router.put('/item/quantity/:id', auth, async (req, res) => {
const { action } = req.body;
try {
let list = await List.findOne({ user: req.user.id });
const item = list.items.find(
(item) => item._id.toString() === req.params.id
);
list = list.updateOne(
{ 'items._id': req.params.id },
{ $set: { 'items.quantity': item.quantity + 1 } }
);
await list.save();
return res.json(list);
} catch (error) {
console.error(error.message);
res.status(500).send('Server Error');
}
});
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const ListSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
},
name: {
type: String,
default: 'Shopping List',
},
items: [
{
name: {
type: String,
default: '',
},
note: {
type: String,
default: '',
},
image: {
type: String,
default: '',
},
category: {
type: String,
default: '',
},
quantity: {
type: Number,
default: 1,
},
},
],
date: {
type: Date,
default: Date.now,
},
});
module.exports = List = mongoose.model('list', ListSchema);
Look this is my update-vendor route here I'm updating nested street and city name.
router.put("/update-vendors", async (req, res, next) => {
const vendor = await Vendor.updateOne(
{
"address.street": "Street2",
},
{
$set: {
"address.$.street": req.body.street,
"address.$.city": req.body.city,
},
}
);
res.status(200).json(vendor);
});
You can update particular things with the help of $set and other $push method

Implementing post('updateOne') hook middleware with Mongoose(MongoDB)

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

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();
});

node mongoose : aggregate not giving expected result

withe the following Group Schema,
group.model.js
const Role = new mongoose.Schema({
name: { type: String, required: true }, // ensure uniqueness withn group instance using addToSet
description: { type: String, required: false }
});
const GroupSchema = new mongoose.Schema({
name: { type: String, index: { unique: true, required: true, dropDups: true } },
description: { type: String, required: false },
roles: [Role],
createdAt: {
type: Date,
default: Date.now
}
});
I am trying to list all roles ( subdocument) got a specific group
group.controller.js
function listRoles(req, res) {
const group = req.group;
console.log('GROUP: %j', group);
const limit = parseInt(req.query.limit, 10) || 50;
const skip = parseInt(req.query.skip, 10) || 0;
Group.aggregate([
{ $match: { _id: req.params.groupId } },
{ $unwind: '$roles' },
{ $skip: skip },
{ $limit: limit }
], (err, result) => {
if (err) {
res.status(500);
res.json({ message: 'Error. Cannot list roles', errror: err });
}
res.status(200);
console.log('RESULT: %j', result);
res.json(result);
});
}
I should get an array with one role, but I get an empty array
what's wrong with my aggregate code ? thanks for feedback
note: I tried to aggregate only with the $match in the pipe and I also get an empty array... so I guess. the issue comes from the req.params.groupId should be an ObjectId .. how can I cast it ?
console.log
GROUP: {"_id":"5923e2e83afd4149bdf16c61","name":"Admin","description":"Administration group","__v":1,"createdAt":"2017-05-23T07:21:12.470Z","roles":[{"name":"Role1","description":"description role1","_id":"5923e2e83afd4149bdf16c62"}]}
RESULT: []
To better diagnose this, I'd recommend removing steps from your aggregation pipeline and seeing what the result is. However, I suspect your problem is because you have no match at the first stage because you're comparing a string to an ObjectId. Try this:
const mongoose = require('mongoose')
// and in the aggregation:
{ $match: { _id: mongoose.Types.ObjectId(req.params.groupId) } }

Resources