Vor Board-Flip bemerkt: Akzeptanzkriterium 1 fordert Objekttyp MIT Aufbewahrungsklasse UND Rueckruf-Adresse, retention_class fehlte im ersten Entwurf komplett. Migration, Registration-Struct, Register, ListRegistrations und RegisterHandler ergaenzt, Tests angepasst (Idempotenz jetzt auch fuer retention_class geprueft, nicht nur callback_url). Real auf 131 gedroppt und neu angewendet.
122 lines
5.1 KiB
Go
122 lines
5.1 KiB
Go
// Package moduleadapter implementiert RET-05: die Schnittstelle, über
|
|
// die DMS und Mail ihre Objekttypen bei Archive registrieren, statt
|
|
// eigene Retention-Logik zu bauen. BEWUSST NUR DAS INTERFACE UND
|
|
// ARCHIVES EIGENE SEITE (Registrierungs-API + Rückruf-Auslöser) — die
|
|
// eigentlichen Rückruf-EMPFÄNGER (DMS'/Mails Löschbestätigungs-Endpunkte)
|
|
// sind NICHT Teil dieses Tickets, damit spätere DMS-/Mail-Kacheln
|
|
// gegen ein bereits feststehendes, nicht nachträglich verändertes
|
|
// Interface bauen (Nutzervorgabe).
|
|
package moduleadapter
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// Registration ist EIN registrierter Objekttyp eines Moduls.
|
|
type Registration struct {
|
|
ID string
|
|
ModuleName string
|
|
ObjectType string
|
|
RetentionClass string
|
|
CallbackURL string
|
|
}
|
|
|
|
// Register registriert einen Objekttyp eines Moduls mit Rückruf-Adresse
|
|
// für Löschbestätigung — mandantenfähig durch Modell C (physisch
|
|
// isolierte Tenant-DB, kein tenant_id-Feld nötig). Idempotent im Sinne
|
|
// von Akzeptanzkriterium 3: eine erneute Registrierung DESSELBEN
|
|
// Objekttyps ändert NICHTS am bestehenden Zustand (kein Überschreiben
|
|
// der callback_url) — anders als RET-01s RegisterObject (dort
|
|
// UPSERT-artig), weil ein bereits registrierter Rückruf nicht
|
|
// stillschweigend durch eine zweite, möglicherweise abweichende
|
|
// Registrierung ersetzt werden darf.
|
|
func Register(ctx context.Context, pool *pgxpool.Pool, moduleName, objectType, retentionClass, callbackURL string) (Registration, error) {
|
|
var reg Registration
|
|
err := pool.QueryRow(ctx, `
|
|
INSERT INTO module_registrations (module_name, object_type, retention_class, callback_url)
|
|
VALUES ($1, $2, $3, $4)
|
|
ON CONFLICT (module_name, object_type) DO NOTHING
|
|
RETURNING id, module_name, object_type, retention_class, callback_url
|
|
`, moduleName, objectType, retentionClass, callbackURL).Scan(®.ID, ®.ModuleName, ®.ObjectType, ®.RetentionClass, ®.CallbackURL)
|
|
if err == nil {
|
|
return reg, nil
|
|
}
|
|
// ON CONFLICT DO NOTHING liefert keine Zeile zurueck (pgx: ErrNoRows) -
|
|
// bestehende Registrierung unveraendert nachlesen und zurueckgeben.
|
|
err = pool.QueryRow(ctx, `
|
|
SELECT id, module_name, object_type, retention_class, callback_url FROM module_registrations
|
|
WHERE module_name = $1 AND object_type = $2
|
|
`, moduleName, objectType).Scan(®.ID, ®.ModuleName, ®.ObjectType, ®.RetentionClass, ®.CallbackURL)
|
|
if err != nil {
|
|
return Registration{}, fmt.Errorf("moduleadapter: registrierung lesen/anlegen: %w", err)
|
|
}
|
|
return reg, nil
|
|
}
|
|
|
|
// ListRegistrations liefert alle registrierten Objekttypen — Grundlage
|
|
// für Statusübersichten und Tests (Pflichtprüfung 1: zwei Module
|
|
// parallel registriert ohne Kollision).
|
|
func ListRegistrations(ctx context.Context, pool *pgxpool.Pool) ([]Registration, error) {
|
|
rows, err := pool.Query(ctx, `SELECT id, module_name, object_type, retention_class, callback_url FROM module_registrations ORDER BY module_name, object_type`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("moduleadapter: registrierungen auflisten: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var regs []Registration
|
|
for rows.Next() {
|
|
var r Registration
|
|
if err := rows.Scan(&r.ID, &r.ModuleName, &r.ObjectType, &r.RetentionClass, &r.CallbackURL); err != nil {
|
|
return nil, fmt.Errorf("moduleadapter: registrierungs-zeile lesen: %w", err)
|
|
}
|
|
regs = append(regs, r)
|
|
}
|
|
return regs, rows.Err()
|
|
}
|
|
|
|
// DestructionNotice ist der Rückruf-Payload bei Vernichtung eines
|
|
// Objekts (Akzeptanzkriterium 2) — das feststehende Vertragsformat, das
|
|
// jeder Modul-Rückruf-Empfänger erwarten muss.
|
|
type DestructionNotice struct {
|
|
ObjectType string `json:"object_type"`
|
|
ObjectReference string `json:"object_reference"`
|
|
DestroyedAt time.Time `json:"destroyed_at"`
|
|
}
|
|
|
|
// NotifyDestruction ruft das registrierte Modul beim Vernichten eines
|
|
// Objekts zurück, statt dass Archive selbst Modul-Interna kennen müsste
|
|
// (Akzeptanzkriterium 2). Fehlerverhalten: liefert den Fehler an den
|
|
// Aufrufer zurück, statt ihn zu verschlucken — ein fehlgeschlagener
|
|
// Rückruf ist ein Fehlerzustand, der behandelt/wiederholt werden muss
|
|
// (Wiederholungslogik ist NICHT Teil dieses Tickets, nur der
|
|
// Interface-Vertrag: Erfolg = HTTP 2xx, sonst Fehler).
|
|
func NotifyDestruction(ctx context.Context, client *http.Client, callbackURL string, notice DestructionNotice) error {
|
|
body, err := json.Marshal(notice)
|
|
if err != nil {
|
|
return fmt.Errorf("moduleadapter: rückruf-payload kodieren: %w", err)
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, callbackURL, bytes.NewReader(body))
|
|
if err != nil {
|
|
return fmt.Errorf("moduleadapter: rückruf-request erstellen: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("moduleadapter: rückruf fehlgeschlagen: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return fmt.Errorf("moduleadapter: rückruf-endpunkt antwortete mit status %d", resp.StatusCode)
|
|
}
|
|
return nil
|
|
}
|