Recursively populate parent-child tree from MongoDB collection

I have a MongoDB database with a collection, using a mixture of Parent and Child References.

Each document in the collection includes two properties: parentId and childIds.

The parentId property is an (ObjectId) reference to the parent document.

The childIds property is an array of (ObjectId) references to child documents.

Given the collection:

[
  {
    "_id": {
      "$oid": "625fa5b1a8142aef1a1d034d"
    },
    "taskId": 1,
    "taskName": "New Task 1",
    "parentId": null,
    "childIds": [
      {
        "$oid": "625fa66a409d15316aada92e"
      }
    ],
    "subtasks": []
  },
  {
    "_id": {
      "$oid": "625fa66a409d15316aada92e"
    },
    "taskId": 2,
    "taskName": "New Task 2",
    "parentId": {
      "$oid": "625fa5b1a8142aef1a1d034d"
    },
    "childIds": [
      {
        "$oid": "625fc16c409d15316aada94c"
      }
    ],
    "subtasks": []
  },
  {
    "_id": {
      "$oid": "625fc16c409d15316aada94c"
    },
    "taskId": 3,
    "taskName": "New Task 3",
    "parentId": {
      "$oid": "625fa66a409d15316aada92e"
    },
    "childIds": [],
    "subtasks": []
  },
  {
    "_id": {
      "$oid": "625fc16f409d15316aada94f"
    },
    "taskId": 4,
    "taskName": "New Task 4",
    "parentId": null,
    "childIds": [
      {
        "$oid": "625fc173409d15316aada952"
      }
    ],
    "subtasks": []
  },
  {
    "_id": {
      "$oid": "625fc173409d15316aada952"
    },
    "taskId": 5,
    "taskName": "New Task 5",
    "parentId": {
      "$oid": "625fc16f409d15316aada94f"
    },
    "childIds": [],
    "subtasks": []
  }
]

My goal is to populate a recursive tree structure from that collection that can be sent to the client, so any child object will be in it’s respective parent subtasks array like:

[
  {
    "_id": "625fa5b1a8142aef1a1d034d",
    "taskId": 1,
    "taskName": "New Task 1",
    // This is a parent task so parentId is null
    "parentId": null,
    "childIds": ["625fa66a409d15316aada92e"],
    // subtasks will be the objects of the childIds,
    // just 625fa66a409d15316aada92e in this case
    "subtasks": [
      {
        "_id": "625fa66a409d15316aada92e",
        "taskId": 2,
        "taskName": "New Task 2",
        // This is a child, but also has children,
        // so both parentId and childIds are present
        "parentId": "625fa5b1a8142aef1a1d034d",
        "childIds": ["625fc16c409d15316aada94c"],
        "subtasks": [
          {
            "_id": "625fc16c409d15316aada94c",
            "taskId": 3,
            "taskName": "New Task 3",
            "parentId": "625fa66a409d15316aada92e",
            "childIds": [],
            "subtasks": []
          }
        ]
      }
      // ... More subtasks
    ]
  },
  {
    "_id": "625fc16f409d15316aada94f",
    "taskId": 4,
    "taskName": "New Task 4",
    "parentId": null,
    "childIds": ["625fc173409d15316aada952"],
    "subtasks": [
      {
        "_id": "625fc173409d15316aada952",
        "taskId": 5,
        "taskName": "New Task 5",
        "parentId": "625fc16f409d15316aada94f",
        "childIds": [],
        "subtasks": []
      }
    ]
  }
  // ... More tasks
]

I’m looking through similar issues that suggest using populate or aggregate but don’t have anything concrete to start with.

I currently just have this, where result is the array of documents from the collection as is:

export class Task extends Service {
  constructor(options: Partial<MongooseServiceOptions>, app: Application) {
    super(options);
  }

  find(params: Params): any {
    return super.find(params).then((result: any) => {
      // Todo: Populate parent-child tree structure to send to client

      return result;
    });
  }
}