Zum Inhalt springen

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.

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.

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");
}
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>;
}

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 anzeigt
interface QueryOptions {
where?: WhereClause;
orderBy?: Record<string, "asc" | "desc">;
limit?: number; // Standard: 50, Maximum: 1000
cursor?: string; // Für die Paginierung
}

Filtern Sie nach indizierten Feldern mit diesen Operatoren:

where: {
status: "pending", // Exakte Zeichenkettenübereinstimmung
count: 5, // Exakte Zahlenübereinstimmung
archived: false // Exakte boolesche Übereinstimmung
}

Sortieren Sie Ergebnisse nach indizierten Feldern:

orderBy: { createdAt: "desc" } // Neueste zuerst
orderBy: { score: "asc" } // Niedrigste Werte zuerst

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;
}
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
}

Zählen Sie Dokumente, die den Kriterien entsprechen:

// Alle zählen
const total = await ctx.storage.submissions!.count();
// Zählen mit Filter
const pending = await ctx.storage.submissions!.count({
status: "pending",
});

Verwenden Sie für Massenoperationen Batch-Methoden:

// Mehrere per ID laden
const items = await ctx.storage.submissions!.getMany(["sub_1", "sub_2", "sub_3"]);
// Gibt ein Map<string, T> zurück
// Mehrere einfügen
await ctx.storage.submissions!.putMany([
{ id: "sub_1", data: { formId: "contact", status: "new" } },
{ id: "sub_2", data: { formId: "contact", status: "new" } },
]);
// Mehrere löschen
const deletedCount = await ctx.storage.submissions!.deleteMany(["sub_1", "sub_2"]);

Wählen Sie Indizes basierend auf Ihren Abfragemustern:

AbfragemusterBenö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" } } });

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);
},
},
});

Verwenden Sie den richtigen Speichermechanismus für Ihren Anwendungsfall:

AnwendungsfallSpeicher
Plugin-Betriebsdaten (Logs, Einreichungen, Cache)ctx.storage
Benutzerkonfigurierbare Einstellungenctx.kv mit settings:-Präfix
Interner Plugin-Zustandctx.kv mit state:-Präfix
Im Admin-UI bearbeitbarer InhaltSite-Sammlungen (nicht Plugin-Speicher)

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

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.