You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

155 lines
4.2 KiB
Go

package storage
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/denisovdennis/autohero/internal/model"
)
// LogEntry represents a single adventure log row for API/JSON.
type LogEntry struct {
ID int64 `json:"id"`
HeroID int64 `json:"heroId"`
Message string `json:"message"`
CreatedAt time.Time `json:"createdAt"`
Event *model.AdventureLogEvent `json:"event,omitempty"`
}
// LogStore handles adventure log CRUD operations against PostgreSQL.
type LogStore struct {
pool *pgxpool.Pool
}
// NewLogStore creates a new LogStore backed by the given connection pool.
func NewLogStore(pool *pgxpool.Pool) *LogStore {
return &LogStore{pool: pool}
}
// Add inserts a new adventure log entry for the given hero.
func (s *LogStore) Add(ctx context.Context, heroID int64, line model.AdventureLogLine) error {
var code *string
var argsJSON []byte
var err error
if line.Event != nil {
c := line.Event.Code
code = &c
if line.Event.Args != nil {
argsJSON, err = json.Marshal(line.Event.Args)
if err != nil {
return fmt.Errorf("marshal event args: %w", err)
}
}
}
msg := strings.TrimSpace(line.Message)
_, err = s.pool.Exec(ctx,
`INSERT INTO adventure_log (hero_id, message, event_code, event_args) VALUES ($1, $2, $3, $4)`,
heroID, msg, code, argsJSON,
)
if err != nil {
return fmt.Errorf("add log entry: %w", err)
}
return nil
}
func logEntryFromScan(id int64, heroID int64, message string, createdAt time.Time, code *string, argsBytes []byte) LogEntry {
e := LogEntry{ID: id, HeroID: heroID, Message: message, CreatedAt: createdAt}
if code != nil && *code != "" {
ev := model.AdventureLogEvent{Code: *code}
if len(argsBytes) > 0 {
if err := json.Unmarshal(argsBytes, &ev.Args); err != nil {
ev.Args = map[string]any{"_raw": string(argsBytes)}
}
}
e.Event = &ev
}
return e
}
// GetSince returns log entries for a hero created after the given timestamp,
// ordered oldest-first (chronological). Used to build offline reports from
// real adventure log entries written by the offline simulator.
func (s *LogStore) GetSince(ctx context.Context, heroID int64, since time.Time, limit int) ([]LogEntry, error) {
if limit <= 0 {
limit = 200
}
if limit > 500 {
limit = 500
}
rows, err := s.pool.Query(ctx, `
SELECT id, hero_id, message, created_at, event_code, event_args
FROM adventure_log
WHERE hero_id = $1 AND created_at > $2
ORDER BY created_at ASC
LIMIT $3
`, heroID, since, limit)
if err != nil {
return nil, fmt.Errorf("get log since: %w", err)
}
defer rows.Close()
var entries []LogEntry
for rows.Next() {
var e LogEntry
var code *string
var argsBytes []byte
if err := rows.Scan(&e.ID, &e.HeroID, &e.Message, &e.CreatedAt, &code, &argsBytes); err != nil {
return nil, fmt.Errorf("scan log entry: %w", err)
}
entries = append(entries, logEntryFromScan(e.ID, e.HeroID, e.Message, e.CreatedAt, code, argsBytes))
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("log since rows: %w", err)
}
if entries == nil {
entries = []LogEntry{}
}
return entries, nil
}
// GetRecent returns the most recent log entries for a hero, ordered newest-first.
func (s *LogStore) GetRecent(ctx context.Context, heroID int64, limit int) ([]LogEntry, error) {
if limit <= 0 {
limit = 50
}
if limit > 200 {
limit = 200
}
rows, err := s.pool.Query(ctx, `
SELECT id, hero_id, message, created_at, event_code, event_args
FROM adventure_log
WHERE hero_id = $1
ORDER BY created_at DESC
LIMIT $2
`, heroID, limit)
if err != nil {
return nil, fmt.Errorf("get recent log: %w", err)
}
defer rows.Close()
var entries []LogEntry
for rows.Next() {
var e LogEntry
var code *string
var argsBytes []byte
if err := rows.Scan(&e.ID, &e.HeroID, &e.Message, &e.CreatedAt, &code, &argsBytes); err != nil {
return nil, fmt.Errorf("scan log entry: %w", err)
}
entries = append(entries, logEntryFromScan(e.ID, e.HeroID, e.Message, e.CreatedAt, code, argsBytes))
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("log entries rows: %w", err)
}
if entries == nil {
entries = []LogEntry{}
}
return entries, nil
}