Reputation: 1003
I have external object like this:
var obj = {
a: 2,
b: 0,
c: 1
}
Also I have collection with field "ref" that is referenced to obj. Values in obj object are treated as indexes for array field in collection.
{
ref: "a",
array: ["xyz", "abc", "def"]
}
{
ref: "b",
array: ["sde", "xda", "era"]
}
{
ref: "c",
array: ["wwe", "aea", "fre"]
}
I want to aggregate collection to get only one value from array depending on index that is specified in obj.
.aggregate([
{
$project: {
code: $arrayElemAt: ["$array", { $let: {
vars: { obj: obj },
in: "$$obj.$ref" //Field path used as variable field name of object
}}]
}
}])
But I get following error:
MongoError: FieldPath field names may not start with '$'.
It is related I think for following stuff: in: "$$obj.$ref"
Desired result for aggregation is:
{
ref: "a",
code: "def"
}
{
ref: "b",
code: "sde"
}
{
ref: "c",
code: "aea"
}
Upvotes: 1
Views: 602
Reputation: 151112
You were sort of going down the right track but you are using $let
in the wrong place. So the general idea is to apply the $let
"around" the expressions to evaluate rather than "inside" them.
Also, you really should make the object to match an "array" before feeding to the pipeline as an input value. The operators work with "arrays" and don't handle looking up "objects" by variable names very well.
So this is is probably shortest by using $indexOfArray
along with $arrayElemAt
where both are available in order to match and extract the values:
var obj = {
a: 2,
b: 0,
c: 1
};
var input = Object.keys(obj).map(k => ({ k: k, v: obj[k] }));
// Outputs as
// [{ "k" : "a", "v" : 2 }, { "k" : "b", "v" : 0 }, { "k" : "c", "v" : 1 }]
db.collection.aggregate([
{ "$addFields": {
"array": {
"$let": {
"vars": { "obj": input },
"in": {
"$arrayElemAt": [
"$array",
{ "$arrayElemAt": [
"$$obj.v",
{ "$indexOfArray": [ "$$obj.k", "$ref" ] }
]}
]
}
}
}
}}
])
If you actually have MongoDB 3.4.4 or greater, then you "could" apply $objectToArray
internally instead, but it does not really add any value ( it's just showing off really ):
var obj = {
a: 2,
b: 0,
c: 1
};
//var input = Object.keys(obj).map(k => ({ k: k, v: obj[k] }));
db.collection.aggregate([
{ "$addFields": {
"array": {
"$let": {
"vars": { "obj": { "$objectToArray": obj } },
"in": {
"$arrayElemAt": [
"$array",
{ "$arrayElemAt": [
"$$obj.v",
{ "$indexOfArray": [ "$$obj.k", "$ref" ] }
]}
]
}
}
}
}}
])
If your MongoDB does not support $indexOfArray
, then you can always apply $map
and $filter
with $arrayElemAt
extracting the "first match" from those combined statements instead:
var obj = {
a: 2,
b: 0,
c: 1
};
var input = Object.keys(obj).map(k => ({ k: k, v: obj[k] }));
db.collection.aggregate([
{ "$addFields": {
"array": {
"$arrayElemAt": [
"$array",
{ "$arrayElemAt": [
{ "$map": {
"input": {
"$filter": {
"input": input,
"cond": { "$eq": [ "$$this.k", "$ref" ] }
}
},
"in": "$$this.v"
}},
0
]}
]
}
}}
])
Which also removes any usage of $let
since we aren't notating against the supplied variable for anything other than the "input"
to $filter
directly.
No matter which is applied, all come to the same output which is using the supplied index positions from the input obj
( coerced to input
as an array ) and returning that array entry:
/* 1 */
{
"_id" : ObjectId("59a76d6fad465e105d9136dc"),
"ref" : "a",
"array" : "def"
}
/* 2 */
{
"_id" : ObjectId("59a76d6fad465e105d9136dd"),
"ref" : "b",
"array" : "sde"
}
/* 3 */
{
"_id" : ObjectId("59a76d6fad465e105d9136de"),
"ref" : "c",
"array" : "aea"
}
Upvotes: 1