[Tech] How I build a system for Friday Lunch — Lunchie

Overview

High-level project overview

Firestore

Why?

I realised that my project might not be the best example of using a NoSQL database because of its small scale but since Firestore is quite new to me, I decided to give it a try and learn something new on the way.

What I have learned

It’s schemaless and horizontally scalable 😎: Schemaless means I can always change the rule of the database by adding/removing fields and it will not affect the exisiting objects. Each object under the same collection can have slightly different fields. The impact of this is that developer needs to “code defensively”. The horizontal scalability enables the database to scale easily within seconds on cloud without hassle.

// Query path
db.collection(“...”).document("...").collection("...").document("...")
// but I prefer to use this shortcut format
db.document("collectionPath/documentPath/collectionPath/documentPath")
/**
* Attaches a listener for QuerySnapshot events.
* @param listener The listener to attach.
* @return A FIRListenerRegistration that can be used to remove this listener.
*/
// example use:
document.reference.collection("employees").addSnapshotListener{ (snapshot, error) in
// whenever there's changes on Firestore on Employees,
// it notifies
.....
}

How to install

The setup was pretty straightforward to me and their explanation videos on Youtube were amazing.

Version 1

Why?

Simple: I just need to create a date and it contains the food options and employees who made the orders.

Limitation

Extensibility: Hard to extend functionality of Employee. If I want to analyse the behaviour of the employee(predict his/her next food choices), I will need to query many times on different dates to get all the information needed. Same goes with Food as well.

Version 2 (Pending)

Why?

Employees collection has all employees options history. I wanted to collect employees data in a long run so that I can analyse their food options. For food collection, it stores food properties (isVege, isHalal, isVegan). Order collection is similar to our first version but it will only store the snippet of food and employee data as this is to enable the mobile app can retrieve all the data at one go.

// When there's a new entry in Employees collection, I will sync it up to my Lunch/Orders collectionexport const onFoodOrderCreate = functions.database
.ref('employees/{employeeId}')
.onCreate(async (snapshot, context) => {
<Sync Order collection here>
.....
})
// same goes for onUpdate

Google App Script

Firestore Integration

In the Google online script editor, select the Resources menu item and choose Libraries.... In the Add a library input box, enter 1VUSl4b1r1eoNcRWotZM3e87ygkxvXltOgyDZhixqncz9lQ3MjfT1iKFw and click Add. Choose the most recent version.
(Thanks Grahamearley, nice profile picture btw)

Creating a service account

The details of setting up service account can be seen in Grahamearley’s library README or you can also do it via command line

function Firestore() {  
var key = "-----BEGIN PRIVATE KEY-----\n<your service account private key>\n-----END PRIVATE KEY-----\n";

var email = "<your project>.iam.gserviceaccount.com";
var projectID = "<project ID in Firebase>";
var firestore = FirestoreApp.getFirestore(email, key, projectID);
return firestore;
}

Some useful functions

Google App Script is quite expressive and I just need to go through their documentation whenever I need to do certain functionalities for Spreadsheet implementation.

// CLASSIC GETTING CURRENT ACTIVE SPREADSHEET
var spreadsheet = SpreadsheetApp.getActive();
// ADD MENU ON SPREADSHEET TOOLBAR
var menuItems = [
{name: '1. Create New Sheet', functionName: 'showPrompt'},
{name: '2. Update current Lunch', functionName: 'updateLunch'},
{name: 'Reset Sheet', functionName: 'resetSheet'}
];
spreadsheet.addMenu('Lunchie', menuItems);
// GET SHEET WITH NAME <sheetName>
var newSheet = spreadsheet.getSheetByName(<sheetName>);
// PRE-POPULATE COLUMNS WITH STYLING ON SPECIFIC POSITION
var headers = ['Food', 'Who ordered it']; newSheet.getRange('A1:B1').setValues([headers])
.setFontWeight('bold').setFontFamily('Poppins')
.setFrozenRows(1);
// CLEAR ALL CONTENTS
spreadsheet.clearContents();
// A custom function that delete EMPLOYEES and FOOD collections's documentsfunction deleteLunch(){  
var sheet = SpreadsheetApp.getActiveSheet();
var sheetName = sheet.getName();
var foodPath = BaseURL() + sheetName + "/food/";
var employeePath = BaseURL() + sheetName + "/employees/";

// Let's clear employees docs
var employeeDocs = Firestore().getDocuments(employeePath);
for (var i = 0; i < employeeDocs.length; i++) {
var path = employeeDocs[i].fields["path"];
Firestore().deleteDocument(path);
}
// Let's clear food docs
var foodDocs = Firestore().getDocuments(foodPath);
for (var i = 0; i < foodDocs.length; i++) {
var path = foodDocs[i].fields["path"];
Firestore().deleteDocument(path);
}
}

How to debug

To view your logs, go to your script editor and selectView > Logs(⌘+Enter).

You can run specific function instantly to debug
Logger.log(data)

Version 1 Demo

Version 1: (Left) Google spreadsheet which will be used by our office manager. (Right) Spreadsheet creates Fields in Firestore which will reflect on iOS app.

Limitation

Limited changes from Spreadsheet: As you can see from the high-level architecture above, the spreadsheet notifies Firestore on changes but not the other way. For example, my office manager cannot edit food name(food name is being used as ID) and can only rely for engineers to go on Firebase console.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Amy Cheong

Amy Cheong

Current: Product Manager at Workmate • Always Software Engineer.