Plugin-Speicher
Plugins können ihre eigenen Daten in Dokumentensammlungen speichern, ohne Datenbank-Migrationen schreiben zu müssen. Deklarieren Sie Sammlungen und Indizes in Ihrer Plugin-Definition, und EmDash kümmert sich automatisch um das Schema.
Speicherdeklaration
Abschnitt betitelt „Speicherdeklaration“Definieren Sie Speichersammlungen in definePlugin():
import { definePlugin } from "emdash";
export default definePlugin({ id: "forms", version: "1.0.0",
storage: { submissions: { indexes: [ "formId", // Index für ein einzelnes Feld "status", "createdAt", ["formId", "createdAt"], // Zusammengesetzter Index ["status", "createdAt"], ], }, forms: { indexes: ["slug"], }, },
// ...});Jeder Schlüssel in storage ist ein Sammlungsname. Das indexes-Array listet Felder auf, die effizient abgefragt werden können.
Speichersammlungs-API
Abschnitt betitelt „Speichersammlungs-API“Greifen Sie über ctx.storage in Hooks und Routen auf Sammlungen zu:
"content:afterSave": async (event, ctx) => { const { submissions } = ctx.storage;
// CRUD-Operationen 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");}Vollständige API-Referenz
Abschnitt betitelt „Vollständige API-Referenz“interface StorageCollection<T = unknown> { // Grundlegendes CRUD get(id: string): Promise<T | null>; put(id: string, data: T): Promise<void>; delete(id: string): Promise<boolean>; exists(id: string): Promise<boolean>;
// Batch-Operationen getMany(ids: string[]): Promise<Map<string, T>>; putMany(items: Array<{ id: string; data: T }>): Promise<void>; deleteMany(ids: string[]): Promise<number>;
// Abfrage (nur indizierte Felder) query(options?: QueryOptions): Promise<PaginatedResult<{ id: string; data: T }>>; count(where?: WhereClause): Promise<number>;}Datenabfrage
Abschnitt betitelt „Datenabfrage“Verwenden Sie query(), um Dokumente abzurufen, die den Kriterien entsprechen. Abfragen liefern paginierte Ergebnisse.
const result = await ctx.storage.submissions.query({ where: { formId: "contact", status: "pending", }, orderBy: { createdAt: "desc" }, limit: 20,});
// result.items - Array von { id, data }// result.cursor - Paginierungs-Cursor (falls mehr Ergebnisse)// result.hasMore - Boolean, der weitere Seiten anzeigtAbfrageoptionen
Abschnitt betitelt „Abfrageoptionen“interface QueryOptions { where?: WhereClause; orderBy?: Record<string, "asc" | "desc">; limit?: number; // Standard: 50, Maximum: 1000 cursor?: string; // Für die Paginierung}Where-Clause-Operatoren
Abschnitt betitelt „Where-Clause-Operatoren“Filtern Sie nach indizierten Feldern mit diesen Operatoren:
where: { status: "pending", // Exakte Zeichenkettenübereinstimmung count: 5, // Exakte Zahlenübereinstimmung archived: false // Exakte boolesche Übereinstimmung}where: { createdAt: { gte: "2024-01-01" }, // Größer als oder gleich score: { gt: 50, lte: 100 } // Zwischen (exklusiv/inklusiv)}
// Verfügbar: gt, gte, lt, ltewhere: { status: { in: ["pending", "approved"] }}where: { slug: { startsWith: "blog-" }}Sortierung
Abschnitt betitelt „Sortierung“Sortieren Sie Ergebnisse nach indizierten Feldern:
orderBy: { createdAt: "desc" } // Neueste zuerstorderBy: { score: "asc" } // Niedrigste Werte zuerstPaginierung
Abschnitt betitelt „Paginierung“Ergebnisse sind paginiert. Verwenden Sie cursor, um zusätzliche Seiten abzurufen:
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
Abschnitt betitelt „PaginatedResult“interface PaginatedResult<T> { items: T[]; cursor?: string; // An die nächste Abfrage übergeben, um weitere Ergebnisse zu erhalten hasMore: boolean; // Gibt an, ob weitere Seiten vorhanden sind}Dokumente zählen
Abschnitt betitelt „Dokumente zählen“Zählen Sie Dokumente, die den Kriterien entsprechen:
// Alle zählenconst total = await ctx.storage.submissions!.count();
// Zählen mit Filterconst pending = await ctx.storage.submissions!.count({ status: "pending",});Batch-Operationen
Abschnitt betitelt „Batch-Operationen“Verwenden Sie für Massenoperationen Batch-Methoden:
// Mehrere per ID ladenconst items = await ctx.storage.submissions!.getMany(["sub_1", "sub_2", "sub_3"]);// Gibt ein Map<string, T> zurück
// Mehrere einfügenawait ctx.storage.submissions!.putMany([ { id: "sub_1", data: { formId: "contact", status: "new" } }, { id: "sub_2", data: { formId: "contact", status: "new" } },]);
// Mehrere löschenconst deletedCount = await ctx.storage.submissions!.deleteMany(["sub_1", "sub_2"]);Index-Design
Abschnitt betitelt „Index-Design“Wählen Sie Indizes basierend auf Ihren Abfragemustern:
| Abfragemuster | Benötigter Index |
|---|---|
Filtern nach formId | "formId" |
Filtern nach formId, sortieren nach createdAt | ["formId", "createdAt"] |
Sortieren nur nach createdAt | "createdAt" |
Filtern nach status und formId | "status" und "formId" (separat) |
Zusammengesetzte Indizes unterstützen Abfragen, die nach dem ersten Feld filtern und optional nach dem zweiten sortieren:
// With index ["formId", "createdAt"]:
// Das funktioniert:query({ where: { formId: "contact" }, orderBy: { createdAt: "desc" } });
// Das funktioniert auch (nur Filter):query({ where: { formId: "contact" } });
// Das nutzt den zusammengesetzten Index NICHT (falsche Feldreihenfolge):query({ where: { createdAt: { gte: "2024-01-01" } } });Typsicherheit
Abschnitt betitelt „Typsicherheit“Typisieren Sie Ihre Speichersammlungen für bessere 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) => { // In typisierte Sammlung umwandeln 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); }, },});Speicher vs. Inhalte vs. KV
Abschnitt betitelt „Speicher vs. Inhalte vs. KV“Verwenden Sie den richtigen Speichermechanismus für Ihren Anwendungsfall:
| Anwendungsfall | Speicher |
|---|---|
| Plugin-Betriebsdaten (Logs, Einreichungen, Cache) | ctx.storage |
| Benutzerkonfigurierbare Einstellungen | ctx.kv mit settings:-Präfix |
| Interner Plugin-Zustand | ctx.kv mit state:-Präfix |
| Im Admin-UI bearbeitbarer Inhalt | Site-Sammlungen (nicht Plugin-Speicher) |
Implementierungsdetails
Abschnitt betitelt „Implementierungsdetails“Intern verwendet der Plugin-Speicher eine einzelne Datenbanktabelle:
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 erstellt Ausdrucksindizes für deklarierte Felder:
CREATE INDEX idx_forms_submissions_formId ON _plugin_storage(json_extract(data, '$.formId')) WHERE plugin_id = 'forms' AND collection = 'submissions';Dieses Design bietet:
- Keine Migrationen — Schema lebt im Plugin-Code
- Portabilität — Funktioniert mit D1, libSQL, SQLite
- Isolation — Plugins können nur auf ihre eigenen Daten zugreifen
- Sicherheit — Keine SQL-Injection, validierte Abfragen
Indizes hinzufügen
Abschnitt betitelt „Indizes hinzufügen“Wenn Sie Indizes in einem Plugin-Update hinzufügen, erstellt EmDash sie beim nächsten Start automatisch. Dies ist sicher – Indizes können ohne Datenmigration hinzugefügt werden.
Wenn Sie Indizes entfernen, löscht EmDash sie. Abfragen auf nicht-indizierten Feldern schlagen mit einem Validierungsfehler fehl.