Explorar o código

feat: a Switch node that grows an output per rule

fszontagh hai 1 mes
pai
achega
c2246de725
Modificáronse 2 ficheiros con 177 adicións e 0 borrados
  1. 139 0
      nodes/core/switch.js
  2. 38 0
      tests/nodes/switch.json

+ 139 - 0
nodes/core/switch.js

@@ -0,0 +1,139 @@
+/**
+ * @node switch
+ * @name Switch
+ * @category flow-control
+ * @version 1.0.0
+ * @description Branch several ways on one value, with a fallback for anything unmatched
+ * @icon split
+ */
+
+const outputs = [
+    { name: 'fallback', displayName: 'Fallback', type: 'any', color: '#6b7280' }
+];
+
+const dynamicOutputs = {
+    from: 'rules',
+    namePrefix: 'case',
+    labelFrom: 'label',
+    color: '#3b82f6'
+};
+
+const configSchema = {
+    type: 'object',
+    properties: {
+        field: {
+            type: 'string',
+            title: 'Field',
+            description: 'Path to the value to test, such as data.result.status'
+        },
+        rules: {
+            type: 'array',
+            title: 'Rules',
+            description: 'Each rule adds an output. The first match wins',
+            items: {
+                type: 'object',
+                properties: {
+                    label: {
+                        type: 'string',
+                        title: 'Label',
+                        description: 'Name shown on this output'
+                    },
+                    operator: {
+                        type: 'string',
+                        title: 'Operator',
+                        enum: ['equals', 'not_equals', 'contains', 'starts_with', 'ends_with', 'greater_than', 'less_than', 'regex', 'is_empty', 'is_not_empty'],
+                        default: 'equals'
+                    },
+                    value: {
+                        type: 'string',
+                        title: 'Value'
+                    }
+                }
+            }
+        }
+    }
+};
+
+const inputSchema = {
+    type: 'object',
+    properties: {
+        data: { type: 'any' }
+    }
+};
+
+const outputSchema = {
+    type: 'object',
+    properties: {
+        matchedRule: { type: 'number', description: 'Index of the rule that matched, or -1' },
+        matchedLabel: { type: 'string', description: 'Label of the rule that matched' },
+        _activeBranch: { type: 'string', description: 'The output the data went to' }
+    }
+};
+
+function ruleMatches(fieldValue, rule) {
+    const compare = rule.value;
+
+    switch (rule.operator) {
+        case 'equals':
+            return String(fieldValue) === String(compare);
+        case 'not_equals':
+            return String(fieldValue) !== String(compare);
+        case 'contains':
+            return String(fieldValue).indexOf(String(compare)) !== -1;
+        case 'starts_with':
+            return String(fieldValue).indexOf(String(compare)) === 0;
+        case 'ends_with': {
+            const haystack = String(fieldValue);
+            const needle = String(compare);
+            return haystack.length >= needle.length &&
+                haystack.lastIndexOf(needle) === haystack.length - needle.length;
+        }
+        case 'greater_than':
+            return Number(fieldValue) > Number(compare);
+        case 'less_than':
+            return Number(fieldValue) < Number(compare);
+        case 'regex':
+            try {
+                return new RegExp(String(compare)).test(String(fieldValue));
+            } catch (e) {
+                return false;
+            }
+        case 'is_empty':
+            return fieldValue === undefined || fieldValue === null || fieldValue === '' ||
+                (Array.isArray(fieldValue) && fieldValue.length === 0);
+        case 'is_not_empty':
+            return !(fieldValue === undefined || fieldValue === null || fieldValue === '' ||
+                (Array.isArray(fieldValue) && fieldValue.length === 0));
+        default:
+            return false;
+    }
+}
+
+async function execute(config, input, context) {
+    const rules = Array.isArray(config.rules) ? config.rules : [];
+    const data = input;
+    const fieldValue = smartbotic.utils.getFieldValue(data, config.field);
+
+    for (let i = 0; i < rules.length; i++) {
+        if (ruleMatches(fieldValue, rules[i] || {})) {
+            const branch = 'case' + i;
+            smartbotic.log.info('Switch matched rule ' + i + ' -> ' + branch);
+            return {
+                matchedRule: i,
+                matchedLabel: rules[i].label || branch,
+                _activeBranch: branch,
+                [branch]: data
+            };
+        }
+    }
+
+    smartbotic.log.info('Switch matched no rule -> fallback');
+    return {
+        matchedRule: -1,
+        matchedLabel: '',
+        _activeBranch: 'fallback',
+        fallback: data
+    };
+}
+
+module.exports = { configSchema, inputSchema, outputSchema, outputs, dynamicOutputs, execute };

+ 38 - 0
tests/nodes/switch.json

@@ -0,0 +1,38 @@
+{
+  "name": "verify-switch",
+  "nodes": [
+    {"id": "n1", "name": "Trigger", "type": "click-trigger", "position": {"x": 0, "y": 0}, "config": {}},
+    {"id": "n2", "name": "Fixture", "type": "code", "position": {"x": 0, "y": 100},
+     "config": {"code": "return { status: 'pending' };"}},
+    {"id": "n3", "name": "Switch", "type": "switch", "position": {"x": 0, "y": 200},
+     "config": {
+       "field": "data.result.status",
+       "rules": [
+         {"label": "Paid", "operator": "equals", "value": "paid"},
+         {"label": "Pending", "operator": "equals", "value": "pending"},
+         {"label": "Failed", "operator": "equals", "value": "failed"}
+       ]
+     }},
+    {"id": "m0", "name": "M0", "type": "code", "position": {"x": -150, "y": 300},
+     "config": {"code": "return { marker: 'case0' };"}},
+    {"id": "m1", "name": "M1", "type": "code", "position": {"x": -50, "y": 300},
+     "config": {"code": "return { marker: 'case1' };"}},
+    {"id": "m2", "name": "M2", "type": "code", "position": {"x": 50, "y": 300},
+     "config": {"code": "return { marker: 'case2' };"}},
+    {"id": "mf", "name": "MF", "type": "code", "position": {"x": 150, "y": 300},
+     "config": {"code": "return { marker: 'fallback' };"}}
+  ],
+  "connections": [
+    {"sourceNodeId": "n1", "sourceOutput": "main", "targetNodeId": "n2", "targetInput": "data"},
+    {"sourceNodeId": "n2", "sourceOutput": "main", "targetNodeId": "n3", "targetInput": "data"},
+    {"sourceNodeId": "n3", "sourceOutput": "case0", "targetNodeId": "m0", "targetInput": "data"},
+    {"sourceNodeId": "n3", "sourceOutput": "case1", "targetNodeId": "m1", "targetInput": "data"},
+    {"sourceNodeId": "n3", "sourceOutput": "case2", "targetNodeId": "m2", "targetInput": "data"},
+    {"sourceNodeId": "n3", "sourceOutput": "fallback", "targetNodeId": "mf", "targetInput": "data"}
+  ],
+  "expect": {
+    "n3": {"status": "completed", "output": {"matchedRule": 1, "matchedLabel": "Pending", "_activeBranch": "case1"}},
+    "m1": {"status": "completed", "output": {"result": {"marker": "case1"}}}
+  },
+  "expectMissing": ["m0", "m2", "mf"]
+}