feat: add store CRUD methods for submission/extraction/finding/evidence
Create/Get for submission (including the one legitimate status transition — submission is not append-only, unlike the other three), Create/GetLatest for extraction and evidence_package, Create for finding plus ListCurrentFindings which applies the anti-join documented in the migration (a finding referenced by another row's `supersedes` is not "current"). Tested against real Postgres, including that the append-only trigger still rejects UPDATE on evidence_package via this new code path, and that ListCurrentFindings actually hides a finding once a correction supersedes it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
67
internal/store/finding.go
Normal file
67
internal/store/finding.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Finding ist das Ergebnis einer Regel für einen Beitrag. Append-only:
|
||||
// siehe Migration. ExtractionID ist optional (nil wenn ein Finding nicht
|
||||
// direkt aus einer Extraktion, sondern z. B. manuell erzeugt wurde).
|
||||
type Finding struct {
|
||||
ID string
|
||||
SubmissionID string
|
||||
ExtractionID *string
|
||||
RuleID string
|
||||
RuleVersion int
|
||||
Severity string
|
||||
Message string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// CreateFinding speichert ein Finding.
|
||||
func (s *Store) CreateFinding(ctx context.Context, submissionID string, extractionID *string, ruleID string, ruleVersion int, severity, message string) (Finding, error) {
|
||||
var f Finding
|
||||
err := s.Pool.QueryRow(ctx, `
|
||||
INSERT INTO finding (submission_id, extraction_id, rule_id, rule_version, severity, message)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, submission_id, extraction_id, rule_id, rule_version, severity, message, created_at
|
||||
`, submissionID, extractionID, ruleID, ruleVersion, severity, message).Scan(
|
||||
&f.ID, &f.SubmissionID, &f.ExtractionID, &f.RuleID, &f.RuleVersion, &f.Severity, &f.Message, &f.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return Finding{}, fmt.Errorf("store: create finding: %w", err)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// ListCurrentFindings liefert die aktuell gültigen Findings eines
|
||||
// Beitrags — Zeilen, die von keiner anderen Zeile per supersedes
|
||||
// ersetzt wurden (siehe Migrationskommentar zu finding.supersedes).
|
||||
func (s *Store) ListCurrentFindings(ctx context.Context, submissionID string) ([]Finding, error) {
|
||||
rows, err := s.Pool.Query(ctx, `
|
||||
SELECT f.id, f.submission_id, f.extraction_id, f.rule_id, f.rule_version, f.severity, f.message, f.created_at
|
||||
FROM finding f
|
||||
WHERE f.submission_id = $1
|
||||
AND NOT EXISTS (SELECT 1 FROM finding f2 WHERE f2.supersedes = f.id)
|
||||
ORDER BY f.created_at
|
||||
`, submissionID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("store: list current findings: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var findings []Finding
|
||||
for rows.Next() {
|
||||
var f Finding
|
||||
if err := rows.Scan(&f.ID, &f.SubmissionID, &f.ExtractionID, &f.RuleID, &f.RuleVersion, &f.Severity, &f.Message, &f.CreatedAt); err != nil {
|
||||
return nil, fmt.Errorf("store: scan finding: %w", err)
|
||||
}
|
||||
findings = append(findings, f)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("store: list current findings: %w", err)
|
||||
}
|
||||
return findings, nil
|
||||
}
|
||||
Reference in New Issue
Block a user