Files
insertr/DATABASE_ARCHITECTURE_REFACTOR.md
Joakim 824719f07d Implement collection item reordering with bulk operations and persistent HTML attributes
- 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
2025-10-07 22:59:00 +02:00

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

  1. No Interface Abstraction:

    • Database struct is concrete, not interface-based
    • Hard to mock for testing
    • Tight coupling between handlers and database implementation
  2. 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
  3. Mixed Responsibilities:

    • Single struct holds both SQLite and PostgreSQL queries
    • Database type detection mixed with query execution
    • Leaky abstraction (handlers know about database internals)
  4. Poor Testability:

    • Cannot easily mock database operations
    • Hard to test database-specific edge cases
    • Difficult to test transaction behavior
  5. 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

  1. Single Responsibility: Each repository handles one database type
  2. Open/Closed: Easy to add new database types without modifying existing code
  3. Testability: Can easily mock ContentRepository interface
  4. Clean Code: Handlers focus on HTTP concerns, not database specifics
  5. Type Safety: Interface ensures all database types implement same methods
  6. Performance: No runtime type switching overhead

Migration Strategy

Phase 1: Create Interface and Base Implementations

  • Define ContentRepository interface
  • Create SQLiteRepository implementation
  • Create PostgreSQLRepository implementation
  • 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 Database struct
  • 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:

  1. Adding support for new database types
  2. Significant new database operations are needed
  3. Testing infrastructure needs improvement
  4. Performance optimization becomes critical

The current implementation works correctly but is not maintainable long-term.