3

I'm working in a REST api with ExpressJS and Mongo and I have a collection with N quantity of levels.

So to solve this problem I'm using an recursive table (or collection) in mongo where a field is the id and every register has a parent_id which is at the same level as it's childs. To explain better this, here is an E-R representation

enter image description here

So as you se, mongo will save the data like this json (accounts level 0 has null parent)

[
  { "id": "45TYYU", "parent_id": null, "name":"account 1", "type": 1, "category": 1 },
  { "id": "45TYYXT", "parent_id": "45TYYU", "name":"account 2", "type": 1, "category": 1 },
  { "id": "45TYYPZ", "parent_id": "45TYYU", "name":"account 3", "type": 1, "category": 1 },
  { "id": "45TYYPZRE", "parent_id": "45TYYPZ", "name":"account 4", "type": 1, "category": 1 },
  { "id": "45TYYPZSX", "parent_id": "45TYYPZ", "name":"account 5", "type": 1, "category": 1 },
  { "id": "45TYYPZGP", "parent_id": "45TYYXT", "name":"account 6", "type": 1, "category": 1 }
]

account 2 and account 3 are children of account 1, while account 4 and account 5 are children of account tree and account 6 is child of account 2 ... but every register is at the same logical level only identifying through parent_id.

so I need to transform this data into a GET method to restructure it like this:

[
    { 
        "id": "45TYYU",
        "parent_id": null,
        "name":"account 1",
        "type": 1,
        "category": 1,
        "children": [
            { 
                "id": "45TYYXT",
                "parent_id": "45TYYU",
                "name":"account 2",
                "type": 1,
                "category": 1,
                "children": [
                    { "id": "45TYYPZGP", "parent_id": "45TYYXT", "name":"account 6", "type": 1, "category": 1 }
                ]
            },
            { 
                "id": "45TYYPZ",
                "parent_id": "45TYYU",
                "name":"account 3",
                "type": 1,
                "category": 1,
                "children": [
                    { "id": "45TYYPZRE", "parent_id": "45TYYPZ", "name":"account 4", "type": 1, "category": 1 },
                    { "id": "45TYYPZSX", "parent_id": "45TYYPZ", "name":"account 5", "type": 1, "category": 1 }
                ]
            }
        ]
    },
    { 
        "id": "45TFJK",
        "parent_id": null,
        "name":"account 7",
        "type": 1,
        "category": 1,
        "children": [
            { 
                "id": "47HJJT",
                "parent_id": "45TFJK",
                "name":"account 8",
                "type": 1,
                "category": 1
            },
            { 
                "id": "47YHJU",
                "parent_id": "45TFJK",
                "name":"account 8",
                "type": 1,
                "category": 1
            }
        ]
    }
]

Yes... the parents level 0 has null parent_id and I want to put it's children inside an array called "children" and then send like this in the GET response to my UI

What is the best way to do this in expressJS? Is there a library or component out there that allows me to do this?

Thank you

1 Answer 1

16
+100

You can use $graphLookup and other useful array operators,

  • $match filter that records only have parent_id is null
  • $graphLookup to get child records and depth number in depthField level
  • $unwind deconstruct children array and allow to not remove empty children
  • $sort by depth level field level in descending order
  • $group by id field and reconstruct children array
db.collection.aggregate([
  { $match: { parent_id: null } },
  {
    $graphLookup: {
      from: "collection",
      startWith: "$id",
      connectFromField: "id",
      connectToField: "parent_id",
      depthField: "level",
      as: "children"
    }
  },
  {
    $unwind: {
      path: "$children",
      preserveNullAndEmptyArrays: true
    }
  },
  { $sort: { "children.level": -1 } },
  {
    $group: {
      _id: "$id",
      parent_id: { $first: "$parent_id" },
      name: { $first: "$name" },
      type: { $first: "$type" },
      category: { $first: 1 },
      children: { $push: "$children" }
    }
  },
  • $addFields now find the nested level children and allocate to its level,
    • $reduce to iterate loop of children array.
    • initialize default field level default value is -1, presentChild is [], prevChild is [] for the conditions purpose
    • $let to initialize fields:
      • prev as per condition if both level are equal then return prevChild otherwise return presentChild
      • current as per condition if both level are equal then return presentChild otherwise []
    • in to return level field and prevChild field from initialized fields
      • presentChild $filter children from prev array and return, merge current objects with children array using $mergeObjects and concat with current array of let using $concatArrays
  • $addFields to return only presentChild array because we only required that processed array
  {
    $addFields: {
      children: {
        $reduce: {
          input: "$children",
          initialValue: { level: -1, presentChild: [], prevChild: [] },
          in: {
            $let: {
              vars: {
                prev: {
                  $cond: [
                    { $eq: ["$$value.level", "$$this.level"] },
                    "$$value.prevChild",
                    "$$value.presentChild"
                  ]
                },
                current: {
                  $cond: [{ $eq: ["$$value.level", "$$this.level"] }, "$$value.presentChild", []]
                }
              },
              in: {
                level: "$$this.level",
                prevChild: "$$prev",
                presentChild: {
                  $concatArrays: [
                    "$$current",
                    [
                      {
                        $mergeObjects: [
                          "$$this",
                          {
                            children: {
                              $filter: {
                                input: "$$prev",
                                as: "e",
                                cond: { $eq: ["$$e.parent_id", "$$this.id"] }
                              }
                            }
                          }
                        ]
                      }
                    ]
                  ]
                }
              }
            }
          }
        }
      }
    }
  },
  {
    $addFields: {
      id: "$_id",
      children: "$children.presentChild"
    }
  }
])

Playground

5
  • the problem with this solution is the ordering of records, it keeps changing and we cannot add a sorting to the top level because of the unwind, and the $addFields require the ordering to be by level, do you have a solution for it? Commented Sep 25, 2024 at 15:05
  • @nonsensecreativity You can add $sort stage after the $group stage and before the $addFields stage.
    – turivishal
    Commented Sep 25, 2024 at 19:41
  • that sorted the grouped field, a deeply nested children sort is still depend on mongo, and it's not always the same, sometime i got completely different children order Commented Sep 30, 2024 at 0:13
  • after stress testing the same query i found that the only way to sort the deeply nested children correctly is put the $sortArray inside the $mergeObjects wrapping the $filter aggregation, as seen in here [mongoplayground.net/p/o8ndJ9HT4lA] but i am not sure with the performance, and the sort of the top level need to be put at the end of the pipeline Commented Sep 30, 2024 at 0:28
  • @nonsensecreativity Great, I have not reviewed it yet, but one thing is this kind of query doesn't give performance, this is just to handle some exceptional cases. Otherwise, you have to work on better schema design by choosing the right schema pattern os such cases need to be handled on the client side.
    – turivishal
    Commented Sep 30, 2024 at 15:59

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.