import express from 'express'; import http from 'http'; import path from 'path'; import { fileURLToPath } from 'url'; import env from '../config/env.js'; import { createLogger } from '../util/logger.js'; import * as dbHelper from './dbHelper.js'; const monitorLog = createLogger('backendmonitor'); const __dirname = path.dirname(fileURLToPath(import.meta.url)); let monitorServer; let monitorApp; export function isDevMode() { return ( process.env.npm_lifecycle_event === 'dev' || process.env.DEV_BACKEND_MONITOR === 'true' || process.env.DEV_SQL_STUDIO === 'true' || process.env.DEV_MODE === 'true' ); } export function buildStudioApp() { const app = express(); app.use(express.json({ limit: '50mb' })); app.use(express.urlencoded({ extended: true, limit: '50mb' })); // Unrestricted local CORS app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept'); if (req.method === 'OPTIONS') { return res.sendStatus(204); } next(); }); // 1. Status app.get('/api/status', async (req, res) => { try { const info = await dbHelper.getDatabaseInfo(); res.json(info); } catch (err) { res.status(500).json({ error: err.message }); } }); // 2. Tables list app.get('/api/tables', async (req, res) => { try { const tables = await dbHelper.getTables(); res.json({ tables }); } catch (err) { res.status(500).json({ error: err.message }); } }); // 3. Table schema app.get('/api/tables/:table/schema', async (req, res) => { try { const schema = await dbHelper.getTableSchema(req.params.table); res.json({ tableName: req.params.table, columns: schema }); } catch (err) { res.status(500).json({ error: err.message }); } }); // 4. Table data (browsing) app.get('/api/tables/:table/data', async (req, res) => { try { const data = await dbHelper.getTableData(req.params.table, { page: req.query.page, pageSize: req.query.pageSize, sortBy: req.query.sortBy, sortDir: req.query.sortDir, search: req.query.search, }); res.json(data); } catch (err) { res.status(500).json({ error: err.message }); } }); // 5. Insert row app.post('/api/tables/:table/row', async (req, res) => { try { const result = await dbHelper.insertRow(req.params.table, req.body.row); res.json(result); } catch (err) { res.status(400).json({ error: err.message }); } }); // 6. Update row app.put('/api/tables/:table/row', async (req, res) => { try { const { primaryKey, changes } = req.body; const result = await dbHelper.updateRow(req.params.table, { primaryKey, changes }); res.json(result); } catch (err) { res.status(400).json({ error: err.message }); } }); // 7. Delete row app.delete('/api/tables/:table/row', async (req, res) => { try { const { primaryKey } = req.body; const result = await dbHelper.deleteRow(req.params.table, { primaryKey }); res.json(result); } catch (err) { res.status(400).json({ error: err.message }); } }); // 8. Raw SQL Query execution app.post('/api/query', async (req, res) => { try { const { sql } = req.body; const result = await dbHelper.executeRawQuery(sql); res.json(result); } catch (err) { res.status(500).json({ error: err.message }); } }); // Static assets const publicDir = path.join(__dirname, 'public'); app.use(express.static(publicDir)); // Fallback to index.html for SPA routing app.use((req, res) => { res.sendFile(path.join(publicDir, 'index.html')); }); return app; } export async function startBackendMonitor() { if (monitorServer) { return monitorServer; } const app = buildStudioApp(); monitorApp = app; const serverInstance = http.createServer(app); await new Promise((resolve, reject) => { serverInstance.once('listening', () => resolve()); serverInstance.once('error', (err) => { monitorLog.error(`Failed to listen on ${env.host}:${env.monitorPort}:`, err.message); reject(err); }); serverInstance.listen(env.monitorPort, env.host); }); monitorServer = serverInstance; monitorLog.info(`Backend Monitor Interface running on http://${env.host}:${env.monitorPort} (dev mode, no auth)`); return monitorServer; } export async function stopBackendMonitor() { if (!monitorServer) return; const s = monitorServer; monitorServer = undefined; monitorApp = undefined; await new Promise((resolve, reject) => { s.close((err) => (err ? reject(err) : resolve())); s.closeAllConnections(); }); monitorLog.info('Backend Monitor Interface stopped'); }