# Copyright (c) 2025 Patrick Motsch # Data manipulation node executor: data.aggregate, data.transform, data.filter. import logging from typing import Any, Dict from modules.features.graphicalEditor.portTypes import _unwrapTransit, _wrapTransit logger = logging.getLogger(__name__) class DataExecutor: """Execute data.aggregate, data.transform, data.filter nodes.""" async def execute( self, node: Dict[str, Any], context: Dict[str, Any], ) -> Any: nodeType = node.get("type", "") nodeId = node.get("id", "") nodeOutputs = context.get("nodeOutputs", {}) inputSources = context.get("inputSources", {}).get(nodeId, {}) logger.info("DataExecutor node %s type=%s", nodeId, nodeType) if nodeType == "data.aggregate": return await self._aggregate(node, nodeOutputs, nodeId, inputSources, context) if nodeType == "data.transform": return await self._transform(node, nodeOutputs, nodeId, inputSources) if nodeType == "data.filter": return await self._filter(node, nodeOutputs, nodeId, inputSources) logger.debug("DataExecutor node %s unhandled type %s", nodeId, nodeType) return None async def _aggregate( self, node: Dict, nodeOutputs: Dict, nodeId: str, inputSources: Dict, context: Dict, ) -> Any: """ In loop context: accumulation is handled by the engine (_aggregateAccumulators). Outside loop: collect the single input. """ inp = self._getInput(inputSources, nodeOutputs) mode = (node.get("parameters") or {}).get("mode", "collect") if inp is None: return {"items": [], "count": 0, "_success": True} data = _unwrapTransit(inp) if isinstance(inp, dict) and inp.get("_transit") else inp if mode == "collect": items = [data] if data is not None else [] elif mode == "concat": items = data if isinstance(data, list) else [data] if data is not None else [] elif mode == "sum": val = data if isinstance(data, (int, float)) else 0 items = [val] elif mode == "count": items = [1] if data is not None else [] else: items = [data] if data is not None else [] return {"items": items, "count": len(items), "_success": True} async def _transform( self, node: Dict, nodeOutputs: Dict, nodeId: str, inputSources: Dict, ) -> Any: """Apply mappings to restructure data.""" from modules.workflows.automation2.graphUtils import resolveParameterReferences inp = self._getInput(inputSources, nodeOutputs) data = _unwrapTransit(inp) if isinstance(inp, dict) and inp.get("_transit") else inp mappings = (node.get("parameters") or {}).get("mappings", []) result = {} for mapping in mappings: if not isinstance(mapping, dict): continue outputField = mapping.get("outputField") if not outputField: continue source = mapping.get("source") if source and isinstance(source, dict) and source.get("type") == "ref": resolved = resolveParameterReferences(source, nodeOutputs) result[outputField] = resolved elif source and isinstance(source, dict) and source.get("type") == "value": result[outputField] = source.get("value") elif isinstance(data, dict) and mapping.get("sourceField"): result[outputField] = data.get(mapping["sourceField"]) else: result[outputField] = source result["_success"] = True return result async def _filter( self, node: Dict, nodeOutputs: Dict, nodeId: str, inputSources: Dict, ) -> Any: """Filter items by condition expression. Returns Transit envelope.""" inp = self._getInput(inputSources, nodeOutputs) data = _unwrapTransit(inp) if isinstance(inp, dict) and inp.get("_transit") else inp condition = (node.get("parameters") or {}).get("condition", "") items = self._extractItems(data) originalCount = len(items) if not condition: filtered = items else: filtered = [item for item in items if self._evalFilterCondition(item, condition)] filteredData = data if isinstance(data, dict): filteredData = dict(data) listKey = self._findListKey(data) if listKey: filteredData[listKey] = filtered elif isinstance(data, list): filteredData = filtered return _wrapTransit(filteredData, { "originalCount": originalCount, "filteredCount": len(filtered), }) def _getInput(self, inputSources: Dict, nodeOutputs: Dict) -> Any: """Get data from the first connected input port.""" if 0 not in inputSources: return None srcId, _ = inputSources[0] return nodeOutputs.get(srcId) def _extractItems(self, data: Any) -> list: """Extract the list of items from various data shapes.""" if isinstance(data, list): return data if isinstance(data, dict): for key in ("items", "tasks", "emails", "files", "documents", "documentList"): val = data.get(key) if isinstance(val, list): return val return [] def _findListKey(self, data: Dict) -> str: """Find the key that holds the main list in a dict.""" for key in ("items", "tasks", "emails", "files", "documents", "documentList"): if isinstance(data.get(key), list): return key return "" def _evalFilterCondition(self, item: Any, condition: Any) -> bool: """ Evaluate a filter condition against a single item. Supports structured conditions {field, operator, value} or simple string expressions. """ if isinstance(condition, dict): field = condition.get("field", "") operator = condition.get("operator", "eq") value = condition.get("value") left = item.get(field) if isinstance(item, dict) else item return self._compareValues(left, operator, value) if isinstance(condition, str) and condition.strip(): try: if isinstance(item, dict): return bool(eval(condition, {"__builtins__": {}}, item)) return bool(item) except Exception: return True return True def _compareValues(self, left: Any, operator: str, right: Any) -> bool: """Compare two values with the given operator.""" if operator == "eq": return left == right if operator == "neq": return left != right if operator == "contains": return right is not None and str(right) in str(left or "") if operator == "startsWith": return str(left or "").startswith(str(right or "")) if operator == "isEmpty": return left is None or left == "" or (isinstance(left, (list, dict)) and len(left) == 0) if operator == "isNotEmpty": return left is not None and left != "" and (not isinstance(left, (list, dict)) or len(left) > 0) if operator in ("lt", "lte", "gt", "gte"): try: l = float(left) if left is not None else 0 r = float(right) if right is not None else 0 if operator == "lt": return l < r if operator == "lte": return l <= r if operator == "gt": return l > r return l >= r except (TypeError, ValueError): return False return True