168 lines
4.1 KiB
Go
168 lines
4.1 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
|
|
}
|
|
|
|
func (r *EventRepository) ListLatestBySubjects(ctx context.Context, subjectType string, ids []string) (map[string]domain.TimelineEvent, error) {
|
|
result := make(map[string]domain.TimelineEvent, len(ids))
|
|
if len(ids) == 0 {
|
|
return result, nil
|
|
}
|
|
|
|
placeholders := make([]string, len(ids))
|
|
args := make([]any, 0, len(ids)+1)
|
|
args = append(args, subjectType)
|
|
for i, id := range ids {
|
|
placeholders[i] = "?"
|
|
args = append(args, id)
|
|
}
|
|
|
|
query := `
|
|
SELECT t.id, t.subject_id, t.event_type, t.event_time, t.machine_id, t.part_id, t.firmware_version, t.created_at
|
|
FROM timeline_events t
|
|
WHERE t.subject_type = ? AND t.subject_id IN (` + strings.Join(placeholders, ",") + `)
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM timeline_events t2
|
|
WHERE t2.subject_type = t.subject_type
|
|
AND t2.subject_id = t.subject_id
|
|
AND (
|
|
t2.event_time > t.event_time
|
|
OR (t2.event_time = t.event_time AND t2.id > t.id)
|
|
)
|
|
)
|
|
`
|
|
|
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var event domain.TimelineEvent
|
|
event.SubjectType = subjectType
|
|
var machineID sql.NullString
|
|
var partID sql.NullString
|
|
var firmware sql.NullString
|
|
if err := rows.Scan(&event.ID, &event.SubjectID, &event.EventType, &event.EventTime, &machineID, &partID, &firmware, &event.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
if machineID.Valid {
|
|
event.MachineID = &machineID.String
|
|
}
|
|
if partID.Valid {
|
|
event.PartID = &partID.String
|
|
}
|
|
if firmware.Valid {
|
|
event.FirmwareVersion = &firmware.String
|
|
}
|
|
result[event.SubjectID] = event
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return result, nil
|
|
}
|