How to update an array in MongoDB using mongoose? - javascript

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.

Related

async await not working properly in mongo query

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.

Fetching data from mongo via express, build object, and send to React

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)

async function returns Promise <Pending>

i'm building a crud app for a project,
people can add view and delete entries, i'm using nodejs, js and fireabse for this.
i have such a firestore database structure:
entries:
--entry id
--entry data
users:
--user id
--user email (query)
-- my entries (collection)
-- entries id
--entry id
now i want to display all users entries,
so i created this module.exports function:
module.exports = {
//get all the users entries from database with passed in uid ('/dashboard')
getUserEntries: async uid => {
//get all his entries docs id from "myEntries" collection in an, array
const usersEntriesId = await db
.collection("users")
.doc(uid)
.collection("myEntries")
.get()
.then(entries => {
return entries.docs.map(entry => entry.data().entry);
})
.catch(err => console.log(err));
console.log(usersEntriesId); //works fine, logs array with ids
const userEntriesDocs = usersEntriesId.map(async id => {
const entry = await db
.collection("entries")
.doc(id)
.get()
.then(entry => {
return entry.data();
});
console.log("hello:", entry); //works fine returns logs entry data
return entry;
});
console.log("hello1: ", userEntriesDocs); //doesnt work i get "hello1: [ Promise { <pending> },
// Promise { <pending> },
//Promise { <pending> } ]"
//i got three entries, that's why i get 3 times "Promise { <pending> }"
}
};
so how do i resolve that?
Thanks
well, async function returns Promise, it how they works under the hood. If there was no .map you could just await on that function or use it as arbitrary Promise with .then() and .catch. But since there is array of Promise you need Promise.all to wait until all are resolved.
const userEntriesDocs = await Promise.all(usersEntriesId.map(async id => {
....
);
Beware: unlike .allSettled, .all() will fail immediately if any of subsequent Promise fails. So if for any reason you want to have data from those requests that succeeded, you need more complex logic.
As an alternative you may go through loop manually:
const userEntriesDocs = [];
for(const docPromise of userEntriesId.map(.....)) {
userEntriesDocs.push(await docPromise);
}
But to me await Promise.all[...] is more readable.
Also I highlight there is array of Promises(requests have already been sent). If you try sending requests inside the loop like
const userEntriesDocs = [];
for(const id of userEntriesId) {
userEntriesDocs.push(await db
.collection("entries")
.doc(id)
.get()
.then(entry => {
return entry.data();
})
);
}
you will find that requests are going strictly one-by-one, not in parallel. That will require much more time to process the list.

NodeJS with MariaDB, Cannot read property 'threadId' of undefined

I'm quite desperate, because of this annoying error. I can't find any information about that on the internet.
Actually my application works fine, but if I start multiple queries and wait for them with Promise.all(), the server crashes. But step by step:
In my index.js, I'm initializing the connection pool with 60 max. connections, and exporting the variable.
const pool = mariadb.createPool(config.mariaDB);
module.exports.pool = pool
Later I'm importing the index file as "app" and using app.pool.query(...) to query my database.
Next, I have a post request /getUsersGroups. A User can be a member of multiple groups, so I expect the response to be an array of objects. Each object contains information about the group. Moreover there is a members field inside this object. This field also is an array containing information about the members, so s.th. like that:
[
{
"groupId": 125758,
"title": "test",
"members": [
{userId: 5, name:Max, }, ...]
}, ...
]
Therefore I have a post request, which looks like that. I'm getting all group ids the user is a member. Then for each groupId, I call the method Group.groupInfo.
This returns a promise, therefore I'm collecting all promises in an array and wait for all of them, before sending the response.
router.post("/getUsersGroups", (req, res) => {
let userId = req.body.userId;
let result = []
let promises = []
Group.getGroupsByUser(userId) // this gets all group ids the user is a member
.then((item) => {
let groups = item.map(x => objectParser.parseUnderscoreToCamel(x))
for (let i = 0; i < item.length; i++) {
let groupId = item[i].groupId
promises.push( //adding promises
Group.groupInfo(groupId, userId)
.then((item) => {
result.push(item)
})
.catch((err)=>{console.log(err)}))
}
})
.then(() => {
// waiting for all promises to finish
return Promise.all(promises)
.then(() => {
res.status(200).json({groups: result})
})
})
.catch((err) => {
console.log(err)
}
})
}
So the Promise Group.groupInfo looks like this. It first selects the group information and then for each member inside the group it calls another Promise User.userInfo. The pattern is the same like in the method above. So I push the Promise to an array and wait for them to finish:
module.exports.groupInfo = (groupId, userId)=>{
let groupInfo = {}
let promises = []
return new Promise(((resolve, reject) => {
app.pool.query("SELECT* FROM Groups WHERE group_id=?", [groupId])
.then((item) =>{
groupInfo = item[0]
groupInfo["members"] = [];
return app.pool.query("SELECT user_id FROM Users_groups WHERE group_id=?", [groupId])
.then((item) =>{
let members = [] //contains all user ids
for(let i=0; i<item.length;i++){
members.push(item[i].userId)
}
members.forEach((memberId)=>{
// for each memberID call User.userInfo
promises.push(User.userInfo(userId,memberId)
.then((item)=>{
groupInfo.members.push(item)}))
}
})
})
})
.then(()=>{
// wait for all Promises
return Promise.all(promises)
})
.then(() =>{
return resolve(groupInfo)
})
.catch((err)=>{
return reject(err)
})
}))
}
User.userInfo itself, makes a query to the Database to collect the information about the user.
If I'm calling this, the server crashes and giving me this error:
activeConnections[conn.threadId] = conn;
TypeError: Cannot read property 'threadId' of undefined
at Pool.handleTaskQueue (****\node_modules\mariadb\lib\pool.js:431:30)
at _combinedTickCallback (internal/process/next_tick.js:132:7)
at process._tickCallback (internal/process/next_tick.js:181:9)
I guess the problem is somehow with the Promise.all() right?
But I cannot find any information about this error. I'm thankful for every hint!
My solution was not to use Promise.all but to call the Promises sequentially, but seems not right to me.
return groupsId.reduce((promise, groupId) => {
return promise.then(() => {
return Group.groupInfo(groupId, userId)
.then((item)=>{
result.push(item)
console.log(result)
})
.catch((err)=>{
throw err
})
})
}, Promise.resolve())
I cannot rely on this, as long as I don't understand the error.
Thanks!

Node.js Wait for nested promises with Promise.all

I'm trying to wait for all of my database calls in a loop to complete before I proceed. I add each call, which are promises, to an array and then use Promise.all() to wait for all of them. However, there are nested DB calls like user.increment() or user.create() which Promise.all() does not seem to wait for. The output of this snippet usually goes:
User found, incrementing wins...
Promise.all()
User updated
The Promise.all block is being run before the nested DB calls complete. I feel like I'm missing something really easy.
let dbCalls = [];
for(var i = 0; i < users.length; i++){
let messageUser = users[i];
//try to find user by id
dbCalls.push(db.user.findOne({
where: {
id: messageUser.id
}
}).then(function(user){
if(user){
//found user, increment them
user.increment('wins').then((user) => {
console.log('User found, incrementing wins...');
user.reload();
}).then(user => {
console.log('User updated')
return user;
});
} else{
//user wasn't found, create them
console.log(`${messageUser.username} was not found, creating user...`);
db.user.create({
username: messageUser.username,
id: messageUser.id,
wins: 1
}).then(function(user){
console.log('created user');
return user;
});
}
}).then(res => {
return res;
})
);
}
Promise.all(dbCalls).then(res =>{
console.log('Promise.all()' + res);
});
You forgot to return that promises. So they are not included in your promise chain
Just change lines with db-promises into return user.increment('wins').then((user) => {
Since you are running multiple DB queries that require one to finish before the other one, you should look in NODE.js waterfall calls. It allows promises to be used and you can set which ones require other to finish first before firing.
https://www.npmjs.com/package/promise-waterfall
there is one good example
This library goes as far as even allowing you to wait for a returned value to use in another async call.
You need to chain all of the internal promises to that the item that gets pushed to the array is the full chain. For better readability, consider extracting the increment and create to their own function:
const increment = user => user.increment('wins').then((user) => {
console.log('User found, incrementing wins...');
return user.reload();
}).then(user => {
console.log('User updated')
return user;
});
const create = user => {
//user wasn't found, create them
console.log(`${messageUser.username} was not found, creating user...`);
return db.user.create({
username: messageUser.username,
id: messageUser.id,
wins: 1
}).then(user => {
console.log('created user');
return user;
});
};
const dbCalls = users.map(messageUser => db.user.findOne({
where: {
id: messageUser.id
}
}).then(user => (
user
? increment(user)
: create(user)
)));
Promise.all(dbCalls).then(res =>{
console.log('Promise.all()' + res);
});

Categories