MongoDB how to filter in nested array - mongodb

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

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

Remove multiple inconsistent objects from double nested array 5

here is another challenge:
I need to clean my data from incorrect objects , objects under the array "t" that contain did , dst and den fields are considered correct , #nimrok serok / #rickhg12hs helped with a working solution , but still there is some edge cases where none of objects are valid and stay empty array after the update , so I am wondering if those can be cleared in same update query?
example document:
{
"_id": ObjectId("5c05984246a0201286d4b57a"),
f: "x",
"_a": [
{
"_onlineStore": {}
},
{
"_p": {
"pid": 1,
"s": {
"a": {
"t": [
{
id: 1,
"dateP": "20200-09-20",
did: "x",
dst: "y",
den: "z"
},
{
id: 2,
"dateP": "20200-09-20"
}
]
},
"c": {
"t": [
{
id: 3,
"dateP": "20300-09-22"
},
{
id: 4,
"dateP": "20300-09-23",
}
]
}
},
h: "This must stay"
}
},
{
"_p": {
"pid": 2,
"s": {
"a": {
"t": [
{
id: 1,
"dateP": "20200-09-20",
}
]
},
"c": {
"t": [
{
id: 3,
"dateP": "20300-09-22"
},
{
id: 4,
"dateP": "20300-09-23",
}
]
}
},
h: "This must stay"
}
},
{
x: "This must stay"
}
]
}
Expected output:
{
"_a": [
{
"_onlineStore": {}
},
{
"_p": {
"h": "This must stay",
"pid": 1,
"s": {
"a": {
"t": [
{
"dateP": "20200-09-20",
"den": "z",
"did": "x",
"dst": "y",
"id": 1
}
]
}
}
}
},
{
"_p": {
"h": "This must stay",
"pid": 2,
}
},
{
"x": "This must stay"
}
],
"_id": ObjectId("5c05984246a0201286d4b57a"),
"f": "x"
}
Playground
(As you can see in the playground example , job is almost done , just for cases where all array elements are wrong the array stay empty , so it need to be removed as well ...)
mongodb version 4.4
It touk me some time , but here is the solution for those who face similar problem:
db.collection.update({},
[
{
"$set": {
_a2: {
$filter: {
input: "$_a",
as: "elem",
cond: {
"$eq": [
{
"$type": "$$elem._p.s"
},
"missing"
]
}
}
},
_a: {
$filter: {
input: "$_a",
as: "elem",
cond: {
"$ne": [
{
"$type": "$$elem._p.s"
},
"missing"
]
}
}
}
}
},
{
"$set": {
"_a": {
"$map": {
"input": "$_a",
"as": "elem",
"in": {
"$mergeObjects": [
"$$elem",
{
"_p": {
"$mergeObjects": [
"$$elem._p",
{
s: {
"$arrayToObject": {
"$map": {
"input": {
"$objectToArray": "$$elem._p.s"
},
"as": "anyKey",
"in": {
"k": "$$anyKey.k",
"v": {
"t": {
"$filter": {
"input": "$$anyKey.v.t",
"as": "t",
"cond": {
"$setIsSubset": [
[
"did",
"dst",
"den"
],
{
"$map": {
"input": {
"$objectToArray": "$$t"
},
"in": "$$this.k"
}
}
]
}
}
}
}
}
}
}
}
}
]
}
}
]
}
}
}
}
},
{
"$set": {
"_a": {
"$map": {
"input": "$_a",
"as": "elem",
"in": {
"$mergeObjects": [
"$$elem",
{
"_p": {
"$mergeObjects": [
"$$elem._p",
{
s: {
"$arrayToObject": {
"$filter": {
"input": {
"$objectToArray": "$$elem._p.s"
},
"as": "anyKey",
"cond": {
$ne: [
"$$anyKey.v.t",
[]
]
}
}
}
}
}
]
}
}
]
}
}
}
}
},
{
"$set": {
"_a": {
"$map": {
"input": "$_a",
"as": "elem",
"in": {
"$mergeObjects": [
"$$elem",
{
"_p": {
"$arrayToObject": {
"$filter": {
"input": {
"$objectToArray": "$$elem._p"
},
"as": "anyKey",
cond: {
$not: {
$in: [
"$$anyKey.v",
[
{}
]
]
}
}
}
}
}
}
]
}
}
}
}
},
{
$set: {
_a: {
"$concatArrays": [
"$_a2",
"$_a"
]
}
}
},
{
$unset: "_a2"
}
])
Explained:
Split the array in two arays via $set/$filter , _a2 (contain elements that will not be changed ) and _a ( contain the affected inconsistent )
$map/$mergeObjects/$mergeObjects/$map/$arrayToObject to remove the inconsistent objects inside _a[]._p.s.k.t[]
$map/$mergeObjects/$mergeObjects/$map/$arrayToObject/$filter to remove the empty _a[]._p.s.k.t[] arrays t with theyr keys k.
$map/$mergeObjects/$mergeObjects/$map/$arrayToObject/$filter to remove the empty _a[]._p.s:{} elements.
$concat on _a and _a2 to concatenete the fixed _a[] array elements with the ones that are correct and preserved in _a2[].
$unset the temporary array _a2[] since it has been already concatenated with _a[] in previous stage.
Special thanks to #nimrod serok & #rickhg12hs for the initial ideas!
Playground

Find subdocument nested inside a document by id in mondogb

I have a mongodb document like this
{
"_id": {
"$oid": "6241dd90891458501c17d627"
},
"A": [
{
"_id": {
"$oid": "6241ddb1891458501c17d63e"
},
"B": [
{
"_id": {
"$oid": "6241ddc4891458501c17d674"
}
},
{
"_id": {
"$oid": "6241ddda891458501c17d675"
}
}
]
},
{
"_id": {
"$oid": "6241ddbe891458501c17d63f"
},
"B": [
{
"_id": {
"$oid": "6241ddda891458501c17d678"
}
},
{
"_id": {
"$oid": "6241ddda891458501c17d679"
}
}
]
}
]
}
This document has 2 nested arrays: an array of "A" elements, inside each element of "A" there's an array of "B" elements. I need to search by an _id of a "B" element, let's say 6241ddda891458501c17d679. I need a way to obtain this structure in mongodb
{
"_id": {
"$oid": "6241dd90891458501c17d627"
},
"A": [
{
"_id": {
"$oid": "6241ddbe891458501c17d63f"
},
"B": [
{
"_id": {
"$oid": "6241ddda891458501c17d679"
}
}
]
}
]
}
How can I achieve this? Thanks very much
Maybe something like this:
Option 1, Find:
db.collection.find({
"A.B._id": {
"$oid": "6241ddda891458501c17d679"
}
},
{
"A": {
"$filter": {
"input": {
"$map": {
"input": "$A",
"as": "a",
"in": {
"_id": "$$a._id",
"B": {
"$filter": {
"input": "$$a.B",
"as": "b",
"cond": {
"$eq": [
{
"$oid": "6241ddda891458501c17d679"
},
"$$b._id"
]
}
}
}
}
}
},
"as": "an",
"cond": {
"$ne": [
"$$an.B",
[]
]
}
}
}
})
Explained:
Use find() with match query on "A.B._id" ( good to have index on this filed for best performance)
In the filter part add $filter/map/filter combination to filter only the matching _id for array B elements and preserve the array A _id , also in the initial filter condition use only non-empty arrays [] to avoid having elements from empty arrays in the final result.
playground1
Option 2 , aggregation:
db.collection.aggregate([
{
$match: {
"A.B._id": {
"$oid": "6241ddda891458501c17d679"
}
}
},
{
"$addFields": {
"A": {
"$filter": {
"input": {
"$map": {
"input": "$A",
"as": "a",
"in": {
"_id": "$$a._id",
"B": {
"$filter": {
"input": "$$a.B",
"as": "b",
"cond": {
"$eq": [
{
"$oid": "6241ddda891458501c17d679"
},
"$$b._id"
]
}
}
}
}
}
},
"as": "an",
"cond": {
"$ne": [
"$$an.B",
[]
]
}
}
}
}
}
])
playground2

MongoDB Aggregation - Assign a value of a field in an object to a custom field

Given the following data structure:
[
{
"body": {
"Fields": [
{
"Name": "description",
"Value": "Some text"
},
{
"Name": "size",
"Value": "40"
}
]
}
}
]
I need to get the following output containing keys extracted from 'Name' fields and values extracted by "Value" fields:
[
{
"description": "Some text",
"size": "40"
}
]
Could you please provide me with the ideas?
I've ended up filtering required element, but have no idea how to extract values and assign them to the keys. What I have so far:
db.collection.aggregate([
{
"$project": {
"description": {
"$filter": {
"input": "$body.Fields",
"as": "bfields",
"cond": {
"$eq": [
"$$bfields.Name",
"description"
]
}
}
},
"size": {
"$filter": {
"input": "$body.Fields",
"as": "bfields",
"cond": {
"$eq": [
"$$bfields.Name",
"size"
]
}
}
}
}
}
])
It produces:
[
{
"_id": ObjectId("5a934e000102030405000000"),
"description": [
{
"Name": "description",
"Value": "Some text"
}
],
"size": [
{
"Name": "size",
"Value": "40"
}
]
}
]
db.collection.aggregate([
{
$addFields: {
body: {
$map: {
input: "$body.Fields",
as: "fi",
in: {
k: "$$fi.Name",
v: "$$fi.Value"
}
}
}
}
},
{
"$addFields": {
"body": {
"$arrayToObject": "$body"
}
}
},
{
$project: {
description: "$body.description",
size: "$body.size",
_id: 0
}
}
])
explained:
Use $map to rename the keys to k,v suitable for $arrayToObject
Convert the array to object with $arrayToObject ( the magic trick)
$project to the exact desired output
playground

Join fields when not all have values

I want to modify a field through a projection stage in the aggregation pipeline, this field is combination of other fields values separated by (-)
if the field is null or empty of missing it will not be added to the cocatenated string
{$project:{
//trial-1:
finalField:{
$concat["$field1",'-','$field2','-','$field3',...]
//problem1: $concat will return null if any of it's arguments is null or missing
//problem2: if all the fields are exist with non-null values, the delimiter will exists even if the field dosen't
}
//trial-2:
finalField:{
$concat:[
{$cond:[{field1:null},'',{$concat:['$field1','-']},..]
//the problem: {field1:null} fails if the field dosen't exixt (i.e the expression gives true)
//trial-3
finalField:{
$concat:[
{$cond:[{$or:[{field1:null},{field:{$exists:true}},'',
{$concat:['$field1','-']}
]}]}
]
}
]
}
//trial-4 -> using $reduce instead of $concate (same issues)
}
You basically want $ifNull. It's "sort of" like $exists but for aggregation statements, where it returns a default value when the field expression returns null, meaning "not there":
{ "$project": {
"finalField": {
"$concat": [
{ "$ifNull": [ "$field1", "" ] },
"-",
{ "$ifNull": [ "$field2", "" ] },
"-",
{ "$ifNull": [ "$field3", "" ] }
]
}
}}
For example with data like:
{ "field1": "a", "field2": "b", "field3": "c" },
{ "field1": "a", "field2": "b" },
{ "field1": "a", "field3": "c" }
You get, without any error producing of course:
{ "finalField" : "a-b-c" }
{ "finalField" : "a-b-" }
{ "finalField" : "a--c" }
If you want something fancier, then you would instead dynamically work with the names, as in:
{ "$project": {
"finalField": {
"$reduce": {
"input": {
"$filter": {
"input": { "$objectToArray": "$$ROOT" },
"cond": { "$ne": [ "$$this.k", "_id" ] }
}
},
"initialValue": "",
"in": {
"$cond": {
"if": { "$eq": [ "$$value", "" ] },
"then": { "$concat": [ "$$value", "$$this.v" ] },
"else": { "$concat": [ "$$value", "-", "$$this.v" ] }
}
}
}
}
}}
Which can be aware of what fields were actually present and only attempt to join those:
{ "finalField" : "a-b-c" }
{ "finalField" : "a-b" }
{ "finalField" : "a-c" }
You can even manually specify the list of fields if you don't want the $objectToArray over the document or sub-document:
{ "$project": {
"finalField": {
"$reduce": {
"input": {
"$filter": {
"input": ["$field1", "$field2", "$field3"],
"cond": { "$ne": [ "$$this", null ] }
}
},
"initialValue": "",
"in": {
"$cond": {
"if": { "$eq": [ "$$value", "" ] },
"then": { "$concat": [ "$$value", "$$this" ] },
"else": { "$concat": [ "$$value", "-", "$$this" ] }
}
}
}
}
}}