How can you resolve promises in multiple nested forEach loops? - javascript

For a "flights" portfolio app, I am trying to generate tickets for each seat in a plane (so that flights are prepopulated with some people). This is part of a function that also prepopulates flights to the database to set up the data. There are calls to the database to check to see if the Flight Number, as well as Confirmation Numbers are unique, and not pre-existing. In the case of flights, if the flight number already exists, the flight just gets dropped. However, I am running into some issues with the asynchronous aspect of regenerating a confirmation number if one already exists when it is in loops. So essentially, the issues I'm having:
generateTickets returns an empty tickets array
I am not sure how to structure my nested forEach loops to return promises when generating the tickets for each seat, in each row. (In my searches on Stack Overflow, I have found multiple solutions referencing Promise.all with array.map. I've tried to implement these solutions in a variety of ways, but I'm still getting an empty array, and I think I'm just not understanding it well enough.)
I am getting: SyntaxError: Unexpected reserved word on await generateConfirmationNumber() - even though generateConfirmationNumber is an asynchronous function
I don't know what to say about what I've tried. It's a lot. I've searched through every link for the first 2-3 pages of Google, I've scoured Stack Overflow trying to find solutions, and then trying to implement them. I think I'm just having a hard time interpreting how their code works, when this has additional complexity. The solutions I have on my browser tab currently are:
Why doesn't the code after await run right away? Isn't it supposed to be non-blocking?
Pushing Elements into an Array That Is Inside a Callback Function
How to wait a Promise inside a forEach loop
Why does my async function return an empty array
Best way to wait for .forEach() to complete
Using async/await with a forEach loop
How to return values from async functions using async-await from function?
I've been googling and trying to get this to work for 3 days now, and I figured it was time to ask a question. :P So on to the code! (If you see crazy all caps comments, it just helps me notice them easily in the console.)
createFlight - Called by router, starts everything.
async function createFlight(req, res) {
create_flights().then(() => {
console.log('Done')
res.redirect('/')
})
}
create_flights - Async Function that generates a random flight.
const create_flights = async() => {
let flights_array = []
let flights_with_tickets = []
for(let i = 0; i < 1; i++) {
let airline = randomAirline();
let origin = randomAirport();
let destination = randomDestination(origin.code);
let duration = generateDuration()
let departure = generateDeparture()
let departure_date = dateString(departure)
let flight_data = {
number: generateFlightNumber(airline.code),
airline: airline.name,
plane: {
name: randomPlane()
},
origin: origin,
destination: destination,
departure: departure,
departure_date: departure_date,
duration: duration,
gate: generateGate(),
amenities: randomAmenities(duration),
tickets: []
}
const flight = new Flight(flight_data);
flights_array.push(flight)
}
console.log("FLIGHTS ARRAY")
console.log(flights_array)
for(let flight of flights_array) {
console.log(flight)
const flight_with_ticket = await returnTicketsAsync(flight)
console.log(flight_with_ticket)
if(flight_with_ticket) {
const record = await returnFlight(flight_with_ticket)
console.log(record)
if(record) {
console.log("Created Flight")
console.log(record.tickets[0])
}
}
}
}
returnTicketsAsync - Waits for the tickets to be generated. This is always console logging an empty tickets value.
async function returnTicketsAsync(flight) {
console.log("ASYNC TICKETS?!?!?!!?")
let tickets = await generateTickets(flight)
console.log(tickets)
}
returnFlight - This checks to see if the flight's confirmation number is already in the database. If it is, we just skip this flight and keep moving on. Otherwise, we save it to the database.
const returnFlight = flight => {
return new Promise((resolve, reject) => {
Flight.find({$or:[{number: flight.number}]}, function(error, records) {
if(records.length) {
console.log("Similar flight already exists")
resolve()
} else {
flight.save(function (error, result) {
if(error) {
console.log("ERROR:" + error)
resolve()
} else {
resolve(result)
console.log("Flight " + result.number + " successfully saved.")
}
})
}
})
})
}
generateTickets(flight) - The source of all my troubles. The Flight has a plane map, with sections (first class, preferred class, economy). Each section has rows with seats.
So I need to loop through first class, preferred class, and economy class, generate the tickets for all the seats. There is one function, generateConfirmationNumber() that generates the number, and checks the database for a result. If there's no result, the confirmation number is good to be used, and it returns that. I need to await for that to be done, but I'm getting a SyntaxError here.
async function generateTickets(flight) {
let tickets = []
let prices = generatePrice()
const first_class = await Promise.all(_.map(flight.plane.sections.first_class.rows, async function (row, i, rows) {
let first_class_tickets = await _.map(row, function(seat) {
if(!seat.isle) {
let passenger = generatePassenger()
let confirmation_number = await generateConfirmationNumber()
let tickets = {
confirmation_number: '',
tracking_number: generateTrackingNumber(),
data: generateDataNumber(),
options: '1ST CL',
flight_class: 'first',
seat: {
isle: seat.letter,
number: i + 1
},
boarding_zone: generateBoardingZone(),
price: prices.first,
passenger: passenger
}
if(passenger.first_name !== '' || passenger.first_name.length > 1) {
ticket.confirmation_number = confirmation_number
}
console.log(ticket)
tickets.push(ticket)
}
})
}))
const preferred_class = await Promise.all(_.map(flight.plane.sections.preferred_class.rows, async function (row, i, rows) {
let preferred_class_tickets = await _.map(row, function(seat) {
if(!seat.isle) {
let passenger = generatePassenger()
let confirmation_number = await generateConfirmationNumber()
let tickets = {
confirmation_number: '',
tracking_number: generateTrackingNumber(),
data: generateDataNumber(),
options: 'PREF PLUS',
flight_class: 'preferred',
seat: {
isle: seat.letter,
number: i + flight.plane.sections.first_class.total_rows + 1
},
boarding_zone: generateBoardingZone(),
price: prices.preferred,
passenger: passenger
}
if(passenger.first_name !== '' || passenger.first_name.length > 1) {
ticket.confirmation_number = confirmation_number
}
console.log(ticket)
tickets.push(ticket)
}
})
}))
const economy_class = await Promise.all(_.map(flight.plane.sections.economy_class.rows, async function (row, i, rows) {
let economy_class_tickets = await _.map(row, function(seat) {
if(!seat.isle) {
let passenger = generatePassenger()
let confirmation_number = await generateConfirmationNumber()
let tickets = {
confirmation_number: '',
tracking_number: generateTrackingNumber(),
data: generateDataNumber(),
options: 'PREF PLUS',
flight_class: 'preferred',
seat: {
isle: seat.letter,
number: i + flight.plane.sections.first_class.total_rows + flight.plane.sections.preferred_class.total_rows + 1
},
boarding_zone: generateBoardingZone(),
price: prices.economy,
passenger: passenger
}
if(passenger.first_name !== '' || passenger.first_name.length > 1) {
ticket.confirmation_number = confirmation_number
}
console.log(ticket)
tickets.push(ticket)
}
})
}))
console.log("Tickets")
console.log(ticekts)
}
generateConfirmationNumber - Just throwing this here, too.
async function generateConfirmationNumber() {
let letters = 'ABCDEFGHJKLMNPRSTUVWXYZ'
let number = Math.floor(Math.random() * 10000)
let confirmation = letters[Math.floor(Math.random() * letters.length)] + letters[Math.floor(Math.random() * letters.length)] + letters[Math.floor(Math.random() * letters.length)] + number
return new Promise((resolve, reject) => {
Flight.findOne({'tickets.confirmation_number': confirmation}, (err, result) => {
if (err) console.log(error)
else if (result) return resolve(generateConfirmationNumber())
else return resolve(confirmation)
})
})
}
I'm at a loss for how to get this working. The goal is simply to generate the tickets, wait for the generateConfirmationNumber() to finish, and push all the tickets into a "tickets" array for that flight. Then, return that array so it can be $set to the flight, and so then, the flight can be pushed into the database.

The main issue your codes doesn't work because you try to Promise.all() to a array of the ticket below.
Promise.all only working on promises object, but you can see the ticket itself is not a purely Promise object. Also, the map() function will not await the promise in the loop. If you want to await a promise in a loop, you need to use for loop.
let confirmation_number = await generateConfirmationNumber();
let ticket = {
confirmation_number: confirmation_number,
tracking_number: generateTrackingNumber(),
data: generateDataNumber(),
options: "1ST CL",
flight_class: "first",
seat: {
isle: seat.letter,
number: i + 1,
},
boarding_zone: generateBoardingZone(),
price: prices.first,
passenger: generatePassenger(),
};
The below codes doesn't work because ticket is not a promise object.
Promise.all([ticket1,ticket2,ticket3]);
To make it work, you have two options.
Promises in Parallel: Promise.all all the promises object and use array#map() to create a array of ticket objects.
Promises in Series: await each promise and construct the ticket one by one.
If you want to do a Promises in Parallel way, you can do like below:
const arrayOfPromises = flight.plane.sections.first_class.rows.flatMap(row =>
row.map(seat => generateConfirmationNumber())
);
const confirmationNumbers = await Promise.all(arrayOfPromises);
const tickets = confirmationNumbers.map(cfmNumber => ({
confirmation_number: cfmNumber,
tracking_number: generateTrackingNumber(),
data: generateDataNumber(),
options: "1ST CL",
flight_class: "first",
seat: {
isle: seat.letter,
number: i + 1,
},
boarding_zone: generateBoardingZone(),
price: prices.first,
passenger: generatePassenger(),
}));
The answer I use is 2nd option(Promises in Series).
I use for of loop to await each promise and create a ticket from it.
In the end, I add ticket to a array tickets.
async function generateTickets(flight) {
let tickets = [];
let prices = generatePrice();
const firstClass = [];
for (const row of flight.plane.sections.first_class.rows) {
for (const seat of row) {
if (!seat.isle) {
const passenger = generatePassenger();
const confirmation_number = await generateConfirmationNumber();
const ticket = {
confirmation_number: "",
tracking_number: generateTrackingNumber(),
data: generateDataNumber(),
options: "1ST CL",
flight_class: "first",
seat: {
isle: seat.letter,
number: flight.plane.sections.first_class.rows.indexOf(row) + 1,
},
boarding_zone: generateBoardingZone(),
price: prices.first,
passenger: passenger,
};
if (passenger.first_name !== "" || passenger.first_name.length > 1) {
ticket.confirmation_number = confirmation_number;
}
firstClass.push(ticket);
}
}
}
console.log(firstClass);
const preferredClassTickets = [];
for (const row of flight.plane.sections.preferred_class.rows) {
for (const seat of row) {
if (!seat.isle) {
let passenger = generatePassenger();
let confirmation_number = await generateConfirmationNumber();
let ticket = {
confirmation_number: "",
tracking_number: generateTrackingNumber(),
data: generateDataNumber(),
options: "PREF PLUS",
flight_class: "preferred",
seat: {
isle: seat.letter,
number: flight.plane.sections.preferred_class.rows.indexOf(row) + flight.plane.sections.first_class.total_rows + 1,
},
boarding_zone: generateBoardingZone(),
price: prices.preferred,
passenger: passenger,
};
if (passenger.first_name !== "" || passenger.first_name.length > 1) {
ticket.confirmation_number = confirmation_number;
}
preferredClassTickets.push(ticket);
}
}
}
const economyClass = [];
for (const row of flight.plane.sections.economy_class.rows) {
for (const seat of row) {
if (!seat.isle) {
let passenger = generatePassenger();
let confirmation_number = await generateConfirmationNumber();
let ticket = {
confirmation_number: "",
tracking_number: generateTrackingNumber(),
data: generateDataNumber(),
options: "PREF PLUS",
flight_class: "preferred",
seat: {
isle: seat.letter,
number:
flight.plane.sections.economy_class.rows.indexOf(row) +
flight.plane.sections.first_class.total_rows +
flight.plane.sections.preferred_class.total_rows +
1,
},
boarding_zone: generateBoardingZone(),
price: prices.economy,
passenger: passenger,
};
if (passenger.first_name !== "" || passenger.first_name.length > 1) {
ticket.confirmation_number = confirmation_number;
}
economyClass.push(ticket);
}
}
}
tickets.push(...firstClass, ...preferredClassTickets, ...economyClass);
return tickets;
}
async function generateConfirmationNumber() {
let letters = "ABCDEFGHJKLMNPRSTUVWXYZ";
let number = Math.floor(Math.random() * 10000);
let confirmation =
letters[Math.floor(Math.random() * letters.length)] +
letters[Math.floor(Math.random() * letters.length)] +
letters[Math.floor(Math.random() * letters.length)] +
number;
return new Promise((resolve, reject) => {
Flight.findOne(
{ "tickets.confirmation_number": confirmation },
(err, result) => {
if (err) reject(error);
// else if (result) return resolve(generateConfirmationNumber());
else if (result) return resolve(result);
else return resolve(confirmation);
}
);
});
}
const returnFlight = flight => {
return new Promise((resolve, reject) => {
Flight.find(
{ $or: [{ number: flight.number }] },
function (error, records) {
if (error) reject(error);
if (records.length) {
console.log("Similar flight already exists");
resolve(records);
} else {
flight.save(function (error, result) {
if (error) {
console.log("ERROR:" + error);
reject(error);
} else {
console.log("Flight " + result.number + " successfully saved.");
resolve(result);
}
});
}
}
);
});
};
const create_flights = async () => {
let flights_array = [];
// let flights_with_tickets = [];
for (let i = 0; i < 1; i++) {
let airline = randomAirline();
let origin = randomAirport();
let destination = randomDestination(origin.code);
let duration = generateDuration();
let departure = generateDeparture();
let departure_date = dateString(departure);
let flight_data = {
number: generateFlightNumber(airline.code),
airline: airline.name,
plane: {
name: randomPlane(),
},
origin: origin,
destination: destination,
departure: departure,
departure_date: departure_date,
duration: duration,
gate: generateGate(),
amenities: randomAmenities(duration),
tickets: [],
};
const flight = new Flight(flight_data);
flights_array.push(flight);
}
console.log("FLIGHTS ARRAY");
console.log(flights_array);
for (let flight of flights_array) {
const tickets = await generateTickets(flight);
if (tickets) {
flight.set({
tickets: tickets
})
const record = await returnFlight(flight);
console.log(record);
if (record) {
console.log("Created Flight");
console.log(record.tickets);
}
}
}
};
async function createFlight(req, res) {
try {
await create_flights();
console.log("Done");
res.redirect("/");
} catch (err) {
console.log(err);
}
}

Related

Item doesn't want to be pushed into an array

I am developing a discord bot that allows you to mine for resources, but I can't get the code to work. It's logging the actual items, but it just doesn't add the items to the array, which is why I can't map it into a string.
Code:
module.exports.addItem = async (client, user, item, amount, interaction) => {
if(!client instanceof Client) throw new Error("Client is not an instance of Discord.js Client");
if(!user instanceof User) throw new Error("User is not an instance of Discord.js User");
if(!item) throw new Error("Item is not defined");
if(!amount) throw new Error("Amount is not defined");
if(typeof amount !== "number") throw new Error("Amount is not a number");
if(!interaction) throw new Error("Interaction is not defined");
if(!interaction instanceof Interaction) throw new Error("Interaction is not an instance of Discord.js Interaction");
const userFound = await client.database.user.findOne({ userID: user.id });
if(!userFound) return interaction.followUp({ content: `You haven't started your journey yet!` });
const itemFound = userFound.inventory.find(i => i.name === item.name);
if(!itemFound) {
await client.database.user.findOneAndUpdate(
{
userID: user.id,
}, {
userID: user.id,
$push: {
inventory: {
name: item.name,
amount,
category: item.category
}
}
} , {
upsert: true,
});
} else {
await client.database.user.findOneAndUpdate(
{
userID: user.id,
}, {
userID: user.id,
$pull: {
inventory: {
name: item.name
}
}
} , {
upsert: true,
});
await client.database.user.findOneAndUpdate(
{
userID: user.id,
}, {
userID: user.id,
$push: {
inventory: {
name: item.name,
amount: item.amount + amount,
category: item.category
}
}
} , {
upsert: true,
});
}
return { name: item.name, amount, category: item.category };
}
module.exports.dropItem = async (client, user, category, interaction) => {
if(!client instanceof Client) throw new Error("Client is not an instance of Discord.js Client");
if(!user instanceof User) throw new Error("User is not an instance of Discord.js User");
if(!category) throw new Error("Category is not defined");
if(typeof category !== "string") throw new Error("Category is not a string");
let droppedItems = [];
for (const item of client.items) {
for(let i = 0; i < client.items.length; i++) {
if(!client.items[i].category === category) return;
}
const dropAmount = Math.floor((Math.random() * 3) + 1);
const dropChance = Math.floor(Math.random() * 100);
if(dropChance > item.dropChance) return;
const itemDropped = await this.addItem(client, user, item, dropAmount, interaction)
console.log(itemDropped); // This logs the item correctly
droppedItems.push(itemDropped);
}
return droppedItems; // This always returns "undefined"
}
Expected result: An array with objects, each object containing a name, amount and category value;
Actual result: Console-logging the array returns "undefined"
I have tried awaiting the push, making the array a constant and more small things like that.
Could it be because the array isn't in the loop?
Since you are awaiting a call within the for loop, you will need to await the entire loop as well. You can do this with await Promise.all(<<ARRAY OF PROMISES>>).
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
So you'd do something like
let droppedItems = [];
await Promise.all(client.items.map(async (item) => {
for(let i = 0; i < client.items.length; i++) {
if(!client.items[i].category === category) return;
}
const dropAmount = Math.floor((Math.random() * 3) + 1);
const dropChance = Math.floor(Math.random() * 100);
if(dropChance > item.dropChance) return;
const itemDropped = await this.addItem(client, user, item, dropAmount, interaction)
console.log(itemDropped); // This logs the item correctly
droppedItems.push(itemDropped);
}));
The problem is probably that you have several return statements in your dropItem function that are returning undefined.
For example if(dropChance > item.dropChance) return;
A simple function to illustrate this is:
const doSomething = () => {
const arr = [1, 2, 3]
for (const num of arr) {
if (num !== 10) return
}
return 'done'
}
You are saying that you would expect the above function to return "done", but it will actually return undefined.
I think maybe you want to use break instead of return, but not sure exactly what behaviour you are expecting.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/break

How to convert JavaScript functions that use multiple callbacks, to use promises?

I am trying to convert the way I use asynchronous functions from callbacks to promises.
I understand this basic conversion shown here, where callbacks are converted to the resolve and reject functions:
// CALLBACK
const getData = (id, callback) => {
setTimeout(() => {
if (!id) return callback('ERROR: id is missing')
return callback("The data for id " + id);
}, 1000)
};
getData(111, console.log);
getData(222, console.log);
getData(null, console.log);
// PROMISE
const getData2 = id => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (!id) reject('id is missing');
resolve("The data for id " + id);
}, 1000);
});
};
getData2(333).then(console.log).catch((message) => console.log("ERROR: " + message));
getData2(444).then(console.log).catch((message) => console.log("ERROR: " + message));
getData2(null).then(console.log).catch((message) => console.log("ERROR: " + message));
But I often use callbacks as in the following scenerio where you can have a process which takes a long time and sends out bits of data in numerous callbacks back to the calling code as it processes its information:
sleep = function (ms) {
var start = new Date().getTime();
let now = 0;
let difference = 0;
for (var i = 0; i < 1e17; i++) {
now = new Date().getTime();
difference = now - start;
if (difference > ms) {
break;
}
}
}
const goShopping = (list, cbItemReport, cbFinished, cbError) => {
let count = 0;
let numberFound = 0;
const randomError = Math.floor(Math.random() * 3);
if (randomError == 0) {
cbError('Something went wrong, trip aborted.');
} else {
list.forEach(item => {
const randomFound = Math.floor(Math.random() * 4);
if (randomFound > 0) {
cbItemReport(item, true, ++count);
numberFound++;
} else {
cbItemReport(item, false, ++count);
}
sleep(1000);
})
cbFinished(`Bought ${numberFound} things.`);
}
}
goShopping(['milk', 'eggs', 'sugar', 'bread'],
(item, found, count) => {
console.log(`Item #${count} "${item}" was ${found ? 'found' : 'not found'}.`);
},
(message) => {
console.log("Returned from shopping: " + message);
},
(error) => {
console.log("ERROR: " + error);
});
How would I convert this latter use of callbacks to promises? In this case, the three callbacks cbItemReport, cbFinished, cbError are too many to map to the two that Promise has, i.e. only resolve (cbFinished) and reject (cbError), or what am I missing here?
From what it sounds like, you are looking to implement something like the RxJs library, so why not just use it?
Check out RxJs here
For example a call could look like this then:
const sub = new Subject(); // Create a subject
sub.asObservable().subscribe( // Subscribe to that subject as obserable
({item, found, count}) => { // next
console.log(`Item #${count} "${item}" was ${found ? 'found' : 'not found'}.`);
},
error => { // error
console.log("ERROR: " + error);
},
message => { // complete
console.log("Returned from shopping: " + message);
}
);
const goShopping = (list) => {
let count = 0;
let numberFound = 0;
const randomError = Math.floor(Math.random() * 3);
if (randomError == 0) {
sub.error('Something went wrong, trip aborted.'); // push an error to the subject
} else {
list.forEach(item => {
const randomFound = Math.floor(Math.random() * 4);
if (randomFound > 0) {
sub.next({item: item, found: true, count: ++count}); // push a result to subject (single object)
numberFound++;
} else {
sub.next({item: item, found: true, count: ++count}); // same as above
}
sleep(1000);
})
sub.complete(`Bought ${numberFound} things.`); // push complete to subject. after that no next is allowed anymore
}
}
goShopping(['milk', 'eggs', 'sugar', 'bread'])
Call resolve from cbFinished, reject from cbError and write a cbItemReport that does whatever you want it to do to each item, just as in the existing code.

trouble with displaying data from firestore

I'm having two issues this moment
I am having trouble displaying the data from my firestore document after the user signs in and they do have a document with data
I have a button that lets the user upload a document and I will parse it to extract all important data. That data is then put into the document with Ids, which is basically the userId + what number doc it is belonging to the user. When they upload the data it seems that all data extracted uses the same ID and only the last one extracted gets displayed.
this is my code:
const loansRef = firebase.firestore().collection('goals');
const [ courseGoals, setCourseGoals ] = useState([]);
const [goalCounter, setGoalCounter] = useState(0)
//let the user upload doc, what gets called when they press button
const pickDocument = async () => {
try {
let input = await DocumentPicker.getDocumentAsync({
type: "text/plain",
});
setUserOut(await FileSystem.readAsStringAsync(input.uri));
} catch (error) {
console.log(error);
}
createLoans();
};
//extracts important info and returns array with all info for one loan
const fileParser = () => {
const parsedLoans = [];
var newUserOut = userOut;
if (newUserOut.length == 0) {
return;
}
//remove the grants
var grantPos = newUserOut.search("Grant Type:");
var pos = newUserOut.search("Loan Type:");
//hopefully just the loans now
newUserOut = newUserOut.slice(pos, grantPos);
while (newUserOut.length > 0) {
var lastPos = newUserOut.lastIndexOf("Loan Type:");
parsedLoans.push(newUserOut.slice(lastPos, newUserOut.length));
newUserOut = newUserOut.slice(0, lastPos);
}
//console.log('parsed loans: ' + parsedLoans)
return parsedLoans;
};
//where we actually create loans and get the important data for each loan
const createLoans = () => {
const newLoans = fileParser();
const title= 'Loan Amount:$'
const interest = 'Loan Interest Rate:'
for(let i =0; i < newLoans.length; i++){
let loan = newLoans[i]
let goalTitle=loan.substring(loan.indexOf(title)+title.length,loan.indexOf('Loan Disbursed Amount:'))
//console.log("goalTitle: " + goalTitle)
let interestRate = loan.substring(loan.indexOf(interest)+interest.length,loan.indexOf('Loan Repayment Plan Type'))
//console.log("Interest rate: "+ interestRate)
let years = 0
let paidOff = 0
addGoalHandler(goalTitle,interestRate,years,paidOff)
}
return
};
useEffect(() => {
getPW()
let isMounted = true;
if (isMounted) {
loansRef.doc(userId).onSnapshot(
(docSnapshot) => {
if(!docSnapshot.exists){console.log('doc doesnt exist, start from scratch')}
else{
console.log('loaded successfully '+docSnapshot.data())
setGoalCounter(docSnapshot.data().loans.length)
console.log(goalCounter)
}
},
(error) => {
console.log(error);
}
);
}
return () => {
isMounted = false;
};
}, []);
const addGoalHandler = (goalTitle, interestRate, years, paidOff) => {
//setCourseGoals([...courseGoals, enteredGoal])
setGoalCounter(goalCounter+1)
setCourseGoals((prevGoals) => [
...courseGoals,
{
id:userId.toString() + goalCounter.toString(),
value: goalTitle,
interest: interestRate,
years: years,
paidOff: paidOff
}
]);
addToFB(goalTitle, interestRate,years,paidOff)
//var oldIDS = docIDS
//oldIDS.push(userId.toString() + goalCounter.toString())
//setDocIDS(oldIDS)
setIsAddMode(false);
};
const cancelGoalAdditionHandler = () => {
setIsAddMode(false);
};
const addToFB = async (goalTitle, interestRate, years, paidOff) => {
//adding data to firebase, takes into account if doc exists already
const loadDoc = await loansRef.doc(userId).get()
.then((docSnapshot)=> {
if(docSnapshot.exists){
loansRef.doc(userId).onSnapshot((docu)=>{
const updateLoansArr = loansRef.doc(userId).update({
loans: firebase.firestore.FieldValue.arrayUnion({
id: userId+goalCounter.toString(),
value: goalTitle,
interest: interestRate,
years: years,
paidOff: paidOff
})
})
//setGoalCounter(goalCounter+1)
})
}
else{
const addDoc = loansRef.doc(userId).set({
loans: firebase.firestore.FieldValue.arrayUnion({
id: userId+goalCounter.toString(),
value: goalTitle,
interest: interestRate,
years: years,
paidOff: paidOff
})
})
//setGoalCounter(goalCounter+1)
}})
//setGoalCounter(goalCounter+1)
}
const removeGoalHandler = async (goalId) => {
/*
setCourseGoals((currentGoals) => {
loansRef.doc(goalId).delete().then(console.log('removed correctly'))
return currentGoals.filter((goal) => goal.id !== goalId);
});
setGoalCounter(goalCounter-1)
*/
//need to use the value and not the goalId
const removeGoal = await loansRef.doc(goalId).update({
goals: firebase.firestore.FieldValue.arrayRemove(goalId)
})
setCourseGoals((currentGoals)=> {
return currentGoals.filer((goal)=> goal.id !== goalId)
})
};

How can I get the array size after all timeout be executed

I found a lot of question here related to it, but I can't figure out how to solve my specific problem.
I have a function that gets email users and send them an email.
After each email sent, I add the respective user id in an array.
Now I'm implementing the logger, so I need to save in the log the quantity of emails that were sent, to do that I just need to get the array length.
The problem is that is async, how can I get the array length only after sending the emails to all users?
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}
const waitFor = (ms) => new Promise(r => setTimeout(r, ms));
var sentMail = [];
module.exports = {
sendReminderMail: function(db, mail, consts){
db.find({ $and: [{ subscribed: true }] }, { $not: { feedback: true } }] }, async (err, result) => {
if (err) {
logger.error(`${err.status || 500} - ${err} - '/sendReminderMail' - 'Schedule'`);
} else {
await asyncForEach(result, async (subscriber, index) => {
await waitFor(2000 * index);
if (sentMail.indexOf(subscriber._id) < 0) {
mail.sendMail(subscriber, consts.emailType.REMINDER);
sentMail.push(subscriber._id);
}
});
}
});
}
}
I tried that, but in this case, the logger is called after each interation:
const waitFor = (ms) => new Promise(r => setTimeout(r, ms)).then(
logger.info(sentMail.length + 'mails was sent'));
Suppose that my sentMail array is this [123, 987,2545], so my logger should save 3 mails were sent.
You could easily return the count from that function.
return new Promise(resolve => {
let emailCount = 0;
db.find({ $and: [{ subscribed: true }] }, { $not: { feedback: true } }] }, async (err, result) => {
if (err) {
logger.error(`${err.status || 500} - ${err} - '/sendReminderMail' - 'Schedule'`);
} else {
await asyncForEach(result, async (subscriber, index) => {
await waitFor(2000 * index);
if (sentMail.indexOf(subscriber._id) < 0) {
mail.sendMail(subscriber, consts.emailType.REMINDER);
sentMail.push(subscriber._id);
emailCount++
}
});
resolve(emailCount);
}
});
}

Node JS: How to Promise For Loop

I'm trying to create a NodeJS app. Below is code that is supposed to be called when an admin creates a new product. Most of the code works, but I'm having trouble rendering the view only after the rest of the code has executed (the DB functions are asynchronous). I've wrapped much of the code in promises (to make certain blocks execute in the right order) and console logs (to pinpoint problems).
I'd like to point out that the console.dir(rejProducts)just below console.log(111) logs and empty array. Also, adding console.dir(rejProducts) just before the end bracket of the for loop logs an empty array. Thanks! Please let me know if you need more information.
app.post('/products/new', function (req, res, next) {
// Async function: find all categories
Category.find(function(err, categories) {
// Hidden count that tells num products to be created by form
var numProducts = req.body[`form-item-count`];
// Array of all rejected products adds
var rejProducts = [];
var promiseLoopProducts = new Promise(function(resolve, reject) {
var promiseProducts = [];
// Loop through all addded products
for (let i = 0; i < numProducts; i++) {
var promiseProductCheck = new Promise(function(resolve, reject) {
var name = validate.sanitize(req.body[`name_${i}`]);
var category = validate.sanitize(req.body[`category_${i}`]);
var price = validate.sanitize(req.body[`price_${i}`].replace(/\$/g, ""));
var stock = validate.sanitize(req.body[`stock_${i}`]);
var image = validate.sanitize(req.body[`image_${i}`]);
var description = validate.sanitize(req.body[`description_${i}`]);
var rejProduct;
var rejFields = { 'name': name, 'category': category, 'price': price,
'stock': stock, 'image': image,
'description': description };
var rejErrors = {};
var productData = {
name: name,
category: category,
price: price,
stock: stock,
image: image,
description: description
};
var promiseCategoryCheck = new Promise(function(resolve, reject) {
if (ObjectId.isValid(category)) {
var promiseCategoryCount = new Promise(function(resolve, reject) {
Category.count({'_id': category}, function(error, count) {
rejErrors['name'] = validate.isEmpty(name);
if (count == 0) rejErrors['category'] = true;
rejErrors['price'] = !validate.isPrice(price);
rejErrors['stock'] = !validate.isInt(stock);
if( validate.isEmpty(name) || !validate.isPrice(price) ||
count == 0 || !validate.isInt(stock) ) {
rejProduct = { 'fields': rejFields, 'errors': rejErrors };
rejProducts.push(rejProduct);
console.dir(rejProducts);
console.log(count);
return resolve();
}
else {
Product.create(productData, function (error, product) {
if (error) return next(error);
console.log(77);
console.dir(rejProducts);
return resolve();
});
}
if (error) return next(error);
});
});
promiseCategoryCount.then(function() {
console.dir(rejProducts);
return resolve();
});
} else {
rejErrors['category'] = true;
rejProduct = { 'fields': rejFields, 'errors': rejErrors };
rejProducts.push(rejProduct);
console.dir(rejProducts);
}
});
promiseCategoryCheck.then(function() {
console.dir(rejProducts);
promiseProducts.push(promiseProductCheck);
console.log(promiseProductCheck);
console.log(promiseProducts);
return resolve();
});
});
promiseProductCheck.then(function() {
console.log(106);
console.dir(rejProducts);
});
}
Promise.all(promiseProducts).then(function() {
console.log(111);
console.dir(rejProducts); // Empty array!
return resolve();
});
});
promiseLoopProducts.then(function() {
console.log(118);
console.dir(rejProducts); // Empty array!
res.render('products/new', { categories: categories, rejProducts: rejProducts });
});
});
});
Edit: I've made some changes to the code. There it seems like util.promisify is not being recognized as a function. I am using Node 9.4.
module.exports = function(app){
const util = require('util');
require('util.promisify').shim();
const validate = require('../functions/validate');
const Category = require('../db/categories');
const Product = require('../db/products');
var ObjectId = require('mongoose').Types.ObjectId;
//Category.find as function that returns a promise
const findCategories = util.promisify(Category.find);
const countCategories = (categoryId) => {
util.promisify(Category.count({'_id': categoryId}));
};
const bodyToProduct = (body, i) => {
var name = validate.sanitize(body[`name_${i}`]);
var category = validate.sanitize(body[`category_${i}`]);
var price = validate.sanitize(body[`price_${i}`].replace(/\$/g, ""));
var stock = validate.sanitize(body[`stock_${i}`]);
var image = validate.sanitize(body[`image_${i}`]);
var description = validate.sanitize(body[`description_${i}`]);
return {
name: name,
category: category,
price: price,
stock: stock,
image: image,
description: description
};
};
app.post('/products/new', function (req, res, next) {
// Async function: find all categories
return findCategories()
.then(
categories=>{
// Hidden count that tells num products to be created by form
var numProducts = req.body[`form-item-count`];
// Array of all rejected products adds
var rejProducts = [];
return Promise.all(
Array.from(new Array(numProducts),(v,i)=>i)
.map(//map [0...numProducts] to product object
i=>bodyToProduct(req.body,i)
)
.map(
product => {
var rejErrors;
var rejName = validate.isEmpty(name);
var rejCategory;
var rejPrice = !validate.isPrice(price);
var rejStock = !validate.isInt(stock);
if (ObjectId.isValid(product.category)) {
return countCategories()
.then(
count=> {
if (count == 0) rejCategory = true;
if(rejName || rejCategory || rejPrice || rejStock ) {
rejErrors = {
name: rejName,
category: rejCategory,
price: rejPrice,
stock: rejStock
}
rejProduct = { 'fields': product, 'errors': rejErrors };
rejProducts.push(rejProduct);
console.dir(rejProducts);
console.log(count);
} else {
Product.create(productData, function (error, product) {
if (error) return next(error);
console.log(77);
console.dir(rejProducts);
});
}
}
).catch(function() {
console.log("Count function failed.");
});
} else {
rejCategory = true;
rejErrors = {
name: rejName,
category: rejCategory,
price: rejPrice,
stock: rejStock
}
rejProduct = { 'fields': product, 'errors': rejErrors };
rejProducts.push(rejProduct);
console.dir(rejProducts);
}
}
)
).then(function() {
res.render('products/new', { categories: categories, rejProducts: rejProducts });
}).catch(function() {
console.log("Promise all products failed.");
});
}
).catch(function() {
console.log("Find categories failed.");
})
});
}
Some tips: if your function is over 100 lines you may be doing to much in the function.
If you have to get data from your request the way you get products of it then write better client side code (products should be an array of product objects that should not need to be sanitized). Validation is needed on the server because you an never trust what the client is sending you. But looking at the sanitize you don't even trust what your client script is sending you.
Try to write small functions that do a small thing and try to use those.
Use .map to map type a to type b (for example req.body to array of products as in the example code).
Use the result of .map as an argument to Promise.all
Use util.promisify to change a callback function into a function that returns a promise, since you are using an old version of node I've added an implementation of promisify:
var promisify = function(fn) {
return function(){
var args = [].slice.apply(arguments);
return new Promise(
function(resolve,reject){
fn.apply(
null,
args.concat([
function(){
var results = [].slice.apply(arguments);
(results[0])//first argument of callback is error
? reject(results[0])//reject with error
: resolve(results.slice(1,results.length)[0])//resolve with single result
}
])
)
}
);
}
};
module.exports = function(app){
const validate = require('../functions/validate');
const Category = require('../db/categories');
const Product = require('../db/products');
var ObjectId = require('mongoose').Types.ObjectId;
//Category.find as function that returns a promise
const findCategories = promisify(Category.find.bind(Category));
const countCategories = (categoryId) => {
promisify(Category.count.bind(Category))({'_id': categoryId});
};
const createProduct = promisify(Product.create.bind(Product));
const REJECTED = {};
const bodyToProduct = (body, i) => {
var name = validate.sanitize(body[`name_${i}`]);
var category = validate.sanitize(body[`category_${i}`]);
var price = validate.sanitize(body[`price_${i}`].replace(/\$/g, ""));
var stock = validate.sanitize(body[`stock_${i}`]);
var image = validate.sanitize(body[`image_${i}`]);
var description = validate.sanitize(body[`description_${i}`]);
return {
name: name,
category: category,
price: price,
stock: stock,
image: image,
description: description
};
};
const setReject = product => {
var rejErrors;
var rejName = validate.isEmpty(product.name);
var rejCategory;
var rejPrice = !validate.isPrice(product.price);
var rejStock = !validate.isInt(product.stock);
const countPromise = (ObjectId.isValid(product.category))
? countCategories()
: Promise.resolve(0);
return countPromise
.then(
count => {
if (count == 0) rejCategory = true;
if (rejName || rejCategory || rejPrice || rejStock) {
rejErrors = {
type:REJECTED,
name: rejName,
category: rejCategory,
price: rejPrice,
stock: rejStock
}
return rejErrors;
}
return false;
}
);
};
const productWithReject = product =>
Promise.all([
product,
setReject(product)
]);
const saveProductIfNoRejected = ([product,reject]) =>
(reject===false)
? Product.create(product)
.catch(
err=>({
type:REJECTED,
error:err
})
)
: reject;
app.post('/products/new', function (req, res, next) {
// Async function: find all categories
return findCategories()
.then(
categories => {
// Hidden count that tells num products to be created by form
var numProducts = req.body[`form-item-count`];
// Array of all rejected products adds
var rejProducts = [];
return Promise.all(
Array.from(new Array(numProducts), (v, i) => i)
.map(//map [0...numProducts] to product object
i => bodyToProduct(req.body, i)
)
.map(
product=>
productWithReject(product)
.then(saveProductIfNoRejected)
)
).then(
results =>
res.render(
'products/new',
{
categories: categories,
rejProducts: results.filter(x=>(x&&x.type)===REJECTED)
}
)
).catch(
error=>
console.log("Promise all products failed.",error)
);
}
).catch(
error=>
console.log("Find categories failed.",error)
)
});
}

Categories