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 studioLog = createLogger('sqlstudio'); const __dirname = path.dirname(fileURLToPath(import.meta.url)); let studioServer; let studioApp; export function isDevMode() { return ( process.env.npm_lifecycle_event === 'dev' || 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 || req.body); 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 startSqlStudio() { if (studioServer) { return studioServer; } const app = buildStudioApp(); studioApp = app; const serverInstance = http.createServer(app); await new Promise((resolve, reject) => { serverInstance.once('listening', () => resolve()); serverInstance.once('error', (err) => { studioLog.error(`Failed to listen on port ${env.sqlStudioPort}:`, err.message); reject(err); }); serverInstance.listen(env.sqlStudioPort); }); studioServer = serverInstance; studioLog.info(`SQL Studio running on http://localhost:${env.sqlStudioPort} (dev mode, no auth)`); return studioServer; } export async function stopSqlStudio() { if (!studioServer) return; const s = studioServer; studioServer = undefined; studioApp = undefined; await new Promise((resolve, reject) => { s.close((err) => (err ? reject(err) : resolve())); s.closeAllConnections(); }); studioLog.info('SQL Studio stopped'); }