Aller au contenu

Stockage des Plugins

Les plugins peuvent stocker leurs propres données dans des collections de documents sans écrire de migrations de base de données. Déclarez les collections et les index dans votre définition de plugin, et EmDash gère le schéma automatiquement.

Définissez les collections de stockage dans 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"],
},
},
// ...
});

Chaque clé dans storage est un nom de collection. Le tableau indexes liste les champs qui peuvent être interrogés efficacement.

Accédez aux collections via ctx.storage dans les hooks et les routes :

"content:afterSave": async (event, ctx) => {
const { submissions } = ctx.storage;
// Opérations 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");
}
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>;
// Opérations par lot
getMany(ids: string[]): Promise<Map<string, T>>;
putMany(items: Array<{ id: string; data: T }>): Promise<void>;
deleteMany(ids: string[]): Promise<number>;
// Requête (champs indexés uniquement)
query(options?: QueryOptions): Promise<PaginatedResult<{ id: string; data: T }>>;
count(where?: WhereClause): Promise<number>;
}

Utilisez query() pour récupérer les documents correspondant aux critères. Les requêtes renvoient des résultats paginés.

const result = await ctx.storage.submissions.query({
where: {
formId: "contact",
status: "pending",
},
orderBy: { createdAt: "desc" },
limit: 20,
});
// result.items - Tableau de { id, data }
// result.cursor - Curseur de pagination (si plus de résultats)
// result.hasMore - Booléen indiquant s'il y a plus de pages
interface QueryOptions {
where?: WhereClause;
orderBy?: Record<string, "asc" | "desc">;
limit?: number; // Default 50, max 1000
cursor?: string; // For pagination
}

Filtrez par champs indexés en utilisant ces opérateurs :

where: {
status: "pending", // Exact string match
count: 5, // Exact number match
archived: false // Exact boolean match
}

Triez les résultats par champs indexés :

orderBy: {
createdAt: "desc";
} // Newest first
orderBy: {
score: "asc";
} // Lowest first

Les résultats sont paginés. Utilisez cursor pour récupérer des pages supplémentaires :

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; // Pass to next query for more results
hasMore: boolean; // True if more pages exist
}

Comptez les documents correspondant aux critères :

// Count all
const total = await ctx.storage.submissions!.count();
// Compter avec filtre
const pending = await ctx.storage.submissions!.count({
status: "pending",
});

Pour les opérations en masse, utilisez les méthodes par lot :

// Get multiple by ID
const items = await ctx.storage.submissions!.getMany(["sub_1", "sub_2", "sub_3"]);
// Returns Map<string, T>
// Mettre plusieurs
await ctx.storage.submissions!.putMany([
{ id: "sub_1", data: { formId: "contact", status: "new" } },
{ id: "sub_2", data: { formId: "contact", status: "new" } },
]);
// Supprimer plusieurs
const deletedCount = await ctx.storage.submissions!.deleteMany(["sub_1", "sub_2"]);

Choisissez les index en fonction de vos modèles de requête :

Modèle de requêteIndex nécessaire
Filtrer par formId"formId"
Filtrer par formId, trier par createdAt["formId", "createdAt"]
Trier par createdAt uniquement"createdAt"
Filtrer par status et formId"status" et "formId" (séparés)

Les index composites prennent en charge les requêtes qui filtrent sur le premier champ et trient optionnellement par le second :

// With index ["formId", "createdAt"]:
// Cela fonctionne :
query({ where: { formId: "contact" }, orderBy: { createdAt: "desc" } });
// Cela fonctionne aussi (filtre uniquement) :
query({ where: { formId: "contact" } });
// Cela n'utilise PAS l'index composite (ordre des champs incorrect) :
query({ where: { createdAt: { gte: "2024-01-01" } } });

Typagez vos collections de stockage pour une meilleure 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) => {
// Conversion en collection typée
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);
},
},
});

Utilisez le bon mécanisme de stockage pour votre cas d’utilisation :

Cas d’utilisationStockage
Données opérationnelles du plugin (logs, soumissions, cache)ctx.storage
Paramètres configurables par l’utilisateurctx.kv avec préfixe settings:
État interne du pluginctx.kv avec préfixe state:
Contenu modifiable dans l’interface d’administrationCollections de site (pas le stockage de plugin)

En interne, le stockage de plugin utilise une seule table de base de données :

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 crée des index d’expression pour les champs déclarés :

CREATE INDEX idx_forms_submissions_formId
ON _plugin_storage(json_extract(data, '$.formId'))
WHERE plugin_id = 'forms' AND collection = 'submissions';

Cette conception offre :

  • Aucune migration — Le schéma réside dans le code du plugin
  • Portabilité — Fonctionne sur D1, libSQL, SQLite
  • Isolation — Les plugins ne peuvent accéder qu’à leurs propres données
  • Sécurité — Pas d’injection SQL, requêtes validées

Lorsque vous ajoutez des index dans une mise à jour de plugin, EmDash les crée automatiquement au prochain démarrage. C’est sûr — les index peuvent être ajoutés sans migration de données.

Lorsque vous supprimez des index, EmDash les supprime. Les requêtes sur des champs non indexés échoueront avec une erreur de validation.