サンドボックスプラグインは、プラグインごとの KVストアに設定を保存し、編集UIを Block Kit ページとしてレンダリングします。ネイティブプラグインが使用できる自動生成の admin.settingsSchema フォームは、サンドボックスでは利用できません。代わりに、フォームをJSONで記述し、ルートから提供します。
これは settingsSchema よりも少し手間がかかりますが、すべてはプラグインがすでにフックやルートで使用しているのと同じ仕組みを通じて行われます。特別に学ぶことはありません。
KVストア
すべてのプラグインには、任意のフックまたはルートで ctx.kv としてアクセスできるプライベートなキーバリューストアがあります。これは設定やその他の小さな永続的な状態を保存する標準的な場所です。
interface KVAccess {
get<T>(key: string): Promise<T | null>;
set(key: string, value: unknown): Promise<void>;
delete(key: string): Promise<boolean>;
list(prefix?: string): Promise<Array<{ key: string; value: unknown }>>;
}
KVはプラグインごとに分離されています。書き込んだキーはプラグインIDの下に保存され、他のプラグインからは見えません。
読み取りと書き込み
// 読み取り
const enabled = await ctx.kv.get<boolean>("settings:enabled");
const config = await ctx.kv.get<{ url: string; timeout: number }>("state:config");
// 書き込み
await ctx.kv.set("settings:lastSync", new Date().toISOString());
await ctx.kv.set("state:cache", { data: items, expiry: Date.now() + 3600000 });
// 削除
const deleted = await ctx.kv.delete("state:tempData");
// プレフィックスで一覧取得
const allSettings = await ctx.kv.list("settings:");
// → [{ key: "settings:enabled", value: true }, ...]
キー命名規則
プレフィックスを使用して、異なる種類の値を区別します。EmDashプラグインの規則:
| プレフィックス | 用途 | 例 |
|---|---|---|
settings: | ユーザー設定可能な設定 | settings:apiKey |
state: | 内部プラグイン状態 | state:lastSync |
cache: | キャッシュデータ | cache:results |
// 明確なプレフィックス
await ctx.kv.set("settings:webhookUrl", url);
await ctx.kv.set("state:lastRun", timestamp);
await ctx.kv.set("cache:feed", feedData);
// プレフィックスのないキーは避ける
await ctx.kv.set("url", url);
Block Kitでの設定UI
サンドボックスプラグインは、設定ページをBlock Kitとして記述します。管理画面は、プラグインのルート(通常は routes.admin)に page_load インタラクションを送信し、プラグインはフォームのJSON記述を返します。ユーザーが保存をクリックすると、管理画面は block_action または form_submit インタラクションを送り返します。プラグインはKVに書き込み、更新されたブロックを返します。
import { definePlugin } from "emdash";
import type { PluginContext } from "emdash";
interface BlockInteraction {
type: "page_load" | "block_action" | "form_submit";
page?: string;
action_id?: string;
values?: Record<string, unknown>;
}
export default definePlugin({
routes: {
admin: {
handler: async (routeCtx, ctx: PluginContext) => {
const interaction = routeCtx.input as BlockInteraction;
if (interaction.type === "page_load" && interaction.page === "/settings") {
return renderSettings(ctx);
}
if (interaction.type === "form_submit" && interaction.action_id === "save") {
await saveSettings(ctx, interaction.values ?? {});
return {
...(await renderSettings(ctx)),
toast: { message: "Settings saved", type: "success" },
};
}
return { blocks: [] };
},
},
},
});
async function renderSettings(ctx: PluginContext) {
const apiKey = (await ctx.kv.get<string>("settings:apiKey")) ?? "";
const enabled = (await ctx.kv.get<boolean>("settings:enabled")) ?? true;
const maxItems = (await ctx.kv.get<number>("settings:maxItems")) ?? 100;
return {
blocks: [
{ type: "header", text: "Plugin settings" },
{
type: "form",
block_id: "settings",
fields: [
{
type: "secret_input",
action_id: "apiKey",
label: "API key",
initial_value: apiKey,
},
{
type: "toggle",
action_id: "enabled",
label: "Enabled",
initial_value: enabled,
},
{
type: "number_input",
action_id: "maxItems",
label: "Max items",
min: 1,
max: 1000,
initial_value: maxItems,
},
],
submit: { label: "Save", action_id: "save" },
},
],
};
}
async function saveSettings(ctx: PluginContext, values: Record<string, unknown>) {
for (const [key, value] of Object.entries(values)) {
if (value !== undefined) {
await ctx.kv.set(`settings:${key}`, value);
}
}
}
設定ページを管理サイドバーに接続するには、ディスクリプタで宣言します。
adminPages: [{ path: "/settings", label: "Settings", icon: "settings" }],
EmDashは、そのパスの page_load インタラクションを自動的に admin ルートにルーティングします。
ブロックタイプ、フォームフィールド、条件フィールド、および @emdash-cms/blocks ビルダーヘルパーの完全なセットについては、Block Kit を参照してください。
シークレット値
Block Kitの secret_input フィールドは、マスクされた入力としてレンダリングされます。ユーザーがそこに入力した値は慎重に扱ってください。
{
type: "secret_input",
action_id: "apiKey",
label: "API key",
// 実際のシークレットで initial_value を初期化しないでください。空文字列またはセンチネルを渡し、
// ユーザーが空でない値を入力したときのみ上書きします。
initial_value: "",
}
保存時には、空文字列をスキップして、毎回既存のシークレットをクリアしないようにします。
async function saveSettings(ctx: PluginContext, values: Record<string, unknown>) {
if (typeof values.apiKey === "string" && values.apiKey.length > 0) {
await ctx.kv.set("settings:apiKey", values.apiKey);
}
// ... その他のフィールド
}
デフォルト値
KV読み取りは、書き込まれていないキーに対して null を返します。読み取り時にデフォルトを渡します。
const enabled = (await ctx.kv.get<boolean>("settings:enabled")) ?? true;
const maxItems = (await ctx.kv.get<number>("settings:maxItems")) ?? 100;
またはインストール時にデフォルトを永続化します。
hooks: {
"plugin:install": async (_event, ctx) => {
await ctx.kv.set("settings:enabled", true);
await ctx.kv.set("settings:maxItems", 100);
},
},
トレードオフは、plugin:install がインストールごとに1回しか実行されないことです。後のバージョンで新しい設定を追加した場合、新規インストールのみがデフォルトを取得します。既存のインストールでは、plugin:activate での移行(冪等:欠落している場合のみ書き込み)を行うか、読み取り時のフォールバックを使い続ける必要があります。
設定 vs ストレージ vs KV
| ユースケース | メカニズム |
|---|---|
| 管理者が編集可能な設定 | ctx.kv と settings: プレフィックス + Block Kit ページ |
| 内部プラグイン状態 | ctx.kv と state: プレフィックス |
| ドキュメントコレクション(クエリ) | ctx.storage |
KVは、文字列でキー付けされた小さな値用です。設定、同期カーソル、キャッシュされた計算など。クエリなし、インデックスなし。
Storageは、インデックス付きクエリを使用したドキュメントコレクション用です。フォーム送信、監査ログ、フィルタリング、ページネーション、またはカウントが必要なもの。
ストレージレイアウト
KV値は、プラグイン名前空間キーを持つ _options テーブルに格納されます。コードでは settings:apiKey を使用しますが、EmDashは plugin:<your-plugin-id>:settings:apiKey として保存します。プレフィックスは自動的に追加され、あるプラグインが別のプラグインのKVデータを読み取ったり上書きしたりするのを防ぎます。
ネイティブプラグイン:settingsSchema
ネイティブプラグインを作成している場合(React管理ページやPTコンポーネントが必要な場合)、definePlugin() 内で設定スキーマを直接宣言し、EmDashにフォームを自動生成させることができます。その方法については、ネイティブプラグイン を参照してください。