How to use promise and loop over mongoose collection - javascript

I'm making chat inside my website. To store data I use Chat, User, Messages collections.
I want results to be in Array containing:
[{
username (another one, not me)
last update
last message
}]
In Chat model I have only chatid and array of two members, so I need to loop through User collection to get user name using user id from it. I want to save in array all names (in future I would also like to loop through messages to get latest messages for each chatid). Issue is that when I return chatsList it is empty. I think I need somehow to use Promise, but I'm not completely sure how it should work.
Chat.find({ members: userId })
.then(chats => {
let chatsList = [];
chats.forEach((chat, i) => {
let guestId = chat.members[1 - chat.members.indexOf(userId)];
User.findOne({ _id: guestId })
.then(guest => {
let chatObj = {};
name = guest.name;
chatsList.push(name);
console.log("chatsList", chatsList)
})
.catch(err => console.log("guest err =>", err))
})
return res.json(chatsList)
})
.catch(err => {
errors.books = "There are no chats for this user";
res.status(400).json(errors);
})

Indeed, Promise.all is what you are looking for:
Chat.find({ members: userId })
.then(chats => {
let userPromises = [];
chats.forEach((chat, i) => {
let guestId = chat.members[1 - chat.members.indexOf(userId)];
userPromises.push(User.findOne({ _id: guestId }));
});
return Promise.all(userPromises).then(guests => {
let chatsList = [];
guests.forEach(guest => {
chatsList.push(guest.name);
});
return res.json(chatsList);
});
});
});
although it would probably be better to do a single call to DB with a list of ids ($in query). Something like this:
Chat.find({ members: userId })
.then(chats => {
let ids = [];
chats.forEach((chat, i) => {
let guestId = chat.members[1 - chat.members.indexOf(userId)];
ids.push(guestId);
});
return User.find({_id: {$in: ids}}).then(guests => {
let chatsList = [];
guests.forEach(guest => {
chatsList.push(guest.name);
});
return res.json(chatsList);
});
});
});
You may want to additionally validate if every id had a corresponding guest.

You are running into concurrency issues. For example, running chats.forEach, and inside forEach running User.findOne().then: The return statement is already executed before the User.findOne() promise has resolved. That's why your list is empty.
You could get more readable and working code by using async/await:
async function getChatList() {
const chats = await Chat.find({members: userId});
const chatsList = [];
for (const chat of chats) {
let guestId = chat.members[1 - chat.members.indexOf(userId)];
const guest = await User.findOne({_id: guestId});
chatsList.push(guest.name);
}
return chatsList;
}
Then the code to actually send the chat list back to the user:
try {
return res.json(await getChatList());
} catch (err) {
// handle errors;
}

You can try this:
Chat.find({ members: userId }).then(chats => {
let guestHashMap = {};
chats.forEach(chat => {
let guestId = chat.members.filter(id => id != userId)[0];
// depending on if your ID is of type ObjectId('asdada')
// change it to guestHashMap[guestId.toString()] = true;
guestHashMap[guestId] = true;
})
return Promise.all(
// it is going to return unique guests
Object.keys(guestHashMap)
.map(guestId => {
// depending on if your ID is of type ObjectId('asdada')
// change it to User.findOne({ _id: guestHashMap[guestId] })
return User.findOne({ _id: guestId })
}))
})
.then(chats => {
console.log(chats.map(chat => chat.name))
res.json(chats.map(chat => chat.name))
})
.catch(err => {
errors.books = "There are no chats for this user";
res.status(400).json(errors);
})

Related

Firestore transaction, max documents

It will come at some point that perhaps I will have to update more than 500 documents, but first I have to read and update all the data to be fine. How would you do this with transactions?
I did something similar with _.chunk with batch. But this time I need a transaction but I wouldn't know how to do.
transaction:
if (previousValue.Name !== newValue.Name || previousValue.Image !== newValue.Image) {
const chatRoomQuery = db.collection(chatsCollection).where(userIdsProperty, 'array-contains', userId);
const transactions = _.chunk(chatRoomQuery, maxSize) => {
return db.runTransaction(transaction => {
return transaction.getAll(chatRoomQuery).then(docs => {
docs.forEach(doc => {
let chatRoom = doc.data();
let oldUser = {
Id: previousValue.Id,
Name: previousValue.Name,
Image: previousValue.Image
};
let newUser = {
Id: newValue.Id,
Name: newValue.Name,
Image: newValue.Image
};
let index = chatRoom.Users.indexOf(oldUser);
if (index > -1) {
chatRoom.Users.splice(index, 1, newUser);
transaction.update(doc.ref, chatRoom)
}
})
})
})
});
await Promise.all(transactions);
}
I think I have a syntax error not getting it right.
I leave a screenshot.

Appending an array field, from another field, in MongoDB documents (with MongoClient) in Node.js

I have a collection called Users.
Here is an example of a doc.
{"_id":{"$oid":"xxxxxxxxxxxxxxxxxxx"},
"userId":"ANKIT",
"token":"token123",
"badge":{"$numberInt":"0"},
"useLocalCurrency":true,
"notifyCustomerRejected":true,
"notifyReworkRequest":true,
"notifyMoaApproved":true,
"notifyCustomerAccepted":true,
"__v":{"$numberInt":"0"},
"tokens":[]}
I am trying to push the token into the tokens array for all the docs in a DB migration.
This is what I have tried :
export function up(next) {
let mClient = null;
return MongoClient.connect(url)
.then(client => {
mClient = client;
return client.db('notifications');
})
.then(db => {
const User = db.collection('users');
return User.find().forEach(result => {
let { _id, userId, tokens, token } = result;
tokens.push(token);
tokens = Array.from(new Set(tokens));
result.tokens = tokens;
console.log(result._id);
console.log(result.tokens);
User.update({ _id: _id, userId: userId }, { $set: { tokens: tokens } });
});
})
.then(() => {
mClient.close();
return next();
})
.catch(err => next(err));
}
By doing this I am only getting the 1st document updated the way I want and not the rest. What am I doing wrong here?
Thanks
The call to update only modifies the first matched document. You would need to use updateMany or pass the multi option to affect them all.
This code seems to be finding all documents in the User collection via collscan, and then running a separate update in a forEach loop:
return User.find().forEach(result => {
let { _id, userId, tokens, token } = result;
tokens.push(token);
tokens = Array.from(new Set(tokens));
result.tokens = tokens;
console.log(result._id);
console.log(result.tokens);
User.update({ _id: _id, userId: userId }, { $set: { tokens: tokens } });
});
If you want to append the token to the array for every user try using:
return User.updateMany({},{$push: {tokens: token}})

Wait for server response with axios from different file React

I have a loop. On each round I need to add Question data into MongoDB database. This works fine. However, I want to get _id of the new inserted Question before the loop goes into the next round. This is where I have a problem. It takes certain amount of time before the server returns _id and loop goes to the next round by that time. Therefore, I need a way to wait for the server response and only after that move to the next round of the loop.
Here is my back-end code:
router.post("/createQuestion", (req, res) => {
const newQuestion = new Question({
description: req.body.description,
type: req.body.type,
model: req.body.model
});
newQuestion.save().then(question => res.json(question._id))
.catch(err => console.log(err));
});
Here is my axios function, which is in a separate file and imported into the class:
export const createQuestion = (questionData) => dispatch => {
axios.post("/api/scorecard/createQuestion", questionData)
.then(res => {
return res.data;
}).catch(err =>
console.log("Error adding a question")
);
};
Here is my code inside my class:
JSON.parse(localStorage.getItem(i)).map(question => {
const newQuestion = {
description: question.description,
type: question.questionType,
model: this.props.model
}
const question_id = this.props.createQuestion(newQuestion);
console.log(question_id);
}
Console shows undefined.
i faced the same issue i solved the same by sending the array question to the node and read one by one question and update with the next Question ID.
router.post("/createQuestion", (req, res) => {
let d =[questionarray];
let i = 0;
let length = d.length;
var result = [];
try {
const timeoutPromise = (timeout) => new Promise((resolve) => setTimeout(resolve, timeout));
for (i = 0; i < length; i++) {
await timeoutPromise(1000); // 1000 = 1 second
let CAT_ID = parseInt(d[i].CAT_ID);
let TOPIC_ID = parseInt(d[i].TOPIC_ID);
let Q_DESC = (d[i].Q_DESC);
let OPT_1 = (d[i].OPT_1);
let OPT_2 = (d[i].OPT_2);
let OPT_3 = (d[i].OPT_3);
let OPT_4 = (d[i].OPT_4);
let ANS_ID = (d[i].ANS_ID);
let TAGS = (d[i].TAGS);
let HINT = (d[i].HINT);
let LEVEL = d[i].LEVEL;
let SRNO = d[i].SrNo;
let qid;
const savemyData = async (data) => {
return await data.save()
}
var myResult = await Question.find({ TOPIC_ID: TOPIC_ID }).countDocuments(function (err, count) {
if (err) {
console.log(err);
}
else {
if (count === 0) {
qid = TOPIC_ID + '' + 10001;
const newQuestion = new Question({
Q_ID: qid,
CAT_ID: CAT_ID,
TOPIC_ID: TOPIC_ID,
Q_ID: qid,
Q_DESC: Q_DESC,
OPT_1: OPT_1,
OPT_2: OPT_2,
OPT_3: OPT_3,
OPT_4: OPT_4,
ANS_ID: ANS_ID,
HINT: HINT,
TAGS: TAGS,
LEVEL: LEVEL,
Q_IMAGE: ''
})
await savemyData(newQuestion)
.then(result => { return true })
.catch(err => { return false });
//`${SRNO} is added successfully`
//`${SRNO} is Failed`
}
else if (count > 0) {
// console.log(count)
Question.find({ TOPIC_ID: TOPIC_ID }).sort({ Q_ID: -1 }).limit(1)
.then(question => {
qid = question[0].Q_ID + 1;
const newQuestion = new Question({
Q_ID: qid,
CAT_ID: CAT_ID,
TOPIC_ID: TOPIC_ID,
Q_ID: qid,
Q_DESC: Q_DESC,
OPT_1: OPT_1,
OPT_2: OPT_2,
OPT_3: OPT_3,
OPT_4: OPT_4,
ANS_ID: ANS_ID,
HINT: HINT,
TAGS: TAGS,
LEVEL: LEVEL,
Q_IMAGE: ''
})
await savemyData(newQuestion)
.then(result => { return true })
.catch(err => { return false });
})
.catch(err => console.log(err));
}
}
});
if (myResult)
result.push(`${SRNO} is added successfully`);
else
result.push(`${SRNO} is Failed`);
}
// console.log(result)
return res.json(result);
}
catch (err) {
//res.status(404).json({ success: false })
console.log(err)
}
});
First your function createQuestion doesn't return a value so the assigning to question_id would always be undefined. Anyways, since u have a dispatch in your createQuestion function, I am assuming u r using redux, so I would suggest you to using redux-thnk, split the fetching new action logic to a thunk action, and use the questionID value from the redux state rather than returning a value from createQuestion. In your class u can be listening for a change of the questionID and if that happens, dispatch the saving of the next question.

Efficiency of timeout on Firebase Cloud Functions

I have a function that sends emails every time a new element is added to the db, like so:
export const onWorkCreation = functions.database.ref('/Works/{workId}').onCreate(async (snapshot, context) => {
const work = snapshot.val();
// const emails = ['email1#email.com', 'email2#email.com', 'email3#email.com'];
// TODO sprawdz z jakiej kategorii zadanie, wyslij do uzytkownikow ktorzy maja te kategorie + link do deaktywacji emaili.
let calls = [];
const persons = admin.database().ref('Users').orderByChild('userType').equalTo('person').once('value').then(r => r.val()).catch(err => console.log(1, err));
const companies = admin.database().ref('Users').orderByChild('userType').equalTo('company').once('value').then(r => r.val()).catch(err => console.log(2, err));
const undefineds = admin.database().ref('Users').orderByChild('userType').equalTo('undefined').once('value').then(r => r.val()).catch(err => console.log(3, err));
calls.push(persons, companies, undefineds);
let users = await Promise.all(calls).catch(err => console.log(4, err));
users = [...arrayFromObject(users[0]), ...arrayFromObject(users[1]), ...arrayFromObject(users[2])];
users.filter(u => u.receivesNotifications === undefined || u.receivesNotifications === true);
const usersIds = [];
for (const i in users) {
const user = users[i];
if (user.testInfo[work.category] !== undefined && user.testInfo[work.category.toLowerCase()].status.toLowerCase() === 'approved' && user.receivesNotifications !== false) {
usersIds.push(user.id);
} else {
// console.log(work);
// console.log(user.testInfo[work.category]);
// console.log(work.category);
// console.log(2, user.testInfo[work.category] !== undefined, 3, user.testInfo[work.category] !== undefined && user.testInfo[work.category.toLowerCase()].status.toLowerCase() === 'approved', 4, user.receivesNotifications !== false)
}
}
calls = [];
for (const i in usersIds) {
calls.push(0);
try {
calls[i] = await admin.auth().getUser(usersIds[i]).then(r => r).catch(err => console.log(5, err, usersIds[i]));
} catch (e) {
console.log('user', usersIds[i]);
}
}
users = await Promise.all(calls).catch(err => console.log(6, err));
users = arrayFromObject(users);
console.log('users', users);
const usersDetails = [];
for (const i in users) {
const user = {
email: users[i].email,
id: users[i].uid,
};
usersDetails.push(user);
}
calls = [];
const mailTransport = nodemailer.createTransport({
service: 'gmail',
auth: {
user: 'USER',
pass: 'PASS'
},
});
for (const i in usersDetails) {
const user = usersDetails[i];
calls.push(mailTransport.sendMail({
from: `ZdajTo <noreply#zdajto.com>`,
to: user.email,
subject: `Dostepne sa nowe zadania!`,
html: `<p>Hej! Sprawdz aplikacje ZdajTo! Dostepne sa nowe zadania z kategorii ${work.category}! Aby zrezygnowac z otrzymywania emaili kliknij w ten link</p>`,
}).then(() => null).catch(err => console.log(7, err, user.email)));
}
return Promise.all(calls).then(() => console.log('Emails sent')).catch(err => console.log(8, err));
});
It is a lot of code, but what it does in short is just grab emails for certain users and send emails to these addresses.
Now. I am firing it up every time a new work child is created. Is there a way of checking if the child was hanging in the db for more then 5 mins?
What I want to achieve:
If the work's property (available) is not changed in 5 mins, I want to send the emails again. I could achieve it by firing up a timeout loop, but I was hoping there would be a better way of doing it.
For this, I'd use a CRON function that queries work based upon the status and createTime. (You'll want to populate the createTime value when you add the work element.) The easiest way to execute CRON functions is with Azure Functions, but, you may also look at other options native to GCP/firebase.

Firestore simple leaderboard function

I'm tring to write a cloud function that ranks my users under the /mobile_user node by earned_points and assigns them a rank. I have successfully done this but now i want to write those same 10 users to another node called leaderboard. How can i accomplish this?
Here is my current function which already ranks them from 1 to 10:
exports.leaderboardUpdate2 = functions.https.onRequest((req, res) =>{
const updates = [];
const leaderboard = {};
const rankref = admin.firestore().collection('mobile_user');
const leaderboardRef = admin.firestore().collection('leaderboard');
return rankref.orderBy("earned_points").limit(10).get().then(function(top10) {
let i = 0;
console.log(top10)
top10.forEach(function(childSnapshot) {
const r = top10.size - i;
console.log(childSnapshot)
updates.push(childSnapshot.ref.update({rank: r}));
leaderboard[childSnapshot.key] = Object.assign(childSnapshot, {rank: r});
i++;
console.log(leaderboard)
});
updates.push(leaderboardRef.add(leaderboard));
return Promise.all(updates);
}).then(() => {
res.status(200).send("Mobile user ranks updated");
}).catch((err) => {
console.error(err);
res.status(500).send("Error updating ranks.");
});
});
This successfully updates the /mobile_user node where all my users are but i want to "export" those 10 users to the leaderboard node once the function executes.
(Note that the leaderboard node should have only 10 records at all times)
There are two problems in your Cloud Function:
Firstly you cannot directly use the childSnapshot object (neither with Object.assign nor directly) to create a new document. You have to use childSnapshot.data(), see https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentSnapshot
Secondly, you use childSnapshot.key while it should be childSnapshot.id, see the same document than above.
Finally, note that, with your code structure, the users document are added as maps under a unique leaderboard document. I am not sure it is exactly what you want, so you may adapt your code for this specific point.
So the following should work:
exports.leaderboardUpdate2 = functions.https.onRequest((req, res) => {
const updates = [];
const leaderboard = {};
const rankref = admin.firestore().collection('mobile_user');
const leaderboardRef = admin.firestore().collection('leaderboard');
return rankref
.orderBy('earned_points')
.limit(10)
.get()
.then(function(top10) {
let i = 0;
console.log(top10);
top10.forEach(function(childSnapshot) {
const r = top10.size - i;
updates.push(childSnapshot.ref.update({ rank: r }));
leaderboard[childSnapshot.id] = Object.assign(childSnapshot.data(), {
rank: r
});
i++;
});
updates.push(leaderboardRef.add(leaderboard));
return Promise.all(updates);
})
.then(() => {
res.status(200).send('Mobile user ranks updated');
})
.catch(err => {
console.error(err);
res.status(500).send('Error updating ranks.');
});
});
Following your comment, here is a new version, that writes a doc, in the leaderboard Collection, for each mobile_user. Note that we use a DocumentReference and together with the set() method, as follows: leaderboardRef.doc(childSnapshot.id).set()
exports.leaderboardUpdate2 = functions.https.onRequest((req, res) => {
const updates = [];
const leaderboard = {};
const rankref = admin.firestore().collection('mobile_user');
const leaderboardRef = admin.firestore().collection('leaderboard');
return rankref
.orderBy('earned_points')
.limit(10)
.get()
.then(function(top10) {
let i = 0;
console.log(top10);
top10.forEach(function(childSnapshot) {
const r = top10.size - i;
updates.push(childSnapshot.ref.update({ rank: r }));
updates.push(
leaderboardRef.doc(childSnapshot.id).set(
Object.assign(childSnapshot.data(), {
rank: r
})
)
);
i++;
});
return Promise.all(updates);
})
.then(() => {
res.status(200).send('Mobile user ranks updated');
})
.catch(err => {
console.error(err);
res.status(500).send('Error updating ranks.');
});
});

Categories

Resources