Refactor ban management with domain models and mappers
- Add ban domain model for core business logic separation - Implement mapper pattern for DTO/domain conversions - Update ban service with new domain-driven approach - Refactor router endpoints to use new architecture - Add comprehensive mapper tests - Update documentation with architecture changes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -185,6 +185,43 @@ The HTTP interface layer. Each router maps URL paths to handler functions. Route
|
||||
|
||||
The business logic layer. Services orchestrate operations, enforce rules, and coordinate between repositories, the fail2ban client, and external APIs. Each service covers a single domain.
|
||||
|
||||
**Service Layer Responsibilities:**
|
||||
|
||||
Services **must be independent of HTTP concerns**. They work with domain models (DTOs), not response models. This ensures:
|
||||
- Domain logic can evolve without affecting API shape
|
||||
- Services are reusable across different frontends
|
||||
- Testing is simpler (no mocking HTTP response types)
|
||||
- Changes to endpoint responses don't require service changes
|
||||
|
||||
**Domain Models and Response Mapping:**
|
||||
|
||||
Services return **domain models** (e.g., `DomainActiveBanList`, `DomainBansByCountry`) that represent pure business logic. Response models (e.g., `ActiveBanListResponse`, `BansByCountryResponse`) are defined in `app/models/` and used only by routers.
|
||||
|
||||
Conversion happens at the **router boundary**:
|
||||
1. Router calls service → receives domain model
|
||||
2. Router calls mapper function to convert domain model → response model
|
||||
3. Router returns response model to HTTP client
|
||||
|
||||
Example:
|
||||
```python
|
||||
# In ban_service.py
|
||||
async def get_active_bans(...) -> DomainActiveBanList:
|
||||
"""Service returns domain model (not HTTP-aware)."""
|
||||
...
|
||||
|
||||
# In routers/bans.py (router boundary)
|
||||
domain_result = await ban_service.get_active_bans(...)
|
||||
return map_domain_active_ban_list_to_response(domain_result)
|
||||
```
|
||||
|
||||
Mapper functions live in `app/mappers/` and are thin, mechanical translations between structures.
|
||||
|
||||
**Motivation:**
|
||||
- The Fail2ban domain doesn't care about field names like `country_code` (snake_case) vs `countryCode` (camelCase)
|
||||
- If the API needs pagination metadata added to the response, only the mapper changes
|
||||
- If repositories change their output schema, only services need updating (routers are unaffected)
|
||||
- Services can be tested with simple dataclasses; no need for Pydantic serialization overhead
|
||||
|
||||
| Service | Purpose |
|
||||
|---|---|
|
||||
| `auth_service.py` | Hashes and verifies the master password, creates and validates session tokens, enforces session expiry |
|
||||
@@ -255,6 +292,43 @@ blocklist_service.py (Public API)
|
||||
- Logging is contextual and tied to the appropriate layer
|
||||
- Retry logic and transient error handling are isolated
|
||||
|
||||
#### Mappers (`app/mappers/`)
|
||||
|
||||
The response mapping layer. Mappers convert domain models (returned by services) to response models (consumed by HTTP routers). This layer enforces the separation between business logic and API shape.
|
||||
|
||||
**Location:** `app/mappers/`
|
||||
|
||||
**Responsibilities:**
|
||||
- Convert service domain models to API response models
|
||||
- Mechanical, thin translation — no business logic
|
||||
- Used exclusively at the router boundary
|
||||
|
||||
**Pattern:**
|
||||
|
||||
Each domain model has a corresponding mapper function:
|
||||
|
||||
```python
|
||||
# Domain model (from service)
|
||||
DomainActiveBan → map_domain_active_ban_to_response() → ActiveBan (response)
|
||||
|
||||
# Service returns domain models:
|
||||
async def get_active_bans(...) -> DomainActiveBanList
|
||||
|
||||
# Router converts at the boundary:
|
||||
domain_result = await ban_service.get_active_bans(...)
|
||||
return map_domain_active_ban_list_to_response(domain_result)
|
||||
```
|
||||
|
||||
**Why separate?**
|
||||
|
||||
When API requirements change (e.g., new field added, field renamed), only:
|
||||
1. Response model in `app/models/` changes
|
||||
2. Mapper function in `app/mappers/` updates
|
||||
3. Routers stay the same
|
||||
4. Services don't change
|
||||
|
||||
Without this layer, changes to API shape would require modifying services and their tests.
|
||||
|
||||
#### Repositories (`app/repositories/`)
|
||||
|
||||
The data access layer. Repositories execute raw SQL queries against the application SQLite database. They return plain data or domain models — they never raise HTTP exceptions or contain business logic.
|
||||
|
||||
@@ -1,22 +1,3 @@
|
||||
## 6) Raw DB connection exposed as dependency for all routes
|
||||
- Where found:
|
||||
- [backend/app/dependencies.py](backend/app/dependencies.py)
|
||||
- Why this is needed:
|
||||
- Architectural boundary relies on convention, not enforcement.
|
||||
- Goal:
|
||||
- Enforce repository boundary for persistence access.
|
||||
- What to do:
|
||||
- Prefer repository dependencies in routers.
|
||||
- Restrict direct DB usage to repository/service internals.
|
||||
- Possible traps and issues:
|
||||
- Large refactor may touch many endpoint signatures.
|
||||
- Docs changes needed:
|
||||
- Add dependency layering rule to backend guidelines.
|
||||
- Doc references:
|
||||
- [Docs/Backend-Development.md](Docs/Backend-Development.md)
|
||||
|
||||
---
|
||||
|
||||
## 7) Service layer coupled to response/presentation models
|
||||
- Where found:
|
||||
- [backend/app/services/ban_service.py](backend/app/services/ban_service.py)
|
||||
|
||||
Reference in New Issue
Block a user