feat(m1): backend scaffold - entries CRUD, start/stop, auth, migrations
This commit is contained in:
174
internal/store/entry_store.go
Normal file
174
internal/store/entry_store.go
Normal file
@@ -0,0 +1,174 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user