mongoose-unique-validator detects all inputs as already existed - node.js

I am trying to create a unique email value in MongoDB. I used mongoose-unique-validator to implement that but resulted in error claiming that my unique emails that I just inputted are already existed.
This is the error message I received from trying to input unique email.
"message": "User validation failed: email: Error, expected email to be
unique., _id: Error, expected _id to be unique."
They said the Email and _id are not unique. But.., _id is an auto-generated value to be unique in MongoDB meaning that it should not be detected as a duplicated value.
I am not sure if that was caused by my own implementation so I would look forward to see any assumption or ways to debug to the root cause of this too. I tried restarting from fresh Mongo DB and manually inputting uniquely via Postman but still no hope.
These are a part of the codes that might be related to the data creation on MongoDB
UserModel.js
var uniqueValidator = require('mongoose-unique-validator');
const { Schema } = mongoose;
const UsersSchema = new Schema({
email: { type: String, unique: true, required: true},
hashedPassword: String,
salt: String,
});
UsersSchema.plugin(uniqueValidator, { message: 'Error, expected {PATH} to be unique.' });
UsersSchema.set('autoIndex', false);
Server.js ---- /api/users/register
const finalUser = new Users(user);
finalUser.setPassword(user.password);
finalUser.save((err, data) => {
if (err) {
return res.status(400).json(err)
}
return res.json({ user: finalUser.toAuthJSON() })
})
Additional Information
I tried this solution from Sritam to detect another email with the same value but it still claims that the inputted email is already existed.
UsersSchema.path('email').validate(async (value) => {
const emailCount = await mongoose.models.User.countDocuments({email: value });
return !emailCount;
}, 'Email already exists');
"message": "User validation failed: email: Email already exists"

You can use validateModifiedOnly option in Document.save(). _id and email fields will never be validated unless they are modified. The code should look like this:
finalUser.save({ validateModifiedOnly: true }, (err, data) => {
if (err) {
return res.status(400).json(err)
}
return res.json({ user: finalUser.toAuthJSON() })
})

Found the issue and solution!
They are acting weird like in the question because the model was not initialized.
I must perform Schema.init() before performing any model validation.
The solution is to add UsersSchema.init().then(() => {...your operation})
Now Server.js ---- /api/users/register should look like this.
Users.init().then(() => { // where Users is my UsersModel
finalUser.save((err, data) => {
if (err) {
return res.status(400).json(err)
}
return res.json({ user: finalUser.toAuthJSON() })
})
})
Hope this helps other developers who experience similarly odd error!

Related

Mongoose model methods - create document if unique

I'm new to MongoDB and Mongoose. I'm using it with my Node project (with Express), and I'm trying to keep everything organized and separated. For example, I'm trying to keep all the database queries in each model file. This way all other files could simply use User.createNew({ fields }) and a new user will be created.
I need each user to be unique (based on their usernames), and I'm not sure exactly where to keep this functionality. I set unique: true in the Schema but upon reading Mongoose's documentation, they stated that unique is not real validation (or something about how validation should happen beforehand). So my main problem is how to create a static method to create a new user, and also validate this user doesn't exist beforehand. I could implement all of this in one static method:
userSchema.statics.createUser = function (username, ..., cb) {
this.findOne({ username }, function (err) {
if (err) {
return new this({
username,
...
}).save(cb);
} else {
return Promise.reject(new Error("User already exists!"));
}
});
};
I'm pretty confused with the whole cb function and what I'm supposed to pass to it.
After reading other posts about validation, I realized I could also do something like this:
const userSchema = new mongoose.Schema({
username: {
type: String,
unique: true,
validate: function (val, fn) {
this.find({ username: val }, function (err) {
fn(err || true);
});
},
message: function (props) {
`Username '${props.value}' already exists.`;
},
},
...
Also here I'm confused about what fn accepts and what it even does (I found an answer similar to this with no explanation online).
In the end, I would like to use this model in a controller to create a new user, like this
User.createNew({ username: "example", ...})
.then(doc => console.log("User was created: " + doc))
.catch(err => console.error) // The error is something custom like "This user already exists"
Any help is appreciated!

Custom Validation on a field that checks if field already exists and is active

I have a mongodb Collection "Users" having "Name", "Email", "Active" fields.
I want to add a validation that for every document email should be unique. However if a document is invalid i.e Active is false then the email can be accepted.
Here is the model
email: { type: String, validate: {
validator: function(v) {
return new Promise((resolve, reject)=> {
console.log("validating email")
const UserModel = mongoose.model('User');
UserModel.find({email : v, active: true}, function (err, docs)
{
if (!docs.length){
resolve();
}else{
console.log('user exists: ',v);
reject(new Error("User exists!"));
}
});
})
},
message: '{VALUE} already exists!'
}
},
name: {
type: String,
required: true
},
active: {
type: Boolean,
default: true
}
Problem is whenever i do any updation on this model then this validation is called.
So if i update the name then also this validation is called and it gives the error that email already exists.
How do I add a validation on email field so if someone adds a new entry to database or updates email it checks in database if existing user has same email id and is active?
I would first call Mongoose findOne function if the User is already registered the Mongo DB, for example;
let foundUser = await User.findOne({email});
if (!foundUser) {
// update user, create user etc.
...
}
I think it is better to not use logic inside the Mongoose document object. Maybe there is a way to achieve it but I prefer to do these validations in the code, not in the document, it is just my preference.
Also you can try making email unique as follows:
email: {
type: String,
unique: true
}
I'd use unique compound index, instead of having one more additional query to your db. Your code would look like this:
const schema = = new Schema(...);
schema.index({email: 1, active: 1}, {unique: true});
Mongo itself will reject your documents and you can catch it in your code like this:
const {MongoError} = require('mongodb'); // native driver
try {
await model.updateOne(...).exec(); // or .save()
} catch (err) {
//11000 is error code for unique constraints
if (err instanceof MongoError && err.code === 11000)
console.error('Duplicate email/active pair');
}

How to to access first and last name for the following user model (based on mongoose)?

I am looking at some code, with a user schema, similar to the following.
var UserSchema = new mongoose.Schema(
{
email: {
type: String,
lowercase: true,
unique: true,
required: true
},
password: {
type: String,
required: true
},
profile: {
firstName: {
type: String
},
lastName: {
type: String
}
}
}
);
Now, as far as I can understand, the top-level properties are email, password and profile.... firstName and lastName should only be accessible from within profile. However, the details are being accessed with something like the following.
exports.register = function(req, res, next) {
// Check for registration errors
const email = req.body.email;
const password = req.body.password;
const firstName = req.body.firstName;
const lastName = req.body.lastName;
// Return error if no email provided
if (!email) {
return res.status(422).send({ error: 'You must enter an email address.'});
}
// Return error if no password provided
if (!password) {
return res.status(422).send({ error: 'You must enter a password.' });
}
// Return error if full name not provided
if (!firstName || !lastName) {
return res.status(422).send({ error: 'You must enter your full name.'});
}
...
I don't seem to understand why the firstName and lastName are being accessed directly with req.body.firstName instead of req.body.profile.firstName. There don't seem to be any virtual attributes in place either. So what's going on!?
req.body added by body-parser and this is not related to your mongoose model. You will get data in req.body sent from front-end(client side). Apart from this issue, I would like to recommend you to use following format that may help you
You may like to use schema for sub-document
var profileSchema = new Schema({
firstName: String,
lastName: String
});
var UserSchema = new mongoose.Schema({
email: {
type: String,
lowercase: true,
unique: true,
required: true
},
password: {
type: String,
required: true
},
profile: profileSchema
});
and may use like
exports.register = function(req, res, next) {
if(!req.body)
return res.status(500).send({ error: 'Unable to parse data'});
// Check for registration errors
const userData = {
email: req.body.email,
password: req.body.password,
profile: {
firstName: req.body.firstName,
lastName: req.body.lastName
}
}
// Return error if no email provided
if (!userData.email) {
return res.status(422).send({ error: 'You must enter an email address.'});
}
// Return error if no password provided
if (!userData.password) {
return res.status(422).send({ error: 'You must enter a password.' });
}
// Return error if full name not provided
if (!userData.profile.firstName || !userData.profile.lastName) {
return res.status(422).send({ error: 'You must enter your full name.'});
}
var newUser = new User(userData);// you must import user schema before using User
In an express application request parameters are passed in as first parameter of a route (req, res, next). The sample code posted shows the result of a POST request to a route called /register.
This data does not relate to the model posted with the question.
To be able to work with the model, the data needs to be stored into a new Mongoose object.
So within the route one would write:
exports.register = function(req, res, next) {
const User = new User();
User.profile.firstName = req.body.firstName;
// ... and so on
User.save((err, savedUser) => {
if(err) return next(err);
// end request
});
}
Please note that some kind of sanity check is recommended when dealing with user provided variables. Using it like in my example above may enable an attacker to store a string of arbitrary length inside the database which is most probably not desired.
As pointed out by #DanielKhan, within the above comments, mongoose is only being used to model the data. However, at this point, it has nothing to do with the data coming in directly from the client. Hence, all the fields, including email, password, and first name, and last name will be retrieved at the same level... using req.body.

Validating password / confirm password with Mongoose schema

I have a userSchema that looks like this:
var userSchema = new Schema({
name: {
type: String
, required: true
, validate: [validators.notEmpty, 'Name is empty']
}
, username: {
type: String
, required: true
, validate: [validators.notEmpty, 'Username is empty']
}
, email: {
type: String
, required: true
, validate: [
{ validator: validators.notEmpty, msg: 'Email is empty' }
, { validator: validators.isEmail, msg: 'Invalid email' }
]
}
, salt: String
, hash: String
});
All of my validation is happening in the schema so far, and I'm wondering how to go about achieving this with password validation. The user inputs the password into two fields, and the model should check that they are the same as each other.
Does this kind of validation belong in the schema? I'm new to this sort of validation.
How should I validate the passwords?
I eventually discovered that you can use a combination of virtual paths and the invalidate function to achieve this, as shown in this gist, for the very same purpose of matching passwords: https://gist.github.com/1350041
To quote directly:
CustomerSchema.virtual('password')
.get(function() {
return this._password;
})
.set(function(value) {
this._password = value;
var salt = bcrypt.gen_salt_sync(12);
this.passwordHash = bcrypt.encrypt_sync(value, salt);
});
CustomerSchema.virtual('passwordConfirmation')
.get(function() {
return this._passwordConfirmation;
})
.set(function(value) {
this._passwordConfirmation = value;
});
CustomerSchema.path('passwordHash').validate(function(v) {
if (this._password || this._passwordConfirmation) {
if (!val.check(this._password).min(6)) {
this.invalidate('password', 'must be at least 6 characters.');
}
if (this._password !== this._passwordConfirmation) {
this.invalidate('passwordConfirmation', 'must match confirmation.');
}
}
if (this.isNew && !this._password) {
this.invalidate('password', 'required');
}
}, null);
I think password matching belongs in the client interface and should never get to the server (DB layer is already too much). It's better for the user experience not to have a server roundtrip just to tell the user that 2 strings are different.
As for thin controller, fat model... all these silver bullets out there should be shot back at the originator. No solution is good in any situation. Think everyone of them in their own context.
Bringing the fat model idea here, makes you use a feature (schema validation) for a totally different purpose (password matching) and makes your app dependent on the tech you're using now. One day you'll want to change tech and you'll get to something without schema validation at all... and then you'll have to remember that part of functionality of your app relied on that. And you'll have to move it back to the client side or to the controller.
I know the thread is old but if it could save someone's time...
My approach uses pre-validate hook and works perfectly for me
schema.virtual('passwordConfirmation')
.get(function() {
return this._passwordConfirmation;
})
.set(function(value) {
this._passwordConfirmation = value;
});
schema.pre('validate', function(next) {
if (this.password !== this.passwordConfirmation) {
this.invalidate('passwordConfirmation', 'enter the same password');
}
next();
});
I use express-validator before it ever gets down to the schema level in ./routes/signup.js:
exports.post = function(req, res){
req.assert('email', 'Enter email').notEmpty().isEmail();
req.assert('username', 'Enter username').notEmpty().isAlphanumeric().len(3,20);
req.assert('password', 'Enter password').notEmpty().notContains(' ').len(5,20);
res.locals.err = req.validationErrors(true);
if ( res.locals.err ) {
res.render('signup', { message: { error: 'Woops, looks like we need more info...'} });
return;
}
...//save
};
You can attach custom methods to your model instances by adding new function attributes to Schema.methods (you can also create Schema functions using Schema.statics.) Here's an example that validates a user's password:
userSchema.methods.checkPassword = function(password) {
return (hash(password) === this.password);
};
// You could then check if a user's password is valid like so:
UserModel.findOne({ email: 'email#gmail.com' }, function(err, user) {
if (user.checkPassword('secretPassword')) {
// ... user is legit
}
});
The second verification password doesn't need to be submitted for registration.
You could probably get away with validating the two fields are equal on the client-side.
It's kind of late but for the sake of people having similar issues. i ran into a similar problem lately, and here was how i went about it; i used a library called joi
const joi = require('joi');
...
function validateUser(user){
const schema = joi.object({
username: joi.string().min(3).max(50).required(),
email: joi.string().min(10).max(255).required().email(),
password: joi.string().min(5).max(255).required(),
password2: joi.string().valid(joi.ref('password')).required(),
});
return schema.validate(user);
}
exports.validate = validateUser;

Mongoose unique validation error type

I'm using this schema with mongoose 3.0.3 from npm:
var schema = new Schema({
_id: Schema.ObjectId,
email: {type: String, required: true, unique: true}
});
If I try to save a email that is already in db, I expect to get a ValidationError like if a required field is omitted. However this is not the case, I get a MongoError: E11000 duplicate key error index.
Which is not a validation error (happens even if I remove the unique:true).
Any idea why?
I prefer putting it in path validation mechanisms, like
UserSchema.path('email').validate(function(value, done) {
this.model('User').count({ email: value }, function(err, count) {
if (err) {
return done(err);
}
// If `count` is greater than zero, "invalidate"
done(!count);
});
}, 'Email already exists');
Then it'll just get wrapped into ValidationError and will return as first argument when you call validate or save .
I had some issues with the approved answer. Namely:
this.model('User') didn't work for me.
the callback done wasn't working properly.
I resolved those issues by:
UserSchema.path('email').validate(async (value) => {
const emailCount = await mongoose.models.User.countDocuments({email: value });
return !emailCount;
}, 'Email already exists');
I use async/await which is a personal preference because it is much neater: https://javascript.info/async-await.
Let me know if I got something wrong.
This is expected behavior
The unique: true is equivalent to setting an index in mongodb like this:
db.myCollection.ensureIndex( { "email": 1 }, { unique: true } )
To do this type of validation using Mongoose (Mongoose calls this complex validation- ie- you are not just asserting the value is a number for example), you will need to wire in to the pre-save event:
mySchema.pre("save",function(next, done) {
var self = this;
mongoose.models["User"].findOne({email : self.email},function(err, results) {
if(err) {
done(err);
} else if(results) { //there was a result found, so the email address exists
self.invalidate("email","email must be unique");
done(new Error("email must be unique"));
} else {
done();
}
});
next();
});
Simply response to json
try {
let end_study_year = new EndStudyYear(req.body);
await end_study_year.save();
res.json({
status: true,
message: 'បានរក្សាទុក!'
})
}catch (e) {
res.json({
status: false,
message: e.message.toString().includes('duplicate') ? 'ទិន្នន័យមានរួចហើយ' : e.message.split(':')[0] // check if duplicate message exist
})
}
Sorry for answering an old question. After testing I feel good to have find these answers, so I will give my experience. Both top answers are great and right, just remember that:
if your document is new, you can just validate if count is higher than 0, thats the common situation;
if your document is NOT new and has modified the unique field, you need to validate with 0 too;
if your document is NOT new and has NOT being modified, just go ahead;
Here is what I made in my code:
UserSchema.path('email').validate(async function validateDuplicatedEmail(value) {
if (!this.isNew && !this.isModified('email')) return true;
try {
const User = mongoose.model("User");
const count = await User.countDocuments({ email: value });
if (count > 0) return false;
return true;
}
catch (error) {
return false;
}
}, "Email already exists");

Resources