Firebase JSON REST access library for Apps Script

The BigQuiz app uses Firebase for keep track of the question, category and game scores of individual players. In Firebase custom authentication with goa I showed how to use Goa to manage authentication for Firebase. This firebase access library works with goa, or you can use a different JWT generator if you aleady have one. 

cGoa library is available with this key, and on github.
   MZx5DzNPsYjVyZaR67xXJQai_d-phDA33

cFirebase library is available with this key, and on github
   MBz9GwQn5jZOg-riXY3pBTCz3TLx7pV4j

Why Firebase?

I had originally planned to use the JSON API for Google Game Play Services but I really think Google have got it all wrong for this API.
  • The Game play services API is extremely complicated to set up. There are multiple consoles and credentials, and registrations and thumbnail images and other boring stuff to go through - even if you just want to try it out - I think many people who may otherwise go on to use it, are going to be put off by this and just do something else - as I did.
  • There is a $25 registration fee to get started. Even if you just want to have a look at it to see if you want to use it. Surely marketing 101 says that you don't charge people to have a look at something. By all means charge if it gets used, but not just to look. 
If I'd persevered and swallowed the registration charge and worked through all the complex console stuff, then I probably would have got game play services to work, but a simple solution with Firebase is just fine for my demo BigQuiz app

Try the App here.

So this very simple library happened because the entry barrier for Game Play Services was just too high to be bothered with. 

cFireBase supports returning both promises and regular results (the default).

Getting a handle to the database

The first step is to get credentials set up. I won't go through that again, but it's very simple and covered in Firebase custom authentication with goa. Here's how I get a handle to my database in BigQuiz app

  ns.init = function () {
    
    // make a goa and get a firebase handle
    var goa =  cGoa.make (Demo.PACKAGE_PLAY.name,Demo.PACKAGE_PLAY.props);
    
    // use this handle for everything
    ns.handle = new cFireBase.FireBase().setAuthData (goa);
    
    return ns;
  };  

cFirebase also has its own JWT generator, which you can use if not using cGoa. Note that you'll have to manage your own secret and rules if you do this.

    var handle = new cFireBase
    .FireBase()
    .generateJWT('https://bigquiz.firebaseio.com/',{uid:"bruce"},'aK.....h0h');


If not using goa and have your own JWT generator, then you can set up a handle like this.
    var handle = new cFireBase.FireBase().setAuthData ( {
       root:"your firebase url",  
       key:your JWT authorization string  
    });



Library methods

If you already use Firebase, you'll know that the url is used for queries. I'll use this snippet from the database for the examples

 methodexample  description
 get handle.get() get all the data
 get handle.get ('players/112066118406610145134/ games/jeopardy/categories/AROUND%20THE%20WORLD/summary') get the object at the given location
 put handle.put (newObject, 'players/112066118406610145134/
games/jeopardy/categories/AROUND%20THE%20WORLD/summary')
 the new object replaces the data at the given location    
 post handle.post (newObject, 'players/112066118406610145134/
games/jeopardy/categories/AROUND%20THE%20WORLD/summary')
 the new object adds to the data at the given location. This is how firebase deals with arrays 
 patch handle.patch (partialObject, 'players/112066118406610145134/
games/jeopardy/categories/AROUND%20THE%20WORLD/summary')
 the data at the given location is partially updated with the values inthe partial object
 remove handle.remove ('players/112066118406610145134') Remove the data at the given location. this would remove all data for the selected player.
 removeAll handle.removeAll() remove all the data in the database    
 setAuthData handle.setAuthData (goa) set the root and key using a goa object    
 setAuthData handle.setAuthData ({key:jwt,root:"https://bigquiz.firebase.io"}) if you are not using goa, you can set this using a custom object    
 setPromiseMode handle.setPromiseMode(true) return promises instead of results. More on that later

Responses

All data requests return an object with these properties. Exponential backoff is automatically applied to all requests.
 property description
 ok true if everything has worked fine
 data the data returned from firebase
 response the httprresponse from the request
 path the url that was fetched from

Promise mode

By default, a request like this will result in a response as above.
var result = handle.get();

However it is possible to use Promise mode in order to return a promise rather than a result. This can sometimes be more convenient as described in Using es6 promises server side in Apps Script, although since Apps Script is currently not asynchonous, there are no performance benefits currently (but  preparing for the future is never a bad thing).

Promise mode is turned on like this
handle.setPromiseMode (true);

and responses will be delivered like this
handle.get()
.then (
  function (result) {
    // this is a response like the one documented above
  },
 function (err) {
   // deal with the error
 }
);






The code

The polyfill for Using es6 promises server side in Apps Script is inlined also, but not shown here. The full code is available on github.

/** 
 * used for dependency management
 * @return {LibraryInfo} the info about this library and its dependencies
 */
function getLibraryInfo () {
  return {
    info: {
      name:'cFireBase',
      version:'0.0.3',
      key:'MBz9GwQn5jZOg-riXY3pBTCz3TLx7pV4j',
      share:'https://script.google.com/d/18qhPwatsNUfbJTHBQ1bmsZBKjQsPxj_hMC59zowprZCSFT1z0IrLcMu1/edit?usp=sharing',
      description:'firebase api'
    },
    dependencies:[
      cUseful.getLibraryInfo()
    ]
  }; 
}

/**
 * firebase API
 * @constructor Firebase
 */
var FireBase = function () {

  var self = this, authData_,promiseMode_ = false;;
  

  /**
   * generate a JWT
   * you can use this as an alternative to using cGoa
   * it's up to you to manage your own secrets etc if not using cGoa
   * @param {string} firebaseRoot the db root such as 'https://yourproject.firebaseio.com/'
   * @param {object} firebaseRules your auth rules such as {uid:"bruce"}
   * @param {string} firebaseSecret your secret key , such as aK....0h
   * @return {Firebase} self
   */
  self.generateJWT  = function (firebaseRoot, firebaseRules , firebaseSecret) {
    
    var ft =  JWT.generateJWT ( firebaseRules , firebaseSecret );
    if (!ft) {
      throw 'unable to generate firebase jwt';
    }
    authData_ = {
      key:ft,
      root:firebaseRoot,
    };
    return self;
  };

  
  /**
   * set a goa handler 
   * @param {object||goa} authData the auth data
   * @return {Firebase} self
   */
  self.setAuthData = function (authData) {
    
    // can take a goa as well
    authData_ = authData;
    if (!authData_ || !self.getRoot() || !self.getKey() ) {
      throw 'need an auth object with a root and a key or a goa object';
    }
    return self;
  };
  
  /**
   * set promise mode - in this mode promises are returned for fetches rather than resilts
   * @param {boolean} promiseMode 
   * @return {FireBase} self
   */
  self.setPromiseMode = function(promiseMode) {
    promiseMode_ = promiseMode;
    return self;
  };
  /**
   * do a put (replaces data)
   * @param {string} putObject an object to put
   * @param {string} [childPath=''] a child path 
   * @return 
   */
  self.put = function (putObject,childPath) {
  
    return payload_ ("PUT", putObject, childPath);
    
  };
  
  /**
   * do a delete of all
   * @return 
   */
  self.removeAll = function () {
  
    return fetch_ ( getPath_ (), {method:"DELETE"}); 
    
  };
  
  /**
   * do a delete
   * @param {string} [childPath=''] a child path 
   * @return 
   */
  self.remove = function (childPath) {
    if (!childPath) {
      throw 'childPath is missing - to delete all records use removeAll method';
    }
    return fetch_ ( getPath_ (childPath), {method:"DELETE"}); 
    
  };
  
  /**
   * do a get
   * @param {string} [childPath=''] a child path 
   * @return 
   */
  self.get = function (childPath) {
  
    return fetch_ ( getPath_ (childPath) ); 
    
  };
  
  /**
   * do a post (adds to data and generates a unique key)
   * @param {string} putObject an object to put
   * @param {string} [childPath=''] a child path 
   * @return 
   */
  self.post = function (putObject,childPath) {
  
    return payload_ ("POST", putObject, childPath);
    
  };
  
  /**
   * do a patch (partially replaces an item)
   * @param {string} putObject an object to put
   * @param {string} [childPath=''] a child path 
   * @return 
   */
  self.patch = function (putObject,childPath) {

    return payload_ ("PATCH", putObject, childPath);
    
  };
  
  /**
   * get the path given a child path
   * @param {string} [childPath=''] the childpath
   * @return {string} the path
   */
  function getPath_  (childPath) {
    return self.getRoot() + ( childPath || '' ) + '.json';
  }

  /**
   * do any payload methods
   * @param {string} putObject an object to put
   * @param {string} [childPath=''] a child path 
   * @return 
   */
  function payload_ (method, putObject,childPath) {
    
     return fetch_ ( getPath_ (childPath), {
      method:method,
      payload:JSON.stringify(putObject)
     }); 
  
  }
  
  /**
   * do a fetch
   * @param {string} url the url
   * @param {object} [options={method:'GET'}] 
   * @return {Promise}
   */
  function fetch_ (url, options) {
  
    // defaults
    options = options || {method:'GET'};
    if (!options.hasOwnProperty("muteHttpException")) {
      options.muteHttpExceptions = true;
    }
    var result;

    return promiseMode_ ? 
      new Promise (function (resolve, reject) {
        try {
          result = doRequest();
          resolve (makeResult());
        } 
        catch(err) {
          reject(err);
        }
      }) : makeAndDo();
    
    function makeAndDo () {
      result = doRequest();
      return makeResult();
    }
    
    function doRequest () {
      return cUseful.Utils.expBackoff (function() {
        return UrlFetchApp.fetch (url + "?auth=" + self.getKey(), options);
      });
    }
    
    function makeResult () {
      return {
        ok: result.getResponseCode() === 200,
        data: result.getResponseCode() === 200 ? JSON.parse(result.getContentText()) : null,
        response:result,
        path:url
      };
    }


  };
 
  // deteemine whether we have a goa or a customm object
  function isGoa_ (authData) {
    return typeof authData.getToken === "function" && typeof authData.getProperty === "function";
  }
  
  // get the key
  self.getKey = function () {
    return isGoa_ (authData_ ) ? authData_.getToken() : authData_.key;
  };
  
  // get the database root
  self.getRoot = function () {
    return isGoa_ (authData_ ) ? authData_.getProperty("root") : authData_.root;
  };
  return self;
};

/**
 * @namespace JWT
 * a namespace to generate a firebase jwt
 */
var JWT = (function (ns) {

  /**
   * generate a jwt for firebase using default settings
   * @param {object} data the data package
   * @param {string} secret the jwt secret
   */
  ns.generateJWT = function(data, secret) {
    
    var header = getHeader_ ();
    var claims = getClaims_ (data);
    var jwt = header + "." + claims;
    
    // now sign it 
    var signature = Utilities.computeHmacSha256Signature (jwt, secret);
    var signed = unPad_ (Utilities.base64EncodeWebSafe(signature));

    // and thats the jwt
    return jwt + "." + signed;
  };
  
  /**
   * generate a jwt header
   * return {string} a jwt header b64
   */
  function getHeader_ () {
  
    return unPad_(Utilities.base64EncodeWebSafe(JSON.stringify( {
      "alg": "HS256",
      "typ": "JWT" 
    })));
  }
  
  /**
   * generate a jwt claim for firebase
   * return {string} a jwt claimsm payload b64
   */
  function getClaims_  (data) {
    
    return unPad_ (Utilities.base64EncodeWebSafe( JSON.stringify( {
      "d" : data || {},
      "iat": Math.floor(new Date().getTime()/1000),
      "v": 0
    })));
  }
  
  /**
   * remove padding from base 64
   * @param {string} b64 the encoded string
   * @return {string} padding removed
   */
  function unPad_ (b64) {
    return b64 ?  b64.split ("=")[0] : b64;
  }

  return ns;
})(JWT || {});



For more like this, see Google Apps Scripts snippets. Why not join our forumfollow the blog or follow me on twitter to ensure you get updates when they are available. 





Comments