// server.js — NT DB Server import express from 'express'; import { readFile, writeFile } from 'fs/promises'; import { existsSync, mkdirSync } from 'fs'; import { join } from 'path'; import db, { readSchema, writeSchema } from './api/db.js'; const app = express(); const PORT = process.env.PORT || 7860; // 7860 for HF Spaces, 3000 for local const DB_DIR = process.env.DB_DIR || join(process.cwd(), 'db'); if (!existsSync(DB_DIR)) mkdirSync(DB_DIR, { recursive: true }); app.use(express.json()); // ── CORS (allow all for now) ────────────────────────────────────────────────── app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PATCH,DELETE,OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization,apikey'); if (req.method === 'OPTIONS') return res.sendStatus(204); next(); }); // ── Parse query string filters ──────────────────────────────────────────────── // ?name=eq.Alice&age=gt.25&status=in.(active,trial) function parseFilters(query, builder) { const reserved = new Set(['select', 'order', 'limit', 'offset']); for (const [key, val] of Object.entries(query)) { if (reserved.has(key)) continue; const dot = val.indexOf('.'); if (dot === -1) continue; const op = val.slice(0, dot); let value = val.slice(dot + 1); if (op === 'in' && value.startsWith('(') && value.endsWith(')')) { value = value.slice(1, -1).split(',').map(v => v.trim()); } else if (!isNaN(value) && value !== '') { value = Number(value); } if (typeof builder[op] === 'function') builder[op](key, value); } return builder; } // ── Health ──────────────────────────────────────────────────────────────────── app.get('/health', (_, res) => res.json({ status: 'ok', name: 'NT DB', version: '1.0.0' })); // ── Schema ──────────────────────────────────────────────────────────────────── // GET /rest/v1/schema — view full schema app.get('/rest/v1/schema', async (req, res) => { try { res.json(await readSchema()); } catch (err) { res.status(500).json({ error: err.message }); } }); // POST /rest/v1/schema/tables — create a new table app.post('/rest/v1/schema/tables', async (req, res) => { try { const { table, columns } = req.body; if (!table) return res.status(400).json({ error: 'table name required' }); const schema = await readSchema(); if (schema.tables[table]) return res.status(409).json({ error: `Table '${table}' already exists` }); // Always add id + created_at schema.tables[table] = { columns: { id: { type: 'uuid', primaryKey: true, auto: true }, created_at: { type: 'timestamp', auto: true }, updated_at: { type: 'timestamp', auto: true }, ...(columns || {}) } }; await writeSchema(schema); // Create empty table file const tableFile = join(DB_DIR, `${table}.json`); if (!existsSync(tableFile)) await writeFile(tableFile, '[]', 'utf8'); res.status(201).json({ message: `Table '${table}' created`, schema: schema.tables[table] }); } catch (err) { res.status(500).json({ error: err.message }); } }); // DELETE /rest/v1/schema/tables/:table — drop a table app.delete('/rest/v1/schema/tables/:table', async (req, res) => { try { const schema = await readSchema(); if (!schema.tables[req.params.table]) return res.status(404).json({ error: 'Table not found' }); delete schema.tables[req.params.table]; await writeSchema(schema); res.json({ message: `Table '${req.params.table}' dropped` }); } catch (err) { res.status(500).json({ error: err.message }); } }); // ── REST API ────────────────────────────────────────────────────────────────── // GET /rest/v1/:table app.get('/rest/v1/:table', async (req, res) => { try { let q = db.from(req.params.table).select(req.query.select || '*'); q = parseFilters(req.query, q); if (req.query.order) { const [col, dir] = req.query.order.split('.'); q.order(col, dir || 'asc'); } if (req.query.limit) q.limit(parseInt(req.query.limit)); if (req.query.offset) q.offset(parseInt(req.query.offset)); const { data, error } = await q; if (error) return res.status(400).json({ error }); res.json(data); } catch (err) { res.status(500).json({ error: err.message }); } }); // POST /rest/v1/:table app.post('/rest/v1/:table', async (req, res) => { try { const { data, error } = await db.from(req.params.table).insert(req.body); if (error) return res.status(400).json({ error }); res.status(201).json(data); } catch (err) { res.status(500).json({ error: err.message }); } }); // PATCH /rest/v1/:table app.patch('/rest/v1/:table', async (req, res) => { try { let q = db.from(req.params.table).update(req.body); q = parseFilters(req.query, q); const { data, error } = await q; if (error) return res.status(400).json({ error }); res.json(data); } catch (err) { res.status(500).json({ error: err.message }); } }); // DELETE /rest/v1/:table app.delete('/rest/v1/:table', async (req, res) => { try { let q = db.from(req.params.table).delete(); q = parseFilters(req.query, q); const { data, error } = await q; if (error) return res.status(400).json({ error }); res.json(data); } catch (err) { res.status(500).json({ error: err.message }); } }); // ── Dashboard (simple HTML) ─────────────────────────────────────────────────── app.get('/', async (req, res) => { const schema = await readSchema().catch(() => ({ tables: {} })); const tables = Object.keys(schema.tables); res.send(` NT DB Dashboard
NT
NT DB ● Live
Tables
${tables.map(t => `
${t}
`).join('')}
← Select a table or create one
`); }); app.listen(PORT, () => { console.log(`🟢 NT DB running on port ${PORT}`); console.log(`📋 Dashboard: http://localhost:${PORT}`); console.log(`🔌 API: http://localhost:${PORT}/rest/v1`); });