mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-08 21:14:47 +09:00
Completes the deferred half of #30. InstalledPlugin gains config_json (migration 045); admin-gated GET/PUT /api/v1/plugins/<id>/config persists values; plugins read them via the new plugins_sdk.config(slug) accessor. The Marketplace Installed tab shows a Configure button for plugins declaring a config_schema and renders a real form (string/number/integer/boolean/enum/secret fields, defaults, hints). Values may hold secrets so they are excluded from to_dict() and only served by the admin endpoint. Documented in docs/EXTENSIONS.md. 2 new tests (config round-trip + SDK accessor + secret non-leak; non-object rejection); pipeline suite 18 green; lint 0 errors; build clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
115 lines
4.5 KiB
Python
115 lines
4.5 KiB
Python
"""Installed plugin tracking model."""
|
|
from datetime import datetime
|
|
from app import db
|
|
import json
|
|
|
|
|
|
class InstalledPlugin(db.Model):
|
|
"""Tracks plugins installed from external sources (zips/URLs)."""
|
|
__tablename__ = 'installed_plugins'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
name = db.Column(db.String(128), nullable=False, unique=True)
|
|
display_name = db.Column(db.String(256), nullable=False)
|
|
slug = db.Column(db.String(128), nullable=False, unique=True)
|
|
version = db.Column(db.String(32), nullable=False)
|
|
description = db.Column(db.Text)
|
|
author = db.Column(db.String(128))
|
|
homepage = db.Column(db.String(512))
|
|
repository = db.Column(db.String(512))
|
|
license = db.Column(db.String(64))
|
|
category = db.Column(db.String(64))
|
|
|
|
# Where it came from
|
|
source_url = db.Column(db.String(1024))
|
|
source_type = db.Column(db.String(32), default='url') # url, local, marketplace
|
|
|
|
# Paths relative to ServerKit root
|
|
backend_path = db.Column(db.String(512)) # e.g. app/plugins/serverkit-ai
|
|
frontend_path = db.Column(db.String(512)) # e.g. src/plugins/serverkit-ai
|
|
|
|
# Blueprint registration info from manifest
|
|
entry_point = db.Column(db.String(256)) # e.g. blueprint:ai_assistant_bp
|
|
url_prefix = db.Column(db.String(256)) # e.g. /api/v1/ai-assistant
|
|
|
|
# Frontend entry from manifest
|
|
frontend_entry = db.Column(db.String(256)) # e.g. components/AiAssistant.jsx
|
|
|
|
# Full manifest stored as JSON
|
|
manifest_json = db.Column(db.Text)
|
|
|
|
# Saved per-plugin config values (#49). The manifest's `config_schema`
|
|
# describes the fields; admins edit values from the Marketplace and the
|
|
# plugin reads them via plugins_sdk.config(slug). May hold secrets — kept
|
|
# out of to_dict; served only by the admin-gated config endpoint.
|
|
config_json = db.Column(db.Text)
|
|
|
|
# Status
|
|
STATUS_ACTIVE = 'active'
|
|
STATUS_DISABLED = 'disabled'
|
|
STATUS_ERROR = 'error'
|
|
STATUS_INSTALLING = 'installing'
|
|
status = db.Column(db.String(32), default=STATUS_INSTALLING)
|
|
error_message = db.Column(db.Text)
|
|
|
|
# Has frontend component that needs rebuild
|
|
has_frontend = db.Column(db.Boolean, default=False)
|
|
# Has backend blueprint
|
|
has_backend = db.Column(db.Boolean, default=False)
|
|
|
|
installed_by = db.Column(db.Integer, db.ForeignKey('users.id'))
|
|
installed_at = db.Column(db.DateTime, default=datetime.utcnow)
|
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
|
|
@property
|
|
def manifest(self):
|
|
return json.loads(self.manifest_json) if self.manifest_json else {}
|
|
|
|
@manifest.setter
|
|
def manifest(self, v):
|
|
self.manifest_json = json.dumps(v)
|
|
|
|
@property
|
|
def config(self):
|
|
return json.loads(self.config_json) if self.config_json else {}
|
|
|
|
@config.setter
|
|
def config(self, v):
|
|
self.config_json = json.dumps(v or {})
|
|
|
|
def to_dict(self):
|
|
manifest = self.manifest or {}
|
|
return {
|
|
'id': self.id,
|
|
'name': self.name,
|
|
'display_name': self.display_name,
|
|
'slug': self.slug,
|
|
'version': self.version,
|
|
'description': self.description,
|
|
'author': self.author,
|
|
'homepage': self.homepage,
|
|
'repository': self.repository,
|
|
'license': self.license,
|
|
'category': self.category,
|
|
'source_url': self.source_url,
|
|
'source_type': self.source_type,
|
|
'entry_point': self.entry_point,
|
|
'url_prefix': self.url_prefix,
|
|
'has_frontend': self.has_frontend,
|
|
'has_backend': self.has_backend,
|
|
'status': self.status,
|
|
'error_message': self.error_message,
|
|
# Surface declarative manifest fields so the install UI can
|
|
# show the user what they're approving without making a
|
|
# second round-trip to fetch the manifest.
|
|
'permissions': manifest.get('permissions') or [],
|
|
'contributions': manifest.get('contributions') or {},
|
|
'templates': manifest.get('templates') or [],
|
|
'lifecycle': manifest.get('lifecycle') or {},
|
|
# Schema only — saved VALUES may hold secrets and are served
|
|
# exclusively by the admin-gated /config endpoint.
|
|
'config_schema': manifest.get('config_schema') or {},
|
|
'installed_at': self.installed_at.isoformat() if self.installed_at else None,
|
|
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|