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

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

Related

Lookup in second table but exclude users from first

The bounty expires in 7 days. Answers to this question are eligible for a +150 reputation bounty.
bflemi3 wants to draw more attention to this question.
I have a collection called permissions that I'm joining with another collection called groups.
permissions
[
{
"_id": 1,
"resource": "resource:docs/61",
"permissions": [
{
"permission": "role:documentOwner",
"users": [
"user:def",
"group:abc",
]
},
{
"permission": "document.read",
"users": [
"user:abc",
"user:xxx",
"group:abc"
]
},
{
"permission": "document.update",
"users": [
"user:xxx"
]
}
]
},
{
"_id": 2,
"resource": "resource:docs/38",
"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"
]
}
]
}
]
groups
[
{
"_id": 1,
"id": "abc",
"name": "Test Group",
"users": [
"abc",
"cpo",
"yyy",
"xxx"
]
},
{
"_id": 2,
"id": "bff",
"name": "Something",
"users": [
"xxx"
]
}
]
I'm trying to do two things:
Get all permissions and have any entries in the users array that are prefixed with group: resolved so that the respective group's users are included in the users array.
If a permission document has a user that is listed specifically and also contained in a listed group, then that user is not included from the group. In other words, permissions that are granted to a group's users, can be overridden for a member of the group if they are specifically granted permissions. For instance, I grant group:abc with document.read permission on a resource, but I want user:abc (which is a part of the group) to have document.read and document.update for that resource, so I specifically grant user:abc with those permissions.
For example, here's what permissions._id = 1 would look like...
[
{
"_id": 1,
"resource": "resource:docs/61",
"permissions": [
{
"permission": "role:documentOwner",
"users": [
"user:def",
"user:cpo", // inherited from group:abc
"user:yyy", // inherited from group:abc
]
},
{
"permission": "document.read",
"users": [
"user:abc", // not inherited even though part of group:abc because they're specifically listed in the original document
"user:xxx", // not inherited even though part of group:abc because they're specifically listed in the original document
"user:cpo", // inherited from group:abc
"user:yyy", // inherited from group:abc
]
},
{
"permission": "document.update",
"users": [
"user:xxx" // not inherited even though part of group:abc because they're specifically listed in the original document
]
}
]
},
...
]
I created a Mongo Playground to use for testing. I'm failing miserably though 😞
I appreciate the help!
You can do the followings in an aggregation pipeline:
$unwind to process at permission-resource level
"split" the users array into "user" and "groups" respectively. Locate the array entries using $indexOfCP equal to 0 to check for the prefix.
$lookup to the groups collection
perform $setUnion to union the permissions for individual users and permissions granted from groups
$group again to get back original form
db.permissions.aggregate([
{
$match: {
"_id": 1
}
},
{
"$unwind": "$permissions"
},
{
$set: {
// process users entries
u: {
"$reduce": {
"input": "$permissions.users",
"initialValue": [],
"in": {
"$cond": {
"if": {
$eq: [
0,
{
"$indexOfCP": [
"$$this",
"user:"
]
}
]
},
"then": {
"$concatArrays": [
"$$value",
[
{
"$replaceAll": {
"input": "$$this",
"find": "user:",
"replacement": ""
}
}
]
]
},
"else": "$$value"
}
}
}
},
// process groups entries
g: {
"$reduce": {
"input": "$permissions.users",
"initialValue": [],
"in": {
"$cond": {
"if": {
$eq: [
0,
{
"$indexOfCP": [
"$$this",
"group:"
]
}
]
},
"then": {
"$concatArrays": [
"$$value",
[
{
"$replaceAll": {
"input": "$$this",
"find": "group:",
"replacement": ""
}
}
]
]
},
"else": "$$value"
}
}
}
}
}
},
{
"$lookup": {
"from": "groups",
"localField": "g",
"foreignField": "id",
"as": "g"
}
},
{
$project: {
resource: 1,
permissions: {
permission: 1,
// flatten and union users and looked up groups
users: {
"$setUnion": [
"$u",
{
"$reduce": {
"input": "$g",
"initialValue": [],
"in": {
"$setUnion": [
"$$value",
"$$this.users"
]
}
}
}
]
}
}
}
},
{
// cosmetics
$set: {
"permissions.users": {
"$map": {
"input": "$permissions.users",
"as": "u",
"in": {
"$concat": [
"user:",
"$$u"
]
}
}
}
}
},
{
$group: {
_id: "$_id",
resource: {
$first: "$resource"
},
permissions: {
$push: "$permissions"
}
}
}
])
Mongo Playground

Combine array values from collection and $lookup result

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

mongo query for a one-to-many collection where all records has to match the condition and get an unique record

Employee has multiple employeeActions, the employeeActions data looks like this:
[
{
"email": "one#gmail.com",
"companyRegNo": 105,
"event": {
"created": ISODate("2022-09-16T06:42:04.387Z"),
"desc": "COMPLETED_APPLICATIONS",
"note": "Direct apply"
}
},
{
"email": "one#gmail.com",
"companyRegNo": 105,
"event": {
"created": ISODate("2022-09-20T06:42:42.761Z"),
"desc": "ASKED_TO_REVIEW",
}
},
{
"email": "two#gmail.com",
"companyRegNo": 227,
"event": {
"created": ISODate("2022-09-16T06:42:04.387Z"),
"desc": "COMPLETED_APPLICATIONS",
"note": "Direct apply",
}
},
{
"email": "two#gmail.com",
"companyRegNo": 227,
"event": {
"created": ISODate("2022-09-28T06:42:42.761Z"),
"desc": "ASKED_TO_REVIEW",
}
},
{
"email": "three#gmail.com",
"companyRegNo": 157,
"event": {
"created": ISODate("2022-09-16T06:42:04.387Z"),
"desc": "COMPLETED_APPLICATIONS",
"note": "Direct apply",
}
},
{
"email": "four#gmail.com",
"companyRegNo": 201,
"deleted": true,
"event": {
"created": ISODate("2022-09-15T06:42:42.761Z"),
"desc": "COMPLETED_APPLICATIONS",
}
},
]
I need to write an aggregation query to get all email ids where the employee action of the user
- Does not have an ASKED_TO_REVIEW event created before '2022-09-25'
- deleted is either false or does not exist
The out put should have only
{"email": "one#gmail.com"}
{"email": "three#gmail.com"}
The below match and project query did not work
db.collection.aggregate([
{
"$match": {
"$and": [
{
"deleted": {
"$ne": true
}
},
{
"$or": [
{
"$and": [
{
"event.name": {
"$eq": "ASKED_TO_REVIEW"
}
},
{
"event.created": {
"$lt": ISODate("2022-09-25")
}
}
]
},
{
"event.name": {
"$ne": "ASKED_TO_REVIEW"
}
}
]
}
]
}
},
{
"$project": {
"email": 1,
"_id": 0
}
}
])
How do i go about this?
You need to group the events by email and then apply your filtering logic to those groups, something like this:
db.collection.aggregate([
{
"$group": {
"_id": "$email",
"field": {
"$push": "$$ROOT"
}
}
},
{
"$match": {
$expr: {
"$eq": [
0,
{
"$size": {
"$filter": {
"input": "$field",
"as": "item",
"cond": {
"$or": [
{
"$and": [
{
"$eq": [
{
"$getField": {
"field": "desc",
"input": "$$item.event"
}
},
"ASKED_TO_REVIEW"
]
},
{
"$lt": [
{
"$getField": {
"field": "created",
"input": "$$item.event"
}
},
ISODate("2022-09-25")
]
}
]
},
{
"$eq": [
{
"$getField": {
"field": "deleted",
"input": "$$item"
}
},
true
]
}
]
}
}
}
}
]
}
}
},
{
"$project": {
email: "$_id",
"_id": 0
}
}
])
Playground link.
Figured out the working query. After grouping by email, $elemMatch needs to be used for the and condition between "event.desc" and "event.created"
db.collection.aggregate([
{
"$group": {
"_id": "$email",
"field": {
"$push": "$$ROOT"
}
}
},
{
"$match": {
"$and": [
{
"field.deleted": {
"$ne": true
}
},
{
"$or": [
{
"field": {
"$elemMatch": {
"event.desc": "ASKED_TO_REVIEW",
"event.created": {
"$lt": ISODate("2022-09-25")
}
}
}
},
{
"field.event.desc": {
"$ne": "ASKED_TO_REVIEW"
}
}
]
}
]
}
},
{
"$project": {
email: "$_id",
"_id": 0
}
}
])
Playground Link

MongoDB how to filter in nested array

I have below data. I want to find value=v2 (remove others value which not equals to v2) in the inner array which belongs to name=name2. How to write aggregation for this? The hard part for me is filtering the nestedArray which only belongs to name=name2.
{
"_id": 1,
"array": [
{
"name": "name1",
"nestedArray": [
{
"value": "v1"
},
{
"value": "v2"
}
]
},
{
"name": "name2",
"nestedArray": [
{
"value": "v1"
},
{
"value": "v2"
}
]
}
]
}
And the desired output is below. Please note the value=v1 remains under name=name1 while value=v1 under name=name2 is removed.
{
"_id": 1,
"array": [
{
"name": "name1",
"nestedArray": [
{
"value": "v1"
},
{
"value": "v2"
}
]
},
{
"name": "name2",
"nestedArray": [
{
"value": "v2"
}
]
}
]
}
You can try,
$set to update array field, $map to iterate loop of array field, check condition if name is name2 then $filter to get matching value v2 documents from nestedArray field and $mergeObject merge objects with available objects
let name = "name2", value = "v2";
db.collection.aggregate([
{
$set: {
array: {
$map: {
input: "$array",
in: {
$mergeObjects: [
"$$this",
{
$cond: [
{ $eq: ["$$this.name", name] }, //name add here
{
nestedArray: {
$filter: {
input: "$$this.nestedArray",
cond: { $eq: ["$$this.value", value] } //value add here
}
}
},
{}
]
}
]
}
}
}
}
}
])
Playground
You can use the following aggregation query:
db.collection.aggregate([
{
$project: {
"array": {
"$concatArrays": [
{
"$filter": {
"input": "$array",
"as": "array",
"cond": {
"$ne": [
"$$array.name",
"name2"
]
}
}
},
{
"$filter": {
"input": {
"$map": {
"input": "$array",
"as": "array",
"in": {
"name": "$$array.name",
"nestedArray": {
"$filter": {
"input": "$$array.nestedArray",
"as": "nestedArray",
"cond": {
"$eq": [
"$$nestedArray.value",
"v2"
]
}
}
}
}
}
},
"as": "array",
"cond": {
"$eq": [
"$$array.name",
"name2"
]
}
}
}
]
}
}
}
])
MongoDB Playground

Is there a way in mongodb to group at multiple levels

I have a document which contains an array of array as given below.
This is the first document.
{
"_id": "5d932a2178fdfc4dc41d75da",
"data": [
{
"nestedData": [
{
"_id": "5d932a2178fdfc4dc41d75e1",
"name": "Special 1"
},
{
"_id": "5d932a2178fdfc4dc41d75e0",
"name": "Special 2"
}
]
}
]
}
I need to lookup(join) to another collection with the _id in the nestedData array in the aggregation framework.
The 2nd document from which I need to lookup is
{
"_id": "5d8b1ac3b15bc72d154408e1",
"status": "COMPLETED",
"rating": 4
}
I know I need to $unwind it twice to convert nestedData array into object.
But how do I group back again to form the same object like given below
{
"_id": "5d932a2178fdfc4dc41d75da",
"data": [
{
"array": [
{
"_id": "5d932a2178fdfc4dc41d75e1",
"name": "Special 1",
"data": {
"_id": "5d8b1ac3b15bc72d154408e1",
"status": "COMPLETED",
"rating": 4
},
{
"_id": "5d932a2178fdfc4dc41d75e0",
"name": "Special 2",
"data": {
"_id": "5d8b1ac3b15bc72d154408e0",
"status": "COMPLETED",
"rating": 4
},
}
]
}
]
}
Try this query
db.testers.aggregate([
{$lookup: {
from: 'demo2',
pipeline: [
{ $sort: {'_id': 1}},
],
as: 'pointValue',
}},
{
$addFields:{
"data":{
$map:{
"input":"$data",
"as":"doc",
"in":{
$mergeObjects:[
"$$doc",
{
"nestedData":{
$map:{
"input":"$$doc.nestedData",
"as":"nestedData",
"in":{
$mergeObjects:[
{ $arrayElemAt: [ {
"$map": {
"input": {
"$filter": {
"input": "$pointValue",
"as": "sn",
"cond": {
"$and": [
{ "$eq": [ "$$sn._id", "$$nestedData._id" ] },
]
}
}
},"as": "data",
"in": {
"name": "$$nestedData.name",
"data":"$$data",
}}
}, 0 ] },'$$nestedData'
],
}
}
}
}
]
}
}
}
}
},
{$project: { pointValue: 0 } }
]).pretty()