How to lookup a field with an array in nested subdocument mongodb? - javascript

I am trying to retrieve some lookup data for an embedded array in a document.
Here is a sample of the data:
{
"_id": "58a4fa0e24180825b05e14e9",
"fullname": "Test User",
"username": "testuser"
"teamInfo": {
"challenges": [
{
"levelId": "5e14e958a4fa0",
"title": "test challenge 1.1"
},
{
"levelId": "5e14e958a4fa0",
"title": "test challenge 1.2"
},
{
"levelId": "5e14e958a4fa1",
"title": "test challenge 2.1"
}
]
}
}
As you see, teamInfo.challenges is an array, containing levelId fields. These are pointing to the _id field in another collection called levels.
But how can I do to getting json response like this?
{
"_id": "58a4fa0e24180825b05e14e9",
"fullname": "Test User",
"username": "testuser"
"teamInfo": {
"challenges": [
{
"levelInfo": {
"name": "Level 1"
},
"title": "test challenge 1.1"
},
{
"levelInfo": {
"name": "Level 1"
},
"title": "test challenge 1.2"
},
{
"levelInfo": {
"name": "Level 2"
},
"title": "test challenge 2.1"
}
]
}
}
Im trying using unwind, project, and group. But im so confused.
const user = await User.aggregate([
{
$match: {_id: new mongoose.Types.ObjectId(req.user.userId)}
},
{
$lookup: {
from: 'levels',
localField: 'teamInfo.challenges.levelId',
foreignField: '_id',
as: 'challLevelInfo'
}
},
{
$group: {
_id: "$_id",
........IM CONFUSED HERE........
}
}
]);

You can use lookup pipeline to handle nested lookup
const pipeline = [
{
$match: {_id: new mongoose.Types.ObjectId(req.user.userId)}
},
{
$lookup: {
from: 'levels',
let: { level_id: "$teamInfo.challenges.levelId" },
pipeline: [
{
$match: {
$expr: {
$eq: ["$_id", "$$level_id"]
}
}
},
{
$lookup: {
from: '<level collection>',
localField: "levelId",
foreignField: "_id",
as: "levelInfo"
}
},
{
$project: {
levelInfo: {
name: "$levelInfo.name"
}
title: 1
}
}
],
as: "challenges"
},
},
{ $project: {
_id: 1,
fullname: 1,
username: 1,
teamInfo: {
challenges: "$challenges"
}
}}
]
const result = await User.Aggregate(pipeline)
hope this help !

Related

How to return single object (mongoose/mongoDB)

I have this data stored in database.
{
"_id": "62fa5aa25778ec97bc6ee231",
"user": "62f0eb5ebebd0f236abcaf9d",
"name": "Marketing Plan",
"columns": [
{
"name": "todo",
"_id": "62fa5aa25778ec97bc6ee233",
"tasks": [
{
"title": "Task Four testing 2",
"description": "This is task four",
"subtasks": [
{
"name": "wash dshes test",
"completed": false,
"_id": "62ff74bfe80b11ade2d34456"
},
{
"name": "do homework",
"completed": false,
"_id": "62ff74bfe80b11ade2d34457"
}
],
"_id": "62ff74bfe80b11ade2d34455"
}
]
},
{
"name": "doing",
"_id": "62fa5aa25778ec97bc6ee234",
"tasks": []
},
{
"name": "done",
"_id": "62fa5aa25778ec97bc6ee235",
"tasks": []
}
],
"__v":0
}
I want to be able to return a single object with the id equal to the req.params.id, in this case that would be 62ff74bfe80b11ade2d34455.
{
"title": "Task Four testing 2",
"description": "This is task four",
"subtasks": [
{
"name": "wash dshes test",
"completed": false,
"_id": "62ff74bfe80b11ade2d34456"
},
{
"name": "do homework",
"completed": false,
"_id": "62ff74bfe80b11ade2d34457"
}
],
"_id": "62ff74bfe80b11ade2d34455"
}
I researched stackoverflow and came across this potential solution: Mongoose retrieve one document from nested array which implemented the aggregate framework. But when I test this in postman, the request isn't made.
const getTask = asyncHandler(async (req, res) => {
const task = await Board.aggregate([
{
$match: {
"columns.tasks._id": req.params.id,
},
},
{
$project: {
columns: {
$first: {
$filter: {
input: "$columns.tasks",
cond: {
$eq: ["$$this._id", req.params.id],
},
},
},
},
},
},
{
$replaceRoot: {
newRoot: "$columns",
},
},
]);
});
Having an array inside an array complicates the query a bit, but here's one way to retrieve the data you want.
db.Board.aggregate([
{
$match: {
"columns.tasks._id": req.params.id
}
},
{"$unwind": "$columns"},
{
$match: {
"columns.tasks._id": req.params.id
}
},
{
"$project": {
"task": {
"$first": {
"$filter": {
"input": "$columns.tasks",
"cond": {"$eq": ["$$this._id", req.params.id]}
}
}
}
}
},
{"$replaceWith": "$task"}
])
Try it on mongoplayground.net. [The mongoplayground.net example uses "62ff74bfe80b11ade2d34455" rather than req.params.id.]

MongoDB nested lookup with 3 levels and append new value to the result docs

I need to retrieve the entire single object hierarchy from the database as a JSON. Actually the proposal about any other solution to achive this result would be highly appriciated. I decided to use MongoDB with its $lookup support.
So I have four collections:
Users
{ "_id" : "2", "name" : "john" }
{ "_id" : "1", "name" : "Doe" }
Posts
{"_id": "2","body": "hello", likes: []},
{"_id": "1","name": "hello 4", likes: [1,]},
Comments
{"_id": "c2","body": "hello 3",postId: "1",likes: [1,2]},
{"_id": "c1","body": "hello 2",postId: "1",likes: [1,2]},
Replies
{"_id": "r1","name": "hello 4",commentId: "c1",likes: [1]},
{"_id": "r3","name": "hello five",commentId: "c2",likes: [1,2]}
I basically need to retrieve all posts with all corresponding comments and comments.replies as part of my result . My aggregation:
const posts = await PostModel.aggregate([
{
$lookup: {
from: "comments",
localField: "_id",
foreignField: "postId",
as: "comments",
},
},
{
$unwind: "$comments",
},
{
$lookup: {
from: "replies",
localField: "comments._id",
foreignField: "commentId",
as: "comments.replies",
},
},
]).sort({
createdAt: -1,
});
MongoDb PlayGround
The result is pretty weird. Some records are ok. But comments return an object . There are also some duplications on the post with _id="1".
[
{
"_id": "1",
"comments": {
"_id": "c2",
"body": "hello 3",
"likes": [
1,
2
],
"postId": "1",
"replies": [
{
"_id": "r3",
"commentId": "c2",
"likes": [
1,
2
],
"name": "hello five"
}
]
},
"likes": [
1
],
"name": "hello 4"
},
{
"_id": "1",
"comments": {
"_id": "c1",
"body": "hello 2",
"likes": [
1,
2
],
"postId": "1",
"replies": [
{
"_id": "r1",
"commentId": "c1",
"likes": [
1
],
"name": "hello 4"
}
]
},
"likes": [
1
],
"name": "hello 4"
}
]
Brief, This is my expected result.
I want to get all posts, with all comments and replies associated with them .
I want to append a count of likes likesCount:{$size:["likes"]} and since I have the user auth id(uid) ready I want to check if the user liked the post , comment or reply based on if isLiked: {$in:[ID(uid),"likes"]}
Since each post have multiple comments, after unwinding comments you need to group it together to form an array
Update
I have updated the fetch approach a lil bit like the below.
db.posts.aggregate([
{
$lookup: {
from: "comments",
localField: "_id",
foreignField: "postId",
as: "comments",
},
},
{
$unwind: "$comments",
},
{
$lookup: {
from: "replies",
localField: "comments._id",
foreignField: "commentId",
as: "replies",
},
},
{
$unwind: "$replies",
},
{
"$addFields": {
"replies.countOflikes": {
$size: {
$ifNull: [
"$replies.likes",
[]
]
}
},
"replies.isLiked": {
$cond: {
if: {
$eq: [
{
$size: {
$filter: {
input: "$replies.likes",
as: "item",
cond: {
$eq: [
"$$item",
1//id of the user whom you wanna check if liked the reply
]
}
}
}
},
0
]
},
then: false,
else: true
}
}
}
},
{
$group: {
_id: "$comments._id",
postId: {
$first: "$_id"
},
body: {
$first: "$body"
},
"comments": {
$first: "$comments"
},
replies: {
$push: "$replies"
}
}
},
{
$addFields: {
"comments.replies": "$replies"
}
},
{
$group: {
_id: "$postId",
body: {
$first: "$body"
},
comments: {
$push: "$comments"
}
}
}
])
Summary of the change
Unwinded both comments and it's replies
Added new fields for displaying isLiked and countOfLikes using addFields stage
grouped twice to reform original structure of the data(first grouped by comments then posts)
https://mongoplayground.net/p/lymCfeIIy9j

Mongodb $graphLookup aggregation inconsistent ouput order and sorting

I have this aggregation operation, and it's giving me the correct output, but with an inconsistent order. When I reload, the nested output array (posteriorThread) changes the order of the documents, and there seems to be no rhyme or reason!
I'm confused why the order keeps changing, and I would like to know why it's happening, but I figured I would just sort it, which I did, but I'm having trouble grouping it back together.
I'll show you both of my broken solutions below, but essentially I want output 1 but with the correct order. I'm using mongoose, but that shouldn't make a difference.
Thanks.
1: Inconsistent order solution
const posteriorThread = await Comment.aggregate([
{
$match: {
_id: post.threadDescendant,
},
},
{
$graphLookup: {
from: 'comments',
startWith:'$threadDescendant',
connectFromField: 'threadDescendant',
connectToField: '_id',
as: 'posteriorThread',
},
},
]);
OUTPUT: 1
posteriorThread [
{
"_id": "000",
"name": "foo bar",
"text": "testing one",
"threadDescendant": "123",
"posteriorThread": [
{
"_id": "234",
"name": "foo bar",
"text": "testing four",
"threadDescendant": "345"
},
{
"_id": "345",
"name": "foo bar",
"text": "testing three",
},
{
"_id": "123",
"name": "foo bar",
"text": "testing two",
"threadDescendant": "234"
},
]
}
]
2: Correct older but lose root document
const posteriorThread = await Comment.aggregate([
{
$match: {
_id: post.threadDescendant,
},
},
{
$graphLookup: {
from: 'comments',
startWith: '$threadDescendant',
connectFromField: 'threadDescendant',
connectToField: '_id',
as: 'posteriorThread',
},
},
{
$unwind: '$posteriorThread',
},
{
$sort: { 'posteriorThread.depth': 1 },
},
{
$group: { _id: '$_id', posteriorThread: { $push: '$posteriorThread' } },
},
]);
OUTPUT 2:
posteriorThread [
{
"_id": "000",
"posteriorThread": [
{
"_id": "123",
"name": "foo bar",
"text": "testing two",
"threadDescendant": "234"
},
{
"_id": "234",
"name": "foo bar",
"text": "testing four",
"threadDescendant": "345"
},
{
"_id": "345",
"name": "foo bar",
"text": "testing three",
},
]
}
]
The $graphLookup pipeline stage doesn't offer any built-in sorting capability, thus your second approach is correct. You just need to use $first in order to preserve root object's fields. You can use $replaceRoot and special $$ROOT variable to avoid specifying each field explicitly:
{
$group: {
_id: "$_id",
posteriorThread: { $push: "$posteriorThread" },
root: { $first: "$$ROOT" }
}
},
{
$project: {
"root.posteriorThread": 0
}
},
{
$replaceRoot: {
newRoot: {
$mergeObjects: [
{ posteriorThread: "$posteriorThread" },
"$root"
]
}
}
}
Mongo Playground

How to change field from subdocument a parent field in Mongoose

I am trying to export Mongo data to XLSX which requires all the data to be in the parent map but currently I have data in this format:
[
{
"_id": "eatete",
"competition": {
"_id": "eatete"
"name": "Some competition name"
},
"members": [
{
"_id": "eatete",
"name": "Saad"
},
{
"_id": "eatete",
"name": "Saad2"
}
],
"leader": {
"name": "Saad",
"institute": {
"_id": "eatete",
"name": "Some institute name"
}
},
}
]
Ideally, the data should be:
[
{
"_id": "eatete",
"competition": "Some competition name"
"member0name": "Saad",
"member1name": "Saad2",
"leadername": "Saad",
"institute": "Some institute name"
}
]
So basically what I want is to refer the data of fields of subdocuments as if those were part of parent document, like competitions = competitions.name.
Can you please help me how can I do so using Mongoose.
Thanks
With some more aggregation trick
db.collection.aggregate([
{ "$unwind": { "path": "$members", "includeArrayIndex": "i" }},
{ "$group": {
"_id": "$_id",
"competition": { "$first": "$competition.name" },
"leadername": { "$first": "$leader.name" },
"institute": { "$first": "$leader.institute.name" },
"data": {
"$push": {
"k": { "$concat": ["members", { "$toLower": "$i" }, "name"] },
"v": "$members.name"
}
}
}},
{ "$replaceRoot": {
"newRoot": {
"$mergeObjects": ["$$ROOT", { "$arrayToObject": "$data" }]
}
}},
{ "$project": { "data": 0 }}
])
You can try below aggregation on your Model:
let resultt = await Model.aggregate([
{
$project: {
_id: 1,
competition: "$competition.name",
leadername: "$leader.name",
institute: "$leader.institute.name",
members: {
$map: {
input: { $range: [ 0, { $size: "$members" } ] },
in: {
k: { $concat: [ "member", { $toString: "$$this" }, "name" ] },
v: {
$let: {
vars: { current: { $arrayElemAt: [ "$members", "$$this" ] } },
in: "$$current.name"
}
}
}
}
}
}
},
{
$replaceRoot: {
newRoot: {
$mergeObjects: [ "$$ROOT", { $arrayToObject: "$members" } ]
}
}
},
{
$project: {
members: 0
}
}
])
Since you need to dynamically evaluate your keys based on indexes you can use $map with $range to map a list of indexes into keys of a new object. Then you can use $arrayToObject to get an object from those keys and $mergeObjects with $replaceRoot to flatten this object structure.

$lookup with deeply nested object

I am new to MongoDB and currently working on a recipe App for school which suggests diet plans. Therefore I need to join the "meal" ObjectId in the diet plan of the user (collection "Users") with the ObjectIds in the collections "Meals".
Afterwards I need to join an "ingredient" ObjectID in the "Meals" collection with the ID of the "ingredient" in the "Ingredients" collection. The problem is, that the "ingredient" ObjectID in collection "Meals" is situated in an Object with another integer variable "amount". This object is nested in an array called "ingredients" with many objects such as the one just described.
Below is my Structure:
Users
{
"_id": ObjectId("5b28cab902f28e18b863bd36"),
"username: "testUser1",
"password": "$2a$08$KjddpaSQPjp6aF/gseOhVeddYdqWJCJ4DpFwxfNgsk81G.0TOtN5i",
"dietPlans": Object
{
"dietPlanCurrent":Object
{
"monday":Object
{
"breakfast":Object
{
"meal": ObjectId("5b2b9a8bbda339352cc39ec4")
},
…
},
…
},
…
},
}
Meals
{
"_id" : ObjectId("5b2b9a8bbda339352cc39ec4"),
"name": "Gulasch-breakfast",
"cuisine": "International",
"ingredients":[
{
"ingredient": ObjectId("5b1ec0f939b55efcd4e28a2d"),
"amount": 20
},
{
"ingredient": ObjectId("5b1ec42474fc1f58d84264d4"),
"amount": 20
},
{
"ingredient": ObjectId("5b1ec42474fc1f58d84264d5"),
"amount": 20
},
…
],
"comments": [
…
]
}
Ingredients
{
{
"_id": ObjectId("5b1ec0f939b55efcd4e28a2d"),
"name": "Walnut",
"calories": 654
…
}
{
"_id": ObjectId("5b1ec0f939b55efcd4e28a3d"),
"name": "Apple",
"calories": 123
…
}
…
}
What I am trying to get is:
{
"_id": ObjectId("5b28cab902f28e18b863bd36"),
"username: "testUser1",
"password": "$2a$08$KjddpaSQPjp6aF/gseOhVeddYdqWJCJ4DpFwxfNgsk81G.0TOtN5i",
"dietPlans": Object
{
"dietPlanCurrent":Object
{
"Monday":Object
{
"breakfast":Object
{
"meal": ObjectId("5b2b9a8bbda339352cc39ec4")
"matchedIngredients": [
{
"_id": ObjectId("5b1ec0f939b55efcd4e28a2d"),
"name": "Walnut",
"calories": 654
…
}
…
]
},
…
},
…
},
…
},
}
My approach which is not working (only returning empty matchedIngredients Array)
{
$match: {
'_id': mongoose.Types.ObjectId(req.params.userId)
}
},
{
$lookup: {
from: 'meals',
localField: 'dietPlans.dietPlanCurrent.monday.breakfast.meal',
foreignField: '_id',
as: "dietPlans.dietPlanCurrent.monday.breakfast.mealObject"
}
},
{
$unwind: {
path: "$dietPlans.dietPlanCurrent.monday.breakfast.mealObject",
preserveNullAndEmptyArrays: true
}
},
{
$unwind: {
path: "$dietPlans.dietPlanCurrent.monday.breakfast.mealObject.ingredients",
preserveNullAndEmptyArrays: true
}
},
{
$lookup: {
from: 'ingredients',
localField: 'dietPlans.dietPlanCurrent.monday.breakfast.mealObject.ingredients.ingredient',
foreignField: '_id',
as: "dietPlans.dietPlanCurrent.monday.breakfast.matchedIngredients"
}
}
Help is very much appreciated. I already checked out this approach, but it somehow didn't work:
Approach that didn't work for me
Thank you very much!
What you are trying to do is not possible with mongodb version 3.4 but if you upgrade to 3.6 then you can try below aggregation
db.collection.aggregate([
{ "$match": { "_id": mongoose.Types.ObjectId(req.params.userId) } },
{ "$lookup": {
"from": Meals.collection.name,
"let": { "meal_id": "$dietPlans.dietPlanCurrent.monday.breakfast.meal" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$_id", "$$meal_id" ] } } },
{ "$unwind": "$ingredients" },
{ "$lookup": {
"from": Ingredients.collection.name,
"let": { "ingredient_id": "$ingredients.ingredient" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$_id", "$$ingredient_id" ] } } }
],
"as": "matchedIngredients"
}},
{ "$unwind": "$ingredients.matchedIngredients" },
{ "$group": {
"_id": "$_id",
"name": { "$first":"$name" },
"cuisine": { "$first":"$cuisine" },
"ingredients": { "$push":"$ingredients" }
}}
],
"as": "dietPlans.dietPlanCurrent.monday.breakfast.mealObject"
}},
{ "$unwind": "$dietPlans.dietPlanCurrent.monday.breakfast.mealObject" }
])

Categories

Resources