Files
core/internal/repository/timeline/events.go

105 lines
2.5 KiB
Go

package timeline
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
"reanimator/internal/domain"
)
type EventRepository struct {
db *sql.DB
}
func NewEventRepository(db *sql.DB) *EventRepository {
return &EventRepository{db: db}
}
type Cursor struct {
Time time.Time
ID string
}
func EncodeCursor(cursor Cursor) string {
return fmt.Sprintf("%d:%s", cursor.Time.UnixNano(), cursor.ID)
}
func DecodeCursor(value string) (Cursor, error) {
parts := strings.Split(value, ":")
if len(parts) != 2 {
return Cursor{}, fmt.Errorf("invalid cursor")
}
var ts int64
if _, err := fmt.Sscanf(parts[0], "%d", &ts); err != nil {
return Cursor{}, fmt.Errorf("invalid cursor timestamp")
}
if parts[1] == "" {
return Cursor{}, fmt.Errorf("invalid cursor id")
}
return Cursor{Time: time.Unix(0, ts).UTC(), ID: parts[1]}, nil
}
func (r *EventRepository) List(ctx context.Context, subjectType string, subjectID string, limit int, cursor *Cursor) ([]domain.TimelineEvent, *Cursor, error) {
if limit <= 0 {
limit = 50
}
if limit > 200 {
limit = 200
}
args := []any{subjectType, subjectID}
query := `
SELECT id, subject_type, subject_id, event_type, event_time, machine_id, part_id, firmware_version, created_at
FROM timeline_events
WHERE subject_type = ? AND subject_id = ?`
if cursor != nil {
query += " AND (event_time > ? OR (event_time = ? AND id > ?))"
args = append(args, cursor.Time, cursor.Time, cursor.ID)
}
query += " ORDER BY event_time, id LIMIT ?"
args = append(args, limit+1)
rows, err := r.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, nil, err
}
defer rows.Close()
events := make([]domain.TimelineEvent, 0)
for rows.Next() {
var event domain.TimelineEvent
var machineID sql.NullString
var partID sql.NullString
var firmware sql.NullString
if err := rows.Scan(&event.ID, &event.SubjectType, &event.SubjectID, &event.EventType, &event.EventTime, &machineID, &partID, &firmware, &event.CreatedAt); err != nil {
return nil, nil, err
}
if machineID.Valid {
event.MachineID = &machineID.String
}
if partID.Valid {
event.PartID = &partID.String
}
if firmware.Valid {
event.FirmwareVersion = &firmware.String
}
events = append(events, event)
}
if err := rows.Err(); err != nil {
return nil, nil, err
}
var nextCursor *Cursor
if len(events) > limit {
last := events[limit-1]
nextCursor = &Cursor{Time: last.EventTime.UTC(), ID: last.ID}
events = events[:limit]
}
return events, nextCursor, nil
}