summaryrefslogtreecommitdiff
path: root/internal/storage/events.go
blob: 82016ab939381b012b45037cb50dc7095e33d378 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
package storage

import (
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"github.com/google/uuid"
	"github.com/thepeterstone/claudomator/internal/event"
)

// CreateEvent appends an event to a task's stream. Seq is assigned by the
// database as MAX(seq)+1 within a transaction. On return, e.ID, e.Seq, and
// e.Timestamp are populated (timestamps default to time.Now().UTC()).
func (s *DB) CreateEvent(e *event.Event) error {
	if e == nil {
		return errors.New("event is nil")
	}
	if e.TaskID == "" {
		return errors.New("event.TaskID required")
	}
	if e.Kind == "" {
		return errors.New("event.Kind required")
	}
	if e.Actor == "" {
		return errors.New("event.Actor required")
	}
	if len(e.Payload) == 0 {
		e.Payload = json.RawMessage("{}")
	}
	if e.ID == "" {
		e.ID = uuid.NewString()
	}
	if e.Timestamp.IsZero() {
		e.Timestamp = time.Now().UTC()
	} else {
		e.Timestamp = e.Timestamp.UTC()
	}

	tx, err := s.db.Begin()
	if err != nil {
		return err
	}
	defer tx.Rollback()

	var nextSeq int64
	if err := tx.QueryRow(`SELECT COALESCE(MAX(seq), 0) + 1 FROM events WHERE task_id = ?`, e.TaskID).Scan(&nextSeq); err != nil {
		return fmt.Errorf("computing next seq: %w", err)
	}
	e.Seq = nextSeq

	if _, err := tx.Exec(
		`INSERT INTO events (id, task_id, seq, ts, kind, actor, payload_json) VALUES (?, ?, ?, ?, ?, ?, ?)`,
		e.ID, e.TaskID, e.Seq, e.Timestamp, string(e.Kind), string(e.Actor), string(e.Payload),
	); err != nil {
		return fmt.Errorf("inserting event: %w", err)
	}
	return tx.Commit()
}

// ListEvents returns events for a task in seq order. Pass sinceSeq=0 for the
// full history, or a higher value to fetch only events strictly after that seq
// (for incremental polling / SSE catch-up).
func (s *DB) ListEvents(taskID string, sinceSeq int64) ([]*event.Event, error) {
	rows, err := s.db.Query(
		`SELECT id, task_id, seq, ts, kind, actor, payload_json FROM events WHERE task_id = ? AND seq > ? ORDER BY seq ASC`,
		taskID, sinceSeq,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []*event.Event
	for rows.Next() {
		e, err := scanEvent(rows)
		if err != nil {
			return nil, err
		}
		out = append(out, e)
	}
	return out, rows.Err()
}

func scanEvent(row scanner) (*event.Event, error) {
	var (
		e       event.Event
		kind    string
		actor   string
		payload string
	)
	if err := row.Scan(&e.ID, &e.TaskID, &e.Seq, &e.Timestamp, &kind, &actor, &payload); err != nil {
		return nil, err
	}
	e.Kind = event.Kind(kind)
	e.Actor = event.Actor(actor)
	e.Payload = json.RawMessage(payload)
	return &e, nil
}