Lee Morgan
Lee Morgan

Reputation: 696

What is the proper way to validate email uniqueness with mongoose?

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

Answers (6)

codeguy
codeguy

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

Dev Jariwala
Dev Jariwala

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

Freezystem
Freezystem

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:

  • It's a TypeScript version so just remove type annotations for pure JS.
  • I've included a simple regex pattern for the email but you can use the pattern you want.
  • I'm using two separated validators for readability and to be able to use one without the other if I need to.
  • I'm using RegExp.prototype.test() for performance concern and because the return value is already a boolean.
  • I'm using exists() to just retrieve the useful bits I need: the _id. see doc
  • If a user is found the later condition make sure that it's not an update to avoid false positive.
  • The schema definition is also important: unique: 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 doc
  • lowercase: true is also important to avoid case comparison troubles (false negative)

Hope this little snippet will help :)

Upvotes: 0

PsiKai
PsiKai

Reputation: 1978

Multiple Custom Validations

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

Fraction
Fraction

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

SuleymanSah
SuleymanSah

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

Related Questions