Add filtering support to GET /api/v1/habits endpoint
Implemented comprehensive filtering capabilities for the habits list endpoint: - Filter by type (BOOLEAN, COUNTER, VALUE) - Filter by frequency (DAILY, WEEKLY, MONTHLY) - Filter by archived status - Text search in habit name and description - All filters can be combined - Filters work with pagination Technical changes: - Added FilterParams to GetUserHabitsQuery - Created HabitFilter struct in repository interface - Implemented dynamic SQL query building in SQLite repository - Updated HTTP handler to parse filter query parameters - Added comprehensive tests for repository and handler filtering - Updated all test mocks with new filter methods
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"apocapoc-api/internal/domain/repositories"
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -324,3 +325,11 @@ func TestGetTodaysHabitsHandler_CarryOverDisabled(t *testing.T) {
|
||||
t.Fatalf("Expected 0 habits (no carry-over), got %d", len(results))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockHabitRepo) FindByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockHabitRepo) CountByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
@@ -20,9 +20,17 @@ type HabitDTO struct {
|
||||
SpecificDays []int
|
||||
}
|
||||
|
||||
type FilterParams struct {
|
||||
Type *value_objects.HabitType
|
||||
Frequency *value_objects.Frequency
|
||||
IncludeArchived bool
|
||||
Search string
|
||||
}
|
||||
|
||||
type GetUserHabitsQuery struct {
|
||||
UserID string
|
||||
PaginationParams *pagination.Params
|
||||
FilterParams *FilterParams
|
||||
}
|
||||
|
||||
type GetUserHabitsResult struct {
|
||||
@@ -45,7 +53,29 @@ func (h *GetUserHabitsHandler) Handle(ctx context.Context, query GetUserHabitsQu
|
||||
var paginationResponse *pagination.Response
|
||||
var err error
|
||||
|
||||
if query.PaginationParams != nil {
|
||||
if query.FilterParams != nil {
|
||||
filter := repositories.HabitFilter{
|
||||
Type: query.FilterParams.Type,
|
||||
Frequency: query.FilterParams.Frequency,
|
||||
IncludeArchived: query.FilterParams.IncludeArchived,
|
||||
Search: query.FilterParams.Search,
|
||||
}
|
||||
|
||||
habits, err = h.habitRepo.FindByUserIDFiltered(ctx, query.UserID, filter, query.PaginationParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if query.PaginationParams != nil {
|
||||
totalItems, err := h.habitRepo.CountByUserIDFiltered(ctx, query.UserID, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := pagination.NewResponse(*query.PaginationParams, totalItems)
|
||||
paginationResponse = &response
|
||||
}
|
||||
} else if query.PaginationParams != nil {
|
||||
habits, err = h.habitRepo.FindActiveByUserIDWithPagination(ctx, query.UserID, *query.PaginationParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"apocapoc-api/internal/domain/repositories"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
@@ -277,3 +278,142 @@ func TestGetUserHabitsHandler_WithPagination(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (m *mockGetUserHabitsRepo) FindByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error) {
|
||||
var filtered []*entities.Habit
|
||||
|
||||
for _, habit := range m.habits {
|
||||
if filter.Type != nil && habit.Type != *filter.Type {
|
||||
continue
|
||||
}
|
||||
if filter.Frequency != nil && habit.Frequency != *filter.Frequency {
|
||||
continue
|
||||
}
|
||||
if !filter.IncludeArchived && habit.ArchivedAt != nil {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, habit)
|
||||
}
|
||||
|
||||
if paginationParams != nil {
|
||||
offset := paginationParams.Offset()
|
||||
limit := paginationParams.Limit()
|
||||
|
||||
if offset >= len(filtered) {
|
||||
return []*entities.Habit{}, nil
|
||||
}
|
||||
|
||||
end := offset + limit
|
||||
if end > len(filtered) {
|
||||
end = len(filtered)
|
||||
}
|
||||
|
||||
return filtered[offset:end], nil
|
||||
}
|
||||
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
func (m *mockGetUserHabitsRepo) CountByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter) (int, error) {
|
||||
count := 0
|
||||
|
||||
for _, habit := range m.habits {
|
||||
if filter.Type != nil && habit.Type != *filter.Type {
|
||||
continue
|
||||
}
|
||||
if filter.Frequency != nil && habit.Frequency != *filter.Frequency {
|
||||
continue
|
||||
}
|
||||
if !filter.IncludeArchived && habit.ArchivedAt != nil {
|
||||
continue
|
||||
}
|
||||
count++
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func TestGetUserHabitsHandler_WithFilters(t *testing.T) {
|
||||
habit1 := entities.NewHabit("user-123", "Exercise", value_objects.HabitTypeBoolean, value_objects.FrequencyDaily, false, false)
|
||||
habit1.ID = "habit-1"
|
||||
|
||||
habit2 := entities.NewHabit("user-123", "Read", value_objects.HabitTypeCounter, value_objects.FrequencyWeekly, false, false)
|
||||
habit2.ID = "habit-2"
|
||||
|
||||
habit3 := entities.NewHabit("user-123", "Water", value_objects.HabitTypeValue, value_objects.FrequencyDaily, false, false)
|
||||
habit3.ID = "habit-3"
|
||||
|
||||
habitRepo := &mockGetUserHabitsRepo{habits: []*entities.Habit{habit1, habit2, habit3}}
|
||||
handler := NewGetUserHabitsHandler(habitRepo)
|
||||
|
||||
t.Run("FilterByType", func(t *testing.T) {
|
||||
habitType := value_objects.HabitTypeBoolean
|
||||
query := GetUserHabitsQuery{
|
||||
UserID: "user-123",
|
||||
FilterParams: &FilterParams{
|
||||
Type: &habitType,
|
||||
},
|
||||
}
|
||||
|
||||
result, err := handler.Handle(context.Background(), query)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(result.Habits) != 1 {
|
||||
t.Errorf("Expected 1 BOOLEAN habit, got %d", len(result.Habits))
|
||||
}
|
||||
|
||||
if result.Habits[0].Type != value_objects.HabitTypeBoolean {
|
||||
t.Errorf("Expected BOOLEAN type, got %s", result.Habits[0].Type)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FilterByFrequency", func(t *testing.T) {
|
||||
frequency := value_objects.FrequencyDaily
|
||||
query := GetUserHabitsQuery{
|
||||
UserID: "user-123",
|
||||
FilterParams: &FilterParams{
|
||||
Frequency: &frequency,
|
||||
},
|
||||
}
|
||||
|
||||
result, err := handler.Handle(context.Background(), query)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(result.Habits) != 2 {
|
||||
t.Errorf("Expected 2 DAILY habits, got %d", len(result.Habits))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FilterWithPagination", func(t *testing.T) {
|
||||
frequency := value_objects.FrequencyDaily
|
||||
params := pagination.NewParams(1, 1)
|
||||
query := GetUserHabitsQuery{
|
||||
UserID: "user-123",
|
||||
FilterParams: &FilterParams{
|
||||
Frequency: &frequency,
|
||||
},
|
||||
PaginationParams: ¶ms,
|
||||
}
|
||||
|
||||
result, err := handler.Handle(context.Background(), query)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(result.Habits) != 1 {
|
||||
t.Errorf("Expected 1 habit on first page, got %d", len(result.Habits))
|
||||
}
|
||||
|
||||
if result.Pagination == nil {
|
||||
t.Fatal("Expected pagination metadata")
|
||||
}
|
||||
|
||||
if result.Pagination.TotalItems != 2 {
|
||||
t.Errorf("Expected 2 total DAILY habits, got %d", result.Pagination.TotalItems)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"apocapoc-api/internal/domain/repositories"
|
||||
"apocapoc-api/internal/shared/pagination"
|
||||
"context"
|
||||
"testing"
|
||||
@@ -213,3 +214,11 @@ func (m *mockLoginUserRepo) FindActiveByUserIDWithPagination(ctx context.Context
|
||||
func (m *mockLoginUserRepo) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (m *mockLoginUserRepo) FindByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockLoginUserRepo) CountByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"apocapoc-api/internal/domain/repositories"
|
||||
"apocapoc-api/internal/shared/pagination"
|
||||
"context"
|
||||
"testing"
|
||||
@@ -283,3 +284,19 @@ func (m *mockUserRepositoryForRefresh) FindActiveByUserIDWithPagination(ctx cont
|
||||
func (m *mockUserRepositoryForRefresh) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (m *mockRefreshTokenRepository) FindByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockRefreshTokenRepository) CountByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (m *mockUserRepositoryForRefresh) FindByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockUserRepositoryForRefresh) CountByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user