How to find Mongoose data recursively? - node.js

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: ...

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

Group nested fields with aggregation and return results with others fields mongo db

I need to change the structure of some field in my mongoDB document.
Here the sample:
[
{
_id: "ObjectId('997v2ha1cv9b0036fa648zx3')",
title: "Adidas Predator",
size: "8",
colors: [
{
hex: "005FFF",
name: "Blue"
},
{
hex: "FF003A",
name: "Red"
},
{
hex: "FFFE00",
name: "Yellow"
},
{
hex: "07FF00",
name: "Green"
},
],
extras: [
{
description: "laces",
type: "exterior"
},
{
description: "sole",
type: "interior"
},
{
description: "logo"
},
{
description: "stud",
type: "exterior"
}
],
media: {
images: [
{
url: "http://link.com",
type: "exterior"
},
{
url: "http://link3.com",
type: "interior"
},
{
url: "http://link2.com",
type: "interior"
},
{
url: "http://link4.com",
type: "exterior"
}
]
}
}
];
My goal is to group some fields:
colors need to be and array with just the colors,
extras need to be an array with 3 object each one for a "type" (interior, exterior, null)
the same for images that is inside media
Here what I expected:
{
_id: "ObjectId('997b5aa1cv9b0036fa648ab5')",
title: "Adidas Predator",
size: "8",
colors: ["Blue", "Red", "Yellow", "Green"],
extras: [
{type: exterior, description: ["laces", "stud"]},
{type: interior, description: ["sole"]},
{type: null, description: ["logo"]}
],
images: [
{type: exterior, url: ["http://link.com", "http://link4.com"]},
{type: interior, url: ["http://link2.com", "http://link3.com"]},
]
};
With my code I can achieve my goal but I don't understand how to show all the information together through the pipeline.
Here my code:
db.collection.aggregate([
{
$project: {
title: 1,
size: 1,
colors: "$colors.name",
extras: 1,
media: "$media.images"
},
},
{
$unwind: "$media"
},
{
$group: {
_id: {
type: "$media.type",
url: "$media.url",
},
},
},
{
$group: {
_id: "$_id.type",
url: {
$push: "$_id.url"
},
},
},
]);
The result is:
[
{
_id: "exterior",
url: [
"http://link.com",
"http://link4.com"
]
},
{
_id: "interior",
url: [
"http://link3.com",
"http://link2.com"
]
}
];
If I do the same thing with extras I get the same (correct) structure.
How can I show all the data together like in the expected structure?
Thanks in advice.
The strategy will be to maintain the require parent fields throughout the pipeline using $first to just grab the initial value, It ain't pretty but it works:
db.collection.aggregate([
{
"$addFields": {
colors: {
$map: {
input: "$colors",
as: "color",
in: "$$color.name"
}
}
}
},
{
$unwind: "$extras"
},
{
"$addFields": {
imageUrls: {
$map: {
input: {
$filter: {
input: "$media.images",
as: "image",
cond: {
$eq: [
"$$image.type",
"$extras.type"
]
}
}
},
as: "image",
in: "$$image.url"
}
}
}
},
{
$group: {
_id: {
_id: "$_id",
extraType: "$extras.type"
},
extraDescriptions: {
"$addToSet": "$extras.description"
},
imageUrls: {
"$first": "$imageUrls"
},
colors: {
$first: "$colors"
},
size: {
$first: "$size"
},
title: {
$first: "$title"
}
}
},
{
$group: {
_id: "$_id._id",
colors: {
$first: "$colors"
},
size: {
$first: "$size"
},
title: {
$first: "$title"
},
images: {
$push: {
type: {
"$ifNull": [
"$_id.extraType",
null
]
},
url: "$imageUrls"
}
},
extras: {
$push: {
type: {
"$ifNull": [
"$_id.extraType",
null
]
},
description: "$extraDescriptions"
}
}
}
}
])
Mongo Playground
You can try $function operator, to defines a custom aggregation function or expression in JavaScript.
$project to show required fields and get array of colors name
$function, write your JS logic if you needed you can sort this logic of group, it will return result with 2 fields (extras, images)
$project to show required fields and separate extras and images field from result
db.collection.aggregate([
{
$project: {
title: 1,
size: 1,
colors: "$colors.name",
result: {
$function: {
body: function(extras, images) {
function groupBy(objectArray, k, v) {
var results = [], res = objectArray.reduce((acc, obj) => {
if (!acc[obj[k]]) acc[obj[k]] = [];
acc[obj[k]].push(obj[v]);
return acc;
}, {});
for (var o in res) {
results.push({ [k]: o === 'undefined' ? null : o, [v]: res[o] })
}
return results;
}
return {
extras: groupBy(extras, 'type', 'description'),
images: groupBy(images, 'type', 'url')
}
},
args: ["$extras", "$media.images"],
lang: "js"
}
}
}
},
{
$project: {
title: 1,
size: 1,
colors: 1,
extras: "$result.extras",
images: "$result.images"
}
}
])
Playground
IMPORTANT:
Executing JavaScript inside an aggregation expression may decrease performance. Only use the $function operator if the provided pipeline operators cannot fulfill your application's needs.

I need mongodb group pipeline for multiple condition

Document
[
{
type: 1,//credit
amount: 60
},
{
type: 2,//debit
amount: 35
},
{
type: 3,//credit
amount: 25
},
{
type: 4,//debit
amount: 80
},
{
type: 5,//credit
amount: 70
},
]
Result
[
{
_id: {
Name: "Credition",
Type: [1, 3, 5]
},
Total_Amount: 155
},
{
_id: {
Name: "Debition",
Type: [2, 4]
},
Total_Amount: 115
},
]
In my schema, there are millions of logs records in which few are credited logs, few are debited logs.
I want to use MongoDB aggregate pipe and have to group like above for million records at a time
Yes you can do that first you need to add a new field transaction on the basis of the type of logs, then you can group the logs on the basis of that field.
Working example - https://mongoplayground.net/p/e4kqeKLIuIr
db.collection.aggregate([
{
$addFields: {
transaction: {
$cond: {
if: {
$in: [
"$type",
[
1,
3,
5
]
]
},
then: "Credition",
else: "Debition"
}
}
}
},
{
$group: {
_id: "$transaction",
Type: {
$addToSet: "$type"
},
Total_Amount: {
$sum: "$amount"
}
}
}
])
After this, you can also use $project operator to change the name or structure of the record, if needed
You can use the operator $cond during the grouping stage:
db.collection.aggregate([
{
$group: {
_id: {
$cond: [
{
$in: [ "$type", [1,3,5] ]
},
"Credition",
"Debition"
]
},
type: {
$addToSet: "$type"
},
amount: {
$sum: "$amount"
}
}
},
{
$project: {
_id: {
Name: "$_id",
Type: "$type"
},
Total_Amount: "$amount"
}
}
])
MongoPlayground

Finding in Mongoose with specific parametres

I have a database in Mongodb filled with profiles with different skills.
Here's an example of one profile.
{_id: 570b8b5afdcaf27c24a0a837,
identifier: 'Mr X',
url: 'https://MRX.com',
email: 'Mrx#gmail.com'
skills:
[ { _id: 570b8b5afdcaf27c24a0a858, title: 'Java', number: '74' },
{ _id: 570b8b5afdcaf27c24a0a857, title: 'Linux', number: '48' },
{ _id: 570b8b5afdcaf27c24a0a856, title: 'C++', number: '43' },
{ _id: 570b8b5afdcaf27c24a0a855, title: 'SQL', number: '34' },
{ _id: 570b8b5afdcaf27c24a0a854, title: 'XML', number: '28' },
{ _id: 570b8b5afdcaf27c24a0a853, title: 'MySQL', number: '23' },
{ _id: 570b8b5afdcaf27c24a0a852, title: 'C', number: '22' },
{ _id: 570b8b5afdcaf27c24a0a851,
title: 'Java Enterprise Edition',
number: '18' }]
}
My question: is there a query in mongoose where I can find a profile who has linux in his skills but also the number of that linux skill is greater than 40?
I tried with something like this in the finding option :
var x = {
'skills.title': 'Linux',
'skills.number': {
$gt: 40
},
}
but it doesn't work,the program finds the Linux skill but number 40 is not associated with Linux.
So is there a query to solve my problem?
Use $elemMatch:
db.yourCollection.find(
{ skills: { $elemMatch: { title: "Linux", number: { $gte: 40 } } } }
)
Taken from the docs:
The $elemMatch operator matches documents that contain an array field
with at least one element that matches all the specified query
criteria.
In mongoose you should do something like this:
var myCol = require('.myModel');
myCol.aggregate(
[
{$match:
{
title: "Linux",
number: { $gte: 40 }
}
}
], function (err, result) {
if (err) {
console.log(err);
} else {
console.log(result);
}
});

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