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:
193
internal/store/crud_test.go
Normal file
193
internal/store/crud_test.go
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
package store_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/netcell-it/deklarix/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func openTestStore(t *testing.T) *store.Store {
|
||||||
|
t.Helper()
|
||||||
|
url := testDatabaseURL(t)
|
||||||
|
if err := store.Migrate(url); err != nil {
|
||||||
|
t.Fatalf("Migrate: %v", err)
|
||||||
|
}
|
||||||
|
s, err := store.Open(context.Background(), url)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Open: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(s.Close)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubmissionCRUD(t *testing.T) {
|
||||||
|
s := openTestStore(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
created, err := s.CreateSubmission(ctx, "instagram", "reel", "Werbung fuer ein Produkt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateSubmission: %v", err)
|
||||||
|
}
|
||||||
|
if created.Status != "draft" {
|
||||||
|
t.Fatalf("Status = %q, want draft", created.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := s.GetSubmission(ctx, created.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetSubmission: %v", err)
|
||||||
|
}
|
||||||
|
if got.Platform != "instagram" || got.Caption != "Werbung fuer ein Produkt" {
|
||||||
|
t.Fatalf("GetSubmission = %+v, unerwartete Werte", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.SetSubmissionStatus(ctx, created.ID, "checked"); err != nil {
|
||||||
|
t.Fatalf("SetSubmissionStatus: %v", err)
|
||||||
|
}
|
||||||
|
got, err = s.GetSubmission(ctx, created.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetSubmission nach Statuswechsel: %v", err)
|
||||||
|
}
|
||||||
|
if got.Status != "checked" {
|
||||||
|
t.Fatalf("Status nach SetSubmissionStatus = %q, want checked", got.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSubmissionNotFound(t *testing.T) {
|
||||||
|
s := openTestStore(t)
|
||||||
|
if _, err := s.GetSubmission(context.Background(), "00000000-0000-0000-0000-000000000000"); err == nil {
|
||||||
|
t.Fatal("expected error for a nonexistent submission, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetSubmissionStatusNotFound(t *testing.T) {
|
||||||
|
s := openTestStore(t)
|
||||||
|
err := s.SetSubmissionStatus(context.Background(), "00000000-0000-0000-0000-000000000000", "checked")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error when updating a nonexistent submission, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractionCRUD(t *testing.T) {
|
||||||
|
s := openTestStore(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
sub, err := s.CreateSubmission(ctx, "tiktok", "video", "...")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateSubmission: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := []byte(`{"gegenleistung":"bezahlt"}`)
|
||||||
|
ext, err := s.CreateExtraction(ctx, sub.ID, payload, "claude-sonnet-5", "v1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateExtraction: %v", err)
|
||||||
|
}
|
||||||
|
// JSONB normalisiert die Textform (z. B. Leerzeichen nach ':'), der Inhalt
|
||||||
|
// muss aber semantisch identisch bleiben — kein Byte-Vergleich.
|
||||||
|
var gotPayload, wantPayload map[string]any
|
||||||
|
if err := json.Unmarshal(ext.Payload, &gotPayload); err != nil {
|
||||||
|
t.Fatalf("unmarshal stored payload: %v", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(payload, &wantPayload); err != nil {
|
||||||
|
t.Fatalf("unmarshal input payload: %v", err)
|
||||||
|
}
|
||||||
|
if gotPayload["gegenleistung"] != wantPayload["gegenleistung"] {
|
||||||
|
t.Fatalf("stored payload = %v, want %v", gotPayload, wantPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := s.GetLatestExtraction(ctx, sub.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetLatestExtraction: %v", err)
|
||||||
|
}
|
||||||
|
if got.ID != ext.ID {
|
||||||
|
t.Fatalf("GetLatestExtraction returned a different row than CreateExtraction")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eine zweite Extraktion (erneute Pruefung) muss die "latest" sein.
|
||||||
|
payload2 := []byte(`{"gegenleistung":"keine"}`)
|
||||||
|
ext2, err := s.CreateExtraction(ctx, sub.ID, payload2, "claude-sonnet-5", "v1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateExtraction (2): %v", err)
|
||||||
|
}
|
||||||
|
got, err = s.GetLatestExtraction(ctx, sub.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetLatestExtraction (2): %v", err)
|
||||||
|
}
|
||||||
|
if got.ID != ext2.ID {
|
||||||
|
t.Fatalf("GetLatestExtraction did not return the most recent extraction")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindingCRUDAndSupersedes(t *testing.T) {
|
||||||
|
s := openTestStore(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
sub, err := s.CreateSubmission(ctx, "instagram", "reel", "...")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateSubmission: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
old, err := s.CreateFinding(ctx, sub.ID, nil, "WK-004", 1, "hoch", "alte Fassung")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateFinding (old): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
current, err := s.ListCurrentFindings(ctx, sub.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListCurrentFindings: %v", err)
|
||||||
|
}
|
||||||
|
if len(current) != 1 || current[0].ID != old.ID {
|
||||||
|
t.Fatalf("expected exactly the old finding before any correction, got %+v", current)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Korrektur: neue Zeile, die die alte per supersedes ersetzt.
|
||||||
|
_, err = s.Pool.Exec(ctx, `
|
||||||
|
INSERT INTO finding (submission_id, rule_id, rule_version, severity, message, supersedes)
|
||||||
|
VALUES ($1, 'WK-004', 2, 'hoch', 'korrigierte Fassung', $2)
|
||||||
|
`, sub.ID, old.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("insert superseding finding: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
current, err = s.ListCurrentFindings(ctx, sub.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListCurrentFindings nach Korrektur: %v", err)
|
||||||
|
}
|
||||||
|
if len(current) != 1 {
|
||||||
|
t.Fatalf("expected exactly one current finding after a correction, got %d: %+v", len(current), current)
|
||||||
|
}
|
||||||
|
if current[0].Message != "korrigierte Fassung" {
|
||||||
|
t.Fatalf("expected the corrected finding to be current, got %+v", current[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvidencePackageCRUD(t *testing.T) {
|
||||||
|
s := openTestStore(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
sub, err := s.CreateSubmission(ctx, "instagram", "reel", "...")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateSubmission: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
token := []byte("fake-rfc3161-token-bytes")
|
||||||
|
pkg, err := s.CreateEvidencePackage(ctx, sub.ID, "/var/lib/deklarix/dossiers/x.pdf", "deadbeef", token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateEvidencePackage: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := s.GetLatestEvidencePackage(ctx, sub.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetLatestEvidencePackage: %v", err)
|
||||||
|
}
|
||||||
|
if got.ID != pkg.ID || string(got.TimestampToken) != string(token) {
|
||||||
|
t.Fatalf("GetLatestEvidencePackage = %+v, unerwartete Werte", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append-only: ein UPDATE muss vom Trigger abgelehnt werden.
|
||||||
|
_, err = s.Pool.Exec(ctx, `UPDATE evidence_package SET sha256 = 'geaendert' WHERE id = $1`, pkg.ID)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected UPDATE on evidence_package to be rejected, but it succeeded")
|
||||||
|
}
|
||||||
|
}
|
||||||
54
internal/store/evidence.go
Normal file
54
internal/store/evidence.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EvidencePackage ist das Ergebnis der Archivierung eines Beitrags:
|
||||||
|
// Dossier-Pfad, Hash der kanonisierten Metadaten und RFC-3161-Token.
|
||||||
|
// Append-only: siehe Migration.
|
||||||
|
type EvidencePackage struct {
|
||||||
|
ID string
|
||||||
|
SubmissionID string
|
||||||
|
DossierPath string
|
||||||
|
SHA256 string
|
||||||
|
TimestampToken []byte
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateEvidencePackage speichert ein EvidencePackage.
|
||||||
|
func (s *Store) CreateEvidencePackage(ctx context.Context, submissionID, dossierPath, sha256Hex string, timestampToken []byte) (EvidencePackage, error) {
|
||||||
|
var e EvidencePackage
|
||||||
|
err := s.Pool.QueryRow(ctx, `
|
||||||
|
INSERT INTO evidence_package (submission_id, dossier_path, sha256, timestamp_token)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING id, submission_id, dossier_path, sha256, timestamp_token, created_at
|
||||||
|
`, submissionID, dossierPath, sha256Hex, timestampToken).Scan(
|
||||||
|
&e.ID, &e.SubmissionID, &e.DossierPath, &e.SHA256, &e.TimestampToken, &e.CreatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return EvidencePackage{}, fmt.Errorf("store: create evidence package: %w", err)
|
||||||
|
}
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLatestEvidencePackage liefert das zuletzt erzeugte EvidencePackage
|
||||||
|
// für einen Beitrag.
|
||||||
|
func (s *Store) GetLatestEvidencePackage(ctx context.Context, submissionID string) (EvidencePackage, error) {
|
||||||
|
var e EvidencePackage
|
||||||
|
err := s.Pool.QueryRow(ctx, `
|
||||||
|
SELECT id, submission_id, dossier_path, sha256, timestamp_token, created_at
|
||||||
|
FROM evidence_package
|
||||||
|
WHERE submission_id = $1
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
`, submissionID).Scan(
|
||||||
|
&e.ID, &e.SubmissionID, &e.DossierPath, &e.SHA256, &e.TimestampToken, &e.CreatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return EvidencePackage{}, fmt.Errorf("store: get latest evidence package: %w", err)
|
||||||
|
}
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
56
internal/store/extraction.go
Normal file
56
internal/store/extraction.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Extraction ist das Ergebnis der Stufe-1-Extraktion für einen Beitrag.
|
||||||
|
// Payload ist das rohe, vom Modell gelieferte JSON — nicht eine
|
||||||
|
// abgeleitete Repräsentation. Append-only: siehe Migration.
|
||||||
|
type Extraction struct {
|
||||||
|
ID string
|
||||||
|
SubmissionID string
|
||||||
|
Payload []byte
|
||||||
|
ModelVersion string
|
||||||
|
PromptVersion string
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateExtraction speichert eine Extraktion. payload ist das rohe
|
||||||
|
// JSON, wie es das Modell zurückgegeben hat (siehe extract.Result.RawJSON).
|
||||||
|
func (s *Store) CreateExtraction(ctx context.Context, submissionID string, payload []byte, modelVersion, promptVersion string) (Extraction, error) {
|
||||||
|
var e Extraction
|
||||||
|
err := s.Pool.QueryRow(ctx, `
|
||||||
|
INSERT INTO extraction (submission_id, payload, model_version, prompt_version)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING id, submission_id, payload, model_version, prompt_version, created_at
|
||||||
|
`, submissionID, payload, modelVersion, promptVersion).Scan(
|
||||||
|
&e.ID, &e.SubmissionID, &e.Payload, &e.ModelVersion, &e.PromptVersion, &e.CreatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return Extraction{}, fmt.Errorf("store: create extraction: %w", err)
|
||||||
|
}
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLatestExtraction liest die zuletzt erzeugte Extraktion für einen
|
||||||
|
// Beitrag (append-only: es kann mehrere geben, z. B. bei einer erneuten
|
||||||
|
// Prüfung — die aktuellste zählt).
|
||||||
|
func (s *Store) GetLatestExtraction(ctx context.Context, submissionID string) (Extraction, error) {
|
||||||
|
var e Extraction
|
||||||
|
err := s.Pool.QueryRow(ctx, `
|
||||||
|
SELECT id, submission_id, payload, model_version, prompt_version, created_at
|
||||||
|
FROM extraction
|
||||||
|
WHERE submission_id = $1
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
`, submissionID).Scan(
|
||||||
|
&e.ID, &e.SubmissionID, &e.Payload, &e.ModelVersion, &e.PromptVersion, &e.CreatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return Extraction{}, fmt.Errorf("store: get latest extraction: %w", err)
|
||||||
|
}
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
66
internal/store/submission.go
Normal file
66
internal/store/submission.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Submission ist ein eingereichter Beitrag.
|
||||||
|
type Submission struct {
|
||||||
|
ID string
|
||||||
|
Platform string
|
||||||
|
PostType string
|
||||||
|
Caption string
|
||||||
|
Status string
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSubmission legt einen neuen Beitrag an (Status "draft").
|
||||||
|
func (s *Store) CreateSubmission(ctx context.Context, platform, postType, caption string) (Submission, error) {
|
||||||
|
var sub Submission
|
||||||
|
err := s.Pool.QueryRow(ctx, `
|
||||||
|
INSERT INTO submission (platform, post_type, caption)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
RETURNING id, platform, post_type, caption, status, created_at, updated_at
|
||||||
|
`, platform, postType, caption).Scan(
|
||||||
|
&sub.ID, &sub.Platform, &sub.PostType, &sub.Caption, &sub.Status, &sub.CreatedAt, &sub.UpdatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return Submission{}, fmt.Errorf("store: create submission: %w", err)
|
||||||
|
}
|
||||||
|
return sub, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSubmission liest einen Beitrag anhand seiner ID.
|
||||||
|
func (s *Store) GetSubmission(ctx context.Context, id string) (Submission, error) {
|
||||||
|
var sub Submission
|
||||||
|
err := s.Pool.QueryRow(ctx, `
|
||||||
|
SELECT id, platform, post_type, caption, status, created_at, updated_at
|
||||||
|
FROM submission WHERE id = $1
|
||||||
|
`, id).Scan(
|
||||||
|
&sub.ID, &sub.Platform, &sub.PostType, &sub.Caption, &sub.Status, &sub.CreatedAt, &sub.UpdatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return Submission{}, fmt.Errorf("store: get submission: %w", err)
|
||||||
|
}
|
||||||
|
return sub, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSubmissionStatus setzt den Status eines Beitrags (submission ist,
|
||||||
|
// anders als extraction/finding/evidence_package, NICHT append-only —
|
||||||
|
// der Lebenszyklus draft → checked → published → archived ist eine
|
||||||
|
// normale Zustandsänderung, kein Beweis-Eintrag).
|
||||||
|
func (s *Store) SetSubmissionStatus(ctx context.Context, id, status string) error {
|
||||||
|
tag, err := s.Pool.Exec(ctx, `
|
||||||
|
UPDATE submission SET status = $2, updated_at = now() WHERE id = $1
|
||||||
|
`, id, status)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("store: set submission status: %w", err)
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return fmt.Errorf("store: set submission status: submission %s nicht gefunden", id)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user