Configuración de plugins
Los plugins suelen necesitar configuración: claves API, banderas de funciones o preferencias de visualización. EmDash ofrece dos mecanismos para cubrirlo: un esquema de ajustes para opciones editables por administradores y un almacén KV para acceso programático.
Esquema de ajustes
Sección titulada «Esquema de ajustes»Declara un esquema de ajustes en admin.settingsSchema para generar automáticamente una interfaz de administración:
import { definePlugin } from "emdash";
export default definePlugin({ id: "seo", version: "1.0.0",
admin: { settingsSchema: { siteTitle: { type: "string", label: "Titulo del sitio", description: "Se usa en las etiquetas de titulo y los metadatos", default: "", }, maxTitleLength: { type: "number", label: "Longitud maxima del titulo", description: "Numero de caracteres antes de truncar", default: 60, min: 30, max: 100, }, generateSitemap: { type: "boolean", label: "Generar sitemap", description: "Genera sitemap.xml automaticamente", default: true, }, defaultRobots: { type: "select", label: "Robots por defecto", options: [ { value: "index,follow", label: "Indexar y seguir" }, { value: "noindex,follow", label: "No indexar, seguir" }, { value: "noindex,nofollow", label: "No indexar, no seguir" }, ], default: "index,follow", }, apiKey: { type: "secret", label: "Clave API", description: "Se almacena cifrada", }, }, },});EmDash genera un formulario de ajustes dentro de la sección de administración del plugin. Los usuarios pueden cambiar la configuración sin tocar el código.
Tipos de campos
Sección titulada «Tipos de campos»Campo de texto para cadenas de una sola línea o de varias líneas.
siteTitle: { type: "string", label: "Titulo del sitio", description: "Texto de ayuda opcional", default: "Mi sitio", multiline: false // Usa true para un textarea}Campo numérico con restricciones opcionales de mínimo y máximo.
maxItems: { type: "number", label: "Numero maximo de elementos", default: 100, min: 1, max: 1000}Boolean
Sección titulada «Boolean»Interruptor de alternancia para valores verdadero/falso.
enabled: { type: "boolean", label: "Activado", description: "Activa o desactiva esta funcion", default: true}Menú desplegable para opciones predefinidas.
theme: { type: "select", label: "Tema", options: [ { value: "light", label: "Claro" }, { value: "dark", label: "Oscuro" }, { value: "auto", label: "Sistema" } ], default: "auto"}Campo encriptado para valores sensibles como claves API. Nunca se envía al cliente después de guardar.
apiKey: { type: "secret", label: "Clave API", description: "Se almacena cifrada"}Acceder a la configuración
Sección titulada «Acceder a la configuración»Lee la configuración en hooks y rutas a través de ctx.kv:
"content:beforeSave": async (event, ctx) => { // Leer un ajuste const maxLength = await ctx.kv.get<number>("settings:maxTitleLength"); const apiKey = await ctx.kv.get<string>("settings:apiKey");
// Usa valores por defecto si no están configurados const limit = maxLength ?? 60;
ctx.log.info("Usando longitud máxima", { limit }); return event.content;}Por convención, la configuración se guarda con el prefijo settings:. Así se distinguen los valores configurables por el usuario del estado interno del plugin.
API del almacén KV
Sección titulada «API del almacén KV»El almacén KV (ctx.kv) es un sistema genérico de clave-valor para los datos del plugin:
interface KVAccess { get<T>(key: string): Promise<T | null>; set(key: string, value: unknown): Promise<void>; delete(key: string): Promise<boolean>; list(prefix?: string): Promise<Array<{ key: string; value: unknown }>>;}Leer valores
Sección titulada «Leer valores»// Obtener un valorconst enabled = await ctx.kv.get<boolean>("settings:enabled");
// Obtener con tipoconst config = await ctx.kv.get<{ url: string; timeout: number }>("state:config");Escribir valores
Sección titulada «Escribir valores»// Guardar un valorawait ctx.kv.set("settings:lastSync", new Date().toISOString());
// Establecer valores complejosawait ctx.kv.set("state:cache", { data: items, expiry: Date.now() + 3600000,});Listar valores
Sección titulada «Listar valores»// Listar todos los ajustesconst settings = await ctx.kv.list("settings:");// Devuelve: [{ key: "settings:enabled", value: true }, ...]
// Listar todas las claves del pluginconst all = await ctx.kv.list();Eliminar valores
Sección titulada «Eliminar valores»const deleted = await ctx.kv.delete("state:tempData");// Devuelve true si la clave existíaConvenciones de nombres para claves
Sección titulada «Convenciones de nombres para claves»Usa prefijos para organizar los datos KV:
| Prefijo | Propósito | Ejemplo |
|---|---|---|
settings: | Preferencias configurables por el usuario | settings:apiKey |
state: | Estado interno del plugin | state:lastSync |
cache: | Datos en caché | cache:results |
// Bien: prefijos clarosawait ctx.kv.set("settings:webhookUrl", url);await ctx.kv.set("state:lastRun", timestamp);await ctx.kv.set("cache:feed", feedData);
// Evitar: sin prefijo, propósito poco claroawait ctx.kv.set("url", url);Configuración vs. almacenamiento vs. KV
Sección titulada «Configuración vs. almacenamiento vs. KV»Elige el mecanismo de almacenamiento correcto:
| Caso de Uso | Mecanismo |
|---|---|
| Preferencias editables por el administrador | admin.settingsSchema + ctx.kv con settings: |
| Estado interno del plugin | ctx.kv con state: |
| Colecciones de documentos | ctx.storage |
Configuración es para valores configurables por el usuario—cosas que un administrador podría cambiar. Obtienen una interfaz generada automáticamente.
KV es para estado interno como marcas de tiempo, cursores de sincronización o cálculos en caché. Sin interfaz, solo código.
Almacenamiento es para colecciones de documentos con consultas indexadas—envíos de formularios, registros de auditoría, etc.
Cargar configuración en rutas
Sección titulada «Cargar configuración en rutas»Las rutas API pueden exponer la configuración a componentes de la interfaz de administración:
routes: { settings: { handler: async (ctx) => { const settings = await ctx.kv.list("settings:"); const result: Record<string, unknown> = {};
for (const entry of settings) { const key = entry.key.replace("settings:", ""); result[key] = entry.value; }
return result; } },
"settings/save": { handler: async (ctx) => { const input = ctx.input as Record<string, unknown>;
for (const [key, value] of Object.entries(input)) { if (value !== undefined) { await ctx.kv.set(`settings:${key}`, value); } }
return { success: true }; } }}Valores por defecto
Sección titulada «Valores por defecto»La configuración definida en settingsSchema no se persiste automáticamente. Actúa como valor predeterminado en la interfaz de administración, así que tu código debe manejar los valores faltantes:
"content:afterSave": async (event, ctx) => { // Proporciona siempre un valor de respaldo const enabled = await ctx.kv.get<boolean>("settings:enabled") ?? true; const maxItems = await ctx.kv.get<number>("settings:maxItems") ?? 100;
if (!enabled) return; // ...}Alternativamente, persiste los valores por defecto en plugin:install:
hooks: { "plugin:install": async (_event, ctx) => { // Persistir los valores por defecto del esquema await ctx.kv.set("settings:enabled", true); await ctx.kv.set("settings:maxItems", 100); }}Implementación del almacenamiento
Sección titulada «Implementación del almacenamiento»Los valores KV se almacenan en la tabla _options con claves con espacio de nombres del plugin:
INSERT INTO _options (name, value) VALUES ('plugin:seo:settings:siteTitle', '"Mi sitio"'), ('plugin:seo:settings:maxTitleLength', '60');El prefijo plugin:seo: se agrega automáticamente. Tu código usa settings:siteTitle y EmDash lo almacena como plugin:seo:settings:siteTitle.
Esto asegura que los plugins no puedan sobrescribir accidentalmente los datos de los demás.