query sub document in mongoDB - mongodb

I am trying to query the company name and its admin users.
This is my structure:
companies: [
{
_id: "generatedComp1Id",
name: "abc",
users: [
{ _id: "...", user: "userId1", roles: ["admin"]},
{ _id: "...", user: "userId2", roles: ["user"]}
]
},
{
_id: "generatedComp2Id",
name: "xyz",
users: [
{ _id: "...", user: "userId3", roles: ["admin"]},
{ _id: "...", user: "userId4", roles: ["admin"]},
{ _id: "...", user: "userId5", roles: ["user"]}
]
}
]
I'm trying to query company name and admin users for each company given a companyId.
Output:
for company ID of generatedComp1Id:
{
name: "abc",
adminUsers: [
{user: "userId1"}
]
}
for company ID of generatedComp2Id:
{
name: "xyz",
adminUsers: [
{user: "userId3"},
{user: "userId4"}
]
}
I tried to do the next thing with no success:
[
{ $match: {_id: ObjectId("generatedComp1Id")}},
{
$project: {
name: 1,
adminUsers: {
$filter: {
input: "$roles",
as: "role",
cond: {
$eq: ["$$role", "admin"]
}
}
}
}
}
]
In addition, I tried unwind:
[
{$unwind:'$users'},
{$match:{'users.roles':{$in:['admin']}}},
{$group:{_id:'$_id',users:{$push:'$users'}}},
{ $project: { name: 1 } }
]

$filter to iterate loop of users array, check condition for "admin" is in roles array
$map to iterate loop of above filtered result and return user field
[
{ $match: { _id: ObjectId("generatedComp1Id") } },
{
$project: {
name: 1,
adminUsers: {
$map: {
input: {
$filter: {
input: "$users",
as: "role",
cond: { $in: ["admin", "$$role.roles"] }
}
},
in: { user: "$$this.user" }
}
}
}
}
]
Playground

Related

Adding parent field value in subdocuments while concating

I want to add parent field value inside its subDocs
I want task name inside all of its subdocuments. Example document:
{
_id: 1,
tasks: [
{
_id: 1,
assigned: [
{
_id: 1,
name: "assigned1",
solutions: [
{_id: 1, name: "solution 1"},
{_id: 2, name: "solution 2"}
]
},
{
_id: 2,
name: "assigned2",
solutions: [
{_id: 1, name: "solution 1"},
{_id: 2, name: "solution 2"}
]
}
]
}
]
}
Below is Necessary data related to this question
Mongo PlayGround
If I understand correctly, you want to use $map and $mergeObjects for three hierarchies:
db.collection.aggregate([
{
$set: {
tasks: {
$map: {
input: "$tasks",
as: "task",
in: {
$mergeObjects: [
"$$task",
{
assigned: {
$map: {
input: "$$task.assigned",
as: "assigned",
in: {
$mergeObjects: [
"$$assigned",
{
solutions: {
$map: {
input: "$$assigned.solutions",
as: "solution",
in: {
$mergeObjects: [
"$$solution",
{
taskName: "$$assigned.name"
}
]
}
}
}
}
]
}
}
}
}
]
}
}
}
}
}
])
See how it works on the playground example

Sum of subdocument and document properties conditionally in MongoDB

I'm trying to get a sum of all room notifications + subroom notifications (this one's only if If there is a mutual element between subRoom Roles and Users[x].Roles) and getting aswell a list of all subRooms without taking into account if user has matching roles or not.
*Roles allow Users to access or not to SubRooms
Documents:
[
{
_id: 1,
id: 1,
room: "Room1",
notifications: [
{
id: 1,
read: []
},
{
id: 2,
read: []
},
{
id: 3,
read: ["User1"]
}
],
users: [{
userId: "User1",
roles: ["A", "D"]
},{
userId: "User2",
roles: ["B", "C", "D"]
}],
subRooms: [
{
id: "SubRoom1",
roles: ["A", "B", "C"]
notifications: [
{
id: 1,
read: []
},
{
id: 2,
read: ["User2"]
},
{
id: 3,
read: ["User1", "User2"]
}
]
},
{
id: "SubRoom2",
roles: ["C"]
notifications: [
{
id: 1,
read: []
},
{
id: 2,
read: []
},
{
id: 3,
read: ["User2"]
}
}
]
}
]
Expected Result:
For User1 Search:
[
{
_id: 1,
id: 1,
room: "Room1",
subRooms: ["SubRoom1", "SubRoom2"] // All subRooms without taking into account if user has matching roles or not
totalNotReadNotifications: 4 // 2 notifications of SubRoom1 + 2 notifications of Room
}
]
For User 2 Search:
[
{
_id: 1,
id: 1,
room: "Room1",
subRooms: ["SubRoom1", "SubRoom2"] // All subRooms without taking into account if user has matching roles or not
totalNotReadNotifications: 6 // 1 notification of SubRoom 1 + 2 notifications of SubRoom2 + 3 notifications of Room
}
]
Thank you very much in advance.
$project to show required fields
get main total unread notifications to count by $filter and $size operators
get filtered subRoom by $let and $filter operators
$addFields to get total unread notifications from filtered subRoom and sum with main notifications count
$unset to remove subRoom field its not needed
db.collection.aggregate([
{
$project: {
id: 1,
room: 1,
subRooms: "$subRoom.id",
totalNotReadNotifications: {
$size: {
$filter: {
input: "$notifications",
cond: { $not: { $in: ["User1", "$$this.read"] } }
}
}
},
subRoom: {
$let: {
vars: {
user: {
$arrayElemAt: [
{
$filter: {
input: "$users",
cond: { $eq: ["$$this.userId", "User1"] }
}
},
0
]
}
},
in: {
$filter: {
input: "$subRoom",
cond: {
$ne: [
{
$filter: {
input: "$$this.roles",
cond: { $in: ["$$this", "$$user.roles"] }
}
},
[]
]
}
}
}
}
}
}
},
{
$addFields: {
totalNotReadNotifications: {
$add: [
"$totalNotReadNotifications",
{
$sum: {
$map: {
input: "$subRoom",
in: {
$size: {
$filter: {
input: "$$this.notifications",
cond: { $not: { $in: ["User1", "$$this.read"] } }
}
}
}
}
}
}
]
}
}
},
{ $unset: "subRoom" }
])
Playground

Mongodb: Populate based on condition

I have some collections and I am trying to transform a log object into its details (using populate).
Companies (company with its users):
[
{
_id: "comp123",
companyId: "compName123",
users: [
{ user: "user111", status: "active"},
{ user: "user222", status: "active"},
]
},
{
_id: "comp456",
name: "compName456",
users: [
{ user: "user333", status: "active"}
]
},
{
_id: "comp789",
name: "compName789",
users: [
{ user: "user444", status: "inactive"}
]
},
]
Users:
[
{_id: "user111", firstName: "userName111"},
{_id: "user222", firstName: "userName222"},
{_id: "user333", firstName: "userName333"},
{_id: "user444", firstName: "userName444"},
]
I am trying to transform log collection into data.
examples:
For the first object of the log:
{
companyId: "comp123",
actionDetails: [
entities: [
{ id: "user111", entityType: "User"}
]
]
},
I want it to return:
{
companyId: {_id: "comp123", name: "compName123"}, // taken from companies
userId: { _id: "user111", firstName: "userName111"}, // taken from users
// Does company=comp123 that has a user with user=user111 and status=active exist?
isUserActiveInCompany: true
}
Another example of log:
{
companyId: "comp456",
actionDetails: [
entities: [
{ id: "user444", entityType: "User"}
]
]
}
Output is:
{
companyId: {_id: "comp456", name: "compName456"}, // taken from companies
userId: { _id: "user444", firstName: "userName444"}, // taken from users
isUserActiveInCompany: false // Does company=comp456 that has a user with user=user444 and status=active exist?
}
last important example of log:
{
companyId: "comp789",
actionDetails: [
entities: [
{ id: "attr333", entityType: "Attribute"}
]
]
}
Output:
{
companyId: {_id: "comp789", name: "compName789"}, // taken from companies
userId: {}, // taken from users (entityType is Attribute so we ignore it)
isUserActiveInCompany: null // entityType is Attribute so we ignore it
}
If there will be a log of comp789 with user444, isUserActiveInCompany should be false (cause the user is inactive in his company).
Currently, I do:
populate([
{
path: "actionDetails.entities.id",
select: "id firstName",
},
{
path: "companyId",
select: "name",
},
]
Any help appreciated!
Convert the below Aggregation Pipeline code to Mongoose Equivalent to get the output you desire.
db.log.aggregate([
{
'$match': {
// <-- I highly recommend that you use a `$match` condition since there are 2 lookup operators in the aggregation which will significantly increase execution time.
}
},
{
'$lookup': {
'from': 'Companies',
'let': {'cId': '$companyId'},
'pipeline': [
{
'$match': {
'$expr': {
'$eq': ['$_id', '$$cId']
}
}
},
{
"$project": {
'company': {
"_id": "$_id",
"companyName": "$companyId"
},
'users': {
"$filter": {
'input': "$users",
'as': "usr",
'cond': {
"$eq": ["$$usr.status", "active"],
},
},
},
}
},
],
'as': 'companyDetails'
}
},
{
'$unwind': {
'path': "$actionDetails",
}
},
{
'$unwind': {
'path': "$actionDetails.entities",
}
},
{
'$lookup': {
'from': 'Users',
'let': {"uId": "$actionDetails.entities.id"},
'pipeline': [
{
"$match": {
"$expr": {
"$eq": ["$_id", "$$uId"],
},
},
},
{
"$project": {
"firstName": 1,
},
},
],
'as': "userDetails",
}
},
{
'$project': {
"companyId": {"$arrayElemAt": ["$companyDetails.company", 0]},
"userId": {
"_id": "$actionDetails.entities.id",
"firstName": {"$arrayElemAt": ["$userDetails.firstName", 0]},
},
"isUserActiveInCompany": {
"$switch": {
"branches": [
{
'case': {
"$ne": ["$actionDetails.entities.entityType", "User"]
},
'then': null,
},
{
'case': {
"$in": [
"$actionDetails.entities.id",
{
"$map": {
'input': {"$arrayElemAt": ["$companyDetails.users", 0]},
'as': "elem",
'in': "$$elem.user"
}
}
]
},
'then': true,
},
],
'default': false,
}
}
}
}
], {
'allowDiskUse': true,
});
Let me know if you want a complete explanation and logic of each stage.

MongoDB $lookup on array of documents containing object id

I want to make aggregation pipeline in MongoDB v4.4.
I have two collections.
users collection
[
{ _id: "user1", username: "john" },
{ _id: "user2", username: "bob" },
]
articles collection
[
{
_id: "article1",
view_history: [
{ user: "user1", viewed_at: ISODate("...") },
{ user: "user2", viewed_at: ISODate("...") },
{ user: "user1", viewed_at: ISODate("...") },
]
}
]
My desired result after running aggregation on articles collection is:
[
{
_id: "article1",
view_history: [
{ user: "john", viewed_at: ISODate("...") },
{ user: "bob", viewed_at: ISODate("...") },
{ user: "john", viewed_at: ISODate("...") },
]
}
]
So user fields on each history item in view_history are substituted with the user's username.
I tried many answers on SO, but can't find any solution for my situation.
Here's what I got so far:
db.articles.aggregate([
{
$lookup: {
from: "users",
localField: "view_history.user",
foreignField: "_id",
as: "output",
}
}
])
Which yields:
[
{
_id: "article1",
view_history: [
{ user: "user1", viewed_at: ISODate("...") },
{ user: "user2", viewed_at: ISODate("...") },
{ user: "user1", viewed_at: ISODate("...") },
],
output: [
{ _id: "user1", username: "john" },
{ _id: "user2", username: "bob" },
]
}
]
(There's only one user1 in output, and viewed_at not included)
$unwind deconstruct view_history array
$lookup with users collection
get first element from view_history.user.username because it will return array using $arrayElemAt
$group by _id and re-construct view_history array
db.articles.aggregate([
{ $unwind: "$view_history" },
{
$lookup: {
from: "users",
localField: "view_history.user",
foreignField: "_id",
as: "view_history.user"
}
},
{
$addFields: {
"view_history.user": {
$arrayElemAt: ["$view_history.user.username", 0]
}
}
},
{
$group: {
_id: "$_id",
view_history: {
$push: "$view_history"
}
}
}
])
Playground
Second approach:
$lookup with users collection
$map to iterate loop of view_history array
$filter to iterate loop of output array and match user field
$let declare variable u for above filter, in to get first element from filtered result using $arrayElemAt
db.articles.aggregate([
{
$lookup: {
from: "users",
localField: "view_history.user",
foreignField: "_id",
as: "output"
}
},
{
$project: {
view_history: {
$map: {
input: "$view_history",
as: "v",
in: {
user: {
$let: {
vars: {
u: {
$filter: {
input: "$output",
cond: { $eq: ["$$this._id", "$$v.user"] }
}
}
},
in: { $arrayElemAt: ["$$u.username", 0] }
}
},
viewed_at: "$$v.viewed_at"
}
}
}
}
}
])
Playground

MongoDB Aggregation to $group and conditionnally $addToSet

Given the following dummy collection, I want to extract exporting and importing countries for a given resource:
[{
country: "France",
exchange: {
export: [{
resource: "MILK",
origin: ["Toulouse", "Bordeaux"]
}],
import: [{
resource: "BEEF",
origin: ["Lyon", "Marseille"]
}]
}
}, {
country: "Spain",
exchange: {
export: [{
resource: "PORK",
origin: ["Madrid", "Barcelona"]
}],
import: [{
resource: "MILK",
origin: ["Valencia", "Bilbao"]
}]
}
}]
Expected result:
{
resource: "MILK",
exportingCountries: ["France"],
importingCountries: ["Spain"]
}
I've been playing with $group but I can't find a way to conditionnally $addToSet countries.
You can use $concatArrays to combine exchange.export and exchange.import arrays. This allows you to $group by country and then you need to get back import and export using $filter and $map operators, try:
db.col.aggregate([
{
$project: {
country: 1,
resources: {
$concatArrays: [
{ $map: { input: "$exchange.export", in: { resource: "$$this.resource", exchange: "export" } } },
{ $map: { input: "$exchange.import", in: { resource: "$$this.resource", exchange: "import" } } },
]
}
}
},
{
$unwind: "$resources"
},
{
$group: {
_id: "$resources.resource",
resources: { $addToSet: { country: "$country", exchange: "$resources.exchange" } }
}
},
{
$project: {
_id: 0,
resource: "$_id",
importingCountries: {
$map: {
input: { $filter: { input: "$resources", as: "r", cond: { $eq: [ "$$r.exchange", "import" ] } } },
in: "$$this.country"
}
},
exportingCountries: {
$map: {
input: { $filter: { input: "$resources", as: "r", cond: { $eq: [ "$$r.exchange", "export" ] } } },
in: "$$this.country"
}
}
}
}
])
Output:
{ "resource" : "PORK", "importingCountries" : [ ], "exportingCountries" : [ "Spain" ] }
{ "resource" : "BEEF", "importingCountries" : [ "France" ], "exportingCountries" : [ ] }
{ "resource" : "MILK", "importingCountries" : [ "Spain" ], "exportingCountries" : [ "France" ] }