DevOps Blog - Nicolas Paris

Setup Firestore and Cloud Function

GCP

The application

The idea of the application is very simple. launch the application twice (it's now offline, I did setup a website for testing purpose that i turned down), click on the big rectangle, points will be shown on both screen. Points will be store on the Firestore database, and will be send back in realtime in online browsers.

The setup summary

A quick overview of technology we will playing with in this post.

Setup firebase the project

Firestore from the Google Cloud Platform is import and synchronized within a project in the Firebase platform. There are the same. Cloud functions as well. We will use the firebase tools to deploy functions and host the website. You can quickly add a Google Cloud platform project as seen in the following screenshot.

First, we need a firebase project, or just import a Google Cloud Platform project.

Let's register a new app, and setup the Firebase hosting in the same time.

Create a quick vue bootstrap with vue-cli.

vue create tracker

You need to install the firebase tools.

npm install -g firebase-tools

We initialize the project source code as seen in the 4th step.

firebase login
firebase init

Select hosting and functions, and database if not already running.

Leave options as default values by pressing enter except for the the public directory that would be dist.

Here my example of the firebase.json.

{
  "hosting": {
    "public": "dist",
    "site": "niptracker",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  }
}

Let's deploy the application, first build it and deploy.

npm run build
firebase deploy

This will deploy the website and cloud functions as well once we will setup some.

At this point, you should have a default vue home page one your web.app hosting like this.

Get Firestore credentials

On your firebase console, go to projects setting and get configuration code.

While we are here, setup the write read like this for development purpose.

We initialize a first collection users.

Manage some points on the database

Backend with cloud functions.

Let's setup some backend funcions that will update the database. This is on the functions folder, we will setup like the official documentation, and add firebase on the package.json file.

We create to functions, for initialize the points array, and the second one for inserting data.

const functions = require('firebase-functions');

const firebase = require("firebase");
// Required for side-effects
require("firebase/firestore");

const firebaseConfig = {
    apiKey: "AIzaSyBIjKVw0VqTQlsutpTyd-pwCAVAmw4zVRA",
    authDomain: "nipsandbox.firebaseapp.com",
    databaseURL: "https://nipsandbox.firebaseio.com",
    projectId: "nipsandbox",
    storageBucket: "nipsandbox.appspot.com",
    messagingSenderId: "200494519109",
    appId: "1:200494519109:web:4b34dcdc4bd8d6ec"
};

// Initialize Firebase
firebase.initializeApp(firebaseConfig);

var db = firebase.firestore();

/**
 * INIT USER
 */
exports.initUser = functions.https.onRequest((request, response) => {
    db.collection("users").doc(request.body.color).set(
        {coords: []}
    ).then(function() {
        response.send("Document successfully written!");
    })
    .catch(function(error) {
        response.send(error);
    });
});

/**
 * ADD TICK
 */
exports.addTicks = functions.https.onRequest((request, response) => {
    db.collection("users").doc(request.body.color).update(
        {coords: firebase.firestore.FieldValue.arrayUnion(request.body.coords)}
    ).then(function() {
        response.send("Document successfully written!");
    })
    .catch(function(error) {
        response.send(error);
    });
});

Test the backend

We could deploy, but let's just check if we are good to go. On the package.json inside the functions folder, there is a npm run mode to launch a local server for testing purpose.

"scripts": {
    "serve": "firebase serve --only functions",
    "shell": "firebase functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  },

Let's run num

npm install --save firebase
npm run serve

This will give you something to test with.

Let's initialize and add some tick to the database, with postman, httpie, insomnia or whatever client you have.

Initialize the user.

Add Tick to user.

We can deploy functions and tests them like before in real production environment.

firebase deploy

You can see them on firebase console.

Frontend

We will keep the HelloWorld component from the vuejs setup, take everything off, for a cleanup. We keep the template part just with a canvas.

<template>
  <div>
    <canvas id="tracker" :style="{border: '5px solid ' + color}" width="800" height="400">
    </canvas>
  </div>
</template>

On the data part, we just initialize the color, we dont even keep ticks.

data() {
      return {
        color: '#' + (Math.random()*0xFFFFFF<<0).toString(16)
      }
    },

On the mounted, we setup the initialization of the user, and the click listener. we will handle the realtime as well

let data = {
        color: this.color
      };
      // initalization of the array
      axios.post(baseurl + 'initUser', data).then(r => console.log(r.data));

      let self = this;
      function getCursorPosition(canvas, event) {
        const rect = canvas.getBoundingClientRect();
        const x = event.clientX - rect.left;
        const y = event.clientY - rect.top;

        let data = {
          color: self.color,
          coords: {x, y}
        };
        console.log('', );
        axios.post(baseurl + 'addTicks', data).then(r => console.log(r.data));
      }
        // click listener
      const canvas = document.getElementById('tracker');
      canvas.addEventListener('mousedown', function(e) {
        getCursorPosition(canvas, e)
      });

Realtime callback

On the mounted we will redraw every ticks on the canvas.

// realtime callback
      db.collection("users")
        .onSnapshot(function(snapshot) {
          snapshot.docChanges().forEach(function(change) {
            if (change.type === "added") {
              console.log("New ticks: ", change.doc);
            }
            if (change.type === "modified") {
              var canvasSetup = document.getElementById("tracker");
              var ctx = canvasSetup.getContext("2d");
              change.doc.data().coords.forEach(item => {
                ctx.beginPath();
                ctx.rect(item.x-2, item.y-2, 4, 4);
                ctx.lineWidth = "1";
                ctx.strokeStyle = change.doc.id;
                ctx.stroke();
              });
            }
          });
        });

The complete vue component

Here the complete file from the HelloWorld component.

<template>
  <div>
    <canvas id="tracker" :style="{border: '5px solid ' + color}" width="800" height="400">
    </canvas>
  </div>
</template>

<script>
  import axios from 'axios';
  const baseurl = 'https://us-central1-nipsandbox.cloudfunctions.net/';

  const firebase = require("firebase");
  // Required for side-effects
  require("firebase/firestore");

  const firebaseConfig = {
    apiKey: "AIzaSyBIjKVw0VqTQlsutpTyd-pwCAVAmw4zVRA",
    authDomain: "nipsandbox.firebaseapp.com",
    databaseURL: "https://nipsandbox.firebaseio.com",
    projectId: "nipsandbox",
    storageBucket: "nipsandbox.appspot.com",
    messagingSenderId: "200494519109",
    appId: "1:200494519109:web:4b34dcdc4bd8d6ec"
  };

  // Initialize Firebase
  firebase.initializeApp(firebaseConfig);

  var db = firebase.firestore();

  export default {
    name: 'HelloWorld',
    data() {
      return {
        color: '#' + (Math.random()*0xFFFFFF<<0).toString(16)
      }
    },

    mounted() {
      let data = {
        color: this.color
      };
      // initalization of the array
      axios.post(baseurl + 'initUser', data).then(r => console.log(r.data));

      let self = this;
      function getCursorPosition(canvas, event) {
        const rect = canvas.getBoundingClientRect();
        const x = event.clientX - rect.left;
        const y = event.clientY - rect.top;

        let data = {
          color: self.color,
          coords: {x, y}
        };
        axios.post(baseurl + 'addTicks', data).then(r => console.log(r.data));
      }
        // click listener
      const canvas = document.getElementById('tracker');
      canvas.addEventListener('mousedown', function(e) {
        getCursorPosition(canvas, e)
      });

      // realtime callback
      db.collection("users")
        .onSnapshot(function(snapshot) {
          snapshot.docChanges().forEach(function(change) {
            if (change.type === "added") {
              console.log("New ticks: ", change.doc);
            }
            if (change.type === "modified") {
              var canvasSetup = document.getElementById("tracker");
              var ctx = canvasSetup.getContext("2d");
              change.doc.data().coords.forEach(item => {
                ctx.beginPath();
                ctx.rect(item.x-2, item.y-2, 4, 4);
                ctx.lineWidth = "1";
                ctx.strokeStyle = change.doc.id;
                ctx.stroke();
              });
            }
          });
        });
    }
  }
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
  #tracker {
    margin:auto;
  }
</style>

Make sur you have axios, core and firebase installed.

npm install --save axio
npm install --save firebase

CORS restrictions on cloud functions

Let's test our code, nothing happen it seems we have some CORS problems, let's solve it. The error is very explicit, it's a well documented problem.

Warning about the CORS error as it can show this error for different problem. Error 500 will send back CORS error with others. Even a wrong spell function, it doesn't return a 404 but a CORS error. It's ambiguous.

Access to XMLHttpRequest at 'https://us-central1-nipsandbox.cloudfunctions.net/addInit' from origin 'https://niptracker.web.app' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request.

Install cors as dependency with npm.

npm install --save cors

We update our code, wrap the code with the cors function.

const functions = require('firebase-functions');
const firebase = require("firebase");
// Required for side-effects
require("firebase/firestore");

const firebaseConfig = {
    apiKey: "AIzaSyBIjKVw0VqTQlsutpTyd-pwCAVAmw4zVRA",
    authDomain: "nipsandbox.firebaseapp.com",
    databaseURL: "https://nipsandbox.firebaseio.com",
    projectId: "nipsandbox",
    storageBucket: "nipsandbox.appspot.com",
    messagingSenderId: "200494519109",
    appId: "1:200494519109:web:4b34dcdc4bd8d6ec"
};

// Initialize Firebase
firebase.initializeApp(firebaseConfig);

var db = firebase.firestore();
const cors = require('cors')({origin: true});
/**
 * INIT USER
 */
exports.initUser = functions.https.onRequest((req, res) => {
    cors(req, res, () => {
        db.collection("users").doc(req.body.color).set(
            {coords: []}
        ).then(function() {
            res.send("ok")
        })
        .catch(function(error) {
            res.send(error);
        });
    });
});

/**
 * ADD TICK
 */
exports.addTicks = functions.https.onRequest((req, res) => {
    cors(req, res, () => {
        db.collection("users").doc(req.body.color).update(
            {coords: firebase.firestore.FieldValue.arrayUnion(req.body.coords)}
        ).then(function() {
            res.set({ 'Access-Control-Allow-Origin': '*' }).sendStatus(200)
        })
        .catch(function(error) {
            res.send(error);
        });
    });
});

And deploy the functions

firebase deploy --only functions

Test your application

As describe on the introduction, just launch two google chrome, and click on the rectangle. you should see them on both screen.

Conclusion

This is a quick project, the main goals was to test the realtime of the firestore of google cloud platform (or firebase, as it's the same).

It's easy to setup and gives good results, I'll will definitely use for some projects, personal use for sure, maybe even for professional use.