Mongoose - asynchronous validation of schema object - node.js

I am trying to determine how to do asynchronous validation for a Mongoose schema - specifically in this case the username. TMK, to ensure that the username is unique, we have to manually query the database to see if the same username already exists. This is an asynchronous query. However the methodology of having a 'validate:' property for each schema item, seems to ask for a synchronous validation function. In other words, this line:
validate: [validation.usernameValidator, 'not a valid username']
seems to require that usernameValidator be synchronous, and the problem is I need it to be async, for the reason aforementioned.
So, I have a Mongoose schema for a User like so:
var validation = {
usernameValidator: function (candidate) {
return true;
},
passwordValidator: function (candidate) {
return true;
}
};
userSchema = mongoose.Schema({
username: {
type: String,
isUnique: true,
required: true,
validate: [validation.usernameValidator, 'not a valid username']
},
passwordHash: {
type: String,
required: true,
validate: [validation.passwordValidator, 'not a valid password']
},
email: {
type: String,
isUnique: true,
required: true,
validate: [validation.emailValidator, 'not a valid email address']
}
});
userSchema.pre('save', function (next) {
var self = this;
if (!self.isModified('passwordHash')) {
return next();
}
bcrypt.hash(self.passwordPreHash, SALT_WORK_FACTOR, function (err, hash) {
if (err) {
return next(err);
}
else if(hash == null){
return next(new Error('null/undefined hash'));
}
else {
self.passwordHash = hash;
next();
}
});
});
//is the following function my best bet?
userSchema.path('username').validate(function (value, respond){
this.findOne({ username: value }, function (err, user){
if(user) respond(false);
});
}, 'This username has been already registered');
is my only option to leave out the validation.usernameValidator methodology, and validate username with userSchema.path('username').validate..?

Mongoose should handle this provided that you specify unique: true on that field.
For example
userSchema = mongoose.Schema({
username: {
type: String,
unique: true,
required: true
},
passwordHash: {
type: String,
required: true
},
email: {
type: String,
unique: true,
required: true
}
});
ADDITION:
Mongoose will declare a unique index provided that you specify such in your schema(as done in example above). This prevents having to query into mongodb to see if another document has a field of the same value. You can read about it here.
You can read more about Unique Indexes for mongodb here, if you'd like to learn more about their behaviour.
Note: A validation error will not be throw if a non-unique value is provided. See the mongoose docs for more info on this.

Related

Can´t validate type of data on update/findOneAndUpdate mongoose

In create, the validation works fine and it will throw validation error if missing required or wrong type.
However, when I try to update or findOneAndUpdate it only validates wether any required is missing, but it doesn´t validate the type. At the moment I can update the name property to a number and no validation error happens. Any idea on what to do?
mongoose.set('runValidators', true);
const Post = mongoose.model('Post', {
nome: {
type: String,
required: true,
trim: true
},
email: {
type: String,
required: true,
trim: true
},
morada: {
type: String,
required: true,
trim: true
}
})
module.exports = Post
const update = async (req, res) => {
try {
let post = await Post.findOneAndUpdate(req.params, req.body, {new: true});
res.json(post)
} catch (e) {
res.status(500).json(e)
}
}
You need to explicitly define the Post model using a mongoose schema. Something like the following:
const PostSchema = {
nome: { type: String, required: true, trim: true},
email: { type: String, required: true, trim: true},
morada: { type: String, required: true, trim: true}
};
const Post = mongoose.model('Post', PostSchema);
If that doesn't work, you could use a pre function on the schema. The pre function allows you to run code before certain actions (e.g. save) where you can do things like more granular data validation.
For example:
Post.pre("save", function(next, done) {
let self = this;
if (invalid) { // Replace 'invalid' with whatever checking needs to be done
// Throw an Error
self.invalidate("nome", "name must be a string");
next(new Error("nome must be a string"));
}
next();
});

checking if object id is in an array of object id

I am trying to do a check to see if a logged-in user's id req.user.id. is in an array of followers of the user being checked in req.params.id, bit for some reason it doesn't work.
router.get('/api/:id/isfollowing', auth, async (req, res) => {
if (req.params.id==req.user._id) {
return res.status(200).send({ "isfollowing": "Myself" })
}
try {
const followers = await Follow.find({
user: req.params.id
})
let followersArr = followers.map(follower=>{
return follower.followedBy
})
const yes = followersArr.includes(req.user._id)
// const yes = followersArr.filter((objId) => objId==req.user._id)
console.log(yes, followersArr, req.user._id)
if (yes===false) {
return res.status(200).send({ "isfollowing": false })
}
return res.status(200).send({ "isfollowing": true })
} catch (e) {
res.status(500).send()
}
})
for some reason the check doesn't work and even when using the filter, it still returns nothing. But when I console.log the values, it is right there.
[] [ 5fa4f0af4a7bf5471c41e225, 5f9dc1777a695570e878424d ] 5f9dc1777a695570e878424d
EDIT
schemas below
User schema
const userSchema = new mongoose.Schema({
fullname: {
type: String,
required: true,
trim: true,
lowercase: true
},
username: {
type: String,
unique: true,
required: true,
trim: true,
lowercase: true
},
email: {
type: String,
unique: true,
required: true,
trim: true,
lowercase: true,
validate(value) {
if (!validator.isEmail(value)) {
throw new Error('Email is invalid')
}
}
},
password: {
type: String,
required: true,
minlength: 7,
trim: true,
validate(value) {
if (value.toLowerCase().includes('password')) {
throw new Error('Passwoed cannot contain "password"')
}
}
}
})
follow schema
const followSchema = new mongoose.Schema({
// the logged in user who will be trying to follow someone will be added to "followedBy"
// the user who is getting followed will be added to "user"
user: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'User'
},
followedBy: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'Showcase'
}
}, {
timestamps: true
})
I gave follow its own schema so I can record other info like time and other info whenever a user follows another.
If I've understand well you only need a simple query.
Since you only want to know if the id is into an array, you can check that directly with mongo. You don't need load every document into memory and use JS functions like filter or something similar.
You only need a query similar to this:
db.collection.find({
"user": ObjectId("user_id"),
"followedBy": ObjectId("follower_id")
})
This will return a document that match both values.
Check here it works and tell me if is the behaviour and output you expect.
Also I will code a mongoose query and I'll update the answer.
You can use also this query in mongoose to get how many documents find the query:
var find = await model.find({"user":mongoose.Types.ObjectId(user_id),"followedBy":mongoose.Types.ObjectId(follower_id)}).countDocuments()
Includes cannot be used in this case since you are trying to find ObjectId in an array.
To find if req.user._id is present in followersArr, use Array.some() function as below
const yes = followersArr.some(followerId=>{followerId.equals(req.user._id)})
The some call will iterate over the followersArr array, calling equals on each one to see if it matches req.user._id and stop as soon as it finds a match. If it finds a match it returns true, otherwise false.
You can't use something simpler like indexOf because you want to compare the ObjectIDs by value, not by reference.

I have been trying to solve the duplicate key problem in nodeJS with Mongoose, but nothing works

I'm trying to build a user model, but I want to make sure that username and email are unique. When I created the first user everything was ok, but when I try to create the second user with the same information, I got the some error that I can handle in when I will save, but the duplicate key wasn't there to handle it.
This is my schema file code:
const UserSchema = new Schema({
// this username with SchemaType of string
username: {
type: String,
lowercase: true,
required: [true, "username is required"],
unique: true,
trim: true,
minlength: [4, "try to user longer name"],
maxlength: [60, "your name is way too long"],
},
// virtual name
name: {
// name have two properties
// first is first and refer to first-name
// second is last and refer to last-name
first: {
type: String,
lowercase: true,
trim: true,
minlength: 4,
maxlength: 20
},
last: {
type: String,
lowercase: true,
trim: true,
minlength: 4,
maxlength: 20
}
},
password: {
type: String,
required: [true, "password is required"]
},
email: {
type: String,
required: [true, "email is required"],
unique: true
},
active: {
type: Boolean,
default: true
},
admin: {
type: Boolean,
default: false
},
meta: {
update: {
type: Date,
default: Date.now()
},
timestamp: {
type: Date,
default: Date.now()
}
}
});
UserSchema.virtual("fullname").get(function () {
// return the concatenation of first and last
return this.name.first + " " + this.name.last;
});
// Create User Model
const User = mongoose.model("User", UserSchema);
module.exports = User;
And this is my router code where I tried to handle it:
router.post("/register", (request, response) => {
const user = {
username: request.body.username,
email: request.body.email,
password: request.body.password
};
if (!user.email && !user.username && !user.password) {
return response.json({
"message": "please fill the whole information"
});
}
// put user info in model
const newUser = new User({
username: user.username,
email: user.email,
password: user.password
})
newUser.validate((err) => {
console.log(err);
});
// save User in model
newUser.save()
// return response with info
return response.status(201).json(user);
})
I think the explanation here is quite a simple one. You are specifying the unique attribute in your schema for multiple fields, so mongo will not allow you to create multiple entries with the same information. This is quite obvious.
Also, I noticed a bit of irregularity in your code. The save method you are calling returns a promise, which means the event loop will not block your code and the response will be returned immediately. For this, you either need to handle your response inside the then block or use async await throughout your code.
I would suggest the following changes:
router.post("/register", (request, response) => {
const user = {
username: request.body.username,
email: request.body.email,
password: request.body.password
};
if (!user.email && !user.username && !user.password) {
return response.json({
"message": "please fill the whole information"
});
}
// put user info in model
const newUser = new User({
username: user.username,
email: user.email,
password: user.password
})
newUser.validate((err) => {
if(err) {
response.status(403).json({ message: 'Your custom error message' });
}
newUser.save().then(res => {
return response.status(201).json(user);
}).catch(e => {
return response.status(500).json({ message: 'Your custom error message' });
})
});
})

Prevent server from crashing while interacting with db

I'm trying to create login form using mongoose. The error has been occurred when I try to find for example an email in my database an it doesn't exist. In this case my app crashed.
Here is my UserSchema:
var mongoose = require('mongoose');
var userSchema = mongoose.Schema({
name: {
type: String,
required: true,
trim: true
},
credit:{
type: Number,
default:0
},
email: {
type: String,
unique: true,
required: true,
trim: true
},
telegramId:{
type: String,
trim: true,
required: true
},
password: {
type: String,
required: true
},
inboxMessages: {
type: Array
},
submittedBooks:{
type: Array
},
profilePicture: {
type: String,
default: '/'
}
});
var User = mongoose.model('User', userSchema);
module.exports = User;
and this is my NodeJS code and query:
UserModel.find({name: 'akbar'}, (err, data) =>{
if (err) {
console.log(err);
}
else{
console.log(data[0].password);
}
})
UserModel.find({name: 'akbar'}, ....) cause an error which contains TypeError: Cannot read property 'password' of undefined. How can I prevent crashing my app?
When you try to find an object from the database, there might be 3 scenarios.
Internal server error (You checked this).
Empty or null value. (You have not checked it yet).
Got the desired object. (You print these values)
So to checked scenario 2, use the following code,
UserModel.find({name: 'akbar'}, (err, data) =>{
if (err) {
console.log(err);
} else if ((!data) || (data.length <= 0)) {
console.log('No objecct exist');
} else{
console.log(data[0].password);
}
})
In your NodeJS did you import as
var userModel = require('place_you_defined_the_model')
else, if its just import, like import('place_you_define_model')then you should use,
user.find({name:'Akbar'})

Model with unique attribute is being created twice

I am using this passport-generate-auth module, and I am trying to get my grasp around understanding this whole thing.
So, in my User model, I've got
var User = {
schema: true,
attributes: {
username: {
type: 'string',
unique: true,
required: true
},
email: {
type: 'email',
unique: true,
required: true
},
password: {
type: 'string',
required: true,
minLength: 8
},
}
};
module.exports = User;
And when I call
exports.register = function (req, res, next) {
var email = req.param('email')
, username = req.param('username')
, password = req.param('password');
User.create({
username: username
, email: email
, password: password
}, function (err, user) {
if (err) {
if (err.code === 'E_VALIDATION') {
if (err.invalidAttributes.email) {
req.flash('error', 'Error.Passport.Email.Exists');
} else {
req.flash('error', 'Error.Passport.User.Exists');
}
}
return next(err);
}
});
};
};
when providing username and email that already exist in the database, the new entry is stored in the DB, instead of giving me an error msg.
Isn't User.create() supposed to take care of checking in the schema attributes rules whether they are unique and then check the records in the DB for a record with a value that already exists?

Resources