Skip to content

Instantly share code, notes, and snippets.

@copitz
Last active March 25, 2017 02:43
Show Gist options
  • Save copitz/53ca89011cc544eb6c0de3cc02f60c64 to your computer and use it in GitHub Desktop.
Save copitz/53ca89011cc544eb6c0de3cc02f60c64 to your computer and use it in GitHub Desktop.
Add support for filter expressions in database paths to firebase flashlight
/**
* 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