├── .gitignore ├── LICENSE ├── README.md ├── mongodb ├── mongodb.html └── mongodb.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | *.log 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 Awear Solutions Ltd 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-red-contrib-mongodb3 2 | MongoDB node driver 3.0 interface for Node-RED 3 | 4 | based on [node-red-bluemix-nodes](https://github.com/node-red/node-red-bluemix-nodes/tree/master/mongo) and [MongoDB 3 Driver](http://mongodb.github.io/node-mongodb-native/3.0) 5 | 6 | Please refer to the [mongoDB node driver 'Collection' documentation](http://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html) to read about each operation. 7 | 8 | # Usage 9 | **msg.payload** - 10 | * To pass a single parameter to an operation use `msg.payload` as your parameter (eg `{_id: 1243}`) 11 | * To pass multiple parameters to an operation fill `msg.payload` with an array 12 | * If you want to pass a single parameter WHICH IS AN ARRAY (eg as with `InserMany`), wrap your array in an outer array: `msg.payload` = `[[{_id: 1243}, {_id: 2345}]]` 13 | 14 | Passing the last operation parameter (callbak) is not supported, and will be stripped if provided. 15 | 16 | **URI** - 17 | Using a single URI field allows you to specify Host, Port and Database configuration as well as all other features that are supported by the [MongoClient.connect](http://mongodb.github.io/node-mongodb-native/2.1/api/MongoClient.html#.connect), such as Mongos Proxy Connection and Replicaset Server Connection - more information can be found [here](http://mongodb.github.io/node-mongodb-native/2.0/tutorials/connecting). 18 | Notice that the Username & Password fields did remain. They will still be saved as Node-RED credentials (i.e. kept private). If the Username is not empty or the Password is not empty, they will be escaped and added the the URI after the `mongodb://` prefix (separated by ':' and with a '@' after them). You can also leave these fields empty and enter the credentials directly in the URI, following the standard syntax: `mongodb://youruser:yourpassword@host1.yourmongoprovider.com:27017,host2.yourmongoprovider.com:27017/yourdb?replicaSet=foo`. **Do not enter your credentials both in the URI and the Username & Password fields** - this will create an invalid URI such as: `mongodb://youruserfromfield:yourpasswordformfield@youruserfromuri:yourpasswordfromuri@host1.yourmongoprovider.com:27017,host2.yourmongoprovider.com:27017/yourdb?replicaSet=foo`. 19 | 20 | **specifying authentication database** - 21 | most recent deployments of mongoDB store user credentials in a separate databse (usually `admin`) rather than allongside the data in each Db. Therefore you will likley need to provide a `authSource` parameter in your URI 22 | eg: `mongodb://host1.yourmongoprovider.com:27017/yourdb?ssl=true&authSource=admin&replicaSet=foo` 23 | 24 | **Parallelism Limit** - Sending a lot of commands in a short time to the underlying mongodb-native driver, without waiting for their response, may cause serious problems and slow down the whole app. 25 | This has probably something to do with the connection sockets being clogged and their cache getting filled. 26 | This option allows to limit the number of operations that are sent before getting a response. 27 | For example, if the parallelism-limit is set to 5 and we are making 7 operations in a short period of time, the first 5 operations will start immediately, but the 6th and 7th operations will wait in a queue. 28 | The 6th operation will start only when one of the first 5 operations has finished. 29 | Similarly, the 7th operation will start only when another operation has finished. 30 | 31 | **db & collection operations** - These operations will simply pass the db/collection instance, so they can be used directly (for example, in function nodes). 32 | The db instance is the same one that node-red-contrib-mongodb3 caches and shares between all relevant nodes - if you disconnect it, all the other mongodb3 nodes will fail. 33 | Furthermore, the parallelism-limit does not consider the future operations that you will do with the db/collection instances. 34 | However, if there are many parallel operations, requesting the db/collection will block until some of these operations finish. 35 | 36 | # Change Log 37 | ## 2.0 38 | BREAKING CHANGES : driver response props are now (correctly) added to message.payload, thus chaning the response shape 39 | see https://github.com/ozomer/node-red-contrib-mongodb2/issues/34 40 | 41 | `1.0` message shape: 42 | ``` 43 | msg 44 | |_ payload 45 | |_ ok 46 | |_ n 47 | |_ opTime 48 | |_ electionId 49 | |_ operationTime 50 | |_ "$clusterTime 51 | ``` 52 | `2.0` message shape (example for `find()`): 53 | ``` 54 | msg 55 | |_ payload 56 | |_ insertedCount 57 | |_ ops 58 | |_ insertedIds 59 | |_ result 60 | |_ ok 61 | |_ n 62 | |_ opTime 63 | |_ electionId 64 | |_ operationTime 65 | |_ "$clusterTime 66 | 67 | ``` 68 | 69 | ## Original creation 1.0.0 70 | MongoDB 3 driver is originally based on [MongoDB 2 driver node for Node-RED](https://www.npmjs.com/package/node-red-contrib-mongodb2), and therefore is placed in the same github repository: (https://github.com/ozomer/node-red-contrib-mongodb2) 71 | The very-similar MongoDB 2 driver is in the same git repository, under the node-red-contrib-mongodb2 branch (not the master branch). 72 | 73 | -------------------------------------------------------------------------------- /mongodb/mongodb.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 43 | 44 | 75 | 76 | 106 | 107 | 113 | 114 | 209 | -------------------------------------------------------------------------------- /mongodb/mongodb.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Awear Solutions Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | module.exports = function(RED) { 18 | "use strict"; 19 | const EventEmitter = require('events').EventEmitter; 20 | const appEnv = require('cfenv').getAppEnv(); 21 | const mongodb = require('mongodb'); 22 | const forEachIteration = new Error("node-red-contrib-mongodb3 forEach iteration"); 23 | const forEachEnd = new Error("node-red-contrib-mongodb3 forEach end"); 24 | 25 | let services = []; 26 | Object.keys(appEnv.services).forEach(function(label) { 27 | if ((/^mongo/i).test(label)) { 28 | services = services.concat(appEnv.services[label].map(function(service) { 29 | return { 30 | "name": service.name, 31 | "label": service.label 32 | }; 33 | })); 34 | } 35 | }); 36 | 37 | const operations = {}; 38 | Object.keys(mongodb.Collection.prototype).forEach(function(operationName) { 39 | if ('function' == typeof Object.getOwnPropertyDescriptor(mongodb.Collection.prototype, operationName).value) { 40 | operations[operationName] = mongodb.Collection.prototype[operationName]; 41 | } 42 | }); 43 | // We don't want to pass the find-operation's cursor directly. 44 | delete operations.find; 45 | 46 | operations['find.toArray'] = function() { 47 | const args = Array.prototype.slice.call(arguments, 0); 48 | const callback = args.pop(); 49 | mongodb.Collection.prototype.find.apply(this, args).toArray(callback); 50 | }; 51 | operations['find.forEach'] = function() { 52 | const args = Array.prototype.slice.call(arguments, 0); 53 | const callback = args.pop(); 54 | mongodb.Collection.prototype.find.apply(this, args).forEach(function(doc) { 55 | return callback(forEachIteration, doc); 56 | }, function(err) { 57 | return callback(err || forEachEnd); 58 | }); 59 | }; 60 | 61 | // We don't want to pass the aggregate's cursor directly. 62 | delete operations.aggregate; 63 | operations['aggregate.toArray'] = function() { 64 | const args = Array.prototype.slice.call(arguments, 0); 65 | const callback = args.pop(); 66 | mongodb.Collection.prototype.aggregate.apply(this, args).toArray(callback); 67 | }; 68 | operations['aggregate.forEach'] = function() { 69 | const args = Array.prototype.slice.call(arguments, 0); 70 | const callback = args.pop(); 71 | mongodb.Collection.prototype.aggregate.apply(this, args).forEach(function(doc) { 72 | return callback(forEachIteration, doc); 73 | }, function(err) { 74 | return callback(err || forEachEnd); 75 | }); 76 | }; 77 | 78 | // We don't want to pass the listIndexes's cursor directly. 79 | delete operations.listIndexes; 80 | operations['listIndexes.toArray'] = function() { 81 | const args = Array.prototype.slice.call(arguments, 0); 82 | const callback = args.pop(); 83 | mongodb.Collection.prototype.listIndexes.apply(this, args).toArray(callback); 84 | }; 85 | operations['listIndexes.forEach'] = function() { 86 | const args = Array.prototype.slice.call(arguments, 0); 87 | const callback = args.pop(); 88 | mongodb.Collection.prototype.listIndexes.apply(this, args).forEach(function(doc) { 89 | return callback(forEachIteration, doc); 90 | }, function(err) { 91 | return callback(err || forEachEnd); 92 | }); 93 | }; 94 | 95 | // We don't want to pass the listCollections's cursor directly. 96 | delete operations.listCollections; 97 | operations['db.listCollections.toArray'] = function() { 98 | const args = Array.prototype.slice.call(arguments, 0); 99 | const callback = args.pop(); 100 | mongodb.Db.prototype.listCollections.apply(this, args).toArray(callback); 101 | }; 102 | operations['db.listCollections.forEach'] = function() { 103 | const args = Array.prototype.slice.call(arguments, 0); 104 | const callback = args.pop(); 105 | mongodb.Db.prototype.listCollections.apply(this, args).forEach( 106 | function(doc) { 107 | return callback(forEachIteration, doc); 108 | }, 109 | function(err) { 110 | return callback(err || forEachEnd); 111 | } 112 | ); 113 | }; 114 | 115 | operations.db = function(callback) { 116 | return callback(null, this); 117 | }; 118 | 119 | operations.collection = function(callback) { 120 | return callback(null, this); 121 | }; 122 | 123 | RED.nodes.registerType("mongodb3", function MongoConfigNode(n) { 124 | RED.nodes.createNode(this, n); 125 | this.uri = '' + n.uri; 126 | if (this.credentials.user || this.credentials.password) { 127 | this.uri = this.uri.replace(/^mongodb:\/\//, 'mongodb://' + encodeURIComponent(this.credentials.user) + ':' + encodeURIComponent(this.credentials.password) + '@'); 128 | } 129 | this.name = n.name; 130 | this.parallelism = n.parallelism * 1; 131 | if (!!n.options) { 132 | try { 133 | this.options = JSON.parse(n.options); 134 | } catch (err) { 135 | this.error("Failed to parse options: " + err); 136 | } 137 | } 138 | this.deploymentId = (1 + Math.random() * 0xffffffff).toString(16).replace('.', ''); 139 | }, { 140 | "credentials": { 141 | "user": { 142 | "type": "text" 143 | }, 144 | "password": { 145 | "type": "password" 146 | } 147 | } 148 | }); 149 | 150 | RED.httpAdmin.get('/mongodb3/vcap', function(req, res) { 151 | res.json(services); 152 | }); 153 | 154 | RED.httpAdmin.get('/mongodb3/operations', function(req, res) { 155 | res.json(Object.keys(operations).sort()); 156 | }); 157 | 158 | const mongoPool = {}; 159 | 160 | function getClient(config) { 161 | let poolCell = mongoPool['#' + config.deploymentId]; 162 | if (!poolCell) { 163 | mongoPool['#' + config.deploymentId] = poolCell = { 164 | "instances": 0, 165 | // es6-promise. A client will be called only once. 166 | "promise": mongodb.MongoClient.connect(config.uri, config.options || {}).then(function(client) { 167 | const dbName = decodeURIComponent((config.uri.match(/^.*\/([^?]*)\??.*$/) || [])[1] || ''); 168 | const db = client.db(dbName); 169 | return { 170 | "client": client, 171 | "db": db, 172 | "queue": [], 173 | "parallelOps": 0 // current number of operations 174 | }; 175 | }) 176 | }; 177 | } 178 | poolCell.instances++; 179 | return poolCell.promise; 180 | } 181 | 182 | function closeClient(config) { 183 | const poolCell = mongoPool['#' + config.deploymentId]; 184 | if (!poolCell) { 185 | return; 186 | } 187 | poolCell.instances--; 188 | if (poolCell.instances === 0) { 189 | delete mongoPool['#' + config.deploymentId]; 190 | poolCell.promise.then(function(client) { 191 | client.client.close().catch(function(err) { 192 | node.error("Error while closing client: " + err); 193 | }); 194 | }, function() { // ignore error 195 | // db-client was not created in the first place. 196 | }); 197 | } 198 | } 199 | 200 | RED.nodes.registerType("mongodb3 in", function MongoInputNode(n) { 201 | RED.nodes.createNode(this, n); 202 | this.configNode = n.configNode; 203 | this.collection = n.collection; 204 | this.operation = n.operation; 205 | if (n.service == "_ext_") { 206 | // Refer to the config node's id, uri, options, parallelism and warn function. 207 | this.config = RED.nodes.getNode(this.configNode); 208 | } else if (n.service) { 209 | const configService = appEnv.getService(n.service); 210 | if (configService) { 211 | // Only a uri is defined. 212 | this.config = { 213 | "deploymentId": 'service:' + n.service, // different from node-red deployment ids. 214 | "uri": configService.credentials.uri || configService.credentials.url 215 | }; 216 | } 217 | } 218 | if (!this.config || !this.config.uri) { 219 | this.error("missing mongodb3 configuration"); 220 | return; 221 | } 222 | const node = this; 223 | getClient(node.config).then(function(client) { 224 | let nodeCollection; 225 | if (node.collection) { 226 | nodeCollection = client.db.collection(node.collection); 227 | } 228 | let nodeOperation; 229 | if (node.operation) { 230 | nodeOperation = operations[node.operation]; 231 | } 232 | node.on('input', function(msg) { 233 | if (node.config.parallelism && (node.config.parallelism > 0) && (client.parallelOps >= node.config.parallelism)) { 234 | // msg cannot be handled right now - push to queue. 235 | client.queue.push({ 236 | "node_id": node.id, 237 | "msg": msg 238 | }); 239 | return; 240 | } 241 | client.parallelOps += 1; 242 | setImmediate(function() { 243 | handleMessage(msg); 244 | }); 245 | }); 246 | 247 | node.on('node-red-contrib-mongodb3 handleMessage', function(msg) { 248 | // see: messageHandlingCompleted 249 | setImmediate(function(){ 250 | handleMessage(msg); 251 | }); 252 | }); 253 | 254 | function handleMessage(msg) { 255 | let operation = nodeOperation; 256 | if (!operation && msg.operation) { 257 | operation = operations[msg.operation]; 258 | } 259 | if (!operation) { 260 | node.error("No operation defined", msg); 261 | return messageHandlingCompleted(); 262 | } 263 | let collection; // stays undefined in the case of "db" operation. 264 | if ( 265 | operation != operations.db && 266 | operation != operations['db.listCollections.toArray'] && 267 | operation != operations['db.listCollections.forEach'] 268 | ) { 269 | collection = nodeCollection; 270 | if (!collection && msg.collection) { 271 | collection = client.db.collection(msg.collection); 272 | } 273 | if (!collection) { 274 | node.error("No collection defined", msg); 275 | return messageHandlingCompleted(); 276 | } 277 | } 278 | 279 | delete msg.collection; 280 | delete msg.operation; 281 | let args = msg.payload; 282 | if (!Array.isArray(args)) { 283 | args = [args]; 284 | } 285 | if (args.length === 0) { 286 | // All operations can accept one argument (some can accept more). 287 | // Some operations don't expect a single callback argument. 288 | args.push(undefined); 289 | } 290 | if ((operation.length > 0) && (args.length > operation.length - 1)) { 291 | // The operation was defined with arguments, thus it may not 292 | // assume that the last argument is the callback. 293 | // We must not pass too many arguments to the operation. 294 | args = args.slice(0, operation.length - 1); 295 | } 296 | profiling.requests += 1; 297 | debounceProfilingStatus(); 298 | try { 299 | operation.apply(collection || client.db, args.concat(function(err, response) { 300 | if (err && (forEachIteration != err) && (forEachEnd != err)) { 301 | profiling.error += 1; 302 | debounceProfilingStatus(); 303 | node.error(err, msg); 304 | return messageHandlingCompleted(); 305 | } 306 | if (forEachEnd != err) { 307 | if (!!response) { 308 | // Some operations return a Connection object with the result. 309 | // Passing this large connection object might be heavy - it will 310 | // be cloned over and over by Node-RED, and there is no reason 311 | // the typical user will need it. 312 | // The mongodb package does not export the Connection prototype-function. 313 | // Instead of loading the Connection prototype-function from the 314 | // internal libs (which might change their path), I use the fact 315 | // that it inherits EventEmitter. 316 | if (response.connection instanceof EventEmitter) { 317 | delete response.connection; 318 | } 319 | if (response.result && response.result.connection instanceof EventEmitter) { 320 | delete response.result.connection; 321 | } 322 | } 323 | // `response` is an instance of CommandResult, and does not seem to have the standard Object methods, 324 | // which means that some props are not correctly being forwarded to msg.payload (eg "ops" ouputted from `insertOne`) 325 | // cloning the object fixes that. 326 | response = Object.assign({}, response); 327 | // response.message includes info about the DB op, but is large and never used (like the connection) 328 | delete response.message; 329 | 330 | // send msg (when err == forEachEnd, this is just a forEach completion). 331 | if (forEachIteration == err) { 332 | // Clone, so we can send the same message again with a different payload 333 | // in each iteration. 334 | const messageToSend = RED.util.cloneMessage(msg); 335 | messageToSend.payload = response; 336 | node.send(messageToSend); 337 | } else { 338 | // No need to clone - the same message will not be sent again. 339 | msg.payload = response; 340 | node.send(msg); 341 | } 342 | } 343 | if (forEachIteration != err) { 344 | // clear status 345 | profiling.success += 1; 346 | debounceProfilingStatus(); 347 | messageHandlingCompleted(); 348 | } 349 | })); 350 | } catch(err) { 351 | profiling.error += 1; 352 | debounceProfilingStatus(); 353 | node.error(err, msg); 354 | return messageHandlingCompleted(); 355 | } 356 | } 357 | function messageHandlingCompleted() { 358 | setImmediate(handlePendingMessageOnDemand); 359 | } 360 | function handlePendingMessageOnDemand() { 361 | while (client.queue.length > 0) { 362 | const pendingMessage = client.queue.shift(); 363 | const targetNode = RED.nodes.getNode(pendingMessage.node_id); 364 | if (!targetNode) { 365 | // The node was removed before handling the pending message. 366 | // This is just a warning because a similar scenario can happen if 367 | // a node was removed just before handling a message that was sent 368 | // to it. 369 | const warningMessage = "Node " + pendingMessage.node_id + " was removed while having a pending message"; 370 | if (node.config.warn) { 371 | // The warning will appear from the config node, because the target 372 | // node cannot be found. 373 | node.config.warn(warningMessage, pendingMessage.msg); 374 | } else { 375 | // If the node was configured with a service instead of a config node, 376 | // the warning will appear from the current node. 377 | // This shouldn't happen in real life because in such scenario 378 | // the parallelism limit is not configured. 379 | node.warn(warningMessage, pendingMessage.msg); 380 | } 381 | continue; 382 | } 383 | // Handle the pending message. 384 | if (!targetNode.emit('node-red-contrib-mongodb3 handleMessage', pendingMessage.msg)) { 385 | // Safety check - if emit() returned false it means there are no listeners to the event. 386 | // Was the target node closed? 387 | // This shouldn't happen normally, but if it does, we must try to handle the next message in the queue. 388 | const errorMessage = "Node " + pendingMessage.node_id + " could not handle the pending message"; 389 | if (node.config.error) { 390 | node.config.error(errorMessage, pendingMessage.msg); 391 | } else { 392 | node.error(errorMessage, pendingMessage.msg); 393 | } 394 | continue; 395 | } 396 | // Another message is being handled. The number of parallel ops does not change. 397 | return; 398 | } 399 | // The queue is empty. The number of parallel ops has reduced. 400 | if (client.parallelOps <= 0) { 401 | return node.error("Something went wrong with node-red-contrib-mongodb3 parallel-ops count"); 402 | } 403 | client.parallelOps -= 1; 404 | } 405 | }, function(err) { 406 | // Failed to create db client 407 | node.error(err); 408 | }); 409 | const profiling = { 410 | "requests": 0, 411 | "success": 0, 412 | "error": 0 413 | }; 414 | function profilingStatus() { 415 | node.status({ 416 | "fill": "yellow", 417 | "shape": "dot", 418 | "text": "" + profiling.requests + ", success: " + profiling.success + ", error: " + profiling.error 419 | }); 420 | } 421 | 422 | let debouncer = null; 423 | function debounceProfilingStatus() { 424 | if (debouncer) { 425 | return; 426 | } 427 | // show curent status, create debouncer. 428 | profilingStatus(); 429 | debouncer = setTimeout(function() { 430 | profilingStatus(); // should we call only if there was a change? 431 | debouncer = null; 432 | }, 1000); 433 | } 434 | node.on('close', function() { 435 | if (node.config) { 436 | closeClient(node.config); 437 | } 438 | node.removeAllListeners('node-red-contrib-mongodb3 handleMessage'); 439 | if (debouncer) { 440 | clearTimeout(debouncer); 441 | } 442 | }); 443 | }); 444 | }; 445 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-red-contrib-mongodb3", 3 | "version": "2.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "argparse": { 8 | "version": "1.0.10", 9 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 10 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 11 | "requires": { 12 | "sprintf-js": "~1.0.2" 13 | } 14 | }, 15 | "bson": { 16 | "version": "1.1.0", 17 | "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.0.tgz", 18 | "integrity": "sha512-9Aeai9TacfNtWXOYarkFJRW2CWo+dRon+fuLZYJmvLV3+MiUp0bEI6IAZfXEIg7/Pl/7IWlLaDnhzTsD81etQA==" 19 | }, 20 | "cfenv": { 21 | "version": "1.0.4", 22 | "resolved": "https://registry.npmjs.org/cfenv/-/cfenv-1.0.4.tgz", 23 | "integrity": "sha1-uXoe694lXs7YNnoPSvvC+FQ44LQ=", 24 | "requires": { 25 | "js-yaml": "3.7.x", 26 | "ports": "1.1.x", 27 | "underscore": "1.8.x" 28 | } 29 | }, 30 | "esprima": { 31 | "version": "2.7.3", 32 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", 33 | "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=" 34 | }, 35 | "js-yaml": { 36 | "version": "3.7.0", 37 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz", 38 | "integrity": "sha1-XJZ93YN6m/3KXy3oQlOr6KHAO4A=", 39 | "requires": { 40 | "argparse": "^1.0.7", 41 | "esprima": "^2.6.0" 42 | } 43 | }, 44 | "memory-pager": { 45 | "version": "1.4.0", 46 | "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.4.0.tgz", 47 | "integrity": "sha512-ycuyV5gKpZln7HB/A11wCpAxEY9VQ2EhYU1F56pUAxvmj6OyOHtB9tkLLjAyFsPdghSP2S3Ujk3aYJCusgiMZg==", 48 | "optional": true 49 | }, 50 | "mongodb": { 51 | "version": "3.1.10", 52 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.1.10.tgz", 53 | "integrity": "sha512-Uml42GeFxhTGQVml1XQ4cD0o/rp7J2ROy0fdYUcVitoE7vFqEhKH4TYVqRDpQr/bXtCJVxJdNQC1ntRxNREkPQ==", 54 | "requires": { 55 | "mongodb-core": "3.1.9", 56 | "safe-buffer": "^5.1.2" 57 | } 58 | }, 59 | "mongodb-core": { 60 | "version": "3.1.9", 61 | "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.1.9.tgz", 62 | "integrity": "sha512-MJpciDABXMchrZphh3vMcqu8hkNf/Mi+Gk6btOimVg1XMxLXh87j6FAvRm+KmwD1A9fpu3qRQYcbQe4egj23og==", 63 | "requires": { 64 | "bson": "^1.1.0", 65 | "require_optional": "^1.0.1", 66 | "safe-buffer": "^5.1.2", 67 | "saslprep": "^1.0.0" 68 | } 69 | }, 70 | "ports": { 71 | "version": "1.1.0", 72 | "resolved": "https://registry.npmjs.org/ports/-/ports-1.1.0.tgz", 73 | "integrity": "sha1-twGqKF6V2ujJbNonUhdySh9/bGA=" 74 | }, 75 | "require_optional": { 76 | "version": "1.0.1", 77 | "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", 78 | "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", 79 | "requires": { 80 | "resolve-from": "^2.0.0", 81 | "semver": "^5.1.0" 82 | } 83 | }, 84 | "resolve-from": { 85 | "version": "2.0.0", 86 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", 87 | "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" 88 | }, 89 | "safe-buffer": { 90 | "version": "5.1.2", 91 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 92 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 93 | }, 94 | "saslprep": { 95 | "version": "1.0.2", 96 | "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.2.tgz", 97 | "integrity": "sha512-4cDsYuAjXssUSjxHKRe4DTZC0agDwsCqcMqtJAQPzC74nJ7LfAJflAtC1Zed5hMzEQKj82d3tuzqdGNRsLJ4Gw==", 98 | "optional": true, 99 | "requires": { 100 | "sparse-bitfield": "^3.0.3" 101 | } 102 | }, 103 | "semver": { 104 | "version": "5.6.0", 105 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", 106 | "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" 107 | }, 108 | "sparse-bitfield": { 109 | "version": "3.0.3", 110 | "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", 111 | "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", 112 | "optional": true, 113 | "requires": { 114 | "memory-pager": "^1.0.2" 115 | } 116 | }, 117 | "sprintf-js": { 118 | "version": "1.0.3", 119 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 120 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" 121 | }, 122 | "underscore": { 123 | "version": "1.8.3", 124 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", 125 | "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-red-contrib-mongodb3", 3 | "version": "2.0.1", 4 | "description": "MongoDB 3 driver node for Node-RED", 5 | "dependencies": { 6 | "cfenv": "^1.0.4", 7 | "mongodb": "^3.0.11" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/ozomer/node-red-contrib-mongodb2" 12 | }, 13 | "author": { 14 | "name": "Oren Zomer", 15 | "email": "oren@awearsolutions.com" 16 | }, 17 | "license": "Apache-2.0", 18 | "keywords": [ 19 | "node-red", 20 | "bluemix", 21 | "mongodb" 22 | ], 23 | "node-red": { 24 | "nodes": { 25 | "mongodb": "mongodb/mongodb.js" 26 | } 27 | }, 28 | "jshintConfig": { 29 | "undef": true, 30 | "unused": true, 31 | "node": true 32 | } 33 | } 34 | --------------------------------------------------------------------------------