简体   繁体   中英

Mongoose: attempting to remove last element from an array results in seemingly bizarre behavior

Update: I switched from updateOne and $pull to simply filtering the array and saving it. I don't know why but that solves the first issue of html elements being removed. The same error occurs when deleting the last Item in a Menu however.

I have the following Express router to remove an Item element from an array in a Menu:

router.put('/menus/:menuid/items/:itemid', async (req, res, next) => {
    console.log('attempting to delete menu item...')
    const menu = await Menu.findById(req.params.menuid)
    const item = menu.items.find(item => item.id === req.params.itemid)
    console.log(item)
    console.log('updating...')
    const response = await Menu.updateOne(
        { _id: menu._id },
        { $pull: { items: { _id: item._id } } }
    )
    console.log(response)
    req.menu = await Menu.findById(req.params.menuid) 
})

This successfully removes the desired array element, however the function that called fetch() on this request doesn't proceed with then(); the elements on the page don't change until I refresh the browser:

function redirectThenDeleteElement(path, id, method) {        
        console.log("redirect to " + path + " using " + method)
        fetch(path, { method: method })
            .then(response => {
                if (response.ok) {
                    console.log('fetch successful')
                    const itemToDelete = document.getElementById(id)
                    itemToDelete.remove()
                } else {
                    console.log('error with request')
                }
            }).catch(error => {
                console.error('error with fetch call: \n' + error)
            })
           
}

From here it gets weirder. If i delete the last item from a Menu, it behaves as expected. Same if I add a new item then delete it again. But if I delete all the items from one Menu then delete the last item from another, "updating..." is logged and I get the error: MongoServerError: E11000 duplicate key error collection: pfa-site.menus index: items.title_1 dup key: { items.title: null } This also happens if I seed one Menu with an empty items array and then delete the last item from another Menu. It refers to items.title_dup/items.title, and I don't have a coherent idea why it would, and I don't know what an index means in this context.

My first thought was that if items.title was meant to be the title property of Item, which is unique, the error makes sense if multiple menus are for some reason trying to update their items.title property to null. But the result is the same if I remove the unique parameter from the schema:

const menuItemSchema = new mongoose.Schema({
    title: {
        type: String,
        required: true,
        unique: false
    },
    content: String,
    sanitizedHtml: {
        type: String,
        required: true
    },
    slug: {
        type: String,
        required: true,
        unique: true
    }
})
const menuSchema = new mongoose.Schema({
    title: {
        type: String,
        required: true
    },
    order: {
        type: Number,
        required: true
    },
    items: {
        type: [menuItemSchema]
    },
    slug: {
        type: String,
        required: true,
        unique: true
    }
})

menuSchema.pre('validate', function(next) {
    if (this.title) {
        this.slug = slugify(this.title, { lower: true, strict: true })
    }

    this.items.forEach((item) => {
        console.log('content being sanitized...')
        if (item.content) {
            item.sanitizedHtml = dompurify.sanitize(marked.parse(item.content))            
        }
        if (item.title) {
            item.slug = slugify(item.title, { lower: true, strict: true })
        }
    })
    next()
})
module.exports = mongoose.model('Menu', menuSchema)

Maybe the weirdest part is that if I add a callback function to updateOne that simply logs any error/result, and then attempt to delete any item under any condition, i get the error: MongooseError: Query was already executed: Menu.updateOne({ _id: new ObjectId("63b3737f6d748ace63beef8a... at model.Query._wrappedThunk [as _updateOne]

Here is the code with the callback added:

router.patch('/menus/:menuid/items/:itemid', async (req, res, next) => {
    console.log('attempting to delete menu item...')
    const menu = await Menu.findById(req.params.menuid)
    const item = menu.items.find(item => item.id === req.params.itemid)
    console.log(item)
    console.log('updating...')
    const response = await Menu.updateOne(
        { _id: menu._id },
        { $pull: { items: { _id: item._id } } },
        function(error, result) {
            if (error) {
                console.log(error)
            } else {
                console.log(result)
            }
        }
    )
    console.log(response)
    req.menu = await Menu.findById(req.params.menuid)       
})

Thank you for taking the time to read all of this. You're a real one and I hope you have any idea what's happening!

I've tried using findOneAndUpdate instead of updateOne and using different http request methods, among other things that failed and I forgot about. Results are similar or broken for I think unrelated reasons.

I think this is related to how you handle your requests. As a better and cleaner code, I would suggest making the "put" method to be "delete" so the api will explicitly be meant for deleting an item from a menu. Also, I don't know where the result is being sent back to the frontend. I would suggest something like that:

router.delete('/menus/:menuid/items/:itemid', async (req, res, next) => {
    console.log('attempting to delete menu item...')
    const {menuid, itemid} = req.params;
    const menu = await Menu.findById(menuid)
    const item = menu.items.find(item => item.id === itemid)
    if(!item){
        throw new Error('item not found')
    }
    console.log(item)
    console.log('updating...')
    const response = await Menu.updateOne(
        { _id: menu._id },
        { $pull: { items: { _id: itemid } } },
        { new: true}
    )
    console.log(response)
    return res.status(200).json(response);
})

Solution: it turns out that since the Item schema had unique fields, MongoDB automatically added indices to the parent schema where the index name is eg title_1 and the key:value, in the case of an empty items array, is 'items.title': null. So when the last item is deleted from a second menu some conflict happens where there are duplicate indices? I'm not sure, so any clarification on that would be helpful.

Anyway, the solution was to add sparse: true in addition to unique: true. This directly prevents an index from having a null value. Again, further clarification on exactly how this works would be great. See BahaEddine Ayadi's comment for further details.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM