From fe6764df9ac3eab31825e01023ca41ba5d0990a6 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Sat, 2 May 2026 21:18:10 +0200 Subject: [PATCH] fix: ensure JSONB array fields are always arrays in control API Backend: _ensure_list() converts null/string/malformed JSONB to [] for requirements, test_procedure, evidence, open_anchors, tags. Frontend: defensive Array.isArray() check on ControlDetail.tsx. Fixes: TypeError: A.requirements.map is not a function Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/ControlDetail.tsx | 4 +-- .../services/canonical_control_service.py | 28 +++++++++++++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/admin-compliance/app/sdk/control-library/components/ControlDetail.tsx b/admin-compliance/app/sdk/control-library/components/ControlDetail.tsx index 0297d1c..731d2e1 100644 --- a/admin-compliance/app/sdk/control-library/components/ControlDetail.tsx +++ b/admin-compliance/app/sdk/control-library/components/ControlDetail.tsx @@ -212,14 +212,14 @@ export function ControlDetail({ ) : null} - {ctrl.requirements.length > 0 && ( + {Array.isArray(ctrl.requirements) && ctrl.requirements.length > 0 && (

Anforderungen

    {ctrl.requirements.map((r, i) =>
  1. {r}
  2. )}
)} - {ctrl.test_procedure.length > 0 && ( + {Array.isArray(ctrl.test_procedure) && ctrl.test_procedure.length > 0 && (

Pruefverfahren

    {ctrl.test_procedure.map((s, i) =>
  1. {s}
  2. )}
diff --git a/backend-compliance/compliance/services/canonical_control_service.py b/backend-compliance/compliance/services/canonical_control_service.py index 6a926ba..23820f0 100644 --- a/backend-compliance/compliance/services/canonical_control_service.py +++ b/backend-compliance/compliance/services/canonical_control_service.py @@ -40,6 +40,22 @@ _CONTROL_COLUMNS = """ """ +def _ensure_list(val: Any) -> list: + """Ensure a JSONB value is always a Python list.""" + if isinstance(val, list): + return val + if val is None: + return [] + if isinstance(val, str): + try: + import json + parsed = json.loads(val) + return parsed if isinstance(parsed, list) else [] + except (json.JSONDecodeError, TypeError): + return [] + return [] + + def _control_row(r: Any) -> dict[str, Any]: """Serialize a canonical_controls SELECT row to a response dict.""" return { @@ -49,19 +65,19 @@ def _control_row(r: Any) -> dict[str, Any]: "title": r.title, "objective": r.objective, "rationale": r.rationale, - "scope": r.scope, - "requirements": r.requirements, - "test_procedure": r.test_procedure, - "evidence": r.evidence, + "scope": r.scope if isinstance(r.scope, dict) else {}, + "requirements": _ensure_list(r.requirements), + "test_procedure": _ensure_list(r.test_procedure), + "evidence": _ensure_list(r.evidence), "severity": r.severity, "risk_score": float(r.risk_score) if r.risk_score is not None else None, "implementation_effort": r.implementation_effort, "evidence_confidence": ( float(r.evidence_confidence) if r.evidence_confidence is not None else None ), - "open_anchors": r.open_anchors, + "open_anchors": _ensure_list(r.open_anchors), "release_state": r.release_state, - "tags": r.tags or [], + "tags": _ensure_list(r.tags), "created_at": r.created_at.isoformat() if r.created_at else None, "updated_at": r.updated_at.isoformat() if r.updated_at else None, }