ソースを参照

feat: a Merge node, and two input handles for it to use

fszontagh 1 ヶ月 前
親
コミット
329321445b
2 ファイル変更、145 行追加、0 行削除
  1. 124 0
      nodes/core/merge.js
  2. 21 0
      tests/nodes/merge.json

+ 124 - 0
nodes/core/merge.js

@@ -0,0 +1,124 @@
+/**
+ * @node merge
+ * @name Merge
+ * @category flow-control
+ * @version 1.0.0
+ * @description Join two branches by appending, combining on a key, or taking whichever arrived
+ * @icon git-merge
+ */
+
+const inputs = [
+    { name: 'input1', displayName: 'Input 1', type: 'any', required: false },
+    { name: 'input2', displayName: 'Input 2', type: 'any', required: false }
+];
+
+const configSchema = {
+    type: 'object',
+    properties: {
+        mode: {
+            type: 'string',
+            title: 'Mode',
+            description: 'append joins two arrays, combine-by-key matches items on a field, choose-first takes whichever branch produced anything',
+            enum: ['append', 'combine-by-key', 'choose-first'],
+            default: 'append'
+        },
+        key: {
+            type: 'string',
+            title: 'Key Field',
+            description: 'Field to match on, for combine-by-key'
+        },
+        path1: {
+            type: 'string',
+            title: 'Path In Input 1',
+            description: 'Dotted path to the array inside input 1. Leave empty to use the input as it arrives'
+        },
+        path2: {
+            type: 'string',
+            title: 'Path In Input 2',
+            description: 'Dotted path to the array inside input 2. Leave empty to use the input as it arrives'
+        }
+    }
+};
+
+const inputSchema = {
+    type: 'object',
+    properties: {
+        input1: { type: 'any' },
+        input2: { type: 'any' }
+    }
+};
+
+const outputSchema = {
+    type: 'object',
+    properties: {
+        merged: { type: 'any', description: 'The joined result' },
+        count: { type: 'number', description: 'Number of items in the result' }
+    }
+};
+
+function asArray(value) {
+    if (value === undefined || value === null) return [];
+    return Array.isArray(value) ? value : [value];
+}
+
+function selectInput(value, path) {
+    if (!path) {
+        return value;
+    }
+    return smartbotic.utils.getFieldValue(value, path);
+}
+
+async function execute(config, input, context) {
+    const mode = config.mode || 'append';
+    const first = selectInput(input ? input.input1 : undefined, config.path1);
+    const second = selectInput(input ? input.input2 : undefined, config.path2);
+
+    if (mode === 'choose-first') {
+        const chosen = first !== undefined && first !== null ? first : second;
+        smartbotic.log.info('Merge: choose-first took ' + (first !== undefined && first !== null ? 'input1' : 'input2'));
+        return {
+            merged: chosen === undefined ? null : chosen,
+            count: Array.isArray(chosen) ? chosen.length : (chosen === undefined || chosen === null ? 0 : 1)
+        };
+    }
+
+    if (mode === 'combine-by-key') {
+        const key = config.key;
+        if (!key) {
+            throw new Error('Merge: combine-by-key needs a key field');
+        }
+
+        const byKey = {};
+        const order = [];
+
+        for (const item of asArray(first)) {
+            if (item === null || typeof item !== 'object') continue;
+            const id = String(item[key]);
+            byKey[id] = JSON.parse(JSON.stringify(item));
+            order.push(id);
+        }
+
+        for (const item of asArray(second)) {
+            if (item === null || typeof item !== 'object') continue;
+            const id = String(item[key]);
+            if (byKey[id] === undefined) {
+                byKey[id] = JSON.parse(JSON.stringify(item));
+                order.push(id);
+            } else {
+                for (const field of Object.keys(item)) {
+                    byKey[id][field] = item[field];
+                }
+            }
+        }
+
+        const combined = order.map(function (id) { return byKey[id]; });
+        smartbotic.log.info('Merge: combined ' + combined.length + ' items on "' + key + '"');
+        return { merged: combined, count: combined.length };
+    }
+
+    const appended = asArray(first).concat(asArray(second));
+    smartbotic.log.info('Merge: appended to ' + appended.length + ' items');
+    return { merged: appended, count: appended.length };
+}
+
+module.exports = { configSchema, inputSchema, outputSchema, inputs, execute };

+ 21 - 0
tests/nodes/merge.json

@@ -0,0 +1,21 @@
+{
+  "name": "verify-merge",
+  "nodes": [
+    {"id": "n1", "name": "Trigger", "type": "click-trigger", "position": {"x": 0, "y": 0}, "config": {}},
+    {"id": "a", "name": "BranchA", "type": "code", "position": {"x": -100, "y": 100},
+     "config": {"code": "return [{id: 1, name: 'one'}, {id: 2, name: 'two'}];"}},
+    {"id": "b", "name": "BranchB", "type": "code", "position": {"x": 100, "y": 100},
+     "config": {"code": "return [{id: 2, extra: 'from-b'}, {id: 3, name: 'three'}];"}},
+    {"id": "m", "name": "Merge", "type": "merge", "position": {"x": 0, "y": 220},
+     "config": {"mode": "combine-by-key", "key": "id", "path1": "result", "path2": "result"}}
+  ],
+  "connections": [
+    {"sourceNodeId": "n1", "sourceOutput": "main", "targetNodeId": "a", "targetInput": "data"},
+    {"sourceNodeId": "n1", "sourceOutput": "main", "targetNodeId": "b", "targetInput": "data"},
+    {"sourceNodeId": "a", "sourceOutput": "main", "targetNodeId": "m", "targetInput": "input1"},
+    {"sourceNodeId": "b", "sourceOutput": "main", "targetNodeId": "m", "targetInput": "input2"}
+  ],
+  "expect": {
+    "m": {"status": "completed", "output": {"count": 3}}
+  }
+}