MongoDB aggregation search in objects of array [nodejs, mongoose] - node.js

I'm getting from the client side an filter object like:
{
appId: "01",
items: [ '60522e84feecf7036fa11831', '60522c47feecf7036fa1182d' ],
//offset limit
}
my query is:
await someCollection.aggregate([
{ $match: query },
{
$group: {//some fields}
},
])
.sort({date: -1})
.skip(+req.query.offset)
.limit(+req.query.limit)
collection is:
[
{
"_id": 1,
"shop": 1,
"appId": "01",
"items": [
{
"itemId": "777"
},
{
"itemId": "666"
},
]
},
{
"_id": 2,
"shop": 2,
"appId": "01",
"items": [
{
"itemId": "666"
},
{
"itemId": "123"
},
]
},
{
"_id": 3,
"shop": 2,
"appId": "01",
"items": [
{
"itemId": "x"
},
]
}
]
on my Backend query generates dynamically:
const query = {
'$expr':{
'$and':[
{'$eq': ['$appId', req.user.appId.toString()]},
]
}
}
If coming query have a products array I need to search id's in the objects array.
for example: ['777', 'x'] as result need to have 2 items where "_id": 1 and "_id": 3
my code is:
if(req.query.products) {
typeof req.query.products === 'string' ? req.query.products = [req.query.products] : req.query.products
let bb = req.query.products.map(function(el) { return mongoose.Types.ObjectId(el) })
query['$expr']['$and'].push({
$or: [{
$eq: ['$items.itemId', bb]
}],
}
}
mongoplayground
so, I need to use $in operator with $match & $and dynamically, but I have no idea how

I would try it like this:
const query = { ['$or']: [] }
for (let k of Object.keys(req.user)) {
if (Array.isArray(req.user[k])) {
for (let i in req.user[k])
query['$or'].push({ [`${k}.itemId`]: mongoose.Types.ObjectId(i) });
} else {
query[k] = req.user[k].toString();
}
}
await someCollection.aggregate([
{ $match: query },
{
$group: {//some fields}
},
])

Related

Node js mongoose filter data from array in collection

I am having a response like this
....................................................................................................................................................................................................
{
"data": [
{
"user": "83k13bde05f40640j12075w",
"products": [
{
"type": "shoes",
"amount": 20
},
{
"type": "trousers",
"amount": 6
}
],
"inStock": false
},
{
"user": "9dc3f7de05f40640j12075y",
"products": [
{
"type": "chairs",
"amount": 11
},
{
"type": "bags",
"amount": 16
}
],
"inStock": false
},
{
"user": "6wb3f7ne35f40640m62p2gd",
"products": [
{
"type": "phones",
"amount": 2
},
{
"type": "clothes",
"amount": 15
}
],
"inStock": false
}
]
}
This the function outputting the above response
exports.getProducts = async (req,res) => {
const result = await Products
.find({inStock: false})
.select("-_id -createdAt -__v")
.exec()
if(!result) return res.status(400).json({ data: 'No product found' });
if(result.err) return res.json({ err: err });
return res.json({data: result});
}
But I want to get only products with the amount greater than 10
So my output should be like this
{
"data": [
{
"user": "83k13bde05f40640j12075w",
"products": [
{
"type": "shoes",
"amount": 20
}
],
"inStock": false
},
{
"user": "9dc3f7de05f40640j12075y",
"products": [
{
"type": "chairs",
"amount": 11
},
{
"type": "bags",
"amount": 16
}
],
"inStock": false
},
{
"user": "6wb3f7ne35f40640m62p2gd",
"products": [
{
"type": "clothes",
"amount": 15
}
],
"inStock": false
}
]
}
I tried using
.find({'products.amount': { $gt: 10 }})
But It didn't filter out the response
Did you try the $elemMatch operator ?
const result = await Products
.find({
inStock: false,
products: { $elemMatch: { amount: { $gt: 10 } } }
})
.select("-_id -createdAt -__v")
.exec();
You can use aggregation to achieve this.
First of all you use $match operator to find the documents with items in the array that match with your criteria. Then you can use $project and $filter operator to return the array filtered.
const result = await Products.aggregate([
{
"$match" : {
"products" : {
"$elemMatch" : { amount: { $gt: 10 } }
},
}
},
{
$project: {
user: 1,
inStock: 1,
products: {
$filter: {
input: "$products",
as: "products",
cond: { $gt: ["$products.amount", 10] }
}
}
}
}
])
.select("-_id -createdAt -__v")
.exec();
For further reading: https://studio3t.com/knowledge-base/articles/filter-elements-from-mongodb-arrays/#how-to-use-filter-and-project

How to update an array in mongodb by using another array as both identifier and value?

Imagine the is a document like this:
{
_id: ObjectID('someIdblahbla')
users: [
{
_id: 'id1',
name: 'name1',
},
{
_id: 'id2',
name: 'name2',
},
{
_id: 'id3',
name: 'name3'
}
]
}
I have an array like this:
const newData = [
{_id: 'id1', name: 'newName1'},
{_id: 'id2', 'name': 'newName2', family:'newFamily2'}
]
what I want is to update the array in the document using the corresponding _id and using it to add/update each element.
so my end result would be like:
{
_id: ObjectID('someIdblahbla')
users: [
{
_id: 'id1',
name: 'newName1',
},
{
_id: 'id2',
name: 'newName2',
family:'newFamily2'
},
{
_id: 'id3',
name: 'name3'
}
]
}
my guess was using The filtered positional operator but I am not sure if it's the correct way to go and how to do it.
thank you for your kind tips beforehand.
There is no straight way to add/update in array, you can use update with aggregation pipeline starting from MongoDB 4.2,
First of all, you need to convert _id from string to objectId type, if you are using mongoose npm you can use mongoose.Types.ObjectId method or if you are using mongodb npm you can use ObjectId method,
let newData = [
{ _id: 'id1', name: 'newName1' },
{ _id: 'id2', 'name': 'newName2', family:'newFamily2' }
];
let newIds = [];
newData = newData.map(n => {
n._id = ObjectId(n._id); // or mongoose.Types.ObjectId(n._id)
newIds.push(n._id); // for checking conditions
return n;
});
You can put query condition, and do below operations,
$map to iterate loop of users array, check condition if user._id is in input newIds then do update operation otherwise do insert operation
update operation:
$filter to iterate loop of input newData and filter already present object from input so we can update it
$arrayElemAt to get first object from above filtered array
$mergeObjects to merge current object with above input object
insert operation:
$filter to iterate loop of newData array and return not present object means new items in array of objects
$concatArrays to concat above new and updated result array
db.collection.updateOne(
{ _id: ObjectId("someIdblahbla") },
[{
$set: {
users: {
$concatArrays: [
{
$map: {
input: "$users",
as: "u",
in: {
$cond: [
{ $in: ["$$u._id", newIds] },
{
$mergeObjects: [
"$$u",
{
$arrayElemAt: [
{
$filter: {
input: newData,
cond: { $eq: ["$$this._id", "$$u._id"] }
}
},
0
]
}
]
},
"$$u"
]
}
}
},
{
$filter: {
input: newData,
cond: { $not: { $in: ["$$this._id", "$users._id"] } }
}
}
]
}
}
}]
)
Playground
Query1 (update(merge objects) existing members, doesn't add new members)
Test code here
Replace
[{"_id": "id1","name": "newName1"},{"_id": "id2","name": "newName2","family": "newFamily2"}] with you array or the driver variable that hold the array
db.collection.update({
"_id": {
"$eq": "1"
}
},
[
{
"$addFields": {
"users": {
"$map": {
"input": "$users",
"as": "user",
"in": {
"$reduce": {
"input": [
{
"_id": "id1",
"name": "newName1"
},
{
"_id": "id2",
"name": "newName2",
"family": "newFamily2"
}
],
"initialValue": "$$user",
"in": {
"$let": {
"vars": {
"old_user": "$$value",
"new_user": "$$this"
},
"in": {
"$cond": [
{
"$eq": [
"$$old_user._id",
"$$new_user._id"
]
},
{
"$mergeObjects": [
"$$old_user",
"$$new_user"
]
},
"$$old_user"
]
}
}
}
}
}
}
}
}
}
])
Query2 (update(merge) if found, else push in the end)
Its like the above but finds the not-existing members,and push them in the end.Its a bit more slower and complicated
Test code here
Replace
[{"_id": "id1","name": "newName1"},{"_id": "id2","name": "newName2","family": "newFamily2"},{"_id": "id4","name": "newName4"}]
with your array or the driver variable that hold the array
db.collection.update({
"_id": {
"$eq": "1"
}
},
[
{
"$addFields": {
"yourarray": [
{
"_id": "id1",
"name": "newName1"
},
{
"_id": "id2",
"name": "newName2",
"family": "newFamily2"
},
{
"_id": "id4",
"name": "newName4"
}
]
}
},
{
"$addFields": {
"new-ids": {
"$setDifference": [
{
"$map": {
"input": "$yourarray",
"as": "u",
"in": "$$u._id"
}
},
{
"$map": {
"input": "$users",
"as": "u",
"in": "$$u._id"
}
}
]
}
}
},
{
"$addFields": {
"users": {
"$concatArrays": [
{
"$map": {
"input": "$users",
"as": "user",
"in": {
"$reduce": {
"input": "$yourarray",
"initialValue": "$$user",
"in": {
"$let": {
"vars": {
"old_user": "$$value",
"new_user": "$$this"
},
"in": {
"$cond": [
{
"$eq": [
"$$old_user._id",
"$$new_user._id"
]
},
{
"$mergeObjects": [
"$$old_user",
"$$new_user"
]
},
"$$old_user"
]
}
}
}
}
}
}
},
{
"$filter": {
"input": "$yourarray",
"as": "u",
"cond": {
"$in": [
"$$u._id",
"$new-ids"
]
}
}
}
]
}
}
},
{
"$unset": [
"yourarray",
"new-ids"
]
}
])

Group by the content of array of string with out order in mongo aggregation

i have a problem with aggregation framework in MongoDB (mongoose) this is the problem. i have the following database scheme.so what i want to do is count number of people who has access through Mobile only , Card only, or both. with out any order,
{
'_id': ObjectId,
'user_access_type': ['Mobile' , 'Card']
}
{
'_id': ObjectId,
'user_access_type': ['Card' , 'Mobile']
}
{
'_id': ObjectId,
'user_access_type': ['Mobile']
}
{
'_id': ObjectId,
'user_access_type': ['Card']
}
Now i am using this but it only groups by the order of the user_access_type array,
[ { "$group" : { "_id": {"User" : "$user_access_type"} , "count": {"$sum" : 1} }]
this is the output:
{
"_id": {
"User": [
"Card",
"Mobile"
]
},
"count": 1
},
{
"_id": {
"_id": "5f7dce2359aaf004985f98eb",
"User": [
"Mobile",
"Card"
]
},
"count": 1
},
{
"_id": {
"User": [
"Mobile"
]
},
"count": 1
},
{
"_id": {
"User": [
"Card"
]
},
"count": 1
},
vs what i want:
{
"_id": {
"User": [
"Card",
"Mobile" // we can say both
]
},
"count": 2 // does not depend on order
},
{
"_id": {
"User": [
"Mobile"
]
},
"count": 1
},
{
"_id": {
"User": [
"Card"
]
},
"count": 1
},
You can use other option as well using $function,
$function can allow to add javascript code, you can use sort() to sort the array
db.collection.aggregate([
{
$addFields: {
user_access_type: {
$function: {
body: function(user_access_type){
return user_access_type.sort();
},
args: ["$user_access_type"],
lang: "js"
}
}
}
},
{
$group: {
_id: "$user_access_type",
count: { $sum: 1 }
}
}
])
Second option,
If user_access_type array having always unique elements then you can use $setUnion operator on user_access_type array as self union, some how this will re-order array in same order,
db.collection.aggregate([
{
$addFields: {
user_access_type: {
$setUnion: "$user_access_type"
}
}
},
{
$group: {
_id: "$user_access_type",
count: { $sum: 1 }
}
}
])
Playground

Populate + Aggregate on Array

I am using mongoose (5.x.x) with the populate function to populate an array (meals) with refs.
Within that array I need to multiply a price (result of the populate) and a quantity (part of the base Schema).
My populate result as below:
{
"_id": "5bea354235711482876f8fa8",
"meals": [
{
"meal": {
"_id": "5be93c7074488c77b10fba00",
"name": "Chicken Nuggets",
"price": 3
},
"quantity": 12
},
{
"meal": {
"_id": "5be93c9274488c77b10fba01",
"name": "Beef Burger",
"price": 6
},
"quantity": 4
}
],
"__v": 0
}
The goal would be to add a "total price" within this result set but I can't find any elegant way to do so.
I'd like to avoid manipulating data outside of the query.
Thanks for your help,
So there's a couple of ways to do this as mentioned.
Using $lookup
You basically want to get the "related" data from the other collection and "merge" that with the existing array items. You cannot actually just "target" the existing array since $lookup cannot do that, but it can write another array and then you can "merge" them together:
let result1 = await Order.aggregate([
{ "$lookup": {
"from": Meal.collection.name,
"foreignField": "_id",
"localField": "meals.meal",
"as": "mealitems"
}},
{ "$project": {
"meals": {
"$map": {
"input": "$meals",
"in": {
"meal": {
"$arrayElemAt": [
"$mealitems",
{ "$indexOfArray": [ "$mealitems._id", "$$this.meal" ] }
]
},
"quantity": "$$this.quantity",
"totalPrice": {
"$multiply": [
{ "$arrayElemAt": [
"$mealitems.price",
{ "$indexOfArray": [ "$mealitems._id", "$$this.meal" ] }
]},
"$$this.quantity"
]
}
}
}
}
}},
{ "$addFields": {
"totalOrder": {
"$sum": "$meals.totalPrice"
}
}}
]);
That basically produces another array "mealitems" as the result of $lookup and then uses $map in order to process through the original document array and transpose the returned content array items back into the structure for each item.
You do that in combination with $arrayElemAt and $indexOfArray to find the matched items to transpose here.
There is also some "math" for the other computed elements using $multiply, and even an additional $addFields stage using $sum to "add those up" to give an overall "order total" for the document.
You "could" just do all that math in the $project stage ( which is used because we don't want the "mealitems" content. But that's a little more involved and you probably want to use $let for the array matching so you don't repeat your code so much.
You can even use the "sub-pipeline" form of $lookup if you really want to. Instead of using $map as the operations to alter the returned documents are done "inside" the returned array before the results are returned, by transposing the initial document array into the result documents via it's let argument:
// Aggregate with $lookup - sub-pipeline
let result2 = await Order.aggregate([
{ "$lookup": {
"from": Meal.collection.name,
"let": { "meals": "$meals" },
"pipeline": [
{ "$match": {
"$expr": {
"$in": [ "$_id", "$$meals.meal" ]
}
}},
{ "$replaceRoot": {
"newRoot": {
"meal": "$$ROOT",
"quantity": {
"$arrayElemAt": [
"$$meals.quantity",
{ "$indexOfArray": [ "$$meals.meal", "$_id" ] }
]
},
"totalPrice": {
"$multiply": [
{ "$arrayElemAt": [
"$$meals.quantity",
{ "$indexOfArray": [ "$$meals.meal", "$_id" ] }
]},
"$price"
]
}
}
}}
],
"as": "meals"
}},
{ "$addFields": {
"totalOrder": {
"$sum": "$meals.totalPrice"
}
}}
]);
In either form, that's basically an allegory for what populate() is doing under the hood by "merging" the content, but of course that uses separate database requests where the $lookup aggregation is just one request.
Using populate()
Alternately you can just manipulate the resulting structure in JavaScript. It's already there, and all you really need is the lean() in order to be able to alter the resulting objects:
// Populate and manipulate
let result3 = await Order.find().populate('meals.meal').lean();
result3 = result3.map(r =>
({
...r,
meals: r.meals.map( m =>
({
...m,
totalPrice: m.meal.price * m.quantity
})
),
totalOrder: r.meals.reduce((o, m) =>
o + (m.meal.price * m.quantity), 0
)
})
);
It looks pretty simple and is basically the same thing, with the exceptions that the "merging" was already done for you and that of course this is two requests to the server in order to return all the data.
As a reproducible full listing:
const { Schema } = mongoose = require('mongoose');
// Connection
const uri = 'mongodb://localhost:27017/menu';
const opts = { useNewUrlParser: true };
// Sensible defaults
mongoose.Promise = global.Promise;
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
mongoose.set('debug', true);
// Schema defs
const mealSchema = new Schema({
name: String,
price: Number
});
const orderSchema = new Schema({
meals: [
{
meal: { type: Schema.Types.ObjectId, ref: 'Meal' },
quantity: Number
}
]
});
const Meal = mongoose.model('Meal', mealSchema);
const Order = mongoose.model('Order', orderSchema);
// log helper
const log = data => console.log(JSON.stringify(data, undefined, 2));
// main
(async function() {
try {
const conn = await mongoose.connect(uri, opts);
// clean models
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany())
);
// Set up data
let [Chicken, Beef] = await Meal.insertMany(
[
{ name: "Chicken Nuggets", price: 3 },
{ name: "Beef Burger", price: 6 }
]
);
let order = await Order.create({
meals: [
{ meal: Chicken, quantity: 12 },
{ meal: Beef, quantity: 4 }
]
});
// Aggregate with $lookup - traditional
let result1 = await Order.aggregate([
{ "$lookup": {
"from": Meal.collection.name,
"foreignField": "_id",
"localField": "meals.meal",
"as": "mealitems"
}},
{ "$project": {
"meals": {
"$map": {
"input": "$meals",
"in": {
"meal": {
"$arrayElemAt": [
"$mealitems",
{ "$indexOfArray": [ "$mealitems._id", "$$this.meal" ] }
]
},
"quantity": "$$this.quantity",
"totalPrice": {
"$multiply": [
{ "$arrayElemAt": [
"$mealitems.price",
{ "$indexOfArray": [ "$mealitems._id", "$$this.meal" ] }
]},
"$$this.quantity"
]
}
}
}
}
}},
{ "$addFields": {
"totalOrder": {
"$sum": "$meals.totalPrice"
}
}}
]);
log(result1);
// Aggregate with $lookup - sub-pipeline
let result2 = await Order.aggregate([
{ "$lookup": {
"from": Meal.collection.name,
"let": { "meals": "$meals" },
"pipeline": [
{ "$match": {
"$expr": {
"$in": [ "$_id", "$$meals.meal" ]
}
}},
{ "$replaceRoot": {
"newRoot": {
"meal": "$$ROOT",
"quantity": {
"$arrayElemAt": [
"$$meals.quantity",
{ "$indexOfArray": [ "$$meals.meal", "$_id" ] }
]
},
"totalPrice": {
"$multiply": [
{ "$arrayElemAt": [
"$$meals.quantity",
{ "$indexOfArray": [ "$$meals.meal", "$_id" ] }
]},
"$price"
]
}
}
}}
],
"as": "meals"
}},
{ "$addFields": {
"totalOrder": {
"$sum": "$meals.totalPrice"
}
}}
]);
log(result2);
// Populate and manipulate
let result3 = await Order.find().populate('meals.meal').lean();
result3 = result3.map(r =>
({
...r,
meals: r.meals.map( m =>
({
...m,
totalPrice: m.meal.price * m.quantity
})
),
totalOrder: r.meals.reduce((o, m) =>
o + (m.meal.price * m.quantity), 0
)
})
);
log(result3);
} catch(e) {
console.error(e);
} finally {
mongoose.disconnect();
}
})()
Which returns results like:
Mongoose: meals.deleteMany({}, {})
Mongoose: orders.deleteMany({}, {})
Mongoose: meals.insertMany([ { _id: 5bea4c8f6edcd22d385a13bf, name: 'Chicken Nuggets', price: 3, __v: 0 }, { _id: 5bea4c8f6edcd22d385a13c0, name: 'Beef Burger', price: 6, __v: 0 } ], {})
Mongoose: orders.insertOne({ _id: ObjectId("5bea4c8f6edcd22d385a13c1"), meals: [ { _id: ObjectId("5bea4c8f6edcd22d385a13c3"), meal: ObjectId("5bea4c8f6edcd22d385a13bf"), quantity: 12 }, { _id: ObjectId("5bea4c8f6edcd22d385a13c2"), meal: ObjectId("5bea4c8f6edcd22d385a13c0"), quantity: 4 } ], __v: 0 })
Mongoose: orders.aggregate([ { '$lookup': { from: 'meals', foreignField: '_id', localField: 'meals.meal', as: 'mealitems' } }, { '$project': { meals: { '$map': { input: '$meals', in: { meal: { '$arrayElemAt': [ '$mealitems', { '$indexOfArray': [ '$mealitems._id', '$$this.meal' ] } ] }, quantity: '$$this.quantity', totalPrice: { '$multiply': [ { '$arrayElemAt': [ '$mealitems.price', { '$indexOfArray': [Array] } ] }, '$$this.quantity' ] } } } } } }, { '$addFields': { totalOrder: { '$sum': '$meals.totalPrice' } } } ], {})
[
{
"_id": "5bea4c8f6edcd22d385a13c1",
"meals": [
{
"meal": {
"_id": "5bea4c8f6edcd22d385a13bf",
"name": "Chicken Nuggets",
"price": 3,
"__v": 0
},
"quantity": 12,
"totalPrice": 36
},
{
"meal": {
"_id": "5bea4c8f6edcd22d385a13c0",
"name": "Beef Burger",
"price": 6,
"__v": 0
},
"quantity": 4,
"totalPrice": 24
}
],
"totalOrder": 60
}
]
Mongoose: orders.aggregate([ { '$lookup': { from: 'meals', let: { meals: '$meals' }, pipeline: [ { '$match': { '$expr': { '$in': [ '$_id', '$$meals.meal' ] } } }, { '$replaceRoot': { newRoot: { meal: '$$ROOT', quantity: { '$arrayElemAt': [ '$$meals.quantity', { '$indexOfArray': [ '$$meals.meal', '$_id' ] } ] }, totalPrice: { '$multiply': [ { '$arrayElemAt': [ '$$meals.quantity', [Object] ] }, '$price' ] } } } } ], as: 'meals' } }, { '$addFields': { totalOrder: { '$sum': '$meals.totalPrice' } } } ], {})
[
{
"_id": "5bea4c8f6edcd22d385a13c1",
"meals": [
{
"meal": {
"_id": "5bea4c8f6edcd22d385a13bf",
"name": "Chicken Nuggets",
"price": 3,
"__v": 0
},
"quantity": 12,
"totalPrice": 36
},
{
"meal": {
"_id": "5bea4c8f6edcd22d385a13c0",
"name": "Beef Burger",
"price": 6,
"__v": 0
},
"quantity": 4,
"totalPrice": 24
}
],
"__v": 0,
"totalOrder": 60
}
]
Mongoose: orders.find({}, { projection: {} })
Mongoose: meals.find({ _id: { '$in': [ ObjectId("5bea4c8f6edcd22d385a13bf"), ObjectId("5bea4c8f6edcd22d385a13c0") ] } }, { projection: {} })
[
{
"_id": "5bea4c8f6edcd22d385a13c1",
"meals": [
{
"_id": "5bea4c8f6edcd22d385a13c3",
"meal": {
"_id": "5bea4c8f6edcd22d385a13bf",
"name": "Chicken Nuggets",
"price": 3,
"__v": 0
},
"quantity": 12,
"totalPrice": 36
},
{
"_id": "5bea4c8f6edcd22d385a13c2",
"meal": {
"_id": "5bea4c8f6edcd22d385a13c0",
"name": "Beef Burger",
"price": 6,
"__v": 0
},
"quantity": 4,
"totalPrice": 24
}
],
"__v": 0,
"totalOrder": 60
}
]

Pulling elements from mongodb array

I have a question for which I've wasted more time than I should have and I don't seem to get what I'm doing wrong.
I have the below document in MongoDB:
{
"personal": {
...
},
"preferences": {
....
},
"_id": "5b2efdad564191054807c2b1",
"pets": [],
"conversations": [
{
"unread": 1,
"participants": [
{
"_id": "5b2efdcd564191054807c2b2",
"name": "Mighty Jules"
}
],
"messages": [
{
"sender": "self",
"timestamp": "2018-06-24T12:29:50.656Z",
"_id": "5b2f8ebede342a12a8dcc9d2",
"text": "..."
},
{
"sender": "self",
"timestamp": "2018-06-24T12:29:58.022Z",
"_id": "5b2f8ec6de342a12a8dcc9d8",
"text": "..."
},
{
"sender": "5b2efdcd564191054807c2b2",
"timestamp": "2018-06-24T12:30:27.562Z",
"_id": "5b2f8ee3de342a12a8dcc9e5",
"text": "..."
},
{
"sender": "self",
"timestamp": "2018-06-24T12:32:48.034Z",
"_id": "5b2f8f70d3a83e25bc1abbb2",
"text": "..."
},
{
"sender": "self",
"timestamp": "2018-06-24T12:36:20.027Z",
"_id": "5b2f9044d4137828283c5a60",
"text": "..."
},
{
"sender": "5b2efdcd564191054807c2b2",
"timestamp": "2018-06-24T12:37:39.965Z",
"_id": "5b2f90939b4b2a4af8cf50db",
"text": "..."
}
],
"last_message": "2018-06-24T12:37:39.965Z",
"_id": "5b2efdcd564191054807c2b2"
},
{
"unread": 1,
"participants": [
{
"_id": "5b300ff657957c1aa0ed0576",
"name": "Super Frank"
}
],
"messages": [
{
"sender": "5b300ff657957c1aa0ed0576",
"timestamp": "2018-06-24T21:42:49.392Z",
"_id": "5b30105957957c1aa0ed0583",
"text": "..."
}
],
"last_message": "2018-06-24T21:42:49.392Z",
"_id": "5b300ff657957c1aa0ed0576"
}
],
"created_date": "2018-06-24T02:10:53.314Z",
"lastLogin_date": "2018-06-24T02:10:53.314Z",
"lastUpdate_date": "2018-06-25T02:09:53.281Z",
"__v": 0
}
I am trying to delete just a couple of messages using mongoose:
const user = await User.findOneAndUpdate(
{
_id: mongoose.Types.ObjectId("5b2efdad564191054807c2b1"), //Which is the one that doc
"conversations._id": mongoose.Types.ObjectId("5b2efdcd564191054807c2b2")
},
{
$pull: {
"conversations.$.messages": {
$in: [
{ _id: mongoose.Types.ObjectId("5b2f9044d4137828283c5a60") },
{ _id: mongoose.Types.ObjectId("5b2f90939b4b2a4af8cf50db") }
]
}
}
},
{
new: true,
projection: {
conversations: 1
}
}
);
In the response I get the same, nothing gets deleted, I get no errors.
First of all, _ids in the example document are strings, not ObjectId.
Secondly, the $pull syntax is wrong. Please read https://docs.mongodb.com/manual/reference/operator/update/pull/#remove-items-from-an-array-of-documents. It should be:
{
$pull: {
"conversations.$.messages": {
"_id": {
"$in": ["5b2f9044d4137828283c5a60", "5b2f90939b4b2a4af8cf50db"]
}
}
}
}
It will pull messages from the first matching conversation, if it is what you want. If you want to remove messages from all matching conversations, you need to use $[] instead: "conversations.$[].messages"
function exists(Arr, objId){
for(var i = 0; i < Arr.length; i++)
if(objId.equals(Arr[i]))
return true
return false
}
var user = await User.findOne(
{_id: mongoose.Types.ObjectId("5b2efdad564191054807c2b1")}
)
for(var j = 0; j<user.conversations.length; j++)
if(exists([conId],user.conversations[j]._id)
break
if(j<user.conversations.length)
for(var i =0; i < user.conversations[j].messages.length; i++)
if(exists(Arr, user.conversations.messages[i]._id))
delete user.conversations.messages[i]
user.save()
Edit: using only db operations
If you want to use $in then you will have to pass the whole sub array.
await User.update({_id : "5b2efdad564191054807c2b1", "conversations._id" : "5b2efdcd564191054807c2b2" }, { $pull: { "conversations.$.messages":{ $in : [{"sender" : "self", "timestamp" : "2018-06-24T12:32:48.034Z","_id" : "5b2f8f70d3a83e25bc1abbb2","text" : "..."}] } } }, {multi : true} )
something like that as $in compares the whole document to each element in the array.
In your case it does not find a match but still it succeeded in the process and hence no error.
if you want to do only by id you will have to loop through for each of your ids as follows
for(var i = 0; i < ids.length; i++)
await User.update({_id : "5b2efdad564191054807c2b1", "conversations._id" : "5b2efdcd564191054807c2b2" }, { $pull: { "conversations.$.messages": {_id : ids[i] } } }, {multi : true} )
or you know the _id field so use in to search for $id
await User.update({_id : "5b2efdad564191054807c2b1", "conversations._id" : "5b2efdcd564191054807c2b2" }, { $pull: { "conversations.$.messages":{ "_id" : { $in : ids } } } }, {multi : true} )

Resources