Add complete backend implementation for saved filters CRUD operations with proper service layer architecture and RESTful API endpoints. Service Layer (internal/services/filters.go): - NewFiltersService() constructor following project patterns - GetSavedFilters(): Retrieve all filters for user + resource type - CreateSavedFilter(): Create filter with duplicate name validation - UpdateSavedFilter(): Update filter with ownership verification - DeleteSavedFilter(): Delete filter with user scoping Business Logic: - Filter name uniqueness enforced per user + resource type - User ownership validation on all operations (JWT user_id) - JSONB marshaling/unmarshaling for flexible filter storage - Proper error wrapping with context messages Handler Layer (internal/handlers/filters.go): - NewFiltersHandler() constructor (receives db.Queries) - GetSavedFilters: GET /api/saved-filters?resource_type=X - CreateSavedFilter: POST /api/saved-filters - UpdateSavedFilter: PUT /api/saved-filters/:id - DeleteSavedFilter: DELETE /api/saved-filters/:id Content Negotiation: - Supports both JSON (API clients) and HTML (HTMX) responses - wantsHTML() helper checks Accept header - HX-Redirect header for HTMX form submissions - Proper status codes (200, 201, 204, 400, 401, 404, 409) Router Configuration: - registerFiltersRoutes() function in internal/router/filters.go - JWT middleware protection on all endpoints - RESTful route structure: /api/saved-filters - Registered in main router.go RegisterRoutes() function - Added FiltersHandler to router.Config struct Test Infrastructure: - Added FiltersHandler to test server setup (test_helpers_test.go) - FiltersHandler initialized in setupTestServer() function - Router.Config includes FiltersHandler for integration tests Code Quality: - Follows PROJECT_GUIDELINES.md service layer patterns - Uses database models (not custom domain models) - JSONB returned as []byte (matches collections pattern) - All errors wrapped with context using fmt.Errorf - Handlers create services internally (not dependency injection) Part of: Saved Filters Implementation (Phase 2: Backend) Related: #saved-filters-feature
123 lines
4.0 KiB
Go
123 lines
4.0 KiB
Go
package services
|
|
|
|
import (
|
|
"bookhoard/internal/database"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
type FiltersService struct {
|
|
db *database.Queries
|
|
}
|
|
|
|
func NewFiltersService(db *database.Queries) *FiltersService {
|
|
return &FiltersService{db: db}
|
|
}
|
|
|
|
// GetSavedFilters - Retrieve all saved filters for a user + resource type
|
|
func (s *FiltersService) GetSavedFilters(ctx context.Context, userID uuid.UUID, resourceType string) ([]database.SavedFilters, error) {
|
|
filters, err := s.db.GetSavedFilters(ctx, database.GetSavedFiltersParams{
|
|
UserID: pgtype.UUID{Bytes: userID, Valid: true},
|
|
ResourceType: resourceType,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get saved filters: %w", err)
|
|
}
|
|
|
|
return filters, nil
|
|
}
|
|
|
|
// CreateSavedFilter - Create a new saved filter
|
|
func (s *FiltersService) CreateSavedFilter(ctx context.Context, userID uuid.UUID, name string, resourceType string, filters map[string]string) (database.SavedFilters, error) {
|
|
// Business logic: Validate filter name uniqueness per user + resource type
|
|
existing, err := s.db.GetSavedFilters(ctx, database.GetSavedFiltersParams{
|
|
UserID: pgtype.UUID{Bytes: userID, Valid: true},
|
|
ResourceType: resourceType,
|
|
})
|
|
if err == nil {
|
|
for _, f := range existing {
|
|
if f.Name == name {
|
|
return database.SavedFilters{}, fmt.Errorf("filter with name '%s' already exists for this resource type", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert filters map to JSONB ([]byte)
|
|
filtersJSON, err := json.Marshal(filters)
|
|
if err != nil {
|
|
return database.SavedFilters{}, fmt.Errorf("failed to marshal filters: %w", err)
|
|
}
|
|
|
|
filter, err := s.db.CreateSavedFilter(ctx, database.CreateSavedFilterParams{
|
|
UserID: pgtype.UUID{Bytes: userID, Valid: true},
|
|
Name: name,
|
|
ResourceType: resourceType,
|
|
Filters: filtersJSON,
|
|
})
|
|
if err != nil {
|
|
return database.SavedFilters{}, fmt.Errorf("failed to create saved filter: %w", err)
|
|
}
|
|
|
|
return filter, nil
|
|
}
|
|
|
|
// UpdateSavedFilter - Update an existing saved filter
|
|
func (s *FiltersService) UpdateSavedFilter(ctx context.Context, userID uuid.UUID, filterID uuid.UUID, name string, filters map[string]string) (database.SavedFilters, error) {
|
|
// Business logic: Verify filter exists and belongs to user
|
|
existing, err := s.db.GetSavedFilterByID(ctx, database.GetSavedFilterByIDParams{
|
|
ID: pgtype.UUID{Bytes: filterID, Valid: true},
|
|
UserID: pgtype.UUID{Bytes: userID, Valid: true},
|
|
})
|
|
if err != nil {
|
|
return database.SavedFilters{}, fmt.Errorf("filter not found or access denied: %w", err)
|
|
}
|
|
|
|
// Business logic: Check name uniqueness (excluding current filter)
|
|
allFilters, err := s.db.GetSavedFilters(ctx, database.GetSavedFiltersParams{
|
|
UserID: pgtype.UUID{Bytes: userID, Valid: true},
|
|
ResourceType: existing.ResourceType,
|
|
})
|
|
if err == nil {
|
|
for _, f := range allFilters {
|
|
existingID := uuid.Must(uuid.FromBytes(f.ID.Bytes[:]))
|
|
if f.Name == name && existingID != filterID {
|
|
return database.SavedFilters{}, fmt.Errorf("filter with name '%s' already exists for this resource type", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert filters to JSONB
|
|
filtersJSON, err := json.Marshal(filters)
|
|
if err != nil {
|
|
return database.SavedFilters{}, fmt.Errorf("failed to marshal filters: %w", err)
|
|
}
|
|
|
|
updated, err := s.db.UpdateSavedFilter(ctx, database.UpdateSavedFilterParams{
|
|
ID: pgtype.UUID{Bytes: filterID, Valid: true},
|
|
UserID: pgtype.UUID{Bytes: userID, Valid: true},
|
|
Name: name,
|
|
Filters: filtersJSON,
|
|
})
|
|
if err != nil {
|
|
return database.SavedFilters{}, fmt.Errorf("failed to update saved filter: %w", err)
|
|
}
|
|
|
|
return updated, nil
|
|
}
|
|
|
|
// DeleteSavedFilter - Delete a saved filter
|
|
func (s *FiltersService) DeleteSavedFilter(ctx context.Context, userID uuid.UUID, filterID uuid.UUID) error {
|
|
err := s.db.DeleteSavedFilter(ctx, database.DeleteSavedFilterParams{
|
|
ID: pgtype.UUID{Bytes: filterID, Valid: true},
|
|
UserID: pgtype.UUID{Bytes: userID, Valid: true},
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete saved filter: %w", err)
|
|
}
|
|
return nil
|
|
}
|