prisme-flow
Version:
prisme platform flow engine
399 lines (377 loc) • 14.2 kB
JavaScript
/**
* Created by prisme.io on 09/06/2017.
*/
module.exports = function (RED) {
'use strict'
var EventEmitter = require('events').EventEmitter
var appEnv = require('cfenv').getAppEnv()
var mongodb = require('mongodb')
var forEachIteration = new Error('mongodb forEach iteration')
var forEachEnd = new Error('mongodb forEach end')
var services = []
Object.keys(appEnv.services).forEach(function (label) {
if ((/^mongo/i).test(label)) {
services = services.concat(appEnv.services[label].map(function (service) {
return {
'name': service.name,
'label': service.label
}
}))
}
})
var operations = {}
Object.keys(mongodb.Collection.prototype).forEach(function (operationName) {
if (typeof Object.getOwnPropertyDescriptor(mongodb.Collection.prototype, operationName).value === 'function') {
operations[operationName] = mongodb.Collection.prototype[operationName]
}
})
// We don't want to pass the find-operation's cursor directly.
delete operations.find
operations['find.toArray'] = function () {
var args = Array.prototype.slice.call(arguments, 0)
var callback = args.pop()
mongodb.Collection.prototype.find.apply(this, args).toArray(callback)
}
operations['find.forEach'] = function () {
var args = Array.prototype.slice.call(arguments, 0)
var callback = args.pop()
mongodb.Collection.prototype.find.apply(this, args).forEach(function (doc) {
return callback(forEachIteration, doc)
}, function (err) {
return callback(err || forEachEnd)
})
}
// We don't want to pass the aggregate's cursor directly.
delete operations.aggregate
operations['aggregate.toArray'] = function () {
var args = Array.prototype.slice.call(arguments, 0)
var callback = args.pop()
mongodb.Collection.prototype.aggregate.apply(this, args).toArray(callback)
}
operations['aggregate.forEach'] = function () {
var args = Array.prototype.slice.call(arguments, 0)
var callback = args.pop()
mongodb.Collection.prototype.aggregate.apply(this, args).forEach(function (doc) {
return callback(forEachIteration, doc)
}, function (err) {
return callback(err || forEachEnd)
})
}
// We don't want to pass the listIndexes's cursor directly.
delete operations.listIndexes
operations['listIndexes.toArray'] = function () {
var args = Array.prototype.slice.call(arguments, 0)
var callback = args.pop()
mongodb.Collection.prototype.listIndexes.apply(this, args).toArray(callback)
}
operations['listIndexes.forEach'] = function () {
var args = Array.prototype.slice.call(arguments, 0)
var callback = args.pop()
mongodb.Collection.prototype.listIndexes.apply(this, args).forEach(function (doc) {
return callback(forEachIteration, doc)
}, function (err) {
return callback(err || forEachEnd)
})
}
operations.db = function (callback) {
return callback(null, this)
}
operations.collection = function (callback) {
return callback(null, this)
}
RED.nodes.registerType('mongodb', function Mongo2ConfigNode (n) {
RED.nodes.createNode(this, n)
this.uri = '' + n.uri
if (this.credentials.user || this.credentials.password) {
this.uri = this.uri.replace('://', '://' + encodeURIComponent(this.credentials.user) + ':' + encodeURIComponent(this.credentials.password) + '@')
}
this.name = n.name
this.parallelism = n.parallelism * 1
if (n.options) {
try {
this.options = JSON.parse(n.options)
} catch (err) {
this.error('Failed to parse options: ' + err)
}
}
this.deploymentId = (1 + Math.random() * 0xffffffff).toString(16).replace('.', '')
}, {
'credentials': {
'user': {
'type': 'text'
},
'password': {
'type': 'password'
}
}
})
RED.httpAdmin.get('/mongodb/vcap', function (req, res) {
res.json(services)
})
RED.httpAdmin.get('/mongodb/operations', function (req, res) {
res.json(Object.keys(operations).sort())
})
var mongoPool = {}
function getClient (config) {
var poolCell = mongoPool['#' + config.deploymentId]
if (!poolCell) {
mongoPool['#' + config.deploymentId] = poolCell = {
'instances': 0,
// es6-promise. A client will be called only once.
'promise': mongodb.MongoClient.connect(config.uri, config.options || {}).then(function (db) {
return {
'db': db,
'queue': [],
'parallelOps': 0 // current number of operations
}
})
}
}
poolCell.instances++
return poolCell.promise
}
function closeClient (config) {
var poolCell = mongoPool['#' + config.deploymentId]
if (!poolCell) {
return
}
poolCell.instances--
if (poolCell.instances === 0) {
delete mongoPool['#' + config.deploymentId]
poolCell.promise.then(function (client) {
client.db.close()
}, function () { // ignore error
// db-client was not created in the first place.
})
}
}
RED.nodes.registerType('mongodb in', function Mongo2InputNode (n) {
RED.nodes.createNode(this, n)
this.configNode = n.configNode
this.collection = n.collection
this.operation = n.operation
if (n.service == '_ext_') {
// Refer to the config node's id, uri, options, parallelism and warn function.
this.config = RED.nodes.getNode(this.configNode)
} else if (n.service) {
var configService = appEnv.getService(n.service)
if (configService) {
// Only a uri is defined.
this.config = {
'deploymentId': 'service:' + n.service, // different from node-red deployment ids.
'uri': configService.credentials.uri || configService.credentials.url
}
}
}
if (!this.config || !this.config.uri) {
this.error('missing mongodb configuration')
return
}
var node = this
getClient(node.config).then(function (client) {
var nodeCollection
if (node.collection) {
nodeCollection = client.db.collection(node.collection)
}
var nodeOperation
if (node.operation) {
nodeOperation = operations[node.operation]
}
node.on('input', function (msg) {
if (node.config.parallelism && (node.config.parallelism > 0) && (client.parallelOps >= node.config.parallelism)) {
// msg cannot be handled right now - push to queue.
client.queue.push({
'node_id': node.id,
'msg': msg
})
return
}
client.parallelOps += 1
setImmediate(function () {
handleMessage(msg)
})
})
node.on('mongodb handleMessage', function (msg) {
// see: messageHandlingCompleted
setImmediate(function () {
handleMessage(msg)
})
})
function handleMessage (msg) {
var operation = nodeOperation
if (!operation && msg.operation) {
operation = operations[msg.operation]
}
if (!operation) {
node.error('No operation defined', msg)
return messageHandlingCompleted()
}
var collection // stays undefined in the case of "db" operation.
if (operation != operations.db) {
collection = nodeCollection
if (!collection && msg.collection) {
collection = client.db.collection(msg.collection)
}
if (!collection) {
node.error('No collection defined', msg)
return messageHandlingCompleted()
}
}
delete msg.collection
delete msg.operation
var args = msg.payload
if (!Array.isArray(args)) {
args = [args]
}
if (args.length === 0) {
// All operations can accept one argument (some can accept more).
// Some operations don't expect a single callback argument.
args.push(undefined)
}
if ((operation.length > 0) && (args.length > operation.length - 1)) {
// The operation was defined with arguments, thus it may not
// assume that the last argument is the callback.
// We must not pass too many arguments to the operation.
args = args.slice(0, operation.length - 1)
}
profiling.requests += 1
debounceProfilingStatus()
try {
operation.apply(collection || client.db, args.concat(function (err, response) {
if (err && (forEachIteration != err) && (forEachEnd != err)) {
profiling.error += 1
debounceProfilingStatus()
node.error(err, msg)
return messageHandlingCompleted()
}
if (forEachEnd != err) {
if (response) {
// Some operations return a Connection object with the result.
// Passing this large connection object might be heavy - it will
// be cloned over and over by Node-RED, and there is no reason
// the typical user will need it.
// The mongodb package does not export the Connection prototype-function.
// Instead of loading the Connection prototype-function from the
// internal libs (which might change their path), I use the fact
// that it inherits EventEmitter.
if (response.connection instanceof EventEmitter) {
delete response.connection
}
if (response.result && response.result.connection instanceof EventEmitter) {
delete response.result.connection
}
}
// send msg (when err == forEachEnd, this is just a forEach completion).
if (forEachIteration == err) {
// Clone, so we can send the same message again with a different payload
// in each iteration.
var messageToSend = RED.util.cloneMessage(msg)
messageToSend.payload = response
node.send(messageToSend)
} else {
// No need to clone - the same message will not be sent again.
msg.payload = response
node.send(msg)
}
}
if (forEachIteration != err) {
// clear status
profiling.success += 1
debounceProfilingStatus()
messageHandlingCompleted()
}
}))
} catch (err) {
profiling.error += 1
debounceProfilingStatus()
node.error(err, msg)
return messageHandlingCompleted()
}
}
function messageHandlingCompleted () {
setImmediate(handlePendingMessageOnDemand)
}
function handlePendingMessageOnDemand () {
while (client.queue.length > 0) {
var pendingMessage = client.queue.shift()
var targetNode = RED.nodes.getNode(pendingMessage.node_id)
if (!targetNode) {
// The node was removed before handling the pending message.
// This is just a warning because a similar scenario can happen if
// a node was removed just before handling a message that was sent
// to it.
var warningMessage = 'Node ' + pendingMessage.node_id + ' was removed while having a pending message'
if (node.config.warn) {
// The warning will appear from the config node, because the target
// node cannot be found.
node.config.warn(warningMessage, pendingMessage.msg)
} else {
// If the node was configured with a service instead of a config node,
// the warning will appear from the current node.
// This shouldn't happen in real life because in such scenario
// the parallelism limit is not configured.
node.warn(warningMessage, pendingMessage.msg)
}
continue
}
// Handle the pending message.
if (!targetNode.emit('mongodb handleMessage', pendingMessage.msg)) {
// Safety check - if emit() returned false it means there are no listeners to the event.
// Was the target node closed?
// This shouldn't happen normally, but if it does, we must try to handle the next message in the queue.
var errorMessage = 'Node ' + pendingMessage.node_id + ' could not handle the pending message'
if (node.config.error) {
node.config.error(errorMessage, pendingMessage.msg)
} else {
node.error(errorMessage, pendingMessage.msg)
}
continue
}
// Another message is being handled. The number of parallel ops does not change.
return
}
// The queue is empty. The number of parallel ops has reduced.
if (client.parallelOps <= 0) {
return node.error('Something went wrong with mongodb parallel-ops count')
}
client.parallelOps -= 1
}
}, function (err) {
// Failed to create db client
node.error(err)
})
var profiling = {
'requests': 0,
'success': 0,
'error': 0
}
function profilingStatus () {
node.status({
'fill': 'yellow',
'shape': 'dot',
'text': '' + profiling.requests + ', success: ' + profiling.success + ', error: ' + profiling.error
})
}
var debouncer = null
function debounceProfilingStatus () {
if (debouncer) {
return
}
// show curent status, create debouncer.
profilingStatus()
debouncer = setTimeout(function () {
profilingStatus() // should we call only if there was a change?
debouncer = null
}, 1000)
}
node.on('close', function () {
if (node.config) {
closeClient(node.config)
}
node.removeAllListeners('mongodb handleMessage')
if (debouncer) {
clearTimeout(debouncer)
}
})
})
}