Files
leocrm-bot 7a5a48fb4c T03: plugin system framework + lifecycle + migrations + event bus + DI
- Plugin registry with discover/install/activate/deactivate/uninstall lifecycle
- PluginManifest Pydantic v2 schema (name, version, dependencies, routes, events, migrations)
- BasePlugin abstract class with lifecycle hooks (on_install/activate/deactivate/uninstall)
- Migration runner with tenant_id validator (rejects tables without tenant_id)
- Event bus integration: register/unregister listeners on activate/deactivate
- Service container DI: plugins receive db, cache, event_bus, storage, notifications
- Idempotent operations (activate active=200, deactivate inactive=200)
- UI registry for frontend component registration
- 47 new tests (14 ACs + 33 unit tests), 103 total tests pass
- Migration 0003: plugins + plugin_migrations tables
- Coverage: 85.92% for plugin modules
2026-06-29 01:18:46 +02:00

69 lines
2.4 KiB
Python

"""Plugin and PluginMigration models — tracks installed plugins and their migrations."""
from __future__ import annotations
import uuid
from datetime import datetime
from typing import Any
from sqlalchemy import String, Boolean, Text, DateTime, func, Index
from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TimestampMixin
class Plugin(Base, TimestampMixin):
"""Plugin record — tracks installation and activation status.
This table is NOT tenant-scoped (plugins are system-wide, not per-tenant).
The tables *created by* plugin migrations MUST have tenant_id.
"""
__tablename__ = "plugins"
__table_args__ = (
Index("ix_plugins_name", "name", unique=True),
)
id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
name: Mapped[str] = mapped_column(String(80), nullable=False, unique=True)
display_name: Mapped[str] = mapped_column(String(120), nullable=False)
version: Mapped[str] = mapped_column(String(40), nullable=False)
status: Mapped[str] = mapped_column(
String(20), nullable=False, default="installed"
)
installed: Mapped[bool] = mapped_column(
Boolean, nullable=False, default=True
)
active: Mapped[bool] = mapped_column(
Boolean, nullable=False, default=False
)
config: Mapped[dict[str, Any] | None] = mapped_column(
Text, nullable=True # JSON string for plugin configuration
)
# Transient attribute for response (not persisted)
# Used by uninstall to report dropped tables
# This is set dynamically by registry.uninstall()
class PluginMigration(Base, TimestampMixin):
"""Tracks individual migration files applied for each plugin."""
__tablename__ = "plugin_migrations"
__table_args__ = (
Index("ix_plugin_migrations_plugin", "plugin_name"),
Index("ix_plugin_migrations_unique", "plugin_name", "migration_file", unique=True),
)
id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
plugin_name: Mapped[str] = mapped_column(String(80), nullable=False)
migration_file: Mapped[str] = mapped_column(String(255), nullable=False)
status: Mapped[str] = mapped_column(
String(20), nullable=False, default="applied"
)