Syntax for chaining mongoose promises - javascript

I am relatively new to using Promises and MongoDB / Mongoose, and am trying to chain a flow through several database queries into an efficient and reliable function.
I want to know if my final function is a good and reliable way of achieving what I want, or if there are any issues or any improvements that can be made.
The process is as follows:
1) Check whether or not the user already exists
usersSchema.findOne({
email: email
}).then(res => {
if(res==null){
// user does not exist
}
}).catch(err => {});
2) Add the new user to the database
var new_user = new usersSchema({ email: email });
new_user.save().then(res => {
// new user id is res._id
}).catch(err => {});
3) Assign a free promotional code to the user
codeSchema.findOneAndUpdate({
used: false,
user_id: true
},{
used: true,
user_id: mongoose.Types.ObjectId(res._id)
}).then(res => {
// user's code is res.code
}).catch(err => {});
Obviously, each query needs to execute in sequence, so after a lot of research and experimentation into how to do this I have combined the queries into the following function, which seems to be working fine so far:
function signup(email){
// check email isn't already signed up
return usersSchema.findOne({
email: email
}).then(res => {
if(res==null){
// add to schema
var new_user = new usersSchema({ email: email });
// insert new user
return new_user.save().then(res => {
var result = parse_result(res);
// assign a code
return codesSchema.findOneAndUpdate({
used: false,
user_id: true
},{
used: true,
user_id: mongoose.Types.ObjectId(result._id),
});
});
}else{
return 'The user already exists';
}
});
}
signup('test#test.com').then(res => {
console.log('success, your code is '+res.code);
}).catch(err => {
console.log(err);
});
I'm still trying to get my head around exactly how and why this works - the function is returning a promise, and each nested promise is returning a promise.
My main concerns are that there is a lot of nesting going on (is there perhaps a way to do this by chaining .then() callbacks instead of nesting everything?) and that the nested promises don't appear to have error catching, although as the signup() function itself is a promise this seems to catch all the errors.
Is anyone knowledgeable on the subject able to confirm whether my process looks good and reliable or not? Thanks!

To avoid indentation-hell, if you return a value from the function passed to a Promise's .then()-method, you can chain multiple .then() as a neat and tidy pipeline. Note that you can also return a Promise that has pending status, and then next function in line will execute when it has resolved.
function signup (email) {
return usersSchema.findOne({
email: email
}).then(res => {
if (res) throw 'The user already exists'
var new_user = new usersSchema({ email: email })
return new_user.save()
}).then(res => {
var result = parse_result(res)
return codesSchema.findOneAndUpdate({
used: false,
user_id: true
},{
used: true,
user_id: mongoose.Types.ObjectId(result._id)
})
})
}
Even better, if you have the possibility to use async/await (Node v7.6 or above), your code can look like normal blocking code:
async function signup (email) {
let user = await usersSchema.findOne({ email: email })
if (user) throw 'The user already exists'
let new_user = await new usersSchema({ email: email }).save()
let result = parse_result(new_user)
return codesSchema.findOneAndUpdate({
used: false,
user_id: true
},{
used: true,
user_id: mongoose.Types.ObjectId(result._id)
})
}
Your original function call code works on both without changes.

you code can be improve in this way
function signup(email){
// check email isn't already signed up
return usersSchema.findOne({
email: email
}).then(res => {
if(res==null){ // add to schema
var new_user = new usersSchema({ email: email });
// insert new user
return new_user.save()
}else{
return Promise.reject(new Error('The user already exists'));
}
})
.then(res => {
var result = parse_result(res);
// assign a code
return codesSchema.findOneAndUpdate({used: false,user_id: true},{used: true,user_id: mongoose.Types.ObjectId(result._id),});
});
}
signup('test#test.com').then(res => {
console.log('success, your code is '+res.code);
}).catch(err => {
console.log(err);
});

Related

Using NodeJS, Why is my data returning as undefined despite logging to the console [duplicate]

This question already has answers here:
How do I return the response from an asynchronous call?
(41 answers)
Closed last year.
I have am building an app in react and using node for my backend, which in turn connects to a cloud mongodb instance. The data is retrieved successfully from the database and logs to the console, but the returned result is showing as undefined. I am confused as to why and cant seem to solve it.
here is the function in mongodb 'utils' to retrieve the data
exports.getUserByUidDb = async function (uid){
try{
const client = db.getDbClient()
await client.connect()
const ark = client.db('ark');
const usersColl = ark.collection('users')
console.log(`getting user by uid: ${uid}`)
query = { "uid": uid }
await usersColl.findOne(query).then(result => {
if (result){
console.log("user found. result below")
console.log(result)
return result
}
else{
return None
}
})
}
catch (err){
console.log(`Error: ${err}`)
}
}
and here is the login route in 'users.js'
var express = require('express');
var router = express.Router();
var dbUtils = require('./mongodb/utils');
const login = async (req) => {
var user = {}
// const result = await dbUtils.getUserByUidDb(req.body.uid)
dbUtils.getUserByUidDb(req.body.uid).then(result => {
console.log("user obtained, below")
console.log(result)
if(result){
console.log("user found")
user = {
uid: result.uid,
email: result.email,
emailVerified: result.emailVerified,
storageQuota: result.storageQuota,
createdAt: result.createdAt,
}
console.log("returning existing user (below")
console.log("Existing user")
return user
}else{
console.log("no user found, creating a new one")
user = {
uid: req.body.uid,
email: req.body.email,
emailVerified: false,
storageQuota: 0,
createdAt: new Date()
}
dbUtils.saveUserDb(user)
return user
}
})
}
router.post('/login', function(req, res){
login(req).then(result => {
console.log("result:")
console.log(result)
res.json({'user': result})
})
});
router.post('/logout', function(req, res){
res.send('logged out');
});
module.exports = router;
and here is my console.log on trying to login
[nodemon] starting `node src/index.js`
result:
undefined
getting user by uid: 1kUXYYw1kaMPZxCs3jftjk3z7nC2
user found. result below
{
_id: new ObjectId("61f6db61d47bce3577ac5f66"),
uid: '1kUXYYw1kaMPZxCs3jftjk3z7nC2',
email: 'leerobinson1984#gmail.com',
emailVerified: false,
storageQuota: 0,
createdAt: 2022-01-30T18:39:29.411Z
}
user obtained, below
undefined
no user found, creating a new one
Trying to save user with uid: 1kUXYYw1kaMPZxCs3jftjk3z7nC2
user exists
Cannot Save User
I thought using the 'then' keyword would mean that code following would not execute until after the promise has resolved (or rejected), but it seems to be executing straight away.
What am I doing wrong?
help appreciated as always
Because you use the return inside the then method.
You need to assign the result in a variable and then return the value, like this:
const result = await usersColl.findOne(query);
// All your stuff (logging etc.)
return result;

Separating Mongoose code from Express Router

So basically, I'm trying to separate my code that handles data (mongoose) from my express Router code, since I might want to use it elsewhere too.
The first thing I did was, I got rid of the res.json() calls, since I don't want the code to only work returning a http response. I want it to return data, so I can then return that data from my router as a http response, but still use it as regular data elsewhere.
Here is a function I wrote to get data from mongoose.
module.exports.user_login = data => {
console.log(data);
ModelUser.findOne({email: data.email}).then(user => {
if(!user){
console.log({email: 'E-mail address not found'});
return {
status: response_code.HTTP_404,
response: {email: 'E-mail address not found'}
}
}
bcrypt.compare(data.password, user.password).then(isMatch => {
if(!isMatch){
console.log({password: 'Invalid password'});
return {
status: response_code.HTTP_400,
response: {password: 'Invalid password'}
}
}
const payload = {
id: user.id,
email: user.email
};
jwt.sign(
payload,
config.PASSPORT_SECRET,
{
expiresIn: "1h"
},
(err, token) => {
console.log({
status: response_code.HTTP_200,
response: {
success: true,
token: token
}
});
return {
status: response_code.HTTP_200,
response: {
success: true,
token: token
}
}
}
);
});
});
};
When this code gets executed in my route like so:
router.post("/login", (req, res) => {
const { errors, isValid } = validateLogin(req.body);
if(!isValid) return res.status(400).json(errors);
console.log("ret", dm_user.user_login(req.body));
});
The log says the return value of user_login() is undefined, even though right before the return statement in user_login() I am logging the exact same values and they are getting logged.
Before I changed it to a log, I tried to store the return value in a variable, but obviously that remained undefined as well, and I got the error: 'Cannot read propery 'status' of undefined' when trying to use the value.
I am definitely missing something..
Well you have an small callback hell here. It might be a good idea to go with async / await and splitting up your code into smaller chunks instead of putting everyhing in 1 file.
I rewrote your user_login function:
const { generateToken } = require("./token.js");
module.exports.user_login = async data => {
let user = await ModelUser.findOne({ email: data.email });
if (!user) {
console.log({ email: "E-mail address not found" });
return {
status: response_code.HTTP_404,
response: { email: "E-mail address not found" }
};
}
let isMatch = await bcrypt.compare(data.password, user.password);
if (!isMatch) {
console.log({ password: "Invalid password" });
return {
status: response_code.HTTP_400,
response: { password: "Invalid password" }
};
}
const payload = {
id: user.id,
email: user.email
};
let response = await generateToken(
payload,
config.PASSPORT_SECRET,
response_code
);
return response;
};
I have moved your token signing method into another file and promisfied it:
module.exports.generateToken = (payload, secret, response_code) => {
return new Promise((res, rej) => {
jwt.sign(
payload,
secret,
{
expiresIn: "1h"
},
(err, token) => {
if (err) {
rej(err);
}
res({
status: response_code.HTTP_200,
response: {
success: true,
token: token
}
});
}
);
});
};
Now you need to change your router function into an async:
router.post("/login", async (req, res) => {
const { errors, isValid } = validateLogin(req.body);
if(!isValid) return res.status(400).json(errors);
let result = await dm_user.user_login(req.body);
console.log(result);
});
In addition: You get undefined because you return your value to an callback function
I also would seperate your routes from your controllers instead of writing your code inside an anonymous function
Please notice that whenever you are trying to return any value you are always present in the callback function and that is definitely not going to return any value to its intended place.
There are a couple of things you can improve about your code :
1.Donot use jwt inside your code where you are making database calls, instead move it where your routes are defined or make a separate file.
2.If you are intending to re-use the code, I would suggest you either use async-await as shown in the answer above by Ifaruki or you can use something like async.js. But the above shown approach is better.
Also always use 'error' field when you are making db calls like this:
ModelUser.findOne({email: data.email}).then((error,user) => {

Promise chaining with mongoDB (and mongoose). How to use .save() after .then() and correctly breakout of a promise chain if a response has been sent?

I have the following code for signing up a user. Where I first validate the user input. Secondly I check to see if the user already exists, if yes it should return with response 400. If not go to step 3 and add the new user. Finally in step 4 return the newly created entry. Logically, it works and adds data to database correctly, however it always responds back with 'User already exists' on postman (from step 2) even if it's a new user which has correctly added the user to the db. Which makes me think the third step is being done before a response in step 2 can be sent, which would mean I have not chained the promise correctly. Also the new user is never sent back as response, which I think is because I have not used Promise.then() together with user.save() correctly. I also get the following error (posted after the code), which I understand means I am trying to send a second response after a first has already been sent. I can solve this problem with async and await but want to learn how to do it this way. Thanks, any help is appreciated.
const { User, validateUser } = require('../models/userModel');
const mongoose = require('mongoose');
const express = require('express');
const router = express.Router();
router.post('/', (req, res) => {
return Promise.resolve()
.then(() => {
//Step 1: validae the user input and if there is an error, send 400 res and error message
console.log('My user post body req::', req.body);
const { error } = validateUser(req.body); //this is using Joi.validate() which has a error property if errors are found
if (error) {
return res.status(400).send(error.details[0].message);
}
})
.then(() => {
//step 2: check if user already exists, if yes send res 400
let user = User.findOne({ email: req.body.email });
if (user) {
return res.status(400).send('User already exists');
}
})
.then(() => {
//Step 3: enter new user into the database
user = new User({
name: req.body.name,
email: req.body.email,
password: req.body.password
});
return user.save();
})
.then((result) => {
//step 4: return the newly added user
return res.status(200).send(result);
})
.catch((error) => {
console.log('Error Adding new User', error);
});
});
module.exports = router;
I get the following error message from the catch. Even though I am I am returning with every response
Error Adding new User Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
at ServerResponse.setHeader (_http_outgoing.js:494:11)
at ServerResponse.header (/home/ssaquif/WebDevProjects/movie-reviews-backend/node_modules/express/lib/response.js:771:10)
at ServerResponse.send (/home/ssaquif/WebDevProjects/movie-reviews-backend/node_modules/express/lib/response.js:170:12)
at ServerResponse.json (/home/ssaquif/WebDevProjects/movie-reviews-backend/node_modules/express/lib/response.js:267:15)
at ServerResponse.send (/home/ssaquif/WebDevProjects/movie-reviews-backend/node_modules/express/lib/response.js:158:21)
at /home/ssaquif/WebDevProjects/movie-reviews-backend/routes/users.js:35:27
at processTicksAndRejections (internal/process/task_queues.js:93:5) {
code: 'ERR_HTTP_HEADERS_SENT'
You don't need to use Promise.resolve in your route.
You just need a chain of then blocks, in which you need to return a value to the next one.
I refactored your code like this:
router.post("/", (req, res) => {
//Step 1: validate the user input and if there is an error, send 400 res and error message
console.log("My user post body req::", req.body);
const { error } = validateUser(req.body);
if (error) {
return res.status(400).send(error.details[0].message);
}
//step 2: check if user already exists, if yes send res 400
User.findOne({ email: req.body.email })
.then(user => {
if (user) {
return res.status(400).send("User already exists");
}
return;
})
.then(() => {
//Step 3: enter new user into the database
let user = new User({
name: req.body.name,
email: req.body.email,
password: req.body.password
});
return user.save();
})
.then(result => {
//step 4: return the newly added user
return res.status(200).send(result);
})
.catch(error => {
console.log("Error Adding new User", error);
res.status(500).send("Error");
});
});
You will have a result like this when a user successfully registers:
{
"_id": "5dd65df52f7f615d8067150d",
"name": "ssaquif",
"email": "test#test.com",
"password": "123123",
"__v": 0
}
And when an existing email is used, the response will be like this with statusCode 400.
User already exists
You could solve this somehow by chaining promises correctly in a more complicated way, or you use async / await and get rid of all those problems:
router.post('/', async (req, res) => {
try {
//Step 1: validae the user input and if there is an error, send 400 res and error message
console.log('My user post body req::', req.body);
const { error } = validateUser(req.body); //this is using Joi.validate() which has a error property if errors are found
if (error) {
return res.status(400).send(error.details[0].message);
}
//step 2: check if user already exists, if yes send res 400
let user = await User.findOne({ email: req.body.email });
if (user) {
return res.status(400).send('User already exists');
}
//Step 3: enter new user into the database
user = new User({
name: req.body.name,
email: req.body.email,
password: req.body.password
});
await user.save();
//step 4: return the newly added user
return res.status(200).send(user);
} catch(error) {
// Report error internally
return res.status(500).send("Something bad happened");
}
});
The main problem with your code is that returning from a .then callback will continue executing the next .then callback. Therefore you try to set the headers status multiple times (but that's your smallest problem).
If you look at the error message "Cannot set headers after they are sent to the client" it means you are trying to send something over the response object twice which are not possible. Try log something right before each time you send something as a response and see which two are being called.
Instead of returning the res.status(400).send promise, try call it normally and then return a rejected promise or throw an error instead.

React Formik not handling errors properly

I'm using the basic Formik template to work on a Login Form.
onSubmit={(
values,
{ setSubmitting, setErrors /* setValues and other goodies */ }
) => {
props.logMeIn(values);
// LoginToSystem(values).then(
// user => {
// setSubmitting(false);
// // do whatevs...
// // props.updateUser(user)
// },
// errors => {
// setSubmitting(false);
// // Maybe transform your API's errors into the same shape as Formik's
// //setErrors(transformMyApiErrors(errors));
// console.log(errors);
// }
// );
}}
This problem is within the onSubmit section; The demo code is commented out but it uses a LoginToSystem function that seems to be a promise. I can not figure out 'what' this function is supposed to me. My function that handles this would be props.logMeIn() - Which also does not work as intended
If the login is successful, it will currently work as expected, and everything is fine. However, if the login fails (404, 401, whatever) the form will remain there, and the setSubmitting log stays there so Submit is grayed out but nothing is done.
If I try to replace LoginToSystem with my function, I get an error on the .then that I can't perform .then on undefined.
I'm wondering if perhaps this is because my function is not set up like a Promise?
loginClickHandler = (user) => {
let userObj = {
email: user.email,
password: user.password
}
axios.post('api/v1/auth/sign_in', userObj)
.then((res) => {
console.log(res.headers);
let loggedInUser = {
'access_token': res.headers['access-token'],
'client': res.headers['client'],
'uid':res.headers['uid'],
'signedIn': true
};
this.setState({
user: loggedInUser
})
this.props.retrieve(user.email);
})
.catch((err) => {
console.log(err);
return err
})
};
My function does properly catch (Thanks to axios) on the .then/.catch, but perhaps I am supposed to modify those to provide a callback so that onSubmit can properly fire?
With some guidance I was able to resolve this one simpler. Axios is natively returning a 'promise' so I just needed to ensure the outcome of the function was axios' method in the end.
loginClickHandler = (user) => {
let userObj = {
email: user.email,
password: user.password
}
const request = axios.post('api/v1/auth/sign_in', userObj);
request.then((res) => {
console.log(res.headers);
let loggedInUser = {
'access_token': res.headers['access-token'],
'client': res.headers['client'],
'uid': res.headers['uid'],
'signedIn': true
};
this.setState({user: loggedInUser, auth: true, anchorEl: null})
}).catch((err) => {
console.log(err);
// setErrors({ test: 'This was an error' })
})
return request;
};
In onSubmit there's a second argument for setting your errors. I added flow to be able to see the types better in this answer for you.
<Formik
initialValues={...}
... // Other Props
onSubmit={this.handleSubmit} // This is where you handle your login logic
render={this.renderForm} // Render your form here.
You have a callback to help you set errors in the second argument
handleSubmit = (
user: FormValues,
{ setErrors }: FormikActions<FormValues>
) => {
return axios.post('api/v1/auth/sign_in', userObj)
...
.catch(e) => {
setErrors({ username: 'Invalid Username' }) // Object you want to put here.
}
}
In your render form function you now have errors that you can use based on what you called in your setErrors
renderForm = ({
... // These are your other FormikProps you're using
errors // You want to use this
}: FormikProps<FormValues>) => (
... // Your rendering here
// errors.username
)
For flow types on Formik
https://github.com/flowtype/flow-typed/blob/master/definitions/npm/formik_v0.9.x/flow_v0.53.x-/formik_v0.9.x.js

How do I pass arguments to a passport strategy callback using a bluebird promise?

I’m trying to promisify passport.js’s local strategy. I'm new to both promises and passport, and I’m relying heavily on this comment thread, which deals with passing additional arguments to passport’s done() callback using bluebird’s Promise library. This comment resulted in a new bluebird implementation that handles additional callback parameters, but I can’t get it working in my own code:
const passport = require('passport');
const User = require('../models/user');
const LocalStrategy = require('passport-local');
const NoMatchedUserError = require('../helpers/error_helper').NoMatchedUserError;
const NotActivatedError = require('../helpers/error_helper').NotActivatedError;
const localOptions = { usernameField: 'email' };
const localLogin = new LocalStrategy(localOptions, function(email, password, done) {
let user;
User.findOne({ email: email }).exec()
.then((existingUser) => {
if (!existingUser) { throw new NoMatchedUserError('This is not a valid email address.'); }
user = existingUser;
return user.comparePassword(password, user.password);
})
.then((isMatch) => {
if (!isMatch) { throw new NoMatchedUserError('This is not a valid password.'); }
return user.isActivated();
})
.then((isActivated) => {
if (!isActivated) { throw new NotActivatedError('Your account has not been activated.'); }
return user;
})
.asCallback(done, { spread: true });
});
The user is able to validate without a problem. It’s authentication failure that I’m having an issue with: done(null, false, { message: ‘message’} obviously isn’t being called in the .asCallback method. I’m pretty sure it has to do with throwing an error, so instead I tried using this:
if (!existingUser) { return [ false, { message: 'This is not a valid email address.' } ]; }
But returning an array also doesn't work since it gets passed down the promise chain and breaks the code.
Any thoughts?

Categories