mongoosemongoose-populate

How to dynamically select model in Mongoose while populating


Mongoose - 7.2.1

I have a model like this:

const LessonSchema = new Schema<ILesson>(
  {
    title: { type: String, required: true },
    type: {
      type: String,
      required: true,
      enum: ['video', 'pdf'],
    },
    resource: {
      type: Schema.Types.ObjectId,
      required: true,
    },
    isDraft: { type: Boolean, default: true },
  },
  {
    versionKey: false,
    timestamps: true,
  }
);

I have two separate models, VideoLesson and Note, and ids from these VideoLesson or Note is stored in the resource field. While fetching the document, I want to populate the entire document by dynamically referring the model names based on value of type. I don't want to change the enums to model names, so not sure if refPath can be used or not. I am looking for a way to dynamically take the type of document, and provide the name of model in my populate call based on that.

Something on these lines:

await Lesson.findById(id).populate({
  path: 'resource',
  model: (doc) => doc.type === 'video' ? VideoLesson' : 'Note', // This function can be made better, but is this really possible?
});

If above is not possible, is it possible to have refPath as a dynamic thing?


Solution

  • Yes, you can chain the populate method after you do some conditional check like so:

    const lesson = await Lesson.findById(id);
    
    if(lesson.type === 'video'){
       await lesson.populate({ path: 'resource', model: VideoLesson });
    }else if(lesson.type === 'pdf'){
       await lesson.populate({ path: 'resource', model: Note });
    }else{
    //...
    }
    return lesson;
    

    You can obviously streamline this with a case switch if you have multiple enum values or just use conditional (ternary) operator if you only have the two enums but I'll leave that up to you.

    The findById method returns a <<Query>> which allows you to chain the populate after a matching document has been found.

    Normally when using an ObjectId to reference another collection the textbook approach involves stating the referenced Model like so:

    resource: {
       type: Schema.Types.ObjectId,
       required: true,
       ref: Video //< Explicitly providing the collection Model
    },
    

    Thankfully, your schema doesn't have this, which is good and gives you the flexibility to dynamically specify the collection used in the populate lookup.

    If you have control over modifying the schema then you can also use Dynamic References via refPath.

    You would need to update like so:

    const LessonSchema = new Schema<ILesson>(
      {
        title: { type: String, required: true },
        type: {
          type: String,
          required: true,
          enum: ['video', 'pdf'],
        },
        resource: {
          type: Schema.Types.ObjectId,
          required: true,
          refPath: 'docModel' //< Add this
        },
        docModel: { //< Add this
          type: String,
          required: true,
          enum: ['VideoLesson', 'Note'] //< These are your Models
        },
        isDraft: { type: Boolean, default: true },
      },
      {
        versionKey: false,
        timestamps: true,
      }
    );