Using two collection in MongoDB with Mongoose - node.js

I'm new to NodeJS, and I'm using the following:
Express
Mongoose
passport
HI I have two collections in MongoDB, they both have nothing to do with each other, they're called:
Post
and
User
In a get route for /home in an IF statement I am trying to query information from both collections and render them in an EJS page. My initial thoughts were create find for one model store in a var, then use it in another function when rendering the page.
Here is the relevant code:
app.get("/home", function(req, res){
console.log(req.user._id);
if (req.isAuthenticated()){
// //*Find current logged in user details
User.find({_id: req.user._id}, function(err, users){
var currentUserName = req.user.name
});
//*end
Post.find({}, function(err, posts){
res.render("home", {
startingContent: homeStartingContent,
posts: posts,
currentUser: currentUserName
});
});
} else {
res.redirect("login")
}
});
At the start of my js file, i have declared
var currentUserName
But its not displaying the value in the home.ejs for:
<p><%= currentUser %></p>
Its just blank.
What am I doing wrong here? I thought using var would make it a global variable and then can be used in another function? Other posts have suggested a aggregate, but these collections are completely seperate and have nothing to do with each other.

You have defined currentUserName inside User.find(){...} function and it is accessible only within that function. You need to define the variable that can be block-scoped, outside User.find(){...} function. Check the code snippet below:
app.get("/home", function(req, res){
console.log(req.user._id);
if (req.isAuthenticated()){
let currentUserName;
// //*Find current logged in user details
User.find({_id: req.user._id}, function(err, users){
currentUserName = req.user.name
});
//*end
Post.find({}, function(err, posts){
res.render("home", {
startingContent: homeStartingContent,
posts: posts,
currentUser: currentUserName
});
});
} else {
res.redirect("login")
}
});
Javascript best practices tells that it is better to use let instead of var. Variables declared by var keyword are scoped to the immediate function body i.e. User.find(){...} while let variables are scoped to the immediate enclosing block denoted by { }, i.e. if(req.isAuthenticated()){...}.

It won't work, because every variable has scope. Scope means access to use that var. In this case currentUserName has scope or can be used only in User.find() section. If you want to use it in Post.find(), then do it by declaring it in main function as below.
app.get("/home", function(req, res){
console.log(req.user._id);
let currentUserName;
if (req.isAuthenticated()){
User.find({_id: req.user._id}, function(err, users){
currentUserName = req.user.name
});
Post.find({}, function(err, posts){
res.render("home", {
startingContent: homeStartingContent,
posts: posts,
currentUser: currentUserName
});
});
} else {
res.redirect("login")
}
});
It will work as you require by declaring currentUserName in main function, so it will be used in whole main app.get() function

Related

Update data in MongoDB using Mongoose and Node.js

I am trying to update certain info in a user collection, when the user is visiting a page.
But my method doesn't work. Can anyone help to get it fixed.
app.get('/add-your-accommodation/apartment-type', (req, res, next) => {
if (req.isAuthenticated()) {
res.render('apartment-type.ejs')
} else {
res.render('login.ejs')
}
var id = req.params.id
if(mongoose.Types.ObjectId.isValid(id)) {
User.findByIdAndUpdate(id, {$set: {accomtype: 'house'}},{new: true})
}
});
Your req.params.id is undefined since there is no mention of it in the route path. You can do this,
app.get('/add-your-accommodation/apartment-type', (req, res) => {
if (!req.isAuthenticated()) {
return res.render('login.ejs')
}
res.render('apartment-type.ejs')
var id = req.user._id //since you're using passport (LocalStrategy)
if(mongoose.Types.ObjectId.isValid(id)) {
User.findByIdAndUpdate(id, {$set: {accomtype: 'house'}})
}
})
Now when you call your API, do it like this,
GET /add-your-accommodation/apartment-type
I agree with #kedar-sedai, when you update/change something in your DB, you should not use a GET request. A good practise would be to use the PUT method, even if you have nothing to pass in the body. It makes it easier for you and other developers to understand what your code does at a glance.
Here are 4 HTTP requests that will work in most of the use cases :
GET
You want to retrieve information from your DB (ex: get users, get all the apartment types...)
POST
You want to add information (ex: register user, add an apartment, ...), or send information using the body of the POST request (ex: login, ...)
PUT
You want to update a value (ex: change username, change an apartment type, ...)
DELETE
You simply want to delete something in your DB (ex: delete a user...)
Try findOneAndUpdate. Also, use callback in your query function for getting the error or result.
app.get('/add-your-accommodation/apartment-type/:id', (req, res, next) => {
if (req.isAuthenticated()) {
res.render('apartment-type.ejs')
} else {
res.render('login.ejs')
}
var id = req.params.id
if(mongoose.Types.ObjectId.isValid(id)) {
User.findOneAndUpdate({_id: mongoose.Types.ObjectId(id)}, { $set: { accomtype:'house' } },(err, result)=>{
if (err) throw new Error(err);
console.log(result)
return
})
}
});

Mongoose toArray() undefined [duplicate]

I am very new to Node.js and MongoDB and am trying to piece together my own blogging application. I have a problem trying to query through my 'Blog' model for ones with a specific username. When I try to run:
var userBlogs = function(username) {
ub = Blog.find({author: username}).toArray();
ub = ub.reverse();
};
I get an error:
TypeError: Object #<Query> has no method 'toArray'
I know globals are bad but I've just been trying to get it to work. The Mongo documentation claims that a cursor is returned which can have the toArray() method called on it. I have no idea why it won't work.
Here is my schema/model creation:
var blogSchema = mongoose.Schema({
title: {type:String, required: true},
author: String,
content: {type:String, required: true},
timestamp: String
});
var Blog = mongoose.model('Blog', blogSchema);
Here are the /login and /readblog requests
app.get('/readblog', ensureAuthenticated, function(req, res) {
res.render('readblog', {user: req.user, blogs: ub})
})
app.get('/login', function(req, res){
res.render('login', { user: req.user, message: req.session.messages });
});
app.post('/login',
passport.authenticate('local', { failureRedirect: '/login'}),
function(req, res) {
userBlogs(req.user.username);
res.redirect('/');
});
});
The end result is supposed to work with this Jade:
extends layout
block content
if blogs
for blog in blogs
h2= blog[title]
h4= blog[author]
p= blog[content]
h4= blog[timestamp]
a(href="/writeblog") Write a new blog
How can I get the query to output an array, or even work as an object?
The toArray function exists on the Cursor class from the Native MongoDB NodeJS driver (reference). The find method in MongooseJS returns a Query object (reference). There are a few ways you can do searches and return results.
As there are no synchronous calls in the NodeJS driver for MongoDB, you'll need to use an asynchronous pattern in all cases. Examples for MongoDB, which are often in JavaScript using the MongoDB Console imply that the native driver also supports similar functionality, which it does not.
var userBlogs = function(username, callback) {
Blog.find().where("author", username).
exec(function(err, blogs) {
// docs contains an array of MongooseJS Documents
// so you can return that...
// reverse does an in-place modification, so there's no reason
// to assign to something else ...
blogs.reverse();
callback(err, blogs);
});
};
Then, to call it:
userBlogs(req.user.username, function(err, blogs) {
if (err) {
/* panic! there was an error fetching the list of blogs */
return;
}
// do something with the blogs here ...
res.redirect('/');
});
You could also do sorting on a field (like a blog post date for example):
Blog.find().where("author", username).
sort("-postDate").exec(/* your callback function */);
The above code would sort in descending order based on a field called postDate (alternate syntax: sort({ postDate: -1}).
Try something along the lines of:
Blog.find({}).lean().exec(function (err, blogs) {
// ... do something awesome...
}
You should utilize the callback of find:
var userBlogs = function(username, next) {
Blog.find({author: username}, function(err, blogs) {
if (err) {
...
} else {
next(blogs)
}
})
}
Now you can get your blogs calling this function:
userBlogs(username, function(blogs) {
...
})

Need to send response after forEach is done

I'm working with NodeJS + Mongoose and I'm trying to populate an array of objects and then send it to the client, but I can't do it, response is always empty because it is sent before forEach ends.
router.get('/', isAuthenticated, function(req, res) {
Order.find({ seller: req.session.passport.user }, function(err, orders) {
//handle error
var response = [];
orders.forEach(function(doc) {
doc.populate('customer', function(err, order) {
//handle error
response.push(order);
});
});
res.json(response);
});
});
Is there any way to send it after the loop has finished?
Basically, you could use any solution for async control flow management like async or promises (see laggingreflex's answer for details), but I would recommend you to use specialized Mongoose methods to populate the whole array in one MongoDB query.
The most straightforward solution is to use Query#populate method to get already populated documents:
Order.find({
seller: req.session.passport.user
}).populate('customer').exec(function(err, orders) {
//handle error
res.json(orders);
});
But if, for some reason, you can't use this method, you could call Model.populate method yourself to populate an array of already fetched docs:
Order.populate(orders, [{
path: 'customer'
}], function(err, populated) {
// ...
});
One solution is to use Promises.
var Promise = require('bluebird');
Promise.promisifyAll(Order);
router.get('/', isAuthenticated, function(req, res) {
Order.findAsync({ seller: req.session.passport.user })
.then(function(orders) {
return Promise.all(orders.map(function(doc){
return Promise.promisify(doc.populate).bind(doc)('customer');
}));
}).then(function(orders){
// You might also wanna convert them to JSON
orders = orders.map(function(doc){ return doc.toJSON() });
res.json(orders);
}).catch(function(err){
//handle error
});
});
BlueBird's .promisifyAll creates an …Async version of all functions of an object, which saves you an extra step in configuring the initial promise. So instead of Order.find I used Order.findAsync in above example

User Permission - Display edit in view (express, handlebars)

At the moment I have user profiles that are rendered using Handlbars as such:
exports.profile = function(req, res) {
User.findOne({username: req.params.username}).exec(function(err, user){
res.render('profile/view', {
user: req.user,
name: user.name,
username: user.username
});
});
};
On the rendered template at profile/view I would like to display an edit button if the user can edit the profile being viewed.
Edit your profile here
Additional information:
Every user us currently authenticated with passport-local strategy
Currently have some basic middleware on the route
Middleware
app.get('/:username', isAuth, user.profile);
function isAuth(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
req.flash('alert', 'First you need to log in');
res.redirect('/login');
}
I've looked into middleware for express such as connect-roles and I don't think its what I need?
What I'm thinking is that I should pass a boolean within the local variables of the res.render() method which then allow me to use handlebars to display the button or not.
{{#if profileOwner }}<a href="#">...
Any ideas?
The best way I think is creating an helper that check if userCan for you. Here the helper:
Handlebars.registerHelper('ifUserCan', function(action, options) {
var actions = action.split(/\s*,\s*/);
for (var i = 0; i < actions.length; i++) {
if (this.userCan(actions[i])) {
return options.fn(this);
}
}
return options.inverse(this);
});
You can use it as follow:
{{#ifUserCan 'show all store categories'}}
<li>{{__ 'Show categories'}}</li>
{{/ifUserCan}}
One way you could do this is by hydrating your template model with a profileOwner privilege as you suggested. Suppose you wanted to only allow users to edit their own profiles. Using connect-roles you could set up a rule like this:
user.use('profile owner', function (req, action) {
return req.isAuthenticated() &&
req.user.username === req.params.username;
})
And your route logic could then be:
exports.profile = function(req, res) {
User.findOne({username: req.params.username}).exec(function(err, user){
res.render('profile/view', {
user: req.user,
name: user.name,
username: user.username,
profileOwner: req.user.is('profile owner')
});
});
};
Then your mustache "if" syntax would look like this:
{{#profileOwner}}Tada{{/profileOwner}}
The negative case can be handled like so if you wish:
{{^profileOwner}}You are not a profile owner!{{/profileOwner}}

Model.find().toArray() claiming to not have .toArray() method

I am very new to Node.js and MongoDB and am trying to piece together my own blogging application. I have a problem trying to query through my 'Blog' model for ones with a specific username. When I try to run:
var userBlogs = function(username) {
ub = Blog.find({author: username}).toArray();
ub = ub.reverse();
};
I get an error:
TypeError: Object #<Query> has no method 'toArray'
I know globals are bad but I've just been trying to get it to work. The Mongo documentation claims that a cursor is returned which can have the toArray() method called on it. I have no idea why it won't work.
Here is my schema/model creation:
var blogSchema = mongoose.Schema({
title: {type:String, required: true},
author: String,
content: {type:String, required: true},
timestamp: String
});
var Blog = mongoose.model('Blog', blogSchema);
Here are the /login and /readblog requests
app.get('/readblog', ensureAuthenticated, function(req, res) {
res.render('readblog', {user: req.user, blogs: ub})
})
app.get('/login', function(req, res){
res.render('login', { user: req.user, message: req.session.messages });
});
app.post('/login',
passport.authenticate('local', { failureRedirect: '/login'}),
function(req, res) {
userBlogs(req.user.username);
res.redirect('/');
});
});
The end result is supposed to work with this Jade:
extends layout
block content
if blogs
for blog in blogs
h2= blog[title]
h4= blog[author]
p= blog[content]
h4= blog[timestamp]
a(href="/writeblog") Write a new blog
How can I get the query to output an array, or even work as an object?
The toArray function exists on the Cursor class from the Native MongoDB NodeJS driver (reference). The find method in MongooseJS returns a Query object (reference). There are a few ways you can do searches and return results.
As there are no synchronous calls in the NodeJS driver for MongoDB, you'll need to use an asynchronous pattern in all cases. Examples for MongoDB, which are often in JavaScript using the MongoDB Console imply that the native driver also supports similar functionality, which it does not.
var userBlogs = function(username, callback) {
Blog.find().where("author", username).
exec(function(err, blogs) {
// docs contains an array of MongooseJS Documents
// so you can return that...
// reverse does an in-place modification, so there's no reason
// to assign to something else ...
blogs.reverse();
callback(err, blogs);
});
};
Then, to call it:
userBlogs(req.user.username, function(err, blogs) {
if (err) {
/* panic! there was an error fetching the list of blogs */
return;
}
// do something with the blogs here ...
res.redirect('/');
});
You could also do sorting on a field (like a blog post date for example):
Blog.find().where("author", username).
sort("-postDate").exec(/* your callback function */);
The above code would sort in descending order based on a field called postDate (alternate syntax: sort({ postDate: -1}).
Try something along the lines of:
Blog.find({}).lean().exec(function (err, blogs) {
// ... do something awesome...
}
You should utilize the callback of find:
var userBlogs = function(username, next) {
Blog.find({author: username}, function(err, blogs) {
if (err) {
...
} else {
next(blogs)
}
})
}
Now you can get your blogs calling this function:
userBlogs(username, function(blogs) {
...
})

Resources