Finished HTTP node creation and extensive node creation documentation in project

This commit is contained in:
Richard Osborne
2025-12-29 08:56:46 +01:00
parent fad9f1006d
commit 6fd59e83e6
13 changed files with 1008 additions and 247 deletions

View File

@@ -34,39 +34,39 @@ packages/noodl-runtime/src/nodes/std-library/
var MyNode = {
// REQUIRED: Unique identifier for the node
name: 'net.noodl.MyNode',
// REQUIRED: Display name in Node Picker and canvas
displayNodeName: 'My Node',
// OPTIONAL: Documentation URL
docs: 'https://docs.noodl.net/nodes/category/my-node',
// REQUIRED: Category for organization (Data, Visual, Logic, etc.)
category: 'Data',
// OPTIONAL: Node color theme
// Options: 'data' (green), 'visual' (blue), 'component' (purple), 'javascript' (pink), 'default' (gray)
color: 'data',
// OPTIONAL: Search keywords for Node Picker
searchTags: ['my', 'node', 'custom', 'example'],
// OPTIONAL: Called when node instance is created
initialize: function () {
this._internal.myData = {};
},
// OPTIONAL: Data shown in debug inspector
getInspectInfo() {
return this._internal.inspectData;
},
// REQUIRED: Define input ports
inputs: {
inputName: {
type: 'string', // See "Port Types" section below
type: 'string', // See "Port Types" section below
displayName: 'Input Name',
group: 'General', // Group in property panel
group: 'General', // Group in property panel
default: 'default value'
},
doAction: {
@@ -75,7 +75,7 @@ var MyNode = {
group: 'Actions'
}
},
// REQUIRED: Define output ports
outputs: {
outputValue: {
@@ -94,7 +94,7 @@ var MyNode = {
group: 'Events'
}
},
// OPTIONAL: Methods to handle input changes
methods: {
setInputName: function (value) {
@@ -102,19 +102,19 @@ var MyNode = {
// Optionally trigger output update
this.flagOutputDirty('outputValue');
},
// Signal handler - name must match input name with 'Trigger' suffix
doActionTrigger: function () {
// Perform the action
const result = this.processInput(this._internal.inputName);
this._internal.outputValue = result;
// Update outputs
this.flagOutputDirty('outputValue');
this.sendSignalOnOutput('success');
}
},
// OPTIONAL: Return output values
getOutputValue: function (name) {
if (name === 'outputValue') {
@@ -126,7 +126,7 @@ var MyNode = {
// REQUIRED: Export the node
module.exports = {
node: MyNode,
// OPTIONAL: Setup function for dynamic ports
setup: function (context, graphModel) {
// See "Dynamic Ports" section below
@@ -144,10 +144,10 @@ Add the node to the `registerNodes` function in `packages/noodl-runtime/noodl-ru
function registerNodes(noodlRuntime) {
[
// ... existing nodes ...
// Add your new node
require('./src/nodes/std-library/data/mynode'),
require('./src/nodes/std-library/data/mynode')
// ... more nodes ...
].forEach((node) => noodlRuntime.registerNode(node));
}
@@ -174,10 +174,10 @@ const coreNodes = [
// ... other subcategories ...
{
name: 'External Data',
items: ['net.noodl.MyNode', 'REST2'] // Add your node name here
items: ['net.noodl.MyNode', 'REST2'] // Add your node name here
}
]
},
}
// ... more categories ...
];
```
@@ -188,24 +188,24 @@ const coreNodes = [
### Common Input/Output Types
| Type | Description | Example Use |
|------|-------------|-------------|
| `string` | Text value | URLs, names, content |
| `number` | Numeric value | Counts, sizes, coordinates |
| `boolean` | True/false | Toggles, conditions |
| `signal` | Trigger without data | Action buttons, events |
| `object` | JSON object | API responses, data structures |
| `array` | List of items | Collections, results |
| `color` | Color value | Styling |
| `*` | Any type | Generic ports |
| Type | Description | Example Use |
| --------- | -------------------- | ------------------------------ |
| `string` | Text value | URLs, names, content |
| `number` | Numeric value | Counts, sizes, coordinates |
| `boolean` | True/false | Toggles, conditions |
| `signal` | Trigger without data | Action buttons, events |
| `object` | JSON object | API responses, data structures |
| `array` | List of items | Collections, results |
| `color` | Color value | Styling |
| `*` | Any type | Generic ports |
### Input-Specific Types
| Type | Description |
|------|-------------|
| `{ name: 'enum', enums: [...] }` | Dropdown selection |
| `{ name: 'stringlist' }` | List of strings (comma-separated) |
| `{ name: 'number', min, max }` | Number with constraints |
| Type | Description |
| -------------------------------- | --------------------------------- |
| `{ name: 'enum', enums: [...] }` | Dropdown selection |
| `{ name: 'stringlist' }` | List of strings (comma-separated) |
| `{ name: 'number', min, max }` | Number with constraints |
### Example Enum Input
@@ -239,7 +239,7 @@ Dynamic ports are created at runtime based on configuration. This is useful when
```javascript
module.exports = {
node: MyNode,
setup: function (context, graphModel) {
// Only run in editor, not deployed
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
@@ -248,7 +248,7 @@ module.exports = {
function updatePorts(nodeId, parameters, editorConnection) {
const ports = [];
// Always include base ports from node definition
// Add dynamic ports based on parameters
if (parameters.items) {
@@ -262,7 +262,7 @@ module.exports = {
});
});
}
// Send ports to editor
editorConnection.sendDynamicPorts(nodeId, ports);
}
@@ -348,7 +348,7 @@ For asynchronous operations (API calls, file I/O), use standard async patterns:
methods: {
fetchTrigger: function () {
const self = this;
fetch(this._internal.url)
.then(response => response.json())
.then(data => {
@@ -394,6 +394,348 @@ getInspectInfo() {
---
## ⚠️ CRITICAL GOTCHAS - READ BEFORE CREATING NODES
These issues will cause silent failures with NO error messages. They were discovered during the HTTP node debugging session (December 2024) and cost hours of debugging time.
---
### 🔴 GOTCHA #1: Never Override `setInputValue` in prototypeExtensions
**THE BUG:**
```javascript
// ❌ DEADLY - This silently breaks ALL signal inputs
prototypeExtensions: {
setInputValue: function (name, value) {
this._internal.inputValues[name] = value; // Signal setters NEVER called!
}
}
```
**WHY IT BREAKS:**
- `prototypeExtensions.setInputValue` **completely overrides** `Node.prototype.setInputValue`
- The base method contains `input.set.call(this, value)` which triggers signal callbacks
- Without it, signals never fire - NO errors, just silent failure
**THE FIX:**
```javascript
// ✅ SAFE - Use a different method name for storing dynamic values
prototypeExtensions: {
_storeInputValue: function (name, value) {
this._internal.inputValues[name] = value;
},
registerInputIfNeeded: function (name) {
// Register dynamic inputs with _storeInputValue, not setInputValue
if (name.startsWith('body-')) {
return this.registerInput(name, {
set: this._storeInputValue.bind(this, name)
});
}
}
}
```
---
### 🔴 GOTCHA #2: Dynamic Ports REPLACE Static Ports
**THE BUG:**
```javascript
// Node has static inputs defined in inputs: {}
inputs: {
url: { type: 'string', set: function(v) {...} },
fetch: { type: 'signal', valueChangedToTrue: function() {...} }
},
// But setup function only sends dynamic ports
function updatePorts(nodeId, parameters, editorConnection) {
const ports = [
{ name: 'headers', ... },
{ name: 'queryParams', ... }
];
// ❌ MISSING url, fetch - they won't appear in editor!
editorConnection.sendDynamicPorts(nodeId, ports);
}
```
**WHY IT BREAKS:**
- `sendDynamicPorts()` tells the editor "these are ALL the ports for this node"
- Static `inputs` are NOT automatically merged
- The editor only shows dynamic ports, connections to static ports fail
**THE FIX:**
```javascript
// ✅ SAFE - Include ALL ports in dynamic ports array
function updatePorts(nodeId, parameters, editorConnection) {
const ports = [];
// Dynamic configuration ports
ports.push({ name: 'headers', type: {...}, plug: 'input' });
ports.push({ name: 'queryParams', type: {...}, plug: 'input' });
// MUST include static ports too!
ports.push({ name: 'url', type: 'string', plug: 'input', group: 'Request' });
ports.push({ name: 'fetch', type: 'signal', plug: 'input', group: 'Actions' });
ports.push({ name: 'cancel', type: 'signal', plug: 'input', group: 'Actions' });
// Include outputs too
ports.push({ name: 'success', type: 'signal', plug: 'output', group: 'Events' });
editorConnection.sendDynamicPorts(nodeId, ports);
}
```
---
### 🔴 GOTCHA #3: Configuration Inputs Need Explicit Registration
**THE BUG:**
```javascript
// Dynamic ports send method, bodyType, etc. to editor
ports.push({ name: 'method', type: { name: 'enum', ... } });
ports.push({ name: 'bodyType', type: { name: 'enum', ... } });
// ❌ Values never reach runtime - no setter registered!
// User selects POST in editor, but doFetch() always uses GET
doFetch: function() {
const method = this._internal.method || 'GET'; // Always undefined!
}
```
**WHY IT BREAKS:**
- Dynamic port values are sent to runtime as input values via `setInputValue`
- But `registerInputIfNeeded` is only called for ports not in static `inputs`
- If there's no setter, the value is lost
**THE FIX:**
```javascript
// ✅ SAFE - Register setters for all config inputs
prototypeExtensions: {
// Setter methods
setMethod: function (value) { this._internal.method = value || 'GET'; },
setBodyType: function (value) { this._internal.bodyType = value; },
setHeaders: function (value) { this._internal.headers = value || ''; },
registerInputIfNeeded: function (name) {
if (this.hasInput(name)) return;
// Configuration inputs - bind to their setters
const configSetters = {
method: this.setMethod.bind(this),
bodyType: this.setBodyType.bind(this),
headers: this.setHeaders.bind(this),
timeout: this.setTimeout.bind(this)
};
if (configSetters[name]) {
return this.registerInput(name, { set: configSetters[name] });
}
// Dynamic inputs for prefixed ports
if (name.startsWith('body-') || name.startsWith('header-')) {
return this.registerInput(name, {
set: this._storeInputValue.bind(this, name)
});
}
}
}
```
---
### 🔴 GOTCHA #4: Signal Inputs Use `valueChangedToTrue`, Not `set`
**THE BUG:**
```javascript
// ❌ WRONG - This won't trigger on signal
inputs: {
fetch: {
type: 'signal',
set: function() {
this.doFetch(); // Never called!
}
}
}
```
**WHY IT BREAKS:**
- Signal inputs don't use `set` - they use `valueChangedToTrue`
- The runtime wraps signals with `EdgeTriggeredInput.createSetter()` which tracks state transitions
- Signals only fire on FALSE → TRUE transition
**THE FIX:**
```javascript
// ✅ CORRECT - Use valueChangedToTrue for signals
inputs: {
fetch: {
type: 'signal',
displayName: 'Fetch',
group: 'Actions',
valueChangedToTrue: function () {
this.scheduleFetch();
}
},
cancel: {
type: 'signal',
displayName: 'Cancel',
group: 'Actions',
valueChangedToTrue: function () {
this.cancelFetch();
}
}
}
```
---
### 🔴 GOTCHA #5: Node Registration Path Matters
**THE BUG:**
- Nodes in `noodl-runtime/noodl-runtime.js` → Go through `defineNode()`
- Nodes in `noodl-viewer-react/register-nodes.js` → Go through `defineNode()`
- Raw node object passed directly → Does NOT go through `defineNode()`
**WHY IT MATTERS:**
- `defineNode()` in `nodedefinition.js` wraps signal inputs with `EdgeTriggeredInput.createSetter()`
- Without `defineNode()`, signals are registered but never fire
- The `{node, setup}` export format automatically calls `defineNode()`
**THE FIX:**
```javascript
// ✅ CORRECT - Always export with {node, setup} format
module.exports = {
node: MyNode, // Goes through defineNode()
setup: function (context, graphModel) {
// Dynamic port setup
}
};
// ✅ ALSO CORRECT - Call defineNode explicitly
const NodeDefinition = require('./nodedefinition');
module.exports = NodeDefinition.defineNode(MyNode);
```
---
## Complete Working Pattern (HTTP Node Reference)
Here's the proven pattern from the HTTP node that handles all gotchas:
```javascript
var MyNode = {
name: 'net.noodl.MyNode',
displayNodeName: 'My Node',
category: 'Data',
color: 'data',
initialize: function () {
this._internal.inputValues = {}; // For dynamic input storage
this._internal.method = 'GET'; // Config defaults
},
// Static inputs - signals and essential ports
inputs: {
url: {
type: 'string',
set: function (value) { this._internal.url = value; }
},
fetch: {
type: 'signal',
valueChangedToTrue: function () { this.scheduleFetch(); }
}
},
outputs: {
response: { type: '*', getter: function() { return this._internal.response; } },
success: { type: 'signal' },
failure: { type: 'signal' }
},
prototypeExtensions: {
// Store dynamic values WITHOUT overriding setInputValue
_storeInputValue: function (name, value) {
this._internal.inputValues[name] = value;
},
// Configuration setters
setMethod: function (value) { this._internal.method = value || 'GET'; },
setHeaders: function (value) { this._internal.headers = value || ''; },
// Register ALL dynamic inputs
registerInputIfNeeded: function (name) {
if (this.hasInput(name)) return;
// Config inputs
const configSetters = {
method: this.setMethod.bind(this),
headers: this.setHeaders.bind(this)
};
if (configSetters[name]) {
return this.registerInput(name, { set: configSetters[name] });
}
// Prefixed dynamic inputs
if (name.startsWith('header-') || name.startsWith('body-')) {
return this.registerInput(name, {
set: this._storeInputValue.bind(this, name)
});
}
},
scheduleFetch: function () {
this.scheduleAfterInputsHaveUpdated(this.doFetch.bind(this));
},
doFetch: function () {
const method = this._internal.method; // Now correctly captured!
// ... fetch implementation
}
}
};
module.exports = {
node: MyNode,
setup: function (context, graphModel) {
function updatePorts(nodeId, parameters, editorConnection) {
const ports = [];
// Config ports
ports.push({ name: 'method', type: { name: 'enum', enums: [...] }, plug: 'input' });
ports.push({ name: 'headers', type: { name: 'stringlist' }, plug: 'input' });
// MUST include static ports!
ports.push({ name: 'url', type: 'string', plug: 'input' });
ports.push({ name: 'fetch', type: 'signal', plug: 'input' });
// Outputs
ports.push({ name: 'response', type: '*', plug: 'output' });
ports.push({ name: 'success', type: 'signal', plug: 'output' });
editorConnection.sendDynamicPorts(nodeId, ports);
}
// ... rest of setup
}
};
```
---
## Common Issues
### Node Not Appearing in Node Picker
@@ -416,9 +758,13 @@ getInspectInfo() {
### Signal Not Firing
**Cause:** Method name doesn't match pattern `inputName + 'Trigger'`.
**Cause #1:** Method name pattern wrong - use `valueChangedToTrue`, not `inputName + 'Trigger'`.
**Fix:** Ensure signal handler method is named correctly (e.g., `fetchTrigger` for input `fetch`).
**Cause #2:** Custom `setInputValue` overriding base - see GOTCHA #1.
**Cause #3:** Signal not in dynamic ports - see GOTCHA #2.
**Fix:** Review ALL gotchas above!
---
@@ -437,14 +783,14 @@ getInspectInfo() {
When creating new nodes, reference these existing nodes for patterns:
| Node | File | Good Example Of |
|------|------|-----------------|
| REST | `data/restnode.js` | Full-featured data node with scripts |
| HTTP | `data/httpnode.js` | Dynamic ports, configuration |
| String | `variables/string.js` | Simple variable node |
| Counter | `counter.js` | Stateful logic node |
| Condition | `condition.js` | Boolean logic |
| Node | File | Good Example Of |
| --------- | --------------------- | ------------------------------------ |
| REST | `data/restnode.js` | Full-featured data node with scripts |
| HTTP | `data/httpnode.js` | Dynamic ports, configuration |
| String | `variables/string.js` | Simple variable node |
| Counter | `counter.js` | Stateful logic node |
| Condition | `condition.js` | Boolean logic |
---
*Last Updated: December 2024*
_Last Updated: December 2024_