Return empty object when $arrayToObject missing keys upon Mongo aggregation

26 views Asked by At

The results Mongo collection contains the following documents:

[
  {
    "id": 1,
    "failures": [
      {
        "level": "BASIC",
        "message": "failure",
      },
      {
        "level": "BASIC",
        "message": "failure",
      },
      {
        "level": "WARNING",
        "message": "failure",
      },
    ],
    "rules": ["X", "Y"]
  },
  {
    "id": 2,
    "failures": [
      {
        "level": "BASIC",
        "message": "failure",
      },
      {
        "level": "WARNING",
        "message": "failure",
      }
    ],
    "rules": ["X"]
  },
  {
    "id": 3,
    "failures": [],
    "rules": ["X", "Y"]
  },
]

I would like to create a Mongo query that selects the documents matching the provided IDs, counts the level of each elements of the failures array into a object, and project the rules property. Given the collection above, and when providing as input the IDs [1, 2, 3], this should be the expected output:

[
  {
    "id": 1,
    "counts": {
      "BASIC": 2,
      "WARNING": 1
    }
    "rules": ["X", "Y"]
  },
  {
    "id": 2,
    "counts": {
      "BASIC": 1,
      "WARNING": 1
    },
    "rules": ["X"]
  },
  {
    "id": 3,
    "counts": {},
    "rules": ["X", "Y"]
  },
]

This is the Mongo query I am building:

db.collection.aggregate([
  {
    $match: {
      "id": {
        $in: [
          1,
          2,
          3
        ]
      }
    }
  },
  {
    $unwind: {
      path: "$failures",
      preserveNullAndEmptyArrays: true
    }
  },
  {
    $group: {
      _id: {
        id: "$id",
        level: "$failures.level"
      },
      count: {
        $sum: 1
      },
      rules: {
        $first: "$rules"
      }
    }
  },
  {
    $group: {
      _id: "$_id.id",
      count: {
        $push: {
          k: "$_id.level",
          v: "$count"
        }
      },
      rules: {
        $first: "$rules"
      }
    }
  },
  {
    $project: {
      "_id": 0,
      "id": "$_id.id",
      "count": {
        $arrayToObject: "$count"
      },
      "rules": 1
    }
  }
])

However, this query fails when applying the $arrayToObject operator with the following error:

$arrayToObject requires an object keys of 'k' and 'v'

This is because documents with no elements in the failures array have no key "k" when pushing the "_id.level" property.

How can I fall back on an empty object if any of these keys are not present?

Thanks in advance.

1

There are 1 answers

0
Yong Shun On

Your document with id: 3 contains an empty failures array, if you debug until stage 4th, you will see the count array with the document missing k field.

Approach 1

Use the $cond operator to handle when missing value for failures.level in both $group stages.

db.collection.aggregate([
  {
    $match: {
      "id": {
        $in: [
          1,
          2,
          3
        ]
      }
    }
  },
  {
    $unwind: {
      path: "$failures",
      preserveNullAndEmptyArrays: true
    }
  },
  {
    $group: {
      _id: {
        id: "$id",
        level: "$failures.level"
      },
      count: {
        $sum: {
          $cond: [
            {
              $eq: [
                {
                  $type: "$failures.level"
                },
                "missing"
              ]
            },
            0,
            1
          ]
        }
      },
      rules: {
        $first: "$rules"
      }
    }
  },
  {
    $group: {
      _id: "$_id.id",
      count: {
        $push: {
          $cond: [
            {
              $eq: [
                {
                  $type: "$_id.level"
                },
                "missing"
              ]
            },
            "$$REMOVE",
            {
              k: "$_id.level",
              v: "$count"
            }
          ]
        }
      },
      rules: {
        $first: "$rules"
      }
    }
  },
  
  {
    $project: {
      "_id": 0,
      "id": "$_id",
      "count": {
        $arrayToObject: "$count"
      },
      "rules": 1
    }
  }
])

Demo Approach 1 @ Mongo Playground


Approach 2

  1. Use the $facet stage to handle the failures array which is empty and non-empty separately.

  2. Combine both emptyFailures and withFailures arrays into one.

  3. Deconstruct the failures array.

  4. Replace the input document with the failures object.

db.collection.aggregate([
  {
    $match: {
      "id": {
        $in: [
          1,
          2,
          3
        ]
      }
    }
  },
  {
    $facet: {
      emptyFailures: [
        {
          $match: {
            $expr: {
              $eq: [
                {
                  $ifNull: [
                    "$failures",
                    []
                  ]
                },
                []
              ]
            }
          }
        },
        {
          $set: {
            counts: {
              $literal: {}
            }
          }
        },
        {
          $project: {
            "_id": 0,
            "id": 1,
            "counts": 1,
            "rules": 1
          }
        }
      ],
      withFailures: [
        {
          $match: {
            failures: {
              $ne: []
            }
          }
        },
        {
          $unwind: {
            path: "$failures"
          }
        },
        {
          $group: {
            _id: {
              id: "$id",
              level: "$failures.level"
            },
            count: {
              $sum: 1
            },
            rules: {
              $first: "$rules"
            }
          }
        },
        {
          $group: {
            _id: "$_id.id",
            count: {
              $push: {
                k: "$_id.level",
                v: "$count"
              }
            },
            rules: {
              $first: "$rules"
            }
          }
        },
        {
          $project: {
            "_id": 0,
            "id": "$_id",
            "count": {
              $arrayToObject: "$count"
            },
            "rules": 1
          }
        }
      ]
    }
  },
  {
    $set: {
      failures: {
        $concatArrays: [
          "$withFailures",
          "$emptyFailures"
        ]
      }
    }
  },
  {
    $unwind: "$failures"
  },
  {
    $replaceWith: "$failures"
  }
])

Demo Approach 2 @ Mongo Playground