"""AuditEntry — append-only, immutable record of every mutation. The actor is a User, or the assistant principal acting *on behalf of* a User (actor_type = assistant, actor_user_id = the user it serves). No timestamps mixin and no soft delete: this table is never updated or deleted. """ import uuid from datetime import datetime from sqlalchemy import DateTime, ForeignKey, String, func from sqlalchemy import Enum as SAEnum from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column from app.models.base import Base from app.models.enums import AuditActorType from app.models.mixins import UUIDPrimaryKey class AuditEntry(Base, UUIDPrimaryKey): __tablename__ = "audit_entries" created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False, index=True ) tree_id: Mapped[uuid.UUID | None] = mapped_column( ForeignKey("trees.id", ondelete="SET NULL"), index=True ) actor_type: Mapped[AuditActorType] = mapped_column( SAEnum(AuditActorType, name="audit_actor_type"), default=AuditActorType.user, server_default=AuditActorType.user.value, ) actor_user_id: Mapped[uuid.UUID | None] = mapped_column( ForeignKey("users.id", ondelete="SET NULL"), index=True ) action: Mapped[str] = mapped_column(String(64)) # create | update | delete | restore | ... entity_type: Mapped[str] = mapped_column(String(64)) entity_id: Mapped[uuid.UUID | None] = mapped_column() before: Mapped[dict | None] = mapped_column(JSONB) after: Mapped[dict | None] = mapped_column(JSONB)