"""Audit logging. Every mutation records an append-only AuditEntry attributing the change to a User (or the assistant principal acting for a User). Staged on the session; the caller commits as part of its unit of work. """ import json import uuid from sqlalchemy.ext.asyncio import AsyncSession from app.models.audit import AuditEntry from app.models.enums import AuditActorType def _json_safe(d: dict | None) -> dict | None: """Coerce a change dict to JSON-native types (UUIDs, enums, dates -> str) so it lands in the JSON audit column regardless of what the caller passed.""" if d is None: return None return json.loads(json.dumps(d, default=str)) def record_audit( session: AsyncSession, *, action: str, entity_type: str, entity_id: uuid.UUID | None = None, tree_id: uuid.UUID | None = None, actor_user_id: uuid.UUID | None = None, actor_type: AuditActorType = AuditActorType.user, before: dict | None = None, after: dict | None = None, ) -> AuditEntry: entry = AuditEntry( action=action, entity_type=entity_type, entity_id=entity_id, tree_id=tree_id, actor_user_id=actor_user_id, actor_type=actor_type, before=_json_safe(before), after=_json_safe(after), ) session.add(entry) return entry