mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-30 16:47:01 +09:00
An audit found 102 FK columns with no index — every join, per-parent listing, and the delete cascades from _delete_cascade_policy scanned whole tables on them (deployments.app_id, audit_logs.user_id, dns_records.zone_id, webhook_logs.webhook_id, ...). All FK columns now declare index=True; migration 096 creates the same indexes on existing installs, deriving the list from the live schema at upgrade time (so it is idempotent and cannot drift from what an install actually has). Ratchet: test_every_fk_column_is_indexed pins the rule with no allowlist — a new FK column is born indexed or the build fails. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
120 lines
4.8 KiB
Python
120 lines
4.8 KiB
Python
"""Installed plugin tracking model."""
|
|
from datetime import datetime
|
|
from app import db
|
|
from app.models.json_column_mixin import JsonColumnMixin
|
|
import json
|
|
|
|
|
|
class InstalledPlugin(JsonColumnMixin, 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'), index=True)
|
|
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 self._json_read('manifest_json')
|
|
|
|
@manifest.setter
|
|
def manifest(self, v):
|
|
self.manifest_json = json.dumps(v)
|
|
|
|
@property
|
|
def config(self):
|
|
return self._json_read('config_json')
|
|
|
|
@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 {},
|
|
# Install-time signature verdict (plan 55): None for sources that
|
|
# predate stamping, else {status: verified|untrusted_key|unsigned,
|
|
# key_id?, publisher?}. Panel-managed (reserved config key).
|
|
'signature': (self.config or {}).get('_signature'),
|
|
'installed_at': self.installed_at.isoformat() if self.installed_at else None,
|
|
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|