Ahmed Magdy
Posted on February 8, 2021
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);
}
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 acheckForDublication
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.
Enjoy~
Posted on February 8, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.