175 lines
5.1 KiB
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
|
|
}
|