"""backend/api/validation.py Validation légère pour les payloads de l'API workflows. Auteur : Dom, Alice, Kiro - 08 janvier 2026 Patch #1: - Ce module manquait et bloquait le boot via api/__init__.py - On reste volontairement permissif (on valide les essentiels) """ from __future__ import annotations from typing import Any, Dict, Iterable from .errors import ValidationError _ALLOWED_UPDATE_FIELDS = { "name", "description", "nodes", "edges", "variables", "settings", "tags", "category", "is_template", # Champs additionnels envoyés par le frontend VWB "id", "steps", "connections", } def _ensure_dict(data: Any, context: str = "payload") -> Dict[str, Any]: """S'assure que les données sont un dictionnaire.""" if not isinstance(data, dict): raise ValidationError(f"{context} doit être un objet") return data def _ensure_list(value: Any, context: str) -> Iterable[Any]: """S'assure que la valeur est une liste.""" if value is None: return [] if not isinstance(value, list): raise ValidationError(f"{context} doit être un tableau") return value def validate_workflow_data(data: Any) -> None: """Valide les données d'un workflow lors de la création.""" data = _ensure_dict(data, "workflow") # Champs requis (création) if "name" not in data or not str(data.get("name") or "").strip(): raise ValidationError("Le champ 'name' est requis") if "created_by" not in data or not str(data.get("created_by") or "").strip(): raise ValidationError("Le champ 'created_by' est requis") # Champs structurés optionnels if "nodes" in data: for n in _ensure_list(data.get("nodes"), "nodes"): validate_node_data(n) if "edges" in data: for e in _ensure_list(data.get("edges"), "edges"): validate_edge_data(e) if "variables" in data: for v in _ensure_list(data.get("variables"), "variables"): validate_variable_data(v) if "settings" in data and data.get("settings") is not None: validate_settings_data(data.get("settings")) if "tags" in data and data.get("tags") is not None and not isinstance(data.get("tags"), list): raise ValidationError("Le champ 'tags' doit être un tableau") def validate_update_data(data: Any) -> None: """Valide les données d'un workflow lors de la mise à jour.""" data = _ensure_dict(data, "update") unknown = set(data.keys()) - _ALLOWED_UPDATE_FIELDS if unknown: raise ValidationError(f"Champ(s) inconnu(s) dans la mise à jour: {', '.join(sorted(unknown))}") if "nodes" in data: for n in _ensure_list(data.get("nodes"), "nodes"): validate_node_data(n) if "edges" in data: for e in _ensure_list(data.get("edges"), "edges"): validate_edge_data(e) if "variables" in data: for v in _ensure_list(data.get("variables"), "variables"): validate_variable_data(v) if "settings" in data and data.get("settings") is not None: validate_settings_data(data.get("settings")) if "tags" in data and data.get("tags") is not None and not isinstance(data.get("tags"), list): raise ValidationError("Le champ 'tags' doit être un tableau") def validate_node_data(node: Any) -> None: """Valide les données d'un nœud.""" node = _ensure_dict(node, "node") if "id" not in node or not str(node.get("id") or "").strip(): raise ValidationError("Le champ 'id' du nœud est requis") if "type" not in node or not str(node.get("type") or "").strip(): # Les nœuds ReactFlow ont toujours un type (default est ok) raise ValidationError("Le champ 'type' du nœud est requis") def validate_edge_data(edge: Any) -> None: """Valide les données d'une connexion.""" edge = _ensure_dict(edge, "edge") if "source" not in edge or not str(edge.get("source") or "").strip(): raise ValidationError("Le champ 'source' de la connexion est requis") if "target" not in edge or not str(edge.get("target") or "").strip(): raise ValidationError("Le champ 'target' de la connexion est requis") def validate_variable_data(variable: Any) -> None: """Valide les données d'une variable.""" variable = _ensure_dict(variable, "variable") if "name" not in variable and "key" not in variable: raise ValidationError("Le champ 'name' de la variable est requis") def validate_settings_data(settings: Any) -> None: """Valide les données des paramètres.""" _ensure_dict(settings, "settings")