Neon-AI commited on
Commit
94f0e45
·
verified ·
1 Parent(s): f9dc8ac

Create server.js

Browse files
Files changed (1) hide show
  1. server.js +371 -0
server.js ADDED
@@ -0,0 +1,371 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // server.js — NT DB Server
2
+ import express from 'express';
3
+ import { readFile, writeFile } from 'fs/promises';
4
+ import { existsSync, mkdirSync } from 'fs';
5
+ import { join } from 'path';
6
+ import db, { readSchema, writeSchema } from './api/db.js';
7
+
8
+ const app = express();
9
+ const PORT = process.env.PORT || 7860; // 7860 for HF Spaces, 3000 for local
10
+ const DB_DIR = process.env.DB_DIR || join(process.cwd(), 'db');
11
+
12
+ if (!existsSync(DB_DIR)) mkdirSync(DB_DIR, { recursive: true });
13
+
14
+ app.use(express.json());
15
+
16
+ // ── CORS (allow all for now) ──────────────────────────────────────────────────
17
+ app.use((req, res, next) => {
18
+ res.setHeader('Access-Control-Allow-Origin', '*');
19
+ res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PATCH,DELETE,OPTIONS');
20
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization,apikey');
21
+ if (req.method === 'OPTIONS') return res.sendStatus(204);
22
+ next();
23
+ });
24
+
25
+ // ── Parse query string filters ────────────────────────────────────────────────
26
+ // ?name=eq.Alice&age=gt.25&status=in.(active,trial)
27
+ function parseFilters(query, builder) {
28
+ const reserved = new Set(['select', 'order', 'limit', 'offset']);
29
+ for (const [key, val] of Object.entries(query)) {
30
+ if (reserved.has(key)) continue;
31
+ const dot = val.indexOf('.');
32
+ if (dot === -1) continue;
33
+ const op = val.slice(0, dot);
34
+ let value = val.slice(dot + 1);
35
+ if (op === 'in' && value.startsWith('(') && value.endsWith(')')) {
36
+ value = value.slice(1, -1).split(',').map(v => v.trim());
37
+ } else if (!isNaN(value) && value !== '') {
38
+ value = Number(value);
39
+ }
40
+ if (typeof builder[op] === 'function') builder[op](key, value);
41
+ }
42
+ return builder;
43
+ }
44
+
45
+ // ── Health ────────────────────────────────────────────────────────────────────
46
+ app.get('/health', (_, res) => res.json({ status: 'ok', name: 'NT DB', version: '1.0.0' }));
47
+
48
+ // ── Schema ────────────────────────────────────────────────────────────────────
49
+
50
+ // GET /rest/v1/schema — view full schema
51
+ app.get('/rest/v1/schema', async (req, res) => {
52
+ try { res.json(await readSchema()); }
53
+ catch (err) { res.status(500).json({ error: err.message }); }
54
+ });
55
+
56
+ // POST /rest/v1/schema/tables — create a new table
57
+ app.post('/rest/v1/schema/tables', async (req, res) => {
58
+ try {
59
+ const { table, columns } = req.body;
60
+ if (!table) return res.status(400).json({ error: 'table name required' });
61
+
62
+ const schema = await readSchema();
63
+ if (schema.tables[table]) return res.status(409).json({ error: `Table '${table}' already exists` });
64
+
65
+ // Always add id + created_at
66
+ schema.tables[table] = {
67
+ columns: {
68
+ id: { type: 'uuid', primaryKey: true, auto: true },
69
+ created_at: { type: 'timestamp', auto: true },
70
+ updated_at: { type: 'timestamp', auto: true },
71
+ ...(columns || {})
72
+ }
73
+ };
74
+
75
+ await writeSchema(schema);
76
+
77
+ // Create empty table file
78
+ const tableFile = join(DB_DIR, `${table}.json`);
79
+ if (!existsSync(tableFile)) await writeFile(tableFile, '[]', 'utf8');
80
+
81
+ res.status(201).json({ message: `Table '${table}' created`, schema: schema.tables[table] });
82
+ } catch (err) { res.status(500).json({ error: err.message }); }
83
+ });
84
+
85
+ // DELETE /rest/v1/schema/tables/:table — drop a table
86
+ app.delete('/rest/v1/schema/tables/:table', async (req, res) => {
87
+ try {
88
+ const schema = await readSchema();
89
+ if (!schema.tables[req.params.table]) return res.status(404).json({ error: 'Table not found' });
90
+ delete schema.tables[req.params.table];
91
+ await writeSchema(schema);
92
+ res.json({ message: `Table '${req.params.table}' dropped` });
93
+ } catch (err) { res.status(500).json({ error: err.message }); }
94
+ });
95
+
96
+ // ── REST API ──────────────────────────────────────────────────────────────────
97
+
98
+ // GET /rest/v1/:table
99
+ app.get('/rest/v1/:table', async (req, res) => {
100
+ try {
101
+ let q = db.from(req.params.table).select(req.query.select || '*');
102
+ q = parseFilters(req.query, q);
103
+ if (req.query.order) {
104
+ const [col, dir] = req.query.order.split('.');
105
+ q.order(col, dir || 'asc');
106
+ }
107
+ if (req.query.limit) q.limit(parseInt(req.query.limit));
108
+ if (req.query.offset) q.offset(parseInt(req.query.offset));
109
+
110
+ const { data, error } = await q;
111
+ if (error) return res.status(400).json({ error });
112
+ res.json(data);
113
+ } catch (err) { res.status(500).json({ error: err.message }); }
114
+ });
115
+
116
+ // POST /rest/v1/:table
117
+ app.post('/rest/v1/:table', async (req, res) => {
118
+ try {
119
+ const { data, error } = await db.from(req.params.table).insert(req.body);
120
+ if (error) return res.status(400).json({ error });
121
+ res.status(201).json(data);
122
+ } catch (err) { res.status(500).json({ error: err.message }); }
123
+ });
124
+
125
+ // PATCH /rest/v1/:table
126
+ app.patch('/rest/v1/:table', async (req, res) => {
127
+ try {
128
+ let q = db.from(req.params.table).update(req.body);
129
+ q = parseFilters(req.query, q);
130
+ const { data, error } = await q;
131
+ if (error) return res.status(400).json({ error });
132
+ res.json(data);
133
+ } catch (err) { res.status(500).json({ error: err.message }); }
134
+ });
135
+
136
+ // DELETE /rest/v1/:table
137
+ app.delete('/rest/v1/:table', async (req, res) => {
138
+ try {
139
+ let q = db.from(req.params.table).delete();
140
+ q = parseFilters(req.query, q);
141
+ const { data, error } = await q;
142
+ if (error) return res.status(400).json({ error });
143
+ res.json(data);
144
+ } catch (err) { res.status(500).json({ error: err.message }); }
145
+ });
146
+
147
+ // ── Dashboard (simple HTML) ───────────────────────────────────────────────────
148
+ app.get('/', async (req, res) => {
149
+ const schema = await readSchema().catch(() => ({ tables: {} }));
150
+ const tables = Object.keys(schema.tables);
151
+ res.send(`<!DOCTYPE html>
152
+ <html lang="en">
153
+ <head>
154
+ <meta charset="UTF-8"/>
155
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
156
+ <title>NT DB Dashboard</title>
157
+ <script src="https://cdn.tailwindcss.com"></script>
158
+ <style>
159
+ body{background:#0f172a;color:#e2e8f0;font-family:system-ui,sans-serif}
160
+ input,textarea,select{background:#1e293b;border:1px solid #334155;color:#e2e8f0;border-radius:8px;padding:8px 12px;width:100%;outline:none}
161
+ input:focus,textarea:focus{border-color:#22c55e}
162
+ table{width:100%;border-collapse:collapse}
163
+ th{background:#1e293b;color:#94a3b8;font-size:12px;text-transform:uppercase;padding:10px 16px;text-align:left;border-bottom:1px solid #334155}
164
+ td{padding:10px 16px;border-bottom:1px solid #1e293b;font-size:13px;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
165
+ tr:hover td{background:#1e293b}
166
+ .btn{padding:7px 18px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;border:none;transition:opacity .15s}
167
+ .btn:hover{opacity:.85}
168
+ </style>
169
+ </head>
170
+ <body class="min-h-screen">
171
+ <div class="flex items-center gap-3 px-8 py-4 border-b border-slate-700">
172
+ <div class="w-8 h-8 rounded-lg bg-green-500 flex items-center justify-center font-bold text-black text-sm">NT</div>
173
+ <span class="font-bold text-lg">NT DB</span>
174
+ <span class="ml-2 text-xs bg-green-900 text-green-400 px-2 py-0.5 rounded-full font-semibold">● Live</span>
175
+ </div>
176
+ <div class="flex h-[calc(100vh-57px)]">
177
+ <!-- Sidebar -->
178
+ <div class="w-52 border-r border-slate-700 p-4">
179
+ <div class="flex items-center justify-between mb-3">
180
+ <span class="text-xs font-semibold text-slate-400 uppercase tracking-wider">Tables</span>
181
+ <button onclick="showCreate()" class="text-green-400 text-xl leading-none hover:text-green-300">+</button>
182
+ </div>
183
+ <div id="tableList">
184
+ ${tables.map(t => `<div class="px-3 py-2 rounded-lg text-sm text-slate-300 hover:bg-slate-700 cursor-pointer" onclick="loadTable('${t}')">${t}</div>`).join('')}
185
+ </div>
186
+ </div>
187
+ <!-- Main -->
188
+ <div class="flex-1 overflow-auto p-6" id="main">
189
+ <div class="text-slate-400 text-center mt-24 text-sm">← Select a table or create one</div>
190
+ </div>
191
+ </div>
192
+ <!-- Modal -->
193
+ <div id="modal" class="hidden fixed inset-0 bg-black/60 flex items-center justify-center z-50">
194
+ <div class="bg-slate-800 border border-slate-700 rounded-xl w-full max-w-md p-6" id="modalBody"></div>
195
+ </div>
196
+ <script>
197
+ const BASE = '';
198
+ let curTable = null, curSchema = null, curData = [];
199
+
200
+ async function loadSchema() {
201
+ const r = await fetch(BASE + '/rest/v1/schema');
202
+ curSchema = await r.json();
203
+ }
204
+
205
+ async function loadTable(name) {
206
+ curTable = name;
207
+ await loadSchema();
208
+ const r = await fetch(BASE + '/rest/v1/' + name);
209
+ curData = await r.json();
210
+ renderTable();
211
+ }
212
+
213
+ function renderTable() {
214
+ const cols = curSchema?.tables?.[curTable]
215
+ ? Object.keys(curSchema.tables[curTable].columns)
216
+ : (curData[0] ? Object.keys(curData[0]) : []);
217
+
218
+ document.getElementById('main').innerHTML = \`
219
+ <div class="flex items-center justify-between mb-4">
220
+ <div>
221
+ <h2 class="text-xl font-bold">\${curTable}</h2>
222
+ <p class="text-slate-400 text-sm">\${curData.length} rows</p>
223
+ </div>
224
+ <div class="flex gap-2">
225
+ <input placeholder="Search..." oninput="filterRows(this.value)"
226
+ style="width:200px" class="text-sm"/>
227
+ <button class="btn" style="background:#22c55e;color:#000" onclick="showInsert()">+ Insert</button>
228
+ <button class="btn" style="background:#334155;color:#e2e8f0" onclick="loadTable(curTable)">↻</button>
229
+ </div>
230
+ </div>
231
+ <div style="background:#1e293b;border:1px solid #334155;border-radius:12px;overflow:hidden">
232
+ <div style="overflow-x:auto">
233
+ <table>
234
+ <thead><tr>\${cols.map(c=>\`<th>\${c}</th>\`).join('')}<th>Actions</th></tr></thead>
235
+ <tbody id="tbody">\${curData.map(row=>renderRow(row,cols)).join('')}</tbody>
236
+ </table>
237
+ </div>
238
+ </div>
239
+ \`;
240
+ }
241
+
242
+ function renderRow(row, cols) {
243
+ return \`<tr>
244
+ \${cols.map(c=>\`<td title="\${esc(String(row[c]??''))}">\${esc(String(row[c]??''))}</td>\`).join('')}
245
+ <td class="flex gap-1 py-2">
246
+ <button class="btn" style="background:#334155;color:#e2e8f0;padding:4px 12px" onclick='showEdit(\${JSON.stringify(row)})'>Edit</button>
247
+ <button class="btn" style="background:#ef4444;color:#fff;padding:4px 12px" onclick="delRow('\${row.id}')">Del</button>
248
+ </td>
249
+ </tr>\`;
250
+ }
251
+
252
+ function filterRows(q) {
253
+ const filtered = q ? curData.filter(r=>Object.values(r).some(v=>String(v).toLowerCase().includes(q.toLowerCase()))) : curData;
254
+ const cols = curSchema?.tables?.[curTable] ? Object.keys(curSchema.tables[curTable].columns) : Object.keys(curData[0]||{});
255
+ document.getElementById('tbody').innerHTML = filtered.map(r=>renderRow(r,cols)).join('');
256
+ }
257
+
258
+ function showCreate() {
259
+ modal(\`
260
+ <h3 class="font-bold text-lg mb-4">Create Table</h3>
261
+ <label class="block mb-3"><span class="text-xs text-slate-400 block mb-1">Table Name</span>
262
+ <input id="tName" placeholder="e.g. posts"/></label>
263
+ <label class="block mb-3"><span class="text-xs text-slate-400 block mb-1">Columns (one per line: name:type)</span>
264
+ <textarea id="tCols" rows="5" placeholder="title:string&#10;body:string&#10;author:string&#10;views:number"></textarea></label>
265
+ <div class="flex gap-2 mt-4">
266
+ <button class="btn flex-1" style="background:#22c55e;color:#000" onclick="createTable()">Create</button>
267
+ <button class="btn" style="background:#334155;color:#e2e8f0" onclick="closeModal()">Cancel</button>
268
+ </div>
269
+ \`);
270
+ }
271
+
272
+ async function createTable() {
273
+ const name = document.getElementById('tName').value.trim();
274
+ const raw = document.getElementById('tCols').value.trim();
275
+ const columns = {};
276
+ raw.split('\\n').forEach(line => {
277
+ const [col, type='string'] = line.split(':').map(s=>s.trim());
278
+ if (col) columns[col] = { type };
279
+ });
280
+ await fetch(BASE+'/rest/v1/schema/tables', {
281
+ method:'POST', headers:{'Content-Type':'application/json'},
282
+ body: JSON.stringify({ table: name, columns })
283
+ });
284
+ closeModal();
285
+ location.reload();
286
+ }
287
+
288
+ function showInsert() {
289
+ const cols = Object.entries(curSchema?.tables?.[curTable]?.columns||{}).filter(([,d])=>!d.auto);
290
+ modal(\`
291
+ <h3 class="font-bold text-lg mb-4">Insert into <span class="text-green-400">\${curTable}</span></h3>
292
+ \${cols.map(([c,d])=>\`
293
+ <label class="block mb-3">
294
+ <span class="text-xs text-slate-400 block mb-1">\${c} <span class="text-slate-500">(\${d.type})</span></span>
295
+ \${d.type==='string'&&(c.includes('body')||c.includes('content')||c.includes('text'))
296
+ ? \`<textarea id="f_\${c}" rows="3"></textarea>\`
297
+ : \`<input id="f_\${c}" placeholder="\${d.type}"/>\`}
298
+ </label>
299
+ \`).join('')}
300
+ <div class="flex gap-2 mt-4">
301
+ <button class="btn flex-1" style="background:#22c55e;color:#000" onclick="insertRow()">Insert</button>
302
+ <button class="btn" style="background:#334155;color:#e2e8f0" onclick="closeModal()">Cancel</button>
303
+ </div>
304
+ \`);
305
+ }
306
+
307
+ async function insertRow() {
308
+ const cols = Object.entries(curSchema?.tables?.[curTable]?.columns||{}).filter(([,d])=>!d.auto);
309
+ const body = {};
310
+ cols.forEach(([c,d])=>{
311
+ const el = document.getElementById('f_'+c);
312
+ if(el?.value) body[c] = d.type==='number' ? Number(el.value) : el.value;
313
+ });
314
+ await fetch(BASE+'/rest/v1/'+curTable, {
315
+ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body)
316
+ });
317
+ closeModal(); loadTable(curTable);
318
+ }
319
+
320
+ function showEdit(row) {
321
+ const cols = Object.entries(curSchema?.tables?.[curTable]?.columns||{}).filter(([c,d])=>!d.auto&&!d.primaryKey);
322
+ modal(\`
323
+ <h3 class="font-bold text-lg mb-4">Edit Row</h3>
324
+ \${cols.map(([c,d])=>\`
325
+ <label class="block mb-3">
326
+ <span class="text-xs text-slate-400 block mb-1">\${c}</span>
327
+ \${d.type==='string'&&(c.includes('body')||c.includes('content')||c.includes('text'))
328
+ ? \`<textarea id="e_\${c}" rows="3">\${esc(String(row[c]??''))}</textarea>\`
329
+ : \`<input id="e_\${c}" value="\${esc(String(row[c]??''))}"/>\`}
330
+ </label>
331
+ \`).join('')}
332
+ <div class="flex gap-2 mt-4">
333
+ <button class="btn flex-1" style="background:#22c55e;color:#000" onclick="updateRow('\${row.id}')">Save</button>
334
+ <button class="btn" style="background:#334155;color:#e2e8f0" onclick="closeModal()">Cancel</button>
335
+ </div>
336
+ \`);
337
+ }
338
+
339
+ async function updateRow(id) {
340
+ const cols = Object.entries(curSchema?.tables?.[curTable]?.columns||{}).filter(([c,d])=>!d.auto&&!d.primaryKey);
341
+ const body = {};
342
+ cols.forEach(([c])=>{ const el=document.getElementById('e_'+c); if(el) body[c]=el.value; });
343
+ await fetch(BASE+'/rest/v1/'+curTable+'?id=eq.'+id, {
344
+ method:'PATCH', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body)
345
+ });
346
+ closeModal(); loadTable(curTable);
347
+ }
348
+
349
+ async function delRow(id) {
350
+ if(!confirm('Delete this row?')) return;
351
+ await fetch(BASE+'/rest/v1/'+curTable+'?id=eq.'+id, {method:'DELETE'});
352
+ loadTable(curTable);
353
+ }
354
+
355
+ function modal(html) {
356
+ document.getElementById('modalBody').innerHTML = html;
357
+ document.getElementById('modal').classList.remove('hidden');
358
+ }
359
+ function closeModal() { document.getElementById('modal').classList.add('hidden'); }
360
+ document.getElementById('modal').addEventListener('click', e => { if(e.target===document.getElementById('modal')) closeModal(); });
361
+ function esc(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
362
+ </script>
363
+ </body>
364
+ </html>`);
365
+ });
366
+
367
+ app.listen(PORT, () => {
368
+ console.log(`🟢 NT DB running on port ${PORT}`);
369
+ console.log(`📋 Dashboard: http://localhost:${PORT}`);
370
+ console.log(`🔌 API: http://localhost:${PORT}/rest/v1`);
371
+ });