feat(rulebook): service layer — Rule CRUD with multi-filter list_rules

This commit is contained in:
2026-05-27 21:18:35 -04:00
parent 38e4220015
commit d3833ba5a4
2 changed files with 197 additions and 0 deletions
+132
View File
@@ -211,3 +211,135 @@ async def delete_topic(topic_id: int, user_id: int) -> None:
return
await session.delete(topic)
await session.commit()
# ── Rule CRUD ──────────────────────────────────────────────────────────
from fabledassistant.models.rulebook import Rule
async def _assert_topic_owned(session, topic_id: int, user_id: int) -> None:
"""Raise ValueError if topic doesn't exist or isn't in user's rulebook."""
result = await session.execute(
select(RulebookTopic)
.join(Rulebook, RulebookTopic.rulebook_id == Rulebook.id)
.where(
RulebookTopic.id == topic_id,
Rulebook.owner_user_id == user_id,
)
)
if result.scalar_one_or_none() is None:
raise ValueError(f"topic {topic_id} not found")
async def create_rule(
topic_id: int, user_id: int, title: str, statement: str,
why: str = "", how_to_apply: str = "", order_index: int = 0,
) -> Rule:
async with async_session() as session:
await _assert_topic_owned(session, topic_id, user_id)
rule = Rule(
topic_id=topic_id,
title=title,
statement=statement,
why=why or None,
how_to_apply=how_to_apply or None,
order_index=order_index,
)
session.add(rule)
await session.commit()
await session.refresh(rule)
return rule
async def list_rules(
user_id: int,
rulebook_id: int | None = None,
topic_id: int | None = None,
project_id: int | None = None,
) -> list[Rule]:
"""List rules filtered by any of the three IDs. All filters are ownership-scoped.
project_id resolves rules through project_rulebook_subscriptions.
"""
from fabledassistant.models.rulebook import project_rulebook_subscriptions
async with async_session() as session:
stmt = (
select(Rule)
.join(RulebookTopic, Rule.topic_id == RulebookTopic.id)
.join(Rulebook, RulebookTopic.rulebook_id == Rulebook.id)
.where(Rulebook.owner_user_id == user_id)
)
if topic_id:
stmt = stmt.where(Rule.topic_id == topic_id)
if rulebook_id:
stmt = stmt.where(RulebookTopic.rulebook_id == rulebook_id)
if project_id:
stmt = (
stmt.join(
project_rulebook_subscriptions,
project_rulebook_subscriptions.c.rulebook_id == Rulebook.id,
)
.where(project_rulebook_subscriptions.c.project_id == project_id)
)
stmt = stmt.order_by(
Rulebook.id, RulebookTopic.order_index, Rule.order_index, Rule.title,
)
result = await session.execute(stmt)
return list(result.scalars().all())
async def get_rule(rule_id: int, user_id: int) -> Optional[Rule]:
async with async_session() as session:
result = await session.execute(
select(Rule)
.join(RulebookTopic, Rule.topic_id == RulebookTopic.id)
.join(Rulebook, RulebookTopic.rulebook_id == Rulebook.id)
.where(
Rule.id == rule_id,
Rulebook.owner_user_id == user_id,
)
)
return result.scalar_one_or_none()
async def update_rule(rule_id: int, user_id: int, **fields) -> Optional[Rule]:
async with async_session() as session:
result = await session.execute(
select(Rule)
.join(RulebookTopic, Rule.topic_id == RulebookTopic.id)
.join(Rulebook, RulebookTopic.rulebook_id == Rulebook.id)
.where(
Rule.id == rule_id,
Rulebook.owner_user_id == user_id,
)
)
rule = result.scalar_one_or_none()
if rule is None:
return None
allowed = {"title", "statement", "why", "how_to_apply", "order_index"}
for key, value in fields.items():
if key in allowed and value is not None:
setattr(rule, key, value)
await session.commit()
await session.refresh(rule)
return rule
async def delete_rule(rule_id: int, user_id: int) -> None:
async with async_session() as session:
result = await session.execute(
select(Rule)
.join(RulebookTopic, Rule.topic_id == RulebookTopic.id)
.join(Rulebook, RulebookTopic.rulebook_id == Rulebook.id)
.where(
Rule.id == rule_id,
Rulebook.owner_user_id == user_id,
)
)
rule = result.scalar_one_or_none()
if rule is None:
return
await session.delete(rule)
await session.commit()