Handling Mongoose Dublication Errors

ahmedmagdy11

Ahmed Magdy

Posted on February 8, 2021

Handling Mongoose Dublication Errors

Introduction

If you ever wrote Node.js code and decided to have a document based DB, your main goto will be MongoDB ofc and you will be using mongoose as your ODM then you have met this error before.

MongoError: E11000 duplicate key error collection: testDB.users index: name_1 dup key: { : "some random name" }.

the problem is there are multiple ways to handle it. one of them is using a library called mongoose-unique-validator. but we are not going to use an external library which I don't know how it works under the hood.

Before we keep going

there is some stuff that needs to be clarified
1- name { type :string , unqiue: true} unique param in mongoose is not a validator meaning doing const myUser = new User(data) will not throw an error in case of duplication.
it will only throw and error when doing myUser.save()

2- when trying to add a user I suggest using either insertOne or create function. and keep using it through your whole application because We are about to overwrite one of them.

Note: I will be using create here.

Why?

why we wanna handle duplication error globally anyway?
because you might have 10 or 20 collections where each one has 2 or 3 unique keys and you are not going to check for every one manually.

Implmentation

you can easily overwrite mongoose function by doing

const mongoose = require("mongoose");
// old create function 
const create = mongoose.Model.create;
// overwriting
// it takes this arguments according to mongoose documentation
mongoose.Model.create = async function (doc, options, callback){
     // your logic here;
     // return original function 
    return create.apply(this, arguments);
}
Enter fullscreen mode Exit fullscreen mode

my logic here is when I am using create function I will insert a new option which is some keys to check if they are duplicated or no.

const data = { name : "ahmed"} 

User.create(data, {checkForDublication : ["name"] }).then(console.log).catch(err=> console.error(err)); 
Enter fullscreen mode Exit fullscreen mode

I am going for this format where checkForDublication is a new option I created and will be sending the keys as array format.

Logic

  • check if options has a checkForDublication param.

  • check if its values exist in the schema and are unique.

the last step (checking if the key is unique) is very important, Because we are going to use findOne({$or: searchQuery}) ... and as you know searchQuery is going to be an array, If one Element in this array is not unique or index it's going to perform collectionScan instead of indexScan which is very slow.

  • filter the checkForDublication array meaning remove every key that doesn't exist in the schema or is not unique.

  • generating the search query

  • checking if the result of the search query exist.

Code

mongoose.Model.create = async function (doc, options, callback){
    if (options && options.checkKeysForDublication){
        const searchQuery = getSearchQuery(doc,this.schema.obj, options.checkKeysForDublication);
        await checkForDublication(this, searchQuery);
    }


    return create.apply(this, arguments);
}

Enter fullscreen mode Exit fullscreen mode

getSearchQuery function

function getSearchQuery(doc,schema, keys){
    if (!Array.isArray(keys)||keys.length === 0){
        return;
    }
    const filteredKeys = filterKeys(doc,schema,keys);
    return  makeSearchQuery(doc,filteredKeys);

};
function filterKeys (doc,schema,keys){
    const filteredKeys = keys.filter(key=>{
        if (!schema[key] || !schema[key].unique || !doc[key]){
            console.warn(`${key} key either doesn't exist in this schema or not unique so it will filtered`);
        }
        return schema[key] && schema[key].unique && doc[key];
    });
    return filteredKeys;
}
function makeSearchQuery(doc,keys){
    const query = [];
    keys.forEach(key=>{
        const obj = {};
        obj[key] = doc[key];
        query.push(obj);
    });
    return query;
}
Enter fullscreen mode Exit fullscreen mode

output of getSearchQuery

[{"name" : "ahmed"} // and every key that was common between insterted document and checkForDublication arr].

another example

User.create({name: "ahmed ,
 email :"anymail@gmail.com" , password : "123" }, {checkForDublication : ["name" , "email"] }.then(console.log);
Enter fullscreen mode Exit fullscreen mode

output of getSearchQuery

[{ "name" : "ahmed" , {"email": "anymain@gmail.com"}]

checkForDublication function

async function checkForDublication (Model, searchQuery){
    const result = await Model.findOne({$or: searchQuery});
    if (!result){
        return;
    }
    searchQuery.forEach(singleObject=>{
        //every singleObject has only one keyl
        const key = Object.keys(singleObject)[0];
        if (result[key] === singleObject[key]){
            throw new Error(`${key} already exists`);
        }
    });

}
Enter fullscreen mode Exit fullscreen mode

output Error: name already exists

important note: don't forget to put this line of code require("./fileThatHasOverWrittenCreateFunction.js") at the very start of your project so changes can take effect.

NOTE: you can throw your custom error as well... but this one is for another article.

Finally

The main goal in this article was to make a global way to handle duplication errors.
if You have any feedback feel free to send me on this email ahmedmagdy@creteagency.com.

Enjoy~

💖 💪 🙅 🚩
ahmedmagdy11
Ahmed Magdy

Posted on February 8, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Handling Mongoose Dublication Errors
mongodb Handling Mongoose Dublication Errors

February 8, 2021