wise-json-db
Version:
Blazing fast, crash-proof embedded JSON database for Node.js with batch operations, TTL, indexes, and segmented checkpointing.
102 lines (84 loc) • 8.77 kB
JavaScript
/**
* explorer/server.js
* WiseJSON Data Explorer - HTTP Server
*/
const http = require('http');
const url = require('url');
const fs =require('fs');
const path = require('path');
const WiseJSON = require('../wise-json/index.js');
const { matchFilter } = require('../wise-json/collection/utils.js');
const logger = require('../wise-json/logger');
const { analyzeDatabaseGraph } = require('./schema-analyzer.js');
// --- Конфигурация ---
const PORT = process.env.PORT || 3000;
const DB_PATH = process.env.WISE_JSON_PATH || path.resolve(process.cwd(), 'wise-json-db-data');
const AUTH_USER = process.env.WISEJSON_AUTH_USER;
const AUTH_PASS = process.env.WISEJSON_AUTH_PASS;
const USE_AUTH = !!(AUTH_USER && AUTH_PASS);
const ALLOW_WRITE = process.env.WISEJSON_EXPLORER_ALLOW_WRITE === 'true';
const LOGO_PATH = path.resolve(process.cwd(), 'logo.png');
logger.log(`[Server] DB Path: ${DB_PATH}`);
logger.log(`[Server] Write Operations Allowed: ${ALLOW_WRITE}`);
const db = new WiseJSON(DB_PATH);
// --- Вспомогательные функции ---
function sendJson(res, statusCode, data) { res.writeHead(statusCode, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(data, null, 2)); }
function sendError(res, statusCode, message) { sendJson(res, statusCode, { error: message }); }
function checkAuth(req, res) { if (!USE_AUTH) return true; const authHeader = req.headers['authorization']; if (!authHeader || !authHeader.startsWith('Basic ')) { res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="WiseJSON Data Explorer"' }); res.end('Unauthorized'); return false; } const b64 = authHeader.slice('Basic '.length).trim(); const [user, pass] = Buffer.from(b64, 'base64').toString().split(':'); if (user === AUTH_USER && pass === AUTH_PASS) { return true; } res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="WiseJSON Data Explorer"' }); res.end('Unauthorized'); return false; }
function serveStaticFile(filename, res) { const potentialPaths = [ path.join(__dirname, 'views', filename), path.join(__dirname, 'views', 'components', filename) ]; const filePath = potentialPaths.find(p => fs.existsSync(p)); if (!filePath) { return sendError(res, 404, 'Static file not found.'); } const ext = path.extname(filePath); const contentTypes = { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', }; try { res.writeHead(200, { 'Content-Type': contentTypes[ext] || 'text/plain' }); fs.createReadStream(filePath).pipe(res); } catch { sendError(res, 500, 'Error reading static file.'); } }
function parseFilterFromQuery(query) { const filter = {}; for (const [key, value] of Object.entries(query)) { if (key.startsWith('filter_')) { const tail = key.slice('filter_'.length); const [field, op] = tail.split('__'); let v = value; if (/^-?\d+(\.\d+)?$/.test(v)) v = parseFloat(v); if (op) { if (!filter[field]) filter[field] = {}; filter[field][`$${op}`] = v; } else { filter[field] = v; } } } return filter; }
// --- Основной обработчик запросов ---
async function requestHandler(req, res) {
if (!checkAuth(req, res)) return;
const parsedUrl = url.parse(req.url, true);
const { pathname, query } = parsedUrl;
const method = req.method.toUpperCase();
if (pathname === '/favicon.ico') { try { await fs.promises.access(LOGO_PATH); res.writeHead(200, { 'Content-Type': 'image/png' }); fs.createReadStream(LOGO_PATH).pipe(res); } catch (error) { res.writeHead(204); res.end(); } return; }
// --- Роутинг ---
if (pathname === '/') return serveStaticFile('index.html', res);
if (pathname.startsWith('/static/')) return serveStaticFile(pathname.slice('/static/'.length), res);
if (pathname === '/api/permissions' && method === 'GET') { return sendJson(res, 200, { writeMode: ALLOW_WRITE }); }
if (pathname === '/api/collections' && method === 'GET') { const names = await db.getCollectionNames(); const result = await Promise.all(names.map(async (name) => { const col = await db.collection(name); await col.initPromise; return { name, count: await col.count() }; })); return sendJson(res, 200, result); }
if (pathname === '/api/schema-graph' && method === 'GET') { try { const graphData = await analyzeDatabaseGraph(db); return sendJson(res, 200, graphData); } catch (error) { logger.error(`[Server] Error analyzing database graph: ${error.message}`); return sendError(res, 500, 'Failed to analyze database schema.'); } }
const collectionRouteMatch = pathname.match(/^\/api\/collections\/([^\/]+)\/?$/);
if (collectionRouteMatch && method === 'GET') {
// +++ ИЗМЕНЕНИЕ: Декодируем имя коллекции +++
const colName = decodeURIComponent(collectionRouteMatch[1]);
const col = await db.collection(colName); await col.initPromise;
const filter = parseFilterFromQuery(query); let filterObj = {}; if (query.filter) { try { filterObj = JSON.parse(query.filter); } catch {} }
let docs = await col.find({ ...filter, ...filterObj });
if (query.sort) { docs.sort((a, b) => { if (a[query.sort] < b[query.sort]) return query.order === 'desc' ? 1 : -1; if (a[query.sort] > b[query.sort]) return query.order === 'desc' ? -1 : 1; return 0; }); }
const offset = parseInt(query.offset || '0', 10); const limit = parseInt(query.limit || '10', 10);
return sendJson(res, 200, docs.slice(offset, offset + limit));
}
const statsRouteMatch = pathname.match(/^\/api\/collections\/([^\/]+)\/stats$/);
if (statsRouteMatch && method === 'GET') {
const colName = decodeURIComponent(statsRouteMatch[1]);
const col = await db.collection(colName); await col.initPromise;
const stats = await col.stats(); const indexes = await col.getIndexes();
return sendJson(res, 200, { ...stats, indexes });
}
const docRouteMatch = pathname.match(/^\/api\/collections\/([^\/]+)\/doc\/(.+)$/);
if (docRouteMatch) {
// +++ ИЗМЕНЕНИЕ: Декодируем имя коллекции и ID документа +++
const colName = decodeURIComponent(docRouteMatch[1]);
const docId = decodeURIComponent(docRouteMatch[2]);
const col = await db.collection(colName); await col.initPromise;
if (method === 'GET') { const doc = await col.getById(docId); return doc ? sendJson(res, 200, doc) : sendError(res, 404, 'Document not found.'); }
if (method === 'DELETE') { if (!ALLOW_WRITE) return sendError(res, 403, 'Write operations are disabled.'); const success = await col.remove(docId); return success ? sendJson(res, 200, { message: 'Document removed' }) : sendError(res, 404, 'Document not found.'); }
}
const indexRouteMatch = pathname.match(/^\/api\/collections\/([^\/]+)\/indexes\/?([^\/]+)?$/);
if (indexRouteMatch) {
if (!ALLOW_WRITE) return sendError(res, 403, 'Write operations are disabled.');
// +++ ИЗМЕНЕНИЕ: Декодируем имя коллекции и поля индекса +++
const colName = decodeURIComponent(indexRouteMatch[1]);
const fieldName = indexRouteMatch[2] ? decodeURIComponent(indexRouteMatch[2]) : null;
const col = await db.collection(colName); await col.initPromise;
if (method === 'POST' && !fieldName) { let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', async () => { try { const { fieldName: newFieldName, unique } = JSON.parse(body); if (!newFieldName) return sendError(res, 400, 'fieldName is required.'); await col.createIndex(newFieldName, { unique: !!unique }); sendJson(res, 201, { message: `Index on "${newFieldName}" created.` }); } catch (e) { sendError(res, 500, e.message); } }); return; }
if (method === 'DELETE' && fieldName) { try { await col.dropIndex(fieldName); sendJson(res, 200, { message: `Index on "${fieldName}" dropped.` }); } catch(e) { sendError(res, 500, e.message); } return; }
}
return sendError(res, 404, 'Not Found');
}
// --- Запуск сервера ---
async function startServer() { await db.init(); const server = http.createServer((req, res) => { requestHandler(req, res).catch(err => { logger.error(`[Server] Unhandled request error: ${err.message}`); sendError(res, 500, 'Internal Server Error'); }); }); server.listen(PORT, () => { if (USE_AUTH) { logger.log(`WiseJSON Data Explorer (auth required) is running at http://127.0.0.1:${PORT}/`); } else { logger.log(`WiseJSON Data Explorer is running at http://127.0.0.1:${PORT}/`); } if (!ALLOW_WRITE) { logger.warn('Server is in read-only mode. Set WISEJSON_EXPLORER_ALLOW_WRITE=true to enable changes.'); } }); }
startServer();