Can a Schema use it's own model to validate? - node.js

For example, say I have a user schema, and I want to validate that the username is unique before even attempting to save the user to the database.
...
UserSchema.path('username')
.validate(function (value, respond) {
User.findOne({ username: this.username }) // This isn't valid.
.lean()
.select('_id')
.exec(function (err, user) {
if (err) {
winston.warn('User_username: Error looking for duplicate users');
respond(false);
}
// If a user was returned, then the user is non-unique!
if (user) {
respond(false);
}
respond(true);
});
});
...
var User = mongoose.model('User', UserSchema);
I know I could use mongoose.model('User').findOne(...) but that just seems a bit silly, is there no better way to do it?

You can create an unique index in your schema by setting unique: true. This will make use of the unique index option that is available in mongodb. Here is an example snippet from one of my models using this option:
// The (generated) uniform resource locator
url: {
// ... which is required ...
required: true,
// ... which is an unique index ...
unique: true,
// ... and is a string.
type: String
}
Compound key from comments:
Schema.index({ username: 1, accountCode: 1 }, { unique: true })

Related

Mongoose- Run a function to all Entries in database

I am a beginner in NodeJs and MongoDB. I have a user schema where I have a field which is an array that is filled by the user's input value. After users enter the value, the admin also passes an array of correct answers. I want to create a function which runs on all users array field and on correct answer store the score in users schema. Just wanted to know how do I run the function on all entries of the collection.
//Final result schema by the admin
const resultSchema = new mongoose.Schema({
matchday:Number,
homeTeam:String,
awayTeam:String,
utcDate:Date,
finalUpdateTime:Date,
result:Array
})
//The predicted answer Schema
const predictSchema = new mongoose.Schema({
user:{
type:mongoose.Schema.ObjectId,
ref:'User',
required:[true, 'Predicted Team must belong to a User']
},
teamData:Array,
matchday: Number,
score:{
type:Number,
default:0
},
createdAt: {
type:Date,
default:Date.now()
},
lastUpdated:Date,
},{
toJSON: {
virtuals: true,
},
toObject: {
virtuals: true,
},
})
You can define a static method for your schema. Statics are methods that can be invoked directly by a Model.
See here
You can pass array of correct answers to this method and check the answers for each user in your collection. You can retrieve all users using Find
I managed to solve the issue and it works but not sure if its the correct way to do it
exports.updateUserScore = async (req, res, next) => {
const user = await Predict.find({ matchday: req.body.matchday });
user.map(async (el) => {
let score = 0;
el.teamData.map((e) => {
if (req.body.teamData.includes(e)) score = score + 1;
});
console.log(score, el._id);
await Predict.findByIdAndUpdate(el._id, { score: score });
});
res.status(200).json({
status: 'success',
message: 'Updated User Score Successfully',
});
};

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');
}

Issue in saving registered user to MongoDB in Node JS - Express application

I am new to Node.js and trying to create a chat application program. For that I have created a Signup registration form with express framework. The data will be saved in MongoDB. Application uses passport middleware signup functionality. Issue is when submitting a new user for second time I am not able to see the data in mongoDB, instead i can see only the first data. I set the mongodb debug option to true, after submitting the form, user submitted data will be seen through console.
Please see the github code which i created: https://github.com/Deepesh316/jabarchat
Please see the mongodb user details getting saved data code: passport-local.js
passport.use('local.signup', new LocalStrategy({
usernameField: 'email',
passwordField: 'password',
passReqToCallback: true
}, (req, email, password, done) => {
User.findOne({'email': email}, (err, user) => {
// Network or Internet connection error
if(err) {
return done(err);
}
if(user) {
return done(null, false, req.flash('error', 'User with email already exist'));
}
// Creating new instance of user and save it to database
const newUser = new User();
newUser.username = req.body.username;
newUser.email = req.body.email;
newUser.password = newUser.encryptPassword(req.body.password);
newUser.save((err) => {
done(null, newUser);
});
});
}));
Below is the code snippet for Model:
const mongoose = require('mongoose');
const bcrypt = require('bcrypt-nodejs');
const userSchema = mongoose.Schema({
username: { type: String, unique: true },
fullname: { type: String, unique: true, default: ''},
email: { type: String, unique: true },
password: { type: String, default: ''},
userImage: { type: String, default: 'default.png'},
facebook: { type: String, default: ''},
fbTokens: Array,
google: { type: String, default: ''},
googleTokens: Array
});
userSchema.methods.encryptPassword = function(password) {
return bcrypt.hashSync(password, bcrypt.genSaltSync(10), null);
};
userSchema.methods.validUserPassword = function(password) {
return bcrypt.compareSync(password, this.password);
};
module.exports = mongoose.model('User', userSchema);
The error message is saying that there's already a record with null as the fullname. In other words, you already have a user without an fullname.
The relevant documentation for this:
If a document does not have a value for the indexed field in a unique index, the index will store a null value for this document. Because of the unique constraint, MongoDB will only permit one document that lacks the indexed field. If there is more than one document without a value for the indexed field or is missing the indexed field, the index build will fail with a duplicate key error.
You can combine the unique constraint with the sparse index to filter these null values from the unique index and avoid the error.
Sparse indexes only contain entries for documents that have the indexed field, even if the index field contains a null value.
In other words, a sparse index is ok with multiple documents all having null values.
Check with mydb.users.getIndexes() if this is the case and manually remove the unwanted index with mydb.users.dropIndex()

How to check values against the DB when using the pre-save hook?

On a User schema, I'd like to check if the specified email already exists for the specified shop, before saving.
var UserSchema = new Schema({
_shop: {
type: Schema.Types.ObjectId,
ref: 'Shop',
required: true
},
email: String,
//...
});
UserSchema.pre('save', function(next) {
if (!this.isNew) return next();
// How to do use the static method isThatEmailFreeForThisShop here?
});
UserSchema.statics.isThatEmailFreeForThisShop = function(email, shop_id, cb) {
this.find({email: email, _shop: shop_id}, function(err, users) {
// ...
});
});
There could be different users with the same email as long as they are from different shops.
I do not know how to use the static method in the pre-save hook...
Thanks!
You've created a User Model instance somewhere (I'll call it User):
var User = mongoose.model('user', UserSchema);
So, the isThatEmailFreeForThisShop function is available on the User model:
User.isThatEmailFreeForThisShop(...)
From your save hook:
UserSchema.pre('save', function(next) {
if (!this.isNew) return next();
User.isThatEmailFreeForThisShop(this.email, this._shop,
function(err, result) {
if (result) { // found
// do something
return next({ error: "duplicate found" });
}
return next();
});
});
You may also want to switch to using the pre-validate rather than save.
I'd expect in your function, isThatEmailFreeForThisShop that you'd call the cb parameter when the results have been "found".
You probably would use findOne (reference) rather than find. Given that there's still a race condition, you'd want to add an index as a compound index email and shop_id and set the unique attribute to true to prevent duplicates from sneaking in (then, you'll need to handle the fact that a save on a model instance may throw an error.)
UserSchema.statics.isThatEmailFreeForThisShop = function(email, shop_id, cb) {
this.findOne({email: email, _shop: shop_id}, function(err, user) {
// ...
cb(err, user != null);
});
});

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;

Resources