Enforce repository boundary for persistence access
- Hide raw database connections (DbDep) from routers by removing from public exports - Maintain DbDep as deprecated export for backward compatibility - Add _DbDep internal dependency for use by other dependencies like require_auth - Update module docstring to explain dependency layering rules - Add comprehensive documentation section on dependency layering to Backend-Development.md This enforces the architectural boundary where: - Routers depend on repository dependencies (SessionRepoDep, BlocklistRepositoryDep, etc) - Services orchestrate operations through repositories - Only repositories execute SQL queries The repository boundary is now technically enforced through the dependency injection system, making it impossible for routers to accidentally bypass repositories and access the database directly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user