Firebase Cloud Functions set value operation deletes the trigger originating node - firebase

Hello, above is my simple data structure in firebase's realtime db. I'm working on a simple cloud function that will listen to update in a user node 'score' property and update the 'averageScore' field that sits higher up the hierarchy.
Here's my onWrite callback:
.onWrite((change, context) => {
if (!change.before.exists() || !change.after.exists()) {
return null;
}
const beforeScore = parseFloat(change.before.val()['score']);
const afterScore = parseFloat(change.after.val()['score']);
const userRef = change.after.ref;
var promises = [
userRef.parent.parent.child('userCount').once('value'),
userRef.parent.parent.child('averageScore').once('value')
];
return userRef.transaction(() => {
return Promise.all(promises).then((snapshots) => {
const userCount = snapshots[0].val();
const averageScore = snapshots[1].val();
const currentAverage = (( ( averageScore * userCount ) - beforeScore + afterScore ) / userCount ).toFixed(2);
return userRef.parent.parent.child('averageScore').set(currentAverage);
});
});
});
If I update userId 1234's score, the averageScore field is updated correctly per this code. However, the whole user Id node 1234 gets DELETED following this update. This is quite a head scratcher and hoping to get some insight from the community on what I might be doing wrong.
Cheers.

.onWrite((change, context) => {
if ( !change.before.exists() || !change.after.exists()) {
return null;
}
const beforeScore = parseFloat(change.before.val()['score']);
const afterScore = parseFloat(change.after.val()['score']);
const crowdStatsRef = change.after.ref.parent.parent.child('crowdStats');
return Promise.all( [
crowdStatsRef.child('userCount').once('value'),
crowdStatsRef.child('averageScore').once('value')
]).then((snapshots) => {
return crowdStatsRef.transaction((crowdStatsNode) => {
if (crowdStatsNode) {
const userCount = snapshots[0].val();
const averageScore = snapshots[1].val();
const currentAverage = (( ( averageScore * userCount ) - beforeScore + afterScore ) / userCount ).toFixed(2);
crowdStatsNode.score = parseFloat(currentAverage);
}
return crowdStatsNode;
}, (error, committed, snapshot) => {
if (error) {
console.error(error);
}
});
});
});
Misunderstood how transactions worked. The object you're locking onto must be returned in the callback function. Also, a null check in that callback function is essential here.
Examples are noted here:
https://firebase.googleblog.com/2016/01/keeping-our-promises-and-callbacks_76.html

Related

When doing multiple updates with batch, the page is getting stuck in a loop

I am trying to update multiple documents using batch() and commit().
The intended document are getting updated but the page goes still and I cannot click or go back or refresh the page.
However, when I console log the output message its going on a loop recursively.
let users_DB = db.collection( `users/${ profileEmail }/details` )
let currentDate = new Date()
users_DB.onSnapshot( ( querySnapshot ) =>
{
var batch = firebase.firestore().batch()
querySnapshot.forEach( ( doc ) =>
{
let dbDate = new Date( doc.data().dateInMills * 1000 )
if ( dbDate >= currentDate )
{
batch.update( users_DB.doc( doc.id ), {
'fees': parseInt( fees.value ),
'timeStamp': firebase.firestore.FieldValue.arrayUnion( firebase.firestore.Timestamp.fromDate( new Date() ) ),
} )
}
batch.commit().then(() => {console.log( 'changes made.' );})
} )
} )
How do I ensure avoid this behavior?
Because you are using the CollectionReference#onSnapshot method, you are listening for any changes to the data that matches the query. When you invoke batch.commit(), in addition to writing the new data to the server, all listeners will be notified that their data has changed - including your listener that's doing the update. Because you add a new element to the timeStamp field on every iteration, your code recursively calls itself.
To correct this, switch to a one time data retrieval using CollectionReference#get instead.
const users_DB = db.collection(`users/${profileEmail}/details`);
const currentDate = new Date();
users_DB
.get()
.then((querySnapshot) => {
const batch = db.batch(); // use db instead of firebase.firestore() for consistency
querySnapshot.forEach(doc => {
const dbDate = new Date(doc.get("dateInMills") * 1000); // dateInMills isn't in milliseconds?
if (dbDate >= currentDate) {
batch.update(doc.ref, {
'fees': parseInt( fees.value ),
'timeStamp': firebase.firestore.FieldValue.arrayUnion(firebase.firestore.Timestamp.fromDate(new Date())),
});
}
}
return batch.commit(); // this is a no-op if no changes are needed
})
.then(() => console.log("changes made"));
However, you can refine this with a query (which is how I would approach it):
const nowMS = Date.now(),
newData = {
fees: parseInt(fees.value),
timeStamp: firebase.firestore.FieldValue.arrayUnion(firebase.firestore.Timestamp.fromMillis(nowMS))
};
db.collection(`users/${profileEmail}/details`)
.where("dateInMills", ">=", nowMS / 1000)
.get()
.then(qs => {
const batch = db.batch();
qs.forEach(docSnap => {
batch.update(docSnap.ref, newData);
});
return batch.commit();
})
.then(() => console.log("changes made"));

How to get all items from subcollection Firebase Firestore Vue

How do I get all the comments from the subcollection?
This is mine reusable function to get comments collection.
import { ref, watchEffect } from 'vue';
import { projectFirestore } from '../firebase/config';
const getCollection = (collection, id, subcollection) => {
const comments = ref(null);
const error = ref(null);
// register the firestore collection reference
let collectionRef = projectFirestore
.collection(collection)
.doc(id)
.collection(subcollection);
const unsub = collectionRef.onSnapshot(
snap => {
let results = [];
snap.docs.forEach(doc => {
doc.data().createdAt && results.push(doc.data());
});
// update values
comments.value = results;
error.value = null;
},
err => {
console.log(err.message);
comments.value = null;
error.value = 'could not fetch the data';
}
);
watchEffect(onInvalidate => {
onInvalidate(() => unsub());
});
return { error, comments };
};
export default getCollection;
And this is mine Comments.vue where i passing arguments in setup() function (composition API)
const { comments } = getAllComments('posts', props.id, 'comments');
When i console.log(comments) its null, in snapshot doc.data() is good but somehow results too is empty array even if i push doc.data() to results array and pass it to comments.value.
Can someone help me how to get that subcollection?
This is my Comment.vue component
export default {
props: ['id'],
setup(props) {
const { user } = getUser();
const content = ref('');
const { comments } = getAllComments('posts', props.id, 'comments');
const ownership = computed(() => {
return (
comments.value && user.value && user.value.uid == comments.value.userId
);
});
console.log(comments.value);
}
return { user, content, handleComment, comments, ownership };
},
};
const getCollection = (collection, id, subcollection) => {
const comments = ref(null);
const error = ref(null);
// Firestore listener
return { error, comments };
}
The initial value of comments here is null and since Firebase operations are asynchronous, it can take a while before the data loads and hence it'll log null. If you are using comments in v-for then that might throw an error.
It'll be best if you set initial value to an empty array so it'll not throw any error while the data loads:
const comments = ref([]);
Additionally, if you are fetching once, use .get() instead of onSnapshot()

firebase function on db trigger throws Function returned undefined, expected Promise or value

my firebase function based on realtime database trigger looks like below
exports.on_user_created = functions.database.ref("/users/{id}")
.onCreate((change, context) => {
console.log("start of on_user_created ")
const user = change.val();
console.log("New user:::" + JSON.stringify(user))
const uid = user._uid
const referralCode = user._referralCode
console.log("creating referral node for uid:" + uid + " with code:" + referralCode)
if(referralCode === undefined){
console.error("No referral code created for the user while sign up. Referral node cannot be created.")
return true
}
var db = admin.database();
var ref = db.ref('referrals')
ref.child(referralCode).set({"uid": uid}).then(
(resp) => {
console.log("referral node created")
return true
}
).catch(
(err) => {
console.error("unable to create referral node on user create:" + err)
return true
}
)
})
it on run throws
5:47:02.035 AM on_user_created Function returned undefined, expected Promise or value
I am failing to understand why
Adapted following Doug's comment below: "If you have no async work to be done, it's typical to return null"
This is because you don't return the Promise returned by the set() asynchronous operation.
You should do something like:
exports.on_user_created = functions.database.ref("/users/{id}")
.onCreate((change, context) => {
console.log("start of on_user_created ")
const user = change.val();
console.log("New user:::" + JSON.stringify(user))
const uid = user._uid
const referralCode = user._referralCode
console.log("creating referral node for uid:" + uid + " with code:" + referralCode)
if(referralCode === undefined){
console.error("No referral code created for the user while sign up. Referral node cannot be created.")
return null // <-- See Doug's comment below.
}
var db = admin.database();
var ref = db.ref('referrals')
return ref.child(referralCode).set({"uid": uid}).then( // <-- !! Here we return
(resp) => {
console.log("referral node created")
return null
}
).catch(
(err) => {
console.error("unable to create referral node on user create:" + err)
return null
}
)
})
Note that you could streamline your code as follows if you don't need the console.log()s, e.g. for production.
exports.on_user_created = functions.database.ref("/users/{id}")
.onCreate((change, context) => {
const user = change.val();
const uid = user._uid
const referralCode = user._referralCode
if (referralCode === undefined) {
return null;
} else {
var db = admin.database();
var ref = db.ref('referrals')
return ref.child(referralCode).set({"uid": uid});
}
});
For more detail on the importance of returning the promises in a Cloud Function, I would suggest you watch the official video series and in particular the ones titled "Learn JavaScript Promises": https://firebase.google.com/docs/functions/video-series/

what should I do If I want to do nothing in the one of my execution path in Background trigger cloud function?

as far as I know, background trigger cloud function should return a promise,right? but what if I want to do nothing in the one of my execution path ?
export const updateDataWhenUserUnattendTheEvent = functions.firestore
.document('events/{eventId}/Attendee/{userId}')
.onDelete((snap, context) => {
const eventID = context.params.eventId
const eventRef = snap.ref.firestore.collection('events').doc(eventID)
const db = admin.firestore()
return db.runTransaction(async t => {
const doc = await t.get(eventRef)
if (doc) {
const eventRankPoint = doc.data().rankPoint
let eventCapacity = doc.data().capacity
return t.update(eventRef,{
isFullCapacity : false,
capacity : eventCapacity + 1,
rankPoint: eventRankPoint - 1
})
} else {
// what should I write in here? empty promise?
return new Promise()
}
})
})
I want to my function worked only if the document is exist. so what should I do ? I write new Promise but .... I don't know what to do actually. thanks in advance
You can just return null if there's no asynchronous work to perform in some code path of your functions. You only truly need a promise if it tracks some async work.
Alternatively, you could return a promise that's resolved immediately with Promise.resolve(null)
Because db.runTransaction is an async function it will return a Promise all the time.
You can drop the else statement and the method will perform as expected because runTransaction will return Promise<void> which is a valid response for Cloud Functions
export const updateDataWhenUserUnattendTheEvent = functions.firestore
.document('events/{eventId}/Attendee/{userId}')
.onDelete((snap, context) => {
const eventID = context.params.eventId;
const eventRef = snap.ref.firestore.collection('events').doc(eventID);
const db = admin.firestore();
return db.runTransaction(async t => {
const doc = await t.get(eventRef);
if (doc) {
const eventRankPoint = doc.data().rankPoint;
let eventCapacity = doc.data().capacity ;
return t.update(eventRef,{
isFullCapacity : false,
capacity : eventCapacity + 1,
rankPoint: eventRankPoint - 1
});
}
});
});
You can also make the onDelete function async which means you can force it to always return a Promise - the below is valid and will exit the function correctly.
export const updateDataWhenUserUnattendTheEvent = functions.firestore
.document('events/{eventId}/Attendee/{userId}')
.onDelete(async (snap, context) => {
// Do Nothing
return;
});

How to Count Users with a Firebase Cloud Function (getting Function Returned Undefined error)

I have a Firebase Cloud Function that assigns a number to a user on onWrite. The following code works but something is wrong because the console logs state Function returned undefined, expected Promise or value.
I'm also not sure how to refer to the root from inside the onWrite so I've created several "parent" entries that refer to each other. I'm sure there is a better way.
onWrite triggers on this:
/users/{uid}/username
The trigger counts the children in /usernumbers and then writes an entry here with the uid and the child count + 1:
/usernumbers/uoNEKjUDikJlkpLm6n0IPm7x8Zf1 : 5
Cloud Function:
'use strict';
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.setCount = functions.database.ref('/users/{uid}/username').onWrite((change, context) => {
const uid = context.params.uid;
const parent1 = change.after.ref.parent; //uid
const parent2 = parent1.ref.parent; //users
const parent3usernumbers = parent2.ref.parent.child('/usernumbers/');
const parent3usernumbersuid = parent2.ref.parent.child('/usernumbers/'+uid);
parent3usernumbers.once("value")
.then(function(snapshot) {
var a = snapshot.numChildren();
return parent3usernumbersuid.transaction((current) => {
return (a + 1);
}).then(() => {
return console.log('User Number Written', uid, a);
});
});
});
Is there a better way to do this? How can I get the Function Returned Undefined error to go away?
I should also mention it takes a few seconds for the 'usernumber' entry to be written. I'm guessing it's waiting for the function to return something.
Your function have to return a Promise :
exports.setCount = functions.database.ref('/users/{uid}/username').onWrite((change, context) => {
const uid = context.params.uid;
const parent1 = change.after.ref.parent; //uid
const parent2 = parent1.ref.parent; //users
const parent3usernumbers = parent2.ref.parent.child('/usernumbers/');
const parent3usernumbersuid = parent2.ref.parent.child('/usernumbers/'+uid);
return new Promise((resolve, reject) => {
parent3usernumbers.once("value").then(function(snapshot) {
var a = snapshot.numChildren();
return parent3usernumbersuid.transaction((current) => {
return (a + 1);
}).then(() => {
console.log('User Number Written', uid, a);
resolve({uid : uid, a : a})
}).catch(function(e) {
reject(e)
})
});
});
});

Resources