Handling Mongoose Dublication Errors
Tags: mongodb, node, mongoose, backend
Published: 2021-02-08 16:19:20

##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](https://www.npmjs.com/package/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);
}
 ``` 

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)); 
``` 

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);
}

```

####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;
}
```

####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);
``` 

####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`);
        }
    });

}
```

###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](ahmedmagdy@creteagency.com). 

Enjoy~