package postgres import ( "context" "encoding/json" "fmt" "time" "github.com/jackc/pgx/v5" "knowfoolery/backend/services/admin-service/internal/domain/audit" sharedpostgres "knowfoolery/backend/shared/infra/database/postgres" ) // AuditRepository implements audit.Repository using pgx. type AuditRepository struct { db *sharedpostgres.Client } func NewAuditRepository(db *sharedpostgres.Client) *AuditRepository { return &AuditRepository{db: db} } func (r *AuditRepository) EnsureSchema(ctx context.Context) error { q := ` CREATE TABLE IF NOT EXISTS admin_audit_logs ( id TEXT PRIMARY KEY, at TIMESTAMPTZ NOT NULL, actor_id TEXT NOT NULL, actor_email TEXT NOT NULL, action TEXT NOT NULL, resource TEXT NOT NULL, details JSONB NULL ); CREATE INDEX IF NOT EXISTS idx_admin_audit_logs_at ON admin_audit_logs(at DESC); ` _, err := r.db.Pool.Exec(ctx, q) return err } func (r *AuditRepository) Append(ctx context.Context, e audit.Entry) error { var detailsJSON []byte if e.Details != nil { b, err := json.Marshal(e.Details) if err != nil { return fmt.Errorf("marshal details: %w", err) } detailsJSON = b } _, err := r.db.Pool.Exec(ctx, `INSERT INTO admin_audit_logs(id, at, actor_id, actor_email, action, resource, details) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT (id) DO NOTHING`, e.ID, e.At, e.ActorID, e.ActorEmail, e.Action, e.Resource, detailsJSON, ) return err } func (r *AuditRepository) List(ctx context.Context, limit, offset int) ([]audit.Entry, error) { if limit <= 0 { limit = 50 } if limit > 200 { limit = 200 } if offset < 0 { offset = 0 } rows, err := r.db.Pool.Query(ctx, `SELECT id, at, actor_id, actor_email, action, resource, details FROM admin_audit_logs ORDER BY at DESC LIMIT $1 OFFSET $2`, limit, offset, ) if err != nil { return nil, err } defer rows.Close() var out []audit.Entry for rows.Next() { var e audit.Entry var detailsBytes []byte if err := rows.Scan(&e.ID, &e.At, &e.ActorID, &e.ActorEmail, &e.Action, &e.Resource, &detailsBytes); err != nil { return nil, err } if len(detailsBytes) > 0 { var v any if err := json.Unmarshal(detailsBytes, &v); err == nil { e.Details = v } } out = append(out, e) } return out, rows.Err() } func (r *AuditRepository) Count(ctx context.Context) (int64, error) { var n int64 err := r.db.Pool.QueryRow(ctx, `SELECT COUNT(*) FROM admin_audit_logs`).Scan(&n) return n, err } func (r *AuditRepository) PruneBefore(ctx context.Context, before time.Time) (int64, error) { ct, err := r.db.Pool.Exec(ctx, `DELETE FROM admin_audit_logs WHERE at < $1`, before) if err != nil { return 0, err } return ct.RowsAffected(), nil } var _ audit.Repository = (*AuditRepository)(nil) // Compile-time check for pgx import. var _ = pgx.ErrNoRows