Deleting an item with condition in MongoDB? - mongodb

I want to remove a product from the Cart by checking its quantity. Its quantity should be decremented by 1 unless it reaches zero, and after that, it should pull out from the product array of the Cart.
here is my Logic : (I want to perform the pull and decrement operation inside the single query. But I m stuck on how to perform these two operations together by a simple condition in MongoDb)
const cart = await Cart.findOneAndUpdate({"products.productId": req.body.productId}, {$inc: {"products.$.quantity": -1}}, {new: true})
await Cart.update({"products.productId": req.body.productId}, {$pull: {quantity: 0}})
here is the model for clarification:
import mongoose from 'mongoose';
const cartSchema = new mongoose.Schema({
userId: {
type: String,
required: true,
},
products: [
{
productId: {
type: String,
},
quantity: {
type: Number,
default: 1
}
}
]
}, {timestamps: true});
const Cart = new mongoose.model('Cart', cartSchema);
export default Cart;
Thanks :)

There is no straight way to do this in single regular update query.
To improve your approach you can try this,
first query to check productId and quantity should greater than 1
const cart = await Cart.updateOne(
{
products: {
$elemMatch: {
productId: req.body.productId,
quantity: { $gt: 1 }
}
}
},
{ $inc: { "products.$.quantity": -1 } }
);
Playground
second query if the first query's result is nModified is 0 then pull the product, by checking condition productId and quantity equal-to 1
if (cart.nModified === 0) {
await Cart.updateOne(
{
products: {
$elemMatch: {
productId: req.body.productId,
quantity: { $eq: 1 }
}
}
},
{ $pull: { products: { productId: req.body.productId } } }
)
}
Playground
If you really want to do using single query you can try update with aggregation pipeline starting from MongoDB 4.2,
$map to iterate loop of products array and check condition, if the productId matches then increment/decrement quantity by $add operator otherwise return current quantity
$filter to iterate loop of above result and check condition if productId and quantity is not zero
await Cart.updateOne(
{ "products.productId": req.body.productId },
[{
$set: {
products: {
$filter: {
input: {
$map: {
input: "$products",
in: {
productId: "$$this.productId",
quantity: {
$cond: [
{ $eq: ["$$this.productId", req.body.productId] },
{ $add: ["$$this.quantity", -1] },
"$$this.quantity"
]
}
}
}
},
cond: {
$and: [
{ $eq: ["$$this.productId", req.body.productId] },
{ $ne: ["$$this.quantity", 0] }
]
}
}
}
}
}
])
Playground

Related

mongodb aggregation where document field is less than another field

Using mongoose, I'm trying to make a query that searches for tasks where timeSpent is greater than timeBilled.
Task schema:
const myTaskSchema = new Schema({
date: { type: Date, default: Date.now },
timeSpent: { type: Number },
timeBilled: { type: Number }
})
The query I've tried:
myTaskSchema.aggregate([
{
$match: {
timeSpent: { $gt: '$timeBilled' }
}
}
])
.then(data => {
console.log(data)
})
But I'm getting zero results (I know there should be results)
NOTE: Not every task has a timeSpent or timeBilled.field if that matters.
here is my dirty solution. It'd be nice if I didnt have to add a field but this gets me where I want to be.
myTaskSchema.aggregate([
{
$addFields: {
needToBill: { $gt: ['$timeSpent', '$timeBilled'] }
}
},
{
$match: {
needToBill: true
}
},
{
$project: {
timeSpent: 1,
timeBilled: 1
}
}
])

Conditional operator in mongoose

I am trying to conditionally execute two different mongoose operators but it just return no error and doesn't work to update the document.
My Schema:
const CartSchema: Schema<Document<ICart>> = new Schema({
clientID: { type: String, required: true },
sourceID: { type: String, required: true },
items: { type: Array },
source: { type: String, required: true },
}, { collection: "carts", timestamps: true });
The way I am trying to implement that:
await CartModel.findOneAndUpdate(
{ sourceID: clientID, 'items.id': Types.ObjectId(itemID) },
{
$cond: {
if: {
$eq: 1,
},
then: { $pull: { 'items.$.id': Types.ObjectId(itemID) }},
else: { $inc: { 'items.$.amount': -1 }},
}
},
{ new: true }
).lean({ virtuals: true })
And I also tried to have this kind of query: { sourceID: clientID } but it didn't help. I thought maybe I could not find the element and it just silently pass through.
The main idea here of what I am gonna do is - have a conditional mongoose request where I'll either remove the object from the array if the current value in the field amount is equal to 1, or decrement the value to -1.
Apparently, the $cond operator works only in the aggregation framework but in my Atlas tier I cannot check if the query works properly, but I suppose it should look something like this:
await CartModel.aggregate([
{ $match: {
sourceID: clientID,
'items.id': Types.ObjectId(itemID)
}
},
{
$cond: {
if: {
$eq: 1,
},
then: { $pull: { 'items.$.id': Types.ObjectId(itemID) }},
else: { $inc: { 'items.$.amount': -1 }},
}
}
])

How to find all matched elements from array of objects in mongoose

Schema:
new Schema({
productId: String,
types: [{
productType: String,
lastModified: Date
}]
});
Query:
{
productId: "1",
email: "test#test.com",
productType: "test",
}
I tried this but its returning only first matched element:
const productType = 'test';
const result = await this.model(email)
.find(
{ productId, 'types.productType': productType },
{ 'types.$': productType }
).lean();
with aggregate, it return empty array result:
const result = await this.model(email).aggregate([
{ $match: { productId, 'types.productType': 'productType' } },
{
$project: {
types: {
$filter: {
input: '$types',
as: 'r',
cond: { $eq: ['$$r.productType', productType] }
}
},
_id: 0
}
}
]);
I need to find all matching elements where projection $ returns the first matched

Mongoose find and update nested document

I am trying to find and update a sub document under another sub document. I am not getting the result as I expect. This is what I currently have setup:
const SiteSchema = new mongoose.Schema({
domain: { type: String, required: true },
keywords: [],
campaigns: [
{
campaign: {
type: mongoose.Schema.Types.ObjectId,
ref: "Campaign",
},
responses: [
{
message: { type: String },
asking_fee: { type: Number },
date: { type: Date },
},
],
}],
})
I would like to find and edit a particular response. Here is the code I have now. I am new to mongoose and MongoDB.
const site = await Site.findOneAndUpdate({
"campaigns.responses._id": responseId, // will it fetch the response ?
}, {
$set: { // I am struggling with the following
"campaigns.$.responses.message": message,
"campaigns.$.responses.asking_price": asking_price,
"campaigns.$.responses.date": date,
},
}
);
If you don't have campaigns.campaign id then you have to use update with aggregation pipeline starting from MongoDB 4.2,
$set to update campaigns field, $map to iterate loop of campaigns array, $map to iterate loop of campaigns.responses array and check condition if responseId match then return updateFields otherwise return old fields and merge with current object using $mergeObjects
let responseId = 1;
let updateFields = {
message: "new message",
asking_fee: 10,
date: new Date()
};
const site = await Site.findOneAndUpdate(
{ "campaigns.responses._id": responseId },
[{
$set: {
campaigns: {
$map: {
input: "$campaigns",
in: {
$mergeObjects: [
"$$this",
{
responses: {
$map: {
input: "$$this.responses",
in: {
$mergeObjects: [
"$$this",
{
$cond: [
{ $eq: ["$$this._id", responseId] },
updateFields,
"$$this"
]
}
]
}
}
}
}
]
}
}
}
}
}]
)
Playground
Second option if you have campaigns.campaign id then you can use $[<identifier>] arrayFilters,
let campaign = 1;
let responseId = 1;
let updateFields = {
message: "new message",
asking_fee: 10,
date: new Date()
};
const site = await Site.findOneAndUpdate({
"campaigns.campaign": campaign,
"campaigns.responses._id": responseId
},
{
$set: {
"campaigns.$[parent].responses.$[child]": updateFields
}
},
{
arrayFilters: [
{ "child._id": responseId },
{ "parent.campaign": campaign }
]
})
Playground

How to create multiple groups from a single selection in Mongoose?

I would like to select all events with a certain type from an events collection and then return 2 different groups using a single selection.
For example I currently have the following 2 selections:
const sessions = await Event.aggregate([
{
$match: {
isAdmin: { $ne: true }
}
}, {
$group: {
_id: '$sessionId'
}
}
]);
const users = await Event.aggregate([
{
$match: {
isAdmin: { $ne: true }
}
}, {
$group: {
_id: '$userId'
}
}
]);
I would like to achieve an end result of:
{
numberOfSessions: sessions.length,
numberOfUsers: users.length
}
By using a single query.
Thanks in advance!
You could use facet aggregation pipeline which will provide the capability to create multi-dimensions data within a single stage. For Eg:
const sessions = await Event.aggregate([
{
$match: {
isAdmin: { $ne: true }
}
}, {
$facet: {
sessions: [{
$sortByCount: "$sessionId"
}],
users: [{
$sortByCount: "$userId"
}]
}
}
]);