Database

[{"id":"1","name":"First"},{"id":"2","name":"Second","ref1":{"referenceId":"3"},"ref2":[{"referenceId":"1",},{"referenceId":"3"}],"ref3":{"referenceId":"4"}},{"name":"Third","id":"3"},{"name":"Fourth","id":"4","ref3":[{"referenceId":"3"}]}]

Query

db.collection.aggregate([/** filter out objects with referenceId inside*/{$addFields:{"references":{$map:{input:{"$objectToArray":"$$ROOT"},as:"item",in:{$cond:{if:{$ne:["$$item.v.referenceId",undefined]},then:["$$item.k",["$$item.v.referenceId",{"$cond":[{"$ne":["$$item.v.referenceId.0",undefined]},{$size:"$$item.v.referenceId"},0]}]],else:null}}}}}},/** filter out potential null values from above*/{"$addFields":{"references":{$filter:{input:"$references",cond:{$ne:["$$this",null]}}}}},/** remove the referenced objects from root. this needs to be* done because otherwise there will be a clash. when reducing* the data at the end*/{"$replaceRoot":{"newRoot":{"$mergeObjects":[{"references":"$references"},{"$arrayToObject":{$map:{input:{$objectToArray:"$$ROOT"},as:"item",in:{$cond:{if:{$eq:["$$item.v.referenceId",undefined]},then:["$$item.k","$$item.v"],else:["_","_"]}}}}}]}}},/** prepare lookup via references array with k, v for static* key lookup via $references.v*/{"$addFields":{"references":{$map:{input:"$references",as:"item",in:{k:{"$arrayElemAt":["$$item",0]},v:{"$arrayElemAt":["$$item",1]}}}}}},{$unwind:{path:"$references",preserveNullAndEmptyArrays:true}},/** perform lookup into field resolved*/{$lookup:{"from":"collection","localField":"references.v.0","foreignField":"id","as":"resolved"}},{$unwind:{path:"$resolved",preserveNullAndEmptyArrays:true}},/** prepare grouping via key and value*/{"$addFields":{"k":"$references.k","v":"$resolved",type:{"$arrayElemAt":["$references.v",1]}}},/** remove temp variables references and resolved*/{$project:{resolved:0,references:0,}},/** group by key as id*/{$group:{_id:["$k","$_id"],resolved:{$push:"$v"},root:{$first:"$$ROOT"}}},/** create a ref field for grouping later* make 1-sized arrays into fields - this is not optimal,* since we don't know what it was before. optimal would be* storing the size from the input document*/{$addFields:{ref:{$cond:[{$eq:["$root.k",undefined]},{},{$arrayToObject:[[{k:"$root.k",v:{$cond:[{$and:[{$eq:[{$size:"$resolved"},1]},{$lt:["$root.type",1]}]},{$arrayElemAt:["$resolved",0]},"$resolved"]}}]]}]}}},/** remove temp fields which were nested before* group by id, merge by ref, merge with root for reduce*/{$project:{"root.k":0,"root.v":0,"root._":0,"root.type":0}},/** group by id, merge by ref, merge with root for reduce*/{$group:{_id:"$root.id",ref:{$push:{$mergeObjects:["$root","$ref"]}},root:{$first:"$$ROOT"}}},/** merge by ref, merge with root for reduce* reduce the ref array into final result*/{$project:{result:{$reduce:{input:"$ref",initialValue:{},in:{$mergeObjects:["$$this","$$value"]}}}}},/** replace root with result*/{$replaceRoot:{newRoot:"$result"}}])

Result