feat(rulebook): SQLAlchemy models for rulebooks, topics, rules

This commit is contained in:
2026-05-27 21:16:49 -04:00
parent 05e379263a
commit 45c2197cdf
2 changed files with 120 additions and 0 deletions
+3
View File
@@ -37,3 +37,6 @@ from fabledassistant.models.share import NoteShare, ProjectShare # noqa: E402,
from fabledassistant.models.notification import Notification # noqa: E402, F401
from fabledassistant.models.api_key import ApiKey # noqa: E402, F401
from fabledassistant.models.user_profile import UserProfile # noqa: E402, F401
from fabledassistant.models.rulebook import ( # noqa: E402, F401
Rulebook, RulebookTopic, Rule, project_rulebook_subscriptions,
)
+117
View File
@@ -0,0 +1,117 @@
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
class Rulebook(Base):
__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):
__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):
__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)),
)