I'm trying to execute a function after another function (API call) has returned its result. The problem is, the program always ends up executing the second one before the first one has given the result.
The thing is, I need to place a contact email on a Mailing List using Mailjet, but first I have to create that contact. So, the contact creation works, but not the placement on the list, as this function is executed before the contact creation finishes.
I tried multiple things for some days, mostly using async/await, but I still don't get my head around it.
Here's my code:
routes/index.js
router.post('/', async (req, res, next) => {
const { email, name } = req.body;
const mktListId = process.env.MAILJET_ID_MARKETING;
try {
const contactCreated = await createContact(email, name);
addEmailToList(email, mktListId);
res.status(201).send({ message: 'Email Successfully subscribed to Marketing List' });
} catch (err) {
res.status(400).json({
status: 'fail',
message: err,
});
}
});
function createContact(email, name) {
const mailjet = require('node-mailjet').connect(
process.env.MAILJET_MASTER_APIPUBLIC,
process.env.MAILJET_MASTER_APISECRET
);
const request = mailjet.post('contact', { version: 'v3' }).request({
IsExcludedFromCampaigns: 'true',
Name: `${name}`,
Email: `${email}`,
});
request
.then(result => {
console.log('result mailjet create contact', result.body);
})
.catch(err => {
console.log('error mailjet create contact', err.statusCode, err.ErrorMessage);
});
}
function addEmailToList(email, listId) {
const mailjet = require('node-mailjet').connect(
process.env.MAILJET_MASTER_APIPUBLIC,
process.env.MAILJET_MASTER_APISECRET
);
const request = mailjet.post('listrecipient', { version: 'v3' }).request({
IsUnsubscribed: 'true',
ContactAlt: `${email}`,
ListID: `${listId}`,
});
request
.then(result => {
console.log('result mailjet add to list', result.body);
})
.catch(err => {
console.log('error mailjet add to list', err.statusCode, err.ErrorMessage);
});
}
Any help with be much appreciated. Thank you!
Without a promise, await doesn't really do anything.
await new Promise (resolve => {
console.log ("A");
resolve ();
});
await new Promise (resolve => {
console.log ("B");
resolve ();
});
await new Promise (resolve => {
console.log ("C");
resolve ();
});
The create function needs to look more like:
function createContact(email, name) {
return new Promise ((resolve, reject) => {
const mailjet = require('node-mailjet').connect(
process.env.MAILJET_MASTER_APIPUBLIC,
process.env.MAILJET_MASTER_APISECRET
);
const request = mailjet.post('contact', { version: 'v3' }).request({
IsExcludedFromCampaigns: 'true',
Name: `${name}`,
Email: `${email}`,
});
request
.then(result => {
console.log('result mailjet create contact', result.body);
resolve ();
})
.catch(err => {
console.log('error mailjet create contact', err.statusCode, err.ErrorMessage);
reject ();
});
});
}
For future reference, this is the clean finished and refactored code:
routes/index.js
const express = require('express');
const router = express.Router();
const emailController = require('../controllers/emailController');
router.post('/', subscriberValidationRules(), validate, emailController.create);
Then we put the router logic on:
controllers/emailController.js
const { createContact, addEmailToList } = require('../helpers/addEmailToList');
const create = async (req, res, next) => {
const { email, name } = req.body;
const mktListId = process.env.MAILJET_ID_MARKETING;
try {
await createContact(email, name);
await addEmailToList(email, mktListId);
return res.status(201).send({ message: 'Email Successfully subscribed to Marketing List' });
} catch (err) {
res.status(400).json({
status: 'fail',
message: err,
});
}
}
module.exports = {
create,
};
And then our functions on:
helpers/addEmailToList.js
const createContact = async (email, name) => {
try {
const mailjet = require('node-mailjet').connect(
process.env.MAILJET_MASTER_APIPUBLIC,
process.env.MAILJET_MASTER_APISECRET,
)
const { body } = await mailjet.post('contact', { version: 'v3' }).request({
IsExcludedFromCampaigns: 'true',
Name: `${name}`,
Email: `${email}`,
})
console.info('result mailjet create contact', body)
return body
} catch (err) {
console.info('error mailjet create contact', err.statusCode, err.ErrorMessage)
}
}
const addEmailToList = async (email, listId) => {
const mailjet = require('node-mailjet').connect(
process.env.MAILJET_MASTER_APIPUBLIC,
process.env.MAILJET_MASTER_APISECRET
);
try {
const { body } = await mailjet.post('listrecipient', { version: 'v3' }).request({
IsUnsubscribed: 'true',
ContactAlt: `${email}`,
ListID: `${listId}`,
});
console.info('result mailjet add to list', body);
return body;
} catch (err) {
console.info('error mailjet add to list', err.statusCode, err.ErrorMessage);
}
};
module.exports = {
createContact,
addEmailToList,
};
Related
I have been working on my first node.js backend and I am trying to refactor the login function, but i am not understanding something about promise chaining and error handling. I am hoping someone can help point out what I am missing here.
I have read about promise chaining and I understand that one catch should be able to handle any error in the chain, but I cant seem to get it to work without having an individual .catch for every .then
Here is my userLogin function.
const loginUser = handleAsync(async (req, res, next) => {
let userEmail = req.body.email;
let submittedPassword = req.body.password;
userLogin(userEmail, submittedPassword)
.then((userObject) => {
res.json(userObject);
})
.catch((err) => {
res.status(401).send(err);
console.log("any error inside userLogin: " + err);
});
});
async function userLogin(email, submittedPassword) {
console.log("in user login");
return new Promise(function (resolve, reject) {
getUserAccountByEmail(email)
.then((userAccountResults) => {
let email = userAccountResults[0].email;
let storedPassword = userAccountResults[0].password;
//the user exists check the password
//the user exists. Now Check password.
checkUserPassword(submittedPassword, storedPassword)
.then((passwordIsAMatch) => {
//The password is a match. Now Generate JWT Token.
generateJWTToken(email)
.then((tokens) => {
//build json object with user information and tokens.
createUserDataObject(tokens, userAccountResults)
.then((userObject) => {
//send the user object to the front end.
resolve(userObject);
})
.catch((err) => {
reject(err);
});
})
.catch((err) => {
reject(err);
});
})
.catch((err) => {
reject(err);
});
})
.catch((err) => {
reject(err);
});
});
}
And here is one of the functions that is a part of the chain
function getUserAccountByEmail(email) {
return new Promise(function (resolve, reject) {
logger.info("Looking up user email");
const users = User.findAll({
where: {
email: email,
},
limit: 1,
attributes: [
"email",
"password",
"userId",
"stripeConnectAccountId",
"isStripeAccountSet",
"stripeCustomerId",
],
})
.then((userResults) => {
if (doesUserExist(userResults)) {
resolve(userResults);
} else {
console.log("user doesnt exist in getuseraccount");
reject("User Email Does Not Exist In Database");
}
})
.catch((error) => {
console.log("Error Accessing Database: UserAccountByEmail");
logger.error("Error in getUserAccountByEmail: " + error);
});
});
}
Any help would be appreciated. Thanks
I took jfriend00 's advice and refactored using await instead of nesting.
async function userLogin(email, submittedPassword) {
return new Promise(async function (resolve, reject) {
try {
//Get User Account Information
const userAccountResults = await getUserAccountByEmail(email);
//Password Authentication
let storedPassword = userAccountResults[0].password;
const passwordIsAMatch = await checkUserPassword(
submittedPassword,
storedPassword
);
//Generate JWT Tokens
const tokens = await generateJWTToken(email);
//Prepare user data JSON for sending to the frontend.
const userData = await createUserDataObject(tokens, userAccountResults);
resolve(userData);
} catch (error) {
reject(error);
}
});
}
The problem I'm having is that when using module.exports I'm getting the output as text.
profile.js
const dbProfile = require("./dbProfile");
async function User(interaction) {
const embed = new EmbedBuilder()
.setColor(0x0099ff)
.setTitle("user profile ")
.setURL("https://https://www.youtube.com/watch?v=dQw4w9WgXcQ")
.setDescription(`ExarID: ${dbProfile.profileInfo}`);
await interaction.reply({
ephemeral: true,
embeds: [embed],
fetchReply: true,
});
return 0;
}
dbProfile.js
const db = require("./dbStart");
module.exports = {
profileInfo: function () {
console.log(db.users);
return db.users;
},
};
dbStart.js
exports.users = function () {
conn.query("SELECT ExarID FROM users", function (err, result, fields) {
if (err) {
console.log(err);
}
console.log(JSON.parse(result));
return result;
});
};
What the bot replies:
You're never running your functions, and the .toString() of uncalled functions returns the source code.
You need to change your code to call the functions
in dbProfile.js
const users = db.users();
console.log(users)
return users;
in profile.js
.setDescription(`ExarID: ${dbProfile.profileInfo()}`)
You also have a bug in dbStart.js since you're never actually returning anything (see How do I return the response from an asynchronous call?)
This involves changing all your code to work asynchronously
profile.js
const dbProfile = require("./dbProfile");
async function User(interaction) {
const profileInfo = await dbProfile.profileInfo();
const embed = new EmbedBuilder()
.setColor(0x0099ff)
.setTitle("user profile ")
.setURL("https://https://www.youtube.com/watch?v=dQw4w9WgXcQ")
.setDescription(`ExarID: ${profileInfo}`);
await interaction.reply({
ephemeral: true,
embeds: [embed],
fetchReply: true,
});
return 0;
}
dbProfile.js
const db = require("./dbStart");
module.exports = {
profileInfo: async function () {
const users = await db.users();
console.log(users);
return users;
},
};
dbStart.js
exports.users = function () {
return new Promise((resolve) => {
conn.query("SELECT ExarID FROM users", function (err, result, fields) {
if (err) {
console.log(err);
}
console.log(JSON.parse(result));
resolve(result);
});
});
};
exports.signupController = async (req, res) => {
const { phone, password } = req.body;
try {
const user = await User.findOne({ phone }).exec()
if (user) {
return res.status(400).json({
errorMessage: 'Phone Number already exists',
});
}
const newUser = new User();
newUser.phone = phone;
const salt = await bcrypt.genSalt(10);
newUser.password = await bcrypt.hash(password, salt);
await newUser.save();
return res.status(200).json({
successMessage: 'Registration success. Please login',
});
} catch (err) {
console.log('signupController error: ', err);
res.status(500).json({
errorMessage: 'Server error',
});
}};
**I upload a node application in shared hosting! **
*But an error was showing in this controller function. All the time the catch block is running on the json. The error is unhandled promise rejection. *
signup(data)
.then((response) => {
console.log('Axios signup success: ', response);
setFormData({
phone: '',
password: '',
password2: '',
loading: false,
successMsg: response.data.successMessage,
});
history.push('/signin');
})
.catch((err) => {
console.log('Axios signup error: ', err);
setFormData({
...formData,
loading: false,
errorMsg: err.response.data.errorMessage,
});
});
this is react front end event handler
import axios from 'axios';
export const signup = async (data) => {
const config = {
headers: {
'Content-Type': 'application/json',
},
};
const response = await axios.post('/api/auth/signup', data, config);
return response;
};
the signup api function
Mongoose queries are not promises. They have a .then() function for co and async/await as a convenience. If you need a fully-fledged promise, use the .exec() function. for example:
const query = Band.findOne({name: "Guns N' Roses"});
assert.ok(!(query instanceof Promise));
// A query is not a fully-fledged promise, but it does have a `.then()`.
query.then(function (doc) {
// use doc
});
// `.exec()` gives you a fully-fledged promise
const promise = query.exec();
assert.ok(promise instanceof Promise);
promise.then(function (doc) {
// use doc
});
If you are using exec() on your findOne query you should use:
exports.signupController = async (req, res) => {
const { phone, password } = req.body;
try {
const user = await User.findOne({ phone }).exec();
/// just a pseudo code
user.then('do your things').catch( 'log error')
const newUser = new User();
newUser.phone = phone;
const salt = await bcrypt.genSalt(10);
newUser.password = await bcrypt.hash(password, salt);
await newUser.save();
return res.status(200).json({
successMessage: 'Registration success. Please login',
});
} catch (err) {
console.log('signupController error: ', err);
res.status(500).json({
errorMessage: 'Server error',
});
}};
for more details check this out: https://mongoosejs.com/docs/promises.html#should-you-use-exec-with-await?
I have this test:
describe('createNote', () => {
beforeEach(() => {
res = {
json: sinon.spy(),
sendStatus: sinon.spy(),
};
});
afterEach(() => {
noteService.createUserNote.restore();
});
it('should return user note object', async () => {
// Arrange
modelResponse = {
id: 1,
userId: req.user.id,
...req.body,
};
sinon.stub(noteService, 'createUserNote')
.resolves(modelResponse);
// Act
await userController.createNote(req, res);
// Assert
sinon.assert.calledWith(
noteService.createUserNote,
req.user,
req.body.note,
);
sinon.assert.calledWith(res.json, { note: modelResponse });
});
It fails on line sinon.assert.calledWith(res.json, { note: modelResponse });
I don't really understand sinon so I'm not sure why though.
This is my userController code:
createNote: async (req, res, next) => {
try {
const createNote = await noteService.createUserNote(
req.user,
req.body.note,
);
const note = await noteService.getUserNote(
req.user.id,
createNote.id,
);
return res.json({ note });
} catch (err) {
return next(err);
}
},
I recently changed it from this so assume something in what I've done has caused the test to fail:
createNote: async (req, res, next) => {
try {
const note = await noteService.createUserNote(
req.user,
req.body.note,
);
return res.json({ note });
} catch (err) {
return next(err);
}
},
This is the error I get:
1) User userController
createNote
should return user note object:
AssertError: async (user, text) => {
const [note] = await db.Note.createUserNote(user.id, text, db);
await emailService.userAlert(text, user.name);
return note;
} is not stubbed
at Object.fail (node_modules/sinon/lib/sinon/assert.js:106:21)
at /opt/atlassian/pipelines/agent/build/node_modules/sinon/lib/sinon/assert.js:35:24
at Array.forEach (<anonymous>)
at verifyIsStub (node_modules/sinon/lib/sinon/assert.js:22:5)
at Object.assert.(anonymous function) [as calledWith] (node_modules/sinon/lib/sinon/assert.js:77:9)
at Context.it (app/__tests__/controllers/user/userController.test.js:56:20)
at <anonymous>
Can anybody explain what is wrong and how to fix this?
You need to mock getUserNote as well. After the change, you are getting note from getUserNote and then sending it to res.json
But in the test case you have not stubbed it. Try adding this in the test case:
sinon.stub(noteService, 'getUserNote')
.resolves(modelResponse);
I used this method because I am storing an array of classified messages, I would like to vividly understand why it doesn't update.
Here's the db.js:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const ObjectId = mongoose.Types.ObjectId;
const usersessionSchema = new Schema({
fb_id: String,
fb_name: String,
fb_profpic: String,
message_body: [
{
message: String,
message_type: String,
timestamp: String
}
],
admin: Boolean,
branch: String
});
const model = (prefix) => {
prefix = prefix || '';
console.log(prefix);
if (prefix.length > 0) {
return mongoose.model(prefix + "-usersessions", usersessionSchema);
} else {
return new Error('Undefined collection prefix!');
}
}
/** Push message into message body*/
module.exports.pushsession =
async(model, id, data) => {
return new Promise((resolve, reject) => {
console.log(data);
model.findOneAndUpdate({fb_id: id}, {$push: {data}},{safe: true})
.then(res => {
console.log(res);
/
resolve(res);
})
.catch(err => {
reject(err);
console.log(err);
throw err;
});
});
}
Here's the controller.js:
/** Push usersession message */
module.exports.pushsession =
async(req, res, next) => {
try {
//jwt.validateToken(req);
var en = "en";
var dateEn = moment().locale(en);
format = "MM/DD/YYYY h:mm:ss A"; //h:mm:ss.SSS if you want miliseconds
var datetime_now = dateEn.format(format);
console.log(datetime_now);
var request = {
message_body: {
message: req.body.message,
message_type: req.body.message_type,
timestamp: datetime_now
}
};
const model = usersessionDB(req.query['client']);
const id = req.body.fb_id;
const result = await usersessionDB.pushsession(model, id, request);
if (result) {
response.success(res, next, result, 200, response.HTTP_STATUS_CODES.ok);
} else {
response.failure(res, next, {
message: 'ID does not exist'
}, 404, response.HTTP_STATUS_CODES.not_found);
}
} catch (err) {
response.failure(res, next, err, 500, response.HTTP_STATUS_CODES.internal_server_error);
}
}
Here's the route.js:
const controller = require('../controller/usersession-controller');
module.exports =
(server) => {
server.post('/api/session', controller.create);
server.get('/api/session', controller.list);
server.get('/api/session/:id', controller.get);
server.put('/api/session/:id', controller.update);
server.del('/api/session/:id', controller.delete);
server.put('/api/pushsession', controller.pushsession);
}
Visually, if you run this using postman, you can see that it display the one I want to search and update
Result of the postman
What I want to happen is to insert another set of array inside that message_body
Data I've inserting
My desired output
This code is working without that promise something, but in my project it is needed so I can't remove that thing.
So, based on :
This code is working without that promise something
i can point a thing or two,
in db.js
module.exports.pushsession =
async(model, id, data) => {
return new Promise((resolve, reject) => {
you don't need async since you're returning a promise so replace this
async(model, id, data) => {
with
(model, id, data) => {
and since you're returning a promise and removed async , you don't need the await on the other side ( controller.js ), so this
const result = await usersessionDB.pushsession(model, id, request);
if (result) {
response.success(res, next, result, 200, response.HTTP_STATUS_CODES.ok);
} else {
should be
usersessionDB.pushsession(model, id, request).then(
(result) => { // when resolved
response.success(res, next, result, 200, response.HTTP_STATUS_CODES.ok);
},
(err) => { // when rejected
response.failure(res, next, {
message: 'ID does not exist'
}, 404, response.HTTP_STATUS_CODES.not_found);
});
this is a comparison between async/await and promises : Javascript Promises vs Async Await. Difference?
and here's some good examples of using promises : https://medium.com/dev-bits/writing-neat-asynchronous-node-js-code-with-promises-32ed3a4fd098
i think your $push is ok but you already said
This code is working without that promise something
i hope this helps and Good luck :)
I tried cleaning my code
here's the controller.js:
/** Push usersession message */
module.exports.pushsession =
async (req, res, next) => {
try {
//jwt.validateToken(req);
var en = "en";
var dateEn = moment().locale(en);
format = "MM/DD/YYYY h:mm:ss A"; //h:mm:ss.SSS if you want miliseconds
var datetime_now = dateEn.format(format);
console.log(datetime_now);
var data = {
message: req.body.message,
message_type: req.body.message_type,
timestamp: datetime_now
};
const model = usersessionDB(req.query['client']);
const id = req.body.fb_id;
console.log(id);
const result = await usersessionDB.pushsession(model, id, data).then(
(result) => { // when resolved
response.success(res, next, result, 200, response.HTTP_STATUS_CODES.ok);
},
(err) => { // when rejected
response.failure(res, next, {
message: 'ID does not exist'
}, 404, response.HTTP_STATUS_CODES.not_found);
});
} catch (err) {
response.failure(res, next, err, 500, response.HTTP_STATUS_CODES.internal_server_error);
}
}
Here's the db.js:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const ObjectId = mongoose.Types.ObjectId;
const usersessionSchema = new Schema({
fb_id: String,
fb_name: String,
fb_profpic: String,
message_body:[{
message: String,
message_type: String,
timestamp: String
}],
admin: Boolean,
branch: String
});
/** Push message into message body*/
module.exports.pushsession =
async(model, id, data) => {
console.log(data);
return new Promise((resolve, reject) => {
model.findOneAndUpdate({fb_id: id}, { $push: { message_body: data }})
.then(res => {
console.log(res);
resolve(res);
})
.catch(err => {
reject(err);
console.log(err);
throw err;
});
});
}
Out of the blue after I tried to replace $push with $set then again I replace it with $push, it worked.
I don't if there's a difference, or I miss something, feel free to point it out.