How to wait for MongoDB results in multiple nested loops - javascript

I am trying to load a certain page from my website only after the data is loaded from the database. However, it prints the results too early, giving an empty array, and it doesn't group the names at all. I believe this may be solved with async and await functions or callbacks, but I'm new to those and despite what I've searched, I still can't find the solution. So far, I've just been messing around with wrapping certain loops or functions in async functions and awaiting them. Other solutions have been focused on one .find() or other loop. Any help is appreciated! Thank you!
Routes.js:
app.get("/test", async (req, res) => {
var result = [];
users.findById(mongo.ObjectID("IDString"), async (err, res) => {
result = await res.getGroups(users, messages);
});
console.log(result);
//this should print [["name1"]["name1", "name2"],["name3", "name1"]]
res.redirect("/");
});
getGroups:
UserSchema.methods.getGroups = async function(users, messages) {
var result = [];
await messages.find({Group: this._id}, (err, group) => {
for(var i = 0; i < group.length; i++){
var members = group[i].Group;
var subArray = [];
members.forEach((memberID) => {
users.findById(memberID, (err, req) => {
console.log(req.name);
subArray.push(req.name);
console.log(subArray);
});
});
//After function finishes:
console.log(subArray);
result.push(subArray);
subArray = [];
}
});
return result;
};

In your first code snippet, you must set
console.log(result);
//this should print [["name1"]["name1", "name2"],["name3", "name1"]]
res.redirect("/");
in the callback function of findById. Like this:
app.get("/test", async (req, res) => {
var result = [];
users.findById(mongo.ObjectID("IDString"), async (err, res) => {
result = await res.getGroups(users, messages);
console.log(result);
//this should print [["name1"]["name1", "name2"],["name3", "name1"]]
res.redirect("/");
});
});
This is because of asynchronous nature of Javascript. Read the article below
https://medium.com/better-programming/is-javascript-synchronous-or-asynchronous-what-the-hell-is-a-promise-7aa9dd8f3bfb
If you want to use async/await paradigm, you can write this code like this:
app.get("/test", async (req, res) => {
var result = [];
result = await users.findById(mongo.ObjectID("IDString"));
console.log(result);
//this should print [["name1"]["name1", "name2"],["name3", "name1"]]
res.redirect("/");
});
In your second code snippet, you should use await OR callback. Just as explained above.

Related

Pushing to array in asynchonous map

I know this question has already been asked but i just can't manage to make this right. Tried using promises but to no avail. When console logging req.user items is still an empty array. I know I should use promises but I am having trouble implementing it. Help appreciated
app.get('/cart',checkAuthenticated, async (req, res) => {
if(req.user.cart.length > 0){
req.user.items = []
req.user.cart.map(async (item) => {
var itemDescription = await productsModel.findOne({id: item.itemId})
req.user.items.push(itemDescription)
});
console.log(req.user)
}
The reason it's empty it's because you don't await for all the async functions in the map to finish. Try this:
await Promise.all(req.user.cart.map(async (item) => {
var itemDescription = await productsModel.findOne({id: item.itemId})
req.user.items.push(itemDescription)
}));
Note as #jfriend00 has commented this implementation will not guarantee the order of items in req.user.items.
Because you are already using map it's just simpler to do the following and it also guarantees the order of items:
req.user.items = await Promise.all(req.user.cart.map(async (item) => {
var itemDescription = await productsModel.findOne({id: item.itemId})
return itemDescription;
}));
.map() is not promise-aware. It doesn't pay any attention to the promise that your async callback function returns. So, as soon as you hit the await productsModel.findOne(...), that async function returns an unfulfilled promise and the .map() advances to the next iteration of the loop.
There are many different ways to solve this. If you want to use .map(), then you need to pay attention to the promise that your callback is returned like this:
app.get('/cart', checkAuthenticated, async (req, res) => {
if (req.user.cart.length > 0) {
req.user.items = await Promise.all(req.user.cart.map((item) => {
return productsModel.findOne({ id: item.itemId });
}));
console.log(req.user)
}
});
The above implementation will attempt to run all the database lookups in parallel.
A somewhat simpler implementation just uses a plain for loop and runs the database lookups one at a time:
app.get('/cart', checkAuthenticated, async (req, res) => {
if (req.user.cart.length > 0) {
req.user.items = [];
for (let item of req.user.cart) {
req.user.items.push(await productsModel.findOne({ id: item.itemId }));
}
console.log(req.user)
}
});
In your example, the array is still empty because the callback in the map function works asynchronously, hence you need to wait while the code will complete. Because map function returns array of promises, they all need to be settled using Promise.all:
app.get('/cart', checkAuthenticated, async (req, res) => {
if (req.user.cart.length > 0) {
req.user.items = []
const promises = req.user.cart.map(async (item) => {
var itemDescription = await productsModel.findOne({ id: item.itemId })
req.user.items.push(itemDescription)
});
await Promise.all(promises);
console.log(req.user)
}
});
Otherwise, you can replace map function with for loop:
app.get('/cart', checkAuthenticated, async (req, res) => {
if (req.user.cart.length > 0) {
req.user.items = []
for (const item of req.user.cart) {
var itemDescription = await productsModel.findOne({ id: item.itemId })
req.user.items.push(itemDescription)
}
console.log(req.user)
}
});

How to resolve a Promise within a loop function

I am trying to to make several requests that are asynchronous, then push the data obtained into an array by using for loop. However, I encountered a problem that forloop itself doesnt return value which is required to resolve promise.. Also, I would like to use the array for rendering for GET method.
So How do I complete promise within for loop and make sure that the array is filled before I render it for GET method. Sorry for my bad English. I am still new to the asynchronous concept, please teach me in easy words if possible. Also, if there are any better ways to solve the problem I would like to know.
const currencyName = ["btc", "eth", "xrp"];
let dataCollections = [];
for (i = 0; i < currencyName.length; i ++) {
dataCollections.push(new Promise(function(resolve, reject) {
request.get(`https://apiv2.bitcoinaverage.com/indices/global/ticker/${currencyName[i]}jpy`, function(error, response, body) {
if (error) {
reject(error);
} if (dataCollections.length === 3) {
dataColletions = [];
} else {
resolve(JSON.parse(body));
}
});
}));
}
//Promise.all(??).then(??)
//For rendering dataCollections
app.get("/", function(req, res) {
res.render("home", {dataCollections: dataCollections});
}
1) consider naming your array currencyNames plural so that you can name your iterator currencyName.
2) Your for loop is reinventing the array.map function. Consider using array.map instead.
3) Your for loop has an impossible if clause (if (dataCollections.length === 3)) which also means the else clause is on the wrong if statement. I assume this is a typo.
Now to the core of your question, yes Proimse.all(Array<Promise>) is what you want; it resolves when all elements in its param resolve.
const currencyNames = ["btc", "eth", "xrp"];
let dataCollections = currencyNames.map(currencyName => new Promise((resolve, reject) =>
request.get(`https://apiv2.bitcoinaverage.com/indices/global/ticker/${currencyName}jpy`, (error, response, body) => {
if (error)
reject(error);
else
resolve(JSON.parse(body));
})));
//For rendering dataCollections
app.get("/", async (req, res) =>
res.render("home", {dataCollections: await Promise.all(dataCollections)}));
Make the endpoint async:
app.get("/", async function(req, res) {
res.render("home", {dataCollections: await Promise.all(dataCollections) });
});

Get async result in async.filter() array nodejs

Need to parse some XML files from mass array with file_path values.
Try to use async, fs, xml2js.
When use single string file_path all works perfect. But when I use aync.filter() with array I can't understand how I can return result from xml.parseString()
const fs = require('fs');
const xml2js = require('xml2js');
const async = require('async');
var mass=['/file1.xml','/fil2.xml','/file3.xml',...]
async.filter(mass, async function(file_path, callback){
if(fs.statSync(file_path)['size']>0){
fs.readFileSync(file_path, 'utf8', function(err, data) {
xml.parseString(data, function (err, result) {
console.log(Object.keys(result)[0]);
return result; //need get this result to results array
})
})
}
}, function(err, results) {
console.log(results)
});
Who can understand how it works and what I need to change in my code.
Thanks a lot!
You are trying to map and filter at the same time. Since your filter condition is synchronously available, use the array filter method for that, and then pass that to async.map.
You should then call the callback function, that async.map provides to you, passing it the result. So don't return it, but call the callback.
The readFileSync method does not take a callback like its asynchronous counterpart. It just returns the data.
Also, drop the async keyword, as you are not using the await keyword at all.
async.map(mass.filter((file_path) => fs.statSync(file_path).size > 0),
function(file_path, callback){
var data = fs.readFileSync(file_path, 'utf8');
xml.parseString(data, function (err, result) {
console.log(Object.keys(result)[0]);
callback(null, result);
})
}, function(err, results) {
console.log(results)
});
It should be noted however, that since Node now comes with the Promise API, and even the async/await extension to that, the async module has become much less interesting. Consider using Promises.
const promises = mass.filter(file_path => {
return fs.statSync(file_path).size > 0
}).map(function(file_path) {
return new Promise(resolve => {
const data = fs.readFileSync(file_path, 'utf8');
xml.parseString(data, function (err, result) {
console.log(Object.keys(result)[0]);
resolve(result);
});
});
});
Promise.all(promises).then(results => {
console.log(results);
});

Wait for multiple database calls

Having trouble wrapping a loop in a promise
I need to make an array of users before i render my page but i cant seem to figure out how to wrap my database calls
Thanks in advance.
router.get('/friends', auth.isLogged(), (req, res) => {
let friendsList = [];
User.findById(req.user._id,
{
friends: 1,
},
(err, user) => {
user.friends.map(friend => {
User.findById(friend._id).then(doc => {
friendsList.push(doc);
});
});
console.log(friendsList); <-- gets called before the loop is done
});
});
Turn the friends into an array of promises, and await a Promise.all over all of them, like this:
router.get('/friends', auth.isLogged(), (req, res) => {
let friendsList = [];
User.findById(req.user._id, {
friends: 1,
},
async (err, user) => {
const friendsList = await Promise.all(
user.friends.map(({ _id }) => User.findById(_id))
);
console.log(friendsList);
}
);
});
wrap it in Promise.all():
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
Promise.all() doesn't resolve until all the promises are resolved or one i rejected.

res.render ONLY after multiple data queries have finished

On my app.get in my server.js, I am returning a data set from mongo db, and then rendering my page whilst passing the data to it.
as shown bellow:
//page load
app.get('/', (req, res) => {
//find all data in test table
var articles;
db.collection('test').find().toArray((err, result) => {
if (err) return console.log(err)
articles = result
// renders index.ejs and passes result to quotes
res.render('./pages/index.ejs', {
quotes: articles
})
})
})
I want to be able to put multiple db queries, and then pass multiple variables of data to my render, my problem is that when I put my res.render outside of a db query, it tries to render before the db gets its data set.
see what I tried bellow:
//page load
app.get('/', (req, res) => {
//find all data in test table
var articles;
db.collection('test').find().toArray((err, result) => {
if (err) return console.log(err)
articles = result
})
// renders index.ejs and passes result to quotes
res.render('./pages/index.ejs', {
quotes: articles
})
})
my question is:
How could I make sure that the render happens ONLY after my db queries have run and returned the data to a variable?
Ultimately I would want to be able to do something like this:
//page load
app.get('/', (req, res) => {
//find all data in table 1
var AAA;
db.collection('test1').find().toArray((err, result) => {
if (err) return console.log(err)
AAA = result
})
//find all data in table 2
var BBB;
db.collection('test2').find().toArray((err, result) => {
if (err) return console.log(err)
BBB = result
})
//find all data in table 3
var CCC;
db.collection('test3').find().toArray((err, result) => {
if (err) return console.log(err)
CCC = result
})
// renders index.ejs and passes result to quotes
res.render('./pages/index.ejs', {
quotes: AAA,
quotes2: BBB,
quotes3: CCC
})
})
Any help or advice on this is appreciated. Thank you in advance.
Try this code, it's not tested but I think it should work:
app.get('/', (req, res) => {
var list = {};
db.collection('test').find().toArray().then(result => {
list.result = result;
}).then(() => {
return Promise.resolve(db.collection('foo').find().toArray());
}).then(result2 => {
list.result2 = result2;
}).then(() => {
return Promise.resolve(db.collection('bar').find().toArray());
}).then(result3 => {
list.result3 = result3;
}).then(() => {
res.render('./pages/index.ejs', list);
}).catch(e => {
console.error(e);
});
});
Update: we can use async/await for any method that returns Promise so the code would be cleaner:
// you can use express-async-errors package to make routes async
app.get('/', async(req, res) => {
let list = [];
list.result = await db.collection('test').find().toArray();
list.result2 = await db.collection('foo').find().toArray();
list.result3 = await db.collection('bar').find().toArray();
res.render('./pages/index.ejs', list);
});
You can use async module for that. Use async.parallel for functions which are independant of each other and use async.series for dependent processes. Check async.

Categories

Resources