プラグインストレージ
プラグインは、データベースマイグレーションを記述することなく、ドキュメントコレクションに独自のデータを保存できます。プラグイン定義でコレクションとインデックスを宣言すると、EmDashが自動的にスキーマを処理します。
ストレージの宣言
Section titled “ストレージの宣言”definePlugin() 内でストレージコレクションを定義します:
import { definePlugin } from "emdash";
export default definePlugin({ id: "forms", version: "1.0.0",
storage: { submissions: { indexes: [ "formId", // Single-field index "status", "createdAt", ["formId", "createdAt"], // Composite index ["status", "createdAt"], ], }, forms: { indexes: ["slug"], }, },
// ...});storage 内の各キーはコレクション名です。indexes 配列は効率的にクエリ可能なフィールドをリストします。
ストレージコレクション API
Section titled “ストレージコレクション API”フックとルート内で ctx.storage 経由でコレクションにアクセスします:
"content:afterSave": async (event, ctx) => { const { submissions } = ctx.storage;
// CRUD 操作 await submissions.put("sub_123", { formId: "contact", email: "user@example.com" }); const item = await submissions.get("sub_123"); const exists = await submissions.exists("sub_123"); await submissions.delete("sub_123");}完全な API リファレンス
Section titled “完全な API リファレンス”interface StorageCollection<T = unknown> { // Basic CRUD get(id: string): Promise<T | null>; put(id: string, data: T): Promise<void>; delete(id: string): Promise<boolean>; exists(id: string): Promise<boolean>;
// バッチ操作 getMany(ids: string[]): Promise<Map<string, T>>; putMany(items: Array<{ id: string; data: T }>): Promise<void>; deleteMany(ids: string[]): Promise<number>;
// クエリ (インデックス付きフィールドのみ) query(options?: QueryOptions): Promise<PaginatedResult<{ id: string; data: T }>>; count(where?: WhereClause): Promise<number>;}データのクエリ
Section titled “データのクエリ”query() を使用して条件に一致するドキュメントを取得します。クエリはページネーションされた結果を返します。
const result = await ctx.storage.submissions.query({ where: { formId: "contact", status: "pending", }, orderBy: { createdAt: "desc" }, limit: 20,});
// result.items - { id, data } の配列// result.cursor - ページネーションカーソル (さらに結果がある場合)// result.hasMore - さらにページがあるかどうかを示すブール値クエリオプション
Section titled “クエリオプション”interface QueryOptions { where?: WhereClause; orderBy?: Record<string, "asc" | "desc">; limit?: number; // Default 50, max 1000 cursor?: string; // For pagination}Where 句演算子
Section titled “Where 句演算子”これらの演算子を使用して、インデックス付きフィールドでフィルタリングします:
where: { status: "pending", // Exact string match count: 5, // Exact number match archived: false // Exact boolean match}where: { createdAt: { gte: "2024-01-01" }, // 以上 score: { gt: 50, lte: 100 } // 間 (排他的/包括的)}
// 利用可能: gt, gte, lt, ltewhere: { status: { in: ["pending", "approved"] }}where: { slug: { startsWith: "blog-" }}インデックス付きフィールドで結果を並べ替えます:
orderBy: { createdAt: "desc";} // Newest firstorderBy: { score: "asc";} // Lowest firstページネーション
Section titled “ページネーション”結果はページネーションされます。追加のページを取得するには cursor を使用します:
async function getAllSubmissions(ctx: PluginContext) { const allItems = []; let cursor: string | undefined;
do { const result = await ctx.storage.submissions!.query({ orderBy: { createdAt: "desc" }, limit: 100, cursor, });
allItems.push(...result.items); cursor = result.cursor; } while (cursor);
return allItems;}PaginatedResult
Section titled “PaginatedResult”interface PaginatedResult<T> { items: T[]; cursor?: string; // Pass to next query for more results hasMore: boolean; // True if more pages exist}ドキュメントのカウント
Section titled “ドキュメントのカウント”条件に一致するドキュメントをカウントします:
// Count allconst total = await ctx.storage.submissions!.count();
// フィルター付きカウントconst pending = await ctx.storage.submissions!.count({ status: "pending",});一括操作には、バッチメソッドを使用します:
// Get multiple by IDconst items = await ctx.storage.submissions!.getMany(["sub_1", "sub_2", "sub_3"]);// Returns Map<string, T>
// 複数保存await ctx.storage.submissions!.putMany([ { id: "sub_1", data: { formId: "contact", status: "new" } }, { id: "sub_2", data: { formId: "contact", status: "new" } },]);
// 複数削除const deletedCount = await ctx.storage.submissions!.deleteMany(["sub_1", "sub_2"]);インデックス設計
Section titled “インデックス設計”クエリパターンに基づいてインデックスを選択します:
| クエリパターン | 必要なインデックス |
|---|---|
formId でフィルター | "formId" |
formId でフィルター、createdAt で並べ替え | ["formId", "createdAt"] |
createdAt のみで並べ替え | "createdAt" |
status と formId でフィルター | "status" と "formId" (別々) |
複合インデックスは、最初のフィールドでフィルタリングし、オプションで2番目のフィールドで並べ替えるクエリをサポートします:
// With index ["formId", "createdAt"]:
// これは動作します:query({ where: { formId: "contact" }, orderBy: { createdAt: "desc" } });
// これも動作します (フィルターのみ):query({ where: { formId: "contact" } });
// これは複合インデックスを使用しません (フィールド順序が異なります):query({ where: { createdAt: { gte: "2024-01-01" } } });ストレージコレクションに型を付けて、より良い IntelliSense を実現します:
interface Submission { formId: string; email: string; data: Record<string, unknown>; status: "pending" | "approved" | "spam"; createdAt: string;}
definePlugin({ id: "forms", version: "1.0.0",
storage: { submissions: { indexes: ["formId", "status", "createdAt"], }, },
hooks: { "content:afterSave": async (event, ctx) => { // 型付きコレクションにキャスト const submissions = ctx.storage.submissions as StorageCollection<Submission>;
const submission: Submission = { formId: "contact", email: "user@example.com", data: { message: "Hello" }, status: "pending", createdAt: new Date().toISOString(), };
await submissions.put(`sub_${Date.now()}`, submission); }, },});ストレージ vs コンテンツ vs KV
Section titled “ストレージ vs コンテンツ vs KV”ユースケースに適したストレージメカニズムを使用します:
| ユースケース | ストレージ |
|---|---|
| プラグイン運用データ (ログ、送信、キャッシュ) | ctx.storage |
| ユーザー設定可能な設定 | ctx.kv と settings: プレフィックス |
| 内部プラグイン状態 | ctx.kv と state: プレフィックス |
| 管理 UI で編集可能なコンテンツ | サイトコレクション (プラグインストレージではありません) |
内部では、プラグインストレージは単一のデータベーステーブルを使用します:
CREATE TABLE _plugin_storage ( plugin_id TEXT NOT NULL, collection TEXT NOT NULL, id TEXT NOT NULL, data JSON NOT NULL, created_at TEXT, updated_at TEXT, PRIMARY KEY (plugin_id, collection, id));EmDash は宣言されたフィールドに対して式インデックスを作成します:
CREATE INDEX idx_forms_submissions_formId ON _plugin_storage(json_extract(data, '$.formId')) WHERE plugin_id = 'forms' AND collection = 'submissions';この設計により、以下が提供されます:
- マイグレーション不要 — スキーマはプラグインコード内に存在
- 移植性 — D1、libSQL、SQLite で動作
- 分離性 — プラグインは自身のデータのみにアクセス可能
- 安全性 — SQL インジェクションなし、検証済みクエリ
インデックスの追加
Section titled “インデックスの追加”プラグイン更新でインデックスを追加すると、EmDash は次回起動時に自動的に作成します。これは安全です — データマイグレーションなしでインデックスを追加できます。
インデックスを削除すると、EmDash はそれらを削除します。インデックスが付いていないフィールドでのクエリは、検証エラーで失敗します。