Repository: Automattic/mongoose (JavaScript)

Issue:
Ref array .push crashes with "Model is not a constructor" if populated in .find().cursor() call
### Prerequisites

- [X] I have written a descriptive issue title
- [X] I have searched existing issues to ensure the bug has not already been reported


### Mongoose version

6.6.5, tested also with 7.3.1

### Node.js version

16.19.1

### MongoDB server version

5.0.17

### Typescript version (if applicable)

_No response_

### Description

When using `.populate` during a `.find` query with `.cursor().eachAsync` on a relation array (`field: [{ type: Schema.Types.ObjectId, ref: 'MyModel' }]`), all .push calls (and possibly other array methods) fail with `Model is not a costructor` seemingly because the populated field is not correctly marked as populated.

### Steps to Reproduce

Create a new npm/yarn folder, install mongoose and run following script:
```javascript
const mongoose = require("mongoose");

const { Schema } = mongoose;

const Company = mongoose.model(
  "Company",
  new Schema({ name: String }),
  "companies"
);
const User = mongoose.model(
  "User",
  new Schema({
    name: String,
    companies: [{ type: Schema.Types.ObjectId, ref: "Company" }],
  }),
  "users"
);

const main = async () => {
  await mongoose.connect(
    "mongodb://localhost:27017/populate-push-crash-testing"
  );

  await Company.insertMany([{ name: "Company 1" }, { name: "Company 2" }]);
  const company = await Company.findOne({ name: "Company 1" });
  const company2 = await Company.findOne({ name: "Company 2" });

  await User.insertMany([{ name: "User 1", companies: [company.id] }]);

  await User.find()
    .populate("companies")
    .cursor()
    .eachAsync(async (user) => {
      if (!user.populated("companies")) await user.populate("companies");

      user.companies.push(company2);

      await user.save();
    });
};

main()
  .catch(console.error)
  .finally(() => {
    process.exit(0);
  });
```

It will produce following error:
```
TypeError: Model is not a constructor
    at Proxy._cast (PROJECT-PATH/node_modules/mongoose/lib/types/array/methods/index.js:242:17)
    at Proxy._mapCast (PROJECT-PATH/node_modules/mongoose/lib/types/array/methods/index.js:260:17)
    at Arguments.map (<anonymous>)
    at Proxy.push (PROJECT-PATH/node_modules/mongoose/lib/types/array/methods/index.js:683:21)
    at PROJECT-PATH/index.js:36:22
    at handleNextResult (PROJECT-PATH/node_modules/mongoose/lib/helpers/cursor/eachAsync.js:173:22)
    at PROJECT-PATH/node_modules/mongoose/lib/helpers/cursor/eachAsync.js:164:11
    at PROJECT-PATH/node_modules/mongoose/lib/cursor/QueryCursor.js:552:7
    at PROJECT-PATH/node_modules/kareem/index.js:191:16
    at processTicksAndRejections (node:internal/process/task_queues:78:11)
```

However if you remove the `.populate` call in the `.find` query, everything seems to work fine. The example is simplified, in our real case we call a method on the model and want to be sure the field is populated, for now I've added a depopulate call and a populate call after that, which makes it work.

Another interesting thing is if I remove the cursor and simply do:
```javascript
  for (const user of await User.find().populate("companies")) {
    if (!user.populated("companies")) await user.populate("companies");

    user.companies.push(company2);

    await user.save();
  }
```
It also seems to work, so this error might be related to cursor?

### Expected Behavior

There should be no Error, mongoose should correctly mark the field as populated and the push should work.


Discussion:

Patch:
diff --git a/lib/helpers/populate/getModelsMapForPopulate.js b/lib/helpers/populate/getModelsMapForPopulate.js
index 7ac134266d7..314661ff2e3 100644
--- a/lib/helpers/populate/getModelsMapForPopulate.js
+++ b/lib/helpers/populate/getModelsMapForPopulate.js
@@ -540,6 +540,7 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re
       // Used internally for checking what model was used to populate this
       // path.
       options[populateModelSymbol] = Model;
+      currentOptions[populateModelSymbol] = Model;
       available[modelName] = {
         model: Model,
         options: currentOptions,
diff --git a/lib/model.js b/lib/model.js
index 71a15f83ded..115fb82030a 100644
--- a/lib/model.js
+++ b/lib/model.js
@@ -4264,7 +4264,7 @@ const excludeIdReg = /\s?-_id\s?/;
 const excludeIdRegGlobal = /\s?-_id\s?/g;
 
 function populate(model, docs, options, callback) {
-  const populateOptions = { ...options };
+  const populateOptions = options;
   if (options.strictPopulate == null) {
     if (options._localModel != null && options._localModel.schema._userProvidedOptions.strictPopulate != null) {
       populateOptions.strictPopulate = options._localModel.schema._userProvidedOptions.strictPopulate;
diff --git a/lib/types/array/methods/index.js b/lib/types/array/methods/index.js
index d7aae6567d9..61e18c958cd 100644
--- a/lib/types/array/methods/index.js
+++ b/lib/types/array/methods/index.js
@@ -226,6 +226,9 @@ const methods = {
     if (populated && value !== null && value !== undefined) {
       // cast to the populated Models schema
       Model = populated.options[populateModelSymbol];
+      if (Model == null) {
+        throw new MongooseError('No populated model found for path `' + this[arrayPathSymbol] + '`. This is likely a bug in Mongoose, please report an issue on github.com/Automattic/mongoose.');
+      }
 
       // only objects are permitted so we can safely assume that
       // non-objects are to be interpreted as _id


Test patch:
diff --git a/test/model.populate.test.js b/test/model.populate.test.js
index d16e0513a19..690e7783872 100644
--- a/test/model.populate.test.js
+++ b/test/model.populate.test.js
@@ -10504,4 +10504,39 @@ describe('model: populate:', function() {
     });
     assert.deepStrictEqual(doc.children.map(c => c.name).sort(), ['Chad', 'Luke']);
   });
+
+  it('allows pushing to model populated in a query cursor (gh-13575)', async function() {
+    const Company = db.model('Company', Schema({ name: String }));
+    const User = db.model(
+      'User',
+      Schema({ name: String, companies: [{ type: Schema.Types.ObjectId, ref: 'Company' }] })
+    );
+
+    await User.deleteMany({});
+    await Company.insertMany([{ name: 'Company 1' }, { name: 'Company 2' }]);
+    const company = await Company.findOne({ name: 'Company 1' });
+    const company2 = await Company.findOne({ name: 'Company 2' });
+
+    await User.insertMany([{ name: 'User 1', companies: [company.id] }]);
+
+    await User.find()
+      .populate('companies')
+      .cursor()
+      .eachAsync(async(user) => {
+        if (!user.populated('companies')) {
+          await user.populate('companies');
+        }
+
+        user.companies.push(company2);
+
+        await user.save();
+      });
+
+    const user = await User.findOne();
+    assert.equal(user.name, 'User 1');
+    assert.deepEqual(
+      user.toObject().companies.sort().map(v => v.toString()),
+      [company._id.toString(), company2._id.toString()]
+    );
+  });
 });