Mongoose find and update nested document - mongodb

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

Related

Deleting an item with condition in 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

$in operator not working in MongoDB Aggregation

I'm trying to make a discover page for a social media website. The discover page queries the database for all posts that satisfy four things:
User has not already liked post
Post tags do not violate user's filtered tag content
Post text content does not violate user's filtered post content
And finally the part of the aggregation giving me trouble:
Post tagIds contain a given tagId from user (a post using the same tag that the user already follows)
Here's the function:
const asyncFetchTagPosts = async (
query,
//here's a given tag that a user already follows
tagId,
likedPostIds,
Post,
User,
mongoose,
handleFilterTagRegex,
handleFilterPostContentRegex
) => {
var recastTagId = mongoose.Types.ObjectId(tagId)
var user = await User.findOne({ blogName: query })
var filteredTagRegex = handleFilterTagRegex(user)
var filteredPostContentRegex = handleFilterPostContentRegex(user)
var posts = await Post.aggregate([
{
$lookup: {
from: 'posts',
let: {
likedPostIds: likedPostIds,
tagId: recastTagId,
filteredTagRegex: filteredTagRegex,
filteredPostContentRegex: filteredPostContentRegex
},
pipeline: [
{
$match: {
$expr: {
$and: [
{ $not: { $in: ["$_id", "$$likedPostIds"] } },
{ $not: [
{
$regexMatch: {
input: "$tagTitles",
regex: "$$filteredTagRegex"
}
}
]
},
{ $not: [
{
$regexMatch: {
input: "$allText",
regex: "$$filteredPostContentRegex"
}
}
]
},
{ $or: [
//here's the bad expression, $tagIds won't resolve to an array
{ $in: [ "$$tagId", "$tagIds" ] },
]
}
]
}
}
}
],
as: 'posts'
}
},
{ $unwind: '$posts' },
{ $replaceRoot: { "newRoot": "$posts" } },
{ $sort: { "notesHeatLastTwoDays": -1 } },
{ $limit: 5 }
])
return posts
}
Here's the Post model:
import mongoose from 'mongoose';
const Schema = mongoose.Schema;
const options = { discriminatorKey: 'kind' }
const PostSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'User'
},
allText: {
type: String
},
descriptions: [
{
kind: String,
content: String,
displayIdx: Number
}
],
descriptionImages: [
{
type: Schema.Types.ObjectId,
ref: 'Image'
}
],
tagIds: [
{
type: Schema.Types.ObjectId,
ref: 'Tag'
}
],
tagTitles: {
type: String
},
mentions: [
{
type: Schema.Types.ObjectId,
ref: 'Mention'
}
],
notesCount: {
type: Number,
default: 0
},
notesHeatLastTwoDays: {
type: Number,
default: 0
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
},
kind: {
type: String,
default: 'Post'
}
}, options)
const Post = mongoose.model('Post', PostSchema, 'posts')
export default Post;
I keep getting this error:
Error: $in requires an array as a second argument, found: missing
When I comment out the last part of the query the aggregation works. It returns data in this shape:
{
_id: 60c18ee43730198901cfae9b,
descriptionImages: [],
//here's the array I'm trying to get to resolve in the aggregation
tagIds: [],
mentions: [],
notesCount: 1,
notesHeatLastTwoDays: 0,
kind: 'VideoPost',
descriptions: [],
createdAt: 2021-06-10T04:02:44.744Z,
updatedAt: 2021-06-11T08:51:38.166Z,
user: 608f213bb4a094bd91e02936,
videoLink: 60c3241a6c9ed4d1fc908270,
allText: '',
__v: 1,
tagTitles: ''
},
I thought using the $ operator in the aggregation gave me access to each document, does it just not work if you try to use the variable as the first expression?
you need to handle missing "$tagIds" by setting it to empty array []
{
$ifNull: [
"$tagIds",
[]
]
}
https://docs.mongodb.com/manual/reference/operator/aggregation/ifNull/
so you pipeline stage would be
{ $or: [
//here's the bad expression, $tagIds won't resolve to an array
{ $in: [ "$$tagId", { $ifNull: [ "$tagIds", [] ] } ] },
]
}

mongoose find and update removes the other fields

I have schema like this:
this.schema = new Schema({
userEmail: String
environments: [
{
envId: String,
appPreference: String,
language: String,
timeZone: String,
summaryNotificationSchedule: {
timeOfTheDay: String
}
}
]
});
Update request:
{
"envId": "u2",
"appPreference": "put2",
"timeZone": "gmt",
"summaryNotificationSchedule.timeOfTheDay": "32400",
}
As you can see, I am not sending "language": "abc", in the update request and in the result I see the language field is removed. I want to update the fields but not remove the other fields
Mongoose find and update call:
await this.model.findOneAndUpdate({ userEmail, 'environments.envId': envId }, { $set: { 'environments.$': setPreferenceFields } }, { new: true });
You can create update object from your request first:
let request = {
"envId": "u2",
"appPreference": "put2",
"timeZone": "gmt",
"summaryNotificationSchedule.timeOfTheDay": "32400",
};
let update = Object.keys(request).reduce((acc, cur) => {
acc[`environments.$.${cur}`] = request[cur];
return acc;
}, {})
console.log(update);
Then pass it to the update:
await this.model.findOneAndUpdate({ userEmail, 'environments.envId': envId }, { $set: update }, { new: true });
You have to specify property with parent key name of an array, it should be like this way,
await this.model.findOneAndUpdate(
{
userEmail,
'environments.envId': envId
},
{
$set: {
'environments.$.envId': "u2",
'environments.$.appPreference': "put2",
'environments.$.timeZone': "gmt",
'environments.$.summaryNotificationSchedule.timeOfTheDay': "32400"
}
},
{ new: true }
)
Another option, update with aggregation pipeline start from MongoDB v4.2, this little lengthy process then above method,
$map to iterate loop of environments array
$cond check condition if envId is equal to matching envId then merge objects update objects and current objects using $mergeObjects otherwise return current object
await this.model.findOneAndUpdate(
{ userEmail },
[
{
$set: {
environments: {
$map: {
input: "$environments",
in: {
$cond: [
{$eq: ["$$this.envId", envId]}, // add update id
{
$mergeObjects: [
"$$this",
setPreferenceFields // your update fields
]
},
"$$this"
]
}
}
}
}
}
],
{new: true}
)

Problem using aggregation in mongodb retrieving data from two collections

i am strugling with a query that i don't know how to perform... I have two collections,
Tarifas Collection
tarifaConfig = new Schema({
producto: { type: String },
titulo: { type: String },
bloqueo: { type: Boolean },
margen: { type: Number },
precioVenta: { type: Number },
precioVentaIva: { type: Number },
})
const tarifaSchema = new Schema({
codigo: { type: String },
titulo: { type: String },
margen: { type: Number },
estado: { type: Boolean },
bloqueo: { type: Boolean },
configs: [tarifaConfig]
})
Producto Collection
const productosSchema = new Schema({
ref: { type: String },
nombre: { type: String },
precioCompra: { type: Number },
precioCompraIva: { type: Number },
precioVenta: { type: Number },
precioVentaIva: { type: Number },
iva: { type: Number },
})
Now i am using an Aggregation method to retrieve both collection in a response
productosModel.aggregate([
{
$match: { _id: ObjectId(req.params.id) }
},
{
$lookup: {
from: "tarifas",
as: "tarifas",
pipeline: []
}
}
]).then((producto) => {
res.json(producto);
})
This is working and gives me both collections in the response... but..
In tarifa's collection i have a propertie called 'configs' that is an array with lot of sub collections... this sub collections are a config of each product that i have,
So what i need to do is, retrieve all tarifas that has a configs for the product, and if the configs does not contain retrieve the tarifa with a empty array.
Expected result
{
ref: 'rbe34',
nombre: 'bike',
precioCompra: 10,
precioCompraIva: 12.1,
precioVenta: "",
precioVentaIva: "",
iva: 21,
tarifas:[
{
codigo: 'NOR',
titulo: 'Normal tarifa',
margen: 33,
estado: true,
bloqueo: true,
configs: [], ///HERE I NEED A EMPTY ARRAY IF THERE IS NOT ANY CONFIG THAT MATCH WITH THE PRODUCT ID,
}
]
}
i tried to add $match in my aggregation pipeline.
productosModel.aggregate([
{
$match: { _id: ObjectId(req.params.id) }
},
{
$lookup: {
from: "tarifas",
as: "tarifas",
pipeline: [
{ $match: { 'configs.producto': req.params.id } }
]
}
}
])
But if there is not any config that match the product it doesn't retrieve the rest of Tarifa's collection
It seems you are trying to $filter the array after you retrieve it.
This pipeline will return only the configs for which the producto field from the config matches the ref field from the product.
[
{
$match: { _id: ObjectId(req.params.id) }
},
{
$lookup: {
from: "tarifas",
as: "tarifas",
pipeline: [
{
$addFields: {
"tarifas.configs":{ $filter:{
input: "$tarifas.configs",
cond: {$eq:["$$this.producto","$ref"]}
} }
}
}
]
}
},
]
Change the fields in the $eq array to the ones you need to match.

Mongoose: Filter doc and manipulate nested array

I have an image schema that has a reference to a category schema and a nested array that contains an object with two fields (user, createdAt)
I am trying to query the schema by a category and add two custom fields to each image in my query.
Here is the solution with virtual fields:
totalLikes: Count of all nested attributes
schema.virtual("totalLikes").get(function() {
return this.likes.length;
});
canLike: Check if user with id "5c8f9e676ed4356b1de3eaa1" is included in the nested array. If user is included it should return false otherwise true
schema.virtual("canLike").get(function() {
return !this.likes.find(like => {
return like.user === "5c8f9e676ed4356b1de3eaa1";
});
});
In sql it would be a simple SUBQUERY but I can't get it working in Mongoose.
Schema:
import mongoose from "mongoose";
const model = new mongoose.Schema(
{
category: {
type: mongoose.Schema.Types.ObjectId,
ref: "Category"
},
likes: [{
user: {
type: String,
required: true
},
createdAt: {
type: Date,
required: true
}
}]
})
here is a sample document:
[{
category:5c90a0777952597cda9e9c8d,
likes: [
{
_id: "5c90a4c79906507dac54e764",
user: "5c8f9e676ed4356b1de3eaa1",
createdAt:"2019-03-19T08:13:59.250+00:00"
}
]
},
{
category:5c90a0777952597cda9e9c8d,
likes: [
{
_id: "5c90a4c79906507dac54e764",
user: "5c8f9e676ed4356b1dw223332",
createdAt:"2019-03-19T08:13:59.250+00:00"
},
{
_id: "5c90a4c79906507dac54e764",
user: "5c8f9e676ed4356b1d8498933",
createdAt:"2019-03-19T08:13:59.250+00:00"
}
]
}]
Here is how it should look like:
[{
category:5c90a0777952597cda9e9c8d,
likes: [
{
_id: "5c90a4c79906507dac54e764",
user: "5c8f9e676ed4356b1de3eaa1",
createdAt:"2019-03-19T08:13:59.250+00:00"
}
],
totalLikes: 1,
canLike: false
},
{
category:5c90a0777952597cda9e9c8d,
likes: [
{
_id: "5c90a4c79906507dac54e764",
user: "5c8f9e676ed4356b1dw223332",
createdAt:"2019-03-19T08:13:59.250+00:00"
},
{
_id: "5c90a4c79906507dac54e764",
user: "5c8f9e676ed4356b1d8498933",
createdAt:"2019-03-19T08:13:59.250+00:00"
}
],
totalLikes: 2,
canLike: true
}]
Here is what I tried:
Resolver:
1) Tried in Mongoose call - Fails
const resources = await model.aggregate([
{ $match: {category: "5c90a0777952597cda9e9c8d"},
$addFields: {
totalLikes: {
$size: {
$filter: {
input: "$likes",
as: "el",
cond: "$$el.user"
}
}
}
},
$addFields: {
canLike: {
$match: {
'likes.user':"5c8f9e676ed4356b1de3eaa1"
}
}
}
}
])
2) Tried to change it after db call - works but not preferred solution
model.where({ competition: "5c90a0777952597cda9e9c8d" }).exec(function (err, records) {
resources = records.map(resource => {
resource.likes = resource.likes ? resource.likes: []
const included = resource.likes.find(like => {
return like.user === "5c8f9e676ed4356b1de3eaa1";
});
resource.set('totalLikes', resource.likes.length, {strict: false});
resource.set('canLike', !included, {strict: false});
return resource
});
})
Does anyone know how I can do it at runtime? THX
you can achieve it using aggregate
Model.aggregate()
.addFields({ // map likes so that it can result to array of ids
likesMap: {
$map: {
input: "$likes",
as: "like",
in: "$$like.user"
}
}
})
.addFields({ // check if the id is present in likesMap
canLike: {
$cond: [
{
$in: ["5c8f9e676ed4356b1de3eaa1", "$likesMap"]
},
true,
false
]
},
totalLikes: {
$size: "$likes"
}
})
.project({ // remove likesMap
likesMap: 0,
})