Files
justin dffd05d303 Add layered service/API for tenancy and people with the privacy seam
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>
2026-06-06 10:40:19 -04:00

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