diff --git a/ai-compliance-sdk/internal/api/handlers/iace_handler_hazards.go b/ai-compliance-sdk/internal/api/handlers/iace_handler_hazards.go index e24d496..322b485 100644 --- a/ai-compliance-sdk/internal/api/handlers/iace_handler_hazards.go +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_hazards.go @@ -148,19 +148,19 @@ func (h *IACEHandler) ListHazards(c *gin.Context) { hazards = []iace.Hazard{} } - // Enrich hazards with latest risk assessment + // Enrich hazards with latest risk assessment (single batch query instead of N+1) type enrichedHazard struct { iace.Hazard RiskAssessment interface{} `json:"risk_assessment"` } + assessmentMap, _ := h.store.GetLatestAssessmentsByProject(c.Request.Context(), projectID) + enriched := make([]enrichedHazard, len(hazards)) for i, hz := range hazards { enriched[i] = enrichedHazard{Hazard: hz} - // Get latest assessment for this hazard - assessments, err := h.store.ListAssessments(c.Request.Context(), hz.ID) - if err == nil && len(assessments) > 0 { - enriched[i].RiskAssessment = assessments[len(assessments)-1] + if ra, ok := assessmentMap[hz.ID]; ok { + enriched[i].RiskAssessment = ra } } diff --git a/ai-compliance-sdk/internal/iace/store_hazards.go b/ai-compliance-sdk/internal/iace/store_hazards.go index bd1ecb0..142b0e3 100644 --- a/ai-compliance-sdk/internal/iace/store_hazards.go +++ b/ai-compliance-sdk/internal/iace/store_hazards.go @@ -302,6 +302,49 @@ func (s *Store) ListAssessments(ctx context.Context, hazardID uuid.UUID) ([]Risk return assessments, nil } +// GetLatestAssessmentsByProject fetches the latest risk assessment for ALL hazards +// of a project in a single query. Returns map[hazardID]RiskAssessment. +func (s *Store) GetLatestAssessmentsByProject(ctx context.Context, projectID uuid.UUID) (map[uuid.UUID]RiskAssessment, error) { + rows, err := s.pool.Query(ctx, ` + SELECT DISTINCT ON (ra.hazard_id) + ra.id, ra.hazard_id, ra.version, ra.assessment_type, + ra.severity, ra.exposure, ra.probability, + ra.inherent_risk, ra.control_maturity, ra.control_coverage, + ra.test_evidence_strength, ra.c_eff, ra.residual_risk, + ra.risk_level, ra.is_acceptable, ra.acceptance_justification, + ra.assessed_by, ra.created_at + FROM iace_risk_assessments ra + JOIN iace_hazards h ON h.id = ra.hazard_id + WHERE h.project_id = $1 + ORDER BY ra.hazard_id, ra.version DESC, ra.created_at DESC + `, projectID) + if err != nil { + return nil, fmt.Errorf("get latest assessments by project: %w", err) + } + defer rows.Close() + + result := make(map[uuid.UUID]RiskAssessment) + for rows.Next() { + var a RiskAssessment + var assessmentType, riskLevel string + err := rows.Scan( + &a.ID, &a.HazardID, &a.Version, &assessmentType, + &a.Severity, &a.Exposure, &a.Probability, + &a.InherentRisk, &a.ControlMaturity, &a.ControlCoverage, + &a.TestEvidenceStrength, &a.CEff, &a.ResidualRisk, + &riskLevel, &a.IsAcceptable, &a.AcceptanceJustification, + &a.AssessedBy, &a.CreatedAt, + ) + if err != nil { + return nil, fmt.Errorf("scan assessment: %w", err) + } + a.AssessmentType = AssessmentType(assessmentType) + a.RiskLevel = RiskLevel(riskLevel) + result[a.HazardID] = a + } + return result, nil +} + // ============================================================================ // Risk Summary (Aggregated View) // ============================================================================