mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-12 07:12:54 +01:00
895 lines
24 KiB
Markdown
895 lines
24 KiB
Markdown
# Creating Nodes in OpenNoodl
|
|
|
|
This guide documents the complete process for creating new nodes in the OpenNoodl runtime, based on learnings from building the HTTP Request node.
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
Nodes in Noodl are defined in the `noodl-runtime` package and need to be:
|
|
|
|
1. **Created** - Define the node in a `.js` file
|
|
2. **Registered** - Add to `noodl-runtime.js`
|
|
3. **Indexed** - Add to `nodelibraryexport.js` for Node Picker visibility
|
|
|
|
---
|
|
|
|
## Step 1: Create the Node File
|
|
|
|
Create a new file in the appropriate category folder:
|
|
|
|
```
|
|
packages/noodl-runtime/src/nodes/std-library/
|
|
├── data/ # Data nodes (REST, HTTP, collections)
|
|
├── variables/ # Variable nodes (string, number, boolean)
|
|
├── user/ # User authentication nodes
|
|
└── *.js # General utility nodes
|
|
```
|
|
|
|
### Basic Node Structure
|
|
|
|
```javascript
|
|
'use strict';
|
|
|
|
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
|
|
displayName: 'Input Name',
|
|
group: 'General', // Group in property panel
|
|
default: 'default value'
|
|
},
|
|
doAction: {
|
|
type: 'signal',
|
|
displayName: 'Do Action',
|
|
group: 'Actions'
|
|
}
|
|
},
|
|
|
|
// REQUIRED: Define output ports
|
|
outputs: {
|
|
outputValue: {
|
|
type: 'string',
|
|
displayName: 'Output Value',
|
|
group: 'Results'
|
|
},
|
|
success: {
|
|
type: 'signal',
|
|
displayName: 'Success',
|
|
group: 'Events'
|
|
},
|
|
failure: {
|
|
type: 'signal',
|
|
displayName: 'Failure',
|
|
group: 'Events'
|
|
}
|
|
},
|
|
|
|
// OPTIONAL: Methods to handle input changes
|
|
methods: {
|
|
setInputName: function (value) {
|
|
this._internal.inputName = value;
|
|
// 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') {
|
|
return this._internal.outputValue;
|
|
}
|
|
}
|
|
};
|
|
|
|
// REQUIRED: Export the node
|
|
module.exports = {
|
|
node: MyNode,
|
|
|
|
// OPTIONAL: Setup function for dynamic ports
|
|
setup: function (context, graphModel) {
|
|
// See "Dynamic Ports" section below
|
|
}
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Step 2: Register the Node
|
|
|
|
Add the node to the `registerNodes` function in `packages/noodl-runtime/noodl-runtime.js`:
|
|
|
|
```javascript
|
|
function registerNodes(noodlRuntime) {
|
|
[
|
|
// ... existing nodes ...
|
|
|
|
// Add your new node
|
|
require('./src/nodes/std-library/data/mynode')
|
|
|
|
// ... more nodes ...
|
|
].forEach((node) => noodlRuntime.registerNode(node));
|
|
}
|
|
```
|
|
|
|
**Important:** The order in this array doesn't matter, but group related nodes together for readability.
|
|
|
|
---
|
|
|
|
## Step 3: Add to Node Picker Index
|
|
|
|
**CRITICAL:** This step is often forgotten! Without it, the node won't appear in the Node Picker.
|
|
|
|
Edit `packages/noodl-runtime/src/nodelibraryexport.js` and add your node to the appropriate category in the `coreNodes` array:
|
|
|
|
```javascript
|
|
const coreNodes = [
|
|
// ... other categories ...
|
|
{
|
|
name: 'Read & Write Data',
|
|
description: 'Arrays, objects, cloud data',
|
|
type: 'data',
|
|
subCategories: [
|
|
// ... other subcategories ...
|
|
{
|
|
name: 'External Data',
|
|
items: ['net.noodl.MyNode', 'REST2'] // Add your node name here
|
|
}
|
|
]
|
|
}
|
|
// ... more categories ...
|
|
];
|
|
```
|
|
|
|
---
|
|
|
|
## Port Types
|
|
|
|
### 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 |
|
|
|
|
### Input-Specific Types
|
|
|
|
| Type | Description |
|
|
| -------------------------------- | --------------------------------- |
|
|
| `{ name: 'enum', enums: [...] }` | Dropdown selection |
|
|
| `{ name: 'stringlist' }` | List of strings (comma-separated) |
|
|
| `{ name: 'number', min, max }` | Number with constraints |
|
|
|
|
### Example Enum Input
|
|
|
|
```javascript
|
|
inputs: {
|
|
method: {
|
|
type: {
|
|
name: 'enum',
|
|
enums: [
|
|
{ value: 'GET', label: 'GET' },
|
|
{ value: 'POST', label: 'POST' },
|
|
{ value: 'PUT', label: 'PUT' },
|
|
{ value: 'DELETE', label: 'DELETE' }
|
|
]
|
|
},
|
|
displayName: 'Method',
|
|
default: 'GET',
|
|
group: 'Request'
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Dynamic Ports
|
|
|
|
Dynamic ports are created at runtime based on configuration. This is useful when the number or names of ports depend on user settings.
|
|
|
|
### Setup Function Pattern
|
|
|
|
```javascript
|
|
module.exports = {
|
|
node: MyNode,
|
|
|
|
setup: function (context, graphModel) {
|
|
// Only run in editor, not deployed
|
|
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
|
return;
|
|
}
|
|
|
|
function updatePorts(nodeId, parameters, editorConnection) {
|
|
const ports = [];
|
|
|
|
// Always include base ports from node definition
|
|
// Add dynamic ports based on parameters
|
|
if (parameters.items) {
|
|
parameters.items.split(',').forEach((item) => {
|
|
ports.push({
|
|
name: 'item-' + item.trim(),
|
|
displayName: item.trim(),
|
|
type: 'string',
|
|
plug: 'input',
|
|
group: 'Items'
|
|
});
|
|
});
|
|
}
|
|
|
|
// Send ports to editor
|
|
editorConnection.sendDynamicPorts(nodeId, ports);
|
|
}
|
|
|
|
function managePortsForNode(node) {
|
|
updatePorts(node.id, node.parameters || {}, context.editorConnection);
|
|
|
|
node.on('parameterUpdated', function (event) {
|
|
if (event.name === 'items') {
|
|
updatePorts(node.id, node.parameters, context.editorConnection);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Listen for graph import completion
|
|
graphModel.on('editorImportComplete', () => {
|
|
// Listen for new nodes of this type
|
|
graphModel.on('nodeAdded.net.noodl.MyNode', function (node) {
|
|
managePortsForNode(node);
|
|
});
|
|
|
|
// Handle existing nodes
|
|
for (const node of graphModel.getNodesWithType('net.noodl.MyNode')) {
|
|
managePortsForNode(node);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Handling Signals
|
|
|
|
Signals are trigger-based ports (no data, just an event).
|
|
|
|
### Receiving Signals (Input)
|
|
|
|
```javascript
|
|
// In methods object
|
|
methods: {
|
|
// Pattern: inputName + 'Trigger'
|
|
fetchTrigger: function () {
|
|
// Called when 'fetch' signal is triggered
|
|
this.doFetch();
|
|
}
|
|
}
|
|
```
|
|
|
|
### Sending Signals (Output)
|
|
|
|
```javascript
|
|
// Send a signal pulse
|
|
this.sendSignalOnOutput('success');
|
|
this.sendSignalOnOutput('failure');
|
|
```
|
|
|
|
---
|
|
|
|
## Updating Outputs
|
|
|
|
When an output value changes, you must flag it as dirty:
|
|
|
|
```javascript
|
|
// Flag a single output
|
|
this.flagOutputDirty('outputValue');
|
|
|
|
// Flag multiple outputs
|
|
this.flagOutputDirty('response');
|
|
this.flagOutputDirty('statusCode');
|
|
|
|
// Then send signal if needed
|
|
this.sendSignalOnOutput('complete');
|
|
```
|
|
|
|
---
|
|
|
|
## Async Operations
|
|
|
|
For asynchronous operations (API calls, file I/O), use standard async patterns:
|
|
|
|
```javascript
|
|
methods: {
|
|
fetchTrigger: function () {
|
|
const self = this;
|
|
|
|
fetch(this._internal.url)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
self._internal.response = data;
|
|
self.flagOutputDirty('response');
|
|
self.sendSignalOnOutput('success');
|
|
})
|
|
.catch(error => {
|
|
self._internal.error = error.message;
|
|
self.flagOutputDirty('error');
|
|
self.sendSignalOnOutput('failure');
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Debug Inspector
|
|
|
|
Provide data for the debug inspector popup:
|
|
|
|
```javascript
|
|
getInspectInfo() {
|
|
// Return an array of objects with type and value
|
|
return [
|
|
{ type: 'text', value: 'Status: ' + this._internal.status },
|
|
{ type: 'value', value: this._internal.response }
|
|
];
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Testing Your Node
|
|
|
|
1. Start the dev server: `npm run dev`
|
|
2. Open the Node Picker (click in the node graph)
|
|
3. Search for your node by name or search tags
|
|
4. Navigate to the category to verify placement
|
|
5. Add the node and test inputs/outputs
|
|
6. Check console for any errors
|
|
|
|
---
|
|
|
|
## ⚠️ 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 (Signals Not Wrapping)
|
|
|
|
**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);
|
|
```
|
|
|
|
---
|
|
|
|
### 🔴 GOTCHA #6: Signal in Static `inputs` + Dynamic Ports = Duplicate Ports (Dec 2025)
|
|
|
|
**THE BUG:**
|
|
|
|
```javascript
|
|
// Signal defined in static inputs with handler
|
|
inputs: {
|
|
fetch: {
|
|
type: 'signal',
|
|
valueChangedToTrue: function() { this.scheduleFetch(); }
|
|
}
|
|
}
|
|
|
|
// updatePorts() ALSO adds fetch - CAUSES DUPLICATE!
|
|
function updatePorts(nodeId, parameters, editorConnection) {
|
|
const ports = [];
|
|
// ... other ports ...
|
|
ports.push({ name: 'fetch', type: 'signal', plug: 'input' }); // ❌ Duplicate!
|
|
editorConnection.sendDynamicPorts(nodeId, ports);
|
|
}
|
|
```
|
|
|
|
**SYMPTOM:** When trying to connect to the node, TWO "Fetch" signals appear in the connection popup.
|
|
|
|
**WHY IT BREAKS:**
|
|
|
|
- GOTCHA #2 says "include static ports in dynamic ports" which is true for MOST ports
|
|
- But signals with `valueChangedToTrue` handlers ALREADY have a runtime registration
|
|
- Adding them again in `updatePorts()` creates a duplicate visual port
|
|
- The handler still works, but UX is confusing
|
|
|
|
**THE FIX:**
|
|
|
|
```javascript
|
|
// ✅ CORRECT - Only define signal in static inputs, NOT in updatePorts()
|
|
inputs: {
|
|
fetch: {
|
|
type: 'signal',
|
|
valueChangedToTrue: function() { this.scheduleFetch(); }
|
|
}
|
|
}
|
|
|
|
function updatePorts(nodeId, parameters, editorConnection) {
|
|
const ports = [];
|
|
// ... dynamic ports ...
|
|
|
|
// NOTE: 'fetch' signal is defined in static inputs (with valueChangedToTrue handler)
|
|
// DO NOT add it here again or it will appear twice in the connection popup
|
|
|
|
// ... other ports ...
|
|
editorConnection.sendDynamicPorts(nodeId, ports);
|
|
}
|
|
```
|
|
|
|
**RULE:** Signals with `valueChangedToTrue` handlers → ONLY in static `inputs`. All other ports (value inputs, outputs) → in `updatePorts()` dynamic ports.
|
|
|
|
---
|
|
|
|
### 🔴 GOTCHA #7: Require Path Depth for noodl-runtime (Dec 2025)
|
|
|
|
**THE BUG:**
|
|
|
|
```javascript
|
|
// File: src/nodes/std-library/data/mynode.js
|
|
// Trying to require noodl-runtime.js at package root
|
|
|
|
const NoodlRuntime = require('../../../noodl-runtime'); // ❌ WRONG - only 3 levels
|
|
// This breaks the entire runtime with "Cannot find module" error
|
|
```
|
|
|
|
**WHY IT MATTERS:**
|
|
|
|
- From `src/nodes/std-library/data/` you need to go UP 4 levels to reach the package root
|
|
- Path: data → std-library → nodes → src → (package root)
|
|
- One wrong `../` and the entire app fails to load
|
|
|
|
**THE FIX:**
|
|
|
|
```javascript
|
|
// ✅ CORRECT - Count the directories carefully
|
|
// From src/nodes/std-library/data/mynode.js:
|
|
const NoodlRuntime = require('../../../../noodl-runtime'); // 4 levels
|
|
|
|
// Reference: cloudstore.js at src/api/ uses 2 levels:
|
|
const NoodlRuntime = require('../../noodl-runtime'); // 2 levels from src/api/
|
|
```
|
|
|
|
**Quick Reference:**
|
|
|
|
| File Location | Levels to Package Root | Require Path |
|
|
| ----------------------------- | ---------------------- | --------------------------- |
|
|
| `src/api/` | 2 | `../../noodl-runtime` |
|
|
| `src/nodes/` | 2 | `../../noodl-runtime` |
|
|
| `src/nodes/std-library/` | 3 | `../../../noodl-runtime` |
|
|
| `src/nodes/std-library/data/` | 4 | `../../../../noodl-runtime` |
|
|
|
|
---
|
|
|
|
## 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
|
|
|
|
**Cause:** Node not added to `nodelibraryexport.js` coreNodes array.
|
|
|
|
**Fix:** Add the node name to the appropriate subcategory items array.
|
|
|
|
### "Cannot read property of undefined" Errors
|
|
|
|
**Cause:** Accessing `this._internal` before `initialize()` runs.
|
|
|
|
**Fix:** Always check for undefined or initialize values in `initialize()`.
|
|
|
|
### Outputs Not Updating
|
|
|
|
**Cause:** Forgot to call `flagOutputDirty()`.
|
|
|
|
**Fix:** Call `this.flagOutputDirty('portName')` after setting internal value.
|
|
|
|
### Signal Not Firing
|
|
|
|
**Cause #1:** Method name pattern wrong - use `valueChangedToTrue`, not `inputName + 'Trigger'`.
|
|
|
|
**Cause #2:** Custom `setInputValue` overriding base - see GOTCHA #1.
|
|
|
|
**Cause #3:** Signal not in dynamic ports - see GOTCHA #2.
|
|
|
|
**Fix:** Review ALL gotchas above!
|
|
|
|
---
|
|
|
|
## File Checklist for New Nodes
|
|
|
|
- [ ] Create node file in `packages/noodl-runtime/src/nodes/std-library/[category]/`
|
|
- [ ] Add `require()` to `packages/noodl-runtime/noodl-runtime.js`
|
|
- [ ] Add node name to `packages/noodl-runtime/src/nodelibraryexport.js` coreNodes
|
|
- [ ] Test node appears in Node Picker
|
|
- [ ] Test all inputs/outputs work correctly
|
|
- [ ] Verify debug inspector shows useful info
|
|
|
|
---
|
|
|
|
## Reference Files
|
|
|
|
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 |
|
|
|
|
---
|
|
|
|
_Last Updated: December 2024_
|