Last active
March 25, 2017 02:43
-
-
Save copitz/53ca89011cc544eb6c0de3cc02f60c64 to your computer and use it in GitHub Desktop.
Add support for filter expressions in database paths to firebase flashlight
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* This allows for filter expressions in firebase flashlight paths, e.g.: | |
* flashlight.paths.users => { | |
* path: '/users', | |
* index: 'myindex', | |
* type: 'user', | |
* filter: "!data.deleted && !ref('/blocked-users/' + $id) && ref('/user-roles/' + $id) !== 'guest'" | |
* } | |
* | |
* As you see above, there's: | |
* - data: The current object to be indexed | |
* - $id: The key of the current object | |
* - ref(path): Loads and returns the data at the specified path | |
* (when data at path is updated, the filter will be reevaluated as well) | |
*/ | |
const fs = require('fs'); | |
const vm = require('vm'); | |
require('colors'); | |
// That's so ugly but as flashlight doesn't export PathMonitor ¯\_(ツ)_/¯ | |
// (when using flashlight with docker this should be fine but | |
// try finding another way otherwise - e.g. fork flashlight) | |
fs.appendFileSync('./lib/PathMonitor.js', '\n\nexports.PathMonitor = PathMonitor;'); | |
// Override the _process method to add support for filter functions from DB | |
const PathMonitor = require('./lib/PathMonitor').PathMonitor; | |
PathMonitor.prototype._process = function(fn, snap) { | |
const snVal = snap.val(); | |
const snKey = snap.key; | |
if (fn === this._childRemoved) { | |
// When child will be removed, filtering shouldn't bother | |
this._childRemoved(snKey, snVal); | |
if (this.filterRefs) { | |
const filterRefs = this.filterRefs; | |
Object.keys(filterRefs).forEach((path) => { | |
delete filterRefs[path].keys[key]; | |
if (!Object.keys(filterRefs[path].keys).length) { | |
filterRefs[path].ref.off('value'); | |
delete filterRefs[path]; | |
} | |
}); | |
} | |
return; | |
} | |
if (typeof this.filter === 'string') { | |
if (!this.filterScript) { | |
try { | |
this.filterScript = new vm.Script('RESULT = ' + this.filter, { | |
filename: this.ref.toString() + '/filter', | |
displayErrors: true | |
}); | |
} catch (e) { | |
console.error('Error in filter expression:'.red); | |
console.error(e); | |
this.filter = () => false; | |
return; | |
} | |
this.filterRefs = {}; | |
} | |
const filterRefs = this.filterRefs; | |
let load = {}; | |
const invalidPathErrors = []; | |
const paths = {}; | |
const context = vm.createContext({ | |
ref: function (path) { | |
if (typeof path !== 'string' || !path) { | |
throw new Error('INVALID_PATH'); | |
} | |
if (filterRefs[path]) { | |
paths[path] = true; | |
return filterRefs[path].val; | |
} | |
load[path] = true; | |
throw new Error('LOADING_REF'); | |
}, | |
data: snVal, | |
$id: snKey, | |
RESULT: false | |
}); | |
const run = () => { | |
try { | |
this.filterScript.runInContext(context); | |
} catch (e) { | |
if (e.message === 'INVALID_PATH') { | |
if (invalidPathErrors.indexOf(e.toString()) < 0) { | |
invalidPathErrors.push(e.toString()); | |
} else { | |
console.error('Invalid path at %s'.red, this.filter); | |
console.error(e); | |
return; | |
} | |
} else if (e.message !== 'LOADING_REF') { | |
console.error('Error at %s'.red, this.filter); | |
console.error(e); | |
return; | |
} | |
} | |
const promises = []; | |
Object.keys(load).forEach((path) => { | |
promises.push(new Promise((resolve, reject) => { | |
const ref = this.ref.root.child(path); | |
let initial = true; | |
ref.on('value', (sn) => { | |
if (initial) { | |
initial = false; | |
filterRefs[path] = { ref, keys: {}, val: sn.val() }; | |
resolve(); | |
} else { | |
filterRefs[path].val = sn.val(); | |
Object.keys(filterRefs[path].keys).forEach((key) => { | |
this.ref.child(key).once('value', this._process.bind( | |
this, filterRefs[path].keys[key] ? this._childChanged : this._childAdded | |
)); | |
}); | |
} | |
}, (e) => { | |
console.error('Firebase error at %s'.red, this.filter); | |
console.error(e); | |
if (initial) { | |
initial = false; | |
reject(); | |
} | |
}); | |
})); | |
}); | |
load = {}; | |
if (promises.length) { | |
Promise.all(promises).then(run, () => {}); | |
} else { | |
Object.keys(paths).forEach((path) => { | |
filterRefs[path].keys[snKey] = context.RESULT; | |
}); | |
if (context.RESULT) { | |
fn.call(this, snKey, this.parse(snVal)); | |
} else if (fn === this._childChanged) { | |
this._childRemoved(snKey, snVal); | |
} | |
} | |
}; | |
run(); | |
} else if (this.filter(snVal)) { | |
fn.call(this, snKey, this.parse(snVal)); | |
} | |
}; | |
PathMonitor.prototype._oldStop = PathMonitor.prototype._stop; | |
PathMonitor.prototype._stop = function () { | |
this._oldStop(); | |
if (this.filterRefs) { | |
Object.keys(this.filterRefs).forEach((path) => { | |
this.filterRefs[path].ref.off('value'); | |
}); | |
this.filterRefs = {}; | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment