Armazenamento de Plugin
Os plugins podem armazenar seus próprios dados em coleções de documentos sem escrever migrações de banco de dados. Declare coleções e índices na definição do seu plugin, e o EmDash gerencia o esquema automaticamente.
Declarando Armazenamento
Seção intitulada “Declarando Armazenamento”Defina coleções de armazenamento em 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"], }, },
// ...});Cada chave em storage é um nome de coleção. O array indexes lista campos que podem ser consultados de forma eficiente.
API da Coleção de Armazenamento
Seção intitulada “API da Coleção de Armazenamento”Acesse coleções via ctx.storage em hooks e rotas:
"content:afterSave": async (event, ctx) => { const { submissions } = ctx.storage;
// Operações 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");}Referência Completa da API
Seção intitulada “Referência Completa da 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>;
// Operações em lote getMany(ids: string[]): Promise<Map<string, T>>; putMany(items: Array<{ id: string; data: T }>): Promise<void>; deleteMany(ids: string[]): Promise<number>;
// Consulta (apenas campos indexados) query(options?: QueryOptions): Promise<PaginatedResult<{ id: string; data: T }>>; count(where?: WhereClause): Promise<number>;}Consultando Dados
Seção intitulada “Consultando Dados”Use query() para recuperar documentos que correspondam aos critérios. As consultas retornam resultados paginados.
const result = await ctx.storage.submissions.query({ where: { formId: "contact", status: "pending", }, orderBy: { createdAt: "desc" }, limit: 20,});
// result.items - Array de { id, data }// result.cursor - Cursor de paginação (se houver mais resultados)// result.hasMore - Booleano indicando mais páginasOpções de Consulta
Seção intitulada “Opções de Consulta”interface QueryOptions { where?: WhereClause; orderBy?: Record<string, "asc" | "desc">; limit?: number; // Default 50, max 1000 cursor?: string; // For pagination}Operadores da Cláusula Where
Seção intitulada “Operadores da Cláusula Where”Filtre por campos indexados usando estes operadores:
where: { status: "pending", // Exact string match count: 5, // Exact number match archived: false // Exact boolean match}where: { createdAt: { gte: "2024-01-01" }, // Maior ou igual score: { gt: 50, lte: 100 } // Entre (exclusivo/inclusivo)}
// Disponíveis: gt, gte, lt, ltewhere: { status: { in: ["pending", "approved"] }}where: { slug: { startsWith: "blog-" }}Ordenação
Seção intitulada “Ordenação”Ordene resultados por campos indexados:
orderBy: { createdAt: "desc";} // Newest firstorderBy: { score: "asc";} // Lowest firstPaginação
Seção intitulada “Paginação”Os resultados são paginados. Use cursor para buscar páginas adicionais:
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
Seção intitulada “PaginatedResult”interface PaginatedResult<T> { items: T[]; cursor?: string; // Pass to next query for more results hasMore: boolean; // True if more pages exist}Contando Documentos
Seção intitulada “Contando Documentos”Conte documentos que correspondam aos critérios:
// Count allconst total = await ctx.storage.submissions!.count();
// Contar com filtroconst pending = await ctx.storage.submissions!.count({ status: "pending",});Operações em Lote
Seção intitulada “Operações em Lote”Para operações em massa, use métodos em lote:
// Get multiple by IDconst items = await ctx.storage.submissions!.getMany(["sub_1", "sub_2", "sub_3"]);// Returns Map<string, T>
// Inserir múltiplosawait ctx.storage.submissions!.putMany([ { id: "sub_1", data: { formId: "contact", status: "new" } }, { id: "sub_2", data: { formId: "contact", status: "new" } },]);
// Excluir múltiplosconst deletedCount = await ctx.storage.submissions!.deleteMany(["sub_1", "sub_2"]);Design de Índices
Seção intitulada “Design de Índices”Escolha índices com base nos seus padrões de consulta:
| Padrão de Consulta | Índice Necessário |
|---|---|
Filtrar por formId | "formId" |
Filtrar por formId, ordenar por createdAt | ["formId", "createdAt"] |
Ordenar apenas por createdAt | "createdAt" |
Filtrar por status e formId | "status" e "formId" (separados) |
Índices compostos suportam consultas que filtram no primeiro campo e opcionalmente ordenam pelo segundo:
// With index ["formId", "createdAt"]:
// Isso funciona:query({ where: { formId: "contact" }, orderBy: { createdAt: "desc" } });
// Isso também funciona (apenas filtro):query({ where: { formId: "contact" } });
// Isso NÃO usa o índice composto (ordem de campos errada):query({ where: { createdAt: { gte: "2024-01-01" } } });Segurança de Tipos
Seção intitulada “Segurança de Tipos”Tipifique suas coleções de armazenamento para melhor 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) => { // Converte para coleção tipada 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); }, },});Armazenamento vs Conteúdo vs KV
Seção intitulada “Armazenamento vs Conteúdo vs KV”Use o mecanismo de armazenamento correto para seu caso de uso:
| Caso de Uso | Armazenamento |
|---|---|
| Dados operacionais do plugin (logs, submissões, cache) | ctx.storage |
| Configurações editáveis pelo usuário | ctx.kv com prefixo settings: |
| Estado interno do plugin | ctx.kv com prefixo state: |
| Conteúdo editável na UI de administração | Coleções do site (não armazenamento do plugin) |
Detalhes de Implementação
Seção intitulada “Detalhes de Implementação”Nos bastidores, o armazenamento do plugin usa uma única tabela de banco de dados:
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));O EmDash cria índices de expressão para os campos declarados:
CREATE INDEX idx_forms_submissions_formId ON _plugin_storage(json_extract(data, '$.formId')) WHERE plugin_id = 'forms' AND collection = 'submissions';Este design fornece:
- Sem migrações — O esquema vive no código do plugin
- Portabilidade — Funciona em D1, libSQL, SQLite
- Isolamento — Plugins só podem acessar seus próprios dados
- Segurança — Sem injeção de SQL, consultas validadas
Adicionando Índices
Seção intitulada “Adicionando Índices”Quando você adiciona índices em uma atualização do plugin, o EmDash os cria automaticamente na próxima inicialização. Isso é seguro — índices podem ser adicionados sem migração de dados.
Quando você remove índices, o EmDash os descarta. Consultas em campos não indexados falharão com um erro de validação.