Firebase Cloud Function (async) not returning result - firebase

I am rather new to Firebase/Firestore/Cloud functions and been trying a little project where a client app calls a Firebase Cloud Function to generate some random keys (random numbers), adds them to Firestore, and when successfully written, returns these keys to the client app. Kinda like a random number generator.
The function is called correctly by the client (according to Firebase Console), does generate the keys, checks if they exist in the Firestore, and if not adds them. All works up to the part where it should return the result to the client. Here the client never gets the result (the keys array). In fact, the callback in the client app (iOS/Swift) is never called.
I am suspecting the problem lies in the promise being returned? According to the Firebase documentation here, async callables should return a Promise although I am not entirely sure what I am doing is correct https://firebase.google.com/docs/functions/callable
Here is the code for the cloud function:
export const generateRandomKeys = functions.https.onCall(async (data, context) => {
// Read data passed from client
const numberOfKeys = data.numberOfKeys
console.log("Number of keys to generate: ", numberOfKeys)
// Generate some keys
const generatedKeys = KeyMaker.newKeys(numberOfKeys)
try {
const randomkeys = []
// Write keys to DB
for (const key of generatedKeys) {
const addedKey = await writeKeyToDB(key)
randomkeys.push(addedKey)
}
return Promise.resolve(JSON.stringify(randomkeys))
} catch (error) {
console.log("Error has occured: ", error)
throw new Error("An Error has occured: " + error)
}
})
async function writeKeyToDB(key: string){
try {
// Check if a document with the same key already exists in the DB
const docRef = db.collection("randomKeys").doc(key)
const doc = await docRef.get()
// Document with same key found!
if (doc.exists) {
// Generate a new key and try again
const newKey = KeyMaker.newKey()
console.log("Will generate a new key and try again!. New key: ", newKey)
await writeKeyToDB(newKey)
}
const keyDoc = {
somefield: somevalue,
}
// Write to DB then return result
await docRef.set(keyDoc)
return Promise.resolve(key)
} catch (error) {
return Promise.reject(error)
}
}
Client (Swift)
public static func generateNewRandomNumbers(numberOfKeys: Int) {
FirebaseApp.configure()
let functions = Functions.functions(region: FIRESTORE_REGION)
functions.httpsCallable("generateRandomKeys").call(["numberOfKeys": numberOfKeys]) { (result, error) in
// Nothing here executes
print("----------got reply---------")
if let error = error as NSError? {
if error.domain == FunctionsErrorDomain {
let code = FunctionsErrorCode(rawValue: error.code)
let message = error.localizedDescription
print("Error \(String(describing: code)): " + message)
}
}
if let keys = (result?.data as? [String]) {
dump(keys)
}
}
}

Dont combine Async/Await and Promise. Async functions as itself returning Promise.
First change return of your cloud function to :
return JSON.stringify(randomkeys);
Also in writeKeyToDb change return to:
return key;
and catch part to:
throw Error(error);
I see also problem that in cloud function you calling your writeKeyToDb function with 2 parameters, but that function have only one. But that code probably is in progress

Finally found the issue, thanks for Doug and Dominik for guiding me in the right direction. I removed the promises and returned directly the values but more importantly, I didn't need to convert the array to JSON. I came across HTTPSCallableResult documentation
I simply changed this
return JSON.stringify(randomkeys);
to
return randomkeys
and on the client, instead of
if let keys = (result?.data as? [String]) {
dump(keys)
}
I do
if let keys = (result?.data as? NSArray) {
dump(keys)
}

Related

Is return value important in Firebase Cloud Functions

I am writing the Firebase Could Functions with TypeScript and the following is a simple method to update a document.
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp(functions.config().firebase);
export const handleTestData = functions.firestore.document('test/{docID}').onCreate(async (snap, context) => {
const data = snap.data();
if (data) {
try {
await admin.firestore().doc('test1/' + context.params.docID + '/').update({duplicate : true});
} catch (error) {}
}
});
In this method, the promise is handled by async await and there is no return statement and it's working fine. Most of the examples/tutorials I have seen always have a return statement in each method.
Is there any impact/difference I don't return anything in Firebase Cloud Functions? If I should return something, can I return null?
Is return value important in Firebase Cloud Functions?
Yes, it is really key, in a Cloud Function which performs asynchronous processing (also known as "background functions") to return a JavaScript promise when all the asynchronous processing is complete, as explained in the documentation.
Doing so is important for two main reasons (excerpts from the doc):
You make sure that the Cloud Functions instance running your Cloud Function does not shut down before your function successfully reaches its terminating condition or state.
You can avoid excessive charges from Cloud Functions that run for too long or loop infinitely.
Why is your Cloud Function running correctly even if you don't return a Promise?
Normally your Cloud Function should be terminated before the asynchronous operations are completed, because you don't return a Promise and therefore indicate to the Cloud Functions platform that it can terminate the Cloud Functions instance running the Cloud Function.
But sometimes, the Cloud Functions platform does not terminate the Function immediately and the asynchronous operations can be completed. This is not at all guaranteed and totally out of your control.
Experience has shown that for short asynchronous operations this last case happens quite often and the developer thinks that everything is ok. But, all of sudden, one day, the Cloud Function does not work... and sometimes it does work: The developer is facing an "erratic" behaviour without any clear logic, making things very difficult to debug. You will find a lot of questions in Stack Overflow that illustrate this situation.
So concretely, in your case you can adapt your code like:
export const handleTestData = functions.firestore.document('test/{docID}').onCreate(async (snap, context) => {
const data = snap.data();
if (data) {
try {
// See the return below: we return the Promise returned by update()
return admin.firestore().doc('test1/' + context.params.docID + '/').update({duplicate : true});
} catch (error) {
return null; // <- See the return
}
} else {
return null; // <- See the return
}
});
or like
export const handleTestData = functions.firestore.document('test/{docID}').onCreate(async (snap, context) => {
const data = snap.data();
if (data) {
try {
await admin.firestore().doc('test1/' + context.params.docID + '/').update({duplicate : true});
return null; // <- See the return
} catch (error) {
return null; // <- See the return
}
} else {
return null; // <- See the return
}
});
Returning null (or true, or 1...) is valid since an async function always returns a Promise.

How to complete login only after functions.auth.user().onCreate is finished

I'm using firebase functions and I have a function which add new collection when user is creating. The problem is sometimes user is logged in before function is done, so user is logged in but new collection is not created yet (and then I have error message 'Missing or insufficient permissions. because a rule cannot find that collection'). How can I handle it?
Is it possible to finish login user (for example using google provider) only when all stuff from
export const createCollection = functions.auth.user().onCreate(async user => {
try {
const addLanguages = await addFirst();
const addSecondCollection = await addSecond();
async function addFirst() {
const userRef = admin.firestore().doc(`languages/${user.uid}`);
await userRef.set(
{
language: null
},
{ merge: true }
);
return 'done';
}
async function addSecond() {
// ...
}
return await Promise.all([addLanguages, addSecondCollection]);
} catch (error) {
throw new functions.https.HttpsError('unknown', error);
}
});
is finished? So google provider window is closed and user is logged in only after that? (and don't using setTimeouts etc)
AFAIK it is not possible to directly couple the two processes implied in your application:
On one hand you have the Google sign-in flow implemented in your front-end (even if there is a call to the Auth service in the back-end), and;
On the other hand you have the Cloud Function that is executed in the back-end.
The problem you encounter comes from the fact that as soon as the Google sign-in flow is successful, your user is signed in to your app and tries to read the document to be created by the Cloud Function.
In some cases (due for example to the Cloud Function cold start) this document is not yet created when the user is signed in, resulting in an error.
One possible solution would be to set a Firestore listener in your front-end to wait for this document to be created, as follows. Note that the following code only takes into account the Firestore document created by the addFirst() function, since you don't give any details on the second document to be created through addSecond().
firebase.auth().signInWithPopup(provider)
.then(function(result) {
var token = result.credential.accessToken;
var user = result.user;
//Here we know the userId then we can set a listener to the doc languages/${user.uid}
firebase.firestore().collection("languages").doc(user.uid)
.onSnapshot(function(doc) {
if(doc.exists) {
console.log("Current data: ", doc.data());
//Do whatever you want with the user doc
} else {
console.log("Language document not yet created by the Cloud Function");
}
});
}).catch(function(error) {
var errorCode = error.code;
var errorMessage = error.message;
var email = error.email;
var credential = error.credential;
// ...
});
As said above, in the above code we only take into account the first Firestore document created by the addFirst() function. But you probably need to wait for the two docs to be created before reading them from the front-end.
So, you may modify you CF as follows:
export const createCollection = functions.auth.user().onCreate(async user => {
try {
await addFirst();
await addSecond();
return null;
async function addFirst() {
const userRef = admin.firestore().doc(`languages/${user.uid}`);
await userRef.set(
{
language: null
},
{ merge: true }
);
}
async function addSecond() {
// ...
}
} catch (error) {
console.log(error);
return null;
}
});
Note that you don't need to use Promise.all(): the following two lines already execute the two document writes to Firestore. And, since you use async/await the second document is only written after the first one is written.
const addLanguages = await addFirst();
const addSecondCollection = await addSecond();
So you just need to set the listener on the path of the second document, and you are done!
Finally, note that doing
throw new functions.https.HttpsError('unknown', error);
in your catch block is the way you should handle errors for a Callable Cloud Function. Here, you are writing a background triggered Cloud Function, and you can just use return null;

Trouble reading data in Firebase Cloud Function

Trying to read a pushToken from a given user in the users collection (after an update operation on another collection) returns undefined
exports.addDenuncia = functions.firestore
.document('Denuncias/{denunciaID}')
.onWrite((snap, context) => {
const doc = snap.after.data()
const classificadoId = doc.cid
const idTo = doc.peerId
db.collection('Classificados').doc(classificadoId)
.update({
aprovado: false
})
.then(r => {
getToken(idTo).then(token => {
// sendMsg...
})
}).catch(updateErr => {
console.log("updateErr: " + updateErr)
})
async function getToken(id) {
let response = "getTokenResponse"
console.log("id in getToken: " + id)
return db.collection('users').doc(id).get()
.then(user => {
console.log("user in getToken: " + user.data())
response = user.data().pushToken
})
.catch(e => {
console.log("error get userToken: " + e)
response = e
});
return response
}
return null
});
And this is from the FB console log:
-1:43:33.906 AM Function execution started
-1:43:36.799 AM Function execution took 2894 ms, finished with status: 'ok'
-1:43:43.797 AM id in getToken: Fm1RwJaVfmZoSgNEFHq4sbBgoEh1
-1:43:49.196 AM user in getToken: undefined
-1:43:49.196 AM error get userToken: TypeError: Cannot read property 'pushToken' of undefined
-1:43:49.196 AM returned token: undefined
And we can see in this screenshot from the db that the doc does exist:
Hope someone can point me to what I'm doing wrong here.
added screenshot of second example of #Renaud as deployed:
As Doug wrote in his comment, you need to "return a promise from the top level function that resolves when all the async work is complete". He also explains that very well in the official video series: https://firebase.google.com/docs/functions/video-series/ (in particular the 3 videos titled "Learn JavaScript Promises"). You should definitely watch them, highly recommended!
So, the following modifications to your code should work (untested):
exports.addDenuncia = functions.firestore
.document('Denuncias/{denunciaID}')
.onWrite(async (snap, context) => { // <- note the async keyword
try {
const doc = snap.after.data()
const classificadoId = doc.cid
const idTo = doc.peerId
await db.collection('Classificados').doc(classificadoId)
.update({
aprovado: false
});
const userToSnapshot = await db.collection('users').doc(idTo).get();
const token = userToSnapshot.data().pushToken;
await sendMsg(token); // <- Here you should take extra care to correctly deal with the asynchronous character of the sendMsg operation
return null; // <-- This return is key, in order to indicate to the Cloud Function platform that all the asynchronous work is done
} catch (error) {
console.log(error);
return null;
}
});
Since you use an async function in your code, I've used the async/await syntax but we could very well write it by chaining the promises with the then() method, as shown below.
Also, I am not sure, in your case, that it adds any value to put the code that gets the token in a function (unless you want to call it from other Cloud Functions but then you should move it out of the addDenuncia Cloud Function). That's why it has been replaced by two lines of code within the main try block.
Version with chaining promises via the then() method
In this version we chain the different promises returned by the asynchronous methods with the then() method. Compared to the async/await version above, it shows very clearly what means "to return a promise from the top level function that resolves when all the asynchronous work is complete".
exports.addDenuncia = functions.firestore
.document('Denuncias/{denunciaID}')
.onWrite((snap, context) => { // <- no more async keyword
const doc = snap.after.data()
const classificadoId = doc.cid
const idTo = doc.peerId
return db.collection('Classificados').doc(classificadoId) // <- we return a promise from the top level function
.update({
aprovado: false
})
.then(() => {
return db.collection('users').doc(idTo).get();
})
.then(userToSnapshot => {
if {!userToSnapshot.exists) {
throw new Error('No document for the idTo user');
}
const token = userToSnapshot.data().pushToken;
return sendMsg(token); // Again, here we make the assumption that sendMsg is an asynchronous function
})
.catch(error => {
console.log(error);
return null;
})
});

HTTPS Callable Function that returns async function returning empty object

I have a HTTPS Callable function of with the following structure:
/**
* Accepts a friend request from another user
*/
export const acceptFriendRequest = functions.https.onCall(
(data : standardStructs.fromToStruct, context) => {
standardChecks(data, context)
if (!context.auth || context.auth.uid === data.to){
throw new functions.https.HttpsError(
'invalid-argument',
'You cannot do this operation to yourself!');
}
return async () => {
const fromSnapshot = await admin.database().ref(...).once('value');
const toSnapshot = await admin.database().ref(...).once('value');
const inboxSnapshot = await admin.database().ref(...).once('value');
const updates = {} as any;
const response = {} as any
updates[`...`] = null;
updates[`...`] = null;
//If the desintation doesn't exist, then let's just erase this friend request
if (!toSnapshot.exists()){
response.status = standardHttpsData.returnStatuses.NOTO
}else if (!inboxSnapshot.exists()){
//This user is trying to accept a request that was never sent to them
response.status = standardHttpsData.returnStatuses.INVALID
}else{
updates[`...`] = toSnapshot.val();
updates[`...`] = fromSnapshot.val();
response.status = standardHttpsData.returnStatuses.OK
}
await admin.database().ref().update(updates);
return response
};
});
For some reason, this function returns an empty object. Additionally, it doesn't make any writes to the database, even though the necessary snapshots exists for it to do so.
I'm not sure why, because I am indeed retuning a promise, just like the documentation says I should if I'm doing asynchronous commands.
Instead of returning an async function within your Callable Cloud Function, you should declare the handler function as async, as follows:
export const acceptFriendRequest = functions.https.onCall(
async (data : standardStructs.fromToStruct, context) => {
standardChecks(data, context)
if (!context.auth || context.auth.uid === data.to){
throw new functions.https.HttpsError(
'invalid-argument',
'You cannot do this operation to yourself!');
}
const fromSnapshot = await admin.database().ref(...).once('value');
const toSnapshot = await admin.database().ref(...).once('value');
const inboxSnapshot = await admin.database().ref(...).once('value');
//...
await admin.database().ref().update(updates);
return response
});
You don't give details on what is the standardHttpsData object and associated returnStatuses (nor you give details on standardChecks), but you should note that you need to return, in your Cloud Function, some "data that can be JSON encoded", see the doc.
Update following your comment above:
The solution proposed here will work with your returnStatuses map, since the map value can be JSON encoded. However, note that the documentation recommends handling the error in a different way, using an instance of functions.https.HttpsError.

Firebase Cloud Function Does Not Send Response

here is my code of http cloud function that reads some documents and then send response
res.set('Access-Control-Allow-Origin', '*');
var orderId;
var result = "";
var userId;
var promoCode;
var promoRef;
var userDocRef;
var promoCodeDoc;
//userId = req.body.userId;
//orderId = req.body.orderId;
promoCode = req.body.promoCode;
//userDocRef = db.collection("Users").doc()
promoRef = db.collection("PromoCodes").doc(promoCode);
var transaction = db.runTransaction(t => {
return t.get(promoRef)
.then(promoCodeDoc => {
if(promoCodeDoc.exists){
result = "OK";
res.json(result);
}else{
result = "Invalid Promocode!";
res.json(result);
}
//t.update(cityRef, {population: newPopulation});
return true;
});
}).then(result => {
console.log('Transaction success!');
return true;
}).catch(err => {
console.log('Transaction failure:', err);
});
return Promise.all(transaction());
But This is not sending the response because functions ends but Firestore Transaction is still runnning in background .
Any Solution to my problem ?
Promise.all() expects a single array of promises as its argument, but you're not giving it an array argument. Secondly, the transaction variable is a promise, not a function. You can't call () a promise.
So I think the correct code would be return Promise.all([transaction]). This being said, you only have one promise so you don't need Promise.all and can just return transaction.
Not sure if this will solve all your problems though. If you log into the firebase console, navigate to the functions section, there's a "Logs" tab that allows you to see debugging output from your function executions. It might help you track down all the problems. I imagine there are already console errors logged pointing out the fact that transaction() is not a function.

Resources