├── .gitignore ├── nodes ├── api │ ├── icons │ │ ├── particle.png │ │ └── particle@2x.png │ ├── api.js │ └── api.html ├── publish │ ├── icons │ │ ├── particle.png │ │ └── particle@2x.png │ ├── publish.js │ └── publish.html ├── function │ ├── icons │ │ ├── particle.png │ │ └── particle@2x.png │ ├── function.js │ └── function.html ├── subscribe │ ├── icons │ │ ├── particle.png │ │ └── particle@2x.png │ ├── subscribe.js │ └── subscribe.html ├── variable │ ├── icons │ │ ├── particle.png │ │ └── particle@2x.png │ ├── variable.js │ └── variable.html └── config │ ├── config.js │ ├── config.html │ └── helpers.js ├── images └── red-subscribe-publish.gif ├── README.md ├── src ├── helpers.js ├── api.js └── particle-base-node.js ├── package.json └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /nodes/api/icons/particle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/node-red-contrib-particle-official/HEAD/nodes/api/icons/particle.png -------------------------------------------------------------------------------- /images/red-subscribe-publish.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/node-red-contrib-particle-official/HEAD/images/red-subscribe-publish.gif -------------------------------------------------------------------------------- /nodes/api/icons/particle@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/node-red-contrib-particle-official/HEAD/nodes/api/icons/particle@2x.png -------------------------------------------------------------------------------- /nodes/publish/icons/particle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/node-red-contrib-particle-official/HEAD/nodes/publish/icons/particle.png -------------------------------------------------------------------------------- /nodes/function/icons/particle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/node-red-contrib-particle-official/HEAD/nodes/function/icons/particle.png -------------------------------------------------------------------------------- /nodes/publish/icons/particle@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/node-red-contrib-particle-official/HEAD/nodes/publish/icons/particle@2x.png -------------------------------------------------------------------------------- /nodes/subscribe/icons/particle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/node-red-contrib-particle-official/HEAD/nodes/subscribe/icons/particle.png -------------------------------------------------------------------------------- /nodes/variable/icons/particle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/node-red-contrib-particle-official/HEAD/nodes/variable/icons/particle.png -------------------------------------------------------------------------------- /nodes/function/icons/particle@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/node-red-contrib-particle-official/HEAD/nodes/function/icons/particle@2x.png -------------------------------------------------------------------------------- /nodes/subscribe/icons/particle@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/node-red-contrib-particle-official/HEAD/nodes/subscribe/icons/particle@2x.png -------------------------------------------------------------------------------- /nodes/variable/icons/particle@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/node-red-contrib-particle-official/HEAD/nodes/variable/icons/particle@2x.png -------------------------------------------------------------------------------- /nodes/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (RED) => { 4 | function ParticleConfigNode(n) { 5 | RED.nodes.createNode(this, n); 6 | this.clientId = n.clientId; 7 | this.clientSecret = n.clientSecret; 8 | } 9 | 10 | RED.nodes.registerType('particle-config', ParticleConfigNode); 11 | 12 | }; 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node-RED Particle nodes 2 | 3 |  4 | 5 | ## Installation 6 | 7 | ```bash 8 | $ cd ~/.node-red 9 | $ npm install @particle/node-red-contrib-particle-official 10 | ``` 11 | 12 | ## Version History 13 | 14 | #### 0.1.7 (2021-05-13) 15 | 16 | - Upgrade lodash from 4.17.19 to 4.17.21 for security vulnerability fix 17 | 18 | #### 0.1.6 (2020-11-30) 19 | 20 | - Fixed a bug where a subscribe node would keep using the same value it received on the first event, even if the value changed. 21 | "lodash": "^4.17.19", 22 | "lodash": "^4.17.21", -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const resetStatus = ({ status, timeout = 0 }) => { 4 | setTimeout(() => { 5 | status({}); 6 | }, timeout); 7 | }; 8 | 9 | const onSuccess = ({ status, message }) => { 10 | status({ fill: 'green', shape: 'dot', text: message }); 11 | 12 | resetStatus({ 13 | status, 14 | timeout: 1000 15 | }); 16 | }; 17 | 18 | const onInfo = ({ status, message }) => { 19 | status({ fill: 'blue', shape: 'dot', text: message }); 20 | }; 21 | 22 | const onError = ({ status, message }) => { 23 | status({ fill: 'red', shape: 'ring', text: message }); 24 | }; 25 | 26 | module.exports = { 27 | onSuccess, 28 | onInfo, 29 | onError, 30 | resetStatus 31 | }; 32 | -------------------------------------------------------------------------------- /nodes/publish/publish.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ParticleBaseNode = require('../../src/particle-base-node'); 4 | 5 | module.exports = (RED) => { 6 | function ParticlePublishNode(node) { 7 | RED.nodes.createNode(this, node); 8 | 9 | new ParticleBaseNode({ 10 | self: this, 11 | node: node, 12 | RED: RED, 13 | properties: ['name', 'scope', 'product', 'event', 'payload'], 14 | inputProperties: ['event', 'payload'], 15 | functionName: 'publishEvent', 16 | functionArguments: [ 17 | { key: 'name', value: 'event' }, 18 | { key: 'product', value: 'product' }, 19 | { key: 'data', value: 'payload' }, 20 | { key: 'isPrivate', defaultValue: true } 21 | ], 22 | mandatoryArguments: ['name'], 23 | success: { 24 | status: 'published' 25 | }, 26 | info: { 27 | status: 'publishing' 28 | }, 29 | error: { 30 | status: 'failed', 31 | onRuntime: 'Failed to publish event' 32 | } 33 | }); 34 | } 35 | 36 | RED.nodes.registerType('particle-publish', ParticlePublishNode); 37 | 38 | }; 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@particle/node-red-contrib-particle-official", 3 | "version": "0.1.7", 4 | "description": "Official Node-RED Particle nodes", 5 | "scripts": {}, 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/particle-iot/node-red-contrib-particle-official.git" 9 | }, 10 | "keywords": [ 11 | "node-red" 12 | ], 13 | "node-red": { 14 | "nodes": { 15 | "config": "nodes/config/config.js", 16 | "publish": "nodes/publish/publish.js", 17 | "subscribe": "nodes/subscribe/subscribe.js", 18 | "variable": "nodes/variable/variable.js", 19 | "function": "nodes/function/function.js", 20 | "api": "nodes/api/api.js" 21 | } 22 | }, 23 | "author": "Wojtek 'suda' Siudzinski", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/particle-iot/node-red-contrib-particle-official/issues" 27 | }, 28 | "homepage": "https://github.com/particle-iot/node-red-contrib-particle-official#readme", 29 | "dependencies": { 30 | "lodash": "^4.17.21", 31 | "particle-api-js": "^9.0.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Particle Industries 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /nodes/variable/variable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ParticleBaseNode = require('../../src/particle-base-node'); 4 | 5 | module.exports = (RED) => { 6 | function ParticleVariableNode(node) { 7 | RED.nodes.createNode(this, node); 8 | 9 | new ParticleBaseNode({ 10 | self: this, 11 | node: node, 12 | RED: RED, 13 | properties: ['name', 'scope', 'device', 'product', 'variable'], 14 | inputProperties: ['device', 'variable'], 15 | functionName: 'getVariable', 16 | functionArguments: [ 17 | { key: 'deviceId', value: 'device' }, 18 | { key: 'name', value: 'variable' }, 19 | { key: 'product', value: 'product' } 20 | ], 21 | mandatoryArguments: ['deviceId', 'name'], 22 | success: { 23 | fields: [ 24 | { key: 'payload' }, 25 | { key: 'variable', value: 'variable' }, 26 | { key: 'device', value: 'device' } 27 | ], 28 | status: 'fetched' 29 | }, 30 | info: { 31 | status: 'fetching' 32 | }, 33 | error: { 34 | status: 'failed', 35 | onRuntime: 'Failed to fetch varaible' 36 | } 37 | }); 38 | } 39 | 40 | RED.nodes.registerType('particle-variable', ParticleVariableNode); 41 | 42 | 43 | }; 44 | -------------------------------------------------------------------------------- /nodes/function/function.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ParticleBaseNode = require('../../src/particle-base-node'); 4 | 5 | module.exports = (RED) => { 6 | function ParticleFunctionNode(node) { 7 | RED.nodes.createNode(this, node); 8 | 9 | new ParticleBaseNode({ 10 | self: this, 11 | node: node, 12 | RED: RED, 13 | properties: ['name', 'scope', 'device', 'product', 'function', 'argument'], 14 | inputProperties: ['device', 'function', 'argument'], 15 | functionName: 'callFunction', 16 | functionArguments: [ 17 | { key: 'deviceId', value: 'device' }, 18 | { key: 'name', value: 'function' }, 19 | { key: 'argument', value: 'argument' }, 20 | { key: 'product', value: 'product' } 21 | ], 22 | mandatoryArguments: ['deviceId', 'name'], 23 | success: { 24 | fields: [ 25 | { key: 'payload' }, 26 | { key: 'function', value: 'function' }, 27 | { key: 'device', value: 'device' } 28 | ], 29 | status: 'called' 30 | }, 31 | info: { 32 | status: 'calling' 33 | }, 34 | error: { 35 | status: 'failed', 36 | onRuntime: 'Failed to call function' 37 | } 38 | }); 39 | } 40 | 41 | RED.nodes.registerType('particle-function', ParticleFunctionNode); 42 | 43 | }; 44 | -------------------------------------------------------------------------------- /nodes/config/config.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 32 | -------------------------------------------------------------------------------- /nodes/api/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ParticleBaseNode = require('../../src/particle-base-node'); 4 | 5 | module.exports = (RED) => { 6 | function ParticleAPINode(node) { 7 | RED.nodes.createNode(this, node); 8 | 9 | new ParticleBaseNode({ 10 | self: this, 11 | node: node, 12 | RED: RED, 13 | properties: ['name', 'method', 'url', 'payload'], 14 | inputProperties: ['method', 'url', 'payload'], 15 | functionName: 'request', 16 | functionArguments: [ 17 | { key: 'uri', value: 'url', prefix: 'https://api.particle.io/', customValue: ({ url }) => { 18 | if (url[0] === '/') { 19 | url = url.substring(1); 20 | } 21 | 22 | return url; 23 | } }, 24 | { key: 'method', value: 'method' }, 25 | { key: 'data', value: 'payload', format: 'object' } 26 | ], 27 | mandatoryArguments: ['uri', 'method'], 28 | success: { 29 | fields: [ 30 | { key: 'payload', path: 'body' }, 31 | { key: 'statusCode', path: 'statusCode' } 32 | ], 33 | status: 'got response' 34 | }, 35 | info: { 36 | status: 'requesting' 37 | }, 38 | error: { 39 | status: 'failed', 40 | onRuntime: 'Failed to call endpoint', 41 | onConfig: 'Configuration error' 42 | } 43 | }); 44 | } 45 | 46 | RED.nodes.registerType('particle-api', ParticleAPINode); 47 | 48 | }; 49 | -------------------------------------------------------------------------------- /nodes/subscribe/subscribe.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const helpers = require('../../src/helpers'); 4 | const ParticleBaseNode = require('../../src/particle-base-node'); 5 | 6 | module.exports = (RED) => { 7 | function ParticleSubscribeNode(node) { 8 | RED.nodes.createNode(this, node); 9 | 10 | new ParticleBaseNode({ 11 | self: this, 12 | node: node, 13 | RED: RED, 14 | properties: ['name', 'scope', 'device', 'product', 'event'], 15 | functionName: 'listenToEventStream', 16 | functionArguments: [ 17 | { key: 'deviceId', customValue: ({ device, product }) => { 18 | if (product) { 19 | return device === '' ? undefined : device; 20 | } else { 21 | return device === '' ? 'mine' : device; 22 | } 23 | } }, 24 | { key: 'name', value: 'event' }, 25 | { key: 'product', value: 'product' }, 26 | { key: 'onEvent', defaultValue: (data) => { 27 | helpers.onSuccess({ status: this.status.bind(this), message: 'new event' }); 28 | 29 | this.send({ 30 | event: data.name, 31 | payload: data.data, 32 | published_at: data.published_at, 33 | device: data.coreid 34 | }); 35 | } } 36 | ], 37 | mandatoryArguments: [], 38 | runOnLoad: true, 39 | info: { 40 | status: 'setting up stream' 41 | }, 42 | error: { 43 | status: 'failed to set up stream', 44 | onRuntime: 'Failed to set up stream' 45 | } 46 | }); 47 | } 48 | 49 | RED.nodes.registerType('particle-subscribe', ParticleSubscribeNode); 50 | 51 | }; 52 | -------------------------------------------------------------------------------- /src/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Particle = require('particle-api-js'); 4 | 5 | module.exports = class Api { 6 | /** 7 | * API wrapper constructor 8 | * 9 | * @param {ParticleConfigNode} auth 10 | * @param {Console} logger 11 | */ 12 | constructor(auth, logger=console) { 13 | this._auth = auth; 14 | this._logger = logger; 15 | this._defaultExpiresIn = 2147483647; 16 | this._init(); 17 | } 18 | 19 | /** 20 | * Login using the client credentials, store the access token 21 | * and set up token refresh before it expires 22 | * 23 | * @returns {Promise} Login promise 24 | */ 25 | login() { 26 | return this._particle.loginAsClientOwner({}).then((res) => { 27 | this._logger.log('Authenticated with Particle'); 28 | this._accessToken = res.body.access_token; 29 | // Reauthenticate before the token expires 30 | // There is res.body.expires_in property but 31 | // as setTimeout is using 32 bit int the maximum 32 | // we can set is 2147483647 (~24 days) 33 | const expiresIn = Math.min(this._defaultExpiresIn, res.body.expires_in); 34 | this._expirationTimer = setTimeout( 35 | this._reauthenticate.bind(this), 36 | expiresIn 37 | ); 38 | 39 | return this._accessToken; 40 | }); 41 | } 42 | 43 | /** 44 | * Get the EventStream and subscribe to the 'event' event 45 | * with onEvent callback 46 | * 47 | * @param {Function} onEvent 48 | * @param {String} options.deviceId 49 | * @param {String} options.name 50 | * @param {String} options.product 51 | * @returns {Promise} Resolves when adding listener succeeded 52 | */ 53 | listenToEventStream({ onEvent, deviceId, name, product }) { 54 | this._lastListenArguments = arguments; 55 | return this._particle.getEventStream({ 56 | deviceId, name, product, auth: this._accessToken 57 | }).then((stream) => { 58 | this._stream = stream; 59 | this._stream.on('event', onEvent); 60 | this._stream.on('error', this._reauthenticate.bind(this)); 61 | this._stream.on('end', this._reauthenticate.bind(this)); 62 | }); 63 | } 64 | 65 | /** 66 | * Publish a Particle event 67 | * 68 | * @param {Object} params Event params 69 | * @param {String} params.name Event name 70 | * @param {String} params.data Event data 71 | * @param {String} params.product Event for this product ID or slug 72 | * @param {Boolean} params.isPrivate Should the event be publicly available? 73 | * @returns {Promise} Resolves when event has been published 74 | */ 75 | publishEvent(params) { 76 | return this._particle.publishEvent(Object.assign({ 77 | auth: this._accessToken 78 | }, params)); 79 | } 80 | 81 | /** 82 | * Get the value of a device variable 83 | * @param {Object} params Options for this API call 84 | * @param {String} params.deviceId Device ID or Name 85 | * @param {String} params.name Variable name 86 | * @param {String} [params.product] Device in this product ID or slug 87 | * @param {String} params.auth Access Token 88 | * @return {Promise} Resolves when the variable was fetched 89 | */ 90 | getVariable(params) { 91 | return this._particle.getVariable(Object.assign({ 92 | auth: this._accessToken 93 | }, params)) 94 | .then(response => { 95 | return response.body.result; 96 | }, (error) => { 97 | throw error; 98 | }); 99 | } 100 | 101 | /** 102 | * Call a device function 103 | * @param {Object} options Options for this API call 104 | * @param {String} options.deviceId Device ID or Name 105 | * @param {String} options.name Function name 106 | * @param {String} options.argument Function argument 107 | * @param {String} [options.product] Device in this product ID or slug 108 | * @param {String} options.auth Access Token 109 | * @return {Promise} Resolves when the function was called 110 | */ 111 | callFunction(options) { 112 | return this._particle.callFunction(Object.assign({ 113 | auth: this._accessToken 114 | }, options)) 115 | .then(response => { 116 | return response.body.return_value; 117 | }, (error) => { 118 | throw error; 119 | }); 120 | } 121 | 122 | request(params) { 123 | return this._particle.request(Object.assign({ 124 | auth: this._accessToken 125 | }, params)); 126 | } 127 | 128 | /** 129 | * Close the stream, remove all timers, listeners etc. 130 | */ 131 | cleanup() { 132 | clearTimeout(this._expirationTimer); 133 | 134 | if (this._stream) { 135 | this._stream.abort(); 136 | } 137 | 138 | this._particle.deleteCurrentAccessToken({ 139 | auth: this._accessToken 140 | }); 141 | } 142 | 143 | /** 144 | * Instantiate Particle and set the config 145 | * 146 | * @private 147 | */ 148 | _init() { 149 | this._particle = new Particle({ 150 | clientId: this._auth.clientId, 151 | clientSecret: this._auth.clientSecret 152 | }); 153 | this._particle.setContext('tool', { 154 | name: 'node-red-contrib-particle' 155 | }); 156 | } 157 | 158 | /** 159 | * Callback that fetches a new token and recreates all 160 | * event callbacks to prevent data loss. 161 | * 162 | * @private 163 | */ 164 | _reauthenticate() { 165 | this._logger.log('Reauthenticating...'); 166 | this.cleanup(); 167 | 168 | this.login().then(() => { 169 | this.listenToEventStream.apply(this, this._lastListenArguments); 170 | }, () => { 171 | const retryIn = 5; 172 | this._logger.error(`Failed to reauthenticate. Trying again in ${retryIn} seconds`); 173 | setTimeout(this._reauthenticate.bind(this), retryIn * 1000); 174 | }); 175 | } 176 | }; 177 | -------------------------------------------------------------------------------- /src/particle-base-node.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Api = require('./api'); 4 | const _ = require('lodash'); 5 | const helpers = require('./helpers'); 6 | 7 | class ParticleBaseNode { 8 | constructor({ self, node, RED, properties, functionName, functionArguments, mandatoryArguments, onAPIResponse, onAPIError, inputProperties, runOnLoad, success, error, info }) { 9 | this.self = self; 10 | this.node = node; 11 | this.RED = RED; 12 | this.message = null; 13 | this.variables = {}; 14 | this.functionName = functionName; 15 | this.functionArguments = functionArguments; 16 | this.mandatoryArguments = mandatoryArguments; 17 | this.inputProperties = inputProperties; 18 | this.onAPIResponse = onAPIResponse; 19 | this.onAPIError = onAPIError; 20 | this.runOnLoad = runOnLoad || node.runOnLoad; 21 | this.success = success || node.success; 22 | this.error = error; 23 | this.info = info || {}; 24 | 25 | properties.forEach(field => { 26 | this.variables[field] = node[field]; 27 | }); 28 | 29 | this.auth = this.RED.nodes.getNode(node.auth); 30 | 31 | if (this.auth) { 32 | this.api = new Api(this.auth); 33 | } else if (node.testAuth) { 34 | this.api = new Api(node.testAuth); 35 | } 36 | 37 | this.run(); 38 | } 39 | 40 | hasMandatoryArguments(args = [], params = {}) { 41 | let ok = true; 42 | 43 | args.forEach(arg => { 44 | if (!params[arg]) { 45 | ok = false; 46 | 47 | this.self.error(`Missing the ${arg} parameter`); 48 | } 49 | }); 50 | 51 | return ok; 52 | } 53 | 54 | parametersFromVariables(variables, functionArguments) { 55 | let params = {}; 56 | let ok = true; 57 | 58 | functionArguments.forEach(argument => { 59 | let value = null; 60 | 61 | if (argument.defaultValue) { 62 | value = argument.defaultValue; 63 | } 64 | 65 | if (argument.value && variables[argument.value]) { 66 | value = variables[argument.value]; 67 | } 68 | 69 | if (argument.customValue) { 70 | value = argument.customValue(variables); 71 | } 72 | 73 | if (argument.format === 'object' && typeof value !== 'object') { 74 | try { 75 | value = JSON.parse(value); 76 | 77 | if (typeof value !== 'object') { 78 | throw 'Argument not a valid JSON object'; 79 | } 80 | } catch (e) { 81 | if (this.error.onConfig) { 82 | this.self.error(this.error.onConfig); 83 | } 84 | 85 | ok = false; 86 | } 87 | } 88 | 89 | if (argument.prefix && value) { 90 | value = `${argument.prefix}${value}`; 91 | } 92 | 93 | params[argument.key] = value; 94 | }); 95 | 96 | return ok ? params : null; 97 | } 98 | 99 | successStatus(message = 'OK') { 100 | return helpers.onSuccess({ status: this.self.status.bind(this.self), message }); 101 | } 102 | 103 | errorStatus(message = 'NOT OK') { 104 | return helpers.onError({ status: this.self.status.bind(this.self), message }); 105 | } 106 | 107 | infoStatus(message = 'processing') { 108 | return helpers.onInfo({ status: this.self.status.bind(this.self), message }); 109 | } 110 | 111 | hideStatus() { 112 | return helpers.resetStatus({ status: this.self.status.bind(this.self), timeout: 0 }); 113 | } 114 | 115 | makeAPIRequest(variables) { 116 | const parameters = this.parametersFromVariables(variables, this.functionArguments); 117 | 118 | if (!parameters) { 119 | return null; 120 | } 121 | 122 | const hasMandatoryArguments = this.hasMandatoryArguments(this.mandatoryArguments, parameters); 123 | 124 | if (!hasMandatoryArguments) { 125 | return null; 126 | } 127 | 128 | this.infoStatus(this.info.status); 129 | 130 | this.api[this.functionName](parameters) 131 | .then(result => { 132 | this.hideStatus(); 133 | 134 | if (this.success && this.success.status) { 135 | this.successStatus(this.success.status); 136 | } 137 | 138 | if (this.onAPIResponse) { 139 | return this.onAPIResponse(variables, this.message, result); 140 | } else if (this.success && this.success.fields) { 141 | const fields = this.success.fields; 142 | let message = this.message || {}; 143 | 144 | fields.forEach(field => { 145 | if (field.path) { 146 | message[field.key] = _.get(result, field.path); 147 | } else if (field.value) { 148 | message[field.key] = variables[field.value]; 149 | } else if (field.key === 'payload') { 150 | message.payload = result; 151 | } 152 | }); 153 | 154 | return this.self.send(message); 155 | } else { 156 | return true; 157 | } 158 | }, (error) => { 159 | this.hideStatus(); 160 | 161 | if (this.error && this.error.status) { 162 | this.errorStatus(this.error.status); 163 | } 164 | 165 | if (this.onAPIError) { 166 | return this.onAPIError(error); 167 | } 168 | 169 | if (this.error && this.error.onRuntime) { 170 | this.self.error(`${this.error.onRuntime} (${error})`, this.message || {}); 171 | } 172 | 173 | return false; 174 | }); 175 | } 176 | 177 | onClose() { 178 | this.api.cleanup(); 179 | } 180 | 181 | mergePropertiesFromMsg(variables, inputProperties, msg) { 182 | let _var = Object.assign({}, variables); 183 | const data = msg; 184 | 185 | if (data) { 186 | inputProperties.forEach(field => { 187 | if (data[field] && !_var[field]) { 188 | _var[field] = data[field]; 189 | } 190 | }); 191 | } 192 | 193 | return _var; 194 | } 195 | 196 | onInput(msg) { 197 | if (msg) { 198 | this.message = msg; 199 | } 200 | 201 | let variables = this.mergePropertiesFromMsg(this.variables, this.inputProperties, msg); 202 | 203 | this.makeAPIRequest(variables); 204 | } 205 | 206 | run() { 207 | if (!this.api) { 208 | return null; 209 | } 210 | 211 | this.api.login() 212 | .then(() => { 213 | this.self.on('input', this.onInput.bind(this)); 214 | this.self.on('close', this.onClose.bind(this)); 215 | 216 | if (this.runOnLoad) { 217 | this.makeAPIRequest(this.variables); 218 | } 219 | }); 220 | } 221 | } 222 | 223 | module.exports = ParticleBaseNode; 224 | -------------------------------------------------------------------------------- /nodes/config/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof window !== 'undefined' && !window.helpers) { 4 | 5 | const generateFieldScopeSelector = () => { 6 | return ` 7 |
${title}
82 | `; 83 | 84 | if (inputs) { 85 | html += ` 86 |${details}
125 |