Mongoose $inc with maximum value - node.js

So I'm currently trying to perform this operation
return this.model.findByIdAndUpdate(id, { $push: { certifiedBy: certifier } }, { $inc: {score: 1}}, { new: true })
The issue here is that score will grow without limit, I would like to prevent this and make it so when this is happening it cannot increment if score <= 5 but still add certifier into my certifiedBy array.
Can it be done with mongoose directly or do I have to get the object first check if it over 5 and call a different query in that case ?
Thanks

You can't change the $inc behaviour but you can do a checkpoint to stop it before 5
return this.model.findOneAndUpdate({
_id: id,
score: {
$lte: 5
}
}, {
$push: { certifiedBy: certifier },
$inc: { score: 1 }
},
{
new: true
})

Related

Conditionally update item or delete item in one query in mongoose

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!'));
}
}

How to sum all amount paid by users

I am finding it defficult to add up all amount paid by customers that ordered items
Order Schema
const orderschema = new Mongoose.Schema({
created: { type: Date, default: Date.now() },
amount: { type: Number, default: 0 }
User: [{ type: Mongoose.Schema.ObjectId, ref: 'Users'}]
...
})
Route
Get('/total-amount', total-amount)
Controller
Exports.total-amount = () => {
Order.find()...
}
I don't know what to add here to get the total amount made by all customers.
Using NodeJS and MongoDB.
Thank you for you help
You can use $sum in an aggregation stage like this:
First $group all (without _id is to group all values)
Then create field total which is the sum of al amount.
And an optional stage, $project to output only total field.
db.order.aggregate({
"$group": {
"_id": null,
"total": {
"$sum": "$amount"
}
}
},
{
"$project": {
"_id": 0
}
})
Example here
To add into a controller using nodeJS and Mongoose you can use something like this piece of code:
Exports.total - amount = (req, res) => {
Order.aggregate({
"$group": {
"_id": null,
"total": {
"$sum": "$amount"
}
}
}, {
"$project": {
"_id": 0
}
}).then(response => {
res.status(200).send(response)
}).catch(e => res.status(400).send())
}
Note hoy the operation is done using your mongoose model (in this case Order). You are calling aggregate method in the same way you call find method for example: Instead of doing
yourModel.find()
Is
yourModel.aggregate()
And the response will be:
[
{
"total": 6
}
]
So even you can update your controller to add a if/else block like this:
if(response[0].total)
res.status(200).send(response[0].total)
else
res.status(404).send()

Push into a List and Pop Conditionally in Mongoose and MongoDB

I am creating a list of scores for a user in mongoDB by adding a new score 1 at a time and sorting the list. I want to remove the lowest score when the list grows larger than 5 elements.
The reason for this is because I want to store the top 5 scores of the user.
What would be the best way to do this? Is there a way to make the whole thing an atomic operation?
My code is below. I'm using NodeJS with Mongoose and MongoDB.
const maxScoresToStore = 5
var scoreEntrySchema = new Schema({
score: Number,
when: { type: Date, default: Date.now }
})
var scoreSchema = new Schema({
_userid: { type: Schema.Types.ObjectId, ref: 'Users' },
username: {type: String, index:{unique: true}},
scores: [scoreEntrySchema]
})
const scoreModel = mongoose.model("Scores", scoreSchema)
exports.addUserScore = (uid, uname, score) => {
var query = {_userid:uid, username:uname},
update = { $push : {"scores" : {$each: [{"score": score}], $sort: {"score":-1}}} }, // sorts in descending order after pushing
options = { upsert: true, new: true, setDefaultsOnInsert: true };
scoreModel.findOneAndUpdate(query, update, options).then(
(result)=>{
if(result.scores.length > maxScoresToStore)
{
// ToDo:
// result.update({$pop: {"scores" : 1 }}) // pops the last element of the list
}
}
)
}
You can use $slice operator, And your query looks like:
let score = await scoreModel.findOneAndUpdate({ _userid: uid, username: uname },
{
$push: {
scores: {
$each: [{ score: score }],
$sort: { score: -1 },
$slice: maxScoresToStore
}
}
},
{
upsert: true,
new: true,
setDefaultsOnInsert: true,
multi: true
});
[DO VOTE TO THIS ANSWER, IF ITS HELPFUL TO YOU]
You can add slice option to your update option:
update = {
$push: {
scores: { $each: [{ score: score }], $sort: { score: -1 }, $slice: maxScoresToStore }
}
}
Here is the full method code written in async/await style:
exports.addUserScore = async (uid, uname, score) => {
const query = { _userid: uid, username: uname };
const update = {
$push: {
scores: {
$each: [{ score: score }],
$sort: { score: -1 },
$slice: maxScoresToStore
}
}
};
const options = {
upsert: true,
new: true,
setDefaultsOnInsert: true,
multi: true
};
try {
let score = await scoreModel.findOneAndUpdate(query, update, options);
if (!score) res.send(404).send("Score not found");
res.send("Everything is ok");
} catch (err) {
console.log(err);
res.status(500).send("Something went wrong");
}
};
I'm not certain If this would help, but it might work
scoreModel.update(
{ "scores.5": { "$exists": 1 } },
{ "$pop": { "scores": 1 } },
{ "multi": true }
)
As you are already sorting by descending, you can check if the array length is greater than 5 by using scores.5, If this returns true then you can pop the last element using $pop.
If $exists return false then it will skip the query. you can run this update after .then() and you won't have to use if condition.
But keep in mind $pop will only remove 1 element.

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