mongoDB lookups upto 4-5 levels with arrays - mongodb

I am trying to create aggregate code which can join multiple collections and fetches data based on the condition
My collections looks like the following
Users Collection(Example Document)
{
"_id":{"$oid":"6137038058d4632824c84872"},
"first_name":"test",
"last_name":".",
"email":"valid#gmail.com",
"enrolled_courses":[
{
"completed_lessons":[],
"completed":false,
"_id": {"$oid":"6142d1c76d657428903420db"},
"course_id":{"$oid":"614033510a61aa1f3c8b94ff"}
},
{
"completed_lessons":[],
"completed":false,
"_id":{"$oid":"6156d686dcbc58503c7ec679"},
"course_id":{"$oid":"61486e57460b5a422c6e63ca"}
},
{
"completed_lessons":["616d181e62cb265db07d9273","616d182e62cb265db07d9293"],
"completed":false,
"_id":{"$oid":"616d2424149936380c186c2d"},
"course_id":{"$oid":"616d105662cb265db07d91cf"}
}
]
}
Courses Collection(Example Document)
{
"_id":{"$oid":"6135f735b399f03ba03d74ce"},
"type":1,
"enrolled_users":["61374040c27237b1ed87480b"],
"is_deleted":0,
"status":1,
"company_id":{"$oid":"6135f6c9b399f03ba03d74a0"},
"course":"Course 1"
}
Modules Collection(Example Document)
{
"_id":{"$oid":"616d17fd62cb265db07d9240"},
"type":1,
"is_always_available":false,
"is_deleted":0,
"status":1,
"company_id":{"$oid":"6135f6c9b399f03ba03d74a0"},
"course_id":{"$oid":"616d105662cb265db07d91cf"},
"module_name":"module 3",
"due_date":{"$date":"2021-10-18T00:00:00.000Z"}
}
Lessons Collection(Example Document)
{
"_id":{"$oid":"616d184962cb265db07d92cb"},
"lessson_image":null,
"status":1,
"is_deleted": 0,
"module_id":{"$oid":"616d17e462cb265db07d921f"},
"lessson_name":"Lesson 3"
}
I want to fetch details of all the courses user have enrolled and fetch all course's modules and its lessons
My expected output should be like following -
[
{
//CourseDetail 1 Keys,
modules: [
{
//Module Details
lessons: [{
//lessons of this module
}]
},
{
//Module Details
lessons: [{
//lessons of this module
}]
}
]
},
{
//CourseDetail 2 Keys,
modules: [
{
//Module Details
lessons: [{
//lessons of this module
}]
},
{
//Module Details
lessons: [{
//lessons of this module
}]
}
]
}
]
My current query is following, what it currently does is it groups the modules and its lessons together and bind it to single course object. The fetching of modules and lessons is perfect as per my requirement but course array contains only 1 single object and it holds list of all the modules from other courses also.
Current Query Code
let courseDetails = await users.aggregate([
{
$match: { _id: mongoose.Types.ObjectId(user._id), }
},
{$unwind:"$enrolled_courses"},
{
$lookup: {
from: 'courses',
localField: "enrolled_courses.course_id",
foreignField: "_id",
as: "courseDetails"
}
},
{ $unwind: { path: "$courseDetails", preserveNullAndEmptyArrays: true}},
{
$lookup: {
from: "modules",
let: { currentCourseId: "$courseDetails._id"},
pipeline: [{
$match: {
$expr: {
$and: [
{ $eq: ["$course_id", "$$currentCourseId"]},
{ $eq: ["$status", 1]},
{ $eq: ["$is_deleted", 0]},
{
$or: [
{ $eq: ["$is_always_available", true] },
{ $gte: ["$due_date", convertedDate]}
]
}
]
}
}
},
{
$project: {
_id: 1,
module_name: 1,
company_id: 1,
course_id: 1
}
}],
as: "moduleList"
}
},
{ $unwind: { path: "$moduleList", preserveNullAndEmptyArrays: true }},
{
$lookup: {
from: "lessons",
let: { curentModuleId: "$moduleList._id"},
pipeline: [{
$match: {
$expr: {
$and: [
{ $eq: ["$status", 1] },
{ $eq: ["$is_deleted", 0]},
{ $eq: ["$module_id", "$$curentModuleId"]}
]
}
}
}, {
$project: {
_id: 1,
lessson_name: 1,
module_id: 1
}
}],
as: "moduleList.lessons"
}
},
{
$group: {
_id: "$_id",
course_id: { $first: "$enrolled_courses.course_id"},
course_name: { $first: "$courseDetails.course"},
completed_lessons: { $first: "$enrolled_courses.completed_lessons"},
current_lessons: { $first: "$enrolled_courses.current_lessons"},
completed: { $first: "$enrolled_courses.completed"},
enrolled_users_count: {
$first: "$courseDetails.enrolled_users"
},
modules: {
$push: "$moduleList"
}
}
},
{
$project: {
course_id: 1,
course_name: 1,
completed_lessons: 1,
current_lessons: 1,
completed: 1,
enrolled_users_count: {
$size: "$enrolled_users_count"
},
modules: 1
}
},
{
$group: {
_id: "$_id",
course_id: { $first: "$course_id"},
course_name: { $first: "$course_name"},
completed_lessons: { $first: "$completed_lessons"},
current_lessons: { $first: "$current_lessons"},
completed: { $first: "$completed"},
enrolled_users_count: { $first: "$enrolled_users_count"},
modules: { $first: "$modules"}
}
}
]);

Related

MongoDB: Aggregation/Grouping for poll votes

Am trying to create a poll results aggregation
I have two collections
poll - here is one document
{
"_id": {
"$oid": "636027704f7a15587ef74f26"
},
"question": "question 1",
"ended": false,
"options": [
{
"id": "1",
"option": "option 1"
},
{
"id": "2",
"option": "option 2"
},
{
"id": "3",
"option": "option 3"
}
]
}
Vote - here is one document
{
"_id": {
"$oid": "635ed3210acbf9fd14af8fd1"
},
"poll_id": "636027704f7a15587ef74f26",
"poll_option_id": "1",
"user_id": "1"
}
and i want to perform an aggregate query to get poll results
so am doing the following query
db.vote.aggregate(
[
{
$addFields: {
poll_id: { "$toObjectId": "$poll_id" }
},
},
{
$lookup: {
from: "poll",
localField: "poll_id",
foreignField: "_id",
as: "details"
}
},
{
$group:
{
_id: { poll_id: "$poll_id", poll_option_id: "$poll_option_id" },
details: { $first: "$details" },
count: { $sum: 1 }
}
},
{
$addFields: {
question: { $arrayElemAt: ["$details.question", 0] }
}
},
{
$addFields: {
options: { $arrayElemAt: ["$details.options", 0] }
}
},
{
$group: {
_id: "$_id.poll_id",
poll_id: { $first: "$_id.poll_id" },
question: { $first: "$question" },
options: { $first: "$options" },
optionsGrouped: {
$push: {
id: "$_id.poll_option_id",
count: "$count"
}
},
count: { $sum: "$count" }
}
}
]
)
That is giving me this form of results
{ _id: ObjectId("636027704f7a15587ef74f26"),
poll_id: ObjectId("636027704f7a15587ef74f26"),
question: 'question 1',
options:
[ { id: '1', option: 'option 1' },
{ id: '2', option: 'option 2' },
{ id: '3', option: 'option 3' } ],
optionsGrouped:
[ { id: '1', count: 2 },
{ id: '2', count: 1 } ],
count: 3 }
So what am interested in i want to have the results looking like ( like merging both options & options Group)
{ _id: ObjectId("636027704f7a15587ef74f26"),
poll_id: ObjectId("636027704f7a15587ef74f26"),
question: 'question 1',
optionsGrouped:
[ { id: '1', option: 'option 1', count: 2 },
{ id: '2', option: 'option 2', count: 1 },
{ id: '3', option: 'option 3', count: 0 } ],
count: 4 }
Another question is the DB structure acceptable overall or i can represent that in a better way ?
One option is to group first and use the $lookup later, in order to fetch less data from the poll collection. After the $lookup, use $map with $cond to merge the arrays:
db.vote.aggregate([
{$group: {
_id: {poll_id: {$toObjectId: "$poll_id"}, poll_option_id: "$poll_option_id"},
count: {$sum: 1}
}},
{$group: {
_id: "$_id.poll_id",
counts: {
$push: {count: "$count", option: {$concat: ["option ", "$_id.poll_option_id"]}}
},
countAll: {$sum: "$count"}
}},
{$lookup: {
from: "poll",
localField: "_id",
foreignField: "_id",
as: "poll"
}},
{$project: {poll: {$first: "$poll"}, counts: 1, countAll: 1}},
{$project: {
optionsGrouped: {
$map: {
input: "$poll.options",
in: {$mergeObjects: [
"$$this",
{$cond: [
{$gte: [{$indexOfArray: ["$counts.option", "$$this.option"]}, 0]},
{$arrayElemAt: ["$counts", {$indexOfArray: ["$counts.option", "$$this.option"]}]},
{count: 0}
]}
]}
}
},
count: "$countAll",
question: "$poll.question"
}}
])
See how it works on the playground example
I had reworked the query to match my desires
and this query is achieving the question i have asked
db.poll.aggregate([
{
$addFields: {
_id: {
$toString: "$_id"
}
}
},
{
$lookup: {
from: "poll_vote",
localField: "_id",
foreignField: "poll_id",
as: "votes"
}
},
{
$replaceRoot: {
newRoot: {
$let: {
vars: {
count: {
$size: "$votes"
},
options: {
$map: {
input: "$options",
as: "option",
in: {
$mergeObjects: [
"$$option",
{
count: {
$size: {
$slice: [
{
$filter: {
input: "$votes",
as: "v",
cond: {
$and: [
{
$eq: [
"$$v.poll_option_id",
"$$option._id"
]
}
]
}
}
},
0,
100
]
}
}
},
{
checked: {
$toBool: {
$size: {
$slice: [
{
$filter: {
input: "$votes",
as: "v",
cond: {
$and: [
{
$eq: [
"$$v.user_id",
2
]
},
{
$eq: [
"$$v.poll_option_id",
"$$option._id"
]
}
]
}
}
},
0,
100
]
}
}
}
}
]
}
}
}
},
"in": {
_id: "$_id",
question: "$question",
count: "$$count",
ended: "$ended",
options: "$$options"
}
}
}
}
},
{
$addFields: {
answered: {
$reduce: {
input: "$options",
initialValue: false,
in: {
$cond: [
{
$eq: [
"$$this.checked",
true
]
},
true,
"$$value"
]
}
}
}
}
}
])

Mongo db transaction query

I'm trying to create a mongodb query to see which invoices are paid or not. I would like to add few things to the outcome like:
virtualAmount : original amount - (sum of all creditnotes)
total paid amount : sum of all transactions where delete is false
paid (true/false) : if virtualAmount - total paid amount is 0
I have created mongo playground:
https://mongoplayground.net/p/0OyK_bOZu9X
Anyone know if this is possible?
How to still have the original object when using group?
Is it also possible to create this result with mongoose?
by $unwind array and $group it.
db.collection.aggregate(
[{
$match: {
_id: '62b46391be7c618aa5c9bf86'
}
}, {
$set: {
'transactions': {
$filter: {
'input': '$transactions',
'as': 'item',
'cond': { $eq: ['$$item.deleted', false] }
}
},
}
}, {
$unwind: {
path: '$transactions'
}
}, {
$group: {
_id: '_id',
'total-paid-amount': { $sum: '$transactions.amount.value'},
//keep to next stage
'creditnote': {$first: '$creditnote'},
'original-amount': {$first: '$amount.value'}
}
}, {
$unwind: {
path: '$creditnote'
}
}, {
$group: {
_id: '_id',
'sum-all-creditnotes': {$sum: '$creditnote.amount.value'},
//keep to next stage
'total-paid-amount': {$first: '$total-paid-amount'},
'original-amount': {$first: '$original-amount'}
}
}, {
$addFields: {
'virtual-amount': {$subtract: ['$original-amount','$sum-all-creditnotes']}
}
}, {
$addFields: {
paid: {$eq: [{$subtract: ['$virtual-amount','$total-paid-mount']},0]}
}
}]
result
{
"_id" : "_id",
"sum-all-creditnotes" : 1000,
"total-paid-amount" : 1000,
"original-amount" : 3370,
"virtual-amount" : 2370,
"paid" : false
}
EDIT
or easy way without group
db.collection.aggregate(
[{
$match: {
_id: '62b46391be7c618aa5c9bf86'
}
}, {
$set: {
'transactions': {
$filter: {
'input': '$transactions',
'as': 'item',
'cond': { $eq: ['$$item.deleted', false] }
}
},
}
}, {
$project:
{
'total-paid-amount': { $sum: '$transactions.amount.value' },
'sum-all-creditnotes': { $sum: '$creditnote.amount.value' },
'original-amount': '$amount.value'
}
}, {
$addFields: {
'virtual-amount': { $subtract: ['$original-amount', '$sum-all-creditnotes'] }
}
}, {
$addFields: {
'paid': { $eq: [{ $subtract: ['$virtual-amount', '$total-paid-mount'] }, 0] }
}
}])

Referencing root _id in aggregate lookup match expression not working

This is my first experience using aggregate pipeline. I'm not able to get a "$match" expression to work inside the pipeline. If I remove the "_id" match, I get every document in the collection past the start date, but once I add the $eq expression, it returns empty.
I read a lot of other examples and tried many different ways, and this seems like it is correct. But the result is empty.
Any suggestions?
let now = new Date()
let doc = await Team.aggregate([
{ $match: { created_by: mongoose.Types.ObjectId(req.params.user_oid)} },
{ $sort: { create_date: 1 } },
{ $lookup: {
from: 'events',
let: { "team_oid": "$team_oid" },
pipeline: [
{ $addFields: { "team_oid" : { "$toObjectId": "$team_oid" }}},
{ $match: {
$expr: {
$and: [
{ $gt: [ "$start", now ] },
{ $eq: [ "$_id", "$$team_oid" ] }
]
},
}
},
{
$sort: { start: 1 }
},
{
$limit: 1
}
],
as: 'events',
}},
{
$group: {
_id: "$_id",
team_name: { $first: "$team_name" },
status: { $first: "$status" },
invited: { $first: "$invited" },
uninvited: { $first: "$uninvited" },
events: { $first: "$events.action" },
dates: { $first: "$events.start" } ,
team_oid: { $first: "$events.team_oid" }
}
}])
Example Docs (added by request)
Events:
_id:ObjectId("60350837c57b3a15a414d265")
invitees:null
accepted:null
sequence:7
team_oid:ObjectId("60350837c57b3a15a414d263")
type:"Calendar Invite"
action:"Huddle"
status:"Questions Issued"
title:"Huddle"
body:"This is a Huddle; you should receive new questions 5 days befor..."
creator_oid:ObjectId("5ff9e50a206b1924dccd691e")
start:2021-02-26T07:00:59.999+00:00
end:2021-02-26T07:30:59.999+00:00
__v:0
Team:
_id:ObjectId("60350837c57b3a15a414d263")
weekly_schedule:1
status:"Live"
huddle_number:2
reminders:2
active:true
created_by:ObjectId("5ff9e50a206b1924dccd691e")
team_name:"tESTI"
create_date:2021-02-23T13:50:47.172+00:00
__v:0
This is just a guess since you don't have schema in your question. But it looks like your have some of your _ids mixed up. Where you are currently trying to $match events whose _id is equal to a team_oid. Rather than the event's team_oid field being equal to the current 'team' _id.
I'm pretty confident this will produce the correct output. If you post any schema or sample docs I will edit it.
https://mongoplayground.net/p/5i1w2Ii7KCR
let now = new Date()
let doc = await Team.aggregate([
{ $match: { created_by: mongoose.Types.ObjectId(req.params.user_oid)} },
{ $sort: { create_date: 1 } },
{ $lookup: {
from: 'events',
// Set tea_oid as the current team _id
let: { "team_oid": "$_id" },
pipeline: [
{ $match: {
$expr: {
$and: [
{ $gt: [ "$start", now ] },
// Match events whose 'team_oid' field matches the 'team' _id set above
{ $eq: [ "$team_oid", "$$team_oid" ] }
]
},
}
},
{
$sort: { start: 1 }
},
{
$limit: 1
}
],
as: 'events',
}},
{
$group: {
_id: "$_id",
team_name: { $first: "$team_name" },
status: { $first: "$status" },
invited: { $first: "$invited" },
uninvited: { $first: "$uninvited" },
events: { $first: "$events.action" },
dates: { $first: "$events.start" } ,
team_oid: { $first: "$events.team_oid" }
}
}])

$match in aggregate don't return data in mongodb

I have three tables below is the structure like below
I'm looking to get a result like below
"type1": [ -- type from Accounts collection
{
"_id": "5e97e9a224f62f93d5x3zz46", -- _id from Accounts collection
"locs": "sampleLocks 1", -- field from Accounts collection
"solutions": "sample solutions 1", -- field from Accounts collection
"Clause": "clause 1" -- field from AccountsDesc collection
},
{
"_id": "5e97e9a884f62f93d5x3zz46",
"locs": "sampleLocks2",
"solutions": "sample solutions2",
"Clause": "clause2"
}
],
"type2": [
// same data construction as of type1 above
]
_id, locks, solution to be coming from Accounts collection
Clause field to be coming from AccountsDesc collection
accounts_id is kind of a foreign key in AccountsDesc coming from Account
competitor_id is kind of a foreign key in AccountsDesc coming from Competitor
Below is what my query looks like
db.accountDesc.aggregate([
{
$match : {accounts_Id : "123456"}, active: true}
},
{
$lookup: {
from: 'accounts',
pipeline: [{ $match: { type: { $in: ["type1, type2, type3"] } } }],
as: 'accountsData'
}
},
{
$group: {
_id: "$accountsData.type",
data: {
$push: {_id: "$accountsData._id", clause: "$clause", locs: "$type.locs", solutions: "$type.solutions"}
}
}
},
{
$group: {
_id: null,
data: {
$push: {
k: {
$toString: '$_id'
},
v: '$data'
}
}
}
},
{
$replaceRoot: {
newRoot: {
$arrayToObject: '$data'
}
}
}
])
Issues related with the query -
$match : {accountId : "123456"}, active: true} -- No data is returned if i use match on AccountsDesc collection
cant set localField, foriegnField if im using pipeline, then how the mapping will happen like a LEFT join.
clause: "$clause" don't get the value of this field in the response
As we discussed in chat, you want RIGHT OUTER JOIN for your aggregation.
Try the query below:
db.User_Promo_Map.aggregate([
{
$match: {
user_Id: ObjectId("5e8c1180d59de1704ce68112")
}
},
{
$lookup: {
from: "promo",
pipeline: [
{
$match: {
active: true,
platform: {
$in: [
"twitch",
"youtube",
"facebook"
]
}
}
}
],
as: "accountsData"
}
},
{
$unwind: "$accountsData"
},
{
$group: {
_id: "$accountsData.platform",
data2: {
$addToSet: {
amount: "$amount",
promo_Id: "$promo_Id"
}
},
data: {
$addToSet: {
_id: "$accountsData._id",
format: "$accountsData.format",
description: "$accountsData.description"
}
}
}
},
{
$addFields: {
data: {
$map: {
input: "$data",
as: "data",
in: {
"_id": "$$data._id",
"description": "$$data.description",
"format": "$$data.format",
amount: {
$reduce: {
input: "$data2",
initialValue: "$$REMOVE",
in: {
$cond: [
{
$eq: [
"$$this.promo_Id",
"$$data._id"
]
},
"$$this.amount",
"$$value"
]
}
}
}
}
}
}
}
},
{
$group: {
_id: null,
data: {
$push: {
k: {
$toString: "$_id"
},
v: "$data"
}
}
}
},
{
$replaceRoot: {
newRoot: {
$arrayToObject: "$data"
}
}
}
])
MongoPlayground

How to do lookup on an aggregated collection in mongodb that is being grouped?

For some reason, I can't retrieve the author name from another collection on my aggregate query.
db.getCollection('books').aggregate([
{
$match: {
authorId: { $nin: [ObjectId('5b9a008575c50f1e6b02b27b'), ObjectId('5ba0fb3275c50f1e6b02b2f5'), ObjectId('5bc058b6ae9a2a4d6df330b1')]},
isBorrowed: { $in: [null, false] },
status: 'ACTIVE',
},
},
{
$lookup: {
from: "authors",
localField: "authorId", // key of author id in "books" collection
foreignField: "_id", // key of author id in "authors" collection
as: "bookAuthor",
}
},
{
$group: {
_id: {
author: '$authorId',
},
totalSalePrice: {
$sum: '$sale.amount',
},
},
},
{
$project: {
author: '$_id.author',
totalSalePrice: '$totalSalePrice',
authorName: '$bookAuthor.name', // I can't make this appear
_id: 0,
},
},
{ $sort: { totalSalePrice: -1 } },
])
Any advice on where I had it wrong? Thanks for the help.
Two things that are missing here: you need $unwind to convert bookAuthor from an array into single object and then you need to add that object to your $group stage (so that it will be available in next stages), try:
db.getCollection('books').aggregate([
{
$match: {
authorId: { $nin: [ObjectId('5b9a008575c50f1e6b02b27b'), ObjectId('5ba0fb3275c50f1e6b02b2f5'), ObjectId('5bc058b6ae9a2a4d6df330b1')]},
isBorrowed: { $in: [null, false] },
status: 'ACTIVE',
},
},
{
$lookup: {
from: "authors",
localField: "authorId",
foreignField: "_id",
as: "bookAuthor", // this will be an array
}
},
{
$unwind: "$bookAuthor"
},
{
$group: {
_id: {
author: '$authorId',
},
bookAuthor: { $first: "$bookAuthor" },
totalSalePrice: {
$sum: '$sale.amount',
},
},
},
{
$project: {
author: '$_id.author',
totalSalePrice: '$totalSalePrice',
authorName: '$bookAuthor.name',
_id: 0,
},
},
{ $sort: { totalSalePrice: -1 } },
])
Actually you have lost the bookAuthor field in the $group stage. You have to use $first accumulator to get it in the next $project stage.
{ "$group": {
"_id": { "author": "$authorId" },
"totalSalePrice": { "$sum": "$sale.amount" },
"authorName": { "$first": "$bookAuthor" }
}},
{ "$project": {
"author": "$_id.author",
"totalSalePrice": "$totalSalePrice",
"authorName": { "$arrayElemAt": ["$bookAuthor.name", 0] }
"_id": 0,
}}