From cbeed4b26d44221470abbdfe8840509d23b5fd8d Mon Sep 17 00:00:00 2001 From: Leopoldadmin Date: Fri, 3 Jul 2026 16:48:10 +0000 Subject: [PATCH] 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. --- app/services/plugin_service.py | 72 +++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/app/services/plugin_service.py b/app/services/plugin_service.py index bf76272..63948fa 100644 --- a/app/services/plugin_service.py +++ b/app/services/plugin_service.py @@ -6,6 +6,7 @@ import logging import uuid from typing import Any +from fastapi import HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from app.core.audit import log_audit @@ -43,6 +44,7 @@ class PluginService: Runs migrations, calls on_install hook, creates DB record. Idempotent: returns existing record if already installed. + Checks that all declared dependencies are installed first. """ try: record = await self._registry.install(db, name) @@ -66,7 +68,13 @@ class PluginService: "message": "Plugin installed successfully", } 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: raise MigrationValidationError(str(exc)) from None @@ -80,6 +88,7 @@ class PluginService: """Activate a plugin by name. Registers event listeners and routes. Idempotent. + Checks that all declared dependencies are active first. """ try: record = await self._registry.activate(db, name) @@ -106,7 +115,13 @@ class PluginService: else "Plugin activated successfully", } 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( self, @@ -187,6 +202,59 @@ class PluginService: except ValueError as exc: 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]: """Return the manifest schema documentation.""" from app.plugins.manifest import MANIFEST_SCHEMA_DOC