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.

Brief Review

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

What are they? Well, they are functions written by you in either JavaScript or Typescript that are stored on a Google Cloud server and run in a NodeJS environment. These are incredibly useful as they give you the opportunity to write asynchronous “backend” logic that can be used to react to many different events tied to your project. These are very similar to the AWS Lambda service if you are familiar. As your function triggers scale, so do your resources. Google Cloud will spin up as many isolated instances of your function as are needed to handle the load. These resources will then be torn down when inactive to avoid charging you. Normally when an application sees an unexpected surge of new users, things slow down and there may be some downtime while the development team adds resources to handle the new load. This can result in downtime and decreased performance for a period of time. If you need to add cloud resources or extra hardware, it can also get expensive.

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.

Authentication 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 functions/auth-functions.js find the module.exports.newUser function. You may note that the syntax here is different than what we are used to so far. All functions will be written in JavaScript for a NodeJS environment.

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.

Auth-function.js

Index.js

The newUser callback function in auth-functions.js will be passed a Firebase User object, and an EventContext object that contains information about the event.

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 Events

Next 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 Manipulation

updateBreweryReview 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.

Updating Reviews

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 Function

At 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.

Deleting Reviews

The deleteBreweryReview Function

Now 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.

HTTP Functions

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.

Now, either start your development server via ng serve or build and deploy your app using the command: ng build && firebase deploy and visit any of the brewery pages. You should soon see the number of views tick up by one. Since your functions are not under heavy load it may take a moment for the first one to finish as the virtual instance will have to cold start. The more traffic these functions get, the more instances will be added to handle the load. Also, note that functions under the free tier may be a little slower than the paid tier.

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!

Next Time

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.

0 Comments

Other recent posts:

Team Building in a Remote Environment

Team Building in a Remote Environment

Blog Barista: Dana Graham | June 15th, 2022 | Culture | Brew time: 5 min
Let me start by saying I don’t care for the term “work family.” I have a family I love, and they have absolutely nothing to do with my career. I want my work life to be its own entity. I like boundaries (and the George Costanza Worlds Theory). Certainly, I want to enjoy and trust my coworkers, and I want to feel supported and cared for…

read more

Pin It on Pinterest