Jelajahi Sumber

refactor: drop the data.loop.item rewrite, address the item for real

The engine used to rewrite expression text before evaluating it, turning
"data.loop.item" into a bare "item" identifier. Nothing named "loop"
existed in the data; the spelling only worked because a find-and-replace
made it work. That kept the loop item invisible to every schema and to the
editor's field-path suggestions.

The previous commit gave a loop body a real "loop" object. This one
finishes the job: the ten expressions that used the old spelling, across
four workflows, now say loop.item, and the rewrite is gone.

Migrated first and verified while both spellings still worked, then the
shim was removed and it was verified again - so there was no point at
which a live workflow depended on something that had already been deleted.
The two active ones are 35photo2anime, which reads
loop.item.enclosure.url, and the zsebhoki publisher, which reads
loop.item.slug and the two name fields.

Nested access confirmed on a two-node body over two iterations, before and
after removal:

  iteration 1   first: url=http://a/1.jpg slug=one   second: url=http://a/1.jpg
  iteration 2   first: url=http://a/2.jpg slug=two   second: url=http://a/2.jpg

Two test cases and the two deploy copies moved with it. 83 passed, 0
failed, 2 skipped.
fszontagh 1 bulan lalu
induk
melakukan
3084504b96

+ 42 - 30
deploy/workflows/35photo2anime-sdcpp.json

@@ -1,5 +1,7 @@
 {
-  "active": false,
+  "_id": "wf_e6140002-c6b1-428c-afab-68a301591ce6",
+  "_version": 95,
+  "active": true,
   "connections": [
     {
       "sourceNodeId": "node_1785733293322",
@@ -116,6 +118,10 @@
       "targetNodeId": "node_1785832349865"
     }
   ],
+  "consecutiveFailures": 0,
+  "deactivatedReason": "",
+  "description": "",
+  "groupId": "wfg_07ea58b4-3d38-4633-b6d8-3186355c5f51",
   "name": "35photo2anime - sdcpp",
   "nodes": [
     {
@@ -135,7 +141,7 @@
       "id": "node_1785732919652",
       "name": "35photo pro",
       "position": {
-        "x": 3160,
+        "x": 0,
         "y": 180
       },
       "type": "rss-reader"
@@ -148,7 +154,7 @@
       "id": "node_1785733004181",
       "name": "Click Trigger",
       "position": {
-        "x": 3140,
+        "x": 20,
         "y": 0
       },
       "type": "click-trigger"
@@ -166,8 +172,8 @@
       "id": "node_1785733293322",
       "name": "Loop",
       "position": {
-        "x": 3300,
-        "y": 540
+        "x": 220,
+        "y": 560
       },
       "type": "loop"
     },
@@ -186,14 +192,14 @@
         "retryMaxDelayMs": 30000,
         "storeDownload": false,
         "timeout": 30000,
-        "url": "{{data.loop.item.enclosure.url}}"
+        "url": "{{loop.item.enclosure.url}}"
       },
       "disabled": false,
       "id": "node_1785733345099",
       "name": "Fetch Image",
       "position": {
-        "x": 3280,
-        "y": 720
+        "x": 200,
+        "y": 740
       },
       "type": "http-request"
     },
@@ -212,7 +218,7 @@
       "id": "node_1785761774374",
       "name": "Schedule Trigger",
       "position": {
-        "x": 3460,
+        "x": 360,
         "y": 0
       },
       "type": "schedule-trigger"
@@ -226,8 +232,8 @@
       "id": "node_dedupe",
       "name": "Dedupe Check",
       "position": {
-        "x": 3280,
-        "y": 860
+        "x": 200,
+        "y": 940
       },
       "type": "code"
     },
@@ -246,8 +252,8 @@
       "id": "node_if_new",
       "name": "Is New Image",
       "position": {
-        "x": 3280,
-        "y": 980
+        "x": 200,
+        "y": 1120
       },
       "type": "if-condition"
     },
@@ -268,8 +274,8 @@
       "id": "node_1785832349865",
       "name": "Nextcloud Talk",
       "position": {
-        "x": 3540,
-        "y": 2420
+        "x": 320,
+        "y": 1680
       },
       "type": "nextcloud-talk"
     },
@@ -289,8 +295,8 @@
       "id": "node_1785909802844",
       "name": "IF Condition",
       "position": {
-        "x": 3400,
-        "y": 1760
+        "x": 160,
+        "y": 1480
       },
       "type": "if-condition"
     },
@@ -311,8 +317,8 @@
       "id": "node_gen_failed",
       "name": "Say It Failed",
       "position": {
-        "x": 3280,
-        "y": 1900
+        "x": 60,
+        "y": 1680
       },
       "type": "nextcloud-talk"
     },
@@ -327,8 +333,8 @@
       "id": "node_1786088830206_58567",
       "name": "Merge",
       "position": {
-        "x": 3300,
-        "y": 360
+        "x": 220,
+        "y": 380
       },
       "type": "merge"
     },
@@ -349,7 +355,7 @@
       "id": "node_1786088853294_0",
       "name": "35photo ru",
       "position": {
-        "x": 3460,
+        "x": 340,
         "y": 180
       },
       "type": "rss-reader"
@@ -370,8 +376,8 @@
       "id": "node_no_feed",
       "name": "Did Any Feed Answer",
       "position": {
-        "x": 3300,
-        "y": 3240
+        "x": 160,
+        "y": 1860
       },
       "type": "if-condition"
     },
@@ -392,8 +398,8 @@
       "id": "node_feeds_down",
       "name": "Say No Feed Answered",
       "position": {
-        "x": 3300,
-        "y": 3420
+        "x": 180,
+        "y": 2040
       },
       "type": "nextcloud-talk"
     },
@@ -412,12 +418,17 @@
       "id": "node_call_process",
       "name": "Process Image",
       "position": {
-        "x": 3540,
-        "y": 1200
+        "x": 180,
+        "y": 1300
       },
       "type": "call-workflow"
     }
   ],
+  "ownerId": "usr_dc798bf2-4e5b-4f86-b003-0b9184b2a528",
+  "projectId": "prj_97b218e7-0bc1-41db-aa75-7b9263c876c0",
+  "publishedAt": 1786513547285,
+  "publishedBy": "admin",
+  "publishedVersion": 94,
   "settings": {
     "continueOnError": false,
     "errorWorkflowId": "wf_766fcbcb-6345-4e9c-b7f8-f219505ee45c",
@@ -431,5 +442,6 @@
       },
       "defaultAccess": "none"
     }
-  }
-}
+  },
+  "updatedAt": 1786341677586
+}

+ 17 - 6
deploy/workflows/swf-zsebhoki-publish-anime-post.json

@@ -1,4 +1,6 @@
 {
+  "_id": "wf_234820e9-807a-44a9-b8cf-d8830ea8257c",
+  "_version": 29,
   "active": true,
   "connections": [
     {
@@ -74,6 +76,9 @@
       "targetNodeId": "out"
     }
   ],
+  "consecutiveFailures": 0,
+  "deactivatedReason": "",
+  "groupId": "wfg_07ea58b4-3d38-4633-b6d8-3186355c5f51",
   "name": "[SWF] zsebhoki - publish anime post",
   "nodes": [
     {
@@ -238,7 +243,7 @@
     {
       "config": {
         "authType": "credential",
-        "body": "{\n  \"slug\": \"{{$node['Prepare'].slug}}\",\n  \"is_adult\": {{Number($node['Workflow Input'].is_adult) === 1 ? 1 : 0}},\n  \"title_hu\": \"{{$node['Workflow Input'].title_hu}}\",\n  \"title_en\": \"{{$node['Workflow Input'].title_en}}\",\n  \"content_hu\": \"{{$node['Workflow Input'].content_hu}}\",\n  \"content_en\": \"{{$node['Workflow Input'].content_en}}\",\n  \"status\": \"{{$node['Workflow Input'].status || 'draft'}}\",\n  \"cover_url\": \"/storage/media/{{$node['Prepare'].mediaPath}}\",\n  \"cover_alt_hu\": \"{{$node['Workflow Input'].cover_alt_hu || $node['Workflow Input'].title_hu}}\",\n  \"cover_alt_en\": \"{{$node['Workflow Input'].cover_alt_en || $node['Workflow Input'].title_en}}\"\n}",
+        "body": "{\n  \"slug\": \"{{$node['Prepare'].slug}}\",\n  \"is_adult\": {{Number($node['Workflow Input'].is_adult) === 1 ? 1 : 0}},\n  \"title_hu\": \"{{$node['Workflow Input'].title_hu}}\",\n  \"title_en\": \"{{$node['Workflow Input'].title_en}}\",\n  \"content_hu\": \"{{$node['Workflow Input'].content_hu}}\",\n  \"content_en\": \"{{$node['Workflow Input'].content_en}}\",\n  \"status\": \"{{$node['Workflow Input'].status || 'draft'}}\",\n  \"cover_file_id\": \"{{data.body.id}}\",\n  \"cover_alt_hu\": \"{{$node['Workflow Input'].cover_alt_hu || $node['Workflow Input'].title_hu}}\",\n  \"cover_alt_en\": \"{{$node['Workflow Input'].cover_alt_en || $node['Workflow Input'].title_en}}\"\n}",
         "contentType": "application/json",
         "credentialId": "cred_c6b24bca-c3e9-4be5-a266-25ddc8538f36",
         "downloadCollection": "downloads",
@@ -435,15 +440,15 @@
         "fields": [
           {
             "name": "slug",
-            "value": "{{data.loop.item.slug}}"
+            "value": "{{loop.item.slug}}"
           },
           {
             "name": "name_hu",
-            "value": "{{data.loop.item.name_hu}}"
+            "value": "{{loop.item.name_hu}}"
           },
           {
             "name": "name_en",
-            "value": "{{data.loop.item.name_en}}"
+            "value": "{{loop.item.name_en}}"
           }
         ],
         "mode": "set"
@@ -458,5 +463,11 @@
       "type": "set-fields"
     }
   ],
-  "settings": {}
-}
+  "ownerId": "usr_dc798bf2-4e5b-4f86-b003-0b9184b2a528",
+  "projectId": "prj_97b218e7-0bc1-41db-aa75-7b9263c876c0",
+  "publishedAt": 1786513551410,
+  "publishedBy": "admin",
+  "publishedVersion": 28,
+  "settings": {},
+  "updatedAt": 1786207328826
+}

+ 1 - 45
src/runner/workflow_engine.cpp

@@ -3344,47 +3344,6 @@ static std::string convertReservedWordAccess(const std::string& expression) {
     return result;
 }
 
-// Legacy compatibility shim: rewrites the pre-existing "data.loop.item"
-// convention into the bare "item" identifier before evaluation.
-// e.g., "data.loop.item.hasAttachments" -> "item.hasAttachments"
-//
-// This is purely textual - it does not consult any real object path, and
-// "data.loop" is not a nested field that actually exists anywhere in the
-// data an expression sees. It survives only because live workflows already
-// depend on the "data.loop.item" spelling; new work should address the loop
-// item through the real "loop" object the engine now attaches to every loop
-// body node's input (const loop = input.loop, below), which is a genuine
-// nested JSON value and does not need this rewrite.
-static std::string simplifyLoopVariablePaths(const std::string& expression) {
-    // Loop variables that should be simplified
-    static const std::vector<std::string> loop_vars = {
-        "item", "index", "currentItem", "currentIndex", "totalItems", "isFirst", "isLast"
-    };
-
-    std::string result = expression;
-
-    for (const auto& var : loop_vars) {
-        // Pattern: data.loop.var -> var
-        std::string full_path = "data.loop." + var;
-        size_t pos = 0;
-
-        while ((pos = result.find(full_path, pos)) != std::string::npos) {
-            // Check if this is at the start or preceded by a non-identifier char
-            bool valid_start = (pos == 0) ||
-                               (!std::isalnum(result[pos - 1]) && result[pos - 1] != '_');
-
-            if (valid_start) {
-                // Replace data.loop.var with just var
-                result.replace(pos, full_path.length(), var);
-                pos += var.length();
-            } else {
-                pos += full_path.length();
-            }
-        }
-    }
-
-    return result;
-}
 
 nlohmann::json WorkflowEngine::evaluateJavaScriptExpression(
     const std::string& expression,
@@ -3392,11 +3351,8 @@ nlohmann::json WorkflowEngine::evaluateJavaScriptExpression(
     const std::unordered_map<std::string, NodeExecutionResult>& results,
     const Workflow& workflow) {
 
-    // Simplify loop variable paths (data.loop.item -> item)
-    std::string simplified_expression = simplifyLoopVariablePaths(expression);
-
     // Convert reserved word property access to bracket notation
-    std::string safe_expression = convertReservedWordAccess(simplified_expression);
+    std::string safe_expression = convertReservedWordAccess(expression);
 
     // Debug logging for expression transformation
     if (expression != safe_expression) {

+ 1 - 1
tests/nodes/loop-disabled-back-edge.json

@@ -7,7 +7,7 @@
     {"id": "loop", "name": "Loop", "type": "loop", "position": {"x": 0, "y": 200},
      "config": {"inputField": "data.result.items", "continueOnError": true}},
     {"id": "work", "name": "Work", "type": "set-fields", "position": {"x": 0, "y": 300},
-     "config": {"mode": "only-set", "fields": [{"name": "seen", "value": "{{data.loop.item}}"}]}},
+     "config": {"mode": "only-set", "fields": [{"name": "seen", "value": "{{loop.item}}"}]}},
     {"id": "notify", "name": "Notify", "type": "set-fields", "position": {"x": 0, "y": 400},
      "disabled": true,
      "config": {"mode": "only-set", "fields": [{"name": "told", "value": "yes"}]}},

+ 1 - 1
tests/nodes/workflow-output-in-loop.json

@@ -15,7 +15,7 @@
         {"id": "out", "name": "Out", "type": "workflow-output", "position": {"x": 0, "y": 300},
          "config": {"source": "fields",
                     "fields": [{"name": "named", "value": "from inside the loop"},
-                               {"name": "item", "value": "{{data.loop.item}}"}]}}
+                               {"name": "item", "value": "{{loop.item}}"}]}}
       ],
       "connections": [
         {"sourceNodeId": "in", "sourceOutput": "main", "targetNodeId": "items", "targetInput": "data"},