Files
apocapoc-api/internal/infrastructure/persistence/sqlite/habit_entry_repository.go
T
david 9f673bfca3 Refactor habit entries: remove soft delete and add smart pagination
Remove soft delete from HabitEntry:
- Eliminate DeletedAt field from entity and database
- Change UnmarkHabit from soft delete to hard delete
- Simplify all queries removing deleted_at checks
- Update migration to remove deleted_at column

Add date filtering and smart pagination to GetHabitEntries:
- Support optional from/to date parameters
- Implement intelligent pagination rules:
  * No date range: pagination required
  * Date range > 1 year: pagination required
  * Date range ≤ 1 year: pagination optional
- Pagination defaults (page=1, limit=50) only when required
- Return metadata with total count, page, and limit

Technical improvements:
- Cleaner codebase without soft delete complexity
- Better performance (no filtering in queries)
- More intuitive API with flexible pagination
- Comprehensive test coverage for validation rules
2025-11-26 16:33:40 +01:00

221 lines
4.8 KiB
Go

package sqlite
import (
"context"
"database/sql"
"fmt"
"time"
"apocapoc-api/internal/domain/entities"
"apocapoc-api/internal/shared/errors"
"github.com/google/uuid"
)
type HabitEntryRepository struct {
db *sql.DB
}
func NewHabitEntryRepository(db *sql.DB) *HabitEntryRepository {
return &HabitEntryRepository{db: db}
}
func (r *HabitEntryRepository) Create(ctx context.Context, entry *entities.HabitEntry) error {
entry.ID = uuid.New().String()
query := `
INSERT INTO habit_entries (id, habit_id, scheduled_date, completed_at, value)
VALUES (?, ?, ?, ?, ?)
`
_, err := r.db.ExecContext(ctx, query,
entry.ID,
entry.HabitID,
entry.ScheduledDate.Format("2006-01-02"),
entry.CompletedAt,
entry.Value,
)
if err != nil {
if isUniqueConstraintError(err) {
return errors.ErrAlreadyExists
}
return fmt.Errorf("failed to create entry: %w", err)
}
return nil
}
func (r *HabitEntryRepository) FindByHabitIDAndDateRange(
ctx context.Context,
habitID string,
from, to time.Time,
) ([]*entities.HabitEntry, error) {
query := `
SELECT id, habit_id, scheduled_date, completed_at, value
FROM habit_entries
WHERE habit_id = ?
AND scheduled_date >= ?
AND scheduled_date <= ?
ORDER BY scheduled_date ASC
`
rows, err := r.db.QueryContext(ctx, query,
habitID,
from.Format("2006-01-02"),
to.Format("2006-01-02"),
)
if err != nil {
return nil, fmt.Errorf("failed to find entries: %w", err)
}
defer rows.Close()
return r.scanEntries(rows)
}
func (r *HabitEntryRepository) Update(ctx context.Context, entry *entities.HabitEntry) error {
query := `
UPDATE habit_entries
SET value = ?, completed_at = ?
WHERE id = ?
`
result, err := r.db.ExecContext(ctx, query, entry.Value, entry.CompletedAt, entry.ID)
if err != nil {
return fmt.Errorf("failed to update entry: %w", err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return errors.ErrNotFound
}
return nil
}
func (r *HabitEntryRepository) scanEntries(rows *sql.Rows) ([]*entities.HabitEntry, error) {
var entries []*entities.HabitEntry
for rows.Next() {
var (
entry entities.HabitEntry
scheduledDate string
)
err := rows.Scan(
&entry.ID,
&entry.HabitID,
&scheduledDate,
&entry.CompletedAt,
&entry.Value,
)
if err != nil {
return nil, err
}
parsedDate, err := time.Parse("2006-01-02", scheduledDate)
if err != nil {
parsedDate, err = time.Parse(time.RFC3339, scheduledDate)
if err != nil {
return nil, fmt.Errorf("failed to parse scheduled_date: %w", err)
}
}
entry.ScheduledDate = parsedDate
entries = append(entries, &entry)
}
return entries, nil
}
func (r *HabitEntryRepository) FindByID(ctx context.Context, id string) (*entities.HabitEntry, error) {
query := `
SELECT id, habit_id, scheduled_date, completed_at, value
FROM habit_entries
WHERE id = ?
`
var (
entry entities.HabitEntry
scheduledDate string
)
err := r.db.QueryRowContext(ctx, query, id).Scan(
&entry.ID,
&entry.HabitID,
&scheduledDate,
&entry.CompletedAt,
&entry.Value,
)
if err == sql.ErrNoRows {
return nil, errors.ErrNotFound
}
if err != nil {
return nil, fmt.Errorf("failed to find entry: %w", err)
}
parsedDate, err := time.Parse("2006-01-02", scheduledDate)
if err != nil {
parsedDate, err = time.Parse(time.RFC3339, scheduledDate)
if err != nil {
return nil, fmt.Errorf("failed to parse scheduled_date: %w", err)
}
}
entry.ScheduledDate = parsedDate
return &entry, nil
}
func (r *HabitEntryRepository) FindByHabitID(ctx context.Context, habitID string) ([]*entities.HabitEntry, error) {
query := `
SELECT id, habit_id, scheduled_date, completed_at, value
FROM habit_entries
WHERE habit_id = ?
ORDER BY scheduled_date DESC
`
rows, err := r.db.QueryContext(ctx, query, habitID)
if err != nil {
return nil, fmt.Errorf("failed to find entries: %w", err)
}
defer rows.Close()
return r.scanEntries(rows)
}
func (r *HabitEntryRepository) FindPendingByHabitID(ctx context.Context, habitID string, beforeDate time.Time) ([]*entities.HabitEntry, error) {
query := `
SELECT id, habit_id, scheduled_date, completed_at, value
FROM habit_entries
WHERE habit_id = ?
AND scheduled_date < ?
ORDER BY scheduled_date DESC
`
rows, err := r.db.QueryContext(ctx, query, habitID, beforeDate.Format("2006-01-02"))
if err != nil {
return nil, fmt.Errorf("failed to find pending entries: %w", err)
}
defer rows.Close()
return r.scanEntries(rows)
}
func (r *HabitEntryRepository) Delete(ctx context.Context, id string) error {
query := `DELETE FROM habit_entries WHERE id = ?`
result, err := r.db.ExecContext(ctx, query, id)
if err != nil {
return fmt.Errorf("failed to delete entry: %w", err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return errors.ErrNotFound
}
return nil
}