node-red-contrib-salesforce-connection-emitter 1.4.3
A set of Node Red commands that allows nodes to better connect to salesforce (using environment variables, connection pools and config nodes)
Overview
A set of Node Red commands that allows nodes to better connect to salesforce (using environment variables, connection pools and config nodes).
Many of the connections we have seen for Salesforce rely on creating a configuration node to store the credentials. This is then used by the various other nodes to connect to salesforce - each with their own connection.
We would instead like to ensure the following:
- Credentials are secured using best practices
- While explicitly stating credentials to the config / resulting JSON is sill allowed, environment variables are now also supported. (Providing greater reusability, support for heroku and security)
- Connections are established at the Connection Credential and emitted to those dependent.
- Child nodes can get notified of the current connection, when it is disconnected or request it be reset through events.
- Support for ES6 Classes and subclassing
- By providing classes that can be extended, listening for the events can become quite simple.
Nodes
- connection.SfConnectionEmitter
- platformEvents.SfPlatformEventSubscriber
- platformEvents.SfPlatformEventPublisher
- query.SfUniversalQuery
- describe.SfUniversalDescribe
- http.SfUniversalHttp
connection.SfConnectionEmitter
Configuration Node - used by most (if not all of the other Salesforce commands.
When other nodes specify their type as sf-connection-emitter
, then a drop-down dialog allows them to choose which configuration to use.
Each configuration manages the connection to salesforce, and emits events to those listening when:
- (newConnection) - a connection is established
- (connectionLost) - the connection has been disconnected
- (refresh) - the connection should be restarted (logout and re-established)
- (logout) - request the connection be severed
For nodes that subclass the connection.SfConnectionReceiver - this is all handled for you... For more information, please see that class
Configuration
Name | Type | Description | Example |
---|---|---|---|
Name | String | Label to show in Node Red Editor | SF Connection |
Host | string | Domain to login with OR name of environment variable |
https://test.salesforce.com or test.salesforce.com or login.salesforce.com or SF_HOST |
Password | string | Password for that user OR name of environment variable (note: token does not use environment variable, so include in password if using environment variable) |
t0tallyVALID! or t0talyVALID!0abcd or SF_PASSWORD |
Security Token | string | Security Token for user | 0abcd |
Events
The SfConnectionEmitter dispatches four types of events, automatically handled by the
Each configuration manages the connection to salesforce, and emits events to those listening when:
- (newConnection) - a connection is established
- (connectionLost) - the connection has been disconnected
- (refresh) - the connection should be restarted (logout and re-established)
- (logout) - request the connection be severed
For nodes that subclass the connection.SfConnectionReceiver - this is all handled for you... For more information, please see that class
platformEvents.SfPlatformEventSubscriber
Use this to listen to Salesforce Platform Events
More on Platform Events can also be found on Trailhead.Salesforce.com
Name | Type | Description | Example |
---|---|---|---|
Name | String | Label to show in Node Red Editor | PE Subscription |
Connection | connection.SfConnectionEmitter | The connection emitter configuration to use | sfconn |
Event API Name | string | Platform Event Object API Name | ltng_Hello__e |
The Replay Id to start listening to messages from.
(-1 to only listen to those moving forward).
See here for more information NOTE: the replay Id captured is preserved for you automatically. To force the replay Id, configure it with an exclaimation mark / bang at the end: For example: 12! |
As mentioned above, the replay Id captured is preserved for you automatically.
Note that this is preserved to be only visible to that same node, as opposed to the flow or within the whole project. Please see Node Red's documentation on Node Context for more
While this is a decent stop-gap, future work will allow it to be stored to external services (such as a Redis store).
To force the replay Id, configure it with an exclaimation mark / bang at the end: For example: 12!
platformEvents.SfPlatformEventPublisher
Use this to publish Salesforce Platform Events
Simply apply the object you want to publish as the msg.payload and it will handle the rest.
More on Platform Events can also be found on Trailhead.Salesforce.com
Name | Type | Description | Example |
---|---|---|---|
Name | String | Label to show in Node Red Editor | PE Subscription |
Connection | connection.SfConnectionEmitter | The connection emitter configuration to use | sfconn |
Event API Name | string | Platform Event Object API Name | ltng_Hello__e |
query.SfUniversalQuery
Use this to do a SOQL or Tooling API query within Salesforce.
Supports selection of the API, queries that can be (environment variables, global settings, property within a message, etc) and you can specify where the results go.
Example Flow
[{"id":"d2048d63.a9936","type":"tab","label":"Flow 1","disabled":false,"info":""},{"id":"802b9dc4.7cebc","type":"inject","z":"d2048d63.a9936","name":"","topic":"","payload":"{\"query\":\"select id from Apexclass\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":140,"wires":[["3251ec56.5aef64"]]},{"id":"3251ec56.5aef64","type":"sf-universal-query","z":"d2048d63.a9936","name":"","sfconn":"73000fb4.ffb8e","api":"soql","query":"payload.query","queryType":"msg","target":"payload.some.result.somewhere","limit":100,"x":360,"y":140,"wires":[["a7b76b3.b6f7c98"]]},{"id":"a7b76b3.b6f7c98","type":"debug","z":"d2048d63.a9936","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":590,"y":140,"wires":[]},{"id":"73000fb4.ffb8e","type":"sf-connection-emitter","z":"","host":"SF_HOST","hostType":"env","username":"SF_USERNAME","usernameType":"env","password":"SF_PASSWORD","passwordType":"env","token":"","tokenType":"env"}]
Properties
Name | Type | Description | Example |
---|---|---|---|
Name | String | Label to show in Node Red Editor | Universal Query |
Connection | connection.SfConnectionEmitter | The connection emitter configuration to use | sfconn |
API | (SOQL|Tooling) | The API to run the query against | SOQL |
Query | * | The query to run | SELECT Id from Accounts (soql) -OR- SELECT Id,Name from ApexClass (tooling) |
Target | String | The path within the message to put the results | payload.results |
describe.SfUniversalDescribe
Use this node to describe all objects or just a particular object using the Metadata API
, Tooling API
or SOAP API
.
The name of the object to describe can also be defined either as a message property, or as a literal string.
Please note there is a bug with Node Red where the sobject appears required when it is not. The sobject is only required when not performing a describe all
Example Flow
[{"id":"7061841b.7dc20c","type":"tab","label":"Describe","disabled":false,"info":""},{"id":"ba6d47c3.0396f8","type":"inject","z":"7061841b.7dc20c","name":"Start","topic":"","payload":"{}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":150,"y":80,"wires":[["806d933.686d77"]]},{"id":"806d933.686d77","type":"sf-universal-describe","z":"7061841b.7dc20c","name":"","sfconn":"73000fb4.ffb8e","api":"soap","describeAll":true,"objectName":"","objectNameType":"msg","target":"payload.describe","x":380,"y":80,"wires":[["221f1521.c786aa"]]},{"id":"221f1521.c786aa","type":"debug","z":"7061841b.7dc20c","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":600,"y":80,"wires":[]},{"id":"ea764edf.67e0c","type":"comment","z":"7061841b.7dc20c","name":"Describe Everything","info":"","x":170,"y":40,"wires":[]},{"id":"6c23a421.ca6f3c","type":"comment","z":"7061841b.7dc20c","name":"","info":"","x":140,"y":160,"wires":[]},{"id":"5c26c6e1.7c68d8","type":"inject","z":"7061841b.7dc20c","name":"{\"sobject\":\"Account\"}","topic":"","payload":"{\"sobject\":\"Account\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":180,"y":220,"wires":[["722eb5e5.bc948c"]]},{"id":"722eb5e5.bc948c","type":"sf-universal-describe","z":"7061841b.7dc20c","name":"","sfconn":"73000fb4.ffb8e","api":"soap","describeAll":false,"objectName":"payload.sobject","objectNameType":"msg","target":"payload.describe","x":420,"y":220,"wires":[["c941c43f.b7d5b8"]]},{"id":"c941c43f.b7d5b8","type":"debug","z":"7061841b.7dc20c","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":640,"y":220,"wires":[]},{"id":"73000fb4.ffb8e","type":"sf-connection-emitter","z":"","host":"SF_HOST","hostType":"env","username":"SF_USERNAME","usernameType":"env","password":"SF_PASSWORD","passwordType":"env","token":"","tokenType":"env"}]
Name | Type | Description | Example |
---|---|---|---|
Name | String | Label to show in Node Red Editor | PE Subscription |
Connection | connection.SfConnectionEmitter | The connection emitter configuration to use | sfconn |
Describe All | boolean | Whether to describe everything (true) or only an sobject (false) | false |
SObject Name | string | API Name of the SObject to describe | Account |
Target | String | The path within the message to put the results | payload.results |
http.SfUniversalHttp
Use this to perform an HTTP Get request to salesforce.
This is quite often used with the Universal Describe to get further information.
(Note: A common example is to use node-red-contrib-literal-utils to pick the urls from a set of describes, and then use node-red-contrib-serial-iterator to then iterate through each of those values and get the results)
NOTE: Currently, we are only supporting GET. If others factors are needed, please submit an issue and it can be discussed.
Example Flow
[{"id":"d2048d63.a9936","type":"tab","label":"Flow 1","disabled":false,"info":""},{"id":"b684172.c49dfe8","type":"debug","z":"d2048d63.a9936","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":510,"y":200,"wires":[]},{"id":"67520728.547d28","type":"inject","z":"d2048d63.a9936","name":"{\"url\":\"/services/data/v42.0/sobjects/Account/describe\"}","topic":"","payload":"{\"url\":\"/services/data/v42.0/sobjects/Account/describe\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":270,"y":120,"wires":[["75031448.d6721c"]]},{"id":"75031448.d6721c","type":"sf-universal-http","z":"d2048d63.a9936","name":"","sfconn":"73000fb4.ffb8e","url":"payload.url","urlType":"msg","target":"payload","x":280,"y":200,"wires":[["b684172.c49dfe8"]]},{"id":"73000fb4.ffb8e","type":"sf-connection-emitter","z":"","host":"SF_HOST","hostType":"env","username":"SF_USERNAME","usernameType":"env","password":"SF_PASSWORD","passwordType":"env","token":"","tokenType":"env"}]
Example Flow with Repeater
A great example of using the http callout is if you have a list of URLs - such as templated from a previous describe...
[{"id":"c9dd3425.d23d88","type":"tab","label":"Flow 2","disabled":false,"info":""},{"id":"f9b2421e.23d19","type":"inject","z":"c9dd3425.d23d88","name":"{\"urls\":[...]}","topic":"","payload":"{\"urls\":[\"/services/data/v42.0/sobjects/Account/describe/compactLayouts\",\"/services/data/v42.0/sobjects/Account/describe/approvalLayouts\",\"/services/data/v42.0/sobjects/Account/listviews\",\"/services/data/v42.0/sobjects/Account/describe\",\"https://speed-inspiration-3102-dev-ed.cs69.my.salesforce.com/001/e\",\"/services/data/v42.0/sobjects/Account/quickActions\",\"/services/data/v42.0/sobjects/Account/describe/layouts\",\"/services/data/v42.0/sobjects/Account\"]}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":120,"y":140,"wires":[["f0240db8.37fa9"]]},{"id":"f0240db8.37fa9","type":"Serial Iterator","z":"c9dd3425.d23d88","name":"Serial Iterator","property":"payload.urls","inputFlow":"feedback","saveOutput":1,"recursive":0,"storeId":0,"x":320,"y":140,"wires":[["c545ae04.d3d54"],["36d218df.df27e8"]]},{"id":"c545ae04.d3d54","type":"sf-universal-http","z":"c9dd3425.d23d88","name":"","sfconn":"73000fb4.ffb8e","url":"payload","urlType":"msg","target":"payload","x":320,"y":220,"wires":[["f0240db8.37fa9"]]},{"id":"36d218df.df27e8","type":"debug","z":"c9dd3425.d23d88","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","x":550,"y":140,"wires":[]},{"id":"73000fb4.ffb8e","type":"sf-connection-emitter","z":"","host":"SF_HOST","hostType":"env","username":"SF_USERNAME","usernameType":"env","password":"SF_PASSWORD","passwordType":"env","token":"","tokenType":"env"}]
Example Flow with Describe and Repeater
(Note: A common example is to use node-red-contrib-literal-utils to pick the urls from a set of describes, and then use node-red-contrib-serial-iterator to then iterate through each of those values and get the results)
[{"id":"833f466c.8e4288","type":"tab","label":"Simple Flow","disabled":false,"info":""},{"id":"9694019f.d3cb7","type":"inject","z":"833f466c.8e4288","name":"Blank Payload","topic":"","payload":"{}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":80,"wires":[["abe1fcd2.d03e5"]]},{"id":"abe1fcd2.d03e5","type":"sf-universal-describe","z":"833f466c.8e4288","name":"","sfconn":"73000fb4.ffb8e","api":"soap","describeAll":true,"objectName":"","objectNameType":"msg","target":"describe","x":340,"y":80,"wires":[["740e1493.83e07c","3de2ebf4.276014"]]},{"id":"740e1493.83e07c","type":"debug","z":"833f466c.8e4288","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":550,"y":40,"wires":[]},{"id":"55cc2fc3.d9035","type":"debug","z":"833f466c.8e4288","name":"Complete","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":360,"y":420,"wires":[]},{"id":"c73609ff.5022c8","type":"Serial Iterator","z":"833f466c.8e4288","name":"","property":"describe.urls","inputFlow":"feedback","saveOutput":1,"recursive":0,"storeId":0,"x":130,"y":300,"wires":[["7bcef9d2.ed5e48"],["55cc2fc3.d9035"]]},{"id":"7bcef9d2.ed5e48","type":"sf-universal-http","z":"833f466c.8e4288","name":"","sfconn":"73000fb4.ffb8e","url":"payload","urlType":"msg","target":"payload","x":370,"y":300,"wires":[["c73609ff.5022c8"]]},{"id":"e5f2e6e5.3be2c8","type":"comment","z":"833f466c.8e4288","name":"Describe the list of objects...","info":"","x":160,"y":40,"wires":[]},{"id":"de2df7bd.d94808","type":"function","z":"833f466c.8e4288","name":"Only describe the first 3 urls","func":"msg.describe.urls = msg.describe.urls.slice(0,3);\nreturn msg;","outputs":1,"noerr":0,"x":380,"y":200,"wires":[["6db56361.e766dc","c73609ff.5022c8"]]},{"id":"3de2ebf4.276014","type":"pick-array-value","z":"833f466c.8e4288","name":"","arrayPath":"describe.sobjects","valuePath":"urls.sobject","targetPath":"describe.urls","x":150,"y":200,"wires":[["de2df7bd.d94808"]]},{"id":"6db56361.e766dc","type":"debug","z":"833f466c.8e4288","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":590,"y":160,"wires":[]},{"id":"5fcec93a.536a38","type":"comment","z":"833f466c.8e4288","name":"Pick the URLs from the array of objects...","info":"","x":200,"y":160,"wires":[]},{"id":"a758b017.2a287","type":"comment","z":"833f466c.8e4288","name":"Loop through the URLs one at a time...","info":"","x":210,"y":260,"wires":[]},{"id":"73000fb4.ffb8e","type":"sf-connection-emitter","z":"","host":"SF_HOST","hostType":"env","username":"SF_USERNAME","usernameType":"env","password":"SF_PASSWORD","passwordType":"env","token":"","tokenType":"env"}]
Properties
Name | Type | Description | Example |
---|---|---|---|
Name | String | Label to show in Node Red Editor | Universal Describe |
Connection | connection.SfConnectionEmitter | The connection emitter configuration to use | sfconn |
URL | (Message property | Global Setting | Environment Variable | Literal Value) |
URL to request | Payload (ex: serial) -OR- Payload.url -OR- /services/data/v42.0/sobjects/Account/describe -OR- https://speed-inspiration-3102-dev-ed.cs69.my.salesforce.com/001/e |
Target | String | The path within the message to put the results | payload.results |
Extending
While the request to use ES6 classes is currently underway within Node-Red modules, the following is the current structure for the nodes:
All nodes can be found by importing the module:
- connection
- query
- platformEvents
Note that Node-Red gets access to the setupNodeRed function, using require(...) directly gives access to the es6 class.
Also note, although NodeRed does not support TypeScript, care has been taken to support jsdoc / intellisense - to make extending these modules easier...
connection.SfConnectionReceiver
Base Class for many of the other commands.
Note that this provides a couple convenience functions, such as setting the status and base methods for listening to newConnection and connectionLost events.
Simply override the handleNewConnection(JsForceConnection) and handleConnectionLost(JsForceConnection) methods - respectively...
Properties
Name | Type | Description | Example |
---|---|---|---|
RED | @types/node-red#Red | Node Red instance - captured during initialization | |
config | object | Configuration passed to the node from the node red editor | {name:'query',query:'...'} |
nodeRedNode | @types/node-red#Node | Node Red Node instance to be manipulated | |
connectionEmitter | Node Red Config Id | The connection emitter configuration to use | sfconn |
STATUS_CONNECTED | string | Use this with the #status(string) command to set the status on the node | |
STATUS_DISCONNECTED | string | Use this with the #status(string) command to set the status on the node |
initialize
Intitialize the node
Name | Type | Description | Example |
---|---|---|---|
RED | @types/node-red#Red | Node Red instance - captured during initialization | |
config | object | Configuration passed to the node from the node red editor | {name:'query',query:'...'} |
nodeRedNode | @types/node-red#Node | Node Red Node instance to be manipulated |
Returns the instance, to support chaining...
listenToConnection
Starts listening to a single salesforce connection emitter...
Just give it the name of the property on the connection that holds the value, it will figure out the rest.
Name | Type | Description | Example |
---|---|---|---|
connectionPropName | String | The property of the connection to check the value for | sfconn |
Returns void
setStatus
Sets the status on the node
Name | Type | Description | Example |
---|---|---|---|
status | string | STATUS_CONNECTED|STATUS_DISCONNECTED |
Sets the status on the node so it appears connected or disconnected...
handleNewConnection
Overwrite this method to get notified when a connection is established.
(note that existing connections can be compared and so can also be disconnected, or see handleConnectionLost method below)
Name | Type | Description | Example |
---|---|---|---|
connection | JSForce.Connection | The new connection established |
handleConnectionLost
Overwrite this method to get notified when the connection is lost.
(This will always get called before handleNewConnection on a connectionEmitter#refresh event)
Name | Type | Description | Example |
---|---|---|---|
connection | JSForce.Connection | The new connection established |
Subclassing
For example: to access the ConnectionReceiver (for subclassing), use the following:
const connectionEmitter = require('node-red-contrib-salesforce-connection-emitter');
const SfConnectionReceiver = connectionEmitter.connection.SfConnectionReceiver;
//-- or directly through destructuring
const {connection: {SfConnectionReceiver}} = require('node-red-contrib-salesforce-connection-emitter');
class MyClass extends SfConnectionReceiver {...}
one further example - changing the name of the class:
const {connection: {SfConnectionReceiver:ConnectionReceiver}} = require('node-red-contrib-salesforce-connection-emitter');
class MyClass extends ConnectionReceiver {...}
Running Tests
- To test the project run
npm run test
ornpm run test:watch
to continuously test.
Running Linter
- To run linters on the project, run
npm run lint
ornpm run lint:watch
to continously lint.