Can I filter a mongo query by virtual populates?

54 views Asked by At

I have a parent reference relationship between a property model and a sale model. Within my sale model, I include a field to reference the parent property:

property: {
   type: mongoose.Schema.ObjectId,
   ref: "Property",
   required: [true, "Sale must belong to a property"],
}

At the end of my sale model, I include a mongoose pre find middleware to populate the name of the referenced property so that it can be filtered by in the query:

saleSchema.pre(/^find/, function (next) {
  this.populate({
    path: "property",
    select: "propInfo.propName",
  });
  next();
});

I would like to submit a request to my sales Router to return sales that have only applied to that property name:

{{URL}}api/v1/sales?property.propInfo.propName=SampleName

When I send a getAll request to {{URL}}api/v1/sales without any filters in the parameters, all of the property names will be populated in the response.

However, whenever I try to filter or query by the virtual populated property name, it will not return anything. It seems as if Mongo is populating the fields of the parent property after the query, even though I have a pre find middleware to populate the virtuals.

Any help or guidance would be greatly appreciated.

1

There are 1 answers

2
jQueeny On

This is a very good question but common misconception. Unfortunately, the pre hook in this case attaches the populate method to the find method. As a result, you cannot query the populated fields as they will not be be added until after the query executes.

With mongoose populate you cannot filter parent documents based on query conditions of child documents. This is one of the limitations.

Thankfully, mongoose models have the Model.aggregate() method where this kind of thing is pretty straightforward.

In the example below I have assumed you have sales collection and a properties collection based on your Sale and Property model names. I have also included some extra random fields such as name just for illustration.

  1. $lookup: this is the same as populate where a lookup is done against the properties colletion to match the Sale.property field against the Property._id field.
  2. $unwind: turn the array from $lookup into an object.
  3. $match: now you can query the propName with your SampleName
  4. $project: only output the fields you want.
const sales = await Sale.aggregate([
  {
    $lookup: {
      from: "properties",
      localField: "property",
      foreignField: "_id",
      as: "property"
    }
  },
  {
    $unwind: "$property"
  },
  {
    $match: {
      "property.propInfo.propName": SampleName
    }
  },
  {
    $project: {
      "name": 1,
      "property.propInfo.propName": 1
    }
  }
])

See HERE for a working example with some test data.