How to do a mongoose Populate Virtual with a localField inside an array of objects

72 views Asked by At

I am trying to use mongoose to auto populate a model with another model. following this official documentation: https://mongoosejs.com/docs/populate.html

I cannot use the standard ref: 'ModelName' because that only works if you use _id fields as your relational fields... and I have my own relational linking fields like 'itemId' and 'subItemIs' and I cannot make use of the '_id' fields

Therefore I am using the sub-feature called Populate Virtuals (official documentation here: https://mongoosejs.com/docs/populate.html#populate-virtuals)

And although I can make that feature work perfectly on some modeling cases.... the particular model case that I have is the only one in which I cannot make it work.

This is one of the cases of "a picture says more than a thousand words" so instead of describing the problem is better to simply read a data+code snippet that exposes when it works and when it fails

I actually created a self-explanatory isolated piece of data + code that fully describes the need and the failure to achieve what's expected:

Mongoose version: 8.1.1 Node.js version: 20.5.0 MongoDB server version: 7.0.4 Typescript version: not used

// Mongo Data:
//
// Collection: units
// { unitId: 'in', label: 'Inches' }
// { unitId: 'ft', label: 'Feet' }
// { unitId: 'yr', label: 'Yard' }
//
// Collection: reports:
// {
//   reportId: 'r1',
//   singleMeasurement:
//     { value: '1', unitId: 'in' },
//   multipleMeasurements: [
//     { value: '2', unitId: 'ft' },
//     { value: '3', unitId: 'yr' }
//   ]
// }

import mongoose from 'mongoose'

const unitSchema = new mongoose.Schema({
  unitId: { type: String },
  label: { type: String }
})

const reportSchema = new mongoose.Schema({
  reportId: { type: String },
  singleMeasurement: {
    value: { type: Number },
    unitId: { type: String } // This WORKS to assist creating 'unit' attribute in 'singleMeasurement' obj
  },
  multipleMeasurements: [{
    value: { type: Number },
    unitId: { type: String } // This FAILS to assist creating 'unit' attribute in 'multipleMeasurements' obj array
  }]
})

reportSchema.virtual('singleMeasurement.unit', { ref: 'Unit', localField: 'singleMeasurement.unitId', foreignField: 'unitId' })
reportSchema.virtual('multipleMeasurements.unit', { ref: 'Unit', localField: 'multipleMeasurements.unitId', foreignField: 'unitId' })

reportSchema.set('toJSON', { virtuals: true })
reportSchema.set('toObject', { virtuals: true })

// // // // // // // // // // // // // // //

const reportModel = mongoose.model('Report', reportSchema, 'reports')
const unitModel = mongoose.model('Unit', unitSchema, 'units')

// // // // // // // // // // // // // // //

await mongoose.connect('mongodb://username:password@localhost:27017/database')

const report = await reportModel.findOne({ reportId: 'r1' })

await report.populate('singleMeasurement.unit')
await report.populate('multipleMeasurements.unit')

console.log('\n\n singleMeasurement: ' + report.singleMeasurement.toString())
// this WORKS; by including singleMeasurement.unit

console.log('\n\n multipleMeasurements: ' + report.multipleMeasurements.toString())
// this FAILS; by missing multipleMeasurements.unit

These where the expected results and assumptions:

// report.singleMeasurement.unit      => Expected: Object => Received: Object      => OK
// report.multipleMeasurements[].unit => Expected: Object => Received: undefined   => FAIL

I also tried changing the localField to 'multipleMeasurements.$*.unitId' which is the syntax required to support populating inside Maps.... but it also didn't work.

Is there may be another syntax similar to that one but for arrays that I'm missing to understand ?

1

There are 1 answers

0
jQueeny On

The best way to do this would be to create a multipleMeasurement schema and register the virtual on that schema. You can then use that schema as a type for your multipleMeasurements array. Here is an example:

  1. Create a separate multipleMeasurementSchema
const multipleMeasurementSchema = new mongoose.Schema({
    value: {
        type: Number
    },
    unitId: {
        type: String
    }
}, {
    toJSON: {
        virtuals: true
    },
    toObject: {
        virtuals: true
    }
});
  1. Register the virtual on the multipleMeasurementSchema
multipleMeasurementSchema.virtual('unit', { ref: 'Unit', localField: 'unitId', foreignField: 'unitId' });
  1. Set the multipleMeasurementSchema as the type of multipleMeasurements array
const reportSchema = new mongoose.Schema({
    reportId: { type: String },
    singleMeasurement: {
        value: { type: Number },
        unitId: { type: String }
    },
    multipleMeasurements: [multipleMeasurementSchema]
})
  1. Register the virtual on the singleMeasurement.unit as normal
reportSchema.virtual('singleMeasurement.unit', { ref: 'Unit', localField: 'singleMeasurement.unitId', foreignField: 'unitId' });
reportSchema.set('toJSON', { virtuals: true })
reportSchema.set('toObject', { virtuals: true })
  1. Now when you populate
const report = await reportModel.findOne({ reportId: 'r1' })
.populate('singleMeasurement.unit')
.populate('multipleMeasurements.unit')