package timeline import ( "context" "database/sql" "fmt" "strconv" "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 int64 } func EncodeCursor(cursor Cursor) string { return fmt.Sprintf("%d:%d", 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") } ts, err := strconv.ParseInt(parts[0], 10, 64) if err != nil { return Cursor{}, fmt.Errorf("invalid cursor") } id, err := strconv.ParseInt(parts[1], 10, 64) if err != nil || id <= 0 { return Cursor{}, fmt.Errorf("invalid cursor") } return Cursor{Time: time.Unix(0, ts).UTC(), ID: id}, nil } func (r *EventRepository) List(ctx context.Context, subjectType string, subjectID int64, 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, asset_id, component_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 assetID sql.NullInt64 var componentID sql.NullInt64 var firmware sql.NullString if err := rows.Scan(&event.ID, &event.SubjectType, &event.SubjectID, &event.EventType, &event.EventTime, &assetID, &componentID, &firmware, &event.CreatedAt); err != nil { return nil, nil, err } if assetID.Valid { event.AssetID = &assetID.Int64 } if componentID.Valid { event.ComponentID = &componentID.Int64 } 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 }