Search code examples
node.jsmongodbmongooseaggregation-frameworkmongoose-schema

How to unwind subdocument array, and then reverse the process, without losing other keys?


I'm completely new to Mongoose.

I have this Schema:

const houseSchema = new mongoose.Schema({  
  _id: String,
  housename:  {type: String,unique: true},
  adress: String,
  people: [ {
      name: String,
      age: Number
    } ],
});

And document:

 let house = new House({
    _id: 20,
    housename: "white",
    adress: "St1",
    people:[
      {name: "Jon", age: 23},
      {name: "Ann", age:50},
      {name: "Pat", age:20},
      {name: "Helen", age:15}]
  });

I want to find this document by id, and filter people array by age, then return full document without filtered objects. So expected output would be: (for age>21 ):

{
    _id: 20,
    housename: "white",
    adress: "St1",
    people:[
      {_id:"65976faeaa644d02c4090826", name: "Jon", age: 23},
      {_id:"65976faeaa644d02c4090827", name: "Ann", age:50}
    ]
 }

My solution to this problem, after hours of trying is:

app.get("/api/test", (req, res) => {
  var searchID = "20";
  var minAge = 21;
  House.aggregate([{$match: {_id: searchID}}])
  .unwind("people").match({'people.age': {$gt: minAge}})
  .group({    
        _id: "$_id",       
        people: {$push: "$people"}         
  }) 
  .exec((err,data)=>{
        res.json(data); 
    }); 
});

So first I match id, then rewind people array - so I can filter it out, and then I try to rejoin to the first form. However after grouping I lose housename and adress fields. Here is output:

{"_id":"20",
"people":[
{"_id":"65976faeaa644d02c4090826","name":"Jon","age":23},{"_id":"65976faeaa644d02c4090827","name":"Ann","age":50}]}

I have no idea how to keep housename and adress fields at the output. I've tried adding.projection({housename:1 ,adress:1 ... }); ,but it did nothing, I think those keys don't exist in pipe after group(). I also have been thinking about saving those values to vars after match() and then adding them at the end, but I don't know how to access them in the middle of the chain.


Solution

  • You can use a simple $filter to filter the people objects that match your filter condition like so:

    app.get("/api/test", async (req, res) => { //< Mark as async
      var searchID = "20";
      var minAge = 21;
      try{
          const data = await House.aggregate([
          {
             $match: {
                "_id": searchID 
             }
          },
          {
             "$addFields": {
                people: {
                   $filter: {
                      input: "$people",
                      as: "p",
                      cond: {
                         $gt: [
                            "$$p.age",
                            minAge 
                         ]
                      }
                   }
                }
             }
          }
          ]);
          res.json(data);
       } catch(err){
          console.log(err);
          res.json({message: 'Error on server'});
       }
    });
    

    See HERE for a working example.

    The use of $addFields is important here because:

    Adds new fields to documents. $addFields outputs documents that contain all existing fields from the input documents and newly added fields. The $addFields stage is equivalent to a $project stage that explicitly specifies all existing fields in the input documents and adds the new fields.

    which I think was where you were having trouble with the $project. All I have done is use $addFields to overwrite the existing people property with the new filtered version.