refactor: Beanie documents are the entities, with no domain copies or hand-written conversions
Summary
Closes #423, part of #308. Every stored entity lived three times, as a schema, a domain dataclass and a Beanie document, and routes and repositories copied between them with **model_dump() and asdict(). mypy can't check those copies, so #308's SecretStr change would have meant three unchecked edits per field.
Changes
The document is the entity now. Services and repositories take and return documents, the mirror dataclasses and their create and update variants are gone, and nothing under services/ or db/ imports a schema. An update's changes are a TypedDict that the route builds with a TypeAdapter: a field left out keeps its value, null clears a nullable one, and null on a field that can't be empty now answers 422. Embedded values are domain dataclasses in bson_encoders, and indexed fields are Annotated[..., Indexed()], so mypy sees their real types.
[!WARNING]
Where this departs from #423:
- Unit tests run
init_beanie once on a mocked database, so documents build through their constructors; model_construct() would skip validation.
SystemSettings stays. It's the value inside SystemSettingsDocument.config, not a copy of the document.
- The settings update keeps
null as "unchanged", since the settings page sends null for the sections it didn't touch.
- The uncalled
get_replay_status and ReplaySessionStatusInfo are deleted instead of converted.
- mypy doesn't check document constructor arguments until lazy-model ships
py.typed (BeanieODM/lazy_model#16).
Type of Change
- Bug fix (non-breaking change that fixes an issue)
- New feature (non-breaking change that adds functionality)
- Breaking change (fix or feature that would cause existing functionality to change)
- Documentation update
- Refactoring (no functional changes)
- CI/CD or tooling changes
Test Plan
- Backend unit tests pass (
cd backend && uv run pytest tests/unit) - Frontend unit tests pass (
cd frontend && npm run test) - Backend E2E tests pass (
docker compose exec -T backend sh -c 'uv sync --group test --no-dev --frozen --no-install-project --no-build && pytest tests/e2e') - Frontend E2E tests pass (
cd frontend && npm run test:e2e) - API spec and client regenerated if routes or schemas changed (
./deploy.sh openapi, then./deploy.sh types), never edited by hand - Manual testing performed
Validation: ruff, mypy --strict, the orphan check, 573 unit tests, the Grafana contract tests and mkdocs build --strict pass. ./deploy.sh openapi leaves the spec unchanged. The full backend e2e suite with k3s in a container and the worker gives 493 passed, 1 skipped here and 489 passed, 1 skipped on main in the same setup. The 4 extra are the new tests.
Checklist
- My code follows the project's style guidelines
- I have run
pre-commit run --all-filesand all checks pass - I have added tests that prove my fix/feature works
- I have updated documentation if needed
- My changes don't introduce new warnings
Summary by cubic
Makes Beanie documents the single entity for stored data. They previously lived three times — as an API schema, a domain dataclass, and a Beanie document — with routes and repositories copying between them via **model_dump() and asdict(); those copies bypass mypy, so the planned SecretStr change would have meant three unchecked edits per field. Services and repositories now take and return documents, the mirror dataclasses and their create/update variants are gone, and nothing under services/ or db/ imports a schema.
Behavior and adoption notes
- Update changes are
TypedDicts validated byTypeAdapterin the routes: a missing key keeps the stored value,nullclears a nullable field, andnullon a field that can't be empty answers 422. - The user settings endpoint keeps
nullas "unchanged", since the settings page sendsnullfor sections it didn't touch. SystemSettingsstays as the value insideSystemSettingsDocument.config, and the uncalledget_replay_statusandReplaySessionStatusInfoare deleted instead of converted.- Indexed fields are declared
Annotated[..., Indexed()]so mypy sees the wrapped type; document constructor arguments aren't checked untillazy-modelshipspy.typed. - Unit tests run
init_beanieonce on a mocked database, so documents build through their validated constructors rather thanmodel_construct(). - The OpenAPI spec is unchanged; repository and service signatures changed, so callers now build documents directly.