Files
justin 297cb797d6 Add core data model (12 tables) and initial Alembic migration
All core entities from ARCHITECTURE §5: tenancy (User, Tree, TreeMembership), people (Person, Name, Relationship), facts (Event, Place, PlaceName), provenance (Source, Citation), and the append-only AuditEntry. Cross-cutting mixins give every row a UUID key, timestamps, soft delete, and (where tree-owned) a tree_id for uniform tenant isolation.

Modeling choices: parentage as qualified edges (biological/adoptive/step/foster/donor/guardian) so non-traditional families are first-class; events keep both a verbatim date string and a normalized start/end range; closed sets are PG enums while GEDCOM-extensible vocabularies (event/name/source type) stay strings; CHECK constraints enforce single-subject events and single-target citations. Place is tree-scoped in Phase 0 (see ARCHITECTURE note). The migration is verified reversible (upgrade/downgrade drops tables and enum types) and matches the models (alembic check clean); applied on the deploy target. Dockerfile now ships migrations.

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:00 -04:00

43 lines
1.6 KiB
Python

"""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)