|
|
@@ -3324,7 +3324,11 @@ bool WorkflowEngine::executeLoopBody(
|
|
|
std::unordered_map<std::string, NodeExecutionResult> iteration_results;
|
|
|
|
|
|
// Create input for first body node with current item
|
|
|
- nlohmann::json loop_input;
|
|
|
+ // Starts from the enclosing iteration's context, if there is one, so an
|
|
|
+ // outer loop's variables stay addressable inside this one. Everything
|
|
|
+ // below overwrites, so the inner loop's own values always win.
|
|
|
+ nlohmann::json loop_input = ctx.inherited.is_object() ? ctx.inherited
|
|
|
+ : nlohmann::json::object();
|
|
|
loop_input[ctx.item_variable] = ctx.items[i];
|
|
|
loop_input[ctx.index_variable] = i;
|
|
|
loop_input["totalItems"] = ctx.items.size();
|
|
|
@@ -3631,6 +3635,9 @@ bool WorkflowEngine::executeLoopBody(
|
|
|
inner_ctx.item_variable = body_result.output.value("_itemVariable", "item");
|
|
|
inner_ctx.index_variable = body_result.output.value("_indexVariable", "index");
|
|
|
inner_ctx.continue_on_error = body_result.output.value("_continueOnError", true);
|
|
|
+ // What this iteration can see is what the nested loop's body
|
|
|
+ // should see too, minus anything the inner loop redefines.
|
|
|
+ inner_ctx.inherited = loop_input;
|
|
|
|
|
|
const std::string inner_key_prefix =
|
|
|
key_prefix + body_node_id + "_iter_" + std::to_string(i) + "_";
|
|
|
@@ -4100,6 +4107,49 @@ nlohmann::json WorkflowEngine::evaluateJavaScriptExpression(
|
|
|
|
|
|
// Build JavaScript code to evaluate the expression
|
|
|
// Wrap in module.exports.execute format expected by ScriptEngine
|
|
|
+ // A loop may name its item whatever it likes - itemVariableName is part of
|
|
|
+ // the loop node's configuration - and the names above are only the fixed
|
|
|
+ // ones. Without this, a loop over emails whose item is called "email" makes
|
|
|
+ // {{email.from}} an undefined identifier, and the expression yields nothing
|
|
|
+ // at all: an OCR reply came out addressed to "" and the send failed with
|
|
|
+ // "At least one recipient is required".
|
|
|
+ //
|
|
|
+ // Every remaining input key that is a usable identifier is bound, so both
|
|
|
+ // the canonical loop.item form and the author's chosen name work.
|
|
|
+ std::string custom_bindings;
|
|
|
+ if (input.is_object()) {
|
|
|
+ static const std::set<std::string> already_bound = {
|
|
|
+ "item", "index", "currentItem", "currentIndex", "totalItems",
|
|
|
+ "isFirst", "isLast", "loop", "data"
|
|
|
+ };
|
|
|
+ // Binding one of these would be a syntax error and take the whole
|
|
|
+ // expression down with it.
|
|
|
+ static const std::set<std::string> reserved = {
|
|
|
+ "break", "case", "catch", "class", "const", "continue", "debugger",
|
|
|
+ "default", "delete", "do", "else", "export", "extends", "finally",
|
|
|
+ "for", "function", "if", "import", "in", "instanceof", "new",
|
|
|
+ "return", "super", "switch", "this", "throw", "try", "typeof",
|
|
|
+ "var", "void", "while", "with", "yield", "let", "static", "enum",
|
|
|
+ "await", "implements", "package", "protected", "interface",
|
|
|
+ "private", "public", "null", "true", "false", "undefined",
|
|
|
+ "arguments", "eval", "input", "config", "context", "module",
|
|
|
+ "exports", "execute"
|
|
|
+ };
|
|
|
+ for (const auto& [key, value] : input.items()) {
|
|
|
+ if (already_bound.count(key) || reserved.count(key)) continue;
|
|
|
+ if (key.empty()) continue;
|
|
|
+ const bool valid_start = std::isalpha(static_cast<unsigned char>(key[0])) ||
|
|
|
+ key[0] == '_' || key[0] == '$';
|
|
|
+ if (!valid_start) continue;
|
|
|
+ const bool valid = std::all_of(key.begin(), key.end(), [](unsigned char c) {
|
|
|
+ return std::isalnum(c) || c == '_' || c == '$';
|
|
|
+ });
|
|
|
+ if (!valid) continue;
|
|
|
+ custom_bindings += " const " + key + " = input[" +
|
|
|
+ nlohmann::json(key).dump() + "];\n";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
std::string js_code = R"(
|
|
|
const $node = )" + node_outputs.dump() + R"(;
|
|
|
|
|
|
@@ -4144,7 +4194,7 @@ nlohmann::json WorkflowEngine::evaluateJavaScriptExpression(
|
|
|
// still runs first and keeps producing the bare "item" form for
|
|
|
// expressions written before this existed.
|
|
|
const loop = input.loop || {};
|
|
|
-
|
|
|
+)" + custom_bindings + R"(
|
|
|
return )" + safe_expression + R"(;
|
|
|
}
|
|
|
|