fix(plugin): convert dependency errors to HTTPException 400, add config API

Bug 3: install_plugin() and activate_plugin() now catch dependency-related
ValueErrors and raise HTTPException 400 with the error detail.

Bug 4: Added get_plugin_config() and update_plugin_config() methods to
the service layer for the plugin config API endpoints.
This commit is contained in:
2026-07-03 16:48:10 +00:00
parent 826cf69c9a
commit cbeed4b26d
+70 -2
View File
@@ -6,6 +6,7 @@ import logging
import uuid import uuid
from typing import Any from typing import Any
from fastapi import HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.audit import log_audit from app.core.audit import log_audit
@@ -43,6 +44,7 @@ class PluginService:
Runs migrations, calls on_install hook, creates DB record. Runs migrations, calls on_install hook, creates DB record.
Idempotent: returns existing record if already installed. Idempotent: returns existing record if already installed.
Checks that all declared dependencies are installed first.
""" """
try: try:
record = await self._registry.install(db, name) record = await self._registry.install(db, name)
@@ -66,7 +68,13 @@ class PluginService:
"message": "Plugin installed successfully", "message": "Plugin installed successfully",
} }
except ValueError as exc: except ValueError as exc:
raise ValueError(str(exc)) from None msg = str(exc)
if "dependencies" in msg.lower():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=msg,
) from None
raise ValueError(msg) from None
except MigrationValidationError as exc: except MigrationValidationError as exc:
raise MigrationValidationError(str(exc)) from None raise MigrationValidationError(str(exc)) from None
@@ -80,6 +88,7 @@ class PluginService:
"""Activate a plugin by name. """Activate a plugin by name.
Registers event listeners and routes. Idempotent. Registers event listeners and routes. Idempotent.
Checks that all declared dependencies are active first.
""" """
try: try:
record = await self._registry.activate(db, name) record = await self._registry.activate(db, name)
@@ -106,7 +115,13 @@ class PluginService:
else "Plugin activated successfully", else "Plugin activated successfully",
} }
except ValueError as exc: except ValueError as exc:
raise ValueError(str(exc)) from None msg = str(exc)
if "dependencies" in msg.lower():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=msg,
) from None
raise ValueError(msg) from None
async def deactivate_plugin( async def deactivate_plugin(
self, self,
@@ -187,6 +202,59 @@ class PluginService:
except ValueError as exc: except ValueError as exc:
raise ValueError(str(exc)) from None raise ValueError(str(exc)) from None
async def get_plugin_config(self, db: AsyncSession, name: str) -> dict[str, Any]:
"""Get the configuration for a plugin.
Returns the plugin's config field parsed from JSON string.
"""
record = await self._registry._get_plugin_record(db, name)
if record is None:
raise ValueError(f"Plugin '{name}' is not installed")
import json
config_str = getattr(record, "config", None) or "{}"
try:
return json.loads(config_str) if isinstance(config_str, str) else config_str or {}
except (json.JSONDecodeError, TypeError):
return {}
async def update_plugin_config(
self,
db: AsyncSession,
name: str,
config: dict[str, Any],
tenant_id: uuid.UUID | None = None,
user_id: uuid.UUID | None = None,
) -> dict[str, Any]:
"""Update the configuration for a plugin.
Stores the config as a JSON string in the plugin's config field.
"""
import json
record = await self._registry._get_plugin_record(db, name)
if record is None:
raise ValueError(f"Plugin '{name}' is not installed")
record.config = json.dumps(config)
await db.flush()
if tenant_id and user_id:
await log_audit(
db,
tenant_id,
user_id,
action="plugin.update_config",
entity_type="plugin",
entity_id=getattr(record, "id", None),
changes={"name": name, "config": config},
)
return {
"name": name,
"config": config,
"message": "Plugin configuration updated successfully",
}
def get_manifest_schema(self) -> dict[str, Any]: def get_manifest_schema(self) -> dict[str, Any]:
"""Return the manifest schema documentation.""" """Return the manifest schema documentation."""
from app.plugins.manifest import MANIFEST_SCHEMA_DOC from app.plugins.manifest import MANIFEST_SCHEMA_DOC