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>
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user