Adding Firebase functions to our internal toolset

by | Mar 26, 2020 | Tech

It’s been coming for a while. We have a system hosted on Firebase, which we use for managing our timesheets and various related bits of information. The system grew from the need for pulling all sorts of information together and ended up being immensely useful. We have been wanting to integrate our holiday booking functionality with slack for a while. We never go around do it. The requirement was simple, whenever someone books a holiday a message pops up on slack about it. I had a bit of time last week and decided to give it a go.

slack notification
Image by Csaba Berenyi

The system is written in Angular and is hosted on Firebase. If you are not familiar with Firebase it’s a backend as a service offering from Google. It is relatively simple to use, comes with a great Javascript SDK that can be added to Angular applications quickly.

The holiday system

Up to this point, we used the Auth, Firestore and Hosting services. Looking at the requirements I needed something that could trigger a function based on a data write to a specific table. In traditional services, this could be done by using a database trigger or by adding a hook to the server after it wrote the data into the database. In our case, Firebase offers Functions that can be hooked into various parts of the system and react to those. For example when data gets written to a data collection.

In our system holidays are stored in a collection. They are simple records of start-end dates and some supporting fields.

Firestore Data Structure
Image by Csaba Berenyi

What we needed is a way to hook into the write function.

Adding functions

First thing I needed to do was to add firebase functions support to the project. Using the Firebase CLI it was simply running firebase init I wasn’t sure how to do this as we had Firebase initialised for our project already. Luckily it recognised that and allowed me to tick the Functions option. After selecting the options I needed it created a folder called functions, added a sample project, updated the firebase.json file to include this new feature.

Firebase init
Image by Csaba Berenyi

Functions are server-side code that can react to a number of triggers, for example, HTTPS requests or database actions such as write. The default script provides simple HTTPS request handler, a hello world if you like. To test simply publish the function using the CLI. firebase deploy --only functions if you have more than one project (like I did) use the --project option to specify the project (should match the value in your .firebaserc file).

Implementing functions

The requirement is to write the cloud functions and export them from the main file. In our case, the code was in TypeScript so it is compiled before it can be used. The code below shows how firebase is initialised, and how to get the new record in the cloud function that gets triggered when a record gets added to the holidays collection.

import * as admin from 'firebase-admin'; import * as functions from 'firebase-functions'; // Kick firestore into action. Needed to perform queries in the functions. admin.initializeApp(); const db = admin.firestore(); /** Holiday definition */ interface IHolidayData { status: string; start: admin.firestore.Timestamp; end: admin.firestore.Timestamp; note: string; user: string; } // React to adding new records in the holidays collection. export const holidayNotification = functions.firestore .document('holidays/{holidayId}') .onCreate((change, context) => { const holidayData: IHolidayData = change.data() as IHolidayData; if (!holidayData) { return; } // Use the new record to do stuff... });
Code language: TypeScript (typescript)

At this point, I needed to have the ability to somehow forward this to slack. Luckily it is possible to add our own apps to it. Simply go to the Slack API site, log in and add an app. I did the same, configured some of its parameters and specified that my app would be posting via webhooks.

Slack app
Image by Csaba Berenyi

Webhook can post to a single channel so I created two. One for development (it goes to an obscure dev channel we have) and one for the actual notifications channel we use. Posting to slack from this point is simply making a post request to the webhook.

Slack webhooks
Image by Csaba Berenyi

Once I had Slack configured and verified the webhook using curl, I moved onto adding this to the script. To make requests simple I added axios as a dependency to the project and made a simple post request to verify it worked.

import axios from 'axios'; ... export const holidayNotification = functions.firestore .document('holidays/{holidayId}') .onCreate((change, context) => { const holidayData: IHolidayData = change.data() as IHolidayData; if (!holidayData) { return; } const holidayNotificationUrl = 'https://...'; const api = axios.create(); const message = `Holiday booking test ${holidayData.user}`; return api.post(holidayNotificationUrl, message); });
Code language: TypeScript (typescript)

This worked fine. I needed to add two more things to the script. First is to look up the user based on its ID in the users collection and configure the URL based on the environment the code runs in.

Looking up users

To look up users first the script needs to initialise the database (which it did as shown in the code snippet above) then simply performing a query on the users collection to find the user account based on its ID. I chose to use promises so the code can be chained nicely.

import axios from 'axios'; import * as admin from 'firebase-admin'; import * as functions from 'firebase-functions'; // Kick firestore into action. Needed to perform queries in the functions. admin.initializeApp(); const db = admin.firestore(); ... export const holidayNotification = functions.firestore .document('holidays/{holidayId}') .onCreate((change, context) => { const holidayData: IHolidayData = change.data() as IHolidayData; if (!holidayData) { return; } const holidayNotificationUrl = 'https://...'; const api = axios.create(); return db.collection('users') .doc(holidayData.user) .get() .then((document) => { let userName = holidayData.user; let profileImageUrl: string = ''; if (document.exists) { const user = document.data() as IUser; userName = user.name; profileImageUrl = user.photoUrl; } const message = composeSlackMessage(userName, profileImageUrl, formattedStart, formattedEnd); if (!message) { throw new Error('Unable to send message.'); } return api.post(holidayNotificationUrl, message); }).then(() => { console.log(`Post to slack succeeded.`); }) .catch(err => { console.error(`Unable to post to slack. Error: ${JSON.stringify(err)}`); }); });
Code language: JavaScript (javascript)

Please note the console.log and console.error messages are redirected to the Firebase logs so they can be inspected via the web UI.

Firebase functions log
Image by Csaba Berenyi

Configure slack URLs

Firebase functions come with the ability to store per-project configuration. You can set them using the firebase CLI and query them using the script.

Setting a value is done by the functions:config:set command. If you have more than one project don’t forget the --project option. firebase functions:config:set slack.holidaynotificationurl="WEBHOOK_URL" --project YOUR_ID. I set this up for both of the projects. Then I looked into how to code it.

I did not need complicated coping mechanisms for handling missing configs so I went for the following solution. (The code below sets the holidayNotificationUrl value that is used in the code example above).

... const config = functions.config(); const holidayNotificationUrl = config.slack.holidaynotificationurl ? config.slack.holidaynotificationurl : null; if (!holidayNotificationUrl) { console.error('Unable to find slack.holidaynotificationurl config setting. Holiday notifications will be disabled.'); } ...
Code language: TypeScript (typescript)

Please note: At the time of writing, testing cloud functions that use the config locally seems to be somewhat broken. I am sure they will be fixed soon.

Formatting slack messages

Up to this point, I only sent a single text element to slack. This method supports extremely limited formatting options using a markdown syntax of sorts, but Slack can do much better. You can specify blocks for the messages that allow placement and other options. Using Slack’s block builder helped to come up with the layout I needed. I added the code it produced to a function that can fall back to text messages if for any reason the user does not have a profile image.

/** * Generates a slack message from the specified inputs. * It either creates a nice profile picture thing or * just a simple update text. * See https://api.slack.com/block-kit/building */ function composeSlackMessage(userName: string, profileImageUrl: string, formattedStart: string, formattedEnd: string): any { let message = null; if (!profileImageUrl) { message = { blocks: [{ type: 'section', text: { type: 'mrkdwn', text: `*${userName}* will be on holiday between *${formattedStart}* => *${formattedEnd}*` } }] }; } else { message = { blocks: [ { type: 'section', block_id: 'section567', text: { type: 'mrkdwn', text: `*${userName}* will be on holiday.\n*Start*: ${formattedStart}\n*End*: ${formattedEnd}` }, accessory: { type: 'image', image_url: profileImageUrl, alt_text: userName } }] }; } return JSON.stringify(message); }
Code language: JavaScript (javascript)

What remained was for me to update our Bitbucket pipeline scripts to lint, build and publish functions and now we have holiday notifications in Slack.

Summary

Firebase functions are a great tool to react to database changes. They are simple to implement and allow interaction with other systems. I thoroughly enjoyed implementing this bit of functionality and already started planning what else we’ll add to our Slack.

Top tips

  • To make an HTTPS request (or any connection to be honest) a non-Google service you need to upgrade your project to a paid tier.
  • If you have multiple projects use the --project option in the CLI. Check .firebaserc for possible values
  • You can run firebase init on a project that has some of the Firebase services initialised
  • When you publish to firebase on your CI service using the --non-interactive flag you cannot delete functions. Those need removing manually using the CLI.

If you have any comments, ideas or personal experiences, feel free to share with us. We are curious to hear new ideas, solutions, and perspectives regarding the above. Don’t forget to follow us on our social media!

You can also find the rest of our blog here!

Related posts