39

How to do validations before saving the edited data in mongoose?

For example, if sample.name already exists in the database, the user will receive a some sort of error, something like that, here's my code below

//Post: /sample/edit
app.post(uri + '/edit', function (req, res, next) {
  Sample.findById(req.param('sid'), function (err, sample) {

    if (err) {
      return next(new Error(err));
    }

    if (!sample) {
      return next(new Error('Invalid reference to sample information'));
    }

    // basic info
    sample.name = req.body.supplier.name;
    sample.tin = req.body.supplier.tin;

    // contact info
    sample.contact.email = req.body.supplier.contact.email;
    sample.contact.mobile = req.body.supplier.contact.mobile;
    sample.contact.landline = req.body.supplier.contact.landline;
    sample.contact.fax = req.body.supplier.contact.fax;

    // address info
    sample.address.street = req.body.supplier.address.street;
    sample.address.city = req.body.supplier.address.city;
    sample.address.state = req.body.supplier.address.state;
    sample.address.country = req.body.supplier.address.country;
    sample.address.zip = req.body.supplier.address.zip;

    sample.save(function (err) {
      if (err) {
        return next(new Error(err));
      }

      res.redirect(uri + '/view/' + sample._id);
    });

  });
});
ROMANIA_engineer
  • 54,432
  • 29
  • 203
  • 199
Miguel Lorenzo
  • 560
  • 1
  • 5
  • 12

8 Answers8

70

Typically you could use mongoose validation but since you need an async result (db query for existing names) and validators don't support promises (from what I can tell), you will need to create your own function and pass a callback. Here is an example:

var mongoose = require('mongoose'),
    Schema = mongoose.Schema,
    ObjectId = Schema.ObjectId;

mongoose.connect('mongodb://localhost/testDB');

var UserSchema = new Schema({
    name: {type:String}
});

var UserModel = mongoose.model('UserModel',UserSchema);

function updateUser(user,cb){
    UserModel.find({name : user.name}, function (err, docs) {
        if (docs.length){
            cb('Name exists already',null);
        }else{
            user.save(function(err){
                cb(err,user);
            });
        }
    });
}

UserModel.findById(req.param('sid'),function(err,existingUser){
   if (!err && existingUser){
       existingUser.name = 'Kevin';
       updateUser(existingUser,function(err2,user){
           if (err2 || !user){
               console.log('error updated user: ',err2);
           }else{
               console.log('user updated: ',user);
           }

       });
   } 
});

UPDATE: A better way

The pre hook seems to be a more natural place to stop the save:

UserSchema.pre('save', function (next) {
    var self = this;
    UserModel.find({name : self.name}, function (err, docs) {
        if (!docs.length){
            next();
        }else{                
            console.log('user exists: ',self.name);
            next(new Error("User exists!"));
        }
    });
}) ;

UPDATE 2: Async custom validators

It looks like mongoose supports async custom validators now so that would probably be the natural solution:

    var userSchema = new Schema({
      name: {
        type: String,
        validate: {
          validator: function(v, cb) {
            User.find({name: v}, function(err,docs){
               cb(docs.length == 0);
            });
          },
          message: 'User already exists!'
        }
      }
    });
mr.freeze
  • 13,731
  • 5
  • 36
  • 42
  • 4
    You're not calling "next()" in if the user exists, I think you should call next() with an error – Amir T Nov 22 '13 at 18:39
  • 13
    why not just using the `unique` param on the name field? – udidu Oct 22 '14 at 17:02
  • how will user be having save() method on it? – A.B Dec 31 '14 at 16:42
  • 1
    I get this error when I put a `find` in the `pre` middleware: `Uncaught TypeError: Object # has no method 'find'` – Jeremy Jun 16 '15 at 05:18
  • 3
    Using a unique index is the best practice here. Otherwise you have to query the collection before insert, whereas if you use a unique index mongodb will just reject the insert, saving round trip time, and time spent querying a collection. A unique index would be the very best solution here. – tsturzl Aug 05 '16 at 18:25
  • What if 2 people register the same email at the same time? – tu4n Nov 03 '16 at 14:39
  • You'd probably need to create a global flag to indicate that there is a pending async validation in progress. – mr.freeze Nov 03 '16 at 15:16
  • In "Async custom validators", how can I ignore it when updating? – Sergio Rodrigues Mar 01 '17 at 20:56
  • @tsturzi: http://mongoosejs.com/docs/validation.html#the-unique-option-is-not-a-validator – Agusti-N Jun 23 '18 at 16:47
  • check out https://mongoosejs.com/docs/api.html#document_Document-isNew for the pre "save" method, this.isNew tells you directly if the model you are trying to save is a new document or not (no need to query the db yourself) – Yuri Scarbaci Aug 21 '18 at 10:37
  • In the Update 2... Inside Validate > Validator you uses `User` . Where is this user defined? – JuMoGar May 10 '19 at 17:00
  • @udidu You can't customize the error message! So doing `unique: [true, 'This email already registered!']` won't work. – cyonder Apr 19 '20 at 15:37
  • Please be noticed that using `unique` key does NOT prevent duplicate insert as mongoose docs itself and this question https://stackoverflow.com/questions/21971666/mongoose-unique-field – Pouria Moosavi Oct 05 '20 at 07:00
11

Another way to continue with the example @nfreeze used is this validation method:

UserModel.schema.path('name').validate(function (value, res) {
    UserModel.findOne({name: value}, 'id', function(err, user) {
        if (err) return res(err);
        if (user) return res(false);
        res(true);
    });
}, 'already exists');
John Linhart
  • 1,746
  • 19
  • 23
  • 1
    What is path? could you elaborate on this – Andrew Ravus Nov 15 '16 at 14:53
  • This worked for me and does seem like the most elegant way, but it throws a deprecation error, i.e. DeprecationWarning: Implicit async custom validators (custom validators that take 2 arguments) are deprecated in mongoose >= 4.9.0. – Marko Jul 02 '17 at 02:29
  • 1
    http://mongoosejs.com/docs/validation.html#async-custom-validators Even if you don't want to use asynchronous validators, be careful, because mongoose 4 will assume that all functions that take 2 arguments are asynchronous, like the validator.isEmail function. This behavior is considered deprecated as of 4.9.0, and you can shut it off by specifying isAsync: false on your custom validator. – Marko Jul 02 '17 at 02:37
3

In addition to already posted examples, here is another approach using express-async-wrap and asynchronous functions (ES2017).

Router

router.put('/:id/settings/profile', wrap(async function (request, response, next) {
    const username = request.body.username
    const email = request.body.email
    const userWithEmail = await userService.findUserByEmail(email)
    if (userWithEmail) {
        return response.status(409).send({message: 'Email is already taken.'})
    }
    const userWithUsername = await userService.findUserByUsername(username)
    if (userWithUsername) {
        return response.status(409).send({message: 'Username is already taken.'})
    }
    const user = await userService.updateProfileSettings(userId, username, email)
    return response.status(200).json({user: user})
}))

UserService

async function updateProfileSettings (userId, username, email) {
    try {
        return User.findOneAndUpdate({'_id': userId}, {
            $set: {
                'username': username,
                'auth.email': email
            }
        }, {new: true})
    } catch (error) {
        throw new Error(`Unable to update user with id "${userId}".`)
    }
}

async function findUserByEmail (email) {
    try {
        return User.findOne({'auth.email': email.toLowerCase()})
    } catch (error) {
        throw new Error(`Unable to connect to the database.`)
    }
}

async function findUserByUsername (username) {
    try {
        return User.findOne({'username': username})
    } catch (error) {
        throw new Error(`Unable to connect to the database.`)
    }
}

// other methods

export default {
    updateProfileSettings,
    findUserByEmail,
    findUserByUsername,
}

Resources

async function

await

express-async-wrap

3

For anybody falling on this old solution. There is a better way from the mongoose docs.

var s = new Schema({ name: { type: String, unique: true }});
s.path('name').index({ unique: true });
WebSam101
  • 101
  • 2
  • 5
  • `(node:18024) DeprecationWarning: collection.ensureIndex is deprecated. Use createIndexes instead.` – zipzit Oct 18 '19 at 05:43
  • oops. Use `mongoose.connect(DATABASE_URL, { useNewUrlParser: true, useUnifiedTopology: true, useCreateIndex: true }, err => { ...` to preclude all warnings... – zipzit Oct 18 '19 at 05:54
2

Here is another way to accomplish this in less code.

UPDATE 3: Asynchronous model class statics

Similar to option 2, this allows you to create a function directly linked to the schema, but called from the same file using the model.

model.js

 userSchema.statics.updateUser = function(user, cb) {
  UserModel.find({name : user.name}).exec(function(err, docs) {
    if (docs.length){
      cb('Name exists already', null);
    } else {
      user.save(function(err) {
        cb(err,user);
      }
    }
  });
}

Call from file

var User = require('./path/to/model');

User.updateUser(user.name, function(err, user) {
  if(err) {
    var error = new Error('Already exists!');
    error.status = 401;
    return next(error);
  }
});
DBrown
  • 5,111
  • 2
  • 23
  • 24
2

check with one query if email or phoneNumber already exists in DB

let userDB = await UserS.findOne({ $or: [
  { email: payload.email },
  { phoneNumber: payload.phoneNumber }
] })

if (userDB) {
  if (payload.email == userDB.email) {
    throw new BadRequest({ message: 'E-mail already exists' })
  } else if (payload.phoneNumber == userDB.phoneNumber) {
    throw new BadRequest({ message: 'phoneNumber already exists' })
  }
}
Lucas Breitembach
  • 1,515
  • 11
  • 15
1

If you're searching by an unique index, then using UserModel.count may actually be better for you than UserModel.findOne due to it returning the whole document (ie doing a read) instead of returning just an int.

Dale Annin
  • 11
  • 2
1

There is a more simpler way using the mongoose exists function

router.post("/groups/members", async (ctx) => {
    const group_name = ctx.request.body.group_membership.group_name;
    const member_name = ctx.request.body.group_membership.group_members;
    const GroupMembership = GroupModels.GroupsMembers;
    console.log("group_name : ", group_name, "member : ", member_name);
    try {
        if (
            (await GroupMembership.exists({
                "group_membership.group_name": group_name,
            })) === false
        ) {
            console.log("new function");
            const newGroupMembership = await GroupMembership.insertMany({
                group_membership: [
                    { group_name: group_name, group_members: [member_name] },
                ],
            });
            await newGroupMembership.save();
        } else {
            const UpdateGroupMembership = await GroupMembership.updateOne(
                { "group_membership.group_name": group_name },
                { $push: { "group_membership.$.group_members": member_name } },
            );
            console.log("update function");
            await UpdateGroupMembership.save();
        }
        ctx.response.status = 201;
        ctx.response.message = "A member added to group successfully";
    } catch (error) {
        ctx.body = {
            message: "Some validations failed for Group Member Creation",
            error: error.message,
        };
        console.log(error);
        ctx.throw(400, error);
    }
});
Mega Alpha
  • 61
  • 1
  • 3