Frb is a SuperFetch plugin to easily access a Firebase Real time database. SuperFetch is a proxy for UrlFetchApp with additional features – see SuperFetch – a proxy enhancement to Apps Script UrlFetch for how it works and what it does.

This is another in my series on SuperFetch plugins.

Motivation

I use Firebase a fair bit, mainly from Node, but also occassionally from Apps Script. I like the idea of making everything relative to an object path – as you have with the Node Firebase client .ref() and a very simple API structure with just the things I need to use a lot.

Most SuperFetch plugin APIS have the concept of .ref() and support caching out of the box, so this Plugin will help to access Firebase REST API from Apps Script.

Firebase is pretty fast, so there’s not a huge speed benefit from caching, but if you’re on a pay as go plan, SuperFertch caching can reduce your Firebase costs.

As with all SuperFetch plugins, it is dependency free, minimal and implements only the methods that are in common use.

If you need additional methods exposed, or if you want to contribute one of your own (creating a plugin is quick and easy), ping me.

Script and Firebase Preparation

You’ll need the bmSuperFetch library and if you want to run some tests, the bmUnitTest library – details at the end of the article.

I use a regular cloud project for all of these plugins rather than the Apps Script managed one (you can change it in Project settings), and you’ll need at least these scopes enabled in your manifest file (appsscript.json)

"oauthScopes": [
  "https://www.googleapis.com/auth/script.external_request",
  "https://www.googleapis.com/auth/firebase.database"
]
scopes

You’ll also need to create your Firebase Realtime database in the firebase console if you don’t already have one. By default, the database name will be ‘yourprojectid-default-rtdb’.

I’ll give some pointers on Firebase security rules later – you can just leave it open access for now till we get going.

Instantiation

First you need a SuperFetch instance. If you don’t need caching, then just omit the cacheService property.

  const { Plugins, SuperFetch } = bmSuperFetch

  // use this basic setup for all tests
  const superFetch = new SuperFetch({
    fetcherApp: UrlFetchApp,
    tokenService: ScriptApp.getOAuthToken,
    cacheService: CacheService.getUserCache()
  })
superfetch instance

Frb instance

This will be the handle for accessing firebase

    
    const { Frb } = Plugins
	
    const frb = new Frb({
      superFetch,
      dbName: 'yourprojectid-default-rtdb',
      base: 'test,
      noCache: true
    })
frb instance

There are various other standard SuperFetch Plugin parameters that I’ll deal with later, but the ones shown are of special interest to this article.

dbName property (required)

The dbName will be ‘yourprojectid-default-rtdb’.

noCache property (optional)

In this instance, I’m turning caching off for now.

base property (optional)

You can ignore the base property if you want to be able to write to the top level of the database, but for testing, I recommend always adding ‘test’ as a base. This means that all object paths accessed by this instance will be relative to test/. This prevents overwriting any real data later if I do some testing.

Calling structure

Like most SuperFetch Plugins, Frb has the concept of ref and path to point to a particular place in the database hierarchy

Path

This defines the path relative to the base you want to operate on – so to access the object at ‘test/users/profiles’ where the frb base is ‘test’ the path object would be

    const frb = new Frb({
      superFetch,
      dbName: 'yourprojectid-default-rtdb',
      base: 'test'
    })

    const path = frb.path({path: 'users/profiles'})
path

Ref

Ref can be used to create a new Frb instance with a different base. So we could also access the object at ‘test/users/profiles’  using various combinations of path and ref – for example

// accessing test/users/profiles with a base of 'test'
// all these are requivalent
    const frb = new Frb({
      superFetch,
      dbName: 'yourprojectid-default-rtdb',
      base: 'test'
    })

    const path = frb.path({path: 'users/profiles'})
    const path = frb.ref().path({path: 'users/profiles'})
    const path = frb.ref('users').path({path: 'profiles'})
    const path = frb.ref('users/profiles').path()
combining ref and path

Methods

Database access methods in Frb (get, set and patch) are methods of the path object. Every method returns a standard SuperFetch response (see later)

get

Gets the JSON object at a given path

// returns a SuperFetch standard response
const result = frb.path({path: 'users/profile'}).get()

// or equivalent
const path = frb.path({path: 'users/profile'})
const result = path.get()
get an object at a given path

 

set

Sets the JSON object at a given path. This will overwrite everything from the path downwards – so children in the database, but not in the new data will be removed

// returns a SuperFetch standard response
const result = frb.path({path: 'users/profile'}).set({data})

// or equivalent
const path = frb.path({path: 'users/profile'})
const result = path.set({data})
set an object at a given path

patch

Sets the JSON object at a given path. This will only overwrite children if the are in the new data – those missing from the new data will not be overwritten – this operation is known as update in the firebase clients of some other platforms, but patch in the REST API.

// returns a SuperFetch standard response
const result = frb.path({path: 'users/profile'}).patch({data})

// or equivalent
const path = frb.path({path: 'users/profile'})
const result = path.patch({data})
set an object at a given path

path domain

The calling plan for the complete path domain looks like this

  /**
   * path domain
   * @param {object} params
   * @param {string} params.path the folder/file path
   * @return {object} containing the functions available in the path domain
  */
  path({ path = '' } = {}) {
    const self = this
    return {
      /**
       * get object
       * @param {...*} params any additional api params
       * @return {PackResponse} standard response with data from firebase in .data
       */
      get: (...params) => self.get({ path }, ...params),

      /**
       * set object
       * @param {object} p
       * @param {object} p.data the data to write
       * @param {...*} params any additional api params
       * @return {PackResponse} standard response with data from firebase in .data
       */
      set: ({ data }, ...params) => self.set({ path, data, method: "PUT" }, ...params),

      /**
       * patch object
       * @param {object} p
       * @param {object} p.data the data to patch
       * @param {...*} params any additional api params
       * @return {PackResponse} standard response with data from firebase in .data
       */
      patch: ({ data }, ...params) => self.set({ path, data, method: "PATCH" }, ...params)

    }
  }
path domain

SuperFetch reponse

The result of each of these operations is a SuperFetch response that looks like this.

/**
 * This is the response from the apply proxied function
 * @typedef PackResponse
 * @property {boolean} cached whether item was retrieved from cache
 * @property {object||null} data parsed data
 * @property {number} age age in ms of the cached data
 * @property {Blob|| null} blob the recreated blob if there was one
 * @property {boolean} parsed whether the data was parsed
 * @property {HttpResponse} response fetch response
 * @property {function} throw a function to throw on error
 * @property {Error || string || null} the error if there was one
 * @property {number} responseCode the http response code 
 */
result

You’ll mainly be interested in these properties

    // the parsed data - the contents of the requested path
	const data = result.data
	
	// whether there was an error
	if (result.error) { 
	 // handle error
	 console.log(result.error) 
	}

    // whether it came from cache
	const wasCached = result.cached
	
	// how many ms since it was written to cache
	const cacheAge = result.age
	
	// if you want it to throw an error if there was one add .throw() after result
	// if no error you'll get the result required
	// for example
	const data = result.throw().data
	
common result properties

For more info on this and other response properties see  SuperFetch – a proxy enhancement to Apps Script UrlFetch

Constructor parameters

I’ve mentioned the usual Frb constructor parameters earlier, but here’s a full list of those expected by the constructor.

/**
 * @typedef FrbApiOptions
 * @property {_SuperFetch} superFetch a superfetch instance
 * @property {boolean} noCache whether to cache
 * @property {string} dbName the firebase database name
 * @property {string} base the base path on the bucket (all paths will be relative to this)
 * @property {boolean} showUrl whether to showUrls when fetching
 * @property {object[]} extraParams always add these params
 */
Frb class constructor

Ref parameters

The ref method creates a new FRB instance, but with a new base. The FrbApiOptions argument is the same object as for the Frb constructor, but the base property is ignored.

const ref = frb.ref (newBase, frbApiOptions)
ref parameters

Firebase Security Rules

This is a little outside the scope of this article, but here’s  just a quick tip, as you’ll want to lock down your database.  Normally you’ll want to be setting specifc access rules for various paths in  your database, but one of the useful things I’ve found is to create the concept of admin access – ie. giving special access to particular users.

Admins Path

The first thing is to create an admins path. My database looks like this.

firebase rules

The admins path should contain the email address of those you need to have admin access. Note that the email address needs to be escaped. This is because Firebase can’t have property names with ‘.’ in them.

Admin Rules

In the rules section of the database add this to give full access to all those in the admins path.

{
  "rules": {
		".read": "root.child('admins').child(auth.token.email.replace('.','%2E')).exists()", 
		".write":"root.child('admins').child(auth.token.email.replace('.','%2E')).exists()",
  }
}
database rules

Unit testing

I’ll use Simple but powerful Apps Script Unit Test library to demonstrate calls and responses. It should be straightforward to see how this works and the responsese to expect from calls. These tests demonstrate each of the topics mentioned in this article and could serve as a useful crib sheet for the plugin


const testFrb = ({
  force = false,
  unit } = {}) => {

  // manage skipping individual tests
  const skipTest = {
    frb: false && !force,
    ref: false && !force,
    caching: false && !force
  }

  // get a testing instance (or use the one passed over)
  unit = unit || new bmUnitTester.Unit({ showErrorsOnly: true })

  // import required modules
  const { Plugins, SuperFetch } = bmSuperFetch
  const { Frb } = Plugins

  // use this basic setup for all tests
  const superFetch = new SuperFetch({
    fetcherApp: UrlFetchApp,
    tokenService: ScriptApp.getOAuthToken,
    cacheService: CacheService.getUserCache()
  })

  // test data
  const assets = {
    top: {
      value: 100,
      inside: {
        value: 200
      }
    }
  }

  unit.section(() => {
    // get an instance to test sb starting at the testing base endpoint
    const frb = new Frb({
      superFetch,
      dbName: SETTINGS.frb.dbName,
      base: SETTINGS.frb.base,
      noCache: true
    })
    unit.is(assets, frb.path().set({ data: assets }).throw().data, {
      description: 'set assets for tests'
    })
    unit.is(assets, frb.path().get().throw().data, {
      description: 'fixture properly set up'
    })
    unit.is(assets, frb.path().patch({ data: assets }).throw().data, {
      description: 'patching should leave and return it the same'
    })
    unit.is(assets, frb.path().get().throw().data, {
      description: 'fixture still unchanged'
    })

    unit.is({ value: 500 }, frb.path({ path: 'top/inside' }).patch({ data: { value: 500 } }).throw().data, {
      description: 'value from path top/inside is updated'
    })

    unit.is(500, frb.path({ path: 'top/inside/value' }).get().throw().data, {
      description: 'value from path top/inside/value is as expected'
    })

    // path can get stored as a shortcut
    const frbPath = frb.path({ path: 'top/value' })
    unit.is(frbPath.get().throw().data, frb.path({ path: 'top/value' }).get().throw().data, {
      description: 'shortcut path returns same value'
    })

    unit.is(assets.top.value, frbPath.get().throw().data, {
      description: 'value from path top/value is as expected'
    })

    unit.not(assets, frb.path().get().throw().data, {
      description: 'value is not the same as the original'
    })

    unit.is(assets, frb.path().set({ data: assets }).throw().data)

  }, {
    skip: skipTest.frb,
    description: 'Frb basic tests - no caching'
  })

  unit.section(() => {
    // get an instance to test sb starting at the testing base endpoint
    const frb = new Frb({
      superFetch,
      dbName: SETTINGS.frb.dbName,
      base: SETTINGS.frb.base,
      noCache: true
    })

    unit.is(assets, frb.path().set({ data: assets }).throw().data, {
      description: 'set assets for tests'
    })
    unit.is(assets, frb.path().get().throw().data, {
      description: 'fixture properly set up'
    })
    // this ref should do nothing
    const blankRef = frb.ref()

    unit.is(assets, blankRef.path().patch({ data: assets }).throw().data, {
      description: 'patching should leave and return it the same'
    })
    unit.is(frb.path().get().throw().data, blankRef.path().get().throw().data, {
      description: 'a blank ref returns same result'
    })
    // lets make a ref down a bit
    const topRef = frb.ref('top')
    unit.is(frb.path({path:'top/inside'}).get().throw().data, topRef.path({path:'inside'}).get().throw().data, {
      description: 'ref works'
    })
    unit.is({ value: 500 }, topRef.path({ path: 'inside' }).patch({ data: { value: 500 } }).throw().data, {
      description: 'value from path top/inside is updated'
    })

    unit.is(500, topRef.path({ path: 'inside/value' }).get().throw().data, {
      description: 'value from path top/inside/value is as expected'
    })

    // path can get stored as a shortcut
    const frbPath = topRef.path({ path: 'value' })
    unit.is(frbPath.get().throw().data, frb.path({ path: 'top/value' }).get().throw().data, {
      description: 'shortcut ref path returns same value'
    })

    unit.is(assets.top.value, frbPath.get().throw().data, {
      description: 'value from path top/value is as expected'
    })

    unit.not(assets, frb.path().get().throw().data, {
      description: 'value is not the same as the original'
    })

    unit.is(assets, frb.path().set({ data: assets }).throw().data)

  }, {
    skip: skipTest.ref,
    description: 'Frb basic tests - with refs - no caching'
  })

 unit.section(() => {
    // get an instance to test sb starting at the testing base endpoint
    const frb = new Frb({
      superFetch,
      dbName: SETTINGS.frb.dbName,
      base: SETTINGS.frb.base,
      noCache: true
    })

    // we can turn caching on with a ref as well as via the initial instantion
    const frbCached = frb.ref('', {noCache: false})

    unit.is(assets, frbCached.path().set({ data: assets }).throw().data, {
      description: 'set assets for tests'
    })
    unit.is(assets, frbCached.path().get().throw().data, {
      description: 'fixture properly set up'
    })
    
    unit.is(true, frbCached.path().get().throw().cached, {
      description: 'came from cache'
    })

    // this ref should do nothing
    const blankRef = frbCached.ref()
    unit.is(true, blankRef.path().get().throw().cached, {
      description: 'ref also came from cache'
    })

    unit.is(assets, blankRef.path().set({ data: assets }).throw().data, {
      description: 'cached with a ref returns proper result'
    })

    // lets make a ref down a bit
    const topRef = frbCached.ref('top')
    unit.is(frb.path({path:'top/inside'}).get().throw().data, topRef.path({path:'inside'}).get().throw().data, {
      description: 'ref works'
    })

    // path can get stored as a shortcut
    const frbPath = topRef.path({ path: 'value' })
    unit.is(frbPath.get().throw().data, frbCached.path({ path: 'top/value' }).get().throw().data, {
      description: 'shortcut ref path returns same value'
    })

    unit.is(assets.top.value, frbPath.get().throw().data, {
      description: 'value from path top/value is as expected when cached'
    })

    unit.is(true, frbPath.get().throw().cached, {
      description: 'value is cached as expected'
    })

    // set all back to normal
    unit.is(assets, frb.path().set({ data: assets }).throw().data)

    // caching should be gone now for top level
    unit.is(false, frbCached.path().get().throw().cached, {
      description: 'top level removed after noncache read'
    })

  }, {
    skip: skipTest.ref,
    description: 'Frb basic tests - with refs - caching'
  })


}
tests

Links

bmSuperFetch: 1B2scq2fYEcfoGyt9aXxUdoUPuLy-qbUC2_8lboUEdnNlzpGGWldoVYg2

IDE

GitHub

bmUnitTester: 1zOlHMOpO89vqLPe5XpC-wzA9r5yaBkWt_qFjKqFNsIZtNJ-iUjBYDt-x

IDE

GitHub

Related

goa twitter oauth2 apps script

Apps Script Oauth2 – a Goa Library refresher

It's been a few years since I first created the Goa library. Initially it was mainly to provide OAuth2 authorization ...
Read More
SuperFetch

SuperFetch plugin – Firebase client for Apps Script

Frb is a SuperFetch plugin to easily access a Firebase Real time database. SuperFetch is a proxy for UrlFetchApp with ...
Read More
SuperFetch

SuperFetch plugin – iam – how to authenticate to Cloud Run from Apps Script

SuperFetch is a proxy for UrlFetchApp with additional features - see SuperFetch - a proxy enhancement to Apps Script UrlFetch for ...
Read More
SuperFetch

SuperFetch – a proxy enhancement to Apps Script UrlFetch

I've written a few articles about JavaScript proxying on here, and I'm a big fan. I also use a lot ...
Read More
SuperFetch

Apps script caching with compression and enhanced size limitations

Motivation Caching is a great way to improve performance, avoid rate limit problems and even save money if you are ...
Read More

Simple but powerful Apps Script Unit Test library

Why unit testing? There are many test packages for Node (my favorite is ava) and there are also a few ...
Read More