Automattic / mongoose

MongoDB object modeling designed to work in an asynchronous environment.
https://mongoosejs.com
MIT License
26.95k stars 3.84k forks source link

Default functions and incorrect "this" document #12328

Closed raphael-papazikas closed 2 years ago

raphael-papazikas commented 2 years ago

Prerequisites

Mongoose version

6.5.3

Node.js version

16.13.1

MongoDB server version

5.0.5

Description

If i have a Subdocument schema which has a default function that depends on an other property of this subdocument, the this context is not the correct Subdocument when querying from database. But when i use new testModel({...}) with the data that i expect in my database, the value is defaulted correctly (test1 compared to test3).

Steps to Reproduce

const mongoose = require("mongoose");
const connection = mongoose.createConnection("mongodb://localhost:27017/test");

const subSchema = new mongoose.Schema({
    propertyA: {type: String},
    propertyB: {type: String, default: function() {
        console.log("this", this);
        if (this.propertyA === "A") {
            return "B"
        }

        return "C"
    }}
})

const userSchema = new mongoose.Schema(
  {
    name: String,
    sub: {type: subSchema, default: () => ({})}
  },
);

const testModel = connection.model("User", userSchema);

async function populateCollection() {
    await new testModel({name: `test-0`}).save()

    const withPropertyA = await new testModel({name: `test-1`, sub: {propertyA: "A"}}).save()
    // remove sub.propertyB so that it should be defaulted on fetch time
    await testModel.findOneAndUpdate({_id: withPropertyA._id}, {$unset: {"sub.propertyB": ""}})

    await new testModel({name: `test-2`, sub: {propertyA: "B"}}).save()
}

async function cleanInsertTest() {
  await testModel.deleteMany({});

  await populateCollection();
}

async function findTest() {
  const test0 = await testModel.findOne({name: `test-0`});
  const test1 = await testModel.findOne({name: `test-1`});
  const test2 = await testModel.findOne({name: `test-2`});
  console.log("test0", test0.sub);
  console.log("test1", test1.sub);
  console.log("test2", test2.sub);

  const test3 = new testModel({sub: {...test1.sub.toObject(), propertyB: undefined}});
  console.log("test3", test3.sub)
  console.log("test1 vs test3", test1.sub.propertyB === test3.sub.propertyB)
}

async function run() {
  await cleanInsertTest();
  await findTest();
  connection.close();
}

run();

Expected Behavior

No response

IslandRhythms commented 2 years ago
const mongoose = require("mongoose");

const subSchema = new mongoose.Schema({
    propertyA: {type: String},
    propertyB: {type: String, default: function() {
        console.log("this", this);
        if (this.propertyA === "A") {
            return "B"
        }

        return "C"
    }}
})

const userSchema = new mongoose.Schema(
  {
    name: String,
    sub: {type: subSchema, default: () => ({})}
  },
);

const testModel = mongoose.model("User", userSchema);

async function populateCollection() {
    await new testModel({name: `test-0`}).save()

    const withPropertyA = await new testModel({name: `test-1`, sub: {propertyA: "A"}}).save()
    // remove sub.propertyB so that it should be defaulted on fetch time
    await testModel.findOneAndUpdate({_id: withPropertyA._id}, {$unset: {"sub.propertyB": ""}})

    await new testModel({name: `test-2`, sub: {propertyA: "B"}}).save()
}

async function cleanInsertTest() {
  await testModel.deleteMany({});

  await populateCollection();
}

async function findTest() {
  const test0 = await testModel.findOne({name: `test-0`});
  const test1 = await testModel.findOne({name: `test-1`});
  const test2 = await testModel.findOne({name: `test-2`});
  console.log("test0", test0.sub);
  console.log("test1", test1.sub);
  console.log("test2", test2.sub);

  const test3 = new testModel({sub: {...test1.sub.toObject(), propertyB: undefined}});
  console.log('==================')
  console.log("test3", test3.sub)
  console.log("test1 vs test3", test1.sub.propertyB === test3.sub.propertyB)
}

async function run() {
  await mongoose.connect('mongodb://localhost:27017/test')
  await mongoose.connection.dropDatabase();
  await cleanInsertTest();
  await findTest();
  await mongoose.connection.close();
}

run();
felipezarco commented 1 year ago

Is the behaviour here expected to be the same with arrow functions? Will both keep this as the saving doc?

vkarpov15 commented 1 year ago

@felipezarco no. Arrow functions use lexical this, so this in an arrow function is the same as this outside the function.