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
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
|
|
}
|