Mongo Query - match and lookup combined - javascript

I’ve defined the following query which fetches me all items with an id which is in a given list of ids, and a status of either active or retracted.
const query = {
$and : [
{
$or: [
{
status: ‘active’,
},
{
status: ‘retracted’,
},
],
},
{
id: { $in: ids },
},
],
};
Each of these items has a parent_id field, which can either be null if the item does not have a parent, or can be the id of the parent.
I want my query to fetch all items with the ids I supply, as well as their parent items, if such a parent exists.
For example, if I supply the following IDs
[1,2,3]
and item 2 has a parent with id 5, while item 1 and 3 have parent_id set to null, I want my query to return the following items:
[1,2,3,5].
To achieve this I wrote the following query:
const collection = db.collection(‘myCollection’);
const data = await collection.aggregate([
{$match : query},
{
$lookup: {
from: ‘myCollection’,
let: { parentID: ‘$parent_id’},
pipeline: [
{
$match: {
$expr: {
$eq: [‘$id’, ‘$$parentID’],
},
},
},
as: ‘parent’,
},
},
]).sort(‘created_date’, ‘desc’).toArray();
return data;
However, this always returns null.
Sample Data:
[
{
id: 1,
parent_id: 3,
data: ‘bla bla’
},
{
id: 2,
parent_id: null,
data: ‘bla bla bla’
},
{
id: 3,
parent_id: null,
data: ‘bla’
}
]
Input: [1]
Output:
[
{
id: 1,
parent_id: 3,
data: ‘bla bla’
},
{
id: 3,
parent_id: null,
data: ‘bla’
}
]

The approach with $lookup being run upon same collection should work however it gives you a nested array so you need few additional stages to flatten such array and get all elements as on result set:
db.collection.aggregate([
{
$match: { id: { $in: [1] } }
},
{
$lookup: {
from: "collection",
localField: "parent_id",
foreignField: "id",
as: "parent"
}
},
{
$project: {
all: {
$concatArrays: [
"$parent",
[ "$$ROOT" ]
]
}
}
},
{
$project: {
"all.parent": 0
}
},
{
$unwind: "$all"
},
{
$replaceRoot: {
newRoot: "$all"
}
}
])
Mongo Playground

Your aggregation was malformed and lack some "]" for example closing the pipeline fied.
If you fix that the query works fine for me. Example

You can try this. The input array is [2,3] where 2 has parent id=1 and that is not in the input array. But the output array has the entry.
Working Playground
db.collection.aggregate([
{
$match: {
_id: {
$in: [
2,
3
]
}
}
},
{
$lookup: {
from: "collection",
localField: "p",
foreignField: "_id",
as: "parent"
}
},
{
$project: {
_id: 0,
id: {
$concatArrays: [
[
"$_id"
],
"$parent._id"
]
}
}
},
{
$unwind: "$id"
},
{
$sort: {
id: 1
}
}
])

Related

Mongo Aggregation pipeline update or push

I have a MongoDB Model which consist of array of members as obejcts.
const guestSchema = new mongoose.Schema({
salutation: {
type: String,
},
members: [membersSchema],
user: {
type: mongoose.Schema.ObjectId,
ref: 'User',
},
});
Members Schema:
const membersSchema = new mongoose.Schema({
name: String,
status: {
type: String,
enum: ['regular', 'helper'],
default: 'regular',
},
});
I want to achieve of doing an update in case documet with given ID exist or push to an array in case ID with document in array does not exist. I use aggregation pipeline, however I am not able to achieve pushing new document to array. Why can't I use push after else statement like this.
const subDocumentToUpsert = { 'name': mem.name, 'status': mem.status, '_id': ObjectId(mem.id)}
const subDocumentNoID = { 'name': mem.name, 'status': mem.status}
await Guest.findOneAndUpdate(
{ "_id": req.params.id },
[
{
$set: {
members: {
$cond: {
if: { $in: [subDocumentToUpsert._id, '$members._id'] },
then: {
$map: {
input: '$members',
as: 'sub_document',
in: {
$cond: {
if: { $eq: ['$$sub_document._id', subDocumentToUpsert._id] },
then: subDocumentToUpsert,
else: '$$sub_document',
},
},
},
},
else: {
$push: {
subDocumentNoID
},
},
},
},
},
},
},
]);
What is the best way of doing so? Thank you
You can do as follow:
db.collection.update({
_id: {
$in: [
1,
2
]
}
},
[
{
$set: {
members: {
$cond: {
if: {
$in: [
5,
"$members._id"
]
},
then: {
$map: {
input: "$members",
as: "sub",
in: {
$cond: {
if: {
$eq: [
"$$sub._id",
5
]
},
then: {
_id: 5,
status: "regular_updated",
name: "Negan_updated"
},
else: "$$sub"
},
},
},
},
else: {
$concatArrays: [
"$members",
[
{
_id: 5,
status: "regular_upserted",
name: "Negan_upserted"
}
]
]
}
}
}
}
}
}
],
{
multi: true
})
Explained:
Check if _id:5 exist in the subobject and update via $map/$cond only the object that has the _id:5.
In case there is no _id:5 add the new object to the array with $concatArrays.
Playground

MongoDB - Aggregate lookup array field and match

With MongoDB aggregate, how do you lookup child documents from an array on the parent document and match a field on the child to a value?
I want to lookup the ShopItems for a Shop. And I only want the ShopItems that are not kidFriendly.
Details:
Let's say I have Shops collection
Shop Schema
{
shopItems: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'ShopItem',
},
],
}
And a shop can have multiple Shop Items:
ShopItem Schema
[
itemName: { type: String },
kidFriendly: { type: Boolean, default: false },
]
I want to lookup the ShopItems for a Shop. And I only want the ShopItems that are not kidFriendly.
I tried
const result3 = await Shop.aggregate([
{
$match: {
_id: mongoose.Types.ObjectId('623ae52ba5b1af0004e1c4ec'),
},
},
{
$lookup: {
from: ShopItem.collection.collectionName,
pipeline: [
{
$match: {
kidFriendly: { $ne: true },
_id: { $in: '$shopItems' },
},
},
],
as: 'adultOnlyItems',
},
},
]);
but I get an error:
$in needs an array.
Example data
// shops
[
{
_id: ObjectId('623ae52ba5b1af0004e1c4ec'),
shopItems: [ObjectId('631e6b133b688a0004a17265'), ObjectId('62f4cc974a255f00044c01b5'), ObjectId('625ffc48eec7b20004c9294c')]
},
{
_id: ObjectId('623ae52ba5b1af0004e1c4eb'),
shopItems: [ObjectId('631e6b133b688a0004a17263')]
}
]
//shop items
[
{
_id: ObjectId('631e6b133b688a0004a17265'),
itemName: "Barbie",
kidFriendly: true
},
{
_id: ObjectId('62f4cc974a255f00044c01b5'),
itemName: "Alcohol",
kidFriendly: false
},
{
_id: ObjectId('625ffc48eec7b20004c9294c'),
itemName: "Glass Vase",
kidFriendly: false
},
{
_id: ObjectId('631e6b133b688a0004a17263'),
itemName: "Beach Ball",
kidFriendly: true
}
]
When you use $lookup with pipeline, you need to have the let to store the field value from shops into variable. And use the $expr operator as well.
db.shops.aggregate([
{
$match: {
_id: mongoose.Types.ObjectId('623ae52ba5b1af0004e1c4ec')
}
},
{
$lookup: {
from: ShopItem.collection.collectionName,
let: {
shopItems: "$shopItems"
},
pipeline: [
{
$match: {
$expr: {
$and: [
{
$ne: [
"$kidFriendly",
true
]
},
{
$in: [
"$_id",
"$$shopItems"
]
}
]
}
}
}
],
as: "adultOnlyItems"
}
}
])
Demo # Mongo Playground
Reference
Join Conditions and Subqueries on a Joined Collection

Compare arrays and filter, using MongoDB aggregation

For my DB, I wrote the following pipeline:
let orders = await Order.aggregate(
{
$unwind: "$candidate",
},
{
$lookup: {
from: "groups",
localField: "candidate.groupId",
foreignField: "_id",
as: "groupData",
},
},
{
$lookup: {
from: "users",
let: {
id: "$candidate.groupId",
},
pipeline: [
{ $match: { groupId: { $ne: null } } },
{
$match: {
$expr: { $in: ["$$id", "$groupId"] },
},
},
{ $project: { name: 1, email: 1, _id: 1 } },
],
as: "members",
},
},
{ $match: { "members._id": new ObjectId(req.userId) } },
{
$lookup: {
from: "users",
let: { ids: "$candidate.autonomousId" },
pipeline: [
{
$match: {
$expr: { $in: ["$_id", "$$ids"] },
},
},
{ $project: { name: 1, email: 1, _id: 1 } },
],
as: "candidate",
},
},
{
$project: {
groupData: 1,
members: 1,
candidate: 1,
stillAvailable: 1,
_id: 0,
},
}
).toArray();
The output was the expected...
{ candidate:
[ { _id: 601817dc2eeecd17db3a68f6,
name: 'Maria' },
{ _id: 601817ef2eeecd17db3a68f7,
name: 'Jose' } ],
groupData:
[ { _id: 606632403fffb851b8c41d12,
name: 'Giraia' } ],
members:
[ { _id: 601817dc2eeecd17db3a68f6,
name: 'Maria' },
{ _id: 601817ef2eeecd17db3a68f7,
name: 'Jose' },
{ _id: 60182cbb2b654330d2458f89,
name: 'Jonas'} ] }
The last step in the pipeline would be to compare the arrays, filter which members were not candidates and add them to the array stillAvailable. I tried in many ways but I couldn't achieve my goal with aggregation. The only solution I could find was to process the result of the incomplete pipeline on my backend. The code is:
orders.forEach(
(order) =>
(order.stillAvailable = order.members.filter(
(autonomous) =>
!order.candidate.some((el) => {
return el._id.toString() === autonomous._id.toString();
})
))
);
With that, I reach the expected output...
{ candidate:
[ { _id: 601817dc2eeecd17db3a68f6,
name: 'Maria' },
{ _id: 601817ef2eeecd17db3a68f7,
name: 'Jose' } ],
groupData:
[ { _id: 606632403fffb851b8c41d12,
name: 'Giraia' ],
members:
[ { _id: 601817dc2eeecd17db3a68f6,
name: 'Maria' },
{ _id: 601817ef2eeecd17db3a68f7,
name: 'Jose' },
{ _id: 60182cbb2b654330d2458f89,
name: 'Jonas' ],
stillAvailable:
[ { _id: 60182cbb2b654330d2458f89,
name: 'Jonas' ] }
The problem is to better compartmentalize my code, it would be necessary to realize the last step (done with javascript on my backend) as one more step on the pipeline. Does anyone have an idea how to reach that?
After I wrote the question here, somehow the idea was better structured and I achieved the result, using $map and one more level of $lookup! I left the answer documented here in case someone falls into the same issue.
let orders = await Order.aggregate(
{
$unwind: "$candidate",
},
{
$lookup: {
from: "groups",
localField: "candidate.groupId",
foreignField: "_id",
as: "groupData",
},
},
{
$lookup: {
from: "users",
let: {
id: "$candidate.groupId",
},
pipeline: [
{ $match: { groupId: { $ne: null } } },
{
$match: {
$expr: { $in: ["$$id", "$groupId"] },
},
},
{ $project: { name: 1, email: 1, _id: 1 } },
],
as: "members",
},
},
{ $match: { "members._id": new ObjectId(req.userId) } },
{
$lookup: {
from: "users",
let: { ids: "$candidate.autonomousId" },
pipeline: [
{
$match: {
$expr: { $in: ["$_id", "$$ids"] },
},
},
{ $project: { name: 1, email: 1, _id: 1 } },
],
as: "candidate",
},
},
{
$project: {
groupData: 1,
members: 1,
candidate: 1,
_id: 0,
stillAvailable: {
$setDifference: [
{
$map: {
input: "$members",
as: "member",
in: "$$member._id",
},
},
{
$map: {
input: "$candidate",
as: "el",
in: "$$el._id",
},
},
],
},
},
},
{
$lookup: {
from: "users",
let: {
ids: "$stillAvailable",
},
pipeline: [
{
$match: {
$expr: { $in: ["$_id", "$$ids"] },
},
},
{ $project: { name: 1, email: 1, _id: 1 } },
],
as: "stillAvailable",
},
}
).toArray();

MongoDB $lookup and $match object.key in foreign array

I am doing a $lookup to find 'events' where a customer is an attendee. The list of attendants is an array like this:
attendee: [{customer: <ID>}]
I tried this but it always returns an empty array:
$lookup: {
from: "events",
let: { customer: "$_id" },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: ['$attendee.customer', '$$customer'] },
]
},
}
},
{ $limit: 1 },
{ $sort: {start: -1} },
{ $project: { id: "$_id", start: 1, end: 1, name: 1, host: 1 } },
],
as: "event"
}
You are matching the fields on array so just replace $eq to $in
Your new code will be
$lookup: {
from: "events",
let: { customer: "$_id" },
pipeline: [
{
$match: {
$expr: {
$in: [
"$attendee.customer",
"$$customer"
]
},
}
},
{ $limit: 1 },
{ $sort: {start: -1} },
{ $project: { id: "$_id", start: 1, end: 1, name: 1, host: 1 } },
],
as: "event"
}

How can I populate and then match based on the 'parent' id

I'm trying to "match" a referenced document (populated) with the parent document id. The goal is to only show members permissions for the specific organization and not all the other permissions they may have. So the 'entity.item', in this case an organization, would have to match the parent organization id. I'm trying to find out how, if possible, to access the parent organization id from a child doc.
let userId = '123';
let organizations = await Organization.find().where('members.includes(userId)').populate({
path: 'members',
options: { sort: { name: 1 } },
populate: {
path: 'permissions',
match: {
'entity.kind': 'Organization',
'entity.item': organization._id //HERE
},
populate: {
path: 'entity.item'
}
}
});
I ended up using the lookup operator on the aggregate method. Still testing use cases but seems to be working. The following answer pointed me in that direction.
"With the mongodb 3.6 and above $lookup syntax it is quite simple to join nested fields without using $unwind."
let organizations = await Organization.aggregate([
{ $sort: { name: 1 } },
{ $match: { $expr: { $in: [ user.id, '$members' ] } } },
{
$lookup: {
from: 'user',
let: { id: '$_id', members: '$members' },
pipeline: [
{ $match: { $expr: { $in: [ '$_id', '$$members' ] } } },
{ $addFields: { id: '$_id' } },
{
$lookup: {
from: 'permission',
pipeline: [
{ $match: { $expr: { $and: [
{ $eq: [ '$entity.kind', 'Organization' ] },
{ $eq: [ '$entity.item', '$$id' ] }
] } } },
{ $addFields: { id: '$_id' } },
{
$lookup: {
from: 'organization',
pipeline: [
{ $match: { $expr: { $eq: [ '$_id', '$$id' ] } } },
{ $addFields: { id: '$_id' } }
],
as: 'entity.item'
}
}
],
as: 'permissions'
}
}
],
as: 'members'
}
},
{ $addFields: { id: '$_id' } }
]);

Categories

Resources