3 level nested lookup with arrays - mongodb

I am building an apllication with mongoose and node.js that enables you to post, comment, and like both the posts and the comments.
I am tryign to build a query that gets all the information from the db.
saw that answer nested 3 level lookup like in here MongoDB nested lookup with 3 levels.
it works, but the project filter returns me an error and if I remove it, I get some comments that looks like that:
"comments":
{
"user": [],
"likes": []
}
the argigate
postArgigate = [
{
//lookup for the post likes
$lookup: {
from: 'likes',
localField: '_id',
foreignField: 'postCommentID',
as: '_likes'
},
},
//the user wrote the post
{
$lookup: {
from: 'users',
localField: 'userID',
foreignField: '_id',
as: 'user'
},
},
{
$lookup: {
from: 'comments',
localField : 'commentsID',
foreignField : '_id',
as: 'comments'
},
},
//unwind the comments to do a nesting lookup
{
$unwind: {
path: "$comments",
preserveNullAndEmptyArrays: true
}
},
//lookup in the comments likes
{
$lookup: {
from: 'likes',
localField : 'comments._id',
foreignField : 'postCommentID',
as: 'comments._likes'
},
},
//lookup in the user that wrote the comment
{
$lookup: {
from: 'users',
localField: 'comments.userID',
foreignField: '_id',
as: 'comments.user'
},
},
{
$set: {
'comments.likes':'$comments._likes.userID',
'likes': '$_likes.userID'
}
},
{
$project: {
/////////FILTER NOT WORKING////////////
// 'comments': {
// $filter: { input: "$comments", as: "cm", cond: { $ifNull: ["$$cm._id", false] } }
// } , //returns an error
'comments.userID':0,
'comments.user.password':0,
'comments._likes':0,
'userID':0,
'user.password':0,
'commentsID':0,
'_likes': 0,
}
},
{
$group: {
_id : "$_id",
user:{$first:'$user'},
likes:{$first:'$likes'},
date:{$first:'$date'},
content:{$first:'$content'},
comments: { $push: "$comments" },
}
}
]
thank you for you help!
comments
[
{
_id: ObjectId("com1"),
userID:ObjectId("eliran1"),
content: 'comment 1',
date: 2022-03-02T22:55:16.224Z,
},
{
_id:ObjectId("com2"),
userID: ObjectId("eliran1"),
content: 'comment 2',
date: 2022-03-05T18:34:52.890Z,
__v: 0
}
]
posts
[
{
_id: new ObjectId("post1"),
userID: new ObjectId("eliran1"),
content: 'post 1',
commentsID: [],
date: 2022-03-05T18:28:11.487Z,
},
{
_id: new ObjectId("post2"),
userID: new ObjectId("shira1"),
content: 'post 2',
commentsID: [ObjectId("com1"),
ObjectId("com2") ],
date: 2022-03-05T18:34:46.364Z,
}
]
users
[
{
_id: new ObjectId("eliran1"),
user: 'eliran222',
password: '123456789',
email: 'fdfd#fdfd.com33',
gender: true,
__v: 0
},
{
_id: new ObjectId("shira1"),
user: 'shira3432',
password: '123456789',
email: 'fdrf#gfge.com',
gender: false,
}
]
likes
[
{
_id: ObjectId("like1"),
userID: ObjectId("eliran1"),
postCommentID:ObjectId("post1"),
},
{
_id:ObjectId("like2"),
userID: ObjectId("shira1"),
postCommentID:ObjectId("com1"),
}
]
expected results:
[
{
_id: new ObjectId("post1"),
user: {
user: 'eliran222',
email: 'fdfd#fdfd.com33',
gender: true,
},
content: 'post 1',
comments: [],
date: 2022-03-05T18:28:11.487Z,
likes:[{/*eliran`s user*/}]
},
{
_id: new ObjectId("post2"),
userID: {
_id: new ObjectId("shira1"),
user: 'shira3432',
email: 'fdrf#gfge.com',
gender: false,
},
content: 'post 2',
comments: [{
_id: ObjectId("com1"),
user:{
_id:objectId(eliran1)
user: 'eliran222',
email: 'fdfd#fdfd.com33',
gender: true,
},
content: 'comment 1',
date: 2022-03-02T22:55:16.224Z,
likes:[{/*shira`s user*/}]
},
{
_id:ObjectId("com2"),
user: {
objectId(eliran1)
user: 'eliran222',
email: 'fdfd#fdfd.com33',
gender: true,
},
content: 'comment 2',
date: 2022-03-05T18:34:52.890Z,
likes:[]
}],
date: 2022-03-05T18:34:46.364Z,
}
]
}
]

Related

Merge $lookup value inside objects nested in array mongoose

So I have 2 models user & form.
User Schema
firstName: {
type: String,
required: true,
},
lastName: {
type: String,
required: true,
},
email: {
type: String,
required: true,
}
Form Schema
approvalLog: [
{
attachments: {
type: [String],
},
by: {
type: ObjectId,
},
comment: {
type: String,
},
date: {
type: Date,
},
},
],
userId: {
type: ObjectId,
required: true,
},
... other form parameters
When returning a form, I'm trying to aggregate the user info of every user in the approvalLog into their respective objects as below.
{
...other form info
approvalLog: [
{
attachments: [],
_id: '619cc4953de8413b548f61a6',
by: '619cba9cd64af530448b6347',
comment: 'visit store for disburement',
date: '2021-11-23T10:38:13.565Z',
user: {
_id: '619cba9cd64af530448b6347',
firstName: 'admin',
lastName: 'user',
email: 'admin#mail.com',
},
},
{
attachments: [],
_id: '619cc4ec3ea3e940a42b2d01',
by: '619cbd7b3de8413b548f61a0',
comment: '',
date: '2021-11-23T10:39:40.168Z',
user: {
_id: '619cbd7b3de8413b548f61a0',
firstName: 'sam',
lastName: 'ben',
email: 'sb#mail.com',
},
},
{
attachments: [],
_id: '61a9deab8f472c52d8bac095',
by: '61a87fd93dac9b209096ed94',
comment: '',
date: '2021-12-03T09:08:59.479Z',
user: {
_id: '61a87fd93dac9b209096ed94',
firstName: 'john',
lastName: 'doe',
email: 'jd#mail.com',
},
},
],
}
My current code is
Form.aggregate([
{
$lookup: {
from: 'users',
localField: 'approvalLog.by',
foreignField: '_id',
as: 'approvedBy',
},
},
{ $addFields: { 'approvalLog.user': { $arrayElemAt: ['$approvedBy', 0] } } },
])
but it only returns the same user for all objects. How do I attach the matching user for each index?
I've also tried
Form.aggregate([
{
$lookup: {
from: 'users',
localField: 'approvalLog.by',
foreignField: '_id',
as: 'approvedBy',
},
},
{
$addFields: {
approvalLog: {
$map: {
input: { $zip: { inputs: ['$approvalLog', '$approvedBy'] } },
in: { $mergeObjects: '$$this' },
},
},
},
},
])
This adds the right user to their respective objects, but I can only add the to the root object and not a new one.
You can try the approach,
$map to iterate loop of approvalLog
$filter to iterate loop of approvedBy array and search for user id by
$arrayElemAt to get first element from above filtered result
$mergeObjects to merge current object properties of approvalLog and filtered user
$$REMOVE don't need approvedBy now
await Form.aggregate([
{
$lookup: {
from: "users",
localField: "approvalLog.by",
foreignField: "_id",
as: "approvedBy"
}
},
{
$addFields: {
approvalLog: {
$map: {
input: "$approvalLog",
as: "a",
in: {
$mergeObjects: [
"$$a",
{
user: {
$arrayElemAt: [
{
$filter: {
input: "$approvedBy",
cond: { $eq: ["$$a.by", "$$this._id"] }
}
},
0
]
}
}
]
}
}
},
approvedBy: "$$REMOVE"
}
}
])
Playground
The second approach using $unwind,
$unwind deconstruct the approvalLog array
$lookup with user collection
$addFields and $arrayElemAt to get first element from lookup result
$group by _id and reconstruct the approvalLog array and get first value of other required properties
await Form.aggregate([
{ $unwind: "$approvalLog" },
{
$lookup: {
from: "users",
localField: "approvalLog.by",
foreignField: "_id",
as: "approvalLog.user"
}
},
{
$addFields: {
"approvalLog.user": {
$arrayElemAt: ["$approvalLog.user", 0]
}
}
},
{
$group: {
_id: "$_id",
approvalLog: { $push: "$approvalLog" },
userId: { $first: "$userId" },
// add your other properties like userId
}
}
])
Playground

MongoDB: How to populate the nested object with lookup query?

I am fetching list of records having some nested reference to other collection, I want to populate that nested ObjectId which is inside array of objects, with mongoDb aggregate lookup query.
This is how DB collection structure is:
{
subject: {type: String},
body: {type: String},
recipients: [{
userId: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},
stutus: {type: String, enum: ['pending','accepted','rejected'], default:'pending'}
}],
sender: {type: mongoose.Schema.Types.ObjectId, ref: 'User'}
}
What I am expecting:
[{
subject: 'Some subject here.',
body: 'Lorem ipsum dolor emit set',
recipients: [{
userId: {firstName: 'John', lastName: 'Doe'},
status: 'accepted'
},{
userId: {firstName: 'Jane', lastName: 'Doe'},
status: 'accepted'
}],
sender: {firstName: 'Jacobs', 'lastName': 'Doe'}
},{
subject: 'Some subject here.',
body: 'Lorem ipsum dolor emit set',
recipients: [{
userId: {firstName: 'Jane', lastName: 'Doe'},
status: 'rejected'
},{
userId: {firstName: 'John', lastName: 'Doe'},
status: 'accepted'
}],
sender: {firstName: 'Jacobs', 'lastName': 'Doe'}
}]
Any kind help will be greatly appreciated.
$unwind deconstruct recipients array
$lookup with users collection for recipients.userId
$unwind deconstruct recipients.userId array
$lookup with users collection for sender
$unwind deconstruct sender array
$group by _id and reconstruct recipients array
db.mails.aggregate([
{ $unwind: "$recipients" },
{
$lookup: {
from: "users",
localField: "recipients.userId",
foreignField: "_id",
as: "recipients.userId"
}
},
{ $unwind: "$recipients.userId" },
{
$lookup: {
from: "users",
localField: "sender",
foreignField: "_id",
as: "sender"
}
},
{ $unwind: "$sender" },
{
$group: {
_id: "$_id",
recipients: { $push: "$recipients" },
subject: { $first: "$subject" },
body: { $first: "$body" },
sender: { $first: "$sender" }
}
}
])
Playground
Try This:
db.emails.aggregate([
{ $unwind: "$recipients" },
{
$lookup: {
from: "users",
let: { userId: "$recipients.userId", status: "$recipients.stutus" },
pipeline: [
{
$match: {
$expr: { $eq: ["$_id", "$$userId"] }
}
},
{
$project: {
"_id": 0,
"userId": {
"firstName": "$firstName",
"lastName": "$lastName",
},
"status": "$$status"
}
}
],
as: "recipient"
}
},
{
$lookup: {
from: "users",
let: { userId: "$sender" },
pipeline: [
{
$match: {
$expr: { $eq: ["$_id", "$$userId"] }
}
},
{
$project: {
"_id": 0,
"firstName": 1,
"lastName": 1
}
}
],
as: "sender"
}
},
{
$group: {
_id: "$_id",
subject: { $first: "$subject" },
body: { $first: "$body" },
recipients: { $push: { $arrayElemAt: ["$recipient", 0] } },
sender: { $first: { $arrayElemAt: ["$sender", 0] } }
}
}
]);

Nested Lookup and filtering

I have below collections
User
[
{
id : 'acd-1234',
name : 'some name',
profile_id : 1,
is_graduate: true,
children: [
{ class: 'User', id: 'abcd-123'},
{ class: 'User', id: 'bcd-33'}
]
},
{
id: 'abcd-123',
name : 'jhon',
profile_id : 2
is_graduate: true,
},
{
id: 'bcd-123',
name : 'jhon due',
profile_id : 3,
is_graduate: false
}
]
Profile
[
{
id: 1,
address: 'some address'
},
{
id: 2,
address: 'some other address'
},
{
id: 3,
address: 'some other other address'
}
]
Final Output that i need is ( Parent with only graduate children)
[
{
id: 'acd-1234',
name: 'some name',
is_graduate: true,
profile : {
id: 1,
address: "some address"
},
children: [
{
id: "abcd-123",
name: "jhon",
is_graduate: true,
profile: {
id: 2,
address: "some other address"
}
}
]
}
]
Where i am really stuck is
Making nested lookup. Showing profile with all the children
applying filtering on the childrens
Below error from mongo is not allowing me to use pipeline with localField
$lookup with 'pipeline' may not specify 'localField' or 'foreignField'
Try this:
db.users.aggregate([
{
$lookup: {
from: "profile",
let: { id: "$profile_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$id", "$$id"] }
}
}
],
as: "profile"
}
},
{ $unwind: "$profile" },
{
$lookup: {
from: "users",
let: {
id: { $ifNull: ["$children.id", []] }
},
pipeline: [
{
$match: {
$expr: { $in: ["$id", "$$id"] }
}
},
{
$lookup: {
from: "profile",
localField: "profile_id",
foreignField: "id",
as: "profile"
}
},
{ $unwind: "$profile" }
],
as: "children"
}
}
]);
If you want to show only non empty children then add below $match stage after last $lookup:
{
$match: {
$expr: {
$gt: [{ $size: "$children" }, 0]
}
}
}

mongoose recursive nesting

in my project a user can create products. each user have a reference to all of its products and each product have a reference to its user.
both the user and the product have a 'name' field.
i need to get all of the users products array, and in that array i want to have the product name and the
user name that created it (and only those fields and no others).
for example:
Users:
{ _id: 1, name: 'josh', productIds: [1,3]}
{ _id: 2, name: 'sheldon', productIds: [2]}
Products:
{ _id: 1, name: 'table', price: 45, userId: 1}
{ _id: 2, name: 'television', price: 25 userId: 2}
{ _id: 3, name: 'chair', price: 14 userId: 1}
i want to get the following result:
{ _id: 1, name: 'josh',
products: {
{ _id: 1, name: 'table', user: { _id: 1, name: 'josh' },
{ _id: 3, name: 'chair', user: { _id: 1, name: 'josh' },
}
}
{ _id: 2, name: 'sheldon',
products: {
{ _id: 2, name: 'television', userId: { _id: 2, name: 'sheldon' }
}
}
i tried the following query that didn't fill the inner userId and left it with only the id (no name):
User.aggregate([
{
$lookup:
{
from: 'products',
localField: 'productIds',
foreignField: '_id',
as: 'products'
}
}
i also tried the following, which did the same as the first query except it only retried the first product for each user:
User.aggregate([
{
$lookup:
{
from: 'products',
localField: 'productIds',
foreignField: '_id',
as: 'products'
}
},
{
$unwind: {
path: "$products",
preserveNullAndEmptyArrays: true
}
},
{
$lookup: {
from: "user",
localField: "products.userId",
foreignField: "_id",
as: "prodUsr",
}
},
{
$group: {
_id : "$_id",
products: { $push: "$products" },
"doc": { "$first": "$$ROOT" }
}
},
{
"$replaceRoot": {
"newRoot": "$doc"
}
}
Product:
const schema = new Schema(
{
name: {
type: String,
required: true
},
price: {
type: Number,
required: true
},
userId: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
}
);
module.exports = mongoose.model('Product', schema);
User:
const schema = new Schema(
{
name: {
type: String,
required: true,
unique: true
},
productIds: [{
type: Schema.Types.ObjectId,
ref: 'Product',
require: false
}],
{ timestamps: true }
);
module.exports = mongoose.model('User', schema);
any help will be highly appreciated
It looks like a perfect scenario for $lookup with custom pipeline and another nested $lookup. The inner one allows you to handle product-> user relationship while the outer one handles user -> product one:
db.Users.aggregate([
{
$project: {
productIds: 0
}
},
{
$lookup: {
from: "Products",
let: { user_id: "$_id" },
pipeline: [
{
$match: {
$expr: {
$eq: [ "$userId", "$$user_id" ]
}
}
},
{
$lookup: {
from: "Users",
localField: "userId",
foreignField: "_id",
as: "user"
}
},
{
$unwind: "$user"
},
{
$project: {
"user.productIds": 0,
"price": 0,
"userId": 0
}
}
],
as: "products"
}
}
])
Mongo Playground

multiple $lookup stages in mongodb aggregation

I'm working with the followings mongoose schemas:
Question schema:
var Question = new Schema({
title: String,
content: String,
createdBy: {
type: Schema.ObjectId,
ref: 'User',
required: true
},
answers: {
type: [ { type: Schema.Types.ObjectId, ref: 'Answer' } ]
}
});
Answer Shchema:
var Answer = new Schema({
content: {
type: String,
require: 'Content cannot be empty.'
},
createdBy: {
type: Schema.Types.ObjectId,
ref: 'User'
},
isBest: {
type: Boolean,
default: false
},
createdAt: {
type: Date,
default: Date.now
},
votes: {
type: Number,
default: 0
},
comments: {
type: [{ type: Schema.Types.ObjectId, ref: 'Comment' }]
}
});
and Comment Schema:
var Comment = new Schema({
content: {
type: String,
required: [true, 'Content cannot be empty']
},
createdBy: {
type: Schema.Types.ObjectId,
ref: 'User'
},
createdAt: {
type: Date,
default: Date.now
}
});
Basically what I'm trying to do is doing a $lookup for answers and for comments array in every answer, and then in $project stage try to add an isOwner field that is going to be true if the user logged is the owner of the answer or comment. This is what I' trying:
Question.aggregate([
{
$match: { '_id': { $eq: questionId } }
},
{
$lookup: {
from: 'answers',
localField: 'answers',
foreignField: '_id',
as: 'answers'
}
},{
$lookup:{
from: 'comments',
localField: 'answers.comments',
foreignField: '_id',
as: 'comments'
}
}, {
$project: {
title: true,
content: true,
createdBy: true,
createdAt: true,
isOwner: { $eq : ['$createdBy', currentUser] },
answers: true,
answers: {
isOwner: { $eq : ['$createdBy', currentUser] },
content: true,
createdBy: true,
createdAt: true,
comments: {
content: true,
createdAt: true,
createdBy: true,
isOwner: { $eq : ['$createdBy', currentUser] }
}
}
}
}
])
This is the ouput that I'm expecting:
{
"_id": "58a7be2c98a28f18acaa4be4",
"title": "Some title",
"createdAt:": "2017-03-03T05:13:41.061Z",
"content": "some content",
"isOwner": true,
"createdBy": {
"_id": "58a3a66c088fe517b42775c9",
"name": "User name",
"image": "imgUrl"
},
"answers": [
{
"_id": "58a3a66c088fe517b42775c9",
"content": "an answer content",
"createdAt": "2017-03-03T05:13:41.061Z",
"isBest": false,
"isOwner": false,
"createdBy":{
"_id": "58a3a66c088fe517b42775c9",
"name": "another user",
"image": "another image"
},
"comments": [
{
"_id": "58aa104a4254221580832a8f",
"content": "some comment content",
"createdBy": {
"_id": "58a3a66c088fe517b42775c9",
"name": "another user",
"image": "another image"
},
}
]
}
]
}
I'm using mongodb 3.4.2
You can try addFields stage to add the isOwner field for all the relations.
Question.aggregate([{
$match: {
'_id': {
$eq: questionId
}
}
}, {
$addFields: {
"isOwner": {
$eq: ['$createdBy', currentUser]
}
}
}, { // No unwind needed as answers is scalar of array values.
$lookup: {
from: 'answers',
localField: 'answers',
foreignField: '_id',
as: 'answers'
}
}, {
$addFields: {
"answers.isOwner": {
$eq: ['$createdBy', currentUser]
}
}
}, {
$unwind: "$answers" //Need unwind here as comments is array of scalar array values
}, {
$lookup: {
from: 'comments',
localField: 'answers.comments',
foreignField: '_id',
as: 'comments'
}
}, {
$addFields: {
"comments.isOwner": {
$eq: ['$createdBy', currentUser]
}
}
}, {
$addFields: { // Move the comments back to answers document
"answers.comments": "$comments"
}
}, {
$project: { // Remove the comments embedded array.
"comments": 0
}
}, {
$group: {
_id: null,
isOwner: {
$first: "$isOwner"
},
answers: {
$push: "$answers"
}
}
}])
THe problem with your code is that you have not unwind the answeeres array before lookup
Please check below comment
Question.aggregate([
{
$match: { '_id': { $eq: questionId } }
},
{
$lookup: {
from: 'answers',
localField: 'answers',
foreignField: '_id',
as: 'answers'
}
},
{$unwind : "$answers"}, // <-- Check here
{
$lookup:{
from: 'comments',
localField: 'answers.comments',
foreignField: '_id',
as: 'comments'
}
},
{
$group : {
_id : null,
title: {$first : true},
content: {$first :true},
createdBy: {$first :true},
createdAt: {$first :true},
isOwner: { $eq : ['$createdBy', currentUser] },
answersStatus: {$first :true},
answers : {$push : $answer}
}
}
])