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

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/

Related

Batched Write/Transaction in Cloud Function keeps failing

I'm trying to make changes to several documents in a cloud function once I receive a callback. My code was working when I only had to update one document, but now I need to update several documents atomically in the same function.
I need to read a certain document and then update other documents based on the information held in an array in the original document. I tried to do this using forEach but I get this error in the console whether I'm using a transaction or a batched write:
Error: Cannot modify a WriteBatch that has been committed.
at WriteBatch.verifyNotCommitted (/workspace/node_modules/#google-cloud/firestore/build/src/write-batch.js:126:19)
at WriteBatch.update (/workspace/node_modules/#google-cloud/firestore/build/src/write-batch.js:315:14)
at loyaltyIds.forEach (/workspace/index.js:323:31)
at process._tickCallback (internal/process/next_tick.js:68:7)
Error: Process exited with code 16
at process.on.code (/layers/google.nodejs.functions-framework/functions-framework/node_modules/#google-cloud/functions-framework/build/src/invoker.js:92:22)
at process.emit (events.js:198:13)
at process.EventEmitter.emit (domain.js:448:20)
at process.exit (internal/process/per_thread.js:168:15)
at sendCrashResponse (/layers/google.nodejs.functions-framework/functions-framework/node_modules/#google-cloud/functions-framework/build/src/logger.js:44:9)
at process.on.err (/layers/google.nodejs.functions-framework/functions-framework/node_modules/#google-cloud/functions-framework/build/src/invoker.js:88:44)
at process.emit (events.js:198:13)
at process.EventEmitter.emit (domain.js:448:20)
at emitPromiseRejectionWarnings (internal/process/promises.js:140:18)
at process._tickCallback (internal/process/next_tick.js:69:34)
And what I end up with is the document outside the for loop is updated but the documents inside the for loop are not - which defeats the purpose of an atomic operation.
It also takes a long time to complete the write operation to Firestore. Where am I going wrong?
Below is what I've tried:
Using batched write:
const txDoc = await txRef.get();
if (txDoc.exists) {
console.log('Transaction Document Found');
const userId = txDoc.data().userId;
const loyaltyIds = txDoc.data().loyaltyIds;
const pointsAwardedMap = txDoc.data().pointsAwarded;
let batch = db.batch();
loyaltyIds.forEach(async lpId => {
// There are 2 elements in the loyaltyIds lis
console.log('Inside for loop');
console.log(lpId);
let cardId = 'u_' + userId + '-l_' + lpId; // 'u_$userId-l_$lpId'
let cardRef = db.collection('users').doc(userId).collection('userLoyaltyCards').doc(cardId);
let lpMap = pointsAwardedMap[lpId];
// Get the user LC doc
let cardDoc = await cardRef.get();
if (cardDoc.exists) {
batch.update(cardRef, {
'pointsBalance': cardDoc.data().pointsBalance + lpMap['points'],
'totalSpend': cardDoc.data().totalSpend + txDoc.data().transactionAmount,
'numberOfPurchases': cardDoc.data().numberOfPurchases + 1,
'pointsEarned': cardDoc.data().pointsEarned + lpMap['points'],
'lastPurchaseDate': admin.database.ServerValue.TIMESTAMP,
});
}
});
// Then we update the tx doc
batch.update(txRef, {
transactionCode: `${receiptNo}`,
transactionType: "purchase",
transactionSuccess: true,
}); // only this gets update
console.log('Firebase Transaction success');
return batch.commit();
} else { return null; }
Using transaction operation:
await db.runTransaction(async t => {
const txDoc = await t.get(txRef);
if (txDoc.exists) {
// userId
// For each lp we update the user loyalty card that goes with it
const userId = txDoc.data().userId;
const loyaltyIds = txDoc.data().loyaltyIds;
const pointsAwardedMap = txDoc.data().pointsAwarded;
// What the pointsAwarded map looks like from the transaction:
// var pointsAwarded = {
// lp1: {
// lpName: 'Jeff',
// lpId: 'lp.lpId',
// points: 'points1',
// cashbackPct: 'lp.cashbackPct',
// vendorId: 'lp.vendorId',
// vendorName: 'lp.vendorName',
// },
// lp2: {
// lpName: 'Susan',
// lpId: 'lp.lpId',
// points: 'points2',
// cashbackPct: 'lp.cashbackPct',
// vendorId: 'lp.vendorId',
// vendorName: 'lp.vendorName',
// },
// };
loyaltyIds.forEach(async (lpId) => {
// We update the user loyalty cards
console.log('Inside for loop');
console.log(lpId);
let cardId = 'u_' + userId + '-l_' + lpId; // 'u_$userId-l_$lpId'
let cardRef = db.collection('users').doc(userId).collection('userLoyaltyCards').doc(cardId);
let lpMap = pointsAwardedMap[lpId];
// Get the user LC doc
let cardDoc = await t.get(cardRef);
// We create the initial loyalty card doc without relying on the cloud function
if (cardDoc.exists) {
// Users LC found, we simply update with this transaction
// `${mpesaReceiptNo}`, this is how to add a var as a field value in firestore
t.update(cardRef, {
'pointsBalance': cardDoc.data().pointsBalance + lpMap['points'],
'totalSpend': cardDoc.data().totalSpend + txDoc.data().transactionAmount,
'numberOfPurchases': cardDoc.data().numberOfPurchases + 1,
'pointsEarned': cardDoc.data().pointsEarned + lpMap['points'],
'lastPurchaseDate': admin.database.ServerValue.TIMESTAMP,
});
}
}); // end of loyalty card update loop
// Then we update the transaction doc
console.log('Transaction Document Found')
t.update(txRef, {
transactionCode: `${mpesaReceiptNo}`,
transactionType: "purchase",
transactionSuccess: true,
});
console.log('Firebase Transaction success');
}
});
UPDATE
I've tried to use a normal for loop but I still get the same errors. I even tried to incorporate the batch.commit statement in the loop so it only executes when the loop completes. Still - same errors.
try {
return txRef.get().then( async txDoc => {
if (txDoc.exists) {
const userId = txDoc.data().userId;
const loyaltyIds = txDoc.data().loyaltyIds;
const pointsAwardedMap = txDoc.data().pointsAwarded;
const batch = db.batch();
// loyaltyIds.forEach(lpId => {
for (let i = 0; i < loyaltyIds.length; i++) {
// We update the user loyalty cards
const lpId = loyaltyIds[i];
console.log('Inside for loop');
console.log(lpId);
const cardId = 'u_' + userId + '-l_' + lpId; // 'u_$userId-l_$lpId'
const cardRef = db.collection('users').doc(userId).collection('userLoyaltyCards').doc(cardId);
const lpMap = pointsAwardedMap[lpId];
// Get the user LC doc
cardRef.get().then(cardDoc => {
// We created the initial loyalty card doc without relying on the cloud function
if (cardDoc.exists) {
console.log('Card found');
// Users LC found, we simply update with this transaction
// `${mpesaReceiptNo}`, this is how to add a var as a field value in firestore
batch.update(cardRef, {
'pointsBalance': cardDoc.data().pointsBalance + lpMap['points'],
'totalSpend': cardDoc.data().totalSpend + txDoc.data().transactionAmount,
'numberOfPurchases': cardDoc.data().numberOfPurchases + 1,
'pointsEarned': cardDoc.data().pointsEarned + lpMap['points'],
'lastPurchaseDate': admin.database.ServerValue.TIMESTAMP,
});
}
});
if (i + 1 == loyaltyIds.length) {
console.log('Loyalty card loop complete, now going to update other things and commit the batch.');
// Update the transaction document
batch.update(txRef, {
transactionCode: `${mpesaReceiptNo}`,
transactionType: "purchase",
transactionSuccess: true,
});
console.log('Committing the batch');
return batch.commit();
}
} // end of for loop
} else {
console.log('Transaction Doc not found, terminating function.');
return null;
}
}).then(function () {
console.log("SUCCESS")
return null;
}
).catch(function (error) {
console.log("UNABLE TO EXECUTE TX BATCH");
console.log(error);
// throw new functions.https.HttpsError('unknown', 'An error occurred when trying to sort the posts.');
return null;
});
I think your problem is related to promises. You must await for the batch.commit(), which was not done in your code. No need to use the await for batch.update(), only for the batch.commit().
Usage of the map with the Promise.all is very important here to ensure you await for all the loop operations to be completed.
I updated your code using awaits, I could not test it since I don't have access to your DB, but I think it should solve your problem with the batch.
try {
const txDoc = await txRef.get();
if (txDoc.exists) {
const userId = txDoc.data().userId;
const loyaltyIds = txDoc.data().loyaltyIds;
const pointsAwardedMap = txDoc.data().pointsAwarded;
const batch = db.batch();
await Promise.all(loyaltyIds.map(async (lpId, i) => {
console.log(lpId);
const cardId = 'u_' + userId + '-l_' + lpId; // 'u_$userId-l_$lpId'
const cardRef = db.collection('users').doc(userId).collection('userLoyaltyCards').doc(cardId);
const lpMap = pointsAwardedMap[lpId];
const cardDoc = await cardRef.get();
if (cardDoc.exists) {
batch.update(cardRef, {
'pointsBalance': cardDoc.data().pointsBalance + lpMap['points'],
'totalSpend': cardDoc.data().totalSpend + txDoc.data().transactionAmount,
'numberOfPurchases': cardDoc.data().numberOfPurchases + 1,
'pointsEarned': cardDoc.data().pointsEarned + lpMap['points'],
'lastPurchaseDate': admin.database.ServerValue.TIMESTAMP,
});
}
if (i + 1 == loyaltyIds.length) {
batch.update(txRef, {
transactionCode: `${mpesaReceiptNo}`,
transactionType: "purchase",
transactionSuccess: true,
});
}
}));
await batch.commit();
return null;
} else {
console.log('Transaction Doc not found, terminating function.');
return null;
}
} catch (error) {
console.log(error);
return null;
}

Firebase listUsers fails to get All users after a certain page

I'm using a pubsub firebase function (cron), and inside this function Im calling firebase auth users, to fill some missing data in a profile collection
Im paginating with the pageToken, the first token passed is undefined then I save it in a config db and read the token to get the next page
The issue is that I have 170K records, and listusers returns an undefined token at the 6th page (6k users) which is frsutrating
here is the code:
functions.pubsub
.schedule('*/30 * * * *')
.onRun(async () => {
const page = firestore.collection('config').doc('pageToken');
const doc = (await page.get()).data();
// Check if last page don't run again
const last = doc?.last;
if (last) return;
// Load page
const pageToken = doc?.pageToken || undefined;
let pageNumber = doc?.pageNumber as number;
return firebaseAdmin
.auth()
.listUsers(1000, pageToken)
.then(async listUsersResult => {
for (const userRecord of listUsersResult.users) {
// Fetch Profile
try {
const profile = await firestore
.collection('profiles')
.doc(userRecord.uid);
// data foramtting here
// compared profile data & fixed data
const payload = JSON.parse(
JSON.stringify({
...profileData,
...{
lastName,
firstName,
language,
...(!userRecord.phoneNumber && {
phone,
}),
},
})
);
// Profile doesn't exist : Create
if (!profileData && payload) {
await profile.create({
...payload,
...{
Migrated: true,
},
});
} else if (profileData && payload) {
const data = compare(profileData, payload);
if (data) {
// Profile exists: Update
await profile.update(data);
if (userRecord.phoneNumber)
await profile.update({
phone: firebaseAdmin.firestore.FieldValue.delete(),
});
}
}
} catch (err) {
functions.logger.error('Some Error', err);
}
}
if (!listUsersResult.pageToken) {
return await firestore
.collection('config')
.doc('pageToken')
.update({
last: true,
});
}
// List next batch of users.
pageNumber++;
return await firestore
.collection('config')
.doc('pageToken')
.update({
pageToken: listUsersResult.pageToken,
pageNumber,
});
});
});
so after in page 6, I have a last:true property added to the firestore however there is 164k data are missing
any idea ?

Firebase: Flutter: Optimizing for stability: many Transactions vs. Batch writes vs. Set calls

A few months ago, a glorious soul here taught me about transactions. I may have gone a little overboard thinking they were they best thing since sliced bread. The problem they solved was obvious, guaranteed concurrent writes on a single doc. However, I've noticed already with as little as three closely timed function triggers that I produced the dreaded: ------------------------"10 ABORTED: Too much contention on these documents."...-------------------------
Optimizing for stability, my question is: Would it be best practice to use a mixed bag of these write calls for different situations? For example: if a cloud function is writing to a location where I do not expect contention, should it just be a set call? Instead of 4 transactions to various locations, should I use a batch?
Reading the Firebase limitations I assumed I was in the clear with max 60w/doc/sec. However, I've learned now that Transactions can timeout AND only try to write 5 times.
Some background on the app and the contention error:
- It's a basic social media app.
- The contention error came from making three posts in close succession from a single user.
- Each post triggers a cloud function that does several transactions to link the post to appropriate places. i.e. followers, feed, groups, sends notifications, and sets activity feed docs for each follower.
Side question: Am I wrongly understanding that firebase can handle an app with this level of activity?
EDIT: I was aware of these firebase limitations early on and did my best work to keep documents and collections spread apart appropriately.
CODE EDIT: index.js: adminPostReview is the specific function to throw the error (did the best I could to simplify).
The specific transaction to throw the error, I believe, is the call to transactionDayIndexAdd().
function transactionDelete(docRef) {
return db.runTransaction(async t => {
var doc = await t.get(docRef);
if (doc.exists)
t.delete(docRef);
})
}
// THIS FUNCTION. Is it bad to read and set two documents?
function transactionDayIndexAdd(docRef, dayPosted, postId, userId) {
return db.runTransaction(async (t) => {
var postMap = {};
const doc = await t.get(docRef.doc(dayPosted));
if (doc.exists) {
postMap = doc.data().pids;
} else {
const indexDoc = await t.get(docRef.doc('index'));
var newIndex = indexDoc.exists ? indexDoc.data().index : {};
newIndex[dayPosted] = true;
t.set(docRef.doc('index'), { 'index': newIndex });
}
postMap[postId] = userId;
t.set(doc.ref, { 'pids': postMap });
})
}
exports.adminPostReview = functions.firestore
.document('/adminPostReview/{postId}')
.onUpdate(async (change, context) => {
const postId = context.params.postId;
const userId = change.before.data().ownerId;
const approvedMaks = change.after.data().approvedMaks;
const approvedRita = change.after.data().approvedRita;
var promises = [];
if (approvedMaks == false || approvedRita == false) {
promises.push(transactionDelete(db.collection('posts').doc(userId).collection('userPosts').doc(postId)));
}
else if (approvedMaks == true || approvedRita == true) {
var newPost = change.after.data();
promises.push(postLive(newPost));
}
if (approvedMaks != null || approvedRita != null) {
promises.push(transactionDelete(db.collection('activityFeed').doc(MAKS_ID).collection('feedItems').doc(`${postId}_review`)));
promises.push(transactionDelete(db.collection('activityFeed').doc(RITA_ID).collection('feedItems').doc(`${postId}_review`)));
}
});
async function postLive(newPost) {
const userId = newPost.ownerId;
const postId = newPost.postId;
const dayPosted = newPost.dayPosted;
var postToFeed = newPost.postToFeed;
var postToGroups = newPost.postToGroups;
newPost.approved = true;
delete newPost.postToFeed;
delete newPost.postToGroups;
var batch = db.batch();
var promises = [];
if (postToFeed == true) {
batch.set(
db.collection('posts').doc(userId).collection('userPosts').doc(postId),
newPost
);
batch.update(
db.collection('userActivity').doc(userId),
'numPosts',
admin.firestore.FieldValue.increment(1),
)
promises.push(batch.commit());
promises.push(transactionDayIndexAdd(db.collection("feedRandom"), dayPosted, postId, userId));
var querySnap = await db.collection('followers')
.doc(userId)
.collection('userFollowers')
.get();
querySnap.docs.forEach(async follower => {
promises.push(transactionDayIndexAdd(
db.collection('feedFollowing').doc(follower.id).collection('feedItems'),
dayPosted, postId, userId));
promises.push(transactionSet(db.collection('activityFeed').doc(follower.id)
.collection('feedItems').doc(postId),
{
media1Url: newPost.media1Url,
media2Url: newPost.media2Url,
postId: newPost.postId,
timestamp: newPost.timestamp,
type: 'newFollowingPost',
userId: userId,
userProfileImg: newPost.ownerProfileImg,
username: newPost.username,
displayName: newPost.displayName,
}
));
if (follower.data().notificationToken != null) {
const payload = {
notification: {
title: 'Someone you follow made a new post!',
body: `${newPost.username} has a new post.`
},
data: {
click_action: "FLUTTER_NOTIFICATION_CLICK",
vestiq_type: 'newFollowingPost',
vestiq_uid: follower.id,
vestiq_fid: userId,
vestiq_pid: postId,
vestiq_displayName: newPost.displayName,
vestiq_photoUrl: newPost.ownerProfileImg,
vestiq_username: newPost.username,
}
};
var user = await db.collection('users').doc(follower.id).get();
if (user.data().notificationOp3 == true)
promises.push(pushNotification(follower.data().notificationToken, payload));
}
});
if (postToGroups != null && postToGroups.length > 0) {
promises.push(pushGroupPosts(postToGroups, userId, postId, newPost));
return Promise.all(promises);
} else return Promise.all(promises);
}
else if (postToGroups != null && postToGroups.length > 0) {
promises.push(pushGroupPosts(postToGroups, userId, postId, newPost));
return Promise.all(promises);
}
}
async function pushGroupPosts(postToGroups, userId, postId, newPost) {
var groupBatch = db.batch();
postToGroups.forEach((gid) => {
groupBatch.set(
db.collection('groups').doc(gid).collection('posts').doc(postId),
newPost,
);
groupBatch.set(
db.collection('usersGroupPosts').doc(userId).collection(gid).doc(postId),
{ 'gid': gid, 'postId': postId },
);
});
return push(groupBatch.commit());
}
I was able to fix the contention problem by splitting transactionDayIndexAdd() into two separate transactions. I flip a bool to determine if the second should run instead.
This leads me to believe that the nested t.get/t.set transaction significantly increases chances for contention issues. Since the split, I have not been able to reproduce the error. Here is the new transactionDayIndexAdd() for those who are curious.
HOWEVER, my original question still stands regarding optimising for stability.
async function transactionDayIndexAdd(docRef, dayPosted, postId, userId) {
var dayAdd = 0;
var promises = [];
await db.runTransaction(async (t) => {
var postMap = {};
const doc = await t.get(docRef.doc(dayPosted));
if (doc.exists)
postMap = doc.data().pids;
else {
dayAdd = 1;
}
postMap[postId] = userId;
t.set(doc.ref, { 'pids': postMap });
});
if (dayAdd == 1) {
promises.push(db.runTransaction(async (t) => {
const indexDoc = await t.get(docRef.doc('index'));
var newIndex = indexDoc.exists ? indexDoc.data().index : {};
newIndex[dayPosted] = true;
t.set(indexDoc.ref, { 'index': newIndex });
}));
}
return await Promise.all(promises);
}

How to subscribe a firebase function so that it executes via a pub/sub trigger

I have been asked to create a firebase (fb) function that triggers onUpdate. Then I have to gather a bunch of information from the fb database and publish a message so that another function triggers at that point.
fb update triggers functionA
functionA publishes a message
functionB is a subscriber to that topic and is triggered after the message publishes.
I have the basics of the onUpdate trigger below:
const functions = require("firebase-functions"),
Promise = require("promise"),
PubSub = require(`#google-cloud/pubsub`),
admin = require("firebase-admin");
const pubsub = new PubSub();
exports.checkInOrder = functions.database
.ref("/orders/{id}")
.onUpdate((change, context) => {
const after = change.after.val();
// check the status: "pending-pickup" or "fulfilled" TODO
if (after.status === "new") {
console.log("ended because package is new.");
return null;
}
let dsObj = {};
const orderId = context.params.id;
const topicName = 'check-in-order';
const subscriptionName = 'check-in-order';
return // how would I send the message to the pubsub here?
});
So to summarize:
How do I send a message to pubsub
How do I subscribe a firebase function to trigger when a topic receives a message?
If it sounds very confusing I'm sorry - I am completely lost here. Thanks!
So I finally figured it out. Pretty straight forward, just felt overwhelmed with learning all of this stuff at once and in a rushed time frame. Below is my code. I included the whole page so in case this might help someone else out there in the future.
const functions = require("firebase-functions"),
Promise = require("promise"),
PubSub = require(`#google-cloud/pubsub`),
admin = require("firebase-admin");
const init = () => {
const topicName = "check-in-order";
pubsub
.createTopic(topicName)
.then(results => {
const topic = results[0];
console.log(`Topic ${topicName} created.`);
return;
})
.catch(err => {
console.error("ERROR on init:", err);
return;
});
};
const pubsub = new PubSub();
exports.orderCreated = functions.database
.ref("/orders/{id}")
.onUpdate((change, context) => {
console.log("it's happening!");
const after = change.after.val();
console.log("after::::>", after)
if (after.status === "new") {
console.log('stopped because status is new');
return null;
}
if (after.status === "pending-pickup" || after.status === "fulfilled") {
const orderId = context.params.id;
const topicName = "check-in-order";
let { assignedSlot, userId, parcelLockerId, carrier, trackingNumber, orderInDate, pickUpCode, status, } = after;
const dsObj = {
order: {
orderId,
userId,
parcelLockerId,
carrier,
trackingNumber,
orderInDate,
pickUpCode,
status,
}
};
const dataBuffer = new Buffer.from(dsObj.toString());
// publish to trigger check in function
return pubsub
.topic(topicName)
.publisher()
.publish(dataBuffer)
.then(messageId => {
console.log(`:::::::: Message ${messageId} has now published. :::::::::::`);
return true;
})
.catch(err => {
console.error("ERROR:", err);
throw err;
});
}
return false;
});
exports.checkInOrder = () => {
}
exports.checkIn = functions.pubsub.topic('check-in-order').onPublish((message) => {
console.log("everything is running now", message);
return true;
});
init();

Firebase Cloud Functions set value operation deletes the trigger originating node

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

Resources