This question already has answers here:
How to use promise in forEach loop of array to populate an object
(5 answers)
Closed 2 years ago.
On the firebase functions, I have next code:
app.post('/licence', (req, res) => {
let { email, machine_id, product_id } = req.body
let arr_product_ids = product_id.split(",").map(function (val) { return {product_id: val}; });
let res_to_print = '';
return new Promise((resolve, reject) => {
arr_product_ids.forEach(function(n){
res_to_print = asyncGetLicences(n.product_id, email, machine_id)
console.log('res_to_print')
console.log(res_to_print)
});
}).then((state) => {
console.log(state)
})
.catch((error) => {
console.log(error)
});
I need to call query two times to firebase query! So I call it in foreach loop.
Here is the function that needs to be called twice:
function asyncGetLicences(product_id, email, machine_id) {
licences_to_print = []
db.collection('licences', 'desc').where('email', '==', email).where('product_id', '==', product_id).get()
.then(data => {
let licences = []
data.forEach(doc => {
console.log(doc.data().email);
licences.push({
id: doc.id,
email: doc.data().email,
product: doc.data().product,
createdAt: doc.data().createdAt
});
});
if(typeof this.licences !== 'undefined' && this.licences.length > 0){
let string = email+machine_id+product_id+'55';
let api = md5(string);
let uppercase = api.toUpperCase()+'-';
licences_to_print.push(uppercase);
return licences_to_print
//return res.send('"'+uppercase+'"');//res.json(this.licences);
} else {
return licences_to_print
//return res.status(200).send('nothing to find');
}
})
}
I'm struggling with this simple promise...I had this in PHP and it was very easy, but node.js and firebase I got stuck!
Add all the promises in an array and insert into Promise.all() and then return this in the main function. This will collectively get the return from each promises asynchronously and return a single collective response.
app.post('/licence', (req, res) => {
let { email, machine_id, product_id } = req.body
let arr_product_ids = product_id.split(",").map(function (val) { return {product_id: val}; });
let res_to_print = '';
const promises = [] // Empty array
arr_product_ids.forEach(function(n){
promises.push(asyncGetLicences(n.product_id, email, machine_id));
});
return Promise.all(promises).then(res_to_print => {
console.log('res_to_print')
console.log(res_to_print)
}).catch((error) => {
console.log(error)
});
The second function:
function asyncGetLicences(product_id, email, machine_id) {
licences_to_print = []
return new Promise((resolve, reject) => {
db.collection('licences', 'desc').where('email', '==', email).where('product_id', '==', product_id).get()
.then(data => {
let licences = []
data.forEach(doc => {
console.log(doc.data().email);
licences.push({
id: doc.id,
email: doc.data().email,
product: doc.data().product,
createdAt: doc.data().createdAt
});
});
if(typeof this.licences !== 'undefined' && this.licences.length > 0){
let string = email+machine_id+product_id+'55';
let api = md5(string);
let uppercase = api.toUpperCase()+'-';
licences_to_print.push(uppercase);
resolve(licences_to_print)
//return res.send('"'+uppercase+'"');//res.json(this.licences);
} else {
resolve(licences_to_print)
//return res.status(200).send('nothing to find');
}
}))
Related
I have tried using async-await, .then and now promise. I am quite new in javascript development.
The code
indexRouter.get('/dashboard', checkSignIn, async(request, response) => {
snapshot = await db.doc('users/accounts').get()
sites = snapshot.data()['sites']
const userId = request.session.userId
snapshot = await db.doc(`users/${userId}`).get()
var linkedSites = snapshot.data()['linked sites']
let getDs = new Promise((resolve,reject)=>{
console.log("1")
linkedSites.forEach((site) =>{
console.log("2")
db.doc(`users/${userId}`).collection(site).get()
.then((snapshot)=>{
console.log("3")
snapshot.forEach((doc) => {
console.log("4")
console.log(doc.id)
emailId = doc.id
keys = doc.data()['keys']
var passwordEncrypt = doc.data()['password']
password = cryptoJS.....
details.push({site:{'email': emailId, 'password': password, 'keys': keys}})
})
})
})
console.log("5")
console.log(details)
resolve(details)
}
);
getDs.then((details)=>{
console.log("rendering")
response.render('dashboard', {'details':details, 'linkedSites': linkedSites, 'sites': sites})
})
}
I am getting the response
1
2
2
5
[]
rendering
error: ...details not found in ejs
3
4
rsp121#gmail.com
3
4
test#gmail.com
According to the output, it seems like db.doc line after console.log(2) is getting executed after a certain time and resolve(details) is sent before.
Found a solution to the problem:
indexRouter.get('/dashboard', checkSignIn, async(request, response) => {
snapshot = await db.doc('users/accounts').get()
sites = snapshot.data()['sites']
const userId = request.session.userId
snapshot = await db.doc(`users/${userId}`).get()
var linkedSites = snapshot.data()['linked sites']
if(linkedSites){
const getSnapshot = (site) => {
return new Promise(resolve => {
db.doc(`users/${userId}`).collection(site).get()
.then((snapshot) =>{
snapshot.forEach((doc) =>{
emailId = doc.id
keys = doc.data()['keys']
var passwordEncrypt = doc.data()['password']
password = cryptoJS
details[site] = {'email': emailId, 'password': password, 'keys': keys}
resolve(true)
})
})
})
}
Promise.all(linkedSites.map(getSnapshot)).then(()=>{
console.log(linkedSites)
response.set('Cache-Control', 'no-store, no-cache, must-revalidate, private')
response.render('dashboard', {'details':details, 'linkedSites': linkedSites, 'sites': sites})
})
}
The problem is your promise resolved before the db.doc is resolved, and as your db.doc promise is inside a loop. So, you should be using the promise.all
The below code should work for you.
indexRouter.get("/dashboard", checkSignIn, async (request, response) => {
snapshot = await db.doc("users/accounts").get();
sites = snapshot.data()["sites"];
const userId = request.session.userId;
snapshot = await db.doc(`users/${userId}`).get();
var linkedSites = snapshot.data()["linked sites"];
let getDs = new Promise((resolve, reject) => {
const promises = [];
console.log("1");
linkedSites.forEach((site) => {
console.log("2");
promises.push(
new Promise((internalResolve) => {
db.doc(`users/${userId}`)
.collection(site)
.get()
.then((snapshot) => {
console.log("3");
snapshot.forEach((doc) => {
console.log("4");
console.log(doc.id);
emailId = doc.id;
keys = doc.data()["keys"];
var passwordEncrypt = doc.data()["password"];
password = cryptoJS;
details.push({
site: {
email: emailId,
password: password,
keys: keys,
},
});
internalResolve();
});
});
})
);
});
Promise.all(promises).then(() => {
console.log("5");
console.log(details);
resolve(details);
});
});
getDs.then((details) => {
console.log("rendering");
return response.render("dashboard", {
details: details,
linkedSites: linkedSites,
sites: sites,
});
});
});
More cleaner with async/await.
indexRouter.get("/dashboard", checkSignIn, async (request, response) => {
snapshot = await db.doc("users/accounts").get();
sites = snapshot.data()["sites"];
const userId = request.session.userId;
snapshot = await db.doc(`users/${userId}`).get();
var linkedSites = snapshot.data()["linked sites"];
console.log("1");
linkedSites.forEach(async (site) => {
console.log("2");
const snapshot = await db.doc(`users/${userId}`).collection(site).get();
console.log("3");
snapshot.forEach((doc) => {
console.log("4");
console.log(doc.id);
emailId = doc.id;
keys = doc.data()["keys"];
var passwordEncrypt = doc.data()["password"];
password = cryptoJS;
details.push({
site: {
email: emailId,
password: password,
keys: keys,
},
});
});
});
console.log("rendering");
return response.render("dashboard", {
details: details,
linkedSites: linkedSites,
sites: sites,
});
});
This is your code corrected, optimized and using last especifications of ECMA Script, the error is as the other comment said you are no waiting for the result of promises inside your "new Promise.." declaration. Dont foget to use try/catch inside async functions if you have not a top error handler.
The optimization is in firsts snapshot variables, this way you are getting data in parallel instead secuantially.
indexRouter.get('/dashboard', checkSignIn, async (request, response) => {
try {
let details = [];
const userId = request.session.userId
let [snapshot1, snapshot2] = await Promise.all([db.doc('users/accounts').get(), await db.doc(`users/${userId}`).get()])
let sites = snapshot1.data()['sites']
var linkedSites = snapshot2.data()['linked sites'];
await Promise.all(
linkedSites.map((site) =>
db.doc(`users/${userId}`).collection(site).get()
.then((snapshot) => {
snapshot.forEach((doc) => {
emailId = doc.id
keys = doc.data()['keys']
var passwordEncrypt = doc.data()['password']
password = cryptoJS
details.push({ site: { 'email': emailId, 'password': password, 'keys': keys } })
})
})
)
)
response.render('dashboard', { 'details': details, 'linkedSites': linkedSites, 'sites': sites })
} catch (e) {
//Render error you want
}})
linkedSites.map.... return an array of Promises that in the end are wrapped inside Promise.all and Promise.all wait until all promises are fullfilled or one of them is rejected in this last case your code goes to catch without reach the line response.render inside the try. You can avoid this catching locally the error of each promise in the map using
.then((snapshot) => {
...
}).catch(e=> { /*Do something with the rror*/})
I have an http trigger in cloud functions that is working, however I am getting some logs come back in a weird order. I am pretty sure I have not executed the promises correctly.
here is the code:
exports.createGame = functions.https.onRequest((req, res) => {
return cors(req, res, () => {
// Check for POST request
if (req.method !== "POST") {
res.status(400).send('Please send a POST request');
return;
}
const createGame = admin.firestore().collection('games')
generatePin()
function generatePin() {
...pin generating code goes here...
addGame(newGidToString)
}
function addGame(newGidToString) {
var getGame = admin.firestore().collection('games')
getGame = getGame.where('gid', '==', newGidToString)
getGame.get().then(snapshot => {
if (snapshot.empty) {
console.log('BODY ', req.body)
const promises = [];
const gameTitle = req.body.gametitle
const targetGID = req.body.target
const numPlayers = req.body.playercount.toString()
const newGID = newGidToString
const collections = ['games', 'clues', 'detours', 'messages', 'questions', 'tagstate']
collections.forEach(function (element) {
console.log('start loop', element)
let elementsRef = admin.firestore().collection(element)
elementsRef = elementsRef.where('gid', '==', targetGID)
elementsRef.get().then(snapshot => {
var batch = admin.firestore().batch()
snapshot.forEach(function (doc) {
// change the gid to the new game gid
let data = doc.data()
data.gid = newGID
if(data.template){
data.template = false
}
if(!data.parent) {
data.parent = targetGID
}
if (!data.playercount) {
data.playercount = numPlayers
}
if (data.gametitle) {
data.gametitle = gameTitle
}
let elementRef = admin.firestore().collection(element).doc()
batch.set(elementRef, data)
})
batch.commit().then(data => {
console.log('complete batch ', element)
})
})
})
res.send({ status: 200, message: 'Game: added.', pin: newGID})
console.log('completed');
} else {
console.log('found a match ', snapshot)
// re-run the generator for a new pin
generatePin()
}
})
}
})
})
Here is a screen shot of the console logs.
Notice the order of the logs. It would seem logical that completed would come at the end fo the loops.
Any help is great appreciated.
UPDATE:
function addGame(newGidToString) {
var getGame = admin.firestore().collection('games')
getGame = getGame.where('gid', '==', newGidToString)
getGame.get().then(snapshot => {
if (snapshot.empty) {
console.log('BODY ', req.body)
const promises = [];
const gameTitle = req.body.gametitle
const targetGID = req.body.target
const numPlayers = req.body.playercount.toString()
const newGID = newGidToString
const collections = ['games', 'clues', 'detours', 'messages', 'questions', 'tagstate']
collections.forEach(function (element) {
console.log('start loop', element)
let elementsRef = admin.firestore().collection(element)
elementsRef = elementsRef.where('gid', '==', targetGID)
// elementsRef.get().then(snapshot => {
const promGet = elementsRef.get(); //lets get our promise
promises.push(promGet); //place into an array
promGet.then(snapshot => { //we can now continue as before
var batch = admin.firestore().batch()
snapshot.forEach(function (doc) {
// change the gid to the new game gid
let data = doc.data()
data.gid = newGID
if(data.template){
data.template = false
}
if(!data.parent) {
data.parent = targetGID
}
if (!data.playercount) {
data.playercount = numPlayers
}
if (data.gametitle) {
data.gametitle = gameTitle
}
let elementRef = admin.firestore().collection(element).doc()
batch.set(elementRef, data)
})
batch.commit().then(data => {
console.log('complete batch ', element)
})
})
})
Promise.all(promises).then(() => {
res.send({ status: 200, message: 'Game: added.', pin: newGID })
console.log('completed');
});
// res.send({ status: 200, message: 'Game: added.', pin: newGID})
// console.log('completed');
} else {
console.log('found a match ', snapshot)
// re-run the generator for a new pin
generatePin()
}
})
}
SECOND UPDATE: (based off Keith's answer)
function addGame(newGidToString) {
var getGame = admin.firestore().collection('games')
getGame = getGame.where('gid', '==', newGidToString)
getGame.get().then(snapshot => {
if (snapshot.empty) {
console.log('BODY ', req.body)
const gameTitle = req.body.gametitle
const targetGID = req.body.target
const numPlayers = req.body.playercount.toString()
const newGID = newGidToString
const promises = [];
const collections = ['games', 'clues', 'detours', 'messages', 'questions', 'tagstate']
collections.forEach(function (element) {
console.log('start loop', element)
let elementsRef = admin.firestore().collection(element)
elementsRef = elementsRef.where('gid', '==', targetGID)
const getPromise = elementsRef.get(); //lets get our promise
promises.push(getPromise); //place into an array
getPromise.then(snapshot => { //we can now continue as before
var batch = admin.firestore().batch()
snapshot.forEach(function (doc) {
// change the gid to the new game gid
let data = doc.data()
data.gid = newGID
if(data.template){
data.template = false
}
if(!data.parent) {
data.parent = targetGID
}
if (!data.playercount) {
data.playercount = numPlayers
}
if (data.gametitle) {
data.gametitle = gameTitle
}
let elementRef = admin.firestore().collection(element).doc()
batch.set(elementRef, data)
})
batch.commit()
})
})
Promise.all(promises).then(() => {
res.send({ status: 200, message: 'Game: added.', pin: newGID })
console.log('completed from promise');
});
} else {
console.log('found a match ', snapshot)
// re-run the generator for a new pin
generatePin()
}
})
}
When looping with promises, one thing to remember is if you do a forEach, it's not going to wait.
It appears that what the OP wants to do is before he hits the complete, and does a res send, wants to make sure all promises have been completed.
Promise.all https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all , is exactly what this does.
So every time you create a promise if you push this onto an array, we can later use Promise.all to make sure all the promises are complete.
So if we replace
elementsRef.get().then(snapshot => {
with
const promGet = elementsRef.get(); //lets get our promise
promises.push(promGet); //place into an array
promGet.then(snapshot => { //we can now continue as before
Your promise is now stored for later so that Promise.all can check to make sure everything has complete. You could do this in one line, but I feel it's easier to see what's happening this way. eg->
promises.push(promGet.then(snapshot => {
Finally now we have got our promise array, before we do our res.send, lets make sure they have all completed.
Promise.all(promises).then(() => {
res.send({ status: 200, message: 'Game: added.', pin: newGID})
console.log('completed');
});
One thing to keep in mind when using Promises this way, is that you have a fair few things going on at the same time, sometimes this can be an issue, eg. connected to some service might put restrictions on the number of concurrent connections etc. So in these cases it might be an idea doing things in series or a mix of both. There are even libs that can do concurrecy, eg. Do only X amounts of promises at a time.
And finally, if you can use async / await, I'd advice using them, as it makes Promises so much nicer to work with.
This question already has answers here:
How do I return the response from an asynchronous call?
(41 answers)
Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference
(7 answers)
Closed 4 years ago.
I do a bulk insert of documents that is initiated by a call to the function, addNewFeedToUserFeeds. addNewFeedToUserFeeds is called from a controller and, I'd like it to return the docs created by the bulk insert function in insertIntoDB, to that controller. However, it seems that because the bulk insert happens inside a promise, the results of that insert are lost. Is there a way to do this that I am missing?
const discover = require('rssdiscovery');
const FeedParser = require('feedparser');
const request = require('request');
const UserFeedEntryMap = require('../models/userFeedEntryMap');
(function () {
let currentuser = {};
function insertIntoDB(dataFromFeed) {
const loop = new Promise((resolve, reject) => {
const ufmObjects = [];
dataFromFeed.forEach((item) => {
const feed = {
title: item.meta.title,
description: item.meta.description,
lastUpdate: item.meta.date, // most recent update
link: item.meta.link,
xmlUrl: item.meta.xmlUrl
};
const feedEntry = {
title: item.title,
link: item.link,
description: item.description,
summary: item.summary,
author: item.author,
pubdate: item.pubDate,
date: item.date,
guid: item.guid,
image: item.image.url,
categories: item.categories,
source: item.source,
};
const ufem = new UserFeedEntryMap({
user_name: currentuser.local.userName,
feed_entry_metaData: feed,
feed_entry: feedEntry
});
ufmObjects.push(ufem);
});
resolve(ufmObjects);
});
loop.then((ufmObjects) => {
UserFeedEntryMap.insertMany(ufmObjects, (error, docs) => {
if (error) {
console.log(error);
} else {
console.log(docs);
}
});
});
}
function parseFeedToObjects(xmlURL) {
const feedItems = [];
const req = request(xmlURL);
const feedparser = new FeedParser();
req.on('error', (error) => {
console.log(error);
});
req.on('response', function (res) {
const stream = this;
if (res.statusCode !== 200) {
this.emit('error', new Error('Bad status code'));
} else {
stream.pipe(feedparser);
}
});
feedparser.on('error', (error) => {
console.log(error);
});
feedparser.on('readable', function () {
const stream = this;
const meta = this.meta;
let item;
while (item = stream.read()) {
feedItems.push(item);
}
});
feedparser.on('end', () => {
// emit error if no items
// console.log(feedItems);
insertIntoDB(feedItems);
});
}
exports.addNewFeedToUserFeeds = function name(user, url) {
currentuser = user;
discover(url, (err, results) => {
const baseURL = url.slice(0, -1);
parseFeedToObjects(results.links.length > 1 ? results.links[0].href : baseURL + results.links[0].href);
});
};
}());
You probably want to be using Promise.all() for returning the results of multiple asynchronous processes. It will give you an array of all of the results and will wait until all processes are completed before resolving.
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)
)
});
}
I have the app on node.js with connecting to firebase. I need to update the data correctly.
How to call the function getOrSetUserTrackDay(day) in a promise to get a good value, but not undefined?
let userData = [];
let userID = req.params.userID;
let today = req.params.today;
let yesterday = req.params.yesterday;
db.collection('users').doc(userID).get()
.then((userDataFromDB) => {
if (!userDataFromDB.exists) {
res.status(404).send('User not found');
}
else {
function getOrSetUserTrackDay(day) {
let userTrackRef = db.collection('history').doc('daily').collection(day).doc(userID);
userTrackRef.get()
.then(userTrackData => {
if (userTrackData.exists) {
return userTrackData.data(); // good
}
else {
let userNewData = {
username: userDataFromDB.data().username,
photoUrl: userDataFromDB.data().photoUrl
};
userTrackRef.update(userNewData);
return userNewData; // good
}
})
}
userData = {
user: userDataFromDB.data(),
today: getOrSetUserTrackDay(today), // undefined
yesterday: getOrSetUserTrackDay(yesterday) // undefined
};
res.json(userData);
}
})
.catch((err) => {
console.log(err);
res.status(404).send(err);
});
well getOrSetUserTrackDay has no return statement, hence it returns undefined - but, since it contains asynchronous code, you'll never be able to use it synchronously
So, you can do the following
let userData = [];
let userID = req.params.userID;
let today = req.params.today;
let yesterday = req.params.yesterday;
db.collection('users').doc(userID).get()
.then((userDataFromDB) => {
if (!userDataFromDB.exists) {
res.status(404).send('User not found');
}
else {
let getOrSetUserTrackDay = day => {
let userTrackRef = db.collection('history').doc('daily').collection(day).doc(userID);
return userTrackRef.get()
.then(userTrackData => {
if (userTrackData.exists) {
return userTrackData.data(); // good
} else {
let userNewData = {
username: userDataFromDB.data().username,
photoUrl: userDataFromDB.data().photoUrl
};
userTrackRef.update(userNewData);
return userNewData; // good
}
});
};
Promise.all([getOrSetUserTrackDay(today), getOrSetUserTrackDay(yesterday)])
.then(([today, yesterday]) => res.json({
user: userDataFromDB.data(),
today,
yesterday
}));
}
}).catch((err) => {
console.log(err);
res.status(404).send(err);
});
Note: changed getOrSetUserTrackDay from a function declaration to a function expression (in this case, an arrow function for no particular reason) - because Function declarations should not be placed in blocks. Use a function expression or move the statement to the top of the outer function.