How do I pass a populated Mongoose object to the rendering code? - node.js

In the following code from my routes.js file, I can successfully populate some refs in a Mongoose object called Map. When I view the page, the console prints the fully populated version of popmap's editor objects.
app.get('/map/:id', function(req, res){
Map
.findOne({ _id: req.map._id })
.populate('_editors')
.run(function (err, popmap) {
console.log('The editors are %s', popmap._editors);
});
res.render('maps/show', {
title: req.map.title,
map: req.map
});
});
I have not figured out, though, how to perform the population step such that the resulting object remains in scope for the rendering code. In other words, how can I pass the populated object to the Jade template instead of just sending req.map?

The problem is you are writing the Mongoose code as if it was synchronous, but you need to call res.render inside the run callback function, because that's when the query gets executed. In your example, the render function would get called before the results are returned by the query.
Besides that, you can pass the popmap variable as a local variable to the view:
app.get('/map/:id', function(req, res){
Map
.findOne({ _id: req.map._id })
.populate('_editors')
.run(function (err, popmap) {
console.log('The editors are %s', popmap._editors);
res.render('maps/show', {
title: req.map.title,
map: req.map,
popmap: popmap // you can access popmap in the view now
});
});
});

Note that the populate task is asynchronous, when the task is done, the callback function will be called on the result. Before that, the populated map will not be available.
Therefore, what you need to do is to put the rendering step inside the callback function.
app.get('/map/:id', function(req, res){
Map
.findOne({ _id: req.map._id })
.populate('_editors')
.run(function (err, popmap) {
console.log('The editors are %s', popmap._editors);
res.render('maps/show', {
title: popmap.title,
map: popmap
});
});
});

Related

Using two collection in MongoDB with Mongoose

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

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

mongo on nodejs collection not found

I connect to a database and receive a client.
The next step is to create a collection (table).
db.createCollection("test", function(err, collection){ //I also tried db.collection(...
if(collection!=null)
{
collection.insert({"test":"value"}, function(error, model){console.log(model)});
}
else if(err!=null)
console.log(err);
});
Now I would have created a collection "test" as well as a document(row) "test" in it.
Next is to get the content of the collection:
db.test.find({}); //An empty query document ({}) selects all documents in the collection
Here I get the error: Cannot call "find" of undefined . So, what did I do wrong?
Edit: I connect to the database this way:
var mongoClient = new MongoClient(new Server("localhost", 27017, {native_parser:true}));
mongoClient.open(function(err,mongoclient){
if(mongoclient!=null)
{
var db = mongoclient.db("box_tests");
startServer(db);
}
else if(err!=null)
console.log(err);
});
In the mongo command line you can use db.test.find({}) but in javascript there is no way to replicate that interface so far (maybe with harmonies proxies some day).
So it throws an error Cannot call "find" of undefined because there is no test in db.
The api for the node.js driver of mongodb is like this:
db.collection('test').find({}).toArray(function (err, docs) {
//you have all the docs here.
});
Another complete example:
//this how you get a reference to the collection object:
var testColl = db.collection('test');
testColl.insert({ foo: 'bar' }, function (err, inserted) {
//the document is inserted at this point.
//Let's try to query
testColl.find({}).toArray(function (err, docs) {
//you have all the docs in the collection at this point
});
});
Also remember that mongodb is schema-less and you don't need to create the collections ahead of time. There are few specific cases like creating a capped collection and few others.
If you call db.test.find "next" after the db.createCollection block it ends up being immediately next before db.createCollection succeeds. So at that point, db.test is undefined.
Remember that node is async.
To get the results I believe you are expecting, db.test.find would have to be in the collection.insert callback where you're calling console.log(model).
db.createCollection("test", function(err, collection){
if(collection!=null)
{
// only at this point does db.test exist
collection.insert({"test":"value"}, function(error, model){
console.log(model)
// collection and inserted data available here
});
}
else if(err!=null)
console.log(err);
});
// code here executes immediately after you call createCollection but before it finishes
Checkout the node async.js module. Good writeup here: http://www.sebastianseilund.com/nodejs-async-in-practice

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