Need to send response after forEach is done - node.js

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

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
})
}
});

deleteMany with Mongoose using array

I am trying to batch delete using deleteMany via Mongoose. Currently I have a few rows with check-boxes and a submit button which POSTs an array of IDs to my deleteMany endpoint like this,
router.get('/list/batchDelete', secured()).delete(function(req, res) {
Booking.deleteMany(
{
_id: {$in: [req.params.ids]},
},
function(err, rowsToDelete) {
if (!err) {
res.send(rowsToDelete);
res.redirect('/list');
} else {
res.send(err);
console.log('Error in batch delete :' + err);
}
},
);
});
I can see the req.params.ids payload.
POSThttp://localhost:8000/list/batchDelete
[HTTP/1.1 404 Not Found 34ms]
Request payload
ids%5B%5D=5e1da4db2f11682b506fc6c8&ids%5B%5D=5e1da522becbb13f24748012&ids%5B%5D=5e1da57a5c7f911db82e5731
But I keep getting Cannot POST /list/batchDelete
Please, what am I missing?
I referred to:
Mongoose Delete Many by Id
Mongoose Docs: Query.prototype.deleteMany()
UPDATE:
I added a post route like this, which now produces 200OK but in the browser JSON view and still no change to the dataset.
router.post('/list/batchDelete', function(req, res) {
const ids = req.body.ids;
res.send(ids);
res.redirect('/list');
});
Use your route like this
router.post('/list/batchDelete', async (req, res) {
const {ids} = req.body;
await Booking.deleteMany(
{
_id: {$in: ids},
})
return res.send('record deleted');
});
Use postman for api call
Call should be POST and on /list/batchDelete route
Body should contain array of ids
e.g {"ids":['id1','id2']}
This will solve your problem of deleting records.

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) {
...
})

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) {
...
})

How do I pass a populated Mongoose object to the rendering code?

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
});
});
});

Resources