Files
wotra/internal/store/entry_store.go

175 lines
5.1 KiB
Go

package store
import (
"context"
"database/sql"
"fmt"
"github.com/wotra/wotra/internal/domain"
)
// EntryStore handles persistence for entries.
type EntryStore struct {
db *sql.DB
}
func NewEntryStore(db *sql.DB) *EntryStore {
return &EntryStore{db: db}
}
func (s *EntryStore) Create(ctx context.Context, e *domain.Entry) error {
_, err := s.db.ExecContext(ctx,
`INSERT INTO entries (id, start_time, end_time, auto_stopped, note, day_key, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
e.ID, e.StartTime, e.EndTime, boolToInt(e.AutoStopped), e.Note, e.DayKey, e.UpdatedAt,
)
return err
}
func (s *EntryStore) Update(ctx context.Context, e *domain.Entry) error {
_, err := s.db.ExecContext(ctx,
`UPDATE entries SET start_time=?, end_time=?, auto_stopped=?, note=?, day_key=?, updated_at=?
WHERE id=? AND deleted_at IS NULL`,
e.StartTime, e.EndTime, boolToInt(e.AutoStopped), e.Note, e.DayKey, e.UpdatedAt, e.ID,
)
return err
}
func (s *EntryStore) SoftDelete(ctx context.Context, id string, nowMs int64) error {
_, err := s.db.ExecContext(ctx,
`UPDATE entries SET deleted_at=?, updated_at=? WHERE id=? AND deleted_at IS NULL`,
nowMs, nowMs, id,
)
return err
}
func (s *EntryStore) GetByID(ctx context.Context, id string) (*domain.Entry, error) {
row := s.db.QueryRowContext(ctx,
`SELECT id, start_time, end_time, auto_stopped, note, day_key, updated_at, deleted_at
FROM entries WHERE id=?`, id)
return scanEntry(row)
}
func (s *EntryStore) ListByDayKey(ctx context.Context, dayKey string) ([]*domain.Entry, error) {
rows, err := s.db.QueryContext(ctx,
`SELECT id, start_time, end_time, auto_stopped, note, day_key, updated_at, deleted_at
FROM entries WHERE day_key=? AND deleted_at IS NULL ORDER BY start_time ASC`, dayKey)
if err != nil {
return nil, err
}
defer rows.Close()
return scanEntries(rows)
}
func (s *EntryStore) ListByDateRange(ctx context.Context, fromDayKey, toDayKey string) ([]*domain.Entry, error) {
rows, err := s.db.QueryContext(ctx,
`SELECT id, start_time, end_time, auto_stopped, note, day_key, updated_at, deleted_at
FROM entries WHERE day_key >= ? AND day_key <= ? AND deleted_at IS NULL ORDER BY start_time ASC`,
fromDayKey, toDayKey)
if err != nil {
return nil, err
}
defer rows.Close()
return scanEntries(rows)
}
// RunningEntry returns the currently running entry (no end_time), if any.
func (s *EntryStore) RunningEntry(ctx context.Context) (*domain.Entry, error) {
row := s.db.QueryRowContext(ctx,
`SELECT id, start_time, end_time, auto_stopped, note, day_key, updated_at, deleted_at
FROM entries WHERE end_time IS NULL AND deleted_at IS NULL LIMIT 1`)
e, err := scanEntry(row)
if err == sql.ErrNoRows {
return nil, nil
}
return e, err
}
// RunningEntryForDay returns a running entry for a specific day, if any.
func (s *EntryStore) RunningEntryForDay(ctx context.Context, dayKey string) (*domain.Entry, error) {
row := s.db.QueryRowContext(ctx,
`SELECT id, start_time, end_time, auto_stopped, note, day_key, updated_at, deleted_at
FROM entries WHERE day_key=? AND end_time IS NULL AND deleted_at IS NULL LIMIT 1`, dayKey)
e, err := scanEntry(row)
if err == sql.ErrNoRows {
return nil, nil
}
return e, err
}
// StopAllRunningBefore stops all entries whose day_key < dayKey with the given end time.
// Used for the midnight auto-stop.
func (s *EntryStore) StopAllRunningBefore(ctx context.Context, dayKey string, endTimeMs int64, nowMs int64) ([]string, error) {
rows, err := s.db.QueryContext(ctx,
`SELECT id FROM entries WHERE end_time IS NULL AND deleted_at IS NULL AND day_key < ?`, dayKey)
if err != nil {
return nil, err
}
var ids []string
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
rows.Close()
return nil, err
}
ids = append(ids, id)
}
rows.Close()
for _, id := range ids {
if _, err := s.db.ExecContext(ctx,
`UPDATE entries SET end_time=?, auto_stopped=1, updated_at=? WHERE id=?`,
endTimeMs, nowMs, id); err != nil {
return nil, fmt.Errorf("auto-stop %s: %w", id, err)
}
}
return ids, nil
}
func scanEntry(row *sql.Row) (*domain.Entry, error) {
var e domain.Entry
var endTime sql.NullInt64
var deletedAt sql.NullInt64
var autoStopped int
err := row.Scan(&e.ID, &e.StartTime, &endTime, &autoStopped, &e.Note, &e.DayKey, &e.UpdatedAt, &deletedAt)
if err != nil {
return nil, err
}
if endTime.Valid {
e.EndTime = &endTime.Int64
}
if deletedAt.Valid {
e.DeletedAt = &deletedAt.Int64
}
e.AutoStopped = autoStopped != 0
return &e, nil
}
func scanEntries(rows *sql.Rows) ([]*domain.Entry, error) {
var result []*domain.Entry
for rows.Next() {
var e domain.Entry
var endTime sql.NullInt64
var deletedAt sql.NullInt64
var autoStopped int
if err := rows.Scan(&e.ID, &e.StartTime, &endTime, &autoStopped, &e.Note, &e.DayKey, &e.UpdatedAt, &deletedAt); err != nil {
return nil, err
}
if endTime.Valid {
e.EndTime = &endTime.Int64
}
if deletedAt.Valid {
e.DeletedAt = &deletedAt.Int64
}
e.AutoStopped = autoStopped != 0
result = append(result, &e)
}
return result, rows.Err()
}
func boolToInt(b bool) int {
if b {
return 1
}
return 0
}