from datetime import datetime, timezone from sqlalchemy import BigInteger, Column, DateTime, ForeignKey, Integer, Table, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column from fabledassistant.models import Base from fabledassistant.models.base import SoftDeleteMixin class Rulebook(Base, SoftDeleteMixin): __tablename__ = "rulebooks" id: Mapped[int] = mapped_column(BigInteger, primary_key=True) owner_user_id: Mapped[int] = mapped_column( BigInteger, ForeignKey("users.id", ondelete="CASCADE") ) title: Mapped[str] = mapped_column(Text) description: Mapped[str | None] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), ) def to_dict(self) -> dict: return { "id": self.id, "owner_user_id": self.owner_user_id, "title": self.title, "description": self.description or "", "created_at": self.created_at.isoformat() if self.created_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None, } class RulebookTopic(Base, SoftDeleteMixin): __tablename__ = "rulebook_topics" __table_args__ = ( UniqueConstraint("rulebook_id", "title", name="uq_topic_per_rulebook"), ) id: Mapped[int] = mapped_column(BigInteger, primary_key=True) rulebook_id: Mapped[int] = mapped_column( BigInteger, ForeignKey("rulebooks.id", ondelete="CASCADE") ) title: Mapped[str] = mapped_column(Text) description: Mapped[str | None] = mapped_column(Text, nullable=True) order_index: Mapped[int] = mapped_column(Integer, default=0) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), ) def to_dict(self) -> dict: return { "id": self.id, "rulebook_id": self.rulebook_id, "title": self.title, "description": self.description or "", "order_index": self.order_index, "created_at": self.created_at.isoformat() if self.created_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None, } class Rule(Base, SoftDeleteMixin): __tablename__ = "rules" __table_args__ = ( UniqueConstraint("topic_id", "title", name="uq_rule_per_topic"), ) id: Mapped[int] = mapped_column(BigInteger, primary_key=True) topic_id: Mapped[int] = mapped_column( BigInteger, ForeignKey("rulebook_topics.id", ondelete="CASCADE") ) title: Mapped[str] = mapped_column(Text) statement: Mapped[str] = mapped_column(Text) why: Mapped[str | None] = mapped_column(Text, nullable=True) how_to_apply: Mapped[str | None] = mapped_column(Text, nullable=True) order_index: Mapped[int] = mapped_column(Integer, default=0) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), ) def to_dict(self) -> dict: return { "id": self.id, "topic_id": self.topic_id, "title": self.title, "statement": self.statement, "why": self.why or "", "how_to_apply": self.how_to_apply or "", "order_index": self.order_index, "created_at": self.created_at.isoformat() if self.created_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None, } # Pure many-to-many — no model class, just the join table. project_rulebook_subscriptions = Table( "project_rulebook_subscriptions", Base.metadata, Column("project_id", BigInteger, ForeignKey("projects.id", ondelete="CASCADE"), primary_key=True), Column("rulebook_id", BigInteger, ForeignKey("rulebooks.id", ondelete="CASCADE"), primary_key=True), Column("created_at", DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)), )