feat: initial project structure (Model Project) - Backend + Multi-Frontend + Docker
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
'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
|
||||
Reference in New Issue
Block a user