/** * Plugin Karbé — accès serveur à l'état des plugins. * * - getPluginState(key): lit la DB (avec cache 5s pour éviter les hammer). * - isPluginEnabled(key): helper booléen. * - syncPluginsFromRegistry(): à appeler au démarrage pour insérer les plugins * nouvellement ajoutés au code (sans toucher aux flags existants). * - togglePlugin(key, enabled): admin action avec hook onEnable/onDisable. */ import "server-only"; import { Prisma } from "@/generated/prisma/client"; import { prisma } from "@/lib/prisma"; import { PLUGINS, type PluginDescriptor } from "./registry"; import { pluginHooks } from "./hooks"; type PluginRow = { key: string; enabled: boolean; config: Record; version: string; category: string; name: string; description: string; }; let cache: Map | null = null; let cacheStamp = 0; const CACHE_TTL_MS = 5000; async function loadAll(): Promise> { const now = Date.now(); if (cache && now - cacheStamp < CACHE_TTL_MS) return cache; const rows = await prisma.plugin.findMany(); const map = new Map(); for (const r of rows) { map.set(r.key, { key: r.key, enabled: r.enabled, config: (r.config ?? {}) as Record, version: r.version, category: r.category, name: r.name, description: r.description, }); } cache = map; cacheStamp = now; return map; } export function invalidatePluginCache(): void { cache = null; cacheStamp = 0; } export async function getPluginState(key: string): Promise { const m = await loadAll(); return m.get(key) ?? null; } export async function isPluginEnabled(key: string): Promise { const s = await getPluginState(key); return !!s?.enabled; } export async function getEnabledPluginKeys(): Promise { const m = await loadAll(); return [...m.values()].filter((r) => r.enabled).map((r) => r.key); } export async function listAllPlugins(): Promise { const m = await loadAll(); return [...m.values()].sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name)); } export async function syncPluginsFromRegistry(): Promise { const existing = new Set((await prisma.plugin.findMany({ select: { key: true } })).map((p) => p.key)); const toInsert: PluginDescriptor[] = PLUGINS.filter((p) => !existing.has(p.key)); if (toInsert.length) { await prisma.plugin.createMany({ data: toInsert.map((p) => ({ key: p.key, name: p.name, description: p.description, category: p.category, version: p.version, enabled: false, config: {}, })), skipDuplicates: true, }); invalidatePluginCache(); } // Update name/description/version if the descriptor changed (idempotent). for (const p of PLUGINS) { if (existing.has(p.key)) { await prisma.plugin.update({ where: { key: p.key }, data: { name: p.name, description: p.description, category: p.category, version: p.version }, }); } } invalidatePluginCache(); } export async function togglePlugin(key: string, enabled: boolean): Promise { const before = await prisma.plugin.findUnique({ where: { key } }); if (!before) return null; if (before.enabled === enabled) return await getPluginState(key); const hook = pluginHooks[key]; if (enabled && hook?.onEnable) await hook.onEnable(); if (!enabled && hook?.onDisable) await hook.onDisable(); await prisma.plugin.update({ where: { key }, data: { enabled, lastEnabledAt: enabled ? new Date() : before.lastEnabledAt, lastDisabledAt: !enabled ? new Date() : before.lastDisabledAt, }, }); invalidatePluginCache(); return await getPluginState(key); } export async function updatePluginConfig(key: string, config: Record): Promise { await prisma.plugin.update({ where: { key }, data: { config: config as Prisma.InputJsonValue }, }); invalidatePluginCache(); return await getPluginState(key); }