Firestore simple leaderboard function - javascript

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.');
});
});

Related

How to use documents and collections Firebase

Im new to firebase and I need help understanding how to work with documents and collections. Specifically I want to be able to write to a document and let it hold an array of 'loan objects' and let it be specific per user. All of these documents will be held in a collection. This code is making multiple documents for one user and I want it to only make one document per user and if I want to add more data for that user I just want to add it to the existing document
const loansRef = firebase.firestore().collection('goals')
useEffect(() => {
getPW()
let isMounted = true;
if (isMounted) {
loansRef.where('authorID', '==', userId).orderBy('createdAt', 'desc').onSnapshot(
(querySnapshot) => {
const newGoals = [];
querySnapshot.forEach((doc) => {
const goal = doc.data();
goal.id = doc.id + goalCounter.toString();
newGoals.push(goal);
});
console.log('new Goals: '+ newGoals)
console.log('old goals: '+ oldGoals)
// this is my attempt to try to make all loans appear in one array
var oldGoals = courseGoals
for(let j =0; j < newGoals.length; j++){
oldGoals.push(newGoals[j])
}
setCourseGoals(oldGoals);
setGoalCounter(goalCounter+1)
},
(error) => {
console.log(error);
}
);
}
return () => {
isMounted = false;
};
}, []);

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.

How to use promise and loop over mongoose collection

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);
})

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.

Web scraping and promises

I am using cheerio and node to do web scraping, but I have a problem with promises. I can scrape an article list from a page but in that list, we have more links for single pages. I need to scrape single pages as well for each item on the list.
I will show you my code for the better solution.
import rp from 'request-promise'
import cheerio from 'cheerio'
import conn from './connection'
const flexJob = `https://www.flexjobs.com`
const flexJobCategory = ['account-management', 'bilingual']
class WebScraping {
//list of article e.g for page 2
results = [] // [[title], [link for page],...]
contentPage = [] //content for each page
scrapeWeb(link) {
let fullLink = `${link}/jobs/${flexJobCategory[1]}?page=2`
const options = {
uri: fullLink,
transform(body) {
return cheerio.load(body)
}
}
rp(options)
.then(($) => {
console.log(fullLink)
$('.featured-job').each((index, value) => {
//html nodes
let shortDescription = value.children[1].children[1].children[3].children[1].children[1].children[0].data
let link = value.children[1].children[1].children[1].children[1].children[1].children[0].attribs.href
let pageLink = flexJob + '' + link
let title = value.children[1].children[1].children[1].children[1].children[1].children[0].children[0].data
let place = value.children[1].children[1].children[1].children[1].children[3].children[1].data
let jobType = value.children[1].children[1].children[1].children[1].children[3].children[0].children[0].data
this.results.push([title, '', pageLink.replace(/\s/g, ''), '', shortDescription.replace(/\n/g, ''), place, jobType, 'PageContent::: '])
})
})
.then(() => {
this.results.forEach(element => {
console.log('link: ', element[2])
this.scrapePage(element[2])
});
})
.then(() => {
console.log('print content page', this.contentPage)
})
.then(() => {
//this.insertIntoDB()
console.log('insert into db')
})
.catch((err) => {
console.log(err)
})
}
/**
* It's going to scrape all pages from list of jobs
* #param {Any} pageLink
* #param {Number} count
*/
scrapePage(pageLink) {
let $this = this
//console.log('We are in ScrapePage' + pageLink + ': number' + count)
//this.results[count].push('Hello' + count)
let content = ''
const options = {
uri: pageLink,
transform(body) {
return cheerio.load(body)
}
}
rp(options)
.then(($) => {
//this.contentPage.push('Hello' + ' : ');
console.log('Heloo')
})
.catch((err) => {
console.log(err)
})
}
/**
* This method is going to insert data into Database
*/
insertIntoDB() {
conn.connect((err) => {
var sql = "INSERT INTO contact (title, department, link, salary, short_description, location, job_type, page_detail) VALUES ?"
var values = this.results
conn.query(sql, [values], function (err) {
if (err) throw err
conn.end()
})
})
}
}
let webScraping = new WebScraping()
let scrapeList = webScraping.scrapeWeb(flexJob)
So, at 'scrapeWeb' method, at second '.then', I am calling 'scrapePage' method, however, the third promise executed before promise inside 'scrapePage' method.
You need a little more control flow at that stage. You do not want that .then()'s promise to resolve until all the calls are resolved.
You could use a Promise library like bluebird to do a Promise.each or a Promise.map for all the results you want to run.
Or use async/await to set up like .then(async () => {}) and do not use .forEach.
for(let element of this.results){
console.log('link: ', element[2])
await this.scrapePage(element[2])
}
You have a race condition problem.
The first tweak you'll need is having scrapePage returning a Promise.
scrapePage(pageLink) {
let $this = this
let content = ''
const options = {
uri: pageLink,
transform(body) {
return cheerio.load(body)
}
}
return rp(options);
}
In the second than, you need to invoke all child pages scraping eg :
.then(() => {
return Promise.all(this.results.map(childPage => this.scrapePage(childPage)));
})
This will wrap all scrapes of child pages into promises and only if all of them are resolved the code will flow.

Categories

Resources