Thomas David Kehoe
Thomas David Kehoe

Reputation: 10930

How to return a promise from a Firebase Firestore set() call?

This is a followup to my question https://stackoverflow.com/questions/54100270/google-translate-api-and-firebase-firestore-are-killing-each-other. Doug Stevenson said that I needed to return my functions. We watched his video Learn JavaScript Promises (Pt. 3) for sequential and parallel work in Cloud Functions a few hundred times...

I'm working with two functions. The first, a call to Google Translate, now returns a translation. The second function is a Firestore set() call, to write the translation to the database. The set() works if I don't do the Google Translate call, but together they crash. Specifically, if I call Firebase set() than the Google Translate function executes, we see Result1, and nothing further. In other words, calling the database stops to code from pushing a translation into the translationArray.

Here's my code now:

exports.Google_EStranslateEN = functions.firestore.document('Users/{userID}/Spanish/Translation_Request_NOT').onUpdate((change, context) => {
    if (change.after.data().word != undefined) {
        const { Translate } = require('@google-cloud/translate');

        // Your Google Cloud Platform project ID
        const projectId = 'myProject-cd99d';

        // Instantiates a client
        const translate = new Translate({
            projectId: projectId,
        });

        // The text to translate
        const text = change.after.data().word;
        console.log(text);
        // The target language
        const target = 'en';

        let translationArray = []; // clear translation array

        const finalPromise = translate.translate(text, target)
            .then(function (results) {
                console.log("Result1: " + results);
                console.log(Array.isArray(results));
                console.log(results[0]);
                let translation = results[0];
                console.log(translation);
                return translation
            })
            .then(function (results) {
                console.log("Translation: " + results);
                translationArray.push(results);
                return translationArray
            })
            .then(function (results) {
                console.log("TranslationArray: " + translationArray);
                console.log("Result2: " + results);
                console.log("Text: " + text)
                return admin.firestore().collection('Dictionaries').doc('Spanish').collection('Words').doc(text).collection('Translations').doc('English').set({
                    translationArray: results,
                    language: 'en',
                    longLanguage: 'English'
                });
            })
            .then(function (results) {
                console.log("Result3: " + results)
                console.log("Write succeeded!");
            })
            .catch(function (error) {
                console.error(error);
            });
    } // close if
    return 0;
}); // close oxfordPronunciationUS

The Firestore set() call returns nothing, and it kills the Google Translation call. Specifically, without the Firestore call the code executes all the way through, logging everything. With the Firestore call, neither function executes (Google Translate is never called) and nothing logs after "text."

I don't understand what const finalPromise does. It looks like an unused constant.

We read Why are the Firebase API asynchronous? and tried this code:

var promise = admin.firestore().collection('Dictionaries').doc('Spanish').collection('Words').doc(text).collection('Translations').doc('English').set({
    translationArray: translationArray,
    language: 'en',
    longLanguage: 'English'
});
promise.then(...)

That didn't help.

I tried switching to IBM Watson Translate, and the same thing is happening. Without the database call the translation function works perfectly. With the Firestore call it gets the translation, runs the forEach, and then stops when I try to push the word into the array. translationsArray doesn't log and nothing writes to the database.

exports.IBM_EStranslateEN = functions.firestore.document('Users/{userID}/Spanish/Translation_Request_IBM').onUpdate((change, context) => {
    if (change.after.data().word != undefined) {

        let word = change.after.data().word;
        let wordArray = [];
        wordArray.push(word);

        var LanguageTranslatorV3 = require('watson-developer-cloud/language-translator/v3');

        var languageTranslator = new LanguageTranslatorV3({
            version: '2018-05-01',
            iam_apikey: 'swordfish',
            url: 'https://gateway.watsonplatform.net/language-translator/api',
            headers: {
                'Content-Type': 'application/json'
            }
        });

        var parameters = {
            "text": wordArray,
            "model_id": "es-en",
            "source": "es",
            "target": "en"
        };

        let translationsArray = [];

        languageTranslator.translate(
            parameters,
            function (err, response) {
                if (err) {
                    console.log('error:', err);
                } else {
                    response.translations.forEach(function (translation) {
                        console.log(translation.translation);
                        translationsArray.push(translation.translation);
                    });
                    console.log(translationsArray);

                    admin.firestore().collection('Dictionaries').doc('Spanish').collection('Words').doc(word).collection('Translations').doc('English').set({
                        translationsArray: translationsArray,
                        language: 'en',
                        longLanguage: 'English'
                    })
                        .then(function () {
                            console.log("Translations written to database.");
                        })
                        .catch(function (error) {
                            console.error(error);
                        });

                }
            }
        );
    }
    return 0;
});

I also wrote the same cloud function calling the Oxford English Dictionary for the translation. This works perfectly, writing the translation to the database:

exports.Oxford_EStranslateEN = functions.firestore.document('Users/{userID}/Spanish/Translation_Request').onUpdate((change, context) => {
    if (change.after.data().word != undefined) {
        let word = change.after.data().word;
        let options = {
            uri: 'https://od-api.oxforddictionaries.com/api/v1/entries/es/' + change.after.data().word + '/translations%3Den', // translations=es
            headers: {
                "Accept": "application/json",
                'app_id': 'groucho',
                'app_key': 'swordfish'
            },
            json: true
        };
        let translationsArray = [];
        return rp(options)
            .then(function (wordObject) {
                wordObject.results.forEach(function (result) {
                    result.lexicalEntries.forEach(function (lexicalEntry) {
                        lexicalEntry.entries.forEach(function (entry) {
                            entry.senses.forEach(function (sense) {
                                if (sense.translations) {
                                    sense.translations.forEach(function (translation) {
                                        translationsArray.push(translation.text);
                                    });
                                } // close if
                                else {
                                    if (sense.subsenses) {
                                        sense.subsenses.forEach(function (subsense) {
                                            if (subsense.translations) {
                                                subsense.translations.forEach(function (translation) {
                                                    translationsArray.push(translation.text);
                                                }); // close forEach
                                            } // close if
                                            else {
                                                // console.log("No Translation");
                                            } // close else
                                        }); // close forEach
                                    } // close if
                                } // close else
                            }); // close forEach
                        }); // close forEach
                    }); // close forEach
                }); // close forEach
                translationsArray = [...new Set(translationsArray)]; // removes duplicates
                return admin.firestore().collection('Dictionaries').doc('Spanish').collection('Words').doc(word).collection('Translations').doc('English').set({
                    translationsArray: translationsArray,
                    source: 'OED',
                    dateAdded: Date.now(), // timestamp
                    longLanguage: 'English',
                    shortLanguage: 'en',
                    word: word
                })
                    .then(function () {
                        // console.log("Document written.");
                    })
                    .catch(function (error) {
                        console.log("Error writing document: ", error);
                    })
            })
            .then(function () {
                // console.log("Document written for Oxford_EStranslateEN.");
            })
            .catch(function (error) {
                console.log("error: " + error);
            });
    } // close if
    // return 0;
});

One difference is that I call the OED via an HTTP request, using rp (request-promise). I call return rp(options). This clearly returns a promise, and the promise is explicitly returned. The problem seems to be that in the Google version I'm not returning the promise when I call Google Translate, and IBM Watson returns a callback, not a promise, and I don't return that.

Upvotes: 3

Views: 10720

Answers (2)

Thomas David Kehoe
Thomas David Kehoe

Reputation: 10930

I got it working:

exports.Google_EStranslateEN = functions.firestore.document('Users/{userID}/Spanish/Translation_Request_Google').onUpdate((change, context) => {
  if (change.after.data().word != undefined) {
    // copied from https://cloud.google.com/translate/docs/quickstart-client-libraries
    // Imports the Google Cloud client library
    const {Translate} = require('@google-cloud/translate');
    // Your Google Cloud Platform project ID
    const projectId = 'myProject-cd99d';
    // Instantiates a client
    const translate = new Translate({
      projectId: projectId,
    });
    const word = change.after.data().word; // The word to translate
    const options = {
      from: 'es', // the source language
      to: 'en', // the target language
      format: 'text' // HTML vs. plain text
    };
    let translationArray = [];  // clear translation array
    return translate.translate(word, options)  // this return is critical
    .then(function(results) {
      let translation = results[0];
      translationArray.push(translation);
      admin.firestore().collection('Dictionaries').doc('Spanish').collection('Words').doc(word).collection('Translations').doc('English').set({
        translationArray: translationArray,
        language: 'en',
        longLanguage: 'English'
      });
    })
    .then(function() {
      console.log("Write succeeded!");
    })
    .catch(function(error) {
      console.error(error);
    });
  } // close if
});

The key was to return the translate function:

    return translate.translate(word, options)

The cloud function also runs faster.

Doug Stevenson answered my previous question:

You're not returning a promise that's resolved when all the async work is complete. If you don't do that, Cloud Functions assumes that all your work is complete, and will clamp down on all resources, and any pending work will be shut down.

The promise returned by translate.translate().then().catch() is being ignored. Your nested call to admin.firestore()...set() has a similar problem. It's not sufficient to just call then() and catch() on every promise because then() and catch() both return yet another promise.

It seems like the phrase "return a promise" is being used in two ways here. Saying that translate.translate() returns a promise is different from saying that I need to return a promise by coding return translate.translate(). Perhaps it would have been clearer if Doug had said to "return the function" instead of "return a promise"?

I'm also not clear on whether I need to return this function:

return admin.firestore()...set({

It works the same with or without the return.

Upvotes: 0

Ronnie Smith
Ronnie Smith

Reputation: 18565

The Firebase JavaScript (Web) SDK .set method returns a promise, i.e., the promise is already there. See Set A Document online documentation.

// Add a new document in collection "cities"
db.collection("cities").doc("LA").set({
    name: "Los Angeles",
    state: "CA",
    country: "USA"
})
.then(function() {
    console.log("Document successfully written!");
})
.catch(function(error) {
    console.error("Error writing document: ", error);
});

You might find async/await easier to use (more intuitive). In that scenario, you would do something like:

async function writeDocument(obj){
  var writeOperation = await db.collection("cities").doc("LA").set(obj);
  //now this code is reached after that async write
}

Upvotes: 3

Related Questions