Lookup in second table but exclude users from first - mongodb

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

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 pull first matching nested array item

I have the following documents...
{ "_id": 2, "name": "Jane Doe", "phones": [ { "type": "Mobile", "digits": [ { "val": 1 }, { "val": 2 } ] }, { "type": "Mobile", "digits": [ { "val": 3 }, { "val": 4 } ] }, { "type": "Land", "digits": [ { "val": 5 }, { "val": 6 } ] } ] }
{ "_id": 1, "name": "John Doe", "phones": [ { "type": "Land", "digits": [ { "val": 1 }, { "val": 2 } ] }, { "type": "Mobile", "digits": [ { "val": 0 }, { "val": 3 }, { "val": 4 } ] }, { "type": "Mobile", "digits": [ { "val": 3 }, { "val": 4 }, { "val": 9 } ] } ] }
...and the following MongoDB query...
db.getCollection("persons").updateOne({"name": "John Doe"},
{
"$pull":
{
"phones.$[condition1].digits":
{
"val: { $in: [ 3, 4 ] }
}
}
},
{
arrayFilters:
[
{ "condition1.type": "Mobile" }
]
})
My problem is that the query removes the last two elements of the array: "phones" of the second document (John Doe) and I want to remove only the first one (and not the last one that have a "9" among the digits). How I can delete only the first matching nested array item?
Query
pipeline update
reduce on phones, starting with {"phones": [], "found": false}
if [3,4] subset of digits.val and not found => ignore it
else keep it (concat arrays to add the member)
$getField to get the phones from the reduced {"phones" : [...]}
*$pull removes all elements that satisfy the condition, maybe there is a way with update operators and not pipeline update, but this works if you dont find more compact way
*alternative to reduce, could be 2 filters, one to keep the values that dont contain the [3,4] and one to keep those that contain, from those that contained, and then concat those arrays removing only one of those that contain the [3,4]
Playmongo
update(
{"name": {"$eq": "John Doe"}},
[{"$set":
{"phones":
{"$getField":
{"field": "phones",
"input":
{"$reduce":
{"input": "$phones",
"initialValue": {"phones": [], "found": false},
"in":
{"$cond":
[{"$and":
[{"$not": ["$$value.found"]},
{"$setIsSubset": [[3, 4], "$$this.digits.val"]}]},
{"phones": "$$value.phones", "found": true},
{"phones": {"$concatArrays": ["$$value.phones", ["$$this"]]},
"found": "$$value.found"}]}}}}}}}])
I have no real sense of motivation for this update, so I am unsure about the details of the logic. I think I have taken the OP's words and partial demonstration literally and I've implemented an update pipeline to fix the stated problem. Given the number of possibilities, this may not be what you are looking for. My pipeline is very similar to the #Takis answer, but the logic is slightly different and therefore the output is different. I look forward to the OP's comments/questions to identify/clarify any discrepancies and/or ambiguities.
db.collection.update({
"name": "John Doe"
},
[
{
"$set": {
"phones": {
"$getField": {
"field": "phones",
"input": {
"$reduce": {
"input": "$phones",
"initialValue": { "phones": [], "pullDone": false },
"in": {
"$cond": [
{
"$and": [
{ "$eq": [ "$$this.type", "Mobile" ] },
{ "$not": "$$value.pullDone" }
]
},
{
"pullDone": true,
"phones": {
"$concatArrays": [
"$$value.phones",
[
{
"$mergeObjects": [
"$$this",
{
"digits": {
"$filter": {
"input": "$$this.digits",
"as": "digit",
"cond": {
"$not": [ { "$in": [ "$$digit.val", [ 3, 4 ] ] } ]
}
}
}
}
]
}
]
]
}
},
{
"pullDone": "$$value.pullDone",
"phones": {
"$concatArrays": [ "$$value.phones", [ "$$this" ] ]
}
}
]
}
}
}
}
}
}
}
])
Try it on mongoplayground.net.

MongoDB insert object into array if not exists

I am working on an app that manages the voting on polls by users. I use MongoDB as DB. (I am a newbie)
My DB is structured as follows:
[
{
"question_id": "0001",
"text": "What's your favourite color ? ",
"answers": [
{
"_id": "872ffaskdfba23jdafs",
"text": "Blue",
"user": {
"user_id": "u0001",
"nickname": "Paul"
}
},
{
"_id": "7832ab28b879293fabb2",
"text": "Red",
"user": {
"user_id": "u0002",
"nickname": "Eric"
}
},
{
"_id": "664oahh229s0f829323av",
"text": "Red",
"user": {
"user_id": "u0003",
"nickname": "Johhny"
}
}
]
}
]
Users can leave a maximum of answer for each question; so, for example, users Paul,Eric and Johhny can't vote anymore since their Id's are already present inside 'answers' array.
How could achieve this kind of control with MongoDB ?
You can use $reduce to achieve the behaviour.
create a union of the original array and the element you want to add
iterate the created union through $reduce, append the current element if the user_id is not already exists in our accumulator result; otherwise keep the accumulator result
db.collection.update({}, [
{
"$addFields": {
"answers": {
"$reduce": {
"input": {
"$concatArrays": [
"$answers",
[
<answer object that you want to insert>
]
]
},
"initialValue": [],
"in": {
"$cond": {
"if": {
"$not": {
"$in": [
"$$this.user.user_id",
"$$value.user.user_id"
]
}
},
"then": {
"$concatArrays": [
"$$value",
[
"$$this"
]
]
},
"else": "$$value"
}
}
}
}
}
}
])
Here is the Mongo playground for your reference

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()