Validation on user inputs with MongoDB and mongoose? - node.js

I would an unified method to validate my schemas assuming a user input, so not only apply the built-in validation on save/update, but also on find(), etc..
var User = mongoose.model("User", new Schema({
name: {type: String, minlength: 5, maxlength: 128, required: true, unique: true});
}));
What I want is to run validators every time before I run the queries with mongoose, to assure that the user inputs comply with the global schema rules.
Something like that in my route:
var username = $.get["username"], //An input from GET querystring
User = mongoose.model("User");
User.validate({name: username}, function(err) {
if (err) return console.log("not valid input"); //i.e. too short
//run query if valid
});
Is there a plugin (assumed that I'm not using Express) or maybe other already included in mongoose for that?

Documentation: http://mongoosejs.com/docs/validation.html
It is supported in mongoose by default. If you are looking for generic validation before each save operation you can specify the field to be validated path and the validation validate(function(valueEntered, howToRespond). If the validation is not passed the error will be thrown as shown in the example below.
Example: Using bluebird for sake of convenience. The following snippet validates the email, before every save operation.
var mongoose = require('bluebird').promisifyAll(require('mongoose'));
var Schema = mongoose.Schema;
var UserSchema = new Schema({
name: String,
email: {
type: String,
lowercase: true
},
password: String,
});
UserSchema
.path('email')
.validate(function(value, respond) {
var self = this;
return this.constructor.findOneAsync({ email: value })
.then(function(user) {
if (user) {
if (self.id === user.id) {
return respond(true);
}
return respond(false);
}
return respond(true);
})
.catch(function(err) {
throw err;
});
}, 'The specified email address is already in use.');

Related

How to query a Mongo collection from inside of the its own Schema method?

In this method, I want to generate a random username for my users when users register. And, I need to check if the generated username is already in use or not.
So, I need to query the 'User Collection'. I couldn't find how to query User Collection from inside of its method.
this. key, mongoose.Collection or UserSchema always ends up with this error;
TypeError: mongoose.Collection.find is not a function
My UserSchema is something like this;
const UserSchema = new Schema({
name: {
type: String,
required: [true, 'name can not be empty'],
default: ' '
},
publicUsername: {
type: String,
required: true
}
})
And finally, the method;
UserSchema.methods.generateRandomUserName = async function(){
// Generate a random username
// Find if username already is in use. But, how???
let user = await mongoose.Collection.find({ publicUserName: username })
if(user){
// Generate again if username is in use
}
this.publicUsername = username
return username
}
Can someone help me, please?
You should be able to access the collection through mongoose.model():
UserSchema.methods.generateRandomUserName = async function(){
const user = await mongoose.model('User').find({ publicUserName: username })
// rest of your code
}
See this example in the docs: https://mongoosejs.com/docs/guide.html#methods

Unable to update MongoDB collection by matching _id

First, I Registered a user using His email and password.
Now if
I try to Update or Make user details by matching the Id assigned by the Mongo Database while registering the user.
it's not accepting it.
error is like this
Parameter "filter" to find() must be an object, got 60b10821af9b63424cf427e8
if I parse it like
Model.find(parseInt(req.params.id))
it shows a different error.
Well here's the Post Request
//Post request to create user details by matching Id.
// Id I am trying to match is the id that was given by the database
app.post("/user/:id", async(req, res) => {
console.log("not found");
if (await Model.find(req.params.id)) {
const users = new Model({
fName: req.body.fName,
sName: req.body.sName,
birth: req.body.birth,
phone: req.body.phone,
SSN: req.body.SSN
});
const result = await users.save();
console.log(result);
return res.send({
"Success": true,
});
} else {
console.log(req.params.id);
res.status(404).send({ "message": false });
}
});
Here's the schema
const LoginSchema = new mongoose.Schema({
email: { type: String, required: true },
password: { type: String, required: false },
otp: String,
token: String,
fName: String,
sName: String,
birth: Number,
phone: Number,
SSN: Number
});
Here are the headers I used
const express = require("express");
const app = express();
const mongoose = require("mongoose");
app.use(express.json());
app.use(express.urlencoded({ extend: true }));
Database id assigned which I use
{
_id: 60b1131271129a3cf8275160,
email: 'Pak#gmail.com',
password: '$2b$10$FWAHu4lP9vn14zS/tWPHUuQJlO7mjAUTlPj.FliFAZmCNA23JA3Ky',
__v: 0
}
The error:
Parameter "filter" to find() must be an object, got 60b10821af9b63424cf427e8
means: You need to provide an object as the argument of the find() method, not a string ("60b10821af9b63424cf427e8" in this case). Moreover, find() will give you an array, if you find an item in the database, use findOne() instead.
Change from :
await Model.find(req.params.id)
to :
await Model.findOne({_id : req.params.id})
Another way is to use findById() method like this : await Model.findById(req.params.id)
Likely the error is caused by the fact that you're passing a string from the request when Mongoose is expecting an instance of mongoose.Types.ObjectId. You should be able to fix the problem by casting the string into said type.
await Model.find(mongoose.Types.ObjectId(req.params.id))

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.

Mongoose - inserting subdocuments

I have a user model, and a log model. The log model is a subdocument of user model. So in my user model I have:
var mongoose = require('mongoose');
var Log = require('../models/log');
var UserSchema = new mongoose.Schema({
username: {
type: String,
unique: true
},
logsHeld: [
Log
]
});
Then in my 'Log' model I have:
var mongoose = require('mongoose');
var logSchema = new mongoose.Schema({
logComment: {
type: String,
},
});
module.exports = mongoose.model('Log', logSchema);
So upon creation of a 'user', the 'logsHeld' always begins empty. I want to know how to add subdocuments to this user model.
I've tried doing this POST method:
router.post('/createNewLog', function(req, res) {
var user = new User ({
logssHeld: [{
logComment: req.body.logComment
}]
});
user.save(function(err) {
if(err) {
req.flash('error', 'Log was not added due to error');
return res.redirect('/home');
} else {
req.flash('success', 'Log was successfully added!');
return res.redirect('/home');
}
});
});
But this doesn't work. It also includes a 'new User' line, which I don't think I need given this would be for an existing user.
You need to use the logSchema instead of the Log model as your subdocument schema in User model. You can access the schema as follows:
var mongoose = require('mongoose');
/* access the Log schema via its Model.schema property */
var LogSchema = require('../models/log').schema; // <-- access the schema with this
var UserSchema = new mongoose.Schema({
username: {
type: String,
unique: true
},
logsHeld: [LogSchema]
});
Picking up from your comments in another answer where you are facing another issue
WriteError({"code":11000,"index":0,"errmsg":"E11000 duplicate key
error index: testDB.users.$email_1 dup key:
you are getting this because there's already a document in your users collection that has most probably a null value on the email field. Even though your schema does not explicitly specify an email field, you may have an existing old and unused unique index on users.email.
You can confirm this with
testDB.users.getIndexes()
If that is the case and manually remove the unwanted index with
testDB.users.dropIndex(<index_name_as_specified_above>)
and carry on with the POST to see if that has rectified the error, I bet my $0.02 that there is an old unused unique index in your users collection which is the main issue.
Try using logSchema which references only the subdocument schema, Log refers to the entire contents of ../models/log
var UserSchema = new mongoose.Schema({
username: {
type: String,
unique: true
},
logsHeld: [
logSchema
]
});
Documentation: http://mongoosejs.com/docs/subdocs.html
Try push to insert item in array in mongoose
var user = new User;
user.logssHeld.push({
logComment: req.body.logComment
});
user.save(function(err, doc) {
//DO whatever you want
});
see the docs here

How do I find missing schema fields on a Mongoose query

If I add new fields directly to my MongoDB database and I forget to add them to my Mongoose schema, how can I alert myself to the problem without it failing silently.
The following example shows that all fields are returned from a query (regardless of the schema) but undefined if you access the key.
var mongoose = require('mongoose');
var user_conn = mongoose.createConnection('mongodb://db/user');
var userSchema = mongoose.Schema({
email: String,
// location: String,
admin: Boolean
});
var User = user_conn.model('User', userSchema);
User.findOne({email: 'foo#bar.com.au'}, function (err, doc) {
if (err) return console.log(err);
console.log(doc);
console.log(doc.email);
console.log(doc.location);
});
Result:
{ _id: 57ce17800c6b25d4139d1f95,
email: 'foo#bar.com.au',
location: 'Australia',
admin: true,
__v: 0 } // <-- console.log(doc);
foo#bar.com.au // <-- console.log(doc.email);
undefined // <-- console.log(doc.location);
I could read each doc key and throw an error if undefined, but is this the only way?
Versions
Node.js: 6.5.0
Mongoose: 4.6.0
You can set strict to false on the schema so it will save all properties even if they are not in schema:
var userSchema = mongoose.Schema({
email: String,
admin: Boolean
}, {strict: false});
http://mongoosejs.com/docs/guide.html#strict
In order to get a property which is not in schema you need to use doc.toObject() and use the returned object, or to use doc.get('location')
Following on from Amiram's answer. I now use lean() to get the object during a query but when I need to update or save it still looks up the schema.
User.findOne({email: 'foo#bar.com.au'}).lean().exec(function (err, doc) {
console.log(doc);
console.log(doc.email);
console.log(doc.location);
});

Resources