How we can flatten lookup collection array of objects in mongodb? - node.js

Given 3 collections, I want to generate a single output containing an array of the selected data. Is it possible to do so?
The collections:
courses:
{
_id: ObjectId("623f0caa35e835e56639ecbf"),
course_id: "ff08780a-b72b-4cb5-948a-7bc34023d750",
name: "MERN Stack Zero To Hero with Full Project",
description: "Description ofCourse goes here",
cost_price: 1999.99,
created_by: "a747e664-92f0-4607-b467-5eab187db0e8",
updatedAt: ISODate("2022-03-27T08:56:14.518+0000"),
createdAt: ISODate("2022-03-26T12:52:58.508+0000"),
__v: NumberInt(0),
}
sections:
{
_id: ObjectId("624026a1866d58dcb644e66a"),
section_id: "1f0c5199-f3b5-47b5-8137-64341a4d17c9",
course_id: "ff08780a-b72b-4cb5-948a-7bc34023d750",
name: "This is Section-1",
createdAt: ISODate("2022-03-27T08:56:01.399+0000"),
updatedAt: ISODate("2022-03-27T08:57:39.088+0000"),
__v: NumberInt(0),
}
contents:
{
_id: ObjectId("624026f5866d58dcb644e686"),
content_id: "1ade4f4f-8ecd-46ac-9263-9be2e68f9c3a",
section_id: "1f0c5199-f3b5-47b5-8137-64341a4d17c9",
name: "This is first content-1",
content_url: "url of the content",
createdAt: ISODate("2022-03-27T08:57:25.977+0000"),
updatedAt: ISODate("2022-03-27T08:57:25.977+0000"),
__v: NumberInt(0),
}
Sections collections hold the courses id and contents collections hold the sectionId
My desired output is:
{
results:[
{id:7556922,title:"This is Section-1 Title",description:"this section description"},
{id:31559070,title:"Section 1 Content title content",description:"",},
{id:31559072,title:"Section 1 content title content",description:""},
{id:7556922,title:"This is Section-2 Title",description:"this section description"},
{id:31559070,title:"Section 2 Content title content",description:"",},
{id:31559072,title:"Section 2 content title content",description:"",},
{id:31559072,title:"Section 2 content title content",description:"",},
{id:31559072,title:"Section 2 content title content",description:"",},
{id:31559072,title:"Section 2 content title content",description:"",}
]
}
This is a piece of code I wrote up to now. Sadly it's not complete and I need help to achieve the desired result:
db.courses.aggregate([
{
$lookup: {
from: "sections",
let: { course_id: "$course_id" },
pipeline: [
{ $match: { $expr: { $eq: ["$course_id", "$$course_id"] } } },
{
$lookup: {
from: "contents",
let: { section_id: "$section_id" },
pipeline: [
{ $match: { $expr: { $eq: ["$section_id", "$$section_id"] } } },
],
as: "contents",
},
},
],
as: "sections",
},
},
{ $unwind: "$sections" },
]);
Any help would be very much appreciated, Thanks you very much in advance.

Related

MongoDB : Push or merge one lookup's result in other lookup's result

I have 3 collection namely constants, activities, applications with mentioned properties.
Now, querying constants collection with activities and activities with applications with matching Id's. I am getting correct results. But now activity_types are shown at per data level.
But expecting the output should be at per item level inside data whichever is matching with item. Because activities are matching for Item and it should be shown in per item level not at data level. I tried with $push and $group but not getting expected results.
Constants
{
_id: id
value : {
categories: [
{
id: 001,
title: "test 1"
},
{
id: 002,
title: "test 2"
},
{
id: 003,
title: "test 3"
}
]
}
}
Activity
{
propert1: "",
propert2: "",
config: {
agenda_item_category_ids: [ObjectId(001), ObjectId(002)]
},
activity_type_id: ObjectId(123)
}
{
propert1: "",
propert2: "",
activity_type_id: ObjectId(456)
config: {
agenda_item_category_ids: [ObjectId(002)]
}
}
applications
{
_id: ObjectId(123),
prop1: "",
prop2: ""
}
{
_id: ObjectId(456),
prop1: "",
prop2: ""
}
Current query
const results = await Constants.aggregate([
{
$match: query,
},
{
$unwind: {
path: '$value.categories',
preserveNullAndEmptyArrays: true,
},
},
{
$lookup: {
from: 'activity',
localField: 'value.categories.id',
foreignField: 'config.agenda_item_category_ids',
as: 'data',
},
},
{
$lookup: {
from: 'applications',
localField: 'items.activity_type_id',
foreignField: '_id',
as: 'activity_type',
},
},
{
$project: {
_id: 0,
category_id: '$value.categories.id',
title: '$value.categories.title',
description: '$value.categories.description',
icon_src: '$value.categories.icon_src',
data: 1,
activity_type: 1,
},
},
]);
Current output
[
{
data: [
{item1},
{item2}
],
activity_type,
title
_id
},
{
data: [
{item1},
{item2}
],
activity_type,
title
_id
}
]
Expected output
[
{
data: [
{
item1,
activity_type
},
{
item2,
activity_type
}
],
title
_id
},
]
Tried method
{
"_id": "$_id",
"activity_type": {
"$push": "$activity_type"
}
}

MongoDB $ifNull empty array still adding data

I have 2 collections, users and tracks. When I fetch the user profile I want to get his tracks.
The user object has tracks as an array of track IDs, and the second collection is of the tracks.
Now I am running this code below:
users
.aggregate([
{
$match: { _id: ObjectId(userId) },
},
{
$lookup: {
from: "tracks",
localField: "tracks",
foreignField: "_id",
as: "tracks",
},
},
{
$unwind: {
path: "$tracks",
preserveNullAndEmptyArrays: true,
},
},
{
$group: {
_id: ObjectId(userId),
username: { $first: "$username" },
profileImg: { $first: "$profileImg" },
socialLinks: { $first: "$socialLinks" },
tracks: {
$push: {
_id: "$tracks._id",
name: "$tracks.name",
categoryId: "$tracks.categoryId",
links: "$tracks.links",
mediaId: isLogged ? "$tracks.mediaId" : null,
thumbnailId: "$tracks.thumbnailId",
views: { $size: { $ifNull: ["$tracks.views", []] } },
downloads: { $size: { $ifNull: ["$tracks.downloads", []] } },
uploadedDate: "$tracks.uploadedDate",
},
},
},
},
])
In case the user does not have tracks or there are no tracks, the $ifNull statement returns an object only with these fields so it looks like that:
user: {
// user data
tracks: [{ views: 0, downloads: 0, mediaId: null }]
}
There are no tracks found so "tracks.views" cannot be read so I added the $ifNull statement, how can I avoid it from returning an empty data? also, the API call knows whether the user is logged or not (isLogged), I set the mediaId to null.
If there are no tracks found, why does the code still add these 3 fields only? no tracks to go through them...
Edit:
Any track has downloads and views containing user IDs whom downloaded / viewed the track, the track looks like that
{
"name": "New Track #2227",
"categoryId": "61695d57893f048528d049e5",
"links": {
"youtube": "https://www.youtube.com",
"soundcloud": null,
"spotify": null
},
"_id": "616c90651ab67bbd0b0a1172",
"creatorId": "61695b5986ed44e5c1e1d29d",
"mediaId": "616c90651ab67bbd0b0a1171",
"thumbnailId": "616c90651ab67bbd0b0a1170",
"plays": [],
"status": "pending",
"downloads": [],
"uploadedDate": 1634504805
}
When the fetched user doesn't have any track post yet, I receive an array with one object that contains the fields mentioned above.
What I expect to get when the user has no tracks is an empty array, as you can see the response above, the track object isn't full and contains only the conditional keys with zero value and null.
Bottom line I want the length of views and downloads only if there is a track or more, also for the mediaId which I want to hide it in case the user isn't logged. If there are no tracks I don't understand why it returns these 3 fields
expected result when the user has one track or more
user: {
// user data
tracks: [
{
name: "New Track #2227",
categoryId: "61695d57893f048528d049e5",
links: {
youtube: "https://www.youtube.com",
soundcloud: null,
spotify: null,
},
_id: "616c90651ab67bbd0b0a1172",
creatorId: "61695b5986ed44e5c1e1d29d",
mediaId: "616c90651ab67bbd0b0a1171",
thumbnailId: "616c90651ab67bbd0b0a1170",
plays: 0,
status: "pending",
downloads: 0,
uploadedDate: 1634504805,
},
];
}
expected result when the user has no tracks
user: {
// user data
tracks: [];
}
Append this stage to your pipeline:
{
$set: {
tracks: {
$filter: {
input: "$tracks",
cond: { $ne: [ { $type: "$$this._id" }, "missing" ] }
}
}
}
}
You could also modify your query to look something like this: (check out a live demo here)
This query uses a conditional push via $cond coupled with the $and operator to search for more than one condition. If tracks.downloads or tracks.plays are not greater than 0, we use the $$REMOVE variable (which just ignores that document and returns an empty array, like you are looking for).
Query
db.users.aggregate([
{
$match: {
_id: ObjectId("616c80793235ab5cc26dbaff")
},
},
{
$lookup: {
from: "tracks",
localField: "tracks",
foreignField: "_id",
as: "tracks"
}
},
{
$unwind: {
path: "$tracks",
preserveNullAndEmptyArrays: true
}
},
{
$group: {
_id: ObjectId("616c80793235ab5cc26dbaff"),
username: {
$first: "$username"
},
profileImg: {
$first: "$profileImg"
},
socialLinks: {
$first: "$socialLinks"
},
tracks: {
$push: {
// "IF" plays and downloads > 0
$cond: [
{
$and: [
{
$gt: [
"$tracks.plays",
0
]
},
{
$gt: [
"$tracks.downloads",
0
]
},
]
},
// "THEN" return document
{
_id: "$tracks._id",
name: "$tracks.name",
categoryId: "$tracks.categoryId",
links: "$tracks.links",
mediaId: "$tracks.mediaId",
thumbnailId: "$tracks.thumbnailId",
plays2: {},
plays: "$tracks.plays",
downloads: "$tracks.downloads",
uploadedDate: "$tracks.uploadedDate"
},
// "ELSE" remove
"$$REMOVE"
]
}
}
}
}
])

Best a very overloaded query mongodb - Nodejs; aggregate, group, porject and lookup

its my first question in this forum. (I am a Spanish speaker, sorry for my basic English)
I have 3 collections: images, artists and exhibitions.
Images has: filename (unique), path and _id and etc.
Artists has: name, url (unique) and description and etc
Exhibitions has: name, url (unique), year, abstract, aristUrl (artist.url) imageCover (image.fieldname) and etc.
Whit my query this is my summary result:
[
_id: null,
documents: [{...}, {...}],
totalExhibitions: 2
]
Full result
[
{
"_id": null,
"documents": [
{
"_id": "5e84d6599891212db0a6dc7e",
"url": "chile",
"imageCover": [
{
"path": "http://localhost:2616/uploads/images/1585763637647.jpg"
}
],
"name": "Almacén Verdad y Justicia",
"year": 2010,
"releaseDate": "2010-08-30T00:00:00.000Z",
"artist": [
{
"name": "Bernardo de Castro Saavedra",
"url": "bernardo-castro-saavedra"
}
]
},
{
"_id": "5e84e0575a3f201aac2df1c2",
"url": "sin-cera",
"imageCover": [
{
"path": "http://localhost:2616/uploads/images/1585766437587.jpg"
}
],
"name": "Sin cera",
"year": 2020,
"releaseDate": "2020-01-31T00:00:00.000Z",
"artist": [
{
"name": "Gonzalo Tapia",
"url": "gonzalo-tapia"
}
]
}
],
"totalExhibitions": 2
}
]
This is my code
getByLastYear(){
const documents = this.mongoDB.aggregate([
{
$addFields: {
"artistObjectId": { $toObjectId: "$artistId" }
}
},{
$lookup: {
from: 'artists',
localField: 'artistObjectId',
foreignField: "_id",
as: "artist"
},
},{
$lookup: {
from: 'images',
localField: 'imageCover',
foreignField: "filename",
as: "imageCover"
},
},{
$project: {
name: 1,
year: 1,
url: 1,
releaseDate: 1,
artist: {
name: 1,
url: 1
},
imageCover: {
path: 1,
alt: 1,
}
}
}, {
$group: {
_id: null,
documents: {
$push: "$$ROOT"
},
totalExhibitions: {
$sum: 1
}
}
}
]);
return documents || [];
};
that is the best form to get my result?
is there any better?
Thank you for your comments and opinions.<3
Assuming that:
you're starting from the exhibitions collection;
the "_id" fields have an ObjectId format;
there's only 1 artist and 1 cover image for each exhibition...
I'd skip the first step and add in $unwind stages to have the artist and the cover image as a subdocument instead of an array.
The reason for that is that it will make it easier to reference the result.
This should be able to run in your Mongo shell:
db.exhibitions.aggregate([
{ $lookup: {
from: 'artists',
localField: "artistId",
foreignField: "_id",
as: "artist"
}},
{ $unwind: {
path: "$artist",
preserveNullAndEmptyArrays: true
}},
{ $lookup: {
from: 'images',
localField: 'imageCover',
foreignField: "filename",
as: "imageCover"
}},
{ $unwind: {
path: "$imageCover",
preserveNullAndEmptyArrays: true
}},
{ $project: {
name: 1,
year: 1,
url: 1,
releaseDate: 1,
artist: {
name: "$artist.name",
url: "$artist.url"
},
imageCover: {
path: "$imageCover.path",
alt: "$imageCover.alt",
}
}},
{ $group: {
_id: null,
documents: {
$push: "$$ROOT"
},
totalExhibitions: {
$sum: 1
}
}}
]);
In the "$project" stage, for artist and imageCover, you would need to specify the full path to build the reduced subdocument.
Hope that answered your question...

How to find Mongoose data recursively?

I am newbie in MEANJS and i have a problem i.e, there are collection called employee and have multiple documents with their boss field. Now i want get all employees with their lower level.
For example:-
1) {_id:ObjectId('587dcd3edca5f235f862fdfd'), name:'John'} //he doesn't have boss
2) {_id:ObjectId('587dcd3edca5f235f86dddew'), name: 'Jimmy', 'boss': ObjectId('587dcd3edca5f235f862fdfd')} //john is boss
3) {_id:ObjectId('587dcd3edca5f235f863ew'), name: 'David', 'boss': ObjectId('587dcd3edca5f235f86dddew')} //john,Jimmy are bosses
4) {_id:ObjectId('587dcd3edca5f235f86qwa'), name: 'Dyan', 'boss': ObjectId('587dcd3edca5f235f86dddew')} //john,Jimmy,David are bosses
5) {_id:ObjectId('587dcd3edca5f235f8ew32'), name:'Jack', 'boss': ObjectId('587dcd3edca5f235f862fdfd')} //john is boss
6) {_id:ObjectId('587dcd3edca5f2wsw23rlot'), name: 'Loren', 'boss':ObjectId('587dcd3edca5f235f8ew32')} //john,Jack is boss
If we take
Jonh then output will ['Jimmy','Jack','David','Dyan','Loren']
Jack then output will ['Loren']
Here is my try code:-
getBosses(user._id)
function getBosses(id){
User.find({boss:id})
.exec(function(err,users){
if(err)
return console.log(err);
//How handle here 'users' array
//for something getBosses call recursively
})
}
As far as I understood you need to find all subordinates of that people. I think the best way to do it is using $graphLookup.
db.bosses.insertMany([
{ _id: "587dcd3edca5f235f862fdfd", name: "John" },
{
_id: "587dcd3edca5f235f86dddew",
name: "Jimmy",
boss: "587dcd3edca5f235f862fdfd",
},
{
_id: "587dcd3edca5f235f863ew",
name: "David",
boss: "587dcd3edca5f235f86dddew",
},
{
_id: "587dcd3edca5f235f86qwa",
name: "Dyan",
boss: "587dcd3edca5f235f86dddew",
},
{
_id: "587dcd3edca5f235f8ew32",
name: "Jack",
boss: "587dcd3edca5f235f862fdfd",
},
{
_id: "587dcd3edca5f2wsw23rlot",
name: "Loren",
boss: "587dcd3edca5f235f8ew32",
},
]);
db.bosses.aggregate([
{
$graphLookup: {
from: "bosses",
startWith: "$_id",
connectFromField: "_id",
connectToField: "boss",
as: "subordinates",
},
},
{
$project: {
_id: false,
name: true,
subordinates: {
$reduce: {
input: "$subordinates",
initialValue: "",
in: { $concat: ["$$value", ", ", "$$this.name"] },
},
},
},
},
{
$project: {
name: true,
subordinates: { $substrBytes: ["$subordinates", 2, -1] },
},
},
]);
The result of the last one is:
[
{ name: 'John', subordinates: 'David, Dyan, Loren, Jack, Jimmy' },
{ name: 'Jimmy', subordinates: 'David, Dyan' },
{ name: 'David', subordinates: '' },
{ name: 'Dyan', subordinates: '' },
{ name: 'Jack', subordinates: 'Loren' },
{ name: 'Loren', subordinates: '' }
]
The most important thing is $graphLookup stage of the aggregate pipeline. Last two $project stages is just response formatting - return only name and subordinates as string field with comma separated names.
To get data for a specific person you can use $match stage before $graphLookup like that:
db.bosses.aggregate([
{ $match: { name: "John" } },
{
$graphLookup: ...

mongodb aggregation: average and sorting

I am quite a newbie and have a problem for mongodb aggregation. I used mongoose.
var SubjectScore = new Schema({
name: {type:String, required:true}, //math, science, history, ...
score: {type:Number, required:true } // 95, 85, 77,....
});
var Subject = new Schema({
year: Number, //2012, 2013, 2014
subjectScore : [SubjectScore]
});
var StudentSchema = new Schema({
name: String,
subject: [Subject], //array length varies for each student
profile: String,
});
So, Input data is like this.
{ _id: 54c921aaa7918d4e4a8c7e51,
name: John,
profile: "He is nice",
subject: [{ year: 2010,
subjectScore: [{ name:"history" score:66},
{ name:"math", score:65},
{ name:"science", score:87}] }]
},{ year: 2011,
subjectScore: [{ name:"history" score:75},
{ name:"math", score:61},
{ name:"science", score:92}] }]
},{ year: 2012,
subjectScore: [{ name:"history" score:83},
{ name:"math", score:82},
{ name:"science", score:86}] }]
},{ year: 2013,
subjectScore: [{ name:"history" score:77},
{ name:"math", score:99},
{ name:"science", score:71}] }]
}]
}
The final result I want to get is like follows.
[
{ _id: "54c921aaa7918d4e4a8c7e51",
name: "John"
profile: "He is nice",
avgScore: [
{name: "math", score: 77},
{name:"history", score:78},
{name:"science", score:86} ]
totalAvg: 82
},
{ _id: "54c921aaa7918d4e4a8c7e5b",
name: "Mary"
profile: "She is kind",
avgScore: [
{name: "math", score: 67},
{name:"history", score:99},
{name:"science", score:96} ]
totalAvg: 82
},
{ _id: "54c921aaa7918d4e4a8c7e56",
name: "Jane"
profile: "She is smart",
avgScore: [
{name: "math", score: 99},
{name:"history", score:99},
{name:"science", score:99} ],
totalAvg: 99
}
..... // 7 more student for first page result
]
I tried following but I couldn't get the field of name, profile. So I needed additional query for getting name&profile fields and sorting again.
{$project:{subject:1}},
{$unwind:"$subject"},
{$unwind:"$subject.subjectScore"},
{$group:{_id:{studentId:"$_id", subjectName:"$subject.subjectScore.name"},
avgScore:{$avg: "$subject.subjectScore.score"}}},
{$group:{_id:"$_id.studentId",
avgScore:{$push: {name:"$_id.subjectName", score:"$avgScore"}},
totalAvg:{$avg:"$avgScore"}}},
{$sort:{totalAvg:-1}},
{$limit:10} // for first page (students per page : 10)
I want know how to keep the fields which is not necessary for aggregation but need to show as result. If it's not possible, do I need additional queries for result I want?
Are there another way to get this result considering performance?
I've already put a few days for this job and googling, but didn't get an answer. Help me please.
Thank you.
You are losing the information in your first operation because the $project is only sending along the value of the subject key.
The operation to keep the value of fields unaffected by aggregation calculations (name and profile) is with $first. It will just grab the first value of that field which should be the same as the rest of them.
{ $unwind: "$subject" },
{ $unwind: "$subject.subjectScore" },
{ $group: { _id: { sId: "$_id", subjectName: "$subject.subjectScore.name" },
avgScore: { $avg: "$subject.subjectScore.score" },
name: { $first: "$name" },
profile: { $first: "$profile" } } },
{ $group: { _id: "$_id.sId",
name: { $first: "$name" },
profile: { $first: "$profile" },
avgScore: { $push: { name: "$_id.subjectName", score: "$avgScore" } },
totalAvg: { $avg: "$avgScore" } } }
{ $sort: { totalAvg: -1 } }, // this is sorting desc, but sample is asc?
{ $limit: 10 }

Resources