Why does using mongoose callback result in saving data twice? - javascript

I have been wondering why adding a callback to the mongoose findOneAndUpdate function results in saving the data twice to the DB?
public async addPersonAsFavorite(userId: string, friendId: string) {
if (!await this.isPersonAlreadyFriend(userId, friendId)) {
const friendList = FriendsList.findOneAndUpdate(
{ _id: userId },
{ $push: { friendsList: friendId } },
{ upsert: true, new: true },
(err, data) => {
if (err) console.error(err);
return data;
}
);
return friendList;
}}
public async isPersonAlreadyFriend(userId: string, friendId: string) {
let isFriendFound = false;
await FriendsList.findById(userId, (err, data) => {
if (data) {
console.log(data.friendsList);
}
if (err) console.error(err);
if (data && data.friendsList.indexOf(friendId) > -1) {
isFriendFound = true;
console.log('already friend');
} else {
console.log('not friend');
isFriendFound = false;
}
})
return isFriendFound;
}
If i remove the callback, the data only gets saved once.
EDIT: added second piece of code and new question.
If someone spams the button to add friend. The friend will be added multiple times because before the first friend is added and the check can be done to prevent this it has already added the person multiple times.
How can i make sure that it completes the write to the DB before allowing the function to be called again?

Maybe the problem is in isPersonAlreadyFriend method, because you are trying to call it using async await but then you are passing a callback, what makes the method not return a promise. The rigth way to use promises with mongodb should be something like this:
public async isPersonAlreadyFriend(userId: string, friendId: string) {
let isFriendFound = false;
const data = await FriendsList.findById(userId);
if (data) {
console.log(data.friendsList);
}
if (data && data.friendsList.indexOf(friendId) > -1) {
isFriendFound = true;
console.log('already friend');
} else {
console.log('not friend');
isFriendFound = false;
}
return isFriendFound;
}
Try with this and let me know if it helps

Related

JS async function returning undefined although calling it inside an asynchronous function

I am using nodeJs Express and I have an async function which is used in another function.
here is my code:
/* get user info */
const getUserInfo = async (req) => {
let userID = req.cookies.userId;
if(!userID) return null;
await connection.query('SELECT * FROM users WHERE id = ' + userID,
function (err, rows, fields) {
if (err) throw err
// if user not found
if (rows.length === 0) {
return null;
}
// if user found
else {
return rows[0];
}
});
}
/* display dashboard page */
router.get("/", async function (req, res, next) {
let userInfo = await getUserInfo(req);
console.log(userInfo) // It's result is undefined
if (userInfo) {
res.render('dashboard/profile', {
fullName: userInfo.fname + " " + userInfo.lname,
email: userInfo.email,
phone: userInfo.phone,
});
}
else {
res.render('authentication/register', {
title: 'ثبت نام',
});
}
});
how to resolve this problem? I need userInfo retun me some data.
await is only useful if the value on the right-hand side is a promise.
connection.query is taking a callback function, which implies it isn't returning a promise.
You need to either:
Find out how to make the API you are using return a promise (if that is possible)
Wrap the callback in a promise
Replace the API with one that supports promises natively
You also need getUserInfo to have a return statement of its own.
You have to return some value from your getUserInfo function
If connection query doesn't support promise you should wrap it like this
/* get user info */
const getUserInfo = async(req) => {
let userID = req.cookies.userId;
if (!userID) return null;
return new Promise((resolve, reject) => {
connection.query('SELECT * FROM users WHERE id = ' + userID,
function(err, rows, fields) {
if (err) {
reject(err)
return;
}
// if user not found
if (rows.length === 0) {
resolve(null)
}
// if user found
else {
resolve(rows[0]);
}
});
});
}

Javascript rerun the function until condition is false

Via API, I would like to list out the whole users. Each time can only have maximum 100 items for each page. So that I need to get the next_page url to rerun the function - runPages to collect the list. So the condition is when the next_page == null, then will stop the function.
In my code, I can get the next_page url. However, it doesn't run further. Can someone figure out what is the problem?
const runPages = async (values) => {
if (values.next_page != null) {
for (const field of values.results) {
row.appendChild(addCell(field.name));
row.appendChild(addCell(field.email));
tblbody.appendChild(row);
}
values = await checkPages(values.next_page); // get new values.data by url
runPages(values);
}
};
runPages(values);
const checkPages = async (value) => {
return new Promise((resolve, reject) => {
const getNewPageFromApi = async () => {
const GetUrl = `${value}`;
const Doorkey = { username: "XXX", password: "*****" };
try {
const response = await Axios.get(GetUrl, { auth: Doorkey });
if (response.data.next_page != null) {
resolve(response.data);
}
} catch (err) {
reject("no more data");
}
};
getNewPageFromApi();
});
};
I don't know if this is the answer you're looking for but since you're checking for values.next_page != null in the runPages function you can call the resolve(response.data) directly in the Promise inside checkPages function without checking for response.data.next_page != null.

Node.js promise fails intermittently, even when handled

I'm learning to use MongoDB by creating a simple blog app. However, a portion of my code that saves a given post seems to give problems with promises occasionally, but not always, and whether the code succeeds simply seems to be luck.
Each post in my database is stored with the following schema:
{
title: String,
author: String,
body: String,
slug: String,
baseSlug: String,
published: { type: Boolean, default: false }
}
The slug defines the link used to access the blog post, and is automatically generated based upon the title of the blog post. However, if article titles are duplicates, the slug will have a number added to the end to differentiate it from similar articles, while the baseSlug will remain the same. For example:
I create the post "My first post", and it is assigned the baseSlug of "my-first-post". Because no other posts have the same baseSlug, the slug is also set to be "my-first-post".
I create another post called "My first post", and it is assigned the baseSlug of "my-first-post". However, because another post has the same baseSlug, it is assigned the slug "my-first-post-1".
To create this behavior, I wrote the following addpost route in Express:
app.post("/addpost", (req, res) => {
let postInfo = req.body;
for (key of Object.keys(postInfo)) {
if (postInfo[key] == "true") postInfo[key] = true;
}
let slug = postInfo.title
.toLowerCase()
.split(" ")
.filter(hasNumber) // return /\d/.test(str);
.slice(0, 5)
.join("-");
postInfo.slug = slug;
var postData;
Post.find({ baseSlug: postInfo.slug }, (error, documents) => {
if (documents.length > 0) {
let largestSlugSuffix = 0;
for (let document of documents) {
var fullSlug = document.slug.split("-");
var suffix = fullSlug[fullSlug.length - 1];
if (!isNaN(suffix)) {
if (parseInt(suffix) > largestSlugSuffix) {
largestSlugSuffix = suffix;
}
}
}
largestSlugSuffix++;
postInfo.baseSlug = postInfo.slug;
postInfo.slug += "-" + largestSlugSuffix;
} else {
postInfo.baseSlug = postInfo.slug;
}
postData = new Post(postInfo);
})
.then(() => {
postData
.save()
.then(result => {
res.redirect("/");
})
.catch(err => {
console.log(err);
res.status(400).send("Unable to save data");
});
})
.catch(err => {
console.log(err);
res.status(400).send("Unable to save data");
});
});
This code seems to work most of the time, but sometimes it fails, and outputs the following:
TypeError: Cannot read property 'save' of undefined
at C:\Users\User\BlogTest\app.js:94:18
at processTicksAndRejections (internal/process/task_queues.js:94:5)
(For reference, line 94 in my file is postData.save())
I suspect it is because the main body of the function takes longer than it should to execute, and the postData variable is not yet defined. However, postData.save() should not be executed until the promise finishes, because of the .then() callback function.
Why is my code behaving like this? Is there any way to fix it?
The issue is that you are mixing promises with callbacks and closures. That's not how this is intended to work.
When you chain promises, whatever you return in the first promise handler will be added as an input to the next one. And if you return a promise, that promise will be resolved first before being sent to the next thenable.
So you need to return promises from your promises, like this:
app.post("/addpost", (req, res) => {
let postInfo = req.body;
for (key of Object.keys(postInfo)) {
if (postInfo[key] == "true") postInfo[key] = true;
}
let slug = postInfo.title
.toLowerCase()
.split(" ")
.filter(hasNumber) // return /\d/.test(str);
.slice(0, 5)
.join("-");
postInfo.slug = slug;
// var postData; <-- Don't do that
Post.find({ baseSlug: postInfo.slug })
.then((documents) => {
if (documents.length > 0) {
let largestSlugSuffix = 0;
for (let document of documents) {
var fullSlug = document.slug.split("-");
var suffix = fullSlug[fullSlug.length - 1];
if (!isNaN(suffix)) {
if (parseInt(suffix) > largestSlugSuffix) {
largestSlugSuffix = suffix;
}
}
}
largestSlugSuffix++;
postInfo.baseSlug = postInfo.slug;
postInfo.slug += "-" + largestSlugSuffix;
} else {
postInfo.baseSlug = postInfo.slug;
}
return new Post(postInfo);
// We could actually have called postData.save() in this method,
// but I wanted to return it to exemplify what I'm talking about
})
// It is important to return the promise generated by postData.save().
// This way it will be resolved first, before invoking the next .then method
.then( (postData) => { return postData.save(); })
// This method will wait postData.save() to complete
.then( () => { res.redirect("/"); })
.catch( (err) => {
console.log(err);
res.status(400).send("Unable to save data");
});
});
It can be greatly simplified with async/await:
app.post("/addpost", async (req, res) => {
try {
let postInfo = req.body;
for (key of Object.keys(postInfo)) {
if (postInfo[key] == "true") postInfo[key] = true;
}
let slug = postInfo.title
.toLowerCase()
.split(" ")
.filter(hasNumber)
.slice(0, 5)
.join("-");
postInfo.slug = slug;
let documents = await Post.find({ baseSlug: postInfo.slug });
if (documents.length > 0) {
let largestSlugSuffix = 0;
for (let document of documents) {
var fullSlug = document.slug.split("-");
var suffix = fullSlug[fullSlug.length - 1];
if (!isNaN(suffix)) {
if (parseInt(suffix) > largestSlugSuffix) {
largestSlugSuffix = suffix;
}
}
}
largestSlugSuffix++;
postInfo.baseSlug = postInfo.slug;
postInfo.slug += "-" + largestSlugSuffix;
} else {
postInfo.baseSlug = postInfo.slug;
}
let postData = new Post(postInfo);
await postData.save();
res.redirect("/");
} catch (err) {
console.log(err);
res.status(400).send("Unable to save data");
};
});
You are mixing callbacks and promises and while it may do something, I'm not sure what it will do exactly. You should pick one or the other and not mix them as much as possible. I would recommend picking promises if you are using a language that supports async/await, otherwise callbacks.
So for example your outter handler could be an async function
app.post("/addpost", async (req, res) => {
//...
})
Your real bug is in handling Post.find you are handling it somewhat with a callback and somewhat with a promise, and probably whats happening is that its random which one will get called first the callback or the promise resolution. Instead of both you should just do this now that you have an async function:
try {
const posts = await Post.find({ baseSlug: postInfo.slug });
// stuff you were doing in the callback
const post = new Post(postInfo)
// Now the promise code
await post.save()
// success!
res.redirect("/");
} catch (err) {
// With an async function you can just catch errors like normal
console.log(err);
res.status(400).send("Unable to save data");
}
If you're not using webpack or typescript and cannot target es7 then and thus cannot use async/await then I would recommend just using callbacks, do not use .then or .catch and that would look more like:
function error(err) {
console.log(err)
res.status(400).send("Unable to save data")
}
Post.find({ baseSlug: postInfo.slug }, (err, documents) => {
if (err) return error(err)
// stuff you're doing in the callback now
const post = new Post(postInfo)
post.save((err) => {
if (err) return error(err)
// success!
res.redirect("/");
})
})

Use async await inside mapping function

In my Node Express App, I want to get all comments for a lesson and modify each comment by adding a fullname field to each comment. For getting full name of a user, I have defined findFullNameByUserId function in UserController.js. I use findAllCommentsByLessonId() in CourseController.js as follows. However, when I'm using it, I always get an empty array. How can I use findFullNameByUserId in findAllCommentsByLessonId() so that fullname field can be added to each comment object?
CourseController.js
findAllCommentsByLessonId: async (courseId,lessonId,callback) => {
Course.find({_id: courseId}, function(err, course) {
if (err) {
callback(err, null);
} else {
const lesson = course[0].lessons.filter(l => l._id !== lessonId)
const comments = lesson[0].comments.map( async c => {
try{
const name = await UserController.findFullNameById(c.user)
return (
{
userId: c.user,
name: name,
content: c.content
}
)
}
catch(err){
console.log(err)
}
})
// console.log(comments) --> This always prints [{}]
callback(null, comments)
}
});
}
UserController.js
module.exports = {
findFullNameById: async (userId) => {
await User.find({_id: userId}, function(err, user) {
if (err) {
console.log(err)
} else {
return user[0].name+" "+( user[0].lname ? user[0].lname : "")
}
});
}
}
in CourseController.js either you can use async-await or you can use callback
async-await way :
findAllCommentsByLessonId: async (courseId,lessonId) => {
let course = await Course.findOne({_id: courseId});
if (course){
let lesson = course.lessons.find(l => l._id == lessonId);
let comments = [];
for(let comment of lesson.comments){
let name = await UserController.findFullNameById(comment.user);
comments.push({userId: comment.user,name: name,content: comment.content});
}
return comments;
}else{
return [];
}
}
callback way :
findAllCommentsByLessonId: (courseId,lessonId,callback) => {
Course.findOne({_id: courseId},function(err, course) {
if (err) {
callback(err, null);
} else {
let lesson = course.lessons.find(l => l._id == lessonId);
let comments = lesson.comments;
comments.map((comment)=>{
UserController.findFullNameById(comment.user).then(name=>{
return {userId: comment.user,name: name,content: comment.content};
});
});
callback(null, comments);
}
});
}
Start by dropping callbacks and actually using promises for await. You haven't specified what find is, but chances are it's a modern library supporting promises. So write
async function findAllCommentsByLessonId(courseId, lessonId) {
const [course] = Course.find({_id: courseId};
const lesson = course.lessons.find(l => l._id !== lessonId);
const comments = lesson.comments.map(async c => {
const name = await UserController.findFullNameById(c.user)
return {
userId: c.user,
name,
content: c.content
};
});
}
module.exports.findFullNameById = async (userId) => {
const [user] = await User.find({_id: userId});
return user.name + " " + (user.lname ? user.lname : "");
};
Then you'll notice that comments is not an array of users, but rather an array of promises for users, so wrap it in a await Promise.all(…) call.
As #SuleymanSah commented, I tried to use .populate and it worked well. I think this is the correct approach as for the exact reasons he's pointed out. The following is how I did it:
Lesson.findOne({ _id: lessonId }).
populate('comments.user').
exec(function(err, lesson) {
if (err) {
console.log(err);
return callback(err, null);
}
if (!lesson) {
console.log("No record found");
return callback(err, null);
}
return callback(null, lesson.comments);
});

function inside function is not waiting for promise in javascript

Sorry if my title is not very explicit I dont know how to explain this properly.
I am trying to use the distinct function for my app that uses loopback 3 and mongodb. It seems to work right but my endpoint wont hit the return inside my function.
This is my code
const distinctUsers = await sellerCollection.distinct('userId',{
hostId : host.id,
eventId:{
"$ne" : eventId
}
}, async function (err, userIds) {;
if(!userIds || userIds.length ==0)
return [];
const filter = {
where:{
id: {
inq: userIds
}
}
};
console.log("should be last")
return await BPUser.find(filter);
});
console.log(distinctUsers);
console.log("wtf??");
//return [];
If I uncomment the return [] it will send the return and later it will show the should be last, so even when I dont have the return it seems to finish. It is now waiting for the response. I dont like the way my code looks so any pointer of how to make this look better I will take it.
It looks like the sellerCollection.distinct takes a callback as one of it's parameters, therefore, you cannot use async/await with a callback-style function, since it's not a promise.
I would suggest turning this call into a promise if you'd like to use async/await:
function findDistinct(hostId, eventId) {
return new Promise((resolve, reject) => {
sellerCollection.distinct(
'userId',
{ hostId, eventId: { "$ne": eventId } },
function (error, userIds) {
if (error) {
reject(error);
return;
}
if (!userIds || userIds.length === 0) {
resolve([]);
return;
}
resolve(userIds);
}
)
})
}
Then, you can use this new function with async/await like such:
async function getDistinctUsers() {
try {
const hostId = ...
const eventId = ...
const distinctUsers = await findDistinct(hostId, eventId)
if (distinctUsers.length === 0) {
return
}
const filter = {
where: {
id: { inq: userIds }
}
}
const bpUsers = await BPUser.find(filter) // assuming it's a promise
console.log(bpUsers)
} catch (error) {
// handle error
}
}

Categories

Resources