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.
Related
I am making a function for firebase cloud functions, I want a function to be called every time a new document is created in "posts". I want this function to perform the tasks that I put inside the "onCeatePost" function.
The problem I have is that I'm not sure if this is the correct way to structure such a function.
In several firebase examples I have seen that it is always called return _; or return null; at the end of a task, but I don't know how to structure the function so that all the tasks are carried out, could someone help me to restructure my function or tell me what is wrong please.
There are several if statements in the function, if the created publication does not comply with them, I would like it to skip them but continue with the other tasks that I put inside the function.
I don't know if it's too much to ask, but I'm new to this language and I haven't been able to find the answer I'm looking for. Thank you!
exports.onPostCreate = functions.firestore.document("/posts/{postId}").onCreate(async (snap) => {
const post = snap.data();
if (post) {
try {
const topic = post.topic;
const contentForFeed = post.contentForFeed;
const uid = post.uid;
const previous = post.prev;
await db.collection("users").doc(uid).update({"stats.posts": admin.firestore.FieldValue.increment(1)});
if (topic) {
await db.collection("topics").doc(topic.id).collection("user-authors").doc(uid).set({"date": snap.createTime});
}
if (contentForFeed == true) {
const userPath = db.collection("users").doc(uid);
await userPath.update({"stats.lastUpdate": snap.createTime});
}
if (previous) {
const previousId = previous.id;
const previousUid = previous.uid;
const refPrev = db.collection("posts").doc(previousId);
await db.runTransaction(async (t) => {
const doc = await t.get(refPrev);
const priority = doc.data().stats.date;
const newDate = new admin.firestore.Timestamp(priority.seconds + 120, priority.nanoseconds);
await db.collection("posts").doc(previousId).update({"newDate": newDate});
});
if (previousUid != uid) {
const path = db.collection("users").doc(uid).collection("user-posts");
const dataToSet = {"timestamp": snap.createTime, "uid": uid, "postId": onReplyToPostId};
await path(dataToSet);
}
}
} catch (err) {
functions.logger.log(err);
}
} else {
return null;
}
});
You'll find below the adapted code (untested) with 4 corrections.
Here are explanations for the two most important ones:
(Correction 2) In a transaction you need to use the transaction's update() method and not the "standard one"
(Correction 4) When all the asynchronous work is complete you need to return a value or a Promise. See this documntation page for more details.
exports.onPostCreate = functions.firestore
.document('/posts/{postId}')
.onCreate(async (snap) => {
const post = snap.data();
if (post) {
try {
const topic = post.topic;
const contentForFeed = post.contentForFeed;
const uid = post.uid;
const previous = post.prev;
await db
.collection('users')
.doc(uid)
.update({
'stats.posts': admin.firestore.FieldValue.increment(1),
});
if (topic) {
await db
.collection('topics')
.doc(topic.id)
.collection('user-authors')
.doc(uid)
.set({ date: snap.createTime });
}
if (contentForFeed == true) {
const userPath = db.collection('users').doc(uid);
await userPath.update({ 'stats.lastUpdate': snap.createTime });
}
let previousUid; // <= Correction 1
if (previous) {
const previousId = previous.id;
previousUid = previous.uid; // <= Correction 1
const refPrev = db.collection('posts').doc(previousId);
await db.runTransaction(async (t) => {
const doc = await t.get(refPrev);
const priority = doc.data().stats.date;
const newDate = new admin.firestore.Timestamp(
priority.seconds + 120,
priority.nanoseconds
);
t.update(refPrev, { newDate: newDate }); // <= Correction 2
});
if (previousUid != uid) {
const path = db
.collection('users')
.doc(uid)
.collection('user-posts');
const dataToSet = {
timestamp: snap.createTime,
uid: uid,
postId: onReplyToPostId,
};
await path.add(dataToSet); // <= Correction 3
}
}
return null; // <= Correction 4
} catch (err) {
functions.logger.log(err);
}
} else {
return null;
}
});
I'm trying to make a list in my react app. I have retrieved data from my database, and pushed it into a list. I have doublechecked that the data shows up correctly in the console, and it does, but array.map() returns nothing. I think the problem might be that array.map() runs two times. I don't know why it runs two times.
function Dashboard() {
const user = firebase.auth().currentUser;
const [teams, setTeams] = useState([])
const history = useHistory();
useEffect(() => {
getTeams()
if (user) {
} else {
history.push("/")
}
}, [])
function Welcome() {
if (user) {
return <h1>Welcome, {user.displayName}</h1>
} else {
}
}
const getTeams = () => {
firebase.firestore().collectionGroup('members').where('user', '==', user.uid).get().then((snapshot) => {
const docList = []
snapshot.forEach((doc) => {
docList.push({
teamId: doc.data().teamId,
})
})
const teamslist = []
docList.forEach((data) => {
firebase.firestore().collection('teams').doc(data.teamId).get().then((doc) => {
teamslist.push({
name: doc.data().name,
teamId: doc.id,
})
})
})
setTeams(teamslist)
})
}
const openTeam = (data) => {
console.log(data.teamId)
}
return (
<div>
<Welcome />
<div>
<ul>
{console.log(teams)}
{teams.map((data) => {
return (
<li onClick={() => openTeam(data)} key={data.teamId}>
<h1>{data.name}</h1>
<p>{data.teamId}</p>
</li>
)
})}
</ul>
</div>
</div>
)
}
export default Dashboard
The getTeams function has a bug where it isn't waiting for the firebase.firestore().collection('teams').doc(data.teamId).get().then promises to finish before calling setTeams, so it is called with an empty array, causing React to trigger a render with the empty array.
As the promises for fetching each team resolve they will be pushed to the same array reference, but this won't trigger a rerender in React since you're not calling setTeams again when the array changes.
Try this code, which won't call setTeams until each team promise generated from docList has been resolved.
const getTeams = () => {
firebase.firestore().collectionGroup('members').where('user', '==', user.uid).get().then((snapshot) => {
const docList = []
snapshot.forEach((doc) => {
docList.push({
teamId: doc.data().teamId,
})
})
const teamslist = [];
Promise.all(docList.map((data) => {
return firebase
.firestore()
.collection('teams')
.doc(data.teamId)
.get()
.then((doc) => {
teamslist.push({
name: doc.data().name,
teamId: doc.id,
})
})
}))
.then(() => setTeams(teamslist));
})
}
A smaller edit would be to call setTeams after each separate team promise resolves, which will trigger a React render each time a new team is resolved:
.then((doc) => {
teamslist.push({
name: doc.data().name,
teamId: doc.id,
});
// create a new array, since using the same array
// reference won't cause react to rerender
setTeams([...teamslist]);
})
Many thanks to #martinstark who provided you an answer while I was unavailable.
However, there are some more things that need to be covered.
User State
In your current component, you pull the current user from Firebase Authentication, but don't handle the state changes of that user - signing in, signing out, switching user. If a user is signed in and they were to navigate directly to your dashboard, firebase.auth().currentUser could be momentarily null while it resolves the user's login state, which would incorrectly send them off to your login page.
This can be added using:
const [user, setUser] = useState(() => firebase.auth().currentUser || undefined);
const userLoading = user === undefined;
useEffect(() => firebase.auth().onAuthStateChanged(setUser), []);
Next, in your first useEffect call, you call getTeams() whether the user is signed in or not - but it should depend on the current user.
useEffect(() => {
if (userLoading) {
return; // do nothing (yet)
} else if (user === null) {
history.push("/");
return;
}
getTeams()
.catch(setError);
}, [user]);
// This getTeams() is a () => Promise<void>
const getTeams = async () => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', user.uid)
.get();
const docList = []
membersQuerySnapshot.forEach((doc) => {
docList.push({
teamId: doc.get("teamId"), // better perfomance than `doc.data().teamId`
});
});
const teamDataList = [];
await Promise.all(docList.map((data) => {
return firebase.firestore()
.collection('teams')
.doc(data.teamId)
.get()
.then(doc => teamDataList.push({
name: doc.get("name"),
teamId: doc.id
}));
}));
setTeams(teamDataList);
}
Optimizing getTeams() - Network Calls
The getTeams function in your question calls setTeams with the array [], which will be empty at the time of calling it as covered in #martinstark's answer. The "get team data" operations are asyncronous and you aren't waiting for them to resolve before updating your state and triggering a new render. While you are pushing data to them after the component has rendered, modifying the array won't trigger a new render.
While you could fetch the data for each team using db.collection("teams").doc(teamId).get(), each of these is requests is a network call, and you can only make a limited number of these in parallel. So instead of fetching 1 team per network call, you could fetch up to 10 teams per network call instead using the in operator and FieldPath.documentId().
Assuming the collectionGroup("members") targets the collections of documents at /teams/{aTeamId}/members which contain (at least):
"/teams/{aTeamId}/members/{memberUserId}": {
teamId: aTeamId,
user: memberUserId, // if storing an ID here, call it "uid" or "userId" instead
/* ... */
}
// this utility function lives outside of your component near the top/bottom of the file
function chunkArr(arr, n) {
if (n <= 0) throw new Error("n must be greater than 0");
return Array
.from({length: Math.ceil(arr.length/n)})
.map((_, i) => arr.slice(n*i, n*(i+1)))
}
// This getTeams() is a () => Promise<void>
const getTeams = async () => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', user.uid)
.get();
const teamIDList = []
membersQuerySnapshot.forEach((doc) => {
teamIDList.push(doc.get("teamId")); // better perfomance than `doc.data().teamId`
})
const chunkedTeamIDList = chunkArr(teamIDList, 10) // split into batches of 10
const teamsColRef = firebase.firestore().collection('teams');
const documentId = firebase.firestore.FieldPath.documentId(); // used with where() to target the document's ID
const foundTeamDocList = await Promise
.all(chunkedTeamIDList.map((chunkOfTeamIDs) => {
// fetch each batch of IDs
return teamsColRef
.where(documentId, 'in', chunkOfTeamIDs)
.get();
}))
.then((arrayOfQuerySnapshots) => {
// flatten results into a single array
const allDocsList = [];
arrayOfQuerySnapshots.forEach(qs => allDocsList.push(...qs.docs));
return allDocsList;
});
const teamDataList = foundTeamDocList
.map((doc) => ({ name: doc.get("name"), teamId: doc.id }));
// sort by name, then by ID
teamDataList.sort((aTeam, bTeam) =>
aTeam.name.localeCompare(bTeam.name) || aTeam.teamId.localeCompare(bTeam.teamId)
)
// update state & trigger render
setTeams(teamDataList);
}
You can also make use of this utility function to simplify & optimize the code a bit. Which gives:
// This getTeams() is a () => Promise<void>
const getTeams = async () => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', user.uid)
.get();
const teamIDList = []
membersQuerySnapshot.forEach((doc) => {
teamIDList.push(doc.get("teamId")); // better perfomance than `doc.data().teamId`
})
const teamsColRef = firebase.firestore().collection('teams');
const teamDataList = [];
await fetchDocumentsWithId(
teamsColRef,
teamIDList,
(doc) => teamDataList.push({ name: doc.get("name"), teamId: doc.id })
);
// sort by name, then by ID
teamDataList.sort((aTeam, bTeam) =>
aTeam.name.localeCompare(bTeam.name) || aTeam.teamId.localeCompare(bTeam.teamId)
)
// update state & trigger render
setTeams(teamDataList);
}
Optimizing getTeams() - Function Definition
As part of the last optimization, you could pull it out of your component or place it in its own file so that it's not redefined with every render:
// define at top/bottom of the file outside your component
// This getTeams() is a (userId: string) => Promise<{ name: string, teamId: string}[]>
async function getTeams(userId) => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', userId)
.get();
const teamIDList = []
membersQuerySnapshot.forEach((doc) => {
teamIDList.push(doc.get("teamId")); // better perfomance than `doc.data().teamId`
})
const teamsColRef = firebase.firestore().collection('teams');
const teamDataList = [];
await fetchDocumentsWithId(
teamsColRef,
teamIDList,
(doc) => teamDataList.push({ name: doc.get("name"), teamId: doc.id })
);
// sort by name, then by ID
teamDataList.sort((aTeam, bTeam) =>
aTeam.name.localeCompare(bTeam.name) || aTeam.teamId.localeCompare(bTeam.teamId)
)
// return the sorted teams
return teamDataList
}
and update how you use it:
useEffect(() => {
if (userLoading) {
return; // do nothing
} else if (user === null) {
history.push("/");
return;
}
getTeams(user.uid)
.then(setTeams)
.catch(setError);
}, [user]);
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.
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);
})
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.');
});
});