Files
BanGUI/Docs/DOMAIN_MODELS.md
Lukas 0d5882b32f Fix HIGH priority issues: unbounded queries, rate limiting, health checks
Issue #3 - Unbounded Query Results (OOM):
- get_all_archived_history() now uses keyset pagination with bounded max_rows (50k default)
- Added 'id' field to records from get_archived_history() and get_archived_history_keyset()
- Protocol signature updated with page_size, max_rows, last_ban_id params

Issue #7 - Docker Health Check Fails:
- Added curl to Dockerfile.backend runtime image
- HEALTHCHECK now uses 'curl -f http://localhost:8000/api/health'
- compose.prod.yml: increased start_period to 40s, timeout to 10s
- Frontend healthcheck proxies to backend /api/health

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-01 21:47:36 +02:00

4.2 KiB

Domain Models — Reference Guide

This document explains the domain model pattern used in BanGUI's backend and where to find examples.


What Are Domain Models?

Domain models (e.g., DomainActiveBanList, DomainJailConfig) are frozen dataclasses that represent pure business logic. They are defined in app/models/{domain}_domain.py and are returned by services.

Response models (e.g., ActiveBanListResponse, JailConfigResponse) are Pydantic models defined in app/models/{domain}.py. They are used only by routers for HTTP serialization.


Why This Separation?

Service (returns domain model)
    ↓
Router (converts domain → response via mapper)
    ↓
HTTP Response (Pydantic model)

Benefits:

  • Domain logic evolves without affecting API shape
  • Services are reusable across different frontends (GraphQL, gRPC, CLI)
  • Testing is simpler (no Pydantic overhead)
  • Changes to endpoint responses don't require service changes

Existing Domain Models

Domain Domain Model(s) Mapper Module
Ban DomainActiveBanList, DomainActiveBan, DomainBansByCountry ban_mappers.py
Jail DomainJailList, DomainJailDetail, DomainJailBannedIps, DomainActiveBan jail_mappers.py
Config DomainJailConfig, DomainJailConfigList, DomainGlobalConfig, DomainServiceStatus, DomainBantimeEscalation, DomainFilterConfig, DomainFilterList, DomainRegexTest, DomainMapColorThresholds config_mappers.py
History DomainHistoryList, DomainHistoryBanItem, DomainIpDetail, DomainIpTimelineEvent history_mappers.py
Server DomainServerSettings, DomainServerSettingsResult server_mappers.py
Blocklist DomainBlocklistSource, DomainImportLogEntry, DomainImportLogList, DomainImportSourceResult, DomainImportRunResult, DomainPreviewResult, DomainScheduleConfig, DomainScheduleInfo blocklist_mappers.py

The Pattern — Step by Step

Step 1: Define Domain Model in app/models/{domain}_domain.py

from dataclasses import dataclass

@dataclass(frozen=True)
class DomainJailConfig:
    """Configuration snapshot of a single jail (domain model)."""

    name: str
    ban_time: int
    max_retry: int
    find_time: int
    fail_regex: list[str]
    actions: list[str]                    # ← no default BEFORE default = FIELD ORDER ERROR
    date_pattern: str | None = None        # ← all fields with defaults come AFTER
    log_encoding: LogEncoding = "UTF-8"

⚠️ Field Order Rule: All fields without defaults must appear before all fields with defaults.

Step 2: Add Mapper in app/mappers/{domain}_mappers.py

def map_domain_jail_config_to_response(domain: DomainJailConfig) -> JailConfig:
    """Convert domain jail config to response model."""
    return JailConfig(
        name=domain.name,
        ban_time=domain.ban_time,
        ...
    )

Step 3: Service Returns Domain Model

# In app/services/jail_service.py
from app.models.config_domain import DomainJailConfig, DomainJailConfigList

async def get_jail_config(socket_path: str, name: str) -> DomainJailConfig:
    ...
    return DomainJailConfig(...)    # ← return domain model

Step 4: Router Uses Mapper at Boundary

# In app/routers/jail_config.py
from app.mappers import config_mappers

@router.get("/{name}", response_model=JailConfigResponse)
async def get_jail_config(...) -> JailConfigResponse:
    domain_result = await config_service.get_jail_config(socket_path, name)
    return config_mappers.map_domain_jail_config_to_response(domain_result)

Reference Implementation

ban_service.py + ban_mappers.py is the canonical example of the correct pattern. Study it first when adding a new service.


Common Issues

Field Ordering Error

TypeError: non-default argument 'actions' follows default argument

Fix: Move all fields with defaults (field: T | None = None) after all fields without defaults.

Forgetting the Mapper

If you refactor a service to return a domain model but forget to update the router, you'll get a type mismatch at the boundary. Always update router + service together.