Firebase Cloud Function not executing - firebase

I'm working on the group functionality of my app where members of the group can add task to a group that they are currently working on. So, when a task is added I want to notify all members using FCM that a task had been added to the group.
EDIT:
The code to add the task to a group is run on the client and works successfully. The purpose of the cloud function is just to send cloud messages to all the members of the group that a task has been added by a particular member.
This is my logic for the cloud function :
1st. As a task can be added to multiple groups at a time I'm using a forEach().
2nd. I'm fetching the uids of the Members in the groups and pushing them into an array(uids) so that later I can retrieve their fcmTokens.
3rd. Running a forEach on the uids to retrieve the fcmTokens.
4th.Sending Cloud message to devices.
But the cloud function doesn't execute as expected.
This my cloud function:
exports.newTaskAdded = functions.https.onCall(async (data, context) => {
const groups = data.groups; //Can be multiple groups hence an array.
const uid = data.uid;
const author = data.author;
const taskId = data.taskId;
const taskTitle = data.taskTitle;
try {
groups.forEach(async group => {
const groupName = group.groupName;
console.log('groupName: ', groupName);
const groupId = groups.groupId;
const membersPromises = [];
membersPromises.push(
admin
.firestore()
.collection('Groups')
.doc(`${groupId}`)
.collection('Members') //Members collection has document for each user with their uid as the document name.
.get(),
);
console.log('memberPromises: ', membersPromises);//Function stops after this.
const membersSnapshot = await Promise.all(membersPromises);
console.log('membersSnapshots', membersSnapshot);
const uids = [];
membersSnapshot.forEach(doc => {
doc.forEach(snap => {
console.log(snap.id);
uids.push(snap.id);
});
});
console.log(uids);
const uidPromises = [];
uids.forEach(uid => {
uidPromises.push(
admin
.firestore()
.collection('Users')
.doc(`${uid}`)
.get(),
);
});
console.log('uidPromises: ', uidPromises);
const tokensSnapshots = await Promise.all(uidPromises);
const notifPromises = [];
tokensSnapshots.forEach(snap => {
console.log(snap.data());
const token = Object.keys(snap.data().fcmTokens);
const payload = {
notification: {
title: `${author} has added a new task to ${groupName}`,
body: `Task Added: ${taskTitle}`,
sound: 'default',
},
};
notifPromises.push(admin.messaging().sendToDevice(token, payload));
});
await Promise.all(notifPromises);
});
} catch (err) {
console.log(err);
}
return {result: 'OK'};
});
This is my log:
As you can see there is no error shown.
Help would be very much appreciated. Thank you

Related

Send auth context to firebase callable function in unittest

I have been working on a firebase project in which I created a cloud function that creates documents in firestore. This is the function -
export const createExpenseCategory = functions
.region("europe-west1")
.https.onCall(async (data, context) => { // data is a string
if (!context.auth?.uid) { // check that requesting user is authenticated
throw new functions.https.HttpsError(
"unauthenticated",
"Not Authenticated"
);
}
const res = await admin
.firestore()
.collection("/categories/")
.where("uid", "==", context.auth.uid)
.get();
const categoryExists = res.docs.find((doc) => doc.data().name === data); // check that there are not duplicates.
// doc looks like this -
// {
// "name": "Food",
// "uid": "some_long_uid"
// }
if (categoryExists) {
throw new functions.https.HttpsError(
"already-exists",
`Category ${data} already exists`
);
}
return admin
.firestore()
.collection("/categories/")
.add({ name: data, uid: context.auth.uid });
});
As you can see, at the beginning of the function I check whether the user that sent the request is authenticated with the context parameter. Everything works fine when I play around with it in my web app, but I have been trying to figure out a way to create a unittest for this function. My problem is that I can't really figure out how to create an authenticated request to make sure that my function doesn't fail every time. I tried to look online for any documentation but couldn't seem to find any.
Thanks in advance!
You can unit test your functions using the firebase-functions-test SDK. The guide mentions you can mock the data within the eventContext or context parameter passed to your function. This works for mocking the uid field of the auth object:
// Left out authType as it's only for RTDB
wrapped(data, {
auth: {
uid: 'jckS2Q0'
}
});
The guide uses mocha for testing, but you can use other testing frameworks. I made a simple test to see if it would work and I could send the mock uid to the function, which worked as expected:
index.js
exports.authTest = functions.https.onCall( async (data, context) => {
if(!context.auth.uid){
throw new functions.https.HttpsError('unauthenticated', 'Missing Authentication');
}
const q = await admin.firestore().collection('users').where('uid', '==', context.auth.uid).get();
const userDoc = q.docs.find(doc => doc.data().uid == context.auth.uid);
return admin.firestore().collection('users').doc(userDoc.id).update({name: data.name});
});
index.test.js
const test = require('firebase-functions-test')({
projectId: PROJECT_ID
}, SERVICE_ACCTKEY); //Path to service account file
const admin = require('firebase-admin');
describe('Cloud Functions Test', () => {
let myFunction;
before(() => {
myFunction = require('../index.js');
});
describe('AuthTest', () => {
it('Should update user name in UID document', () => {
const wrapped = test.wrap(myFunction.authTest);
const data = {
name: 'FooBar'
}
const context = {
auth: {
uid: "jckS2Q0" //Mocked uid value
}
}
return wrapped(data, context).then(async () => {
//Asserts that the document is updated with expected value, fetches it after update
const q = await admin.firestore().collection('users').where('uid', '==', context.auth.uid).get();
const userDoc = q.docs.find(doc => doc.data().uid == context.auth.uid);
assert.equal(userDoc.data().name, 'FooBar');
});
});
});
});
Let me know if this was useful.

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 cloud functions - update a different object within OnUpdate cloud trigger

Assume there is a collection of users and each user is associated with accounts, which are kept in a separate collection. For each account there is a balance which is updated periodically by some external means (e.g. the http trigger below). I need to be able to query for the user's total balance across all of her accounts.
I added onUpdate trigger which gets called everytime an account changes and updates the total accordingly. However, it seems that there is some race condition e.g. when two accounts get updated around the same time: after onUpdate is called for the first account and updates the total balance, it is still not updated when onUpdate is called for the second account. I'm guessing I need to somehow use "transaction" for the bookkeeping but not sure how.
const data = {
'users/XXX': {
email: "a#b.com",
balance: 0
},
"accounts/YYY": {
title: "Acc1",
userID: "XXX"
balance: 0
},
"accounts/ZZZ": {
title: "Acc2",
userID: "XXX"
balance: 0
}
};
exports.updateAccounts = functions.https.onRequest((request, response) => {
admin.firestore().collection('accounts').get().then((accounts) => {
accounts.forEach((account) => {
return admin.firestore().collection('accounts').doc(account.id).update({balance:
WHATEVER});
})
response.send("Done");
});
exports.updateAccount = functions.firestore
.document('accounts/{accountID}')
.onUpdate((change, context) => {
const userID = change.after.data().userID;
admin.firestore().doc("users/"+userID).get().then((user) => {
const new_balance = change.after.data().balance;
const old_balance = change.before.data().balance;
var user_balance = user.data().balance + new_balance - old_balance;
admin.firestore().doc("users/"+userID).update({balance: user_balance});
});
});
By looking at your code we can see several parts of it that could lead to incorrect results. It is not possible, without thoroughly testing and reproducing your problem, to be sure at 100% that correcting them will totally solve your problem but it is most probably the cause of the problems.
HTTP Cloud Function:
With the forEach() loop you are calling several asynchronous operations (the update() method) but you don't wait that all these asynchronous operations are completed before sending back the response. You should do as follows, using Promise.all() to wait all the asynchronous methods are completed before sending the response:
exports.updateAccounts = functions.https.onRequest((request, response) => {
const promises = [];
admin.firestore().collection('accounts').get()
.then(accounts => {
accounts.forEach((account) => {
promises.push(admin.firestore().collection('accounts').doc(account.id).update({balance: WHATEVER}));
return Promise.all(promises);
})
.then(() => {
response.send("Done");
})
.catch(error => {....});
});
onUpdate background triggered Cloud Function
There you need to correctly return the Promises chain in order to indicate to the platform when the Cloud Function is complete. The following should do the trick:
exports.updateAccount = functions.firestore
.document('accounts/{accountID}')
.onUpdate((change, context) => {
const userID = change.after.data().userID;
return admin.firestore().doc("users/"+userID).get() //Note the return here. (Note that in the HTTP Cloud Function we don't need it! see the link to the video series below)
.then(user => {
const new_balance = change.after.data().balance;
const old_balance = change.before.data().balance;
var user_balance = user.data().balance + new_balance - old_balance;
return admin.firestore().doc("users/"+userID).update({balance: user_balance}); //Note the return here.
});
});
I would suggest that you watch the 3 videos about "JavaScript Promises" from the Firebase video series: https://firebase.google.com/docs/functions/video-series/. They explain all the key points that were corrected above.
At first sight, it seems that if you modify, in the updateAccounts Cloud Function, several account documents that share the same user you will indeed need to implement the user balance update in a transaction, as several instances of the updateAccount Cloud Function may be triggered in parallel. The doc on Transactions is here.
Update:
You could implement a Transaction as follows in the updateAccounts Cloud Function (untested):
exports.updateAccount = functions.firestore
.document('accounts/{accountID}')
.onUpdate((change, context) => {
const userID = change.after.data().userID;
const userRef = admin.firestore().doc("users/" + userID);
return admin.firestore().runTransaction(transaction => {
// This code may get re-run multiple times if there are conflicts.
return transaction.get(userRef).then(userDoc => {
if (!userDoc.exists) {
throw "Document does not exist!";
}
const new_balance = change.after.data().balance;
const old_balance = change.before.data().balance;
var user_balance = userDoc.data().balance + new_balance - old_balance;
transaction.update(userRef, {balance: user_balance});
});
}).catch(error => {
console.log("Transaction failed: ", error);
return null;
});
});
In addition to what #Renaud Tarnec covered in their answer, you may also want to consider the following approaches:
Batched Write
In your updateAccounts function, you are writing many pieces of data at once, if any one of these fail, you may end up with a database that contains a mix of correctly updated data and data that had failed to be updated.
To solve this, you can use a batched write to write the data atomically where all new data is updated successfully or none of your data is written leaving your database in a known state.
exports.updateAccounts = functions.https.onRequest((request, response) => {
const db = admin.firestore();
db.collection('accounts')
.get()
.then((qsAccounts) => { // qs -> QuerySnapshot
const batch = db.batch();
qsAccounts.forEach((accountSnap) => {
batch.update(accountSnap.ref, {balance: WHATEVER});
})
return batch.commit();
})
.then(() => response.send("Done"))
.catch((err) => {
console.log("Error whilst updating balances via HTTP Request:", err);
response.status(500).send("Error: " + err.message)
});
});
Splitting the counters
Instead of storing a single "balance" in your document, it may instead be desirable (based on what you are trying to do) to store each account's balance in the user's document.
"users/someUser": {
...,
"balances": {
"accountId1": 10,
"accountId4": -20,
"accountId23": 5
}
}
If you need the cumulative balance, just add them together on the client. If you need to remove a balance, simply delete it's entry in the user document.
exports.updateAccount = functions.firestore
.document('accounts/{accountID}')
.onUpdate((change, context) => {
const db = admin.firestore();
const accountID = context.params.accountID;
const newData = change.after.data();
const accountBalance = newData.balance;
const userID = newData.userID;
return db.doc("users/"+userID)
.get()
.then((userSnap) => {
return db.doc("users/"+userID).update({["balances." + accountID]: accountBalance});
})
.then(() => console.log(`Successfully updated account #${accountID} balance for user #${userID}`))
.catch((err) => {
console.log(`Error whilst updating account #${accountID} balance for user #${userID}`, err);
throw err;
});
});

How to update document in firebase cloud function

In my cloud function I want to update my document from 'dashboard' collection when a new student added to 'students' collection.
const getActiveStudents = () => {
return db.collection('/students/').where('status', '==', true).get().then(
snapshot => {
let studentsCount = snapshot.docs.length;
db.collection('/dashboard/').where('type', '==', 'students').get().then(
result => {
if (result.docs.length === 0) {
db.collection('dashboard').add({
count: studentsCount,
type: 'students',
label: 'Active students'
});
}else {
result.docs[0].ref.update({
count: studentsCount,
type: 'students',
label: 'Active students'
});
}
return result;
}
).catch(error => {
console.log(error);
});
return snapshot;
}
).catch(error => {
console.log(error);
})
}
exports.onChangesInStudents = functions.firestore.document('/students/{studentId}').onWrite(event => {
getActiveStudents();
return;
});
When I add a new student, instead of updating document it adds a new document to my 'dashboard' collection.
How should I organize my code in order to properly update the quantity of students.
as #Doug mentioned, iterating over the entire collection is too heavy. instead you can stream the query results and iterate over keys, using query.stream().
to access and update a single field in a document, first retrieve the document by its ID with doc(), then use update() while specifying the field.
here's an example of implementation based on your scenario.
package.json
{
"dependencies": {
"firebase-admin": "^6.5.1",
"firebase-functions": "^2.1.0"
}
}
index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
const studentsRef = admin.firestore().collection('students');
const dashboardRef = admin.firestore().collection('dashboard');
exports.addStudent = functions.firestore
.document('students/{studentId}')
.onCreate((snap, context) => {
var newStudent = snap.data();
console.log('New student in collection: ', newStudent);
var activeCount = 0;
studentsRef.where('status', '==', true).select().stream()
.on('data', () => {
++activeCount;
}).on('end', () => {
dashboardRef.where('type', '==', 'students').get()
.then(querySnap => {
if (querySnap.docs[0].data().count == activeCount){
console.log('No new active student: ', querySnap.docs[0].data());
} else {
console.log('New active count: ', activeCount);
console.log('Student Dashboard before update: ', querySnap.docs[0].id, '=>', querySnap.docs[0].data());
dashboardRef.doc(querySnap.docs[0].id).update({
count: activeCount
});
console.log('Active student count updated: ', querySnap.docs[0].data().count, '=>', activeCount);
};
});
});
return null
});
gcloud
gcloud functions deploy addStudent \
--runtime nodejs8 \
--trigger-event providers/cloud.firestore/eventTypes/document.create \
--trigger-resource "projects/[PROJECT_ID]/databases/(default)/documents/students/{studentId}"
When a function is triggered, you might want to get data from a document that was updated, or get the data prior to update.
You can get the prior data by using change.before.data(), which contains the document snapshot before the update.
Similarly, change.after.data() contains the document snapshot state after the update.
Node.js
exports.updateUser = functions.firestore
.document('users/{userId}')
.onUpdate((change, context) => {
// Get an object representing the current document
const newValue = change.after.data();
// ...or the previous value before this update
const previousValue = change.before.data();
//...therefore update the document as.
admin.firestore().collection('user').doc(docId).update(snapshot.after.data());
});
Reference:-
https://firebase.google.com/docs/functions/firestore-events

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