Files
mercado.clube67.com/backend/plugins/index.js
T

141 lines
4.7 KiB
JavaScript

'use strict'
const fs = require('fs')
const path = require('path')
const { execute, queryOne, query } = require('../database/postgres')
const PluginConfigStore = require('./config')
// ─── Tabelas já criadas no schema.sql ───────────────────────────────────────
// plugins e plugin_configs existem — sem necessidade de CREATE aqui.
// ─── PluginManager ───────────────────────────────────────────────────────────
class PluginManager {
constructor(app, dbAccess, httpServer = null) {
this.app = app
this.db = dbAccess // { query, queryOne, execute } do postgres.js
this.httpServer = httpServer // http.Server nativo — para proxy WS
this.plugins = new Map() // id → { manifest, instance, active }
}
// Carrega todos os plugins encontrados em plugins/*/
async loadAll() {
const dir = __dirname
const entries = fs.readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
if (!entry.isDirectory()) continue
const pluginDir = path.join(dir, entry.name)
const manifestPath = path.join(pluginDir, 'manifest.json')
if (!fs.existsSync(manifestPath)) continue
try {
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
await this._register(manifest, pluginDir)
} catch (err) {
console.error(`[plugins] Erro ao carregar "${entry.name}":`, err.message)
}
}
}
// Registra um plugin: garante linha na tabela e ativa se estava ativo
async _register(manifest, pluginDir) {
const { id, name, version, active_by_default = false } = manifest
// Garante registro na tabela (upsert)
await execute(
`INSERT INTO plugins (id, active) VALUES ($1, $2)
ON CONFLICT (id) DO NOTHING`,
[id, active_by_default ? 1 : 0]
)
const record = await queryOne('SELECT active FROM plugins WHERE id = $1', [id])
const instance = require(path.join(pluginDir, 'index.js'))
this.plugins.set(id, { manifest, instance, active: false })
if (record && record.active) {
await this.activate(id)
}
const status = (record && record.active) ? '✓ ativo' : '○ inativo'
console.log(`[plugins] ${name} v${version}${status}`)
}
// Ativa um plugin
async activate(id) {
const plugin = this.plugins.get(id)
if (!plugin) throw new Error(`Plugin "${id}" não encontrado`)
if (plugin.active) return
const ctx = this._buildContext(id)
await plugin.instance.activate(ctx)
plugin.active = true
await execute('UPDATE plugins SET active = 1 WHERE id = $1', [id])
}
// Desativa um plugin
async deactivate(id) {
const plugin = this.plugins.get(id)
if (!plugin) throw new Error(`Plugin "${id}" não encontrado`)
if (!plugin.active) return
const ctx = this._buildContext(id)
if (typeof plugin.instance.deactivate === 'function') {
await plugin.instance.deactivate(ctx)
}
plugin.active = false
await execute('UPDATE plugins SET active = 0 WHERE id = $1', [id])
}
// Contexto injetado nos plugins durante activate/deactivate
_buildContext(id) {
return {
app: this.app,
db: this.db,
httpServer: this.httpServer, // http.Server nativo (pode ser null)
config: new PluginConfigStore(id),
logger: {
info: (msg) => console.log(`[${id}] ${msg}`),
warn: (msg) => console.warn(`[${id}] WARN: ${msg}`),
error: (msg) => console.error(`[${id}] ERR: ${msg}`),
},
}
}
// Lista todos os plugins
async list() {
const records = await query('SELECT id, active, installed_at FROM plugins')
return records.map((r) => {
const p = this.plugins.get(r.id)
return {
id: r.id,
active: !!r.active,
installed_at: r.installed_at,
name: p?.manifest?.name ?? r.id,
version: p?.manifest?.version ?? '?',
description: p?.manifest?.description ?? '',
config_schema: p?.manifest?.config_schema ?? [],
features: p?.manifest?.features ?? [],
ui_path: p?.manifest?.ui_path ?? null,
}
})
}
// Retorna configuração pública de um plugin
async getConfig(id) {
const store = new PluginConfigStore(id)
return store.getAll()
}
// Salva múltiplos pares key/value de uma vez
async setConfig(id, values) {
const store = new PluginConfigStore(id)
for (const [k, v] of Object.entries(values)) {
await store.set(k, v)
}
}
}
module.exports = PluginManager