Reputation: 696
I cannot find a proper way to provide validation for email uniqueness in mongoose. Nothing I have found actually works. I was thinking to use Schema.pre, but how would I go about writing the code for that if that is the case? The Mongoose documentation is very poor and does not describe how or what pre does.
I would appreciate it if somebody could tell me how this is normally done or point me in the right direction. I don't understand why something so simple has no simple solution in mongoose...
Upvotes: 5
Views: 10530
Reputation: 700
Just amending the answer from SuleymanSah, use the try/catch instead of returning 400 response code in the try.
router.post("/register", async (req, res) => {
try {
const { email, password } = req.body;
let user = await User.findOne({ email });
if (user) {
throw new Error("User already registered!");
}
user = new User({ email, password });
user.password = await bcrypt.hash(user.password, 10);
await user.save();
res.send("registered");
} catch (err) {
res.status(400).json({ message: err.message });
}
});
Upvotes: 0
Reputation: 1
const mongoose = require("mongoose");
const validator = require("validator");
// creating schema for collection oof user.......
const userSchema = new mongoose.Schema({
name:{
type:String,
required:true,
minlength:3
},
phone:{
type:Number,
required:true,
minlength:10,
maxlength:10,
unique: true,
},
email:{
type:String,
required:true,
validate: validateEmail,
}
});
// creating document
const User = new mongoose.model("User",userSchema);
async function validateEmail(email) {
if (!validator.isEmail(email)) throw new Error("Please enter a valid email address.")
const user = await User.findOne({ email })
if (user) throw new Error("A user is already registered with this email address.")
}
Upvotes: 0
Reputation: 4884
All the above responses work, but lack optimization:
import type {Types} from 'mongoose';
const emailRegExp = new RegExp(/^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/);
export const isEmail = {
validator: (email: string): boolean => emailRegExp.test(email),
message: 'INVALID_EMAIL',
}
export const emailIsUnique = {
async validator(email: string): Promise<boolean> {
const model = this.constructor as Model<any>;
const id = this._id as Types.ObjectId;
const user = await model.exists({email}).exec();
return user === null || this._id.equals(user._id);
},
message: 'ALREADY_USED_EMAIL',
};
const userSchema = new Schema<User, Model<User>, User>({
email: {
type: String,
unique: true,
lowercase: true,
validate: [isEmail, emailIsUnique],
}
});
const userModel = model('User', userSchema);
From top to bottom:
TypeScript
version so just remove type annotations for pure JS.RegExp.prototype.test()
for performance concern and because the return value is already a boolean.exists()
to just retrieve the useful bits I need: the _id
. see docunique: true
provide a useful unique index on email key for a faster search when using findOne()
. It will also throw if the email we try to add to the index is duplicated, but be careful it's not real a validator. see doclowercase: true
is also important to avoid case comparison troubles (false negative)Hope this little snippet will help :)
Upvotes: 0
Reputation: 1978
For running multiple validation checks on your email field, you will need to throw Error objects with different messages for each validation.
In this example, I specify 3 different validation checks, including uniqueness, with 3 different custom error messages, but you can add as many checks as you need.
const UserSchema = new Schema({
email: {
type: String,
required: [true, "An email address is required."],
validate: validateEmail,
},
})
async function validateEmail(email) {
if (!isEmail(email)) throw new Error("Please enter a valid email address.")
const user = await this.constructor.findOne({ email })
if (user) throw new Error("A user is already registered with this email address.")
}
First, the required
check runs, and if empty, returns the first validation message.
Second, pass a function to the validate
field. In this function, the email
is passed as a parameter. isEmail
can be any type of function that checks that a string is the proper email format. If it fails, specify the error message with a new Error object
To validate the uniqueness with mongoose, run the findOne
query from the answer above and if a user is returned, then throw error with that validation text.
Notes
It is not necessary to return true
or false
from these functions. Simply throw an error IF the validation fails and let the function resolve naturally.
It is important that you do not try to pass an arrow function to the validate
property, as you need the lexically scoped this
available in the function execution.
The answer above is correct, however doesn't solve for the issue of needing to run additional checks besides required and unique, and send custom error messages for each.
Upvotes: 0
Reputation: 12984
You could use a custom validator:
var userSchema = new Schema({
email: {
type: String,
validate: {
validator: async function(email) {
const user = await this.constructor.findOne({ email });
if(user) {
if(this.id === user.id) {
return true;
}
return false;
}
return true;
},
message: props => 'The specified email address is already in use.'
},
required: [true, 'User email required']
}
// ...
});
Upvotes: 10
Reputation: 17868
I prefer to check email uniqueness in the register route.
This way we can fully control which status code or error message should be sent to the client.
router.post("/register", async (req, res) => {
try {
const { email, password } = req.body;
let user = await User.findOne({ email });
if (user) return res.status(400).send("User already registered.");
user = new User({ email, password });
user.password = await bcrypt.hash(user.password, 10);
await user.save();
res.send("registered");
} catch (err) {
console.log(err);
res.status(500).send("Something went wrong");
}
});
Upvotes: 11