Combine array values from collection and $lookup result - mongodb

I have two collections, resource-policies and role-permissions. I'm trying to get all the permissions for resource1, including the ones that are defined in the matching record from role-permissions.
resource-policies
{
resource: 'resource1',
permissions: [
'permission:read',
'role:admin'
]
}
role-permissions
{
name: 'role:admin',
permissions: ['permission:write', 'permission:delete']
}
Desired Result
[
{
resource: 'resource1',
permissions: [
'permission:read',
'permission:write',
'permission:delete'
]
}
]
Here's my current attempt. I'm stuck on the $map projection to create the final combinded permissions field.
resourcePoliciesCollection.aggregate([
{
$match: {
$expr: {
$eq: ['$resource', resource]
},
},
},
{
$lookup: {
from: 'role-permissions',
localField: 'permissions',
foreignField: 'permissions',
as: 'rolePermissions',
},
},
{
$project: {
_id: -1,
permissions: {
$map: {
input: '$permissions',
in: {
$cond: [
{ $eq: ['$$this', '$rolePermissions.name'] },
'$rolePermissions.permissions',
'$$this',
],
},
},
},
resource: 1,
},
}
])
But this produces the following, which is without the permissions from role-permissions.
{
"_id": ObjectId("5a934e000102030405000000"),
"permissions": [
"permission:read",
"role:admin"
],
"resource": "resource1"
}
Mongo Playground

Your schema is suggesting a recursive structure. I prefer using $graphLookup for this reason and perform $setUnion to get all the permissions. Finally do a filter to remove the role entries(i.e. entries starting with "role:")
db.resourcepolicies.aggregate([
{
$match: {
$expr: {
$eq: [
"$resource",
"resource1"
]
}
}
},
{
"$graphLookup": {
"from": "rolepermissions",
"startWith": "$permissions",
"connectFromField": "permissions",
"connectToField": "name",
"as": "rpLookup"
}
},
{
"$project": {
_id: 0,
resource: 1,
permissions: {
"$reduce": {
"input": "$rpLookup",
"initialValue": "$permissions",
"in": {
"$setUnion": [
"$$value",
"$$this.permissions"
]
}
}
}
}
},
{
"$addFields": {
"permissions": {
"$filter": {
"input": "$permissions",
"as": "p",
"cond": {
// remove permissions entry which contains "role:"
$eq: [
-1,
{
"$indexOfCP": [
"$$p",
"role:"
]
}
]
}
}
}
}
}
])
Mongo Playground

Related

Replace items in array with property from matching object in another array

There's a permissions collection that contains permissions and the users or groups that are assigned that permission for a given resource.
permissions
[{
"_id": 1,
"resource": "resource:docs/61",
"permissions": [
{
"permission": "role:documentOwner",
"users": [
"user:abc",
"user:def",
"group:abc",
"group:bff"
]
},
{
"permission": "document.read",
"users": ["user:xxx"]
},
{
"permission": "document.update",
"users": ["user:xxx"]
}
]
}]
And a groups collection that assigns users to a group.
groups
[
{
"_id": 1,
"id": "abc",
"name": "Test Group",
"users": ["cpo", "yyy"]
},
{
"_id": 2,
"id": "bff",
"name": "Another Group",
"users": ["xxx"]
}
]
I'm trying to query the permissions collection for resource:docs/61 and for each permission, resolve any groups in the users property to the matching group's users. See below for desired result.
Desired Result
{
"resource": "resource:docs/61",
"permissions": [
{
"permission": "role:documentOwner",
"users": [
"user:abc",
"user:def",
"user:cpo",
"user:yyy",
"user:xxx"
]
},
{
"permission": "document.read",
"users": ["user:xxx"]
},
{
"permission": "document.update",
"users": ["user:xxx"]
}
]
}
I've setup a Mongo Playground where I've been trying to get this to work... unsuccessfully. Below is my current attempt. I'm unsure how to map the groups to their respectful users and then reverse the $unwind. Or maybe I don't even need the $unwind 🤷‍♂️
db.permissions.aggregate([
{
"$match": {
"resource": "resource:docs/61"
}
},
{
$unwind: "$permissions"
},
{
"$lookup": {
"from": "groups",
"let": {
"users": {
"$filter": {
"input": "$permissions.users",
"as": "user",
"cond": {
"$ne": [
-1,
{
"$indexOfCP": [
"$$user",
"group:"
]
}
]
}
}
}
},
"pipeline": [
{
"$match": {
"$expr": {
"$in": [
{
"$concat": [
"group:",
"$id"
]
},
"$$users"
]
}
}
},
{
"$project": {
"_id": 0,
"id": {
"$concat": [
"group:",
"$id"
]
},
"users": 1
}
}
],
"as": "groups"
}
},
{
"$project": {
"groups": 1,
"permissions": {
"permission": "$permissions.permission",
"users": "permissions.users"
}
}
}
])
You will need to:
$unwind the permissions for easier processing in later stages
$lookup with "processed" key:
remove the prefix group: for the group key
use the $lookup result to perform $setUnion with your permissions.users array. Remember to $filter out the group entries first.
$group to get back the original / expected structure.
db.permissions.aggregate([
{
"$match": {
"resource": "resource:docs/61"
}
},
{
"$unwind": "$permissions"
},
{
"$lookup": {
"from": "groups",
"let": {
"groups": {
"$map": {
"input": "$permissions.users",
"as": "u",
"in": {
"$replaceAll": {
"input": "$$u",
"find": "group:",
"replacement": ""
}
}
}
}
},
"pipeline": [
{
$match: {
$expr: {
"$in": [
"$id",
"$$groups"
]
}
}
}
],
"as": "groupsLookup"
}
},
{
"$addFields": {
"groupsLookup": {
"$reduce": {
"input": "$groupsLookup",
"initialValue": [],
"in": {
$setUnion: [
"$$value",
{
"$map": {
"input": "$$this.users",
"as": "u",
"in": {
"$concat": [
"user:",
"$$u"
]
}
}
}
]
}
}
}
}
},
{
"$project": {
resource: 1,
permissions: {
permission: 1,
users: {
"$setUnion": [
{
"$filter": {
"input": "$permissions.users",
"as": "u",
"cond": {
$eq: [
-1,
{
"$indexOfCP": [
"$$u",
"group:"
]
}
]
}
}
},
"$groupsLookup"
]
}
}
}
},
{
$group: {
_id: "$_id",
resource: {
$first: "$resource"
},
permissions: {
$push: "$permissions"
}
}
}
])
Mongo Playground

MongoDB: how to aggregate from multiple collections with same aggregation pipeline

I'm trying to get aggregations with same aggregation pipeline including $match and $group operations from multiple collections.
For example,
with a users collection and collections of questions, answers and comments where every document has authorId and created_at field,
db = [
'users': [{ _id: 123 }, { _id: 456} ],
'questions': [
{ authorId: ObjectId('123'), createdAt: ISODate('2022-09-01T00:00:00Z') },
{ authorId: ObjectId('456'), createdAt: ISODate('2022-09-05T00:00:00Z') },
],
'answers': [
{ authorId: ObjectId('123'), createdAt: ISODate('2022-09-05T08:00:00Z') },
{ authorId: ObjectId('456'), createdAt: ISODate('2022-09-01T08:00:00Z') },
],
'comments': [
{ authorId: ObjectId('123'), createdAt: ISODate('2022-09-01T16:00:00Z') },
{ authorId: ObjectId('456'), createdAt: ISODate('2022-09-05T16:00:00Z') },
],
]
I want to get counts of documents from each collections with created_at between a given range and grouped by authorId.
A desired aggregation result may look like below. The _ids here are ObjectIds of documents in users collection.
\\ match: { createdAt: { $gt: ISODate('2022-09-03T00:00:00Z) } }
[
{ _id: ObjectId('123'), questionCount: 0, answerCount: 1, commentCount: 0 },
{ _id: ObjectId('456'), questionCount: 1, answerCount: 0, commentCount: 1 }
]
Currently, I am running aggregation below for each collection, combining the results in the backend service. (I am using Spring Data MongoDB Reactive.) This seems very inefficient.
db.collection.aggregate([
{ $match: {
created_at: { $gt: ISODate('2022-09-03T00:00:00Z') }
}},
{ $group : {
_id: '$authorId',
count: {$sum: 1}
}}
])
How can I get the desired result with one aggregation?
I thought $unionWith or $lookup may help but I'm stuck here.
You can try something like this, using $lookup, here we join users, with all the three collections one-by-one, and then calculate the count:
db.users.aggregate([
{
"$lookup": {
"from": "questions",
"let": {
id: "$_id"
},
"pipeline": [
{
"$match": {
$expr: {
"$and": [
{
"$gt": [
"$createdAt",
ISODate("2022-09-03T00:00:00Z")
]
},
{
"$eq": [
"$$id",
"$authorId"
]
}
]
}
}
}
],
"as": "questions"
}
},
{
"$lookup": {
"from": "answers",
"let": {
id: "$_id"
},
"pipeline": [
{
"$match": {
$expr: {
"$and": [
{
"$gt": [
"$createdAt",
ISODate("2022-09-03T00:00:00Z")
]
},
{
"$eq": [
"$$id",
"$authorId"
]
}
]
}
}
}
],
"as": "answers"
}
},
{
"$lookup": {
"from": "comments",
"let": {
id: "$_id"
},
"pipeline": [
{
"$match": {
$expr: {
"$and": [
{
"$gt": [
"$createdAt",
ISODate("2022-09-03T00:00:00Z")
]
},
{
"$eq": [
"$$id",
"$authorId"
]
}
]
}
}
}
],
"as": "comments"
}
},
{
"$project": {
"questionCount": {
"$size": "$questions"
},
"answersCount": {
"$size": "$answers"
},
"commentsCount": {
"$size": "$comments"
}
}
}
])
Playground link. In the above query, we use pipelined form of $lookup, to perform join on some custom logic. Learn more about $lookup here.
Another way is this, perform normal lookup and then filter out the elements:
db.users.aggregate([
{
"$lookup": {
"from": "questions",
"localField": "_id",
"foreignField": "authorId",
"as": "questions"
}
},
{
"$lookup": {
"from": "answers",
"localField": "_id",
"foreignField": "authorId",
"as": "answers"
}
},
{
"$lookup": {
"from": "comments",
"localField": "_id",
"foreignField": "authorId",
"as": "comments"
}
},
{
"$project": {
questionCount: {
"$size": {
"$filter": {
"input": "$questions",
"as": "item",
"cond": {
"$gt": [
"$$item.createdAt",
ISODate("2022-09-03T00:00:00Z")
]
}
}
}
},
answerCount: {
"$size": {
"$filter": {
"input": "$answers",
"as": "item",
"cond": {
"$gt": [
"$$item.createdAt",
ISODate("2022-09-03T00:00:00Z")
]
}
}
}
},
commentsCount: {
"$size": {
"$filter": {
"input": "$comments",
"as": "item",
"cond": {
"$gt": [
"$$item.createdAt",
ISODate("2022-09-03T00:00:00Z")
]
}
}
}
}
}
}
])
Playground link.

MongoDB 5 version Aggregation convert to 4.4 version

I have the following aggregation that is supported by MongoDB 5 but not 4.4. How can I write this in v4.4?
Aggregation Pipeline (V5):
"$ifNull": [
{
"$getField": {
"field": "prices",
"input": {
"$first": "$matchedUsers"
}
}
},
[]
]
Here's a MongoDB Playground for the same.
This pipeline should work in version 4.4:
db.datasets.aggregate([
{
"$lookup": {
"from": "users",
"localField": "assignedTo",
"foreignField": "id",
"as": "matchedUsers"
}
},
{
"$addFields": {
"cgData": {
"$first": "$matchedUsers"
}
}
},
{
"$addFields": {
"cgData": {
"$first": {
"$filter": {
"input": {
"$ifNull": [
"$cgData.prices",
[]
]
},
"as": "currentPrice",
"cond": {
"$and": [
{
"$gte": [
"$firstBillable",
"$$currentPrice.beginDate"
]
},
{
$or: [
{
$eq: [
{
$type: "$$currentPrice.endDate"
},
"missing"
]
},
{
"$lt": [
"$firstBillable",
"$$currentPrice.endDate"
]
}
]
}
]
}
}
}
}
}
},
{
"$addFields": {
cgPrice: "$cgData.price"
}
},
{
"$project": {
cgData: 0,
"matchedUsers": 0
}
}
])
In this, a new $addFields stage is added, to get first element of matchedUsers array.
{
"$addFields": {
"cgData": {
"$first": "$matchedUsers"
}
}
}
Then we use $ifNull like this:
{
"$ifNull": [
"$cgData.prices",
[]
]
}
See it working here.

$mergeObjects requires object inputs, but input is of type array

I am trying to add one property if current user has permission or not based on email exists in array of objects.
My input data looks like below.
[
{
nId: 0,
children0: [
{
nId: 3,
access: [
{
permission: "view",
email: "user1#email.com"
}
]
},
{
nId: 4,
access: [
{
permission: "view",
email: "user2#email.com"
}
]
}
]
}
]
https://mongoplayground.net/p/xZmRGFharAb
[
{
"$addFields": {
"children0": {
"$map": {
"input": "$children0.access",
"as": "accessInfo",
"in": {
"$cond": [
{
"$eq": [
"$$accessInfo.email",
"user1#email.com"
]
},
{
"$mergeObjects": [
"$$accessInfo",
{
"hasAccess": true
}
]
},
{
"$mergeObjects": [
"$$accessInfo",
{
"hasAccess": false
}
]
},
]
}
}
}
}
}
]
I also tried this answer as following, but that is also not merging the object.
https://mongoplayground.net/p/VNXcDnXl_sZ
Try this:
db.collection.aggregate([
{
"$addFields": {
"children0": {
"$map": {
"input": "$children0",
"as": "accessInfo",
"in": {
nId: "$$accessInfo.nId",
access: "$$accessInfo.access",
hasAccess: {
"$cond": {
"if": {
"$ne": [
{
"$size": {
"$filter": {
"input": "$$accessInfo.access",
"as": "item",
"cond": {
"$eq": [
"$$item.email",
"user1#email.com"
]
}
}
}
},
0
]
},
"then": true,
"else": false
}
}
}
}
}
}
}
])
Here, we use one $map to loop over children0 and then we filter the access array to contain only elements with matching emails. If the filtered array is non-empty, we set hasAccess to true.
Playground link.

mongodb `$lookup` or `join` with attributes inside array of objects

I have this object that coming from mongodb
[
{
"_id": "5eaf2fc88fcee1a21ea0d94d",
"migration_customer_union_id": 517,
"__v": 0,
"account": 1,
"createdAt": "2020-05-03T20:55:36.335Z",
"customerUnion": "5eaf2fc7698de8321ccd841d",
"shaufel_customers": [
{
"percent": 50,
"_id": "5eaf2fc8698de8321ccd881f",
"customer": "5eaf2fb9698de8321ccd68c0"
},
{
"percent": 50,
"_id": "5eaf2fc9698de8321ccd8a9d",
"customer": "5eaf2fb9698de8321ccd68c0"
}
],
}
]
you can notice inside shaufel_customers array there is an attribute named customer which I want to use it to join with customers document, so that's what I am doing (wrote this code with help of stackoverflow :) )
const aggregate = await CustomerUnionCustomer.aggregate(
[
{
$match: {migration_customer_union_id: 517}
},
{
$lookup: {
from: 'customers',
localField: 'shaufel_customers.customer',
foreignField: '_id',
as: 'customers',
}
},
{
$project: {
shaufel_customer_union_id: 1,
customerUnion: '$customerUnions',
shaufel_customers: {
$map: {
input: "$customers",
as: "c",
in: {
$mergeObjects: [
"$$c",
{
$arrayElemAt: [{
$filter: {
input: "$shaufel_customers",
cond: {$eq: ["$$this.customer", "$$c._id"]}
}
}, 0]
},
]
}
},
}
}
},
{
"$project": { // this project just to get some specific values inside shaufel_customers
'_id': 0,
"shaufel_customers": {
"$map": {
"input": "$shaufel_customers",
"as": "customer",
"in": {
"customer_id": "$$customer.shaufel_customer_id",
"percent": "$$customer.percent"
}
}
}
}
}
]
)
when executing this code I am getting the following response
[
{
"shaufel_customers": [
{
"customer_id": "869",
"percent": 50
}
]
}
]
you can notice I got one object, although there was two objects inside the original array above, and that's because the customer attribute above has the same ObjectId value 5eaf2fb9698de8321ccd68c0 and that's what I want to ask. I want to get the same two objects even if the ids are the same, so the result I am expecting here is
[
{
"shaufel_customers": [
{
"customer_id": "869",
"percent": 50
},
{
"customer_id": "869",
"percent": 50
},
]
}
]
How should I do that :(
You need to revert your $map and iterate shaufel_customers instead of customer - this will return two results:
{
$project: {
shaufel_customer_union_id: 1,
customerUnion: '$customerUnions',
shaufel_customers: {
$map: {
input: "$shaufel_customers",
as: "sc",
in: {
$mergeObjects: [
"$$c",
{
$arrayElemAt: [{
$filter: {
input: "$customers",
cond: {$eq: ["$$this._id", "$$sc.customer"]}
}
}, 0]
},
]
}
},
}
}
},