Nodejs async-waterfall callback function is not defined - javascript

I am trying to save the file and details of it in the database.
Waterfall function with two calls but the second function is not waiting for the first function to finish.
Even without using waterfall eachseries doesn't work as expected. It doesn't wait for the record to be created and hence uniqueness error occurs due to same id.
What am I doing wrong here and how can I fix it?
Thanks!
async.eachSeries(uploadedPhotos, async function (uploadedFile, callback) {
async.waterfall([
async function() {
var lastUser = await TblUserFiles.find({}).sort('id DESC').limit(1);
// fileID remains undefined if i remove async-await from the function
var fileID = lastUser[0].id;
fileID += 1;
cbb(null, fileID, uploadedFile);
},
async function(file_id, uploadedFile, cbb) {
sails.log("save file id is " + file_id);
sails.log("savee file id is " + uploadedFile['filename']);
var today = moment(new Date()).format('YYYY-MM-DD HH:mm:ss');
await TblUserFiles.findOrCreate({ customer_no: req.session.userId, file_type: 'profile_image' }, {
id: fileID,
customer_no: req.session.userId,
file_name: uploadedFile['filename'],
file_type: 'profile_image',
is_approved: 'No',
approved_by: 0,
approved_on: today,
approved_from: ip,
uploaded_date: today,
modified_date: today
}).exec(async (err, user, wasCreated) => {
if (err) { return res.serverError(err); }
if (wasCreated) {
// created a new user
sails.log("new file was uploaded...")
return cbb(err, "done");
// return res.send("sent");
}
else {
// found existing user
var user = await TblUserFiles.create({
id: fileID,
customer_no: req.session.userId,
file_name: uploadedFile["filename"],
file_type: "image",
is_approved: "No",
approved_by: 0,
approved_on: today,
approved_from: ip,
uploaded_date: today,
modified_date: today
}).intercept(err => {
// Return a modified error here (or a special exit signal)
// and .create() will throw that instead
err.message = "Uh oh: " + err.message;
return err;
}).fetch();
if (user) {
sails.log("found existing files..")
return cbb(err, "done");
}
sails.log("this should not be called");
}
});
}
], (err, success) => {
if (err) sails.log(err);
return callback(err, 'done')
});
}, function (err) {
// if any of the saves produced an error, err would equal that error
if (err) {
sails.log(err);
} else {
return res.json({
message: uploadedPhotos.length + ' file(s) uploaded successfully!',
files: uploadedPhotos
});
}
});

Updated answer:
I rewrite the code.
I forgot that async.js handles promise in different way. Basically, you don't use callback(). Use return instead. If an error occurs, use throw new Error(message).
See here for more information. Read topic: Using ES2017 async functions.
async.eachSeries(uploadedPhotos, async uploadedFile => {
var lastUser = await TblUserFiles.find({}).sort('id DESC').limit(1);
var fileID = lastUser[0].id;
fileID += 1;
sails.log("save file id is " + file_id);
sails.log("savee file id is " + uploadedFile['filename']);
var today = moment(new Date()).format('YYYY-MM-DD HH:mm:ss');
await TblUserFiles.findOrCreate(
{ customer_no: req.session.userId, file_type: 'profile_image' },
{
id: fileID,
customer_no: req.session.userId,
file_name: uploadedFile['filename'],
file_type: 'profile_image',
is_approved: 'No',
approved_by: 0,
approved_on: today,
approved_from: ip,
uploaded_date: today,
modified_date: today
}
).exec(async (err, user, wasCreated) => {
if (err) throw new Error(err);
if (wasCreated) {
sails.log("new file was uploaded...");
return;
} else {
await TblUserFiles.create({
id: fileID,
customer_no: req.session.userId,
file_name: uploadedFile["filename"],
file_type: "image",
is_approved: "No",
approved_by: 0,
approved_on: today,
approved_from: ip,
uploaded_date: today,
modified_date: today
})
.intercept(err => {
throw new Error("Uh oh: " + err.message);
}).fetch();
if (user) {
sails.log("found existing files..");
return;
} else {
sails.log("this should not be called");
return;
}
}
});
}, err => {
// Don't call res.serverError() or res.json() inside the async loop!
if (err) {
sails.log(err);
res.serverError(err);
}
else {
res.json({
message: uploadedPhotos.length + ' file(s) uploaded successfully!',
files: uploadedPhotos
});
}
});
I think the first problem here is that the you forget to give the first task a callback function(cbb in this case).
async.waterfall([
async function(cbb) {
var lastUser = await TblUserFiles.find({}).sort('id DESC').limit(1);
// fileID remains undefined if i remove async-await from the function
var fileID = lastUser[0].id;
fileID += 1;
cbb(null, fileID, uploadedFile);
},
async function(file_id, uploadedFile, cbb) {
...
Secondly, you shouldn't return a callback. Callback is a function not a promise. Just use them normally.
By the way, const async = require('async'); will NOT override async keyword. Compiler can tell the difference between them. This is proved by the following script example:
const async = require('async');
let runPromise = (name, timer, success = true) => {
console.log(`${name} starts.`);
return new Promise((resolve, reject) => {
if (success) {
setTimeout(function () {
resolve(`${name} finished after ${timer / 1000} seconds(resolved).`);
}, timer);
} else {
reject(`${name} failed(rejected).`);
}
});
};
async function asyncFunction() {
let txt = await runPromise('A', 1000);
console.log(txt);
}
asyncFunction();

Related

test is exiting before the method which has async.waterfall, is executed

I'm currently writing unit tests for an API. One of the helper functions is the async.waterfall to avoid "callback hell". I made this method async and await where the method is called.
The test is exiting saying Number of calls: 0 in Node.js. I'm using Jest as my test framework.
Method being tested:
outletInfoController.fetchAllOutletsByUserIdAndCityId = async function (req, res) {
var userId = req.body.userId;
var cityId = req.body.cityId;
await outletInfoHelper.fetchAllOutletsByUserIdAndCityId(userId, cityId, function (error, result) {
if (error || !result) {
responseUtils.buildAndRespond(commonConst.CODE.FAILURE_RESPONSE_CODE, commonConst.MESSAGE.FAILURE_RESPONSE_MESSAGE,
error, null, res);
} else {
responseUtils.buildAndRespond(commonConst.CODE.SUCCESS_RESPONSE_CODE, commonConst.MESSAGE.SUCCESS_RESPONSE_MESSAGE,
null, result, res);
}
});
};
Method which is using async.waterfall:
outletInfoHelper.fetchAllOutletsByUserIdAndCityId = async function (userId, cityId, outerCallback) {
async.waterfall(
[function getRestaurantsByUserId(callback) {
//do something
}, function getRestaurantsDetails(restIds, callback) {
//do something
}, function getRestaurantsUserCount(restaurants, callback) {
//do something
}], function (error, results) {
//do something
});
}
Test code snippet:
test("outletInfo_success_response", async () => {
let req = {
body: {
userId: 8881,
cityId: 211
}
};
let restIds = [];
await factory.restaurantListFactory((result) => {
restIds = result;
});
await fetchUserRestaurantObjFromUserId.mockImplementation((userId, cb) => {
cb(null, restIds);
});
let mockResponse = [];
await factory.restaurantObjectsFactory(function (result) {
mockResponse = result;
});
let successResp = {
statusMessage: 'success',
data: mockResponse,
errorMessage: null,
statusCode: 0
}
await fetchRestaurantsFromIdsAndCityId.mockImplementation((restIds, cityId, cb) => {
cb(null, mockResponse);
});
let response;
await factory.getUserRestMap((result) => {
response = result;
});
await fetchUsersFromOutletId.mockImplementation((restIds, cb) => {
cb(null, response);
});
let res = await getResponseObj();
await outletInfoController.fetchAllOutletsByUserIdAndCityId(req, res);
expect(res.send).toBeCalledWith(successResp);
});

Error when trying to pass value in async function (Stripe charging process)

I am trying to integrate Stipe into my project. In order to create a charge i need to create some values and pass it on to the next function. I currently have a async function chain in order to do so, but for some reason one of the values (created token) is not being passed on (error: undefined).
I tried it with passing the returned value of the function as well as saving the needed value in a object and then passing on the object.
Both customer and token are being correctly generated, but only the customer is being passed on to the addCardToCustomer function.
Can anyone spot a mistake a made here?
router.post("/checkout", async function (req, res, next) {
if (!req.session.cart) {
return res.redirect("/shopping-cart");
}
let cart = new Cart(req.session.cart);
let customerId = {};
let createCustomer = async function () {
var param = {};
param.email = req.body.email;
param.name = req.body.name;
param.description = "";
return stripe.customers.create(param, function (err, customer) {
if (err) {
console.log("err:" + err);
}
if (customer) {
console.log("success: " + JSON.stringify(customer, null, 2));
customerId.id = customer.id;
} else {
console.log("something went wrong");
}
});
//CUSTOMER CREATED
};
let createToken = async function () {
let param = {};
param.card = {
number: req.body.card,
exp_month: req.body.exp_month,
exp_year: req.body.exp_year,
cvc: req.body.security
}
return stripe.tokens.create(param, function (err, token) {
if (err) {
console.log("err:" + err);
}
if (token) {
console.log("success: " + JSON.stringify(token, null, 2));
console.log(req.body);
customerId.t_Id = token.id;
console.log(customerId.t_Id)
} else {
console.log("something went wrong");
}
});
};
//TOKEN CREATED
let addCardToCustomer = async function (createdToken) {
return stripe.customers.createSource(customerId.id, {
source: createdToken
}, function (err, card) {
if (err) {
console.log("err:" + err);
console.log(customerId.id)
//CUSTOMER IS DEFINED
console.log(customerId.t_id);
//TOKEN UNDEFINED
}
if (card) {
console.log("success: " + JSON.stringify(card, null, 2));
} else {
console.log("something went wrong");
}
});
};
//CUSTOMER.ID WORKS; TOKEN ID NOT
let chargeCustomerThroughCustomerID = async function () {
let param = {
amount: cart.totalPrice * 100,
currency: 'eur',
description: 'First payment',
customer: customerId.id
}
return stripe.charges.create(param, function (err, charge) {
if (err) {
console.log("err: " + err);
}
if (charge) {
console.log("success: " + JSON.stringify(charge, null, 2));
} else {
console.log("Something wrong")
}
})
}
try {
const createdCustomer = await createCustomer();
const createdToken = await createToken();
const addedCardToCustomer = await addCardToCustomer(createdToken);
const chargedCustomerThroughCustomerID = await chargeCustomerThroughCustomerID();
res.send("success");
} catch (e) {
console.log(`error ${e}`)
};
});
I already answered you on this question: your function are missing async and arguments.
See answer here: Stripe.create functions not in correct order

Promises, .then and message

I've been struggling with promises and .then(), I have managed to create my two promises and they run, I think, however the ordering is still wrong, the first promise gets an updated spreadsheet and the next one does stuff to that spreadsheet and sends mails with that sheets data, no matter what I do I can't get the ordering to be correct. The message "Done fetching latest Drive sheet.." always prints after the mailing rules have run but it should be before. The getNewSheet promise should run, and then the sendMails promise should. Why isn't getNewSheet() running and then .sendMails?
Please help me understand why this isn't running and what I am doing wrong with the promises, additionally why isnt the message method of the promise return their values? I assume because the promise wasn't resolved in the correct order.
Please note I am still learning and using environment variables for some variables, I have edited out what I can, its a bit long but I am curious if I need to do something further with the containing code/functions in each promise before it would successful.
EDITED: Edited code to be more accurate.
let getNewSheet = function () {
return new Promise(function (resolve, reject) {
const fs = require("fs");
const readline = require("readline");
const {
google
} = require("googleapis");
const SCOPES = "https://www.googleapis.com/auth/drive";
const TOKEN_PATH = "token.json";
fs.readFile("credentials.json", (err, content) => {
if (err) return console.log("Error loading client secret file:", err);
authorize(JSON.parse(content), getLatestListFromDrive);
});
function authorize(credentials, callback) {
const {
client_secret, client_id, redirect_uris } = credentials.installed;
const oAuth2Client = new google.auth.OAuth2(
client_id, client_secret, redirect_uris[0]
);
fs.readFile(TOKEN_PATH, (err, token) => {
if (err) return getAccessToken(oAuth2Client, callback);
oAuth2Client.setCredentials(JSON.parse(token));
callback(oAuth2Client);
});
}
function getAccessToken(oAuth2Client, callback) {
const authUrl = oAuth2Client.generateAuthUrl({
access_type: "offline",
scope: SCOPES
});
console.log("Authorize this app by visiting this url:", authUrl);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question("Enter the code from that page here: ", code => {
rl.close();
oAuth2Client.getToken(code, (err, token) => {
if (err) return console.error("Error retrieving access token", err);
oAuth2Client.setCredentials(token);
fs.writeFile(TOKEN_PATH, JSON.stringify(token), err => {
if (err) console.error(err);
console.log("Token stored to", TOKEN_PATH);
});
callback(oAuth2Client);
});
});
}
function getLatestListFromDrive(auth) {
const drive = google.drive({
version: "v3",
auth
});
var fileId = process.env.NODE_DRIVE_FILE_ID;
var dest = fs.createWriteStream("" + appDir + "\\tmp\\feedbacklist.xlsx");
drive.files.export({
fileId: fileId,
mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
}, {
responseType: "stream"
},
function (err, res) {
res.data
.on("end", () => {
})
.on("error", err => {
console.log("Error", err);
})
.pipe(dest);
}
);
}
resolve();
});
};
var path = require("path");
var appDir = path.dirname(require.main.filename);
let sendMails = function () {
return new Promise(function (resolve, reject) {
const XLSX = require("xlsx");
const workbook = XLSX.readFile("" + appDir + "\\tmp\\feedbacklist.xlsx", {
type: "binary",
cellDates: true,
dateNF: "yyyy-mm-dd;#"
});
const SheetNameList = workbook.SheetNames;
var rows = XLSX.utils.sheet_to_json(workbook.Sheets[SheetNameList[0]], {
raw: false
});
var raw = workbook.Sheets[SheetNameList[0]];
var today = new Date();
today = today.toISOString().slice(0, 10);
var dateYesterday = new Date();
dateYesterday.setDate(dateYesterday.getDate() - 1);
dateYesterday = dateYesterday.toISOString().slice(0, 10);
var dayYesterday = new Date();
var days = [
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
];
dayYesterday.setDate(dayYesterday.getDate() - 1);
dayYesterday = days[dayYesterday.getDay()];
var dateTomorrow = new Date();
dateTomorrow.setDate(dateTomorrow.getDate() + 1);
dateTomorrow = dateTomorrow.toISOString().slice(0, 10);
var dayTomorrow = new Date();
var futureDays = [
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
];
dayTomorrow.setDate(dayTomorrow.getDate() + 1);
dayTomorrow = futureDays[dayTomorrow.getDay()];
var filteredRows = rows.filter(eachRow);
function eachRow(eachRow) {
return eachRow["tourDate"] === dateYesterday;
}
if (filteredRows != 0) {
for (x = 0; x < filteredRows.length; x++) {
console.log(filteredRows[x].emailAddress);
console.log(filteredRows[x].fName);
console.log(filteredRows[x].tourDate);
console.log(filteredRows[x].feedbacksent);
var nodemailer = require("nodemailer");
var transporter = nodemailer.createTransport({
host: process.env.NODEMAILER_SERVICE,
secure: false,
auth: {
user: process.env.NODEMAILER_USER,
pass: process.env.NODEMAILER_PASS
}
});
var mailOptions = {
from: process.env.NODEMAILER_FROM,
to: "" + filteredRows[x].emailAddress + "",
subject: "Hi",
html: "Hi ",
text: "Hi "
};
console.log("I've sent a feedback request to " + filteredRows[x].emailAddress);
transporter.sendMail(mailOptions, function (error, info) {
//check for console errors
if (error) {
console.log(error);
} else {
console.log(
"Feedback request sent to " +
filteredRows[x].emailAddress
);
};
})
}
} else {
console.log("No rows/records to process for yesterday..");
}
resolve();
});
};
getNewSheet(console.log("drive fetched")).then(function (result) {
console.log("mails sent");
return sendMails(result);
}).then(function () {
console.log("promises finished");
});
Call resolve and reject functions when the operations you had scheduled, were actually completed.
Remember that when dealing with asynchronous operations, the code you write is executed out of order, meaning not in the order it is written in the file.
Consider these examples:
Will not work
return new Promise(function (resolve, reject) {
let result
fs.someAsyncOperation(function (data) {
result = data
})
resolve(result) // the promise will resolve immediately with undefined value
}
Will work
return new Promise(function (resolve, reject) {
fs.someAsyncOperation(function (result) {
resolve(result) // the promise will resolve after async operation is completed
})
}
Your code should be reworked to wait for the results of asynchronous operation before calling resolve function.
Under your sendMessage promise function, the resolve should be inside the callback of the sendMail function since it is asynchronous.
transporter.sendMail(mailOptions, function(error,info){
resolve();
});
UPDATE
Async stuff sometimes gets real tricky especially if you're used to synchronous processes. When I started on software this got me confused to.
You're promise functions are good. The problem there was that you were resolving the send function before it actually did it's job.
So below is a simpler version of your code. Doing the same thing you did
// function that returns a message
// duration: 1s
function getMessage() {
return new Promise(resolve => {
setTimeout(() => resolve('important message'), 1000)
});
}
// function that send a message using sendEmail function
// duration should be based on sendEmail
// notice that the resolve is called inside the callback
function sendMessage(message) {
return new Promise((resolve, reject) => {
sendEmail(message, (err, result) => {
// do some stuff if email was sent
})
resolve();
});
}
// function try to send an email asynchronously using a callback
// duration: 0.5s
function sendEmail(message, callback) {
setTimeout(() => {
console.log('email sent!')
callback(null, 'message sent');
}, 500)
}
getMessage().then(result => {
return sendMessage(result);
}).then(() => {
console.log('should be done after email')
})
With the above code your terminal should show
should be done after email
email sent!
But with some modification on the sendMessage function
function sendMessage(message) {
return new Promise((resolve, reject) => {
sendEmail(message, (err, result) => {
resolve(result);
})
});
}
The terminal should now show
email sent!
should be done after email

Callback NodeJS & insert data with mongoose

I know this topic as already asked many times before but I didn't find the right answer to do what I want.
Actually, I try to save two different list of JSON object in MongoDB via Mongoose. To perform both at the same time I use 'async'.
However, when I save it with the command insertMany() I get an error because he calls the callback of async before finishing the insertMany(). Therefore answer[0] is not defined.
What will be the proper way of doing it ?
Here is my code with the async:
const mongoose = require("mongoose");
const async = require("async");
const utils = require("../utils");
const experimentCreate = function(req, res) {
let resData = {};
let experimentList = req.body.experiment;
let datasetList = req.body.datasetList;
async.parallel(
{
dataset: function(callback) {
setTimeout(function() {
answer = utils.createDataset(datasetList);
callback(answer[0], answer[1]);
}, 100);
},
experiment: function(callback) {
setTimeout(function() {
answer = utils.createExp(experimentList);
callback(answer[0], answer[1]);
}, 100);
}
},
function(err, result) {
if (err) {
console.log("Error dataset or metadata creation: " + err);
sendJSONresponse(res, 404, err);
} else {
console.log("Experiment created.");
resData.push(result.dataset);
resData.push(result.experiment);
console.log(resData);
sendJSONresponse(res, 200, resData);
}
}
);
};
Then the two functions called createExp and createDataset are the same in another file. Like this:
const createDataset = function(list) {
let datasetList = [];
for (item of list) {
let temp = {
_id: mongoose.Types.ObjectId(),
name: item.name,
description: item.description,
type: item.type,
};
datasetList.push(temp);
}
Dataset.insertMany(datasetList, (err, ds) => {
if (err) {
console.log("Error dataset creation: " + err);
return [err, null];
} else {
console.log("All dataset created.");
return [null, ds];
}
});
};
There's a few problems with your code. For one, you're not returning anything in your createDataset function. You're returning a value in the callback of insertMany but it doesn't return that value to the caller of createDataset as it's within another scope. To solve this issue, you can wrap your Dataset.insertMany in a promise, and resolve or reject depending on the result of Data.insertMany like this:
const createDataset = function(list) {
let datasetList = [];
for (item of list) {
let temp = {
_id: mongoose.Types.ObjectId(),
name: item.name,
description: item.description,
type: item.type,
};
datasetList.push(temp);
}
return new Promise((resolve, reject) => {
Dataset.insertMany(datasetList, (err, ds) => {
if (err) {
console.log("Error dataset creation: " + err);
reject(err);
} else {
console.log("All dataset created.");
resolve(ds);
}
});
});
};
Now your return object is no longer going to be an array so you won't be able to access both the error and the result via answer[0] and answer[1]. You're going to need to chain a then call after you call createDataset and use callback(null, answer) in the then call (as that means createDataset executed successfully) or use callback(err) if createDataset throws an error like below:
dataset: function(callback) {
setTimeout(function() {
utils.createDataset(datasetList).then(answer => {
callback(null, answer);
}).catch(err => callback(err)); // handle error here);
}, 100);
}
Note: You'll most likely need to alter your createExp code to be structurally similar to what I've produced above if it's also utilizing asynchronous functions.

How to avoid a callback in promise

I am new to NodeJS and JavaScript. I am badly stuck in a problem:
I want to generate QR image of 'some text' and after generating it, I want to query my MySQL database and insert the image to database.
Problem is that QRCode.toDataURL of SOLDAIR module goes in running state and query is called before the QR image is returned from the .toDataUrl function.
Hence it results in error.
I tried everything, promises, nested promises, counters, if statements etc., but I am unable to find a solution.
My code:
router.post('/generateTicket', (req,res) => {
const query1 = `SELECT * FROM booking ORDER BY bookingID DESC LIMIT 1`;
const query2 = `INSERT INTO ticket (ticket_image,BookingID) SET ?`;
let bookingID;
let count;
let ticket_data = {};
Promise.using(mysql.getSqlConn(), conn => {
conn.query(query1).then(resultOfQuery1 => {
bookingID = resultOfQuery1[0].BookingID;
count = resultOfQuery1[0].PeopleCount;
console.log("ID = " + bookingID + " people count = "+count);
promiseToCreateQRcode().then(function (URLfromResolve) {
console.log("the url is " + URLfromResolve );
}).catch(function (fromReject) {
console.log("Rejected "+ fromReject);
}); // catch of promise to create QR
}).catch(err => {
res.json({ status: 500, message: 'Error Occured in query 1 ' + err });
}); // catch of query 1
});
});
var opts = {
errorCorrectionLevel: 'H',
type: 'image/png',
rendererOpts: {
quality: 0.3
}
};
let promiseToCreateQRcode = function () {
let QRImage;
return new Promise(function (resolve,reject) {
QRCode.toDataURL('text', function (err, url) {
if (err) throw err
console.log("\n"+url+"\n");
QRImage = url;
});
if (QRImage)
resolve(QRImage);
else
reject("error occured in url");
});
};
As u can see, the program jumps to if statement and the QR image is not generated yet, hence it goes in "reject":
Try this,
let promiseToCreateQRcode = function () {
return new Promise(function (resolve,reject) {
QRCode.toDataURL('text', function (err, url) {
if (err){
reject(err); // or some message
} else {
resolve(url);
}
});
});
};
This way promise will be resolved only when toDataURL returns QR image.
Have a look at How do I convert an existing callback API to promises?. You need to call resolve or reject in the asynchronous callback!
function promiseToCreateQRcode() {
return new Promise(function(resolve,reject) {
QRCode.toDataURL('text', function (err, url) {
if (err) {
reject(err);
} else {
console.log("\n"+url+"\n");
resolve(url);
}
});
});
}
Using this extra QRImage variable like you did cannot work.

Categories