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