- Frontend v4 accessible sur réseau local (192.168.1.40) - Ports ouverts: 3002 (frontend), 5001 (backend), 5004 (dashboard) - Ollama GPU fonctionnel - Self-healing interactif - Dashboard confiance Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
137 lines
4.5 KiB
Python
137 lines
4.5 KiB
Python
"""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") |