I have a firebase function that runs based on a trigger to create a chat room. The function was running for a long time correctly until I updated it to add a new field on the document it is creating. That is archived: false.
After adding the field on the function, it sometimes adds the field when it runs but at times fails to add the field. So I think that firebase at times runs the updated code and sometimes runs the only code when the function is triggered because all other fields are created apart from the archived: false which is created and sometimes not created.
So I fail to understand why this happens yet in the code, the field archived is not dependent on any other variable.
Below is the function.
// firestore trigger to crete a chat room
exports.createChatRooms = functions.firestore.document("/jobs/{id}").onUpdate((change, context) => {
const job = change.after.data();
if (job.status === "Accepted") {
const roomId = context.params.id;
const room = admin.firestore().collection("rooms").doc(roomId);
return room.set({
name: job.title,
jobId: roomId,
createdAt: new Date().getTime(),
agent: job.agent,
archived: false,
user: job.user,
members: [job.user.id, job.agent.id],
});
} else {
return null;
}
});
Kindly help me understand why this is happening.
Related
Is there any way to wait for a Cloud Function, that was triggered by a Firestore document write, to finish?
Context:
My app has groups. Owners can invite other users to a group via an invite code. Users can write themselves as member of a group if they have the right invite code. They do this by writing the groups/{groupId}/members/{userId} document that contains their profile info.
To make reading more efficient, this info is copied to array members in the groups/{groupId} document by a Cloud Function.
The Cloud Function that does that is triggered by the document write. It is usually finished after a couple of seconds, but there's no predictable execution time and it might take a bit longer if it is a cold start.
After the user has joined the group, I forward them to the groups view in my app which reads the group document. In order for the view to render correctly, the membership info needs to be available. So I would like to forward AFTER the Cloud Function has finished.
I found no way to track the execution of a Cloud Function that was triggered by a Firestore document write.
A fellow developer recommended to just poll the groups/{groupId} document until the info is written and then proceed but this doesn't seem like a clean solution to me.
Any ideas how this could be done better?
Is it possible to get a promise that resolves after the Cloud Function has finished? Is there a way to combine a Firestore document write and a Cloud Function execution into one transaction?
Thanks for the hints, I came up with the following ways to deal with the problem. The approach depends on if/when the user is allowed to read a document:
A) User is member and leaves the group > at the start of the transaction they are allowed to read the group > the moment they can't read anymore confirms that the membership was successfully revoked:
async function leaveGroup (groupId) {
await deleteDoc(doc(db, 'groups', groupId, 'members', auth.currentUser.uid))
// Cloud Function removes the membership info
// from the group doc...
await new Promise((resolve) => {
const unsubscribeFromSnapshot = onSnapshot(
doc(db, 'groups', groupId),
() => { }, // success callback
() => { // error callback
// membership info is not in the group anymore
// > user can't read the doc anymore
// > transaction was successful
// read access was revoked > transaction was successful:
unsubscribeFromSnapshot()
resolve()
}
)
})
}
B) User is not a member and wants to join the group > at the start of the transaction they are allowed to read the group > the moment they can read the group confirms that the membership was successfully confirmed (this is a simplified version that does not check the invite code):
async function joinGroup (groupId) {
try {
await setDoc(
doc(db, 'groups', groupId, 'members', auth.currentUser.uid),
{
userId: auth.currentUser.uid,
userDisplayName: auth.currentUser.displayName
}
)
// Cloud Function adds the membership
// information to the group doc ...
await new Promise((resolve) => {
let maxRetries = 10
const interval = setInterval(async () => {
try {
const docSnap = await getDoc(doc(db, 'groups', groupId))
if (docSnap.data().members.includes(auth.currentUser.uid)) {
// membership info is in the group doc
// > transaction was successful
clearInterval(interval)
resolve()
}
} catch (error) {
if (maxRetries < 1) {
clearInterval(interval)
}
}
maxRetries--
}, 2000)
})
}
Note: I went with polling here, but similar to what #samthecodingman suggested, another solution could be that the Cloud Function confirms the membership by writing back to the members document (which the user can always read) and you listen to snapshot changes on this document.
C) Most straightforward way: someone else (the group owner) removes a member from the group > they have read access through the whole transaction > directly listen to snapshot changes:
async function endMembership (groupId, userId) {
await deleteDoc(doc(db, 'groups', groupId, 'members', userId))
// Cloud Function removes the membership info
// from the group doc...
await new Promise((resolve) => {
const unsubscribe = onSnapshot(doc(db, 'groups', groupId), (doc) => {
if (!doc.data().members.includes(userId)) {
// membership info is not in the group doc anymore
// > transaction was successful
unsubscribe()
resolve()
}
})
})
}
In any case you should do proper error handling that covers other causes. I left them out to demonstrate how to use the error handlers when waiting for gaining/loosing read access.
I'm working on a React web app and I noticed some weird/unusual behavior when using the Firestore emulator...
I have an onUpdate trigger that is pretty basic:
export const onUsernameUpdateFn = functions.firestore
.document("users/{userId}")
.onUpdate(async (change, context) => {
// Retrieve the current and previous value
const newData = change.after.data();
const previousData = change.before.data();
const operation = "username" in previousData ? "update" : "create";
console.log(
"newData",
newData,
"previousData",
previousData,
"operation",
operation
);
// We'll won't update if the username hasn't changed
if (newData.username === previousData.username) {
console.log("Skipping because username hasn't changed");
return null;
}
// Rest of the things
If I delete all data using the Emulator Suite (http://localhost:4000/firestore) and trigger the above code again, change.before.data() still has data in it...
functions: Beginning execution of "onUsernameUpdate"
> newData {
> createdAt: Timestamp { _seconds: 1652132412, _nanoseconds: 273000000 },
> username: 'differentUsername'
> } previousData {
> createdAt: Timestamp { _seconds: 1652132412, _nanoseconds: 273000000 },
> username: 'originalUsername'
> } operation update
... even though the Firestore UI shows it is empty (correctly).
If I wait some time (20-30 mins or more, haven't measured) after the deletion, then it works as expected and the operation becomes "create" because new and previous values are not equal.
I checked and I am not running multiple emulator instances and the correct project is selected.
Is there some caching in Firestore Emulator? Is it safe to assume that when deployed, this will not be an issue?
Or... is there a better way to skip the update when the username hasn't changed, but make sure it executes when the username property gets written for the first time?
Note: When it works "correctly" after waiting some time, the username property is not present, which is what I expect.
I have an app with different 'procedures' (think posts or pages), which one can like. Currently the process works: Tap like => run method "likeProcedure" => run dispatch action "likeProcedure" => update UI. It usually happens almost immediately, but sometimes there's a lag that gives this a "non-native" feel. Is there some sort of way that I could return feedback immediately, while stile holding single origin of truth on the firebase database?
Thank you!
Page Code:
<v-icon
v-if="!userProfile.likedProcedures || !userProfile.likedProcedures[procedure.id]"
color="grey lighten-1"
#click="likeProcedure({ id: procedure.id })"
>
mdi-star-outline
</v-icon>
and
computed: {
...mapState(["userProfile"]),
procedures() {
return this.$store.getters.getFilteredProcedures();
},
},
Vuex code:
async likeProcedure({ dispatch }, postId) {
const userId = fb.auth.currentUser.uid;
// update user object
await fb.usersCollection.doc(userId).update({
[`likedProcedures.${postId.id}`]: true,
});
dispatch("fetchUserProfile", { uid: userId });
},
Side note: I'm trying to remove the dispatch("fetchUserProfile") command, but this doesn't work, because then I'm calling dispatch without using it. And I cannot remove dispatch because then the object calling it is empty. And I cannot remove the object, because then the argument ('postId') isn't working. So if anyone knows how to deal with that, that would be extremely helpful.
Thank you :)
So this is the best solution I've come up yet. It kind of destroys the idea of a single source of truth, but at least it provides an immediate UI update:
async likeProcedure({ dispatch, state }, postId) {
console.log("likeProcedure");
const userId = fb.auth.currentUser.uid;
// line below provides immediate update to state and hence to the UI
state.userProfile.likedProcedures[postId.id] = true;
// line below updates Firebase database
await fb.usersCollection.doc(userId).update({
[`likedProcedures.${postId.id}`]: state.userProfile.likedProcedures[
postId.id
],
});
// line below then fetches the updated profile from Firebase and updates
// the profile in state. Kind of useless, but ensures that client and
// Firebase are in-sync
dispatch("fetchUserProfile", { uid: userId });
},
async fetchUserProfile({ commit }, user) {
// fetch user profile
const userProfile = await fb.usersCollection.doc(user.uid).get();
// set user profile in state
commit("setUserProfile", userProfile.data());
// change route to dashboard
if (router.currentRoute.path === "/login") {
router.push("/");
}
},
I'm using react-native-firebase in my app. The problem i'm facing is how to handle the UI updates when user tries to push data when offline.
If the user is online we can use the on() method to get realtime updates but what to do when they are offline. We know that the pushed data is stored in the cache and pushed when user is online again. Can this cached data be used to do what i aim at achieving?
Here's the code i used to receive realtime updates:
var ref333 = firebase.database().ref(`/user-posts/${uid}/`)
ref333.on('value',function (snap) {
var s = snap.val();
console.log("NEW POSTS "+JSON.stringify(s))
})
The code i use to push the data.
var postData = {
uid: uid,
body: 'body',
title: 'title',
starCount: 0
};
// Get a key for a new Post.
var newPostKey = firebase.database().ref().child('posts').push().key;
var ref222 = firebase.database().ref(`/posts/${newPostKey}`)
var ref333 = firebase.database().ref(`/user-posts/${uid}/${newPostKey}`)
ref222.push(postData, function (onComplete) {
console.log("COMPLETED")
ref333.push(postData,function (onComplete) {
console.log("NEXT COMPLETED")
}, function (error) {
console.log("ERROR IN ",error)
})
}, function (error) {
console.log("error == "+error)
})
The .on snspashot listener should be triggered even if in offline mode.
According to the docs:
https://firebase.google.com/docs/database/web/read-and-write
You can use the value event to read a static snapshot of the contents
at a given path, as they existed at the time of the event. This method
is triggered once when the listener is attached and again every time
the data, including children, changes.
This should work in offline mode as well. If you are not receiving updates - something else is wrong.
This problem was solved by adding this lines of code to your native code:
https://rnfirebase.io/docs/v5.x.x/core/default-app#Enable-Database-Persistence
In the example below, is there a way to get the uid of the user who wrote to /messages/{pushId}/original?
exports.makeUppercase = functions.database.ref('/messages/{pushId}/original')
.onWrite(event => {
// Grab the current value of what was written to the Realtime Database.
const original = event.data.val();
console.log('Uppercasing', event.params.pushId, original);
const uppercase = original.toUpperCase();
// You must return a Promise when performing asynchronous tasks inside a Functions such as
// writing to the Firebase Realtime Database.
// Setting an "uppercase" sibling in the Realtime Database returns a Promise.
return event.data.ref.parent.child('uppercase').set(uppercase);
});
UPDATED ANSWER (v1.0.0+):
As noted in #Bery's answer above, version 1.0.0 of the Firebase Functions SDK introduced a new context.auth object which contains the authentication state such as uid. See "New properties for user auth information" for more details.
ORIGINAL ANSWER (pre v1.0.0):
Yes, this is technically possible, although it is not currently documented. The uid is stored with the event.auth object. When a Database Cloud Function is triggered from an admin situation (for example, from the Firebase Console data viewer or from an Admin SDK), the value of event.auth is:
{
"admin": true
}
When a Database Cloud Function is triggered from an unauthenticated reference, the value of event.data is:
{
"admin": false
}
And finally, when a Database Cloud Function is triggered from an authed, but not admin, reference, the format of event.auth is:
{
"admin": false,
"variable": {
"provider": "<PROVIDER>",
"provider_id": "<PROVIDER>",
"user_id": "<UID>",
"token": {
// Decoded auth token claims such as sub, aud, iat, exp, etc.
},
"uid": "<UID>"
}
}
Given the information above, your best bet to get the uid of the user who triggered the event is to do the following:
exports.someFunction = functions.database.ref('/some/path')
.onWrite(event => {
var isAdmin = event.auth.admin;
var uid = event.auth.variable ? event.auth.variable.uid : null;
// ...
});
Just note that in the code above, uid would be null even if isAdmin is true. Your exact code depends on your use case.
WARNING: This is currently undocumented behavior, so I'll give my usual caveat of "undocumented features may be changed at any point in the future without notice and even in non-major releases."
Ever since Firebase functions reached version 1.0, this behavior is no longer undocumented but has sligtly changed. Be sure to read the docs.
Context has been added to cloud functions and you can use it like this
exports.dbWrite = functions.database.ref('/path/with/{id}').onWrite((data, context) => {
const authVar = context.auth; // Auth information for the user.
const authType = context.authType; // Permissions level for the user.
const pathId = context.params.id; // The ID in the Path.
const eventId = context.eventId; // A unique event ID.
const timestamp = context.timestamp; // The timestamp at which the event happened.
const eventType = context.eventType; // The type of the event that triggered this function.
const resource = context.resource; // The resource which triggered the event.
// ...
});