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
This commit is contained in:
198
DATABASE_ARCHITECTURE_REFACTOR.md
Normal file
198
DATABASE_ARCHITECTURE_REFACTOR.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user