Blog Barista: Michael O’Keefe | Feb 14, 2019 | Web Development | Brew time: 11 min
Welcome to Part 5 of my Firebase series, you’re getting close to the end! At this point, we have a public facing application leveraging HTTPS and Google Clouds extensive CDN. In this part, we will be focusing on using Cloud Functions so that our application can react to different event triggers asynchronously without putting any extra load on the application client-side.
In Part 4, we allowed public users to access our app using Google Cloud’s static hosting and CDN. We were also assigned a free public domain, all for absolutely free.
If you are just joining us, go ahead and clone this repository from GitHub and checkout the cloud-functions branch. You will have to follow Part 1 to get your environment set up and connect to Firebase using your credentials, but the code will be at the same place where we left off in Part 3. If you have already completed the previous parts, there is no need to checkout any other branch.
Introduction to Cloud Functions
With Cloud Functions, you have the backing of one of the largest cloud platforms. Google is constantly adding more power to its arsenal and you only pay (very reasonably) for what you use. Depending on your application, you may never need anything past Google’s free tier. Since cloud functions are stored in the cloud, you also don’t have to deal with configurations or maintenance. The only thing you have to do is write and deploy your code and watch Cloud Functions handle everything else for you.
Today, you will be focusing on three different triggers for Cloud Functions: authentication events, Firestore events and direct https calls. In Part 6, I will cover functions triggered by Cloud Storage events.
Let’s begin with Authentication event triggers. We are going to write a function that runs when a user logs in for the first time. We will save some of the user’s data to a document in Firestore and add some other information that we can use later for security. Then, if the user has an email address, we will send them a welcome email.
Auth Function Code
In this file we are exporting a function newUser via an ES6 module. In the functions/index.js file we import the auth-functions.js module and register a Cloud Function newUser by exporting it from this file. This function will be triggered by auth.user().onCreate event provided by the Firebase functions SDK. We pass this function the newUser function that we exported from auth-functions.js as a callback.
In the newUser function, we will first create a user object that will house all the information we want to store for our new user in Firestore. We will then use the Firebase Admin SDK to add the user to the users collection using the UID as the document ID. This should look fairly similar to what we are used to! We have added console logs to make debugging easier as we are not able to access this code while it is running. All Cloud Functions need to return a promise, and every then block of a promise needs to return something to keep the functions virtual instance from terminating before an asynchronous event has been completed.
Note that since we are using the Admin SDK we are not constrained by the Firestore rules. This should only be used in Cloud Functions where your code is isolated and not vulnerable to misuse or abuse.
Welcome Email Configuration
When the user has been successfully saved to Firestore, we will send a welcome email to the new user. The sendWelcomeEmail function has been mostly completed for you as it is not specific to Firebase. There is one thing that we need to supply in that function, and that is an email and password to an account that will be used to send the email via nodemailer. We could hard-code these in, but this is not a good practice. A more secure way would be to add these to a global config object that exists within the scope of our functions at runtime. For this we can turn to the Firebase CLI. Run the following commands in your project’s root directory:
When we deploy our functions below this config object will be deployed as well and will be accessible within the function runtime.
The config object can be accessed in our functions via functions.config(). Add the following code to the sendWelcomeEmail function to get access to the config values we will be deploying via the CLI.
Now, we can run firebase deploy—only functions
In the result you will notice a region next to each function, in the screenshot above, the region is us-central1. This will default to your projects region but can be set manually by adding .region(‘region’) after functions when setting up a function. See the function location documentation for detailed examples.
Viewing Functions in the Firebase Console
In the Firebase project console, select Functions from the side menu and you should see the functions we just deployed. You will see more than just the newUser function. Ignore those for now, we will be defining those later. You will notice that the trigger column for our newUser function indicates user.create. This event will fire exactly once when a new user is created through Firebase Authentication. To test this out, go to your newly hosted app and sign up with an email and password or Gmail account that you have not already used to sign in with. If you do not have one, you can go to the Authentication > Users section in the project console and just delete your existing user record, then sign up/in again. Note that functions are attached to your app on a session basis. In order for your app to connect to new or updated functions you will have to refresh the client.
After creating a new user, you should soon get a welcome email! Back in the project console there are also new function logs from this function being executed. If you did not get an email, there will most likely be some error here. Make sure that the user you logged in with did not already exist in your project, and that the email and password you added to the function’s configuration were accurate for a real email address (I created a new test email through Gmail to test this out).
Firestore EventsNext we will focus on functions that react to Firestore events. There are 6 functions stubbed out in functions/firestore-functions.js. Four of these functions are exported and will be run in response to different trigger events that are defined in functions/index.js.
Data Aggregation and ManipulationupdateBreweryReview is imported in index.js and used as the callback for the onUpdate event of each document in the reviews collection of each brewery. This function will be called whenever a review document is updated under any brewery. Similarly, there is an updateUserReview function that is assigned to react to updates on review documents under any user document (including when they are created). Currently we do not have any reviews on any user documents, but now that we implemented the user onCreate function, we will have user documents! These functions will be used for two purposes:
1. To aggregate data contained by all the reviews that a brewery owns
2. To sync the updated review with the corresponding review under the opposite collection. For example, If I update a review under a brewery, my updateBreweryReview function will in turn update the corresponding review that lives under my user document and vice versa.Why do these things? Data aggregation is very common in cloud functions. To free up our client application from doing tedious, synchronous calculations, we can aggregate our data quickly in a function and then update some document(s) in Firestore. Those document updates will be caught immediately by the application as a new stream of data. This allows the user to have a more seamless experience and makes our client side more simplistic and event-driven. Also, since collections under documents need to be queried separately, we can now obtain information about our sub collections without having to query them. In this example, whenever a review is updated, we will query the reviews that belong to the brewery and calculate the average rating across them all. This will be saved on the brewery document, which will then show up in the app for all users who are connected without having to put any more load on their browsers. Data duplication can also be a common practice. In this example we will be duplicating review documents between the brewery they belong to and the user that created them. This way, we can easily query and manage the reviews a user made, and the reviews that were made for a brewery without having to make complex queries, and without having to query more data than we need. The only thing that we need to do to make sure this works seamlessly, is to ensure that when an update is made in one collection, it is synced with the corresponding document on the other. Since functions are quick, cheap, scalable and can be run in reaction to document events in Firestore, this is a breeze to accomplish.
The updateBreweryReview Function
The function is very straightforward. Update events from Firestore pass a change object which contains the state of the object before and after the event. As seen before, a context object is also passed from which we can obtain the ids that were in curly braces when defining the document path for the update trigger.
Using the UID from the review (the creator), we obtain a reference to the Userdocument. We then pass the user document reference, new value, previous value and reviewId to the updateReview function.
The updateReview Function
The updateReview function will then check if there was any meaningful change in the review state, and if there was, update the corresponding review document under the document reference that was passed in (in this case the user). If no meaningful change was made, we can terminate the function. Also, if the rating specifically was changed as part of the update, we will then pass the breweryId associated with this review to the aggregateRatings function.
The aggregateRatings Function
The aggregateRatings function will get a reference to the brewery document associated with the given ID, then will query the reviews collection under that document. With these reviews, we will calculate the average rating and update the brewery.
The updateUserReview FunctionAt this point it is trivial to implement the updateUserReview function. We do the exact same thing as updateBreweryReview, but instead of passing a reference to the brewery document we will pass a reference to the user document since this function will be called in response to an update on a review under a brewery.
The deleteBreweryReview FunctionNow our reviews are synced between breweries and users for creating and updating, but what about deleting? It would be quite confusing if a user deleted a review from their list only to find it still out for the public to see! deleteBreweryReview will work very similarly to the update functions we just implemented with some small tweaks. The delete event does not get a change object, but rather snapshot. Instead of updating the corresponding user review we will delete it. We can’t forget to aggregate ratings once the review has been deleted.
The deleteUserReview Function
The deleteUserReview function will work a little differently. Recall that for each review that a user makes on a brewery, a reviewMapping is created so that we can easily track whether they have already reviewed that brewery or not. If a user deletes the review they made, we should also delete the mapping so they can make a new review if desired. Since we do not want to only delete one document, we will use a Firestore batch transaction. Batches are beneficial because they are atomic. If one of the actions carried out in a batch fails, none of them will be attempted. This way we can avoid our data falling into a problematic state. We only need to implement deleting the mapping in one of the delete trigger functions because no matter which review document is deleted, this trigger will be called.
Last but not least we will cover a very basic example of an HTTP triggered function. This is a function that will be set up as an endpoint on a NodeJS Express server that will only be running when the function is called. As with all other Cloud Functions, it will scale with the request load.
Incrementing Brewery Views
In functions/http-functions.js we will set up and export an Express app that allows CORS and will expose a post endpoint at /brewery-viewed. When we go to deploy our functions, the CLI will spit out a URI that we will use in our app (or anywhere else you want!) to hit this endpoint.
The function will be simple and should look familiar if you have used NodeJS with Express before. We will expect the request body to contain a breweryId which we will use to fetch the associated document. We will then add one to the number of views this brewery has, update the brewery, and send a success response back with the new number of views.
Run firebase deploy –only functions and go the Functions section of your project console. Copy the URI in the Trigger column of the httpTriggers row, it should look something like https://us-central1-your-project.cloudfunctions.net/httpTriggers. Adding the endpoint we defined: /brewery-viewed, will give the correct URI needed to post to this function.
Now, in src/app/brewery/brewery.component.ts we can replace the first argument of the http.post method called inside the postView function with our URI and add the breweryId to the request body in the second argument.
Finished! The app now has a robust suite of Cloud Functions that handle some of the maintenance and calculation heavy work that keeps your UI lean and user friendly. Not too shabby huh? Great work!
In the final part of this series (Part 6), we will introduce Cloud Storage. We will also learn to add a new function that will integrate with the Google Vision API to moderate and flag users that upload explicit content, and update your security rules to lock down these mischievous users.
Other recent posts:
The Pros and Cons of On-Site Consulting
Blog Barista: Dana Graham | Mar 13, 2019 | Project Management | Brew time: 5 min
Most consulting practices have their staff work at the client’s site, which allows for close interaction and improved services among consultants. But that means, as a consultant, you don’t always have the same routine or a standard…
Blog Barista: Greg Antrim | March 6, 2019 | Internet of Things | Brew time: 12 min
Welcome to Part 2 of the Command Your Arduino from Anywhere. In case you missed it, we covered port forwarding in Part 1. While that method was easy to implement, it had a few limitations. Today’s post offers an alternative method…