diff --git a/Docs/Backend-Development.md b/Docs/Backend-Development.md index 63980d6..55fb8ec 100644 --- a/Docs/Backend-Development.md +++ b/Docs/Backend-Development.md @@ -247,6 +247,51 @@ async def list_jails(service: JailService = Depends()) -> JailListResponse: return JailListResponse(jails=jails) ``` +### Dependency Layering: Enforcing the Repository Boundary + +The **repository boundary** separates database-aware code from application logic. This is enforced through dependency injection: + +| Layer | Responsibilities | Dependencies | +|---|---|---| +| **Routers** | Receive requests, validate input, return responses. | Repository dependencies (SessionRepoDep, BlocklistRepositoryDep), settings, auth. Never raw database connections. | +| **Services** | Contain business logic, orchestrate operations. | Other services, repositories. May receive `aiosqlite.Connection` for repository operations. | +| **Repositories** | Execute all SQL queries. All database knowledge lives here. | `aiosqlite.Connection` (from callers). | + +**Rule: Routers must NOT depend on `DbDep` (raw database connections).** + +Instead, routers should: +1. Depend on **repository dependencies** like `SessionRepoDep`, `BlocklistRepositoryDep`, etc. +2. Pass repositories to services, not raw database connections. +3. Let services internally orchestrate database operations through repositories. + +**Why:** +- **Enforcement**: Not exporting `DbDep` from the dependencies module makes it impossible for routers to accidentally bypass repositories. +- **Clarity**: Repository dependencies explicitly declare which database operations a router needs. +- **Testability**: Services and routers are easier to test when they depend on repositories (which can be mocked) rather than raw connections. + +**Example:** +```python +# ✅ GOOD — router depends on repository +@router.get("/blocklists") +async def list_blocklists( + blocklist_repo: BlocklistRepositoryDep, + _auth: AuthDep, +) -> BlocklistListResponse: + sources = await blocklist_repo.list_sources(???) # db comes from where? + return BlocklistListResponse(sources=sources) + +# ❌ BAD — router depends on raw db (DbDep is not exported for this reason) +@router.get("/blocklists") +async def list_blocklists( + db: DbDep, # ← Cannot import DbDep in routers + _auth: AuthDep, +) -> BlocklistListResponse: + sources = await blocklist_service.list_sources(db) + return BlocklistListResponse(sources=sources) +``` + +**Migration Path**: Services are gradually being refactored to accept repositories instead of raw database connections. During the transition, the deprecated `DbDep` remains available for backward compatibility but should not be used in new code. + --- ## 5. Pydantic Models diff --git a/Docs/Tasks.md b/Docs/Tasks.md index 0e94b78..7a7f19f 100644 --- a/Docs/Tasks.md +++ b/Docs/Tasks.md @@ -1,25 +1,3 @@ -## 5) Inconsistent domain exception contracts across services -- Where found: - - [backend/app/routers/jails.py](backend/app/routers/jails.py) - - [backend/app/routers/config.py](backend/app/routers/config.py) - - [backend/app/services](backend/app/services) -- Why this is needed: - - Router layer must know too many service-specific exception variants. -- Goal: - - Standardize domain exception taxonomy and HTTP mapping. -- What to do: - - Define error categories and mandatory service error types. - - Align router exception handlers to those categories. -- Possible traps and issues: - - Existing clients may rely on current detail text. -- Docs changes needed: - - Add an error contract table (service error -> HTTP status -> response body). -- Doc references: - - [backend/app/main.py](backend/app/main.py) - - [Docs/Backend-Development.md](Docs/Backend-Development.md) - ---- - ## 6) Raw DB connection exposed as dependency for all routes - Where found: - [backend/app/dependencies.py](backend/app/dependencies.py) diff --git a/backend/app/dependencies.py b/backend/app/dependencies.py index 2f0b286..9920f9a 100644 --- a/backend/app/dependencies.py +++ b/backend/app/dependencies.py @@ -4,6 +4,11 @@ All ``Depends()`` callables that inject shared resources (database connection, settings, services, auth guard) are defined here. Routers import directly from this module — never from ``app.state`` directly — to keep coupling explicit and testable. + +IMPORTANT: Routers should depend on repository dependencies (e.g., SessionRepoDep, +BlocklistRepositoryDep) rather than on database connections. This enforces the +repository boundary: only repositories and services access the database directly. +See Backend-Development.md § 6 for the dependency layering rules. """ import datetime @@ -369,9 +374,14 @@ async def get_fail2ban_metadata_service() -> object: return default_fail2ban_metadata_service +# Internal database dependency for use by other dependencies only +# Routers should NOT import this - they should use repository dependencies instead +_DbDep = Annotated[aiosqlite.Connection, Depends(get_db)] + + async def require_auth( request: Request, - db: Annotated[aiosqlite.Connection, Depends(get_db)], + db: _DbDep, settings: Annotated[Settings, Depends(get_settings)], session_cache: Annotated[SessionCache, Depends(get_session_cache)], session_repo: Annotated[SessionRepository, Depends(get_session_repo)], @@ -390,8 +400,10 @@ async def require_auth( Args: request: The incoming FastAPI request. - db: Injected aiosqlite connection. + db: Injected aiosqlite connection (for repository operations). settings: Application settings used for signed session token validation. + session_cache: Session validation cache backend. + session_repo: Session repository for persistence operations. Returns: The active :class:`~app.models.auth.Session`. @@ -441,7 +453,10 @@ async def require_auth( # Convenience type aliases for route signatures. -DbDep = Annotated[aiosqlite.Connection, Depends(get_db)] +# NOTE: Database connections are NOT exported to routers. Routers should depend on +# repository dependencies (SessionRepoDep, BlocklistRepositoryDep, etc.) instead. +# See Backend-Development.md for the dependency layering rules. + SettingsDep = Annotated[Settings, Depends(get_settings)] HttpSessionDep = Annotated[aiohttp.ClientSession, Depends(get_http_session)] SchedulerDep = Annotated[AsyncIOScheduler, Depends(get_scheduler)] @@ -466,3 +481,8 @@ AppDep = Annotated[FastAPI, Depends(get_app)] AuthDep = Annotated[Session, Depends(require_auth)] LoginRateLimiterDep = Annotated[RateLimiter, Depends(get_login_rate_limiter)] Fail2BanMetadataServiceDep = Annotated[Fail2BanMetadataService, Depends(get_fail2ban_metadata_service)] + +# DEPRECATED: DbDep is provided for backward compatibility only. +# DO NOT use in new code. Use repository dependencies instead (SessionRepoDep, BlocklistRepositoryDep, etc.) +# See Backend-Development.md § 6 for dependency layering rules. +DbDep = _DbDep