node-red-contrib-cloud-firestore
Version:
Node-RED nodes to handle google cloud firestore read and write operations.
207 lines (183 loc) • 5.89 kB
JavaScript
const { objectTypeOf } = require('../utils');
const util = require('util');
const vm = require('vm');
const Mustache = require('mustache');
function FirestoreReadNode(config) {
if (!config.admin) {
throw 'No firebase admin specified';
}
this.id = config.id;
this.RED = config.RED;
this.context = config.context();
this.instance = config.admin.app;
this.firebase = config.admin.firebase;
this.firestore = config.admin.firestore;
this.collection = config.collection;
this.group = config.group;
this.document = config.document;
this.realtime = config.realtime;
this.query = config.query;
this.snapHandler = config.snapHandler;
this.eject = config.eject;
this.snapListener = null;
// register a realtime listener on node start
if (this.realtime) {
const msg = { firestore: {} };
const res = this.context.flow.get(`$fst-input-${this.id}`);
msg.firestore = res || {};
this.main(msg, config.send.bind(config), config.error.bind(config));
}
}
FirestoreReadNode.prototype.main = function (msg, send, error) {
const input = (msg.hasOwnProperty('firestore')) ? msg.firestore || {} : {};
if (input.eject || this.eject) {
msg.firebase = { app: this.instance, admin: this.firebase };
} else {
delete msg.firestore;
}
const col = input.collection || Mustache.render(this.collection, msg);
const group = input.group || this.group;
const doc = input.document || Mustache.render(this.document, msg);
const rt = input.realtime || this.realtime;
const query = input.query || this.query;
const disable = input.disableHandler || false;
if (!col) {
throw 'FireStore collection reference not set';
}
let snapHandler = this.snapHandler;
if (!disable) {
snapHandler = this.prepareSnapHandler(this.snapHandler, {
config: input,
context: this.context,
RED: this.RED, msg
});
}
if (rt) {
// Save input to storage to better handle realtime recalls on node restarts
try {
this.context.flow.set(`$fst-input-${this.id}`, input);
} catch (e) {
}
}
if (doc && group) return Promise.reject('Cannot set document ref in a collection group query');
let baseRef, referenceQuery;
try {
if (group) {
baseRef = this.firestore.collectionGroup(col);
referenceQuery = this.prepareQuery(baseRef, query);
} else {
baseRef = doc ? this.firestore.collection(col).doc(doc) : this.firestore.collection(col);
referenceQuery = doc ? baseRef : this.prepareQuery(baseRef, query);
}
} catch (e) {
return Promise.reject(e);
}
// remove existing listener before registering another
this.unsubscribeListener();
if (disable) {
msg.payload = referenceQuery;
send(msg);
return Promise.resolve();
}
if (!rt) {
return referenceQuery.get()
.then((snap) => snapHandler(snap))
.then((result) => {
msg.payload = result;
send(msg);
})
.catch((err) => error(err, msg));
} else {
this.snapListener = referenceQuery.onSnapshot((snap) => {
Promise.resolve(snapHandler(snap))
.then((result) => {
msg.payload = result;
send(msg);
}).catch(err => error(err, msg));
}, (err) => error(err, msg));
return Promise.resolve();
}
};
FirestoreReadNode.prototype.prepareQuery = function (baseRef, queryObj) {
queryObj.forEach((condition) => {
const key = Object.keys(condition)[0];
let value = condition[key];
let isString = objectTypeOf(value) === '[object String]';
let isArray = objectTypeOf(value) === '[object Array]';
let isObject = objectTypeOf(value) === '[object Object]';
switch (key) {
case 'where':
if (isString) {
baseRef = baseRef.where(value);
} else if (isArray) {
baseRef = baseRef.where(...value);
} else if (isObject) {
baseRef = baseRef.where(value.field, value.operation, value.value);
}
break;
case 'orderBy':
if (isString) {
baseRef = baseRef.orderBy(value);
} else if (isArray) {
baseRef = baseRef.orderBy(...value);
} else if (isObject) {
baseRef = !!value.direction ?
baseRef.orderBy(value.field, value.direction)
: baseRef.orderBy(value.field);
}
break;
case 'startAt':
case 'endAt':
case 'startAfter':
case 'endBefore':
baseRef = isArray ? baseRef[key](...value) : baseRef[key](value);
break;
case 'limit':
case 'offset':
baseRef = baseRef[key](value);
break;
}
});
return baseRef;
};
FirestoreReadNode.prototype.prepareSnapHandler = function (func, ctx = {}) {
if (!func) return this.defaultSnapHandler.bind(this);
const sandbox = {
...ctx,
util: util,
Date: Date,
Buffer: Buffer,
Promise: Promise,
console: console
};
const context = vm.createContext(sandbox);
const script = new vm.Script(`(function() {return function(snap) {${func}}})()`, {
filename: this.id + (this.name ? '[' + this.name + ']' : '') + 'custom handler',
displayErrors: true
});
try {
return script.runInContext(context);
} catch (e) {
throw e.message || e;
}
};
FirestoreReadNode.prototype.defaultSnapHandler = function (snap) {
if (snap.size || !snap.id) { // got an entire collection
const docArray = {};
snap.forEach(function (snapDoc) {
if (!snapDoc.exists) return;
docArray[snapDoc.id] = snapDoc.data();
});
return docArray;
} else {
return snap.data();
}
};
FirestoreReadNode.prototype.unsubscribeListener = function () {
if (objectTypeOf(this.snapListener) === '[object Function]') this.snapListener();
};
FirestoreReadNode.prototype.onClose = function (done) {
this.unsubscribeListener();
done();
};
module.exports = FirestoreReadNode;