Sum of subdocument and document properties conditionally in MongoDB - 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

Related

Query userId for having one condition and NOT having other

Can you please help me with this?
I have these documents:
{
userId: 123,
event: 'A',
properties: {
status: 'Sent'
}
}
{
userId: 123,
event: 'B',
properties: {
status: 'Opened'
}
}
{
userId: 123,
event: 'C',
properties: {
status: 'Clicked'
}
}
I need a query to match all userId == 'A', that also registered for event 'B', but has not done event 'C' with properties.status == "Clicked".
Here's what I did so far:
db.collection.aggregate([
{ $match: { "event": "A"} },
{
$lookup: {
from: "collection",
let: { "userId": "$userId" },
pipeline: [{
$match: {
$expr: { $eq: ["$userId", "$$userId"] }
}
}],
as: "events"
}
}
This gives me this unified output:
{
userId: 123,
event: "A",
properties: {
status: 'Sent'
},
events: [
{ event: 'A' /* and all the other fields from original document */ },
{ event: 'B' /* and all the other fields from original document */ },
{
event: 'C',
properties: {
status: 'Clicked'
}
},
]
I tried to do this next, but it didn't work:
{
$match: {
$expr: {
$not: {
$and: [
{ $eq: ["$events.event", "C"] },
{ $eq: ["$events.properties.status', 'Clicked'] }
]
}
}
}
}
My expectation is that this query does not brings me userId: 123 in this case, but it's on the results.
Can someone please help me with that?
Tks!
I ended up doing this by adding these steps after the lookup. Not sure if it's the best solution, but I couldn't achieve this in any other way I've tried.
{ $unwind: "$events"},
{
$project: {
"userId": 1, "events": "$events",
"exclude": {
$cond: { if: {
$and: [
{ $eq: ["$events.event", "C"] },
{ $eq: ['$events.properties.status', 'Clicked'] }
]
}, then: 1, else: 0 }
}
}
},
{
$group: {
_id: "$userId",
exclude: { $sum: "$exclude" }
}
},
{
$match: { "exclude": 0 }
}

query sub document in 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

mongodb push values into array with $reduce

I have an document structure like this:
_id: 123,
posts: [
{
_id: 234,
likesUser: [345, 456, 567, 678]
}
]
I want to use $reduce here so my expected output should look like this:
output: {
likes: [23, 31, 12, 32], //each element is the size of the "likesUser" array
ids: ["14312", "2342", "2312", "235234"] //each element is the "_id" in the posts array
}
I have try it like this:
let result = await Post.aggregate([
{
$match: {
_id: USER
}
},
{
$project: {
posts: {
$reduce: {
input: "$posts",
initialValue: { likes: [], ids: [] },
in: {
likes: {
$push: { $size: "$$this.likesUser" }
},
ids: {
$push: "$$this._id"
}
}
}
}
}
}
]);
In my frontend i use chart.js. The yAxsis are the likes and the xAxsis are the ID's. So for every element in my posts array i need the length of likesUser and _id
But it doenst work. Has anybody an idea how to achive this?
You are very close, you just have a minor syntax error in your $reduce, Try this:
let result = await Post.aggregate([
{
$match: {
_id: USER
}
},
{
$project: {
posts: {
$reduce: {
input: "$posts",
initialValue: {likes: [], ids: []},
in: {
likes: {
$concatArrays: ["$$value.likes", [{$size: "$$this.likesUser"}]]
},
ids: {
$concatArrays: ["$$value.ids", ["$$this._id"]]
}
}
}
}
}
}
]);
You are on the right way. You need to use $mergeObjects operator to achieve this. Following query will be helpful:
db.collection.aggregate([
{
$match: {
_id: USER
}
},
{
$project: {
post: {
$reduce: {
input: "$posts",
initialValue: {
likes: [],
ids: []
},
in: {
$mergeObjects: [
"$$value",
{
likes: {
$concatArrays: [
"$$value.likes",
[
{
$size: "$$this.likesUser"
}
]
]
},
ids: {
$concatArrays: [
"$$value.ids",
[
"$$this._id"
]
]
}
}
]
}
}
}
}
}
])
MongoPlayGroundLink
I hope the above is helpful.

Returning a document with two fields from the same array in MongoDB

Given documents such as
{
_id: 'abcd',
userId: '12345',
activities: [
{ status: 'login', timestamp: '10000001' },
{ status: 'logout', timestamp: '10000002' },
{ status: 'login', timestamp: '10000003' },
{ status: 'logout', timestamp: '10000004' },
]
}
I am trying to create a pipeline such as all users that have their latest login/logout activities recorded between two timestamps will be returned. For example, if the two timestamp values are between 10000002 and 10000003, the expected document should be
{
_id: 'abcd',
userId: '12345',
login: '10000003',
logout: '10000002'
}
Of if the two timestamp values are between -1 and 10000001, the expected document should be :
{
_id: 'abcd',
userId: '12345',
login: '10000001',
logout: null
}
Etc.
I know it has to do with aggregations, and I need to $unwind, etc., but I'm not sure about the rest, namely evaluating two fields from the same document array
You can try below aggregation:
db.col.aggregate([
{
$unwind: "$activities"
},
{
$match: {
$and: [
{ "activities.timestamp": { $gte: "10000001" } },
{ "activities.timestamp": { $lte: "10000002" } }
]
}
},
{
$sort: {
"activities.timestamp": -1
}
},
{
$group: {
_id: "$_id",
userId: { $first: "$userId" },
activities: { $push: "$activities" }
}
},
{
$addFields: {
login: { $arrayElemAt: [ { $filter: { input: "$activities", as: "a", cond: { $eq: [ "$$a.status", "login" ] } } } , 0 ] },
logout: { $arrayElemAt: [ { $filter: { input: "$activities", as: "a", cond: { $eq: [ "$$a.status", "logout" ] } } } , 0 ] }
}
},
{
$project: {
_id: 1,
userId: 1,
login: { $ifNull: [ "$login.timestamp", null ] },
logout: { $ifNull: [ "$logout.timestamp", null ] }
}
}
])
We need to use $unwind + $sort + $group to make sure that our activities will be sorted by timestamp. After $unwind you can use $match to apply filtering condition. Then you can use $filter with $arrayElemAt to get first (latest) value of filtered array. In the last $project you can explicitly use $ifNull (otherwise JSON key will be skipped if there's no value)
You can use below aggregation
Instead of $unwind use $lte and $gte with the $fitler aggregation.
db.collection.aggregate([
{ "$project": {
"userId": 1,
"login": {
"$max": {
"$filter": {
"input": "$activities",
"cond": {
"$and": [
{ "$gte": ["$$this.timestamp", "10000001"] },
{ "$lte": ["$$this.timestamp", "10000004"] },
{ "$lte": ["$$this.status", "login"] }
]
}
}
}
},
"logout": {
"$max": {
"$filter": {
"input": "$activities",
"cond": {
"$and": [
{ "$gte": ["$$this.timestamp", "10000001"] },
{ "$lte": ["$$this.timestamp", "10000004"] },
{ "$lte": ["$$this.status", "logout"] }
]
}
}
}
}
}}
])

Grouping and counting across documents?

I have a collection with documents similar to the following format:
{
departure:{name: "abe"},
arrival:{name: "tom"}
},
{
departure:{name: "bob"},
arrival:{name: "abe"}
}
And to get output like so:
{
name: "abe",
departureCount: 1,
arrivalCount: 1
},
{
name: "bob",
departureCount: 1,
arrivalCount: 0
},
{
name: "tom",
departureCount: 0,
arrivalCount: 1
}
I'm able to get the counts individually by doing a query for the specific data like so:
db.sched.aggregate([
{
"$group":{
_id: "$departure.name",
departureCount: {$sum: 1}
}
}
])
But I haven't figured out how to merge the arrival and departure name into one document along with counts for both. Any suggestions on how to accomplish this?
You should use a $map to split your doc into 2, then $unwind and $group..
[
{
$project: {
dep: '$departure.name',
arr: '$arrival.name'
}
},
{
$project: {
f: {
$map: {
input: {
$literal: ['dep', 'arr']
},
as: 'el',
in : {
type: '$$el',
name: {
$cond: [{
$eq: ['$$el', 'dep']
}, '$dep', '$arr']
}
}
}
}
}
},
{
$unwind: '$f'
}, {
$group: {
_id: {
'name': '$f.name'
},
departureCount: {
$sum: {
$cond: [{
$eq: ['$f.type', 'dep']
}, 1, 0]
}
},
arrivalCount: {
$sum: {
$cond: [{
$eq: ['$f.type', 'arr']
}, 1, 0]
}
}
}
}, {
$project: {
_id: 0,
name: '$_id.name',
departureCount: 1,
arrivalCount: 1
}
}
]