Initialize collection with data in mongoDB using mongoosejs - node.js

I am trying to implement a learning platform where users answer questions and get points. I want in the user profile/collection to hold the questions and the answers as well as the number of tries the users took to get the correct answer.
My mongoose schema is the following:
username: { type: String, lowercase: true, unique: true },
hash: String,
salt: String,
realname : String,
score: Number,
CourseFinished: Boolean,
questions: [{
question : String,
answer : String,
noOfTries: Number
}]
The route that creates the user is this one:
router.post('/register', function(req, res, next){
if(!req.body.username || !req.body.password){
return res.status(400).json({message: 'Please fill out all fields'});
}
var user = new User();
user.username = req.body.username;
user.setPassword(req.body.password);
user.questions.question = "What is the language used to write websites?"; //this one doesn't work
user.questions.answer.push("HTML"); //this one doesn't work either
user.save(function (err){
if(err){ return next(err); }
return res.json({token: user.generateJWT()})
});
});
How can I initialize an array of questions and answers with the creation of the user?

Related

Mongoose schema for article

I'm building a news website, and I this mongoose schema:
let mongoose = require('mongoose');
let articleSchema = mongoose.Schema({
image1:{
type: String,
required: true
},
title:{
type: String,
required: true
},
author:{
type: String,
required: true
},
date:{
type: String,
required: true
},
updated:{
type: String,
default: 'not updated'
},
title_nd:{
type: String,
required: false
},
body:{
type: String,
required: true
},
comments: [commentsSchema],
likes:{ type:Number, default:0 }
});
let Article = module.exports = mongoose.model('Article', articleSchema);
And I want to add a form so users can add their comments.
The question is how do I create a new schema for comments and link it to article schema, and then if the user adds a comment the comment added to the database and then shows on the article comment section?
Modeling a separate schema for comment is not a good idea in my humble opinion, since it is a classic case of one to few mapping which is an ideal use case for embedding the document. To give you a basic idea about data modeling i am quoting here
You need to consider two factors:
Will the entities on the ā€œNā€ side of the One-to-N ever need to stand alone?
What is the cardinality of the relationship: is it one-to-few; one-to-many; or one-to-squillions?
Based on these factors, you can pick one of the three basic One-to-N schema designs:
Embed the N side if the cardinality is one-to-few and there is no need to access the embedded object outside the context of the parent object
Use an array of references to the N-side objects if the cardinality is one-to-many or if the N-side objects should stand alone for any reasons
Use a reference to the One-side in the N-side objects if the cardinality is one-to-squillions
Please refer to a very well written and articulated post 6 Rules of Thumb for MongoDB Schema Design: Part 1 from mongodb blogs.
Even after this if you think it is a good idea to link to another schema please refer to this SO question - Referencing another schema in Mongoose
so I found a solution for this:
// :id is all articles with all ids
router.post('/:id', function (req, res) {
let comment = {};
comment.body = req.body.body;
comment.user = req.user;
comment.date = new Date(Date.now()).toDateString();
// Express validator
req.checkBody('body').len(5, 100);
let errors = [];
errors = req.validationErrors();
if(errors) {
Article.findById(req.params.id, function (err, article) {
if(err)throw err;
req.flash('danger', 'Body minimum length is 5 and maximum 100!');
res.redirect('/articles/'+article.id);
});
} else {
Article.findById(req.params.id, function (err, article) {
if(err)throw err;
article.comments.push({'body':comment.body,'user':comment.user,'date':comment.date});
article.save(function (err) {
if (err) {
throw err;
}else {
req.flash('success', 'Comment added!');
res.redirect('/articles/'+article.id);
}
});
});
}
});
EDIT: code above in more readable form:
router.post('/:id', async (req, res) => {
let article = await Article.findById(req.params.id);
if (!article) res.status("403");
let articleUrl = "/articles/${article.id}";
let comment = {
body: req.body.body,
user: req.user,
date: new Date(Date.now()).toDateString();
};
if (commment.body.lengh >= 100 || comment.body.length <= 5) {
req.flash('danger', 'Body minimum length is 5 and maximum 100!');
return res.redirect(articleUrl);
}
articles.comments.push(comment);
await article.save();
req.flash('success', 'Comment added!');
res.redirect(articleUrl);
});

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 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 - trying to do 'JOINS' in MEAN stack

I am having a hard time understanding the async nature of NodeJS.
So, I have an articles object with this schema:
var ArticleSchema = new Schema({
created: {
type: Date,
default: Date.now
},
title: {
type: String,
default: '',
trim: true,
required: 'Title cannot be blank'
},
content: {
type: String,
default: '',
trim: true
},
creator: {
type: Schema.ObjectId,
ref: 'User'
}
});
and the User schema is:
var UserSchema = new Schema({
firstName: String,
lastName: String,
...
});
The problem is when I query for all the documents like so:
exports.list = function(req, res) {
// Use the model 'find' method to get a list of articles
Article.find().sort('-created').populate('creator', 'firstName lastName fullName').exec(function(err, articles) {
if (err) {
// If an error occurs send the error message
return res.status(400).send({
message: getErrorMessage(err)
});
} else {
// Send a JSON representation of the article
res.json(articles);
}
});
};
I get all the articles back successfully, but for some reasons, the article creator is returning different results
for locally authenticated users (localStrategy) and facebook authenticated users (facebook strategy) for locally authenticated users, I get:
articles = {
creator: {
id: 123,
firstName: 'Jason',
lastName: 'Dinh'
},
...
}
for fb authenticated users, I get:
articles = {
creator: {
id: 123
},
...
}
I can't seem to get a grip on PassportJS API, so what I want to do is
iterate through articles and for each article, find the user document using the article creator ID and add the user firstName and lastName to the articles object:
for each article in articles {
User.findOne({ '_id': articles[i].creator._id }, function(err, person){
//add user firstName and lastName to article
});
}
res.json(articles);
You can probably already see the problem here... my loop finishes before the documents are returned.
Now, I know that MongoDB doesn't have any 'joins' and what I want to do is essentially return a query that 'joins' two collections. I think I'm running into problems because I don't fundamentally understand the async nature of
node.
Any help?
You can use find instead of findOne and iterate inside your callback function.
User.find({ }, function(err, personList){
for each person in personList {
for each article in articles {
if (person._id === article.creator._id) {
//add user firstName and lastName to article
}
}
}
res.json(articles);
});
UPDATE:
Considering the scenario that #roco-ctz proposed (10M users), you could set a count variable and wait for it to be equal to articles.length:
var count = 0;
for each article in articles {
User.findOne({ '_id': articles[i].creator._id }, function(err, person){
//add user firstName and lastName to article
count += 1;
});
}
while (count < articles.length) {
continue;
}
res.json(articles);

Validation on user inputs with MongoDB and mongoose?

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

Resources