This commit is contained in:
seb
2026-10-13 17:19:16 +02:00
parent 2de2bc0d90
commit c3d4fa5cd9
9 changed files with 3347 additions and 1 deletions

176
src/sqlstudio/server.js Normal file
View File

@@ -0,0 +1,176 @@
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');
}