1

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"
}

1 Answer 1

1

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"
}
Sign up to request clarification or add additional context in comments.

2 Comments

Edit: It doesnt work on 3.2. I can't bind array of objects to $filter, $map or $let
@JakubKutrzeba Of course it works with MongoDB 3.2, which is exactly what the third example demonstrates, and really is simply taking the "first match" from a filtered list of possible matches by using the indexed position of 0 with $arrayElemAt. It seems rather that you are trying something different from what the question asked, which is best addressed by Asking a New Question instead, so you can then clearly present what exactly you are attempting that is different.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.