Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Populate on subdocuments with cloned schema #11538

Closed
alejandro-venegas opened this issue Mar 17, 2022 · 4 comments
Closed

Populate on subdocuments with cloned schema #11538

alejandro-venegas opened this issue Mar 17, 2022 · 4 comments
Labels
confirmed-bug We've confirmed this is a bug in Mongoose and will fix it.
Milestone

Comments

@alejandro-venegas
Copy link

alejandro-venegas commented Mar 17, 2022

Do you want to request a feature or report a bug?
A bug

What is the current behavior?

The is a weird one.

I developed a custom update history plugin which stores a snapshot on a document every time its updated, along with the date of creation and some other data. The plugin uses the cloned schema of the model to store the snapshot, which helps to maintain structure and refs to populate later.

That's working great, the problem is every time I try to populate a path on the snapshot the whole snapshot is populated with the current document, as if the snapshot as a whole was a ref to the document.

This was working on version 6.0.13. My other dependencies haven't change.

Simple example:

// Bar model, where the History schema will run and with a reference to a Foo document
  const BarSchema = new Schema({
    name: String,
    creationDate: {
      type: Date,
      default: () => new Date(),
    },
    foo: {
      type: Schema.Types.ObjectId,
      ref: "Foo",
    },
  });

  // History SubDocument generation, think of this as a plugin

  // Make a BarSchema clone to have a structured document with past properties, as a snapshot
  const BarSchemaClone = BarSchema.clone();

  const HistorySchemaObj = {
    updated: {
      type: Date,
      default: () => new Date(),
    },
    document: BarSchemaClone,
  };

  BarSchema.add({ __history: [HistorySchemaObj] });

  // Before the document saves, add historic data to __history array

  BarSchema.pre("save", function () {
    if (!this.isNew) {
      const historyObject = { document: this };

      this.__history.push(historyObject);
    }
  });

  const Bar = mongoose.model("Bar", BarSchema);

  // Referenced model

  const FooSchema = new Schema({
    name: String,
  });

  const Foo = mongoose.model("Foo", FooSchema);

  // Save some foo documents to add to Bar

  const foo1 = new Foo({ name: "Foo 1" });
  const foo2 = new Foo({ name: "Foo 2" });
  const foo3 = new Foo({ name: "Foo 3" });

  await Promise.all([foo1.save(), foo2.save(), foo3.save()]);

  // Create a new Bar and update it

  const bar1 = new Bar({ name: "Bar", foo: foo1 });
  await bar1.save();

  bar1.foo = foo2;
  await bar1.save();

  bar1.foo = foo2;
  await bar1.save();

  bar1.foo = foo3;
  bar1.name = "Edited name";
  await bar1.save();

  // Populate foo on history subdocuments, for some strange reason the populate actually populates the document path (which is a subdocument without a ref) to
  // the contents of the bar1 document, as if the document path was a ref to the same document

  await bar1.populate({
    path: "__history",
    populate: { path: "document", populate: "foo" },
  });

What is the expected behavior?

The snapshot ("document" path on __history) should only populate the ref paths, mantaining the subdocument intact.

What are the versions of Node.js, Mongoose and MongoDB you are using? Note that "latest" is not a version.

node.js: v16.14.1
mongoose: 6.2.7
MongoDB: 5.0.6

@IslandRhythms IslandRhythms added the confirmed-bug We've confirmed this is a bug in Mongoose and will fix it. label Mar 17, 2022
@IslandRhythms
Copy link
Collaborator

const mongoose = require('mongoose');
const {Schema} = mongoose;

// Bar model, where the History schema will run and with a reference to a Foo document
const BarSchema = new Schema({
    name: String,
    creationDate: {
      type: Date,
      default: () => new Date(),
    },
    foo: {
      type: Schema.Types.ObjectId,
      ref: "Foo",
    },
  });

  // History SubDocument generation, think of this as a plugin

  // Make a BarSchema clone to have a structured document with past properties, as a snapshot
  const BarSchemaClone = BarSchema.clone();

  const HistorySchemaObj = {
    updated: {
      type: Date,
      default: () => new Date(),
    },
    document: BarSchemaClone,
  };

  BarSchema.add({ __history: [HistorySchemaObj] });

  // Before the document saves, add historic data to __history array

  BarSchema.pre("save", function () {
    if (!this.isNew) {
      const historyObject = { document: this };

      this.__history.push(historyObject);
    }
  });

  const Bar = mongoose.model("Bar", BarSchema);

  // Referenced model

  const FooSchema = new Schema({
    name: String,
  });

  const Foo = mongoose.model("Foo", FooSchema);

  // Save some foo documents to add to Bar

  async function run() {
    await mongoose.connect('mongodb://localhost:27017');
    await mongoose.connection.dropDatabase();
    const foo1 = new Foo({ name: "Foo 1" });
    const foo2 = new Foo({ name: "Foo 2" });
    const foo3 = new Foo({ name: "Foo 3" });
    await foo1.save();
    await foo2.save();
    await foo3.save();


  // Create a new Bar and update it

  const bar1 = new Bar({ name: "Bar", foo: foo1 });
  await bar1.save();

  bar1.foo = foo2;
  await bar1.save();

  bar1.foo = foo2;
  await bar1.save();

  bar1.foo = foo3;
  bar1.name = "Edited name";
  await bar1.save();

  // Populate foo on history subdocuments, for some strange reason the populate actually populates the document path (which is a subdocument without a ref) to
  // the contents of the bar1 document, as if the document path was a ref to the same document

console.log('before populate', bar1);
console.log('nested doc', bar1.__history[0]);
console.log('=============================================================')


 const result = await bar1.populate({
    path: "__history",
    populate: { path: "document", populate: "foo" },
  });

  console.log('after populate', result);
  console.log('after nested doc', result.__history[0])
  }

  run();
  

@vkarpov15 vkarpov15 added this to the 6.2.12 milestone Mar 18, 2022
@vkarpov15
Copy link
Collaborator

vkarpov15 commented Apr 16, 2022

The workaround is to do:

 const result = await bar1.populate('__history.document.foo');

That's the correct way to populate anyway. Don't populate('__history'), because __history is an array of embedded documents, not an array of references to documents.

That being said, we'll take a look and see why populate() is behaving this way.

vkarpov15 added a commit that referenced this issue Apr 16, 2022
@vkarpov15 vkarpov15 reopened this Apr 16, 2022
@vkarpov15
Copy link
Collaborator

Note to self: the root cause of this issue is the internal populateModelSymbol option getting set by the top-level populate('__history'). We should look into that some more, because that may cause other bugs with nested populate

@vkarpov15
Copy link
Collaborator

Re: #11538 (comment), I checked and this issue actually isn't due to populateModelSymbol. It is due to the fact that we use the query model by default for populate().

Long story short, we'll have a fix for this issue in 6.3.1. However, it is unnecessary to populate() nested paths unless there's additional data you want to load from the database. Just do await bar1.populate('__history.document.foo');.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
confirmed-bug We've confirmed this is a bug in Mongoose and will fix it.
Projects
None yet
Development

No branches or pull requests

3 participants