Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to have mongoose populate a Mixed field only if the field contains an ObjectId?

Setup

Let's say we have schemas for Foo and Bar. Foo has a field bar. This field can either contain an ObjectId that reference a document Bar, or it could contain other arbitrary objects that are not ObjectIds.

const fooSchema = new mongoose.Schema({
  bar: {
    type: mongoose.Schema.Types.Mixed,
    ref: 'Bar'
  }
});
const Foo = <any>mongoose.model<any>('Foo', fooSchema);

const barSchema = new mongoose.Schema({
  name: String
});
const Bar = <any>mongoose.model<any>('Bar', barSchema);

Problem

Now suppose we have a bunch of Foo documents.

I would like to be able to use mongoose's populate on the bar field to automatically replace references to a Bar document with the actual Bar document itself. I would also like to leave all other objects that are not references to a Bar document unchanged.

Normally, I would use something like this to get all the Foo documents and then populate the bar field:

Foo.find().populate('bar')

However, this method will throw an exception when it encounters objects in the bar field that are not ObjectIds, as opposed to leaving them untouched.

UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): CastError: Cast to ObjectId failed for value "Some arbitrary object" at path "_id" for model "Bar"

Attempt at finding a solution

I have examined using the match option on populate, by requiring that a field on Bar exists:

Foo.find().populate({
  path: 'bar',
  match: {
    name: {
      $exists: true
    }
  }
}

Unfortunately, the error is the same.

Question

So my question is then, is there any way to get mongoose to only populate a field if the field contains an ObjectId, and leave it alone otherwise?

like image 599
Zsw Avatar asked Nov 08 '22 16:11

Zsw


1 Answers

As far as I know you cannot use populate that way. Select property works after trying to get values for population and there's no way to filter it before that.

You would have to do it manually. You could do it manually.

let foos = await Foo.find({});
foos = foos.map(function (f) {
  return new Promise(function (resolve) {
    if (condition()) {
        Foo.populate(f, {path: 'bar'}).then(function(populatedF){
            resolve(f);
        });
    } else {
      resolve(f);
    }
  });
});

await Promise.all(foos).then(function (fs) {
  res.status(200).json(fs);
});

Elegantly would be to wrap it in post hook or static method on your Model.

Another option would be to send 2 queries:

const foosPopulated = Foo.find({ alma: { $type: 2 } }).populate('bar'); // type of string
const foosNotPopulated = Foo.find({ alma: { $type: 3 } }); // type of object

const foos = foosPopulated.concat(foosNotPopulated);

This is of course suboptimal because of 2 queries (and all population queries) but maybe this will not be a problem for you. Readability is much better. Of course you could then change find queries to match your case specifically.

like image 98
barnski Avatar answered Nov 14 '22 23:11

barnski