Conditionally update item or delete item in one query in mongoose - node.js

So, what I'm trying to achieve here is two things. First one is to decrement the quantity of a product. Second is that if that product quantity is 0 then delete that product.
So I'm trying to set a condition that if product quantity is 0 then delete that product otherwise simply decrement the quantity.
Here is my cart document:
And here is my function:
const cart = await Cart.findOneAndUpdate(
{ userID: req.body.userID, "items.productID": req.body.productID },
{$lt: ['items.productID', 0] ? { $pull: { items: { productID: req.body.productID } } } : { $inc: { 'items.$.quantity': -1 } }},
{ new: true }
);
I have tried to achieve that with ternary operator but it didn't worked. Can anybody help me with this please? Thank you.

You cannot use a ternary operator in your query. Not only is this not supported in MongoDB, it will also simply be evaluated by node.js, not by MongoDB itself. In fact, conditional update operations of the nature you're describing are themselves not something that MongoDB is capable of handling.
Your best bet is to perform two queries, where the first one performs the decrement operation and the second pulls any items out that have a quantity less than or equal to 0:
const cartAfterDecrement = await Cart.findOneAndUpdate(
{ userID: req.body.userID, "items.productID": req.body.productID },
{ $inc: { 'items.$.quantity': -1 } },
{ new: true }
);
const cartAfterPull = await Cart.findOneAndUpdate(
{ userID: req.body.userID, items: { $elemMatch: {
productID: req.body.productID,
quantity: { $lte: 0 }
} } },
{ $pull: {items: { quantity: { $lte: 0 } } } },
{ new: true }
);
const cart = cartAfterPull ? cartAfterPull : cartAfterDecrement;
If you wish to allow products with quantities equal to 0, then modifying the second query is a trivial matter.
The only downside to this approach is that there will be a natural, very slight delay between these two updates, which may result in a very rare case of a race condition occurring where a cart is returned and contains an item with a quantity less than or equal to 0. In order to avoid this, you will need to either filter these values out with server code or by retrieving your cart(s) using aggregation, or you will need to make use of transactions to ensure that the two operations occur in sequence without the document being retrieved between the two write operations.
With that said, constructing the appropriate array filtering logic or transaction management is beyond the scope of this question, so will not be included in this answer.

Query
you can do it with a pipeline update ( >= MongoDB 4.2)
find the user that has that productID
reduce the items, to an array with item but
if productID not the one i look for, keep it as it is
else if quantity=0 remove it(don't add it to the reduced array)
else keep it with quantity-1
*findAndModify accepts pipeline also, for nodejs driver you must find the method that wraps this command, or run the command directly
Test code here
update(
{"userID": req.body.userID,
"items.productID": req.body.productID},
[{"$set":
{"items":
{"$reduce":
{"input":"$items",
"initialValue":[],
"in":
{"$switch":
{"branches":
[{"case":{"$ne":["$$this.productID", req.body.productID]},
"then":{"$concatArrays":["$$value", ["$$this"]]}},
{"case":{"$gt":["$$this.quantity", 0]},
"then":
{"$concatArrays":
["$$value",
[{"$mergeObjects":
["$$this", {"$subtract":["$this.quantity", 1]}]}]]}}],
"default":"$$value"}}}}}}])

Thanks #B. Fleming and all others for your answer. I have just use a simple code implementation for that however it wasn't what I was expected but still it's good to go.
module.exports.deleteCartItem = async (req, res, next) => {
var condition;
var update;
const product = await Cart.findOne({ userID: req.body.userID,
items: {
$elemMatch: {
productID: req.body.productID,
quantity: { $lte: 0 }
}
}
});
// IF PRODUCT WITH 0 QUANTITY PULL/DELETE THAT PRODUCT
if (product !== null) {
condition = { userID: req.body.userID, items: { $elemMatch: { productID: req.body.productID, quantity: { $lte: 0 } } } },
update = { $pull: {items: { quantity: { $lte: 0 } } } }
}
// OTHERWISE DECREMENT THE QUANTITY FOR THAT PRODUCT
else {
condition = { userID: req.body.userID, "items.productID": req.body.productID }
update = { $inc: { 'items.$.quantity': -1 } }
}
try {
const cart = await Cart.findOneAndUpdate(condition, update, { new: true });
if (cart !== null) {
res.status(200).json({
statusCode: 1,
message: "Item Removed",
responseData: cart
});
} else {
res.json({
statusCode: 0,
msgCode: 462,
message: 'Not found',
responseData: null
});
}
} catch (err) {
console.log('Server Error: ', err);
next(new Error('Server Error, Something was wrong!'));
}
}

Related

mongoose query multiple operations in one request

I'm trying to use the $set, $addToSet and $inc at the same time for my report of sales and
tbh I'm not even sure if I did the right approach since it's not working.
once I send the request, the console gives me the error 404 but when I check the req.body the data was correct. so I was wondering if the problem is my query on mongoose because this was the first time I use multiple operations on mongoose query
export const report_of_sales = async (req, res) => {
const { id } = req.params;
console.log(req.body);
try {
if (!mongoose.Types.ObjectId.isValid(id)) return res.status(404).json({ message: 'Invalid ID' });
let i;
for (i = 0; i < req.body.sales_report.length; i++) {
await OwnerModels.findByIdAndUpdate(id, {
$inc: {
total_clients: req.body.total_clients,
total_product_sold: req.body.sales_report[i].qty,
sales_revenue: req.body.sales_report[i].amount
},
$set: {
"months.$[s].month_digit": req.body.months[i].month_digit,
"months.$[s].targetsales": req.body.months[i].targetsales,
"months.$[s].sales": req.body.months[i].sales,
},
$addToSet: {
sales_report: {
$each: [{
identifier: req.body.sales_report[i].identifier,
product_name: req.body.sales_report[i].product_name,
generic_name: req.body.sales_report[i].generic_name,
description: req.body.sales_report[i].description,
qty: req.body.sales_report[i].qty,
amount: req.body.sales_report[i].amount,
profit: req.body.sales_report[i].profit
}]
}
}
}, {
arrayFilters: [
{
"s.month_digit": req.body.months[i].month_digit
}
],
returnDocument: 'after',
safe: true,
}, { new: true, upsert: true })
}
} catch (error) {
res.status(404).json(error);
}
}
Well, you are looking at the body, but you are actually using query parameter named id. This is probably undefined, which leads to ObjectId.isValid(id) returning false.
You should decide on whether to pass this data as a query param or in the request body and adjust your code accordingly.

Mongoose: cannot infer query fields to set, path 'participants' is matched twice

I'm using mongoose with Node.js to create a document of chat with participants as one of the fields if the chat doesn't exist.
If it does exist then simply increment the status to 1 or any number.
My current Solution:
try {
let query = { participants: { $all: [CURRENT_USER_ID, TARGETED_ID] } };
let update = { $inc: { status: 1 }};
let options = { upsert: true, new: true };
let chat = await Chat.findOneAndUpdate(
query,
update,
options
).exec();
console.log(chat);
} catch (err) {
console.log(err.message);
}
I will receive an error
"cannot infer query fields to set, path 'participants' is matched
twice"
I even use this solution and it doesn't work, it created an empty list of participants instead.
let query = {
participants: {
$all: [
{ $elemMatch: { $eq: CURRENT_USER_ID } },
{ $elemMatch: { $eq: TARGETED_ID } }
]
}
};
Any help would be really helpful. Thanks

Cannot find id and update and increment sub-document - returns null Mongoose/mongoDB

I have a problem where I cannot seem to retrieve the _id of my nested objects in my array. Specifically the foods part of my object array. I want to find the _id, of lets say risotto, and then increment the orders count dynamically (from that same object).
I'm trying to get this done dynamically as I have tried the Risotto id in the req.body._id and thats fine but i can't go forward and try to increment orders as i get null.
I keep getting null for some reason and I think its a nested document but im not sure. heres my route file and schema too.
router.patch("/update", [auth], async (req, res) => {
const orderPlus = await MenuSchema.findByIdAndUpdate({ _id: '5e3b75f2a3d43821a0fb57f0' }, { $inc: { "food.0.orders": 1 }}, {new: true} );
//want to increment orders dynamically once id is found
//not sure how as its in its own seperate index in an array object
try {
res.status(200).send(orderPlus);
} catch (err) {
res.status(500).send(err);
}
});
Schema:
const FoodSchema = new Schema({
foodname: String,
orders: Number,
});
const MenuSchema = new Schema({
menuname: String,
menu_register: Number,
foods: [FoodSchema]
});
Heres the returned Database JSON
{
"_id": "5e3b75f2a3d43821a0fb57ee",
"menuname": "main course",
"menu_register": 49,
"foods": [
{
"_id": "5e3b75f2a3d43821a0fb57f0",
"foodname": "Risotto",
"orders": 37
},
{
"_id": "5e3b75f2a3d43821a0fb57ef",
"foodname": "Tiramisu",
"orders": 11
}
],
"__v": 0
}
the id for the menuname works in its place but i dont need that as i need to access the foods subdocs. thanks in advance.
You are sending food id (5e3b75f2a3d43821a0fb57f0) to the MenuSchema.findByIdAndUpdate update query. It should be the menu id which is 5e3b75f2a3d43821a0fb57ee
You can find a menu by it's id, and update it's one of the foods by using food _id or foodname using mongodb $ positional operator.
Update by giving menu id and food id:
router.patch("/update", [auth], async (req, res) => {
try {
const orderPlus = await MenuSchema.findByIdAndUpdate(
"5e3b75f2a3d43821a0fb57ee",
{ $inc: { "foods.$[inner].orders": 1 } },
{ arrayFilters: [{ "inner._id": "5e3b75f2a3d43821a0fb57f0" }], new: true }
);
res.status(200).send(orderPlus);
} catch (err) {
res.status(500).send(err);
}
});
Update by giving menu id and foodname:
router.patch("/update", [auth], async (req, res) => {
try {
const orderPlus = await MenuSchema.findByIdAndUpdate(
"5e3b75f2a3d43821a0fb57ee",
{ $inc: { "foods.$[inner].orders": 1 } },
{ arrayFilters: [{ "inner.foodname": "Risotto" }], new: true }
);
res.status(200).send(orderPlus);
} catch (err) {
res.status(500).send(err);
}
});

Mongoose SUM get stacked

I'm trying to make trivial SUM on mongoDB to count number of prices for single client.
My collection:
{"_id":"5d973c71dd93adfbda4c7272","name":"Faktura2019006","clientId":"5d9c87a6b9676069c8b5e15b","expiration":"2019-10-02T01:11:18.965Z","price":999999,"userId":"123"},
{"_id":"5d9e07e0b9676069c8b5e15d","name":"Faktura2019007","clientId":"5d9c87a6b9676069c8b5e15b","expiration":"2019-10-02T01:11:18.965Z","price":888,"userId":"123"}
What I tried:
// invoice.model.js
const mongoose = require("mongoose");
const InvoiceSchema = mongoose.Schema({
_id: String,
name: String,
client: String,
userId: String,
expiration: Date,
price: Number
});
module.exports = mongoose.model("Invoice", InvoiceSchema, "invoice");
and
// invoice.controller.js
const Invoice = require("../models/invoice.model.js");
exports.income = (req, res) => {
console.log("Counting Income");
Invoice.aggregate([
{
$match: {
userId: "123"
}
},
{
$group: {
total: { $sum: ["$price"] }
}
}
]);
};
What happen:
When I now open a browser and code above is being called, I get console log 'Counting Income' in terminal however in browser it's just loading forever and nothing happen.
Most likely I just miss some stupid minor thing but I'm trying to find it out for quite a long time without any success so any advise is welcome.
The reason that the controller never finishes is because you are not ending the response process (meaning, you need to use the res object and send something back to the caller).
In order to get the aggregate value, you also need to execute the pipeline (see this example).
Also, as someone pointed out in the comments, you need to add _id: null in your group to specify that you are not going to group by any specific field (see the second example here).
Finally, in the $sum operator, for what you're trying to do, you just need to remove the array brackets since you only want to sum on a single field (see a few examples down here).
Here is the modified code:
// invoice.controller.js
const Invoice = require("../models/invoice.model.js");
exports.income = (req, res) => {
console.log("Counting Income");
Invoice.aggregate([
{
$match: {
userId: "123"
}
},
{
$group: {
_id: null,
total: { $sum: "$price" }
}
}
]).then((response) => {
res.json(response);
});
};
Edit for your comment about when an empty array is returned.
If you want to always return the same type of object, I would control that in the controller. I'm not sure if there is a fancy way to do this with the aggregate pipeline in mongo, but this is what I would do.
Invoice.aggregate([
{
$match: {
userId: "123"
}
},
{
$group: {
_id: null,
total: { $sum: "$price" }
}
},
{
$project: {
_id: 0,
total: "$total"
}
}
]).then((response) => {
if (response.length === 0) {
res.json({ total: 0 });
} else {
// always return the first (and only) value
res.json(response[0]);
}
});
Here, if you find a userId of 123, then you would get this as the return:
{
"total": 1000887
}
But if you change the userId to, say, 1123 which doesn't exist in your db, the result will be:
{
"total": 0
}
This way, your client can always consume the same type of object.
Also, the reason I put the $project pipeline stage in there was to suppress the _id field (see here for more info).

Mongoose update with limit

I am looking to update X documents all at once. The short is I basically need to randomly select N documents and then update them as "selected". I'm trying to design an API that needs to randomly distribute questions. I can not find a way to do this in mongoose I have tried:
update ends up selecting everything
Question
.update({}, {
$inc: {
answerCount: 1,
lockedCount: 1
},
$push:{
devices: deviceID
}
}, {multi:true})
.limit(4)
--- I also tried
Question
.find()
.sort({
answerCount: 1,
lockedCount: 1
})
.limit(req.query.limit || 4)
.update({}, {
$inc: {
answerCount: 1,
lockedCount: 1
},
$push:{
devices: deviceID
}
}, { multi: true }, callback);
Both resulted in updating all docs. Is there a way to push this down to mongoose without having to use map ? The other thing I did not mention is .update() without multi resulted in 1 document being updated.
You could also pull an array of _ids that you'd like to update then run the update query using $in. This will require two calls to the mongo however the updates will still be atomic:
Question.find().select("_id").limit(4).exec(function(err, questions) {
var q = Question.update({_id: {$in: questions}}, {
$inc: {answerCount: 1, lockedCount:1},
$push: {devices: deviceid}
}, {multi:true});
q.exec(function(err) {
console.log("Done");
});
});
So I did an simple map implementation and will use it unless someone can find a more efficient way to do it.
Question
.find({
devices: { $ne: deviceID}
},
{ name: true, _id: true})
.sort({
answerCount: 1,
lockedCount: 1
})
.limit(req.query.limit || 4)
.exec(updateAllFound );
function updateAllFound(err, questions) {
if (err) {
return handleError(res, err);
}
var ids = questions.map(function(item){
return item._id;
});
return Question.update({ _id: { $in: ids} } ,
{
$inc: {
answerCount: 1,
lockedCount: 1
},
$push:{
devices: deviceID
}
}, { multi: true }, getByDeviceID);
function getByDeviceID(){
return res.json(200, questions);
}
}

Resources