I have a project collection. Under the project schema have a property called tasks which is an array with the ids of the tasks. Now I am trying to write an api for singleProject, which should include the project details with a property taskDetails which will contain the detail of the tasks under the project. And for getting the task details I am using the ids which I stored in the task property under project. Below is the code I am using :
exports.getSingleProject = async (req, res) => {
req.project.logo = undefined;
await req.project.tasks.map((taskItem, index) => {
taskSchema.find({ _id: taskItem.toString() }).exec((err, task) => {
req.project.tasksDetails.push(task);
});
});
responseMessages(
res,
200,
true,
"Single project fetched successfully.",
req.project
);
};
but in the response getting taskDetails as an blank array. I guess I am using async await in wrong place. Need some help on this. Thanks in advance
The await keyword only works with a promise: an array is not a promise, but you can map to a promise and wait for all promises to resolve using Promise.all - see example below:
const tasks = await Promise.all(req.project.tasks.map(item => new Promise((accept, reject) => {
taskSchema.find({ _id: taskItem.toString() }).exec((err, task) => {
if(err) {
reject(err);
return;
}
accept(task);
});
});
The tasks variable would be an array of your tasks, which you can return in the response.
Related
I am currently stuck in asynchronous hell.
In my React, I have a page /menu, that would load data from my mongo instance via expressjs api.
In my database, called menu, i have collections which represent a meal-type eg "breakfast", "lunch" etc. In those collections, the documents for every item looks like this bread collection example:
{
_id: 2398jcs9dn2f9f,
name: "Ciabatta",
desc: "Italian bread",
imageURI: "image01.jpg",
reviews: []
}
This is my api that would be called when the page loads
exports.getAllFoods = (req, res, next) => {
const db = mongoose.connection
const allCollections = {}
try {
db.db.listCollections().toArray((err, collections) => {
collections.forEach((k) => {
allCollections[k.name] = []
})
Object.keys(allCollections).map(k => {
let Meal = mongoose.model(k, MealSchema)
meal = Meal.find((err, docs) => {
allCollections[k] = docs
console.log(allCollections)
})
})
res.send(allCollections)
})
} catch (error) {
console.log(error)
res.send('unable to get all collections')
}
}
The last output of the console.log(allCollections) produces this:
{ snacks:
[ { review: [],
tags: [],
_id: 5fcec3fc4bc5d81917c9c1fe,
name: 'Simosa',
description: 'Indian food',
imageURI: 'image02.jpg',
__v: 0 } ],
breads:
[ { review: [],
tags: [],
_id: 5fcec41a4bc5d81917c9c1ff,
name: 'Ciabatta',
description: 'Italian bread',
imageURI: 'image02.jpg',
__v: 0 } ],
}
This is exactly what I need, but I am stuck in figuring out how to send to React. What am I to do to send the above json? The res.send(allCollections) gives me this:
{
"snacks": [],
"breads": [],
"drinks": []
}
I understand why the above is being sent, but I dont know what I need to do to address it.
This is my React on page load
useEffect(() => {
axios
.get('http://localhost:8888/api/allFoods')
.then((res) => {
setMealTypes(res.data)
})
.catch((err) => [
console.log(err)
])
}, [])
Ultimately, I need the json outputted in console as I wanted to loop through that data and use the key as a title, and then list the values from the value array eg
<div>
<h2>Breads</h2>
<img src=image01.jpg/>
<h3>Ciabatta</h3>
<p>Italian bread</p>
...
</div>
...
I'd appreciate any help, and any docs I should read to help and improve my javascript understandings
I'd prefer to solve this using async/await and Promise.all, replacing most callbacks.
Because you're calling the DB when you're iterating through an array, you have the most annoying callback situation: how do you issue a bunch of async things and then get the results after? You'll need something else to ensure all callbacks are called before sending the results.
Async/await means we can declare a function is async, and await the results of an async operation. async/await is annoying in JS because it abstracts away callbacks and is actually creating a Promise underneath. Complicating things further, async/await doesn't solve issuing multiple async functions, so again we have to rely on this fancy Promise.all() function combined with map-ing the desired input array to async functions.
Original:
Object.keys(allCollections).map(k => {
let Meal = mongoose.model(k, MealSchema)
meal = Meal.find((err, docs) => {
allCollections[k] = docs
console.log(allCollections)
})
});
Suggested async/await:
await Promise.all(Object.keys(allCollections).map(async k => {
let Meal = mongoose.model(k, MealSchema)
let docs = await Meal.find();
allCollections[k] = docs;
console.log(allCollections);
}));
Another advantage is error handling. If any errors happen in the callback of the original example, they won't be caught in this try/catch block.
async/await handles errors like you'd expect, and errors will end up in the catch block.
...
// Now that we have awaited all async calls above, this should be executed _after_ the async calls instead of before them.
res.send(allCollections);
})
} catch (error) {
console.log(error)
res.send('unable to get all collections')
}
}
Technically Promise.all() returns an array of results, but we can ignore that since you're formatting an Object anyway.
There is plenty of room to optimize this further. I might write the whole function as something like:
exports.getAllFoods = async (req, res, next) => {
const db = mongoose.connection.db;
try {
let collections = await db.listCollections().toArray();
let allCollections = {};
collections.forEach((k) => {
allCollections[k.name] = [];
})
// For each collection key name, find docs from the database
// await completion of this block before proceeding to the next block
await Promise.all(Object.keys(allCollections).map(async k => {
let Meal = mongoose.model(k, MealSchema)
let docs = await Meal.find();
allCollections[k] = docs;
}));
// allCollections should be populated if no errors occurred
console.log(allCollections);
res.send(allCollections);
} catch (error) {
console.log(error)
res.send('unable to get all collections')
}
}
Completely untested.
You might find these links more helpful than my explanation:
https://javascript.info/async-await
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
https://medium.com/dailyjs/the-pitfalls-of-async-await-in-array-loops-cf9cf713bfeb
I hope this will help you : You need to first use the stringify method before sending the collections from the express api and then use JSON.parse on the React front end to restore the object.
PS: can you do a console.log(allCollections) one line above res.send(allCollections)?
You need to send it to the front-end in a JSON format.
replace res.send(allCollections) with res.json(allCollections)
This question already has answers here:
Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference
(7 answers)
How do I access previous promise results in a .then() chain?
(17 answers)
Closed 2 years ago.
I'm developing a script that connects with an API, then with the JSON reponse do some operations and then reformat the JSON to send it to another API.
But now I'm stuck in the first step as I can't deal with the first part as my Promises is not working as expected. How can I store the API's response into a variable? For development puropose I stored one API response into a JSON file. This is my code:
declare var require: any;
let url = './data/big buy/big-bui-product-list.json';
const fs = require('fs');
let counter = 0;
const getProductList = () => {
return new Promise((resolve, reject) => {
fs.readFile(url, 'utf8', (err, data) => {
if(err){
return reject (err);
}
else {
return resolve(JSON.parse(data));
}
})
})
}
const getProductStock = () => {
return new Promise((resolve, reject) => {
fs.readFile('./data/big buy/big-bui-product-stock.json', 'utf8', (err, data) => {
if(err) {
return reject(err);
}
else {
return resolve(JSON.parse(data));
}
})
})
}
try {
let products;
console.log('Products:');
Promise.all([getProductList()])
.then(function(result) {
products = result[0];
});
console.log('Stocks:');
const productStock = Promise.all([getProductStock()]);
console.log(products);
}
catch(e) {
console.log((`Ha ocurrido un error: ${e.message}`));
}
finally {
}
In this code, what I do is getting a list of products and then get the stocks of all the products, later I will add a new function that will filter by stock and get me just a list of products where stock is bigger than X units. Now when I launch it from the terminal I dont' get the response stored into products variable but if I add .then((data) => console.log(data)) into the Promise I see on screen the JSON but as I dont' have it stored it in any variable I don't see how can I work with the objects I'm retrieving.
Promises are asynchronous. They are quite literally promises that a value will be yielded in the future. When you do getProductList().then(x => products = x), you're saying that Javascript should fetch the product list in the background, and once it's finished, it should set the products variable to x. The key words there are "in the background", since the code afterwards keeps running while the product list is being fetched. The products variable is only guaranteed to be set after the .then portion is run. So, you can move things into the .then:
try {
let products;
getProductList().then(list => {
products = list;
return getProductStock(); // leverage promise chaining
}).then(stocks => {
console.log("Products:", products);
console.log("Stocks:", stocks);
});
}
catch(e) {
console.log((`Ha ocurrido un error: ${e.message}`));
}
finally {
}
You seem to be missing some fundamental knowledge about promises. I suggest reading through the MDN Using Promises guide to familiarize yourself a bit with them.
A structure like below never works.
let variable;
promise.then(data => variable = data);
console.log(variable);
This is because it is executed in the following manner.
Create the variable variable.
Add a promise handler to the promise.
Log variable.
Promise gets resolved.
Execute the promise handler.
Set variable variable to data.
If you are using Node.js 10 or higher you can use the promise API of file system to simplify your code quite a bit.
const fs = require('fs/promises');
const readJSON = (path) => fs.readFile(path, "utf8").then((json) => JSON.parse(json));
const getProductList = () => readJSON("./data/big buy/big-bui-product-list.json");
const getProductStock = () => readJSON("./data/big buy/big-bui-product-stock.json");
Promise.all([
getProductList(),
getProductStock(),
]).then(([products, stocks]) => {
console.log({ products, stocks });
}).catch((error) => {
console.log("Ha ocurrido un error:", error.message);
}).finally(() => {
// ...
});
I have two schemas, User, and Product. The User schema is where I store all the product ID and the number of items that the user added to the cart.
When the user makes a request to '/checkout' it should update the quantity and then remove it from the cart. I am having an issue when I checkout the quantity is not updating the quantity.
router.post('/checkout', auth, catchAsync(async (req, res) => {
const user = await User.findById(req.session.userId);
const err = [];
if (user && user.products.length > 0) {
user.products.map(async (product) => {
let total = 0;
const p = await Product.findById(product.productID);
if (p.quantity > 0 && p.quantity > product.quantity) {
console.log('IN');
total = p.quantity - product.quantity;
console.log(total);
await Product.findOneAndUpdate({ _id: product.productID }, { $set: { quantity: total } });
} else {
err.push(`Item, ${p.name} is sold out`);
}
});
await User.findOneAndUpdate({ _id: req.session.userId }, { $set: { products: [] } });
if (err.length) {
return res.status(500).json({ message: err });
}
return res.status(200).json({ message: 'OK' });
}
return res.status(200).json({ message: 'Empty cart' });
}));
User Schema:
Product Schema:
I believe the problem in your code is at the user.products.map(...) function, because you never wait for all the promises you create in the map to resolve.
In other words, the map function returns an array of pending promises, but it will not wait for them to be done, and therefore the execution continues through the rest of the code reaching the res.status(...) before any of the code in map had been executed.
You have different options to solve it, but mainly you need to take care of the array of promises returned by the map function and wait for their completion, before you end your code. There is a very good explanation of how to handle this situation with async/await at Google Developers Web fundamentals guide.
I usually leverage Promise.all() function, which returns a single promise from the array of promises, and therefore you can wait until the code in map is executed in parallel for each item in the array (i.e. product in your case). You can read more about it at MDN documentation.
// ...
let promisesArray = user.products.map(async product => {...});
// promisesArray should look like: [Promise { <pending> }, Promise { <pending> }, … ]
// Using Promise.all we wait for each of them to be done in parallel
await Promise.all(promisesArray);
// Now you are certain the code in the map has been executed for each product
// ...
A good practice as well is to use try {} catch(err) {} block around the Promise.all() to handle cases of some promise being rejected.
I've been digging into redis and working on a tiny webapp that uses redis as it's only data storage (I know it's not the intended purpose of redis but I am benefitting from learning the commands and just overall working with redis on Node. I'm using Node_Redis.
Here's what I want to accomplish (all in redis):
I am try to retrieve the users using their emails.
Here's the problem:
I have a Promise.all call which takes all emails (keys) and maps each to go a HGET command. When the Promise.all resolves I expect it to resolve with an array of user objects, but it ends up resolving to an array of booleans (i.e. [true, true, true]).
Here is the logic for /users
router.get("/", (req, res) => {
client.lrange("emails", 0, 1, (err, reply) => {
if (err) return console.log(err);
if (reply) {
Promise.all(
reply.map(email => {
return client.hgetall(email, (err, reply) => {
if (err) return console.log(err);
console.log("reply for hgetall", reply); // this prints a user object correct, but Promise.all still resolves to boolean array :(
return reply;
});
})
)
.then(replies => {
// replies = [true, true, true ...]
res.send(replies);
})
.catch(e => console.log(e));
} else {
res.send([reply, "reply is null"]);
}
});
});
I've actually used Promise.all plenty of times, and when I log the reply from redis, it shows the correct object too, so I'm pretty confused at this point. How can I get Promise.all to resolve to an array of User objects?
The problem is that client.hgetall doesn't return a promise. It's async function and you pass a callback to get a result. You should promisify this function to use it in Promise.all:
...
return new Promise((resolve, reject) => {
client.hgetall(email, (err, reply) => {
if (err) {
return reject(err);
}
resolve(reply);
});
});
You can do promisification manually (as in example above) or can use Bluebird or Q libraries with their promisify and promisifyAll methods.
I currently have a project which makes use of native Promises. I am looking to migrate these Promises over to Async/Await. I am having troubles migrating them over; I am trying to follow this article. Below is the current code with the Promises which need changing to Async/Await.
routes.js
// Importing user information
import user from '../server-controllers/user';
// User Information Route
router.get('/about', (req, res) => {
user.profile().then((data) => {
return res.render('user', {
title: data,
});
}).catch((e) => {
res.status(500, {
error: e,
});
});
});
user.js
/*
This file contains any server side modules needed.
*/
module.exports = {
// Returns information about a user
profile: () => {
return new Promise((resolve, reject) => {
const user = "John Doe";
resolve(user);
});
},
};
If there is any help in what I would need to do to convert these that would be helpful. I don't know if the code needs changing on either the routes or user file (or both).
The error which I am getting in my terminal is [object Error] { ... }
The key thing to remember about async and await is that an async function is really just a function that returns a Promise, and lets you use await to resolve Promises. So when a Promise rejects, if it is awaited then you will get an error thrown in the context of wherever the await is.
So technically, if you want to use async/await syntax, you don't need to change user.js. You could just change routes.js to:
// Importing user information
import user from '../server-controllers/user'
// User Information Route
router.get('/about', async (req, res) => {
try {
const data = await user.profile()
return res.render('user', {
title: data
})
} catch (error) {
// Runs if user.profile() rejects
return res.status(500, {error})
}
})
But user.js is made more succinct when you use an async function:
module.exports = {
// Returns information about a user
// This returns a Promise that resolves to 'John Doe'
profile: async () => 'John Doe'
}