Newer
Older
gn-synapse / app / models / rules.py
"""Правила маршрутизации (Routing Engine, задача #30).

Условия — JSONB:
    {"source": "monitoring" | null,   # null = любой источник
     "subjects": [],                  # пусто = любые subject
     "actions": [],                   # пусто = любые action
     "priority_min": "normal",        # в шкале low/normal/high/critical
     "payload": {"container": "api"} # точное совпадение по top-level ключам
                                    # payload'а; значение-список = any-of}
"""

from datetime import datetime

from sqlalchemy import (
    CheckConstraint,
    DateTime,
    ForeignKey,
    Integer,
    String,
    Text,
    func,
)
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column

from app.database import Base
from app.models.dicts import CHANNELS


class RoutingRule(Base):
    """Правило: условия → набор действий (каналы/цели/шаблоны).

    weight — порядок применения (важен в режиме first). Режим all/first
    задаётся настройкой ROUTING_MATCH_MODE (app/config.py, дефолт all).

    throttle_seconds — шумодав: если доставки в ту же (channel, target)
    были созданы не позже N секунд назад (pending/delivered), эта —
    записывается со статусом skipped. critical проходит всегда
    (обход шумодавов — часть смысла критичности, см. docs/05).

    Теги — позже, когда появится реальное правило, требующее тега.
    """

    __tablename__ = "routing_rules"

    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    name: Mapped[str] = mapped_column(String(120))
    enabled: Mapped[bool] = mapped_column(default=True)
    weight: Mapped[int] = mapped_column(default=0)
    conditions: Mapped[dict] = mapped_column(JSONB)
    # Шумодав: не чаще одной доставки в (channel, target) за N секунд.
    # None = без ограничения. critical НЕ обходится автоматически — см. докстринг.
    throttle_seconds: Mapped[int | None] = mapped_column(Integer)
    # Шаблон по умолчанию для доставок правила (Jinja2/`{{ payload.x }}`);
    # individual action может переопределить.
    template: Mapped[str | None] = mapped_column(Text)
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), server_default=func.now()
    )
    updated_at: Mapped[datetime | None] = mapped_column(
        DateTime(timezone=True), onupdate=func.now()
    )


class RoutingRuleAction(Base):
    """Действие правила: доставить в канал channel в цель target."""

    __tablename__ = "routing_rule_actions"
    __table_args__ = (CheckConstraint(f"channel IN {CHANNELS}", name="ck_channel"),)

    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    rule_id: Mapped[int] = mapped_column(
        ForeignKey("routing_rules.id", ondelete="CASCADE"), index=True
    )
    channel: Mapped[str] = mapped_column(String(32))
    target_id: Mapped[int | None] = mapped_column(
        ForeignKey("channel_targets.id", ondelete="SET NULL")
    )
    template: Mapped[str | None] = mapped_column(Text)  # переопределение шаблона