<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" />
<link rel="stylesheet" href="style.css" />
<script src="https://www.gstatic.com/firebasejs/4.5.0/firebase.js"></script>
<script src="https://code.jquery.com/jquery-3.1.0.min.js"></script>
<script src="TableBuilder.js"></script>
<script src="CookieManager.js"></script>
<script src="AuthManager.js"></script>
<script src="QueryBuilder.js"></script>
<script src="AppManager.js"></script>
<script src="OutputManager.js"></script>
<script src="app.js"></script>
</head>
<body>
<div class="col left">
<form>
<label>
<div>Auth API Key</div>
<input type=text name="apiKey" placeholder="AIza...-VwdXY488E" />
</label>
<label>
<div>Database URL</div>
<input type=text name="url" placeholder="your app name" /><span class="note">https://<span data-id="urltext"></span>.firebaseio.com</span>
</label>
<br />
<label>
<div>Path</div>
<input type=text name="path" placeholder="path/to/data" />
</label>
<label>
<div>orderBy</div>
<input type=text name="orderByChild" placeholder="Enter a field name" />
</label>
<p><span class="note">You can enter <code>true</code>, <code>false</code>, <code>null</code>, or <code>0-9</code> and they will be typed correctly. Put them in quotes to pass a string value instead.</span></p>
<label>
<div>startAt</div>
<input type=text name="startAt" />
</label>
<label>
<div>endAt</div>
<input type=text name="endAt" />
</label>
<label>
<div>limitToFirst</div>
<input type=number name="limitToFirst" value="0" />
</label>
<label>
<div>limitToLast</div>
<input type=number name="limitToLast" value=0 />
</label>
<label><input type="checkbox" name="print_curl">print as CURL request</label>
<label><input type="checkbox" name="output_json"> output as json</label>
<label><input type="checkbox" name="save_config"> save these in a cookie</label>
<!--<button type=submit data-submit="print">Print Query</button>-->
<button type=submit data-submit="run">Run Query</button>
</form>
<p data-id="loggedOut">
You are not authenticated:
<button data-auth="google"><i class="fa fa-google"></i></button>
<button data-auth="password"><i class="fa fa-envelope"></i></button>
<button data-auth="twitter"><i class="fa fa-twitter"></i></button>
<button data-auth="facebook"><i class="fa fa-facebook"></i></button>
</p>
<p data-id="loggedIn" class=hide>Logged in as <span></span>. <button data-id=signout><i class="fa fa-sign-out"></i></button></p>
</div>
<div data-id=out class="col right"><pre></pre></div>
</body>
</html>
div.col {
float: left;
margin-right: 4%;
position: relative;
}
div.col.left {
width: 400px;
max-width: 90%;
}
div.col.right {
min-width: 350px;
}
form {
display: block;
width: 100%;
display: relative;
}
form label, form fieldset {
margin-bottom: 5px;
}
form label { margin-bottom: 5px; display: block; }
form label div { font-weight: bold; }
form label span { color: gray; }
textarea, input[type=text] { width: 90%; }
textarea { height: 8em; }
label { cursor: pointer; }
.hide { display: none }
code { color: #800; font-family: monospace; }
pre {
background-color: #eaeaea;
border: 1px solid gray;
padding: 4px;
min-height: 20px;
max-height: 90%;
overflow: auto;
}
th {
text-align: left;
color: gray;
font-weight: normal;
}
tr td {
background-color: #eee;
}
tr:nth-child(even) td {
background-color: #efe;
}
tr {
margin-bottom: 3px;
margin-left: 3px;
}
.hide {
display: none;
}
class AuthManager {
constructor(appManager) {
this.app = appManager;
}
getUser() {
return this.user;
}
signIn(providerName) {
const provider = this.getProvider(providerName);
return this.getAuth().signInWithPopup(provider);
}
getAuth() {
return this.app.getApp().auth();
}
signOut() {
return this.getAuth().signOut();
}
listen(fn) {
return this.getAuth().onAuthStateChanged(fn);
}
getProvider(providerName) {
let provider = null;
switch(providerName) {
case 'google':
provider = new firebase.auth.GoogleAuthProvider();
//provider.addScope('https://www.googleapis.com/auth/contacts.readonly');
break;
case 'twitter':
provider = new firebase.auth.TwitterAuthProvider();
break;
case 'facebook':
provider = new firebase.auth.FacebookAuthProvider();
//provider.addScope('user_birthday');
break;
case 'password':
default:
throw new Error('Unrecognized provider: ' + providerName);
}
return provider;
}
}
class AppManager {
constructor() {
this.auth = new AuthManager(this);
this.app = null;
this.url = null;
this.apiKey = null;
this.appCounter = 0;
}
setUrl(url) {
this.url = url;
this.reset();
}
setApiKey(apiKey) {
this.apiKey = apiKey;
this.reset();
}
getCreds() {
return {
apiKey: this.apiKey,
authDomain: this.url + ".firebaseapp.com",
databaseURL: `https://${this.url}.firebaseio.com`
};
}
getApp() {
if( !this.app ) {
// we don't use the default app ever; this allows us to change the URL
// or reauthenticate without blowing up along the way
this.appCounter++;
this.app = firebase.initializeApp(this.getCreds(), `app${this.appCounter}`);
}
return this.app;
}
reset() {
if( this.app ) {
this.auth.signOut();
}
this.app = null;
}
ref(path) {
let ref = this.getApp().database().ref();
if( path ) {
ref = ref.child(path);
}
return ref;
}
}
class QueryBuilder {
constructor(appManager, outputManager) {
this.app = appManager;
this.out = outputManager;
this.path = null;
this.opts = {};
}
setPath(path) {
this.path = path;
}
setOptions(opts) {
this.opts = opts;
}
print(outputType) {
console.log('printQuery');
let creds = this.app.getCreds();
let text = null;
if( outputType === 'curl' ) {
text = this.curlText(creds, this.opts);
}
else {
text = this.jsText(creds, this.opts);
}
this.out.log(text);
}
run(outputType) {
console.log('runQuery');
this.out.log("Running...");
// run the query
let query = this.app.ref(this.path);
Object.keys(this.opts).forEach(key => {
query = query[key](this.opts[key]);
});
query.once('value').then(snap => {
// print the results
if( outputType === 'json' ) {
this.out.log(snap.val());
}
else {
this.out.table(buildTableFromSnapshot(snap));
}
}).catch(e => this.out.error(e));
}
curlText(creds, opts) {
console.log(creds, opts);
let text = creds.databaseURL + '/';
if( this.path ) {
text += this.path;
}
text += '.json?print=pretty';
Object.keys(this.opts).forEach(key => {
text += '&';
let v = this.opts[key];
if( typeof v === 'string' ) {
v = enquote(encodeURIComponent(this.opts[key]));
}
if( key === 'orderByChild' ) {
text += 'orderBy=' + v;
}
else {
text += key + '=' + v;
}
});
return text;
}
jsText(creds, opts) {
let credsText = JSON.stringify(creds, null, 2);
let queryText = this.path? 'db.child("' + this.path + '")' : 'db';
Object.keys(opts).forEach(k => {
let v = enquote(opts[k]);
queryText += `\n .${k}(${v})`;
});
return `
// Set the configuration for your app
let config = ${credsText};
firebase.initializeApp(config);
// Get a reference to the database service
let db = firebase.database().ref();
// Create a query
let query = ${queryText};
`;
}
}
class OutputManager {
constructor($el) { this.$out = $el; }
empty() { this.$out.empty(); }
table($table) {
this.$out.empty().append($table);
}
log(data) {
if( typeof data !== 'string' )
data = JSON.stringify(data, null, 2);
$('<pre></pre>').text(data).appendTo(this.$out.empty());
}
error(e) {
console.error(e);
let text = e.toString();
if( typeof e === Error ) {
text += "\n\n" + e.stack;
}
this.log(text);
}
}
class CookieManager {
constructor(fields, callback) {
this.fields = fields;
this.callback = callback;
}
load() {
// load any existing cookie data and put it into the form
let saveEnabled = this.get('saveEnabled');
if( saveEnabled ) {
this.fields.forEach(key => {
this.callback(key, this.get(key));
});
}
return saveEnabled;
}
get(key) {
let n = encodeURIComponent(key) + '=';
let parts = document.cookie.split(';');
for(let i=0, len=parts.length; i < len; i++) {
let p = parts[i];
while(p.charAt(0) === ' ') {
p = p.substring(1, p.length);
}
if(p.indexOf(n) === 0) {
return decodeURIComponent(p.substring(n.length, p.length));
}
}
return null;
}
set(key, value, days) {
let expires = "";
if (days) {
let date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toGMTString();
}
document.cookie = encodeURIComponent(key) + "=" + encodeURIComponent(value) + expires + "; path=/";
}
setField($field) {
let v = null;
if ($field.attr('type') === 'checkbox') {
v = $field.prop('checked') ? 1 : 0;
}
else {
v = $field.val();
}
this.set($field.attr('name'), v);
}
remove() {
this.fields.forEach(key => this.set(key, "", -1));
}
}
class TableBuilder {
constructor() {
this.$table = $('<table></table>');
this.$thead = $('<thead></thead>').appendTo(this.$table);
this.$tbody = $('<tbody></tbody>').appendTo(this.$table);
this.$row = null;
}
header(values) {
let $tr = $('<tr></tr>').appendTo(this.$thead);
values.forEach(v => $('<th></th>').text(v).appendTo($tr));
}
row() {
this.$row = $('<tr></tr>').appendTo(this.$tbody);
}
cell(value) {
$('<td></td>').text(value).appendTo(this.$row);
}
getEl() { return this.$table; }
}
jQuery(function($) {
let QUERY_FIELDS = ['orderByChild', 'startAt', 'endAt', 'limitToFirst', 'limitToLast'];
let COOKIE_FIELDS = ['apiKey', 'url', 'print_curl', 'output_json'];
let $out = $('[data-id=out]');
let $url = $('input[name=url]');
let $apiKey = $('input[name=apiKey]');
let $save = $('input[name=save_config]');
let $path = $('input[name=path]');
let $loggedOut = $('[data-id=loggedOut]');
let $loggedIn = $('[data-id=loggedIn]');
let app = new AppManager();
let output = new OutputManager($out);
let query = new QueryBuilder(app, output);
let cookies = new CookieManager(COOKIE_FIELDS, loadedValue);
initCookies();
initEventListeners();
app.auth.listen(authStateChanged);
function initEventListeners() {
$('[data-auth]').click(signIn);
$('[data-id=signout]').click(() => app.auth.signOut());
$('[data-submit=run]').click(runQuery);
$('form').submit(e => e.preventDefault());
$url.keypress(e => $('span[data-id=urltext]').text($url.val()));
// $('[data-submit=print]').click(printQuery);
$url.add($apiKey).keyup(updateApp).change(updateApp).blur(updateApp);
$('input').keyup(updateQuery).change(updateQuery).blur(updateQuery);
$('[type=checkbox]').click(updateQuery);
$('[name=url], [name=apiKey]').change(checkCreds);
$save.click(toggleSave);
}
function updateApp() {
app.setUrl($url.val());
app.setApiKey($apiKey.val());
}
function updateQuery() {
let type = $('input[name=print_curl]').prop('checked')? 'curl' : 'js';
query.setPath($path.val()||null);
query.setOptions(buildQueryOptions());
query.print(type);
}
function runQuery(e) {
e.preventDefault();
let type = $('input[name=output_json]').prop('checked')? 'json' : 'table';
query.setPath($path.val()||null);
query.setOptions(buildQueryOptions());
query.run(type);
}
function buildCreds() {
let url = $.trim($url.val());
return {
apiKey: "AIzaSyCktxUp-JovGV7Sw7dT9defEV-VwdmGe8E",
authDomain: url + ".firebaseapp.com",
databaseURL: "https://" + url + ".firebaseio.com"
}
}
function buildQueryOptions() {
let opts = {};
QUERY_FIELDS.forEach(key => {
let v = parseValue($(`[name=${key}]`).val());
if( typeof v !== 'undefined' && (v !== 0 || ['limitToFirst','limitToLast'].indexOf(key) == -1) ) {
opts[key] = v;
}
});
return opts;
}
function checkCreds() {
let hasUrl = !!$url.val();
$('button').prop('disabled', !hasUrl);
if( hasUrl ) {
$('span[data-id=urltext]').text($url.val());
}
else {
app.auth.signOut();
}
app.setApiKey($apiKey.val());
app.setUrl($url.val());
}
function loadedValue(key, value) {
console.log('cookie', key, value, typeof value);
let $field = $(`[name=${key}]`);
if( $field.attr('type') === 'checkbox' ) {
$field.prop('checked', value == 1);
}
else if( value ) {
$field.val(value);
}
}
function toggleSave() {
console.log('toggleSave');
if( $save.prop('checked') ) {
COOKIE_FIELDS.forEach(key => {
cookies.setField($(`[name=${key}]`));
});
cookies.set('saveEnabled', 1);
}
else {
cookies.remove();
cookies.set('saveEnabled', 0);
}
}
function initCookies() {
if( cookies.load() ) {
checkCreds();
$save.prop('checked', true);
}
COOKIE_FIELDS.forEach(f => {
let $field = $(`[name=${f}]`);
$field.change(() => {
cookies.set(f, $field.val());
});
});
}
function signIn(e) {
e.preventDefault();
if( !$apiKey.val() || !$url.val() ) {
alert('You must enter an API Key and Database URL first :(');
return;
}
let provider = $(this).attr('data-auth');
app.auth.signIn(provider);
}
function authStateChanged(user) {
const loggedIn = !!user;
console.log('authStateChanged', loggedIn, user);
$loggedOut.toggleClass('hide', loggedIn);
$loggedIn.toggleClass('hide', !loggedIn);
if( loggedIn ) {
$loggedIn.find('span').text(user.displayName || user.email || user.uid)
}
}
});
function parseValue(v) {
if( v === "" ) { return undefined; }
if( /^"(.*)"$/.test(v) ) {
return v.substring(1, v.length - 1);
}
else if( /^-?[0-9]+$/.test(v) ) {
return parseInt(v, 10);
}
switch(v) {
case "true": return true;
case "false": return false;
case "null": return null;
default: return v;
}
}
function enquote(v) {
if( typeof v === 'string' ) {
return '"' + v + '"';
}
return v;
}
function buildTableFromSnapshot(snap) {
let table = new TableBuilder();
if( snap.hasChildren() ) {
let firstRun = true;
snap.forEach(ss => {
let rec = ss.val();
let keys = Object.keys(rec);
if( firstRun ) {
table.header(keys);
firstRun = false;
}
table.row();
keys.forEach(k => table.cell(rec[k]));
});
}
else {
table.header(['value']);
table.row();
table.cell(snap.val());
}
return table.getEl();
}