dffd05d303
Wires the data model through repository -> service -> API/v1. The privacy engine (app/services/privacy.py) is the single enforcement point: every read resolves visibility there (tree role, tree visibility, per-person override; living-person redaction is a marked Phase 2 TODO). All writes record an attributable AuditEntry.
Endpoints: POST /users (open dev bootstrap until auth), GET /users/me, POST/GET /trees, GET /trees/{id}, and POST/GET /trees/{id}/persons. Authn is a temporary X-User-Id header shim; authz is membership-based (owner/editor/viewer). Domain errors map to 401/403/404/409. Verified on the deploy target: private tree -> 403 for non-members, missing actor -> 401, audit log populated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
41 lines
1.4 KiB
Python
41 lines
1.4 KiB
Python
"""Thin data-access layer over SQLAlchemy. No business rules live here — the
|
|
service layer owns those (and the privacy engine). The repository only knows how
|
|
to fetch and stage rows, transparently excluding soft-deleted ones.
|
|
"""
|
|
|
|
from typing import Any
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
class BaseRepository:
|
|
def __init__(self, session: AsyncSession, model: type) -> None:
|
|
self.session = session
|
|
self.model = model
|
|
|
|
def _exclude_deleted(self, stmt):
|
|
if hasattr(self.model, "deleted_at"):
|
|
stmt = stmt.where(self.model.deleted_at.is_(None))
|
|
return stmt
|
|
|
|
async def get(self, id_: Any, *, include_deleted: bool = False):
|
|
stmt = select(self.model).where(self.model.id == id_)
|
|
if not include_deleted:
|
|
stmt = self._exclude_deleted(stmt)
|
|
return (await self.session.execute(stmt)).scalar_one_or_none()
|
|
|
|
async def list(self, *conditions, include_deleted: bool = False, order_by=None):
|
|
stmt = select(self.model)
|
|
for condition in conditions:
|
|
stmt = stmt.where(condition)
|
|
if not include_deleted:
|
|
stmt = self._exclude_deleted(stmt)
|
|
if order_by is not None:
|
|
stmt = stmt.order_by(order_by)
|
|
return list((await self.session.execute(stmt)).scalars().all())
|
|
|
|
def add(self, obj):
|
|
self.session.add(obj)
|
|
return obj
|