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);
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"
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.
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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With