From 6a19b99154b43903ba24d01fb61e509303ad2469 Mon Sep 17 00:00:00 2001 From: Christian Opitz Date: Sat, 25 Mar 2017 00:59:44 +0100 Subject: [PATCH] Added support for filter expressions from database paths to flashlight (firebase/flashlight#149) and added filter for invalid users --- flashlight.js | 137 +++++++++++++++++++++++++++++++++++++++ src/models/Config.js | 2 +- src/models/Flashlight.js | 1 + 3 files changed, 139 insertions(+), 1 deletion(-) diff --git a/flashlight.js b/flashlight.js index b7de350..80f358e 100644 --- a/flashlight.js +++ b/flashlight.js @@ -19,6 +19,143 @@ const fbrc = JSON.parse(fs.readFileSync('.firebaserc')); } }); +// That's so ugly but as flashlight doesn't export anything ¯\_(ツ)_/¯ +fs.appendFileSync('./lib/PathMonitor.js', '\n\nexports.PathMonitor = PathMonitor;'); + +const vm = require('vm'); +require('colors'); +const PathMonitor = require('./lib/PathMonitor').PathMonitor; +PathMonitor.prototype._process = function(fn, snap) { + const snVal = snap.val(); + const snKey = snap.key; + + if (fn === this._childRemoved) { + 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 = {}; + } +}; + module.exports = { paths: appConfig.flashlight.paths.paths || [], diff --git a/src/models/Config.js b/src/models/Config.js index 3b538f7..e21a2c0 100644 --- a/src/models/Config.js +++ b/src/models/Config.js @@ -97,7 +97,7 @@ const Config = { flashlight: { timeout: 3000, paths: { - paths: '/flashlight/paths', + paths: '/flashlight-test', queries: '/flashlight/queries', results: '/flashlight/results' } diff --git a/src/models/Flashlight.js b/src/models/Flashlight.js index 9daac3a..f3b5a6b 100644 --- a/src/models/Flashlight.js +++ b/src/models/Flashlight.js @@ -146,6 +146,7 @@ module.exports = class Flashlight { path: '/users/organizations/' + orgKey, index: orgKey, type: 'users', + filter: "['?', '!'].indexOf(ref('security/organizations/" + orgKey + "/users/' + $id)) < 0" }; } return paths;