Files
clube67.com/backend/src/services/plugin.service.ts
T

174 lines
5.6 KiB
TypeScript

import fs from 'fs';
import path from 'path';
import { Express, Router } from 'express';
import db from '../config/database';
import logger from '../utils/logger';
export interface PluginManifest {
name: string;
slug: string;
version: string;
description: string;
author: string;
entryPoint: string;
dependencies?: string[];
hooks?: string[];
}
export interface PluginInstance {
manifest: PluginManifest;
router?: Router;
hooks: Record<string, Function[]>;
activate: () => Promise<void>;
deactivate: () => Promise<void>;
}
const loadedPlugins: Map<string, PluginInstance> = new Map();
const hookRegistry: Record<string, Function[]> = {};
const PLUGINS_DIR = path.resolve(__dirname, '../../../plugins');
export const pluginService = {
async loadAll(app: Express) {
if (!fs.existsSync(PLUGINS_DIR)) {
fs.mkdirSync(PLUGINS_DIR, { recursive: true });
return;
}
const activePlugins = await db('plugins').where('active', true);
for (const pluginRecord of activePlugins) {
try {
await this.load(app, pluginRecord.slug);
} catch (err) {
logger.error(`Failed to load plugin: ${pluginRecord.slug}`, err);
}
}
logger.info(`Loaded ${loadedPlugins.size} plugins`);
},
async load(app: Express, slug: string) {
const pluginDir = path.join(PLUGINS_DIR, slug);
const manifestPath = path.join(pluginDir, 'manifest.json');
if (!fs.existsSync(manifestPath)) {
throw new Error(`Plugin manifest not found: ${manifestPath}`);
}
const manifest: PluginManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
const entryPath = path.join(pluginDir, manifest.entryPoint || 'index.js');
if (!fs.existsSync(entryPath)) {
throw new Error(`Plugin entry point not found: ${entryPath}`);
}
const pluginModule = require(entryPath);
const instance: PluginInstance = {
manifest,
router: pluginModule.router,
hooks: pluginModule.hooks || {},
activate: pluginModule.activate || (async () => { }),
deactivate: pluginModule.deactivate || (async () => { }),
};
// Register routes
if (instance.router) {
app.use(`/api/plugins/${slug}`, instance.router);
}
// Register hooks
for (const [hookName, handlers] of Object.entries(instance.hooks)) {
if (!hookRegistry[hookName]) hookRegistry[hookName] = [];
if (Array.isArray(handlers)) {
hookRegistry[hookName].push(...handlers);
}
}
await instance.activate();
loadedPlugins.set(slug, instance);
logger.info(`Plugin loaded: ${manifest.name} v${manifest.version}`);
},
async unload(slug: string) {
const instance = loadedPlugins.get(slug);
if (instance) {
await instance.deactivate();
loadedPlugins.delete(slug);
logger.info(`Plugin unloaded: ${slug}`);
}
},
async install(slug: string) {
const pluginDir = path.join(PLUGINS_DIR, slug);
const manifestPath = path.join(pluginDir, 'manifest.json');
if (!fs.existsSync(manifestPath)) {
throw new Error('Manifest not found');
}
const manifest: PluginManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
const existing = await db('plugins').where('slug', slug).first();
if (existing) {
await db('plugins').where('slug', slug).update({
version: manifest.version,
description: manifest.description,
author: manifest.author,
entry_point: manifest.entryPoint,
dependencies: JSON.stringify(manifest.dependencies || []),
hooks: JSON.stringify(manifest.hooks || []),
});
} else {
await db('plugins').insert({
name: manifest.name,
slug: manifest.slug,
version: manifest.version,
description: manifest.description,
author: manifest.author,
entry_point: manifest.entryPoint,
active: false,
dependencies: JSON.stringify(manifest.dependencies || []),
hooks: JSON.stringify(manifest.hooks || []),
directory: pluginDir,
});
}
return manifest;
},
async activate(app: Express, slug: string) {
await db('plugins').where('slug', slug).update({ active: true });
await this.load(app, slug);
},
async deactivate(slug: string) {
await db('plugins').where('slug', slug).update({ active: false });
await this.unload(slug);
},
async getAll() {
return db('plugins').orderBy('name');
},
async remove(slug: string) {
await this.unload(slug);
await db('plugins').where('slug', slug).delete();
// Note: does not delete plugin files for safety
},
async triggerHook(hookName: string, data: any) {
const handlers = hookRegistry[hookName] || [];
for (const handler of handlers) {
try {
await handler(data);
} catch (err) {
logger.error(`Hook error [${hookName}]:`, err);
}
}
},
getLoadedPlugins() {
return Array.from(loadedPlugins.entries()).map(([slug, instance]) => ({
slug,
name: instance.manifest.name,
version: instance.manifest.version,
}));
},
};