How to apply $lookup with conditions in mongodb? - mongodb

I am using mongodb databases and want to apply $lookup on 2 collections but with specific conditions.
I have one collection named Company like this
new Schema({
name: String,
benefit: String,
benefitDesc: String,
company_url: String,
logoUrl: String,
coverUrl: String,
desc: String,
createdAt: String,
categoryId: { type: Schema.Types.ObjectId, ref: 'categories' },
})
And another collection named Referrallinks like this
new Schema({
referral_link: String,
referral_code: String,
isLink: Number,
offer_name: String,
offer_desc: String,
user_email: String,
companyId: { type: Schema.Types.ObjectId, ref: 'companies' },
addedByAdmin: { type: Boolean, default: true },
number_of_clicks: Number,
referral_country: String,
link_status: String,
categoryId: { type: Schema.Types.ObjectId, ref: 'categories' },
number_of_clicks: { type: Number, default: 0 },
createdAt: String,
updatedAt: String,
userId: { type: Schema.Types.ObjectId, ref: 'users' }
})
Now when i apply this $lookup
Company.aggregate([{
$lookup: {
from: 'referrallinks',
localField: '_id',
foreignField: 'companyId',
as: 'referrals'
}
}])
So i am getting all companies with referrals as array like this
[
{name : '', benefit : '', benefiDesc : '', referrals : []},
{name : '', benefit : '', benefiDesc : '', referrals : []},
{name : '', benefit : '', benefiDesc : '', referrals : []},
.....
]
But i want to have only referrals in each company's referrals array that have link_status value Approved.
**Note : ** I also want to have category object with company and tried this aggregation but it gives error
{
from: "referrallinks",
let: { company_id: "$_id" },
pipeline: [{
$match: {
$expr: {
$and: [
{ $eq: [ "$$company_id", "$companyId" ] },
{ $eq: [ "$link_status", "Approved" ] }
]
}
},
{
$lookup : {
from : "categories",
let : {"category_id" : "$categoryId"},
pipeline : [
{
$match : {
$expr : {
$eq : ["category_id","$_id"]
}
}
}
],
as : "company"
}
}
}],
as: "referrals"
}
How can i achieve this?

Use $lookup with custom pipeline to specify additional filtering condition:
Company.aggregate([{
$lookup: {
from: "referrallinks",
let: { company_id: "$_id" },
pipeline: [{
$match: {
$expr: {
$and: [
{ $eq: [ "$$company_id", "$companyId" ] },
{ $eq: [ "$link_status", "Approved" ] }
]
}
}
}],
as: "referrals"
}
}])
EDIT: to run nested $lookup your code should look like below:
Company.aggregate([{
$lookup: {
from: "referrallinks",
let: { company_id: "$_id" },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: [ "$$company_id", "$companyId" ] },
{ $eq: [ "$link_status", "Approved" ] }
]
}
}
},
{
$lookup: {
from: "categories",
localField: "categoryId",
foreignField: "_id",
as: "company"
}
}
],
as: "referrals"
}
}])

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

Getting `[ [Object] ]` as nested array of object response on MongoDB Aggregation query

I'm trying to do an aggregation on two collections that has a linkage between them, and I need to access information in an array of objects in one of those collections.
Here are the schemas:
User Schema:
{
_id: ObjectId,
username: String,
password: String,
associatedEvents: [
{
event_id: ObjectId,
isCreator: boolean,
access_level: String,
}
]
}
Event Schema:
{
_id: ObjectId,
title: String,
associated_users: [
{
user_id: ObjectId
}
]
}
I'm attempting to get the users associated to an event for a specific user, and then get their access level information. Here's the aggregation I have:
const eventsJoined = await Event.aggregate([
{
$match: {
$expr: { $in: [id, "$associatedUserIds"] },
},
},
{
$lookup: {
from: "users",
localField: "associatedUserIds",
foreignField: "_id",
as: "user_info",
},
},
{ $unwind: "$user_info" },
{
$unwind: {
path: "$user_info.associatedEvents",
preserveNullAndEmptyArrays: true,
},
},
{
$group: {
_id: "$_id",
title: { $first: "$title" },
description: { $first: "$description" },
startDate: { $first: "$startdate" },
userInfo: { $first: "$user_info" },
usersAssociatedEvents: { $push: "$user_info.associatedEvents" },
},
},
{
$project: {
title: 1,
description: 1,
startDate: 1,
userInfo: 1,
usersAssociatedEvents: "$usersAssociatedEvents",
},
},
]);
And this is the result I'm getting:
[
{
_id: 609d5ad1ef4cdbeb32987739,
title: 'hello',
description: 'desc',
startDate: null,
usersAssociatedEvents: [ [Object] ]
}
]
As you can see, the query is already aggregating the correct data. But the last thing that's tripping me up is the fact that the aggregation is [ [Object] ] for usersAssociatedEvents instead of the actual contents of the object. Any idea on why that would be?

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]
}
}
}

aggregation lookup and match a nested array

Hello i am trying to join two collections...
#COLLECTION 1
const valuesSchema= new Schema({
value: { type: String },
})
const categoriesSchema = new Schema({
name: { type: String },
values: [valuesSchema]
})
mongoose.model('categories', categoriesSchema )
#COLLECTION 2
const productsSchema = new Schema({
name: { type: String },
description: { type: String },
categories: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'categories',
}]
})
mongoose.model('productos', productsSchema )
Now, what i pretend to do is join these collections and have an output like this.
#Example Product Document
{
name: 'My laptop',
description: 'Very ugly laptop',
categories: ['5f55949054f3f31db0491b5c','5f55949054f3f31db0491b5b'] // these are _id of valuesSchema
}
#Expected Output
{
name: 'My laptop',
description: 'Very ugly laptop',
categories: [{value: 'Laptop'}, {value: 'PC'}]
}
This is what i tried.
{
$lookup: {
from: "categories",
let: { "categories": "$categories" },
as: "categories",
pipeline: [
{
$match: {
$expr: {
$in: [ '$values._id','$$categories']
},
}
},
]
}
}
but this query is not matching... Any help please?
You can try,
$lookup with categories
$unwind deconstruct values array
$match categories id with value id
$project to show required field
db.products.aggregate([
{
$lookup: {
from: "categories",
let: { cat: "$categories" },
as: "categories",
pipeline: [
{ $unwind: "$values" },
{ $match: { $expr: { $in: ["$values._id", "$$cat"] } } },
{
$project: {
_id: 0,
value: "$values.value"
}
}
]
}
}
])
Playground
Since you try to use the non-co-related queries, I appreciate it, you can easily achieve with $unwind to flat the array and then $match. To regroup the array we use $group. The $reduce helps to move on each arrays and store some particular values.
[
{
$lookup: {
from: "categories",
let: {
"categories": "$categories"
},
as: "categories",
pipeline: [
{
$unwind: "$values"
},
{
$match: {
$expr: {
$in: [
"$values._id",
"$$categories"
]
},
}
},
{
$group: {
_id: "$_id",
values: {
$addToSet: "$values"
}
}
}
]
}
},
{
$project: {
categories: {
$reduce: {
input: "$categories",
initialValue: [],
in: {
$concatArrays: [
"$$this.values",
"$$value"
]
}
}
}
}
}
]
Working Mongo template

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}
}
}
])