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.
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 databaseThe 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 });
|
method | example | 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 || {});