- Add bulk reorder API endpoint (PUT /api/collections/{id}/reorder) with atomic transactions
- Replace individual position updates with efficient bulk operations in frontend
- Implement unified ID generation and proper data-item-id injection during enhancement
- Fix collection item position persistence through content edit cycles
- Add optimistic UI with rollback capability for better user experience
- Update sqlc queries to include last_edited_by fields in position updates
- Remove obsolete data-content-type attributes and unify naming conventions
6.1 KiB
6.1 KiB
Database Architecture Refactoring - Technical Debt
Current Problem
The current database layer violates several software architecture principles and creates maintenance issues:
Issues with Current Implementation
-
No Interface Abstraction:
Databasestruct is concrete, not interface-based- Hard to mock for testing
- Tight coupling between handlers and database implementation
-
Type Switching Everywhere:
switch h.database.GetDBType() { case "sqlite3": err = h.database.GetSQLiteQueries().SomeMethod(...) case "postgres": err = h.database.GetPostgreSQLQueries().SomeMethod(...) }- Repeated in every handler method
- Violates DRY principle
- Makes adding new database types difficult
-
Mixed Responsibilities:
- Single struct holds both SQLite and PostgreSQL queries
- Database type detection mixed with query execution
- Leaky abstraction (handlers know about database internals)
-
Poor Testability:
- Cannot easily mock database operations
- Hard to test database-specific edge cases
- Difficult to test transaction behavior
-
Violates Open/Closed Principle:
- Adding new database support requires modifying existing handlers
- Cannot extend database support without changing core business logic
Proposed Solution
1. Repository Pattern with Interface
// Domain-focused interface
type ContentRepository interface {
// Content operations
GetContent(ctx context.Context, siteID, contentID string) (*Content, error)
CreateContent(ctx context.Context, content *Content) (*Content, error)
UpdateContent(ctx context.Context, content *Content) (*Content, error)
DeleteContent(ctx context.Context, siteID, contentID string) error
// Collection operations
GetCollection(ctx context.Context, siteID, collectionID string) (*Collection, error)
GetCollectionItems(ctx context.Context, siteID, collectionID string) ([]*CollectionItem, error)
CreateCollectionItem(ctx context.Context, item *CollectionItem) (*CollectionItem, error)
ReorderCollectionItems(ctx context.Context, siteID, collectionID string, positions []ItemPosition) error
// Transaction support
WithTransaction(ctx context.Context, fn func(ContentRepository) error) error
}
2. Database-Specific Implementations
type SQLiteRepository struct {
queries *sqlite.Queries
db *sql.DB
}
func (r *SQLiteRepository) GetContent(ctx context.Context, siteID, contentID string) (*Content, error) {
result, err := r.queries.GetContent(ctx, sqlite.GetContentParams{
ID: contentID,
SiteID: siteID,
})
if err != nil {
return nil, err
}
return r.convertSQLiteContent(result), nil
}
type PostgreSQLRepository struct {
queries *postgresql.Queries
db *sql.DB
}
func (r *PostgreSQLRepository) GetContent(ctx context.Context, siteID, contentID string) (*Content, error) {
result, err := r.queries.GetContent(ctx, postgresql.GetContentParams{
ID: contentID,
SiteID: siteID,
})
if err != nil {
return nil, err
}
return r.convertPostgreSQLContent(result), nil
}
3. Clean Handler Implementation
type ContentHandler struct {
repository ContentRepository // Interface, not concrete type
authService *auth.AuthService
}
func (h *ContentHandler) GetContent(w http.ResponseWriter, r *http.Request) {
contentID := chi.URLParam(r, "id")
siteID := r.URL.Query().Get("site_id")
// No more type switching!
content, err := h.repository.GetContent(r.Context(), siteID, contentID)
if err != nil {
http.Error(w, "Content not found", http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(content)
}
4. Dependency Injection
func NewContentHandler(repo ContentRepository, authService *auth.AuthService) *ContentHandler {
return &ContentHandler{
repository: repo,
authService: authService,
}
}
// In main.go or wherever handlers are initialized
var repo ContentRepository
switch dbType {
case "sqlite3":
repo = NewSQLiteRepository(db)
case "postgres":
repo = NewPostgreSQLRepository(db)
}
contentHandler := NewContentHandler(repo, authService)
Benefits of Refactoring
- Single Responsibility: Each repository handles one database type
- Open/Closed: Easy to add new database types without modifying existing code
- Testability: Can easily mock
ContentRepositoryinterface - Clean Code: Handlers focus on HTTP concerns, not database specifics
- Type Safety: Interface ensures all database types implement same methods
- Performance: No runtime type switching overhead
Migration Strategy
Phase 1: Create Interface and Base Implementations
- Define
ContentRepositoryinterface - Create
SQLiteRepositoryimplementation - Create
PostgreSQLRepositoryimplementation - Add factory function for repository creation
Phase 2: Migrate Handlers One by One
- Start with simple handlers (GetContent, etc.)
- Update handler constructors to use interface
- Remove type switching logic
- Add unit tests with mocked repository
Phase 3: Advanced Features
- Add transaction support to interface
- Implement batch operations efficiently
- Add connection pooling and retry logic
- Performance optimization for each database type
Phase 4: Cleanup
- Remove old
Databasestruct - Remove database type detection logic
- Update documentation
- Add integration tests
Estimated Effort
- Phase 1: 1-2 days (interface design and base implementations)
- Phase 2: 2-3 days (handler migration and testing)
- Phase 3: 1-2 days (advanced features)
- Phase 4: 1 day (cleanup)
Total: ~5-8 days for complete refactoring
Priority
Medium Priority - This is architectural debt that should be addressed when:
- Adding support for new database types
- Significant new database operations are needed
- Testing infrastructure needs improvement
- Performance optimization becomes critical
The current implementation works correctly but is not maintainable long-term.