|
@@ -166,6 +166,103 @@ module.exports = {
|
|
|
};
|
|
};
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
|
|
+## Outputs That Come From Config
|
|
|
|
|
+
|
|
|
|
|
+A node whose output count depends on how it is configured declares
|
|
|
|
|
+`dynamicOutputs` beside its static `outputs`:
|
|
|
|
|
+
|
|
|
|
|
+```javascript
|
|
|
|
|
+const outputs = [
|
|
|
|
|
+ { name: 'fallback', displayName: 'Fallback', type: 'any', color: '#6b7280' }
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+const dynamicOutputs = {
|
|
|
|
|
+ from: 'rules',
|
|
|
|
|
+ namePrefix: 'case',
|
|
|
|
|
+ labelFrom: 'label',
|
|
|
|
|
+ color: '#3b82f6'
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+module.exports = { configSchema, inputSchema, outputSchema, outputs, dynamicOutputs, execute };
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+For each entry in the placed node's `config.rules`, the editor draws a port named
|
|
|
|
|
+`case0`, `case1` and so on, labelled from that entry's `label` field, followed by
|
|
|
|
|
+the static outputs. Port names are positional, so an edge survives editing a
|
|
|
|
|
+rule's label or value. Deleting a rule drops the edges hanging off the port that
|
|
|
|
|
+went with it.
|
|
|
|
|
+
|
|
|
|
|
+At run time the node routes with `_activeBranch`, exactly as a fixed-port node
|
|
|
|
|
+does - the branch name is just computed:
|
|
|
|
|
+
|
|
|
|
|
+```javascript
|
|
|
|
|
+return { _activeBranch: 'case' + i, ['case' + i]: data };
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## Multiple Inputs
|
|
|
|
|
+
|
|
|
|
|
+A node that joins two branches names its inputs:
|
|
|
|
|
+
|
|
|
|
|
+```javascript
|
|
|
|
|
+const inputs = [
|
|
|
|
|
+ { name: 'input1', displayName: 'Input 1', type: 'any', required: false },
|
|
|
|
|
+ { name: 'input2', displayName: 'Input 2', type: 'any', required: false }
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+module.exports = { configSchema, inputSchema, outputSchema, inputs, execute };
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Each named input becomes its own target handle, and arrives in `execute` under
|
|
|
|
|
+that name - `input.input1`, `input.input2`. A node that declares no `inputs`
|
|
|
|
|
+keeps the single `data` handle it has always had.
|
|
|
|
|
+
|
|
|
|
|
+Note the difference between a node that sets `_activeBranch` and one that does
|
|
|
|
|
+not: with the marker, exactly one output carries data and everything downstream
|
|
|
|
|
+of the others is skipped. Without it, every named output the node returns is
|
|
|
|
|
+live at once, which is how `filter` sends kept and discarded items down two
|
|
|
|
|
+paths in the same run.
|
|
|
|
|
+
|
|
|
|
|
+## Conventions These Nodes Follow
|
|
|
|
|
+
|
|
|
|
|
+Four rules emerged while building the Tier 1 set. New nodes should follow them.
|
|
|
|
|
+
|
|
|
|
|
+**Fail loudly rather than drop data.** A node that cannot do what was asked
|
|
|
|
|
+throws, with a message naming what it received. `set-fields` throws when
|
|
|
|
|
+keep-all mode is handed an array instead of an object; its dotted paths refuse
|
|
|
|
|
+to overwrite a value they would otherwise destroy; `merge` throws when an item
|
|
|
|
|
+lacks the key it was told to combine on. The alternative - returning an empty
|
|
|
|
|
+object, or bucketing everything under the string `undefined` - produces a
|
|
|
|
|
+workflow that keeps running and quietly produces wrong data.
|
|
|
|
|
+
|
|
|
|
|
+**Guard a configurable output name against your own reserved keys.** A node that
|
|
|
|
|
+returns fixed keys alongside a user-named field must reject a name that would
|
|
|
|
|
+collide, throwing early:
|
|
|
|
|
+
|
|
|
|
|
+```javascript
|
|
|
|
|
+if (outputField === 'count' || outputField === 'groups') {
|
|
|
|
|
+ throw new Error('Aggregate: outputField cannot be "' + outputField +
|
|
|
|
|
+ '", which is a reserved output name for this node. Pick another name.');
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+`aggregate`, `sort-limit-dedupe` and `datetime` all need this. `template` and
|
|
|
|
|
+`json` do not, because they return exactly one key and there is nothing to
|
|
|
|
|
+collide with - do not add the guard where it protects nothing.
|
|
|
|
|
+
|
|
|
|
|
+**Config values arrive already evaluated.** The engine resolves `{{...}}` in
|
|
|
|
|
+node config before `execute()` runs, and a config string that is exactly one
|
|
|
|
|
+expression keeps its native type. So a field typed as a string in the schema can
|
|
|
|
|
+legitimately hold an array at run time, which is why several nodes begin with
|
|
|
|
|
+`if (Array.isArray(inputField))`. That branch is not dead code. Nodes must not
|
|
|
|
|
+re-interpolate config values.
|
|
|
|
|
+
|
|
|
|
|
+**Read paths with the shared helper.** Use
|
|
|
|
|
+`smartbotic.utils.getFieldValue(data, path)` rather than writing a private path
|
|
|
|
|
+walker. Two older nodes, `if-condition` and `loop`, still carry their own
|
|
|
|
|
+divergent copies - `loop` silently skips a leading `data.` segment and
|
|
|
|
|
+`if-condition` does not - and that inconsistency is exactly what the shared
|
|
|
|
|
+helper exists to stop spreading.
|
|
|
|
|
+
|
|
|
## Available APIs
|
|
## Available APIs
|
|
|
|
|
|
|
|
Inside `execute()`, you have access to the `smartbotic` global object:
|
|
Inside `execute()`, you have access to the `smartbotic` global object:
|
|
@@ -277,8 +374,17 @@ const hash = smartbotic.utils.sha256('data'); // Returns hex string
|
|
|
// Object utilities
|
|
// Object utilities
|
|
|
const picked = smartbotic.utils.pick(obj, ['key1', 'key2']);
|
|
const picked = smartbotic.utils.pick(obj, ['key1', 'key2']);
|
|
|
const omitted = smartbotic.utils.omit(obj, ['unwantedKey']);
|
|
const omitted = smartbotic.utils.omit(obj, ['unwantedKey']);
|
|
|
|
|
+
|
|
|
|
|
+// Read a nested value by dotted path
|
|
|
|
|
+const city = smartbotic.utils.getFieldValue(input, 'data.user.address.city');
|
|
|
|
|
+const second = smartbotic.utils.getFieldValue(input, 'data.items[1].id');
|
|
|
|
|
+const howMany = smartbotic.utils.getFieldValue(input, 'data.items.length');
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
|
|
+Missing paths return `undefined`. Arrays are reached with `key[0]` or a bare
|
|
|
|
|
+numeric segment, and `.length` works on an array. The path is taken literally,
|
|
|
|
|
+with no special handling of a leading `data.` segment.
|
|
|
|
|
+
|
|
|
## Important Guidelines
|
|
## Important Guidelines
|
|
|
|
|
|
|
|
### Indentation
|
|
### Indentation
|