mongodb-js / mongoose-autopopulate

Always populate() certain fields in your mongoose schemas
Apache License 2.0
221 stars 36 forks source link

Doesn't autopopulate with refPath when documents are autopopulated from a virtual field #96

Closed neohotsauce closed 1 year ago

neohotsauce commented 2 years ago

I am autopopulating a virtual field and the populated documents have a field with refPath. However the field with refPath is not autopopulated. The same field is autopopulated if refPath is replaced with ref.

Schema with the virtual:

 const AddressSchema = new mongoose.Schema(
    {
      name: {
        type: String
      },
      status: {
        type: String,
        enum: ["active", "inactive"],
        default: "active"
      }
    },
    {
      timestamps: true,
      toJSON: { virtuals: true },
      toObject: { virtuals: true }
    }
  );

  AddressSchema.virtual("residentials", {
    ref: "Citizen",
    localField: "_id",
    foreignField: "permanentAddress.address",
    justOne: false,
    autopopulate: true,
    match: {
      status: "active"
    },
    options: {
      select:
        "name nId"
    }
  });

  AddressSchema.plugin(require("mongoose-autopopulate"));

I am autopopulating residentials as a virtual and the populated documents have the below schema.

Schema of the virtually populated documents:

 const CitizenSchema = new mongoose.Schema(
    {
      nId: {
        type: String,
        unique: true,
        required: [true, "Please add national ID card"]
      },
      name: {
        type: String,
        required: [true, "Please add a name"],
        trim: true
      },
      permanentAddress: {
        name: {
          type: String,
          trim: true
        },
        address: {
          type: mongoose.Schema.ObjectId,
          ref: "Address"
        },
      },
      father: {
        type: mongoose.Schema.ObjectId,
        refPath: "fatherType",
        autopopulate: true
      },
      fatherType: {
        type: String,
        enum: ["Citizen", "Guest"],
        required: true
      },
      status: {
        type: String,
        enum: ["active", "inactive"],
        default: "active"
      }
    },
    {
      timestamps: true,
      toJSON: { virtuals: true },
      toObject: { virtuals: true }
    }
  );

  CitizenSchema.plugin(require("mongoose-autopopulate"));

The father field is not autopopulated with refPath but is autopopulated when replaced with ref.

neohotsauce commented 2 years ago

It works if select option is removed from the virtual. Works if the virtual is as below:

 AddressSchema.virtual("residentials", {
    ref: "Citizen",
    localField: "_id",
    foreignField: "permanentAddress.address",
    justOne: false,
    autopopulate: true,
    match: {
      status: "active"
    }
  });
IslandRhythms commented 1 year ago

The virtual is not autopopulated while the document property is.

const mongoose = require('mongoose');

const AddressSchema = new mongoose.Schema(
  {
    name: {
      type: String
    },
    status: {
      type: String,
      enum: ["active", "inactive"],
      default: "active"
    }
  },
  {
    timestamps: true,
    toJSON: { virtuals: true },
    toObject: { virtuals: true }
  }
);

AddressSchema.virtual("residentials", {
  ref: "Citizen",
  localField: "_id",
  foreignField: "permanentAddress.address",
  justOne: false,
  autopopulate: true,
  match: {
    status: "active"
  },
  options: {
    select:
      "name nId"
  }
});

AddressSchema.plugin(require("mongoose-autopopulate"));

const CitizenSchema = new mongoose.Schema(
  {
    nId: {
      type: String,
      unique: true,
      required: [true, "Please add national ID card"]
    },
    name: {
      type: String,
      required: [true, "Please add a name"],
      trim: true
    },
    permanentAddress: {
      name: {
        type: String,
        trim: true
      },
      address: {
        type: mongoose.Schema.ObjectId,
        ref: "Address"
      },
    },
    father: {
      type: mongoose.Schema.ObjectId,
      refPath: "fatherType",
      autopopulate: true
    },
    fatherType: {
      type: String,
      enum: ["Citizen", "Guest"],
      required: true
    },
    status: {
      type: String,
      enum: ["active", "inactive"],
      default: "active"
    }
  },
  {
    timestamps: true,
    toJSON: { virtuals: true },
    toObject: { virtuals: true }
  }
);

CitizenSchema.plugin(require("mongoose-autopopulate"));

const Address = mongoose.model('Address', AddressSchema);

const Citizen = mongoose.model('Citizen', CitizenSchema);

async function run() {
  await mongoose.connect('mongodb://localhost:27017');
  await mongoose.connection.dropDatabase();

  const entry = await Address.create({
    name: "Another name for The Address",
    status: "active"
  });

  const doc = await Citizen.create({
    nId: 'Hello',
    name: 'There',
    permanentAddress: {
      name: 'The Address',
      address: entry._id
    },
    fatherType: "Guest"
  });
  const res = await Citizen.create({
    nId: 'Yo',
    name: 'Test',
    permanentAddress: {
      name: "The Address",
      address: entry._id
    },
    father: doc._id,
    fatherType: "Citizen",
    status: "active"
  });
  console.log(await Citizen.find());
  console.log(await Address.findOne())
}

run();

Output:

[
  {
    permanentAddress: {
      name: 'The Address',
      address: new ObjectId("63bc67003be77fc70e511bff")
    },
    _id: new ObjectId("63bc67003be77fc70e511c02"),
    nId: 'Hello',
    name: 'There',
    fatherType: 'Guest',
    status: 'active',
    createdAt: 2023-01-09T19:12:00.278Z,
    updatedAt: 2023-01-09T19:12:00.278Z,
    __v: 0,
    id: '63bc67003be77fc70e511c02'
  },
  {
    permanentAddress: {
      name: 'The Address',
      address: new ObjectId("63bc67003be77fc70e511bff")
    },
    _id: new ObjectId("63bc67003be77fc70e511c04"),
    nId: 'Yo',
    name: 'Test',
    father: {
      permanentAddress: [Object],
      _id: new ObjectId("63bc67003be77fc70e511c02"),
      nId: 'Hello',
      name: 'There',
      fatherType: 'Guest',
      status: 'active',
      createdAt: 2023-01-09T19:12:00.278Z,
      updatedAt: 2023-01-09T19:12:00.278Z,
      __v: 0,
      id: '63bc67003be77fc70e511c02'
    },
    fatherType: 'Citizen',
    status: 'active',
    createdAt: 2023-01-09T19:12:00.305Z,
    updatedAt: 2023-01-09T19:12:00.305Z,
    __v: 0,
    id: '63bc67003be77fc70e511c04'
  }
]
{
  _id: new ObjectId("63bc67003be77fc70e511bff"),
  name: 'Another name for The Address',
  status: 'active',
  createdAt: 2023-01-09T19:12:00.233Z,
  updatedAt: 2023-01-09T19:12:00.233Z,
  __v: 0,
  residentials: [
    {
      permanentAddress: [Object],
      _id: new ObjectId("63bc67003be77fc70e511c02"),
      nId: 'Hello',
      name: 'There',
      id: '63bc67003be77fc70e511c02'
    },
    {
      permanentAddress: [Object],
      _id: new ObjectId("63bc67003be77fc70e511c04"),
      nId: 'Yo',
      name: 'Test',
      father: new ObjectId("63bc67003be77fc70e511c02"),
      id: '63bc67003be77fc70e511c04'
    }
  ],
  id: '63bc67003be77fc70e511bff'
}
vkarpov15 commented 1 year ago

I took a closer look and the issue is that Mongoose's selectPopulatedFields helper only adds the fields being populated to the projection. It also needs to add the field referenced by refPath to the projection.

As a workaround, you can add fatherType to the projection: select: "name nId fatherType"

vkarpov15 commented 1 year ago

Fix will be in Mongoose 7.4.4.