jsql-neo 5.4.2 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/LICENSE +202 -202
  2. package/README.md +8097 -7971
  3. package/bin/jsql +320 -320
  4. package/bin/jsql-server +90 -90
  5. package/bin/jsql-server-control.js +177 -177
  6. package/index.d.ts +385 -357
  7. package/index.js +124 -118
  8. package/lib/ast.js +214 -0
  9. package/lib/btree.js +413 -413
  10. package/lib/cache.js +328 -328
  11. package/lib/client.js +125 -125
  12. package/lib/database.js +1744 -1744
  13. package/lib/date-types.js +182 -182
  14. package/lib/errors.js +89 -89
  15. package/lib/jsql_format.js +497 -497
  16. package/lib/migrate.js +254 -254
  17. package/lib/mod.js +292 -292
  18. package/lib/mongo_server.js +607 -607
  19. package/lib/multiserver.js +140 -140
  20. package/lib/mysql_compat.js +413 -413
  21. package/lib/mysql_server.js +1237 -1237
  22. package/lib/native_client.js +732 -732
  23. package/lib/nedb_compat.js +506 -506
  24. package/lib/pg_server.js +864 -864
  25. package/lib/plugin.js +34 -34
  26. package/lib/query.js +815 -815
  27. package/lib/redis_server.js +571 -571
  28. package/lib/sql.js +3460 -2925
  29. package/lib/sqlite_compat.js +323 -323
  30. package/lib/sqlite_worker.js +264 -264
  31. package/lib/table.js +1304 -1304
  32. package/lib/tui.js +502 -502
  33. package/lib/wasm.js +1 -1
  34. package/lib/wasm_client.js +718 -718
  35. package/lib/web_ui.js +257 -257
  36. package/native/jsql-neo-native.node +0 -0
  37. package/nativesrc/jsql-neo-core/Cargo.lock +455 -455
  38. package/nativesrc/jsql-neo-core/Cargo.toml +24 -24
  39. package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +455 -455
  40. package/nativesrc/jsql-neo-core/src/engine/memory.rs +168 -168
  41. package/nativesrc/jsql-neo-core/src/engine/mod.rs +62 -62
  42. package/nativesrc/jsql-neo-core/src/engine/table.rs +700 -700
  43. package/nativesrc/jsql-neo-core/src/lib.rs +2 -2
  44. package/nativesrc/jsql-neo-core/src/storage/mod.rs +2 -2
  45. package/nativesrc/jsql-neo-core/src/storage/persistent.rs +1 -1
  46. package/nativesrc/jsql-neo-core/src/storage/wal.rs +85 -85
  47. package/nativesrc/jsql-neo-core/src/types.rs +93 -93
  48. package/nativesrc/jsql-neo-native/Cargo.lock +606 -606
  49. package/nativesrc/jsql-neo-native/Cargo.toml +16 -16
  50. package/nativesrc/jsql-neo-native/build.rs +3 -3
  51. package/nativesrc/jsql-neo-native/package.json +7 -7
  52. package/nativesrc/jsql-neo-native/src/lib.rs +269 -269
  53. package/nativesrc/jsql-neo-wasm/Cargo.lock +465 -465
  54. package/nativesrc/jsql-neo-wasm/Cargo.toml +15 -15
  55. package/nativesrc/jsql-neo-wasm/src/lib.rs +447 -447
  56. package/package.json +87 -86
  57. package/postinstall.js +158 -158
  58. package/test/btree.test.js +128 -128
  59. package/test/coverage.js +187 -187
  60. package/test/join.test.js +110 -110
  61. package/test/native.test.js +158 -158
  62. package/test/regress-5.1.0.js +271 -271
  63. package/test/smoke.js +58 -58
  64. package/test/sql-parser.test.js +273 -0
  65. package/test/wasm.test.js +110 -110
  66. package/wasm/browser.d.ts +89 -89
  67. package/wasm/browser.mjs +569 -569
  68. package/wasm/browser_bg.mjs +597 -597
  69. package/wasm/jsql_neo_wasm.d.ts +38 -38
  70. package/wasm/jsql_neo_wasm.js +520 -520
  71. package/wasm/jsql_neo_wasm_bg.wasm.d.ts +26 -26
  72. package/wasm/package.json +11 -11
package/lib/web_ui.js CHANGED
@@ -1,257 +1,257 @@
1
- /*
2
- * JSQL-NEO built-in web UI: zero-dependency HTTP management console.
3
- *
4
- * const { WebUI } = require('jsql-neo');
5
- * const ui = new WebUI({ port: 8080, dataDir: './data' });
6
- * await ui.start();
7
- *
8
- * Routes:
9
- * GET / management console (HTML, no external deps)
10
- * GET /api/databases [{name, tables, rows}]
11
- * GET /api/tables?db=name {tables: [{name, count}]}
12
- * POST /api/query {db, sql} {columns, rows, rowCount, ok, error?}
13
- */
14
- const http = require('http');
15
- const fs = require('fs');
16
- const path = require('path');
17
- const Database = require('./database');
18
- const { executeSQL } = require('./sql');
19
-
20
- const PAGE = `<!DOCTYPE html>
21
- <html lang="en">
22
- <head>
23
- <meta charset="utf-8">
24
- <meta name="viewport" content="width=device-width, initial-scale=1">
25
- <title>JSQL-NEO</title>
26
- <style>
27
- :root { color-scheme: dark; }
28
- body { margin: 0; font: 14px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; background: #0d1117; color: #e6edf3; display: flex; height: 100vh; }
29
- .side { width: 260px; border-right: 1px solid #21262d; padding: 12px; overflow: auto; }
30
- .main { flex: 1; display: flex; flex-direction: column; }
31
- h1 { font-size: 15px; margin: 4px 0 12px; }
32
- h2 { font-size: 12px; color: #8b949e; text-transform: uppercase; letter-spacing: 1px; margin: 14px 0 6px; }
33
- .db { cursor: pointer; padding: 3px 6px; border-radius: 6px; }
34
- .db:hover, .db.active { background: #1f6feb33; }
35
- .db .rows { color: #8b949e; font-size: 12px; }
36
- .tbl { cursor: pointer; padding: 2px 6px 2px 18px; color: #79c0ff; border-radius: 4px; }
37
- .tbl:hover { background: #21262d; }
38
- textarea { flex: 1; margin: 12px; padding: 10px; background: #010409; color: #e6edf3; border: 1px solid #21262d; border-radius: 8px; resize: none; font: inherit; }
39
- .actions { padding: 0 12px; }
40
- button { background: #238636; border: 0; color: #fff; padding: 6px 16px; border-radius: 6px; cursor: pointer; font: inherit; }
41
- button:hover { background: #2ea043; }
42
- button:disabled { background: #21262d; cursor: wait; }
43
- .status { padding: 0 12px 10px; color: #8b949e; min-height: 20px; }
44
- table { border-collapse: collapse; width: 100%; font-size: 13px; }
45
- th, td { border: 1px solid #21262d; padding: 4px 8px; text-align: left; white-space: pre; }
46
- th { background: #161b22; position: sticky; top: 0; }
47
- tr:nth-child(even) td { background: #0d1117; }
48
- .result { flex: 1.4; overflow: auto; margin: 0 12px 12px; border: 1px solid #21262d; border-radius: 8px; background: #010409; }
49
- .err { color: #f85149; padding: 10px; }
50
- .ok { color: #3fb950; }
51
- </style>
52
- </head>
53
- <body>
54
- <div class="side">
55
- <h1>JSQL-NEO</h1>
56
- <h2>Databases</h2>
57
- <div id="dbs"></div>
58
- <h2>Tables</h2>
59
- <div id="tbls"></div>
60
- </div>
61
- <div class="main">
62
- <textarea id="sql" spellcheck="false" placeholder="SELECT * FROM t LIMIT 100">SELECT 1</textarea>
63
- <div class="actions">
64
- <button id="run" onclick="run()">Run (Ctrl+Enter)</button>
65
- </div>
66
- <div class="status" id="status"></div>
67
- <div class="result" id="res"></div>
68
- </div>
69
- <script>
70
- let dbs = [], cur = null;
71
- const q = async (u, o) => { const r = await fetch(u, o); return r.json(); };
72
- function esc(s) { return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
73
- async function load() {
74
- dbs = await q('/api/databases');
75
- const el = document.getElementById('dbs');
76
- el.innerHTML = dbs.map(d => '<div class="db" onclick="openDb(' + esc(d.name) + ')">' + esc(d.name) + ' <span class="rows">(' + d.tables + ' tbl)</span></div>').join('');
77
- if (!cur && dbs.length) openDb(dbs[0].name);
78
- }
79
- async function openDb(n) {
80
- cur = n;
81
- document.querySelectorAll('.db').forEach(e => e.classList.toggle('active', e.textContent.indexOf(n) === 0));
82
- const r = await q('/api/tables?db=' + encodeURIComponent(n));
83
- document.getElementById('tbls').innerHTML = r.tables.map(t =>
84
- '<div class="tbl" onclick="sel(' + esc(t.name) + ')">' + esc(t.name) + ' (' + t.count + ')</div>').join('');
85
- }
86
- function sel(n) { document.getElementById('sql').value = 'SELECT * FROM ' + n + ' LIMIT 100'; run(); }
87
- async function run() {
88
- const sql = document.getElementById('sql').value;
89
- if (!cur || !sql.trim()) return;
90
- const btn = document.getElementById('run'); btn.disabled = true;
91
- const st = document.getElementById('status'); st.innerHTML = 'running...';
92
- try {
93
- const r = await q('/api/query', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ db: cur, sql }) });
94
- st.innerHTML = r.ok ? '<span class="ok">ok</span> ' + r.rowCount + ' row(s), ' + r.ms + 'ms' : '<span class="err">' + esc(r.error) + '</span>';
95
- if (r.columns && r.columns.length) {
96
- let h = '<table><tr>' + r.columns.map(c => '<th>' + esc(c) + '</th>').join('') + '</tr>';
97
- h += r.rows.map(row => '<tr>' + row.map(c => '<td>' + (c === null ? '<i>NULL</i>' : esc(c)) + '</td>').join('') + '</tr>').join('');
98
- document.getElementById('res').innerHTML = h + '</table>';
99
- } else if (r.affected !== undefined) {
100
- document.getElementById('res').innerHTML = '<p class="ok">' + r.affected + ' row(s) affected</p>';
101
- }
102
- } catch (e) { st.innerHTML = '<span class="err">' + esc(e.message) + '</span>'; }
103
- btn.disabled = false;
104
- }
105
- document.addEventListener('keydown', e => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') run(); });
106
- load();
107
- </script>
108
- </body>
109
- </html>
110
- `;
111
-
112
- class WebUI {
113
- constructor(opts = {}) {
114
- this.port = opts.port !== undefined ? opts.port : 8080;
115
- this.dataDir = opts.dataDir || '.';
116
- this.readonly = !!opts.readonly;
117
- this.host = opts.host || '127.0.0.1';
118
- this.authToken = opts.authToken || null;
119
- this.allowOrigin = opts.allowOrigin || null;
120
- this.echoOrigin = !opts.allowOrigin && !!this.authToken;
121
- this.cache = new Map();
122
- }
123
-
124
- dbPath(name) {
125
- if (!/^[A-Za-z0-9_.-]+$/.test(name)) throw new Error('invalid database name');
126
- const p = path.join(this.dataDir, name + '.json');
127
- if (!fs.existsSync(p)) throw new Error('database not found: ' + name);
128
- return p;
129
- }
130
-
131
- async db(name) {
132
- if (this.cache.has(name)) return this.cache.get(name);
133
- const db = new Database(this.dbPath(name), { autoSave: !this.readonly });
134
- if (db.loadDatabase) await db.loadDatabase();
135
- this.cache.set(name, db);
136
- return db;
137
- }
138
-
139
- listDatabases() {
140
- if (!fs.existsSync(this.dataDir)) return [];
141
- return fs.readdirSync(this.dataDir)
142
- .filter(f => f.endsWith('.json'))
143
- .map(f => {
144
- let tables = 0;
145
- try { tables = Object.keys(JSON.parse(fs.readFileSync(path.join(this.dataDir, f), 'utf8')).__schema__ || {}).length; } catch (_) {}
146
- return { name: f.slice(0, -5), tables };
147
- });
148
- }
149
-
150
- tableList(db) {
151
- const map = db._tables || {};
152
- return Object.values(map).map(t => ({
153
- name: t._name || t.name,
154
- count: (t._rows || t.rows || []).length,
155
- }));
156
- }
157
-
158
- async handle(req, res) {
159
- const url = new URL(req.url, 'http://x');
160
- const reqOrigin = req.headers.origin;
161
- let corsOrigin = null;
162
- if (this.allowOrigin) corsOrigin = this.allowOrigin;
163
- else if (this.echoOrigin && reqOrigin) corsOrigin = reqOrigin;
164
- const send = (code, obj) => {
165
- const body = JSON.stringify(obj);
166
- const headers = { 'Content-Type': 'application/json' };
167
- if (corsOrigin) {
168
- headers['Access-Control-Allow-Origin'] = corsOrigin;
169
- headers['Vary'] = 'Origin';
170
- }
171
- res.writeHead(code, headers);
172
- res.end(body);
173
- };
174
-
175
- try {
176
- if (url.pathname === '/' && req.method === 'GET') {
177
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
178
- return res.end(PAGE);
179
- }
180
- if (req.method === 'OPTIONS' && url.pathname.startsWith('/api/')) {
181
- const h = {};
182
- if (corsOrigin) {
183
- h['Access-Control-Allow-Origin'] = corsOrigin;
184
- h['Access-Control-Allow-Methods'] = 'GET,POST,OPTIONS';
185
- h['Access-Control-Allow-Headers'] = 'Authorization,Content-Type';
186
- h['Vary'] = 'Origin';
187
- }
188
- res.writeHead(204, h);
189
- return res.end();
190
- }
191
- if (this.authToken && url.pathname.startsWith('/api/')) {
192
- const provided = req.headers.authorization || '';
193
- const token = provided.startsWith('Bearer ') ? provided.slice(7) : '';
194
- if (token !== this.authToken) {
195
- res.writeHead(401, { 'Content-Type': 'application/json' });
196
- return res.end(JSON.stringify({ ok: false, error: 'unauthorized' }));
197
- }
198
- }
199
- if (url.pathname === '/api/databases' && req.method === 'GET') {
200
- return send(200, this.listDatabases());
201
- }
202
- if (url.pathname === '/api/tables' && req.method === 'GET') {
203
- const db = await this.db(url.searchParams.get("db"));
204
- return send(200, { tables: this.tableList(db) });
205
- }
206
- if (url.pathname === '/api/query' && req.method === 'POST') {
207
- let body = '';
208
- for await (const chunk of req) body += chunk;
209
- const { db: dbName, sql } = JSON.parse(body || '{}');
210
- if (!dbName || !sql) return send(400, { ok: false, error: 'db and sql are required' });
211
- const db = await this.db(dbName);
212
- const t0 = Date.now();
213
- try {
214
- const res2 = await executeSQL(db, sql);
215
- const ms = Date.now() - t0;
216
- const out = { ok: true, ms };
217
- if (res2 && res2.rows) {
218
- out.columns = res2.columns;
219
- out.rows = res2.rows;
220
- out.rowCount = res2.rows.length;
221
- } else if (res2 && res2.affectedRows !== undefined) {
222
- out.affected = res2.affectedRows;
223
- } else {
224
- out.rowCount = 0;
225
- }
226
- return send(200, out);
227
- } catch (e) {
228
- return send(200, { ok: false, error: String(e.message || e) });
229
- }
230
- }
231
- return send(404, { ok: false, error: 'not found' });
232
- } catch (e) {
233
- return send(500, { ok: false, error: String(e.message || e) });
234
- }
235
- }
236
-
237
- start() {
238
- return new Promise((resolve, reject) => {
239
- this.server = http.createServer((req, res) => this.handle(req, res).catch(e => {
240
- try { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: String(e.message || e) })); } catch (_) {}
241
- }));
242
- this.server.on('error', reject);
243
- this.server.listen(this.port, this.host, () => resolve(this.server.address().port));
244
- });
245
- }
246
-
247
- stop() {
248
- return new Promise(resolve => {
249
- for (const db of this.cache.values()) db.stop();
250
- this.cache.clear();
251
- if (this.server) this.server.close(() => resolve());
252
- else resolve();
253
- });
254
- }
255
- }
256
-
257
- module.exports = { WebUI };
1
+ /*
2
+ * JSQL-NEO built-in web UI: zero-dependency HTTP management console.
3
+ *
4
+ * const { WebUI } = require('jsql-neo');
5
+ * const ui = new WebUI({ port: 8080, dataDir: './data' });
6
+ * await ui.start();
7
+ *
8
+ * Routes:
9
+ * GET / management console (HTML, no external deps)
10
+ * GET /api/databases [{name, tables, rows}]
11
+ * GET /api/tables?db=name {tables: [{name, count}]}
12
+ * POST /api/query {db, sql} {columns, rows, rowCount, ok, error?}
13
+ */
14
+ const http = require('http');
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const Database = require('./database');
18
+ const { executeSQL } = require('./sql');
19
+
20
+ const PAGE = `<!DOCTYPE html>
21
+ <html lang="en">
22
+ <head>
23
+ <meta charset="utf-8">
24
+ <meta name="viewport" content="width=device-width, initial-scale=1">
25
+ <title>JSQL-NEO</title>
26
+ <style>
27
+ :root { color-scheme: dark; }
28
+ body { margin: 0; font: 14px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; background: #0d1117; color: #e6edf3; display: flex; height: 100vh; }
29
+ .side { width: 260px; border-right: 1px solid #21262d; padding: 12px; overflow: auto; }
30
+ .main { flex: 1; display: flex; flex-direction: column; }
31
+ h1 { font-size: 15px; margin: 4px 0 12px; }
32
+ h2 { font-size: 12px; color: #8b949e; text-transform: uppercase; letter-spacing: 1px; margin: 14px 0 6px; }
33
+ .db { cursor: pointer; padding: 3px 6px; border-radius: 6px; }
34
+ .db:hover, .db.active { background: #1f6feb33; }
35
+ .db .rows { color: #8b949e; font-size: 12px; }
36
+ .tbl { cursor: pointer; padding: 2px 6px 2px 18px; color: #79c0ff; border-radius: 4px; }
37
+ .tbl:hover { background: #21262d; }
38
+ textarea { flex: 1; margin: 12px; padding: 10px; background: #010409; color: #e6edf3; border: 1px solid #21262d; border-radius: 8px; resize: none; font: inherit; }
39
+ .actions { padding: 0 12px; }
40
+ button { background: #238636; border: 0; color: #fff; padding: 6px 16px; border-radius: 6px; cursor: pointer; font: inherit; }
41
+ button:hover { background: #2ea043; }
42
+ button:disabled { background: #21262d; cursor: wait; }
43
+ .status { padding: 0 12px 10px; color: #8b949e; min-height: 20px; }
44
+ table { border-collapse: collapse; width: 100%; font-size: 13px; }
45
+ th, td { border: 1px solid #21262d; padding: 4px 8px; text-align: left; white-space: pre; }
46
+ th { background: #161b22; position: sticky; top: 0; }
47
+ tr:nth-child(even) td { background: #0d1117; }
48
+ .result { flex: 1.4; overflow: auto; margin: 0 12px 12px; border: 1px solid #21262d; border-radius: 8px; background: #010409; }
49
+ .err { color: #f85149; padding: 10px; }
50
+ .ok { color: #3fb950; }
51
+ </style>
52
+ </head>
53
+ <body>
54
+ <div class="side">
55
+ <h1>JSQL-NEO</h1>
56
+ <h2>Databases</h2>
57
+ <div id="dbs"></div>
58
+ <h2>Tables</h2>
59
+ <div id="tbls"></div>
60
+ </div>
61
+ <div class="main">
62
+ <textarea id="sql" spellcheck="false" placeholder="SELECT * FROM t LIMIT 100">SELECT 1</textarea>
63
+ <div class="actions">
64
+ <button id="run" onclick="run()">Run (Ctrl+Enter)</button>
65
+ </div>
66
+ <div class="status" id="status"></div>
67
+ <div class="result" id="res"></div>
68
+ </div>
69
+ <script>
70
+ let dbs = [], cur = null;
71
+ const q = async (u, o) => { const r = await fetch(u, o); return r.json(); };
72
+ function esc(s) { return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
73
+ async function load() {
74
+ dbs = await q('/api/databases');
75
+ const el = document.getElementById('dbs');
76
+ el.innerHTML = dbs.map(d => '<div class="db" onclick="openDb(' + esc(d.name) + ')">' + esc(d.name) + ' <span class="rows">(' + d.tables + ' tbl)</span></div>').join('');
77
+ if (!cur && dbs.length) openDb(dbs[0].name);
78
+ }
79
+ async function openDb(n) {
80
+ cur = n;
81
+ document.querySelectorAll('.db').forEach(e => e.classList.toggle('active', e.textContent.indexOf(n) === 0));
82
+ const r = await q('/api/tables?db=' + encodeURIComponent(n));
83
+ document.getElementById('tbls').innerHTML = r.tables.map(t =>
84
+ '<div class="tbl" onclick="sel(' + esc(t.name) + ')">' + esc(t.name) + ' (' + t.count + ')</div>').join('');
85
+ }
86
+ function sel(n) { document.getElementById('sql').value = 'SELECT * FROM ' + n + ' LIMIT 100'; run(); }
87
+ async function run() {
88
+ const sql = document.getElementById('sql').value;
89
+ if (!cur || !sql.trim()) return;
90
+ const btn = document.getElementById('run'); btn.disabled = true;
91
+ const st = document.getElementById('status'); st.innerHTML = 'running...';
92
+ try {
93
+ const r = await q('/api/query', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ db: cur, sql }) });
94
+ st.innerHTML = r.ok ? '<span class="ok">ok</span> ' + r.rowCount + ' row(s), ' + r.ms + 'ms' : '<span class="err">' + esc(r.error) + '</span>';
95
+ if (r.columns && r.columns.length) {
96
+ let h = '<table><tr>' + r.columns.map(c => '<th>' + esc(c) + '</th>').join('') + '</tr>';
97
+ h += r.rows.map(row => '<tr>' + row.map(c => '<td>' + (c === null ? '<i>NULL</i>' : esc(c)) + '</td>').join('') + '</tr>').join('');
98
+ document.getElementById('res').innerHTML = h + '</table>';
99
+ } else if (r.affected !== undefined) {
100
+ document.getElementById('res').innerHTML = '<p class="ok">' + r.affected + ' row(s) affected</p>';
101
+ }
102
+ } catch (e) { st.innerHTML = '<span class="err">' + esc(e.message) + '</span>'; }
103
+ btn.disabled = false;
104
+ }
105
+ document.addEventListener('keydown', e => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') run(); });
106
+ load();
107
+ </script>
108
+ </body>
109
+ </html>
110
+ `;
111
+
112
+ class WebUI {
113
+ constructor(opts = {}) {
114
+ this.port = opts.port !== undefined ? opts.port : 8080;
115
+ this.dataDir = opts.dataDir || '.';
116
+ this.readonly = !!opts.readonly;
117
+ this.host = opts.host || '127.0.0.1';
118
+ this.authToken = opts.authToken || null;
119
+ this.allowOrigin = opts.allowOrigin || null;
120
+ this.echoOrigin = !opts.allowOrigin && !!this.authToken;
121
+ this.cache = new Map();
122
+ }
123
+
124
+ dbPath(name) {
125
+ if (!/^[A-Za-z0-9_.-]+$/.test(name)) throw new Error('invalid database name');
126
+ const p = path.join(this.dataDir, name + '.json');
127
+ if (!fs.existsSync(p)) throw new Error('database not found: ' + name);
128
+ return p;
129
+ }
130
+
131
+ async db(name) {
132
+ if (this.cache.has(name)) return this.cache.get(name);
133
+ const db = new Database(this.dbPath(name), { autoSave: !this.readonly });
134
+ if (db.loadDatabase) await db.loadDatabase();
135
+ this.cache.set(name, db);
136
+ return db;
137
+ }
138
+
139
+ listDatabases() {
140
+ if (!fs.existsSync(this.dataDir)) return [];
141
+ return fs.readdirSync(this.dataDir)
142
+ .filter(f => f.endsWith('.json'))
143
+ .map(f => {
144
+ let tables = 0;
145
+ try { tables = Object.keys(JSON.parse(fs.readFileSync(path.join(this.dataDir, f), 'utf8')).__schema__ || {}).length; } catch (_) {}
146
+ return { name: f.slice(0, -5), tables };
147
+ });
148
+ }
149
+
150
+ tableList(db) {
151
+ const map = db._tables || {};
152
+ return Object.values(map).map(t => ({
153
+ name: t._name || t.name,
154
+ count: (t._rows || t.rows || []).length,
155
+ }));
156
+ }
157
+
158
+ async handle(req, res) {
159
+ const url = new URL(req.url, 'http://x');
160
+ const reqOrigin = req.headers.origin;
161
+ let corsOrigin = null;
162
+ if (this.allowOrigin) corsOrigin = this.allowOrigin;
163
+ else if (this.echoOrigin && reqOrigin) corsOrigin = reqOrigin;
164
+ const send = (code, obj) => {
165
+ const body = JSON.stringify(obj);
166
+ const headers = { 'Content-Type': 'application/json' };
167
+ if (corsOrigin) {
168
+ headers['Access-Control-Allow-Origin'] = corsOrigin;
169
+ headers['Vary'] = 'Origin';
170
+ }
171
+ res.writeHead(code, headers);
172
+ res.end(body);
173
+ };
174
+
175
+ try {
176
+ if (url.pathname === '/' && req.method === 'GET') {
177
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
178
+ return res.end(PAGE);
179
+ }
180
+ if (req.method === 'OPTIONS' && url.pathname.startsWith('/api/')) {
181
+ const h = {};
182
+ if (corsOrigin) {
183
+ h['Access-Control-Allow-Origin'] = corsOrigin;
184
+ h['Access-Control-Allow-Methods'] = 'GET,POST,OPTIONS';
185
+ h['Access-Control-Allow-Headers'] = 'Authorization,Content-Type';
186
+ h['Vary'] = 'Origin';
187
+ }
188
+ res.writeHead(204, h);
189
+ return res.end();
190
+ }
191
+ if (this.authToken && url.pathname.startsWith('/api/')) {
192
+ const provided = req.headers.authorization || '';
193
+ const token = provided.startsWith('Bearer ') ? provided.slice(7) : '';
194
+ if (token !== this.authToken) {
195
+ res.writeHead(401, { 'Content-Type': 'application/json' });
196
+ return res.end(JSON.stringify({ ok: false, error: 'unauthorized' }));
197
+ }
198
+ }
199
+ if (url.pathname === '/api/databases' && req.method === 'GET') {
200
+ return send(200, this.listDatabases());
201
+ }
202
+ if (url.pathname === '/api/tables' && req.method === 'GET') {
203
+ const db = await this.db(url.searchParams.get("db"));
204
+ return send(200, { tables: this.tableList(db) });
205
+ }
206
+ if (url.pathname === '/api/query' && req.method === 'POST') {
207
+ let body = '';
208
+ for await (const chunk of req) body += chunk;
209
+ const { db: dbName, sql } = JSON.parse(body || '{}');
210
+ if (!dbName || !sql) return send(400, { ok: false, error: 'db and sql are required' });
211
+ const db = await this.db(dbName);
212
+ const t0 = Date.now();
213
+ try {
214
+ const res2 = await executeSQL(db, sql);
215
+ const ms = Date.now() - t0;
216
+ const out = { ok: true, ms };
217
+ if (res2 && res2.rows) {
218
+ out.columns = res2.columns;
219
+ out.rows = res2.rows;
220
+ out.rowCount = res2.rows.length;
221
+ } else if (res2 && res2.affectedRows !== undefined) {
222
+ out.affected = res2.affectedRows;
223
+ } else {
224
+ out.rowCount = 0;
225
+ }
226
+ return send(200, out);
227
+ } catch (e) {
228
+ return send(200, { ok: false, error: String(e.message || e) });
229
+ }
230
+ }
231
+ return send(404, { ok: false, error: 'not found' });
232
+ } catch (e) {
233
+ return send(500, { ok: false, error: String(e.message || e) });
234
+ }
235
+ }
236
+
237
+ start() {
238
+ return new Promise((resolve, reject) => {
239
+ this.server = http.createServer((req, res) => this.handle(req, res).catch(e => {
240
+ try { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: String(e.message || e) })); } catch (_) {}
241
+ }));
242
+ this.server.on('error', reject);
243
+ this.server.listen(this.port, this.host, () => resolve(this.server.address().port));
244
+ });
245
+ }
246
+
247
+ stop() {
248
+ return new Promise(resolve => {
249
+ for (const db of this.cache.values()) db.stop();
250
+ this.cache.clear();
251
+ if (this.server) this.server.close(() => resolve());
252
+ else resolve();
253
+ });
254
+ }
255
+ }
256
+
257
+ module.exports = { WebUI };
File without changes