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) <noreply@anthropic.com>
This commit is contained in:
@@ -212,14 +212,14 @@ export function ControlDetail({
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{ctrl.requirements.length > 0 && (
|
||||
{Array.isArray(ctrl.requirements) && ctrl.requirements.length > 0 && (
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2">Anforderungen</h3>
|
||||
<ol className="list-decimal list-inside space-y-1">{ctrl.requirements.map((r, i) => <li key={i} className="text-sm text-gray-700">{r}</li>)}</ol>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{ctrl.test_procedure.length > 0 && (
|
||||
{Array.isArray(ctrl.test_procedure) && ctrl.test_procedure.length > 0 && (
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2">Pruefverfahren</h3>
|
||||
<ol className="list-decimal list-inside space-y-1">{ctrl.test_procedure.map((s, i) => <li key={i} className="text-sm text-gray-700">{s}</li>)}</ol>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user