mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-09 05:24:49 +09:00
An extension that needed to remember anything had to add a model to core AND an
Alembic migration for it — which is why extension tables like
ext_serverkit_minecraft_* sit in the panel's own migration history. That is a
steep tax on a small extension and it puts plugin schema in the panel's upgrade
path. A plugin now writes to a key/value store instead:
mine = store.for_plugin('serverkit-minecraft')
mine.set('world:1:seed', 8675309)
Values are JSON, so dicts and lists round-trip without hand-serialising, and
rows are namespaced by slug so two plugins cannot collide on a key. Reach for a
real table when you need to query BY the data — this is a key/value store, not
a database, and the docstring says so.
Because the table is shared by every installed plugin, three things are refused
rather than trusted: a value that isn't JSON-serializable, a value over 256 KB,
and an empty or overlong key. Validation happens at set() rather than at commit
— by commit time the error is opaque and the session is already poisoned, so
the next unrelated write fails too.
has() exists because get() cannot distinguish a stored None from a missing key,
and a plugin storing None deliberately would otherwise have no way to tell.
Concurrent writers race on insert; the unique constraint makes that safe and
the loser applies its write to the winner's row instead of failing the caller.
Store rows follow the same rule as extension-declared tables: an ordinary
uninstall leaves them, so a reinstall finds its state where it left it, and
"uninstall and delete data" removes them. The purge runs after the uninstall
lifecycle hook, since a hook may legitimately read its own store on the way out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
55 lines
2.1 KiB
Python
55 lines
2.1 KiB
Python
"""Per-plugin key/value storage.
|
|
|
|
An extension that needed to remember anything previously had to add a model to
|
|
core plus an Alembic migration — which is why extension tables like
|
|
``ext_serverkit_minecraft_*`` live in the core migration history. That is a
|
|
steep tax on small extensions and it puts plugin schema in the panel's own
|
|
upgrade path.
|
|
|
|
One row here is one key for one plugin. ``value`` is a native JSON column (the
|
|
dominant pattern in this codebase, and dialect-agnostic across SQLite and
|
|
PostgreSQL), so a plugin stores dicts, lists and scalars without serialising by
|
|
hand. Keys are opaque to the panel; namespace inside your own key space
|
|
(``world:1:seed``) if you want structure.
|
|
|
|
This does NOT replace ``InstalledPlugin.config``: config is panel-owned settings
|
|
an admin edits from the Marketplace, whereas the store is the plugin's own
|
|
state, written at runtime by the plugin itself.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
|
|
from app import db
|
|
|
|
|
|
class PluginStore(db.Model):
|
|
"""One key/value pair belonging to one installed plugin."""
|
|
|
|
__tablename__ = 'plugin_store'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
# Not a ForeignKey to installed_plugins: rows must survive the plugin row
|
|
# being deleted and re-created by a reinstall, which is the whole point of
|
|
# keeping data across an uninstall that wasn't asked to purge.
|
|
plugin_slug = db.Column(db.String(128), nullable=False, index=True)
|
|
key = db.Column(db.String(255), nullable=False)
|
|
value = db.Column(db.JSON)
|
|
|
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
|
|
__table_args__ = (
|
|
db.UniqueConstraint('plugin_slug', 'key', name='uq_plugin_store_slug_key'),
|
|
)
|
|
|
|
def to_dict(self):
|
|
return {
|
|
'plugin_slug': self.plugin_slug,
|
|
'key': self.key,
|
|
'value': self.value,
|
|
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
def __repr__(self):
|
|
return f'<PluginStore {self.plugin_slug}:{self.key}>'
|