- 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
198 lines
6.1 KiB
Markdown
198 lines
6.1 KiB
Markdown
# 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**:
|
|
```go
|
|
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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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. |