feat(B-EVT+B-SCHEMA): Event-System Rollen + Schema Authority dokumentiert
B-EVT: Plugin-Dev-Guide Kapitel 8 — 4 Event-Systeme mit Rollen: - HookRegistry (Lifecycle), EventBus (ephemeral), Outbox (durable), WebhookDispatcher (external) - Entscheidungsregel: Wann welches System - Verboten: dieselbe Funktion über Hook UND EventBus B-SCHEMA: Plugin-Dev-Guide Kapitel 9 — Schema Authority: - Core → Alembic, Plugin → Plugin-Migrationen, Runtime Auto-Sync → nicht authoritative - Migration-Staffelung, Plugin-Migrationen, keine Schema-Drift
This commit is contained in:
@@ -964,4 +964,144 @@ total_cost += result["cost_usd"]
|
||||
|
||||
---
|
||||
|
||||
## 8. Event-System Rollen
|
||||
|
||||
LeoCRM hat **4 Event-Systeme** mit klar getrennten Rollen. **Nicht dieselbe Funktion über Hook UND EventBus triggern.**
|
||||
|
||||
### Übersicht
|
||||
|
||||
| System | Rolle | Persistenz | Use Case |
|
||||
|--------|-------|-----------|----------|
|
||||
| **HookRegistry** | Lifecycle-Erweiterungspunkte | In-Memory | `contact.before_create`, `mail.after_send`, `dms.after_delete` — Plugins können Daten anpassen oder reagieren |
|
||||
| **EventBus** | Flüchtige interne Events | In-Memory | `notification.created`, `ui.contact_selected` — asynchrone Notifikationen, UI-Events, Proactive Suggestions |
|
||||
| **Outbox** | Dauerhafte Domain Events | DB (transactional) | `contact.created`, `mail.received`, `task.completed` — reliable Delivery, Retry, DLQ, Worker-Polling |
|
||||
| **WebhookDispatcher** | Externe HTTP-Zustellung | DB + HTTP | Externe Webhooks an registrierte URLs — Retry, Auth, Payload-Signatur |
|
||||
|
||||
### 8.1 HookRegistry (`app/core/hooks.py`)
|
||||
|
||||
**Wann verwenden:** Wenn ein Plugin bei einem Lifecycle-Punkt Daten anpassen oder reagieren will.
|
||||
|
||||
```python
|
||||
from app.core.hooks import get_hook_registry
|
||||
|
||||
reg = get_hook_registry()
|
||||
|
||||
# Action — kein Return, nur Seiteneffekte
|
||||
reg.register_action("contact.before_create", self._on_contact_create, priority=10)
|
||||
|
||||
# Filter — Return modifizierten Wert
|
||||
def _format_name(self, name: str) -> str:
|
||||
return name.title()
|
||||
reg.register_filter("contact.format_display_name", self._format_name, priority=10)
|
||||
```
|
||||
|
||||
**Aufruf im Core/Plugin-Service:**
|
||||
```python
|
||||
from app.core.hooks import do_action, apply_filters
|
||||
|
||||
await do_action("contact.before_create", contact_data, db=db)
|
||||
display_name = await apply_filters("contact.format_display_name", contact.name)
|
||||
```
|
||||
|
||||
### 8.2 EventBus (`app/core/event_bus.py`)
|
||||
|
||||
**Wann verwenden:** Für flüchtige interne Notifikationen, UI-Events, Proactive Suggestions. **Nicht** für Events die reliable Delivery brauchen.
|
||||
|
||||
```python
|
||||
from app.core.event_bus import get_event_bus
|
||||
|
||||
event_bus = get_event_bus()
|
||||
|
||||
# Subscribe
|
||||
event_bus.subscribe("notification.created", self._on_notification)
|
||||
|
||||
# Publish (ephemeral — geht verloren bei Crash)
|
||||
await event_bus.publish("notification.created", {"user_id": "...", "message": "..."})
|
||||
```
|
||||
|
||||
### 8.3 Outbox (`app/core/outbox.py`)
|
||||
|
||||
**Wann verwenden:** Für dauerhafte Domain Events die reliable Delivery, Retry und Worker-Verarbeitung brauchen.
|
||||
|
||||
```python
|
||||
from app.core.outbox import enqueue_outbox_event
|
||||
|
||||
# In derselben Transaktion wie die Business-Operation
|
||||
await enqueue_outbox_event(db, tenant_id, "contact.created", {
|
||||
"contact_id": str(contact.id),
|
||||
"tenant_id": str(tenant_id),
|
||||
})
|
||||
# Transaction commit → Event ist durable → Worker pollt und published an EventBus
|
||||
```
|
||||
|
||||
**Features:** DLQ (`error_message`, `failed_at`), Replay (`replay_failed_event`), Consumer Registry, Stats.
|
||||
|
||||
### 8.4 WebhookDispatcher (`app/core/webhook_dispatcher.py`)
|
||||
|
||||
**Wann verwenden:** Für externe HTTP-Zustellung an registrierte Webhook-URLs.
|
||||
|
||||
```python
|
||||
from app.core.webhook_dispatcher import register_webhook_event_handlers
|
||||
|
||||
# Wird automatisch im Worker registriert — Plugins müssen nur Webhook-Configs erstellen
|
||||
# und Events über die Outbox publishen
|
||||
```
|
||||
|
||||
### 8.5 Entscheidungsregel
|
||||
|
||||
```text
|
||||
Braucht das Event reliable Delivery + Retry?
|
||||
→ JA → Outbox
|
||||
→ NEIN → Braucht es Daten-Anpassung (Filter)?
|
||||
→ JA → HookRegistry (register_filter)
|
||||
→ NEIN → Braucht es nur Reaktion (Action)?
|
||||
→ JA → HookRegistry (register_action)
|
||||
→ NEIN → Ist es eine flüchtige Notifikation / UI-Event?
|
||||
→ JA → EventBus
|
||||
→ NEIN → Geht es an externe Systeme?
|
||||
→ JA → WebhookDispatcher (über Outbox)
|
||||
→ NEIN → Braucht kein Event
|
||||
```
|
||||
|
||||
**Verboten:** Dieselbe Funktion über Hook UND EventBus triggern — das führt zu Doppel-Ausführung und Race-Conditions.
|
||||
|
||||
---
|
||||
|
||||
## 9. Schema Authority
|
||||
|
||||
LeoCRM hat eine klare Schema-Authority-Hierarchie. **Kein neuer Schema-Mechanismus.**
|
||||
|
||||
### Authority-Regeln
|
||||
|
||||
| Schema-Typ | Authority | Wie |
|
||||
|-----------|----------|-----|
|
||||
| **Core-Tabellen** | Alembic-Migrationen | `alembic revision --autogenerate -m "..."` → `alembic upgrade head` |
|
||||
| **Plugin-Tabellen** | Plugin-Migrationsweg | `plugin/migrations/` → `sync_plugin_schema.py` bei Aktivierung |
|
||||
| **Runtime Auto-Sync** | **Nicht authoritative** | `Base.metadata.create_all` in Tests/dev — nie in Produktion |
|
||||
|
||||
### Verbindliche Regeln
|
||||
|
||||
1. **Core-Schema-Änderungen** immer über Alembic-Migrationen — nie manuelle SQL-Statements in Produktion
|
||||
2. **Plugin-Schema-Änderungen** über Plugin-Migrationen — nie Core-Migrationen für Plugin-Tabellen
|
||||
3. **Runtime Auto-Sync** (`create_all`, `sync_plugin_schema.py`) ist Convenience für Dev/Tests — **nicht** für Produktion authoritative
|
||||
4. **Migration-Staffelung** beachten: neu → migrieren → umstellen → testen → release → alt entfernen
|
||||
5. **Keine Schema-Drift** — wenn Core und Plugin dasselbe Modell nutzen, ist Core authoritative
|
||||
|
||||
### Plugin-Migrationen
|
||||
|
||||
```python
|
||||
# plugin/migrations/001_initial.py
|
||||
from alembic import op
|
||||
|
||||
def upgrade():
|
||||
op.create_table("my_plugin_table", ...)
|
||||
|
||||
def downgrade():
|
||||
op.drop_table("my_plugin_table")
|
||||
```
|
||||
|
||||
Plugin-Migrationen werden bei Plugin-Aktivierung automatisch ausgeführt (`sync_plugin_schema.py`). Bei Deaktivierung bleiben die Tabellen erhalten (Soft-Deactivate). Bei Uninstall werden sie gedroppt.
|
||||
|
||||
---
|
||||
|
||||
*This document is authoritative for all plugin development at LeoCRM.*
|
||||
|
||||
Reference in New Issue
Block a user