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

DocumentArray pull does not work when subdocument has a defaulted field #12294

Closed
2 tasks done
ajwootto opened this issue Aug 18, 2022 · 2 comments
Closed
2 tasks done
Labels
confirmed-bug We've confirmed this is a bug in Mongoose and will fix it.
Milestone

Comments

@ajwootto
Copy link

ajwootto commented Aug 18, 2022

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the bug has not already been reported

Mongoose version

6.5.2

Node.js version

16.x

MongoDB server version

5.x

Description

If you setup a schema which has an array of subdocuments, and the subdocument schema includes a field which has a default configured, then:

  1. the subdocument data in mongo has no value for that field
  2. you retrieve the document with mongoose, allowing the field to be defaulted on retrieval
  3. you attempt to pull the subdocument from the array by passing that subdocument to the pull method
  4. mongoose will correctly remove the subdocument from the document instance's array field, but on save the document will not be correctly updated and the subdocument will still be there.

I believe this is because the pull operation tries to perform an exact match on the contents of the subdocument when submitting the $pull operation to MongoDB. It seems like it should take whether or not the field was defaulted into account when submitting the operation.

Steps to Reproduce

const mongoose = require('mongoose')
const { Schema, model, connect } = mongoose
const assert = require('assert')

const nestedChildSchema = new Schema({
    name: String,
    defaultedField: { type: String, default: 'default' }
})

const ParentModel = model('Parent', new Schema({
    nestedChild: [nestedChildSchema],
    name: String
}))

async function run() {
    console.log('Running test')
    await connect('mongodb://localhost:27017/')
    const dbConnection = mongoose.connections[0]
    const { insertedId } = await dbConnection.collection('parents').insertOne({
        // inserting raw data that omits the defaulted field
        // this simulates a situation where this document already existed in the DB 
        // and the defaulted field was added to the schema later
        nestedChild: [{ name: 'foo' }],
        name: 'Parent'
    })

    const parent = await ParentModel.findOne({ _id: insertedId })
    const child = parent.nestedChild[0]
    parent.nestedChild.pull(child)
    assert(parent.nestedChild.length === 0)

    await parent.save()

    const updatedParent = await ParentModel.findOne({ _id: insertedId })

    // this throws because the child was not removed
    assert(updatedParent.nestedChild.length === 0)
}

run()

Expected Behavior

I'd expect that regardless of if a field was defaulted or not on retrieval from the DB, the pull operation should still work on that subdocument.

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

IslandRhythms commented Aug 24, 2022

You can use the commented out section in the meantime to do your pull operation.

const mongoose = require('mongoose')
const { Schema, model, connect } = mongoose
const assert = require('assert')

const nestedChildSchema = new Schema({
    name: String,
    defaultedField: { type: String, default: 'default' }
})

const ParentModel = model('Parent', new Schema({
    nestedChild: [nestedChildSchema],
    name: String
}))

async function run() {
    console.log('Running test')
    await connect('mongodb://localhost:27017/')
    const dbConnection = mongoose.connections[0]
    const { insertedId } = await dbConnection.collection('parents').insertOne({
        // inserting raw data that omits the defaulted field
        // this simulates a situation where this document already existed in the DB 
        // and the defaulted field was added to the schema later
        nestedChild: [{ name: 'foo' }],
        name: 'Parent'
    })

    const parent = await ParentModel.findOne({ _id: insertedId })
    const child = parent.nestedChild[0];
    // const test = await ParentModel.findOneAndUpdate({ _id: insertedId }, {$pull: {nestedChild: {name: 'foo'}}}, {returnDocument: 'after'});
    // console.log('test', test);
    parent.nestedChild.pull(child);
    assert(parent.nestedChild.length === 0)

    await parent.save()

    const updatedParent = await ParentModel.findOne({ _id: insertedId })
    console.log(updatedParent)
    // this throws because the child was not removed
    assert(updatedParent.nestedChild.length === 0)
}

run()

@ajwootto
Copy link
Author

ajwootto commented Aug 24, 2022

Thanks @IslandRhythms

I also imagine this might be an issue for any other operation mongoose does that affects what data is on a document. Not sure if virtuals would make the same thing happen, or the data type casting that occurs when the document is loaded.

@vkarpov15 vkarpov15 added this to the 6.5.6 milestone Sep 8, 2022
vkarpov15 added a commit that referenced this issue Sep 12, 2022
Improve handling of defaults when using `pull()`
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