jsql-neo 4.2.0 → 4.4.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.
- package/README.md +112 -94
- package/bin/jsql +138 -0
- package/index.d.ts +351 -0
- package/index.js +18 -0
- package/lib/migrate.js +242 -0
- package/lib/mysql_server.js +335 -6
- package/lib/redis_server.js +448 -0
- package/lib/sql.js +533 -87
- package/lib/table.js +4 -2
- package/lib/web_ui.js +226 -0
- package/package.json +66 -47
- package/test/smoke.js +58 -0
- package/wasm/browser.d.ts +88 -0
- package/wasm/browser.mjs +404 -0
- package/wasm/browser_bg.mjs +462 -0
package/lib/table.js
CHANGED
|
@@ -255,7 +255,8 @@ class Table {
|
|
|
255
255
|
}
|
|
256
256
|
for (const field of this._cachedSchemaFields) {
|
|
257
257
|
const def = schema[field];
|
|
258
|
-
if (def.required && (data[field] === undefined || data[field] === null)
|
|
258
|
+
if (def.required && (data[field] === undefined || data[field] === null)
|
|
259
|
+
&& !(def.autoIncrement && data[field] === undefined)) throw createError('ER_BAD_NULL_ERROR', field);
|
|
259
260
|
if (def.unique && data[field] !== undefined) {
|
|
260
261
|
if (uniqueTracker[field].has(data[field])) throw createError('ER_DUP_ENTRY', String(data[field]), field);
|
|
261
262
|
uniqueTracker[field].add(data[field]);
|
|
@@ -986,7 +987,8 @@ class Table {
|
|
|
986
987
|
if (field === '_softDelete') continue;
|
|
987
988
|
|
|
988
989
|
// NOT NULL
|
|
989
|
-
if (def.required && (data[field] === undefined || data[field] === null)
|
|
990
|
+
if (def.required && (data[field] === undefined || data[field] === null)
|
|
991
|
+
&& !(def.autoIncrement && data[field] === undefined)) {
|
|
990
992
|
throw createError('ER_BAD_NULL_ERROR', field);
|
|
991
993
|
}
|
|
992
994
|
|
package/lib/web_ui.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
|
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 || 8080;
|
|
115
|
+
this.dataDir = opts.dataDir || '.';
|
|
116
|
+
this.readonly = !!opts.readonly;
|
|
117
|
+
this.host = opts.host || '0.0.0.0';
|
|
118
|
+
this.cache = new Map();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
dbPath(name) {
|
|
122
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(name)) throw new Error('invalid database name');
|
|
123
|
+
const p = path.join(this.dataDir, name + '.json');
|
|
124
|
+
if (!fs.existsSync(p)) throw new Error('database not found: ' + name);
|
|
125
|
+
return p;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async db(name) {
|
|
129
|
+
if (this.cache.has(name)) return this.cache.get(name);
|
|
130
|
+
const db = new Database(this.dbPath(name), { autoSave: !this.readonly });
|
|
131
|
+
if (db.loadDatabase) await db.loadDatabase();
|
|
132
|
+
this.cache.set(name, db);
|
|
133
|
+
return db;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
listDatabases() {
|
|
137
|
+
if (!fs.existsSync(this.dataDir)) return [];
|
|
138
|
+
return fs.readdirSync(this.dataDir)
|
|
139
|
+
.filter(f => f.endsWith('.json'))
|
|
140
|
+
.map(f => {
|
|
141
|
+
let tables = 0;
|
|
142
|
+
try { tables = Object.keys(JSON.parse(fs.readFileSync(path.join(this.dataDir, f), 'utf8')).__schema__ || {}).length; } catch (_) {}
|
|
143
|
+
return { name: f.slice(0, -5), tables };
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
tableList(db) {
|
|
148
|
+
const map = db._tables || {};
|
|
149
|
+
return Object.values(map).map(t => ({
|
|
150
|
+
name: t._name || t.name,
|
|
151
|
+
count: (t._rows || t.rows || []).length,
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async handle(req, res) {
|
|
156
|
+
const url = new URL(req.url, 'http://x');
|
|
157
|
+
const send = (code, obj) => {
|
|
158
|
+
const body = JSON.stringify(obj);
|
|
159
|
+
res.writeHead(code, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
160
|
+
res.end(body);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
if (url.pathname === '/' && req.method === 'GET') {
|
|
165
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
166
|
+
return res.end(PAGE);
|
|
167
|
+
}
|
|
168
|
+
if (url.pathname === '/api/databases' && req.method === 'GET') {
|
|
169
|
+
return send(200, this.listDatabases());
|
|
170
|
+
}
|
|
171
|
+
if (url.pathname === '/api/tables' && req.method === 'GET') {
|
|
172
|
+
const db = await this.db(url.searchParams.get("db"));
|
|
173
|
+
return send(200, { tables: this.tableList(db) });
|
|
174
|
+
}
|
|
175
|
+
if (url.pathname === '/api/query' && req.method === 'POST') {
|
|
176
|
+
let body = '';
|
|
177
|
+
for await (const chunk of req) body += chunk;
|
|
178
|
+
const { db: dbName, sql } = JSON.parse(body || '{}');
|
|
179
|
+
if (!dbName || !sql) return send(400, { ok: false, error: 'db and sql are required' });
|
|
180
|
+
const db = await this.db(dbName);
|
|
181
|
+
const t0 = Date.now();
|
|
182
|
+
try {
|
|
183
|
+
const res2 = await executeSQL(db, sql);
|
|
184
|
+
const ms = Date.now() - t0;
|
|
185
|
+
const out = { ok: true, ms };
|
|
186
|
+
if (res2 && res2.rows) {
|
|
187
|
+
out.columns = res2.columns;
|
|
188
|
+
out.rows = res2.rows;
|
|
189
|
+
out.rowCount = res2.rows.length;
|
|
190
|
+
} else if (res2 && res2.affectedRows !== undefined) {
|
|
191
|
+
out.affected = res2.affectedRows;
|
|
192
|
+
} else {
|
|
193
|
+
out.rowCount = 0;
|
|
194
|
+
}
|
|
195
|
+
return send(200, out);
|
|
196
|
+
} catch (e) {
|
|
197
|
+
return send(200, { ok: false, error: String(e.message || e) });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return send(404, { ok: false, error: 'not found' });
|
|
201
|
+
} catch (e) {
|
|
202
|
+
return send(500, { ok: false, error: String(e.message || e) });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
start() {
|
|
207
|
+
return new Promise((resolve, reject) => {
|
|
208
|
+
this.server = http.createServer((req, res) => this.handle(req, res).catch(e => {
|
|
209
|
+
try { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: String(e.message || e) })); } catch (_) {}
|
|
210
|
+
}));
|
|
211
|
+
this.server.on('error', reject);
|
|
212
|
+
this.server.listen(this.port, this.host, () => resolve(this.server.address().port));
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
stop() {
|
|
217
|
+
return new Promise(resolve => {
|
|
218
|
+
for (const db of this.cache.values()) db.stop();
|
|
219
|
+
this.cache.clear();
|
|
220
|
+
if (this.server) this.server.close(() => resolve());
|
|
221
|
+
else resolve();
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = { WebUI };
|
package/package.json
CHANGED
|
@@ -1,51 +1,70 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
"name": "jsql-neo",
|
|
3
|
+
"version": "4.4.0",
|
|
4
|
+
"description": "JSQL-NEO — Rust-powered embedded database with WASM, REST API, B-Tree indexes, WAL, crash recovery",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./index.d.ts",
|
|
10
|
+
"browser": "./wasm/browser.mjs",
|
|
11
|
+
"default": "./index.js"
|
|
8
12
|
},
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"bin/",
|
|
13
|
-
"wasm/",
|
|
14
|
-
"native/",
|
|
15
|
-
"nativesrc/",
|
|
16
|
-
"!nativesrc/**/target/",
|
|
17
|
-
"postinstall.js",
|
|
18
|
-
"README.md",
|
|
19
|
-
"LICENSE"
|
|
20
|
-
],
|
|
21
|
-
"scripts": {
|
|
22
|
-
"postinstall": "node postinstall.js"
|
|
13
|
+
"./wasm/browser.mjs": {
|
|
14
|
+
"types": "./wasm/browser.d.ts",
|
|
15
|
+
"default": "./wasm/browser.mjs"
|
|
23
16
|
},
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
17
|
+
"./wasm/*": "./wasm/*",
|
|
18
|
+
"./lib/*": "./lib/*",
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"jsql": "./bin/jsql"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"index.js",
|
|
26
|
+
"index.d.ts",
|
|
27
|
+
"lib/",
|
|
28
|
+
"bin/",
|
|
29
|
+
"wasm/",
|
|
30
|
+
"native/",
|
|
31
|
+
"nativesrc/",
|
|
32
|
+
"!nativesrc/**/target/",
|
|
33
|
+
"postinstall.js",
|
|
34
|
+
"test/",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"postinstall": "node postinstall.js",
|
|
40
|
+
"test": "node test/smoke.js",
|
|
41
|
+
"test:orms": "node examples/orms/run-all.js"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"database",
|
|
45
|
+
"sql",
|
|
46
|
+
"embedded",
|
|
47
|
+
"rust",
|
|
48
|
+
"wasm",
|
|
49
|
+
"json",
|
|
50
|
+
"javascript",
|
|
51
|
+
"jsql",
|
|
52
|
+
"nosql",
|
|
53
|
+
"rest-api",
|
|
54
|
+
"high-performance"
|
|
55
|
+
],
|
|
56
|
+
"author": "Vexify",
|
|
57
|
+
"license": "Apache-2.0",
|
|
58
|
+
"repository": {
|
|
59
|
+
"type": "git",
|
|
60
|
+
"url": "https://github.com/vexify-org/JSQL-neo.git"
|
|
61
|
+
},
|
|
62
|
+
"bugs": "https://github.com/vexify-org/JSQL-neo/issues",
|
|
63
|
+
"homepage": "https://github.com/vexify-org/JSQL-neo#readme",
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=14.0.0"
|
|
66
|
+
},
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"@vexify-org/yaggs": "^8.1.0"
|
|
69
|
+
}
|
|
51
70
|
}
|
package/test/smoke.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Zero-dependency smoke test for the SQL engine core.
|
|
3
|
+
* npm test (or: node test/smoke.js)
|
|
4
|
+
*/
|
|
5
|
+
const { executeSQL, parseSQL, applyParams } = require('../lib/sql.js');
|
|
6
|
+
const { createMysqlServer } = require('../lib/mysql_server.js');
|
|
7
|
+
|
|
8
|
+
let pass = 0, fail = 0;
|
|
9
|
+
const ok = (name, cond, extra) => {
|
|
10
|
+
if (cond) { pass++; console.log('[OK]', name); }
|
|
11
|
+
else { fail++; console.log('[FAIL]', name, extra !== undefined ? '→ ' + JSON.stringify(extra) : ''); }
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const engine = {
|
|
15
|
+
hasTable: () => true,
|
|
16
|
+
truncate: async () => {},
|
|
17
|
+
flush: async () => {},
|
|
18
|
+
find: async () => [
|
|
19
|
+
{ id: 1, name: 'A', age: 30 },
|
|
20
|
+
{ id: 2, name: 'B', age: 40 },
|
|
21
|
+
{ id: 3, name: 'C', age: 25 },
|
|
22
|
+
],
|
|
23
|
+
getTableSchema: async () => ({
|
|
24
|
+
id: { type: 'integer', primaryKey: true },
|
|
25
|
+
name: { type: 'string' },
|
|
26
|
+
age: { type: 'integer' },
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
(async () => {
|
|
31
|
+
const opts = { safety: false };
|
|
32
|
+
|
|
33
|
+
const agg = await executeSQL(engine, 'SELECT COUNT(*) AS n, MAX(age) AS m, SUM(age) AS s, AVG(age) AS a FROM users', opts);
|
|
34
|
+
ok('multi-aggregate SELECT returns all columns', JSON.stringify(agg.columns) === JSON.stringify(['n', 'm', 's', 'a'])
|
|
35
|
+
&& JSON.stringify(agg.rows[0]) === JSON.stringify([3, 40, 95, 31.666666666666668]), { cols: agg.columns, row: agg.rows[0] });
|
|
36
|
+
|
|
37
|
+
const w = await executeSQL(engine, 'SELECT name, age FROM users WHERE age > 29 ORDER BY age DESC', opts);
|
|
38
|
+
ok('WHERE + ORDER BY', w.rows.length === 2 && w.rows[0][1] === 40, w.rows);
|
|
39
|
+
|
|
40
|
+
const funcs = await executeSQL(engine, "SELECT CONCAT(name, '!') AS c, UPPER(name) AS u, IFNULL(age, 0) AS f FROM users LIMIT 1", opts);
|
|
41
|
+
ok('scalar functions CONCAT/UPPER/IFNULL', funcs.rows[0][0] === 'A!' && funcs.rows[0][1] === 'A', funcs.rows);
|
|
42
|
+
|
|
43
|
+
const c1 = await executeSQL(engine, 'SELECT COUNT(1) AS n FROM users', opts);
|
|
44
|
+
ok('COUNT(1) literal', c1.rows[0][0] === 3, c1.rows);
|
|
45
|
+
|
|
46
|
+
ok('parseSQL TRUNCATE', parseSQL('TRUNCATE TABLE users').type === 'truncate');
|
|
47
|
+
ok('parseSQL START TRANSACTION', parseSQL('START TRANSACTION').type === 'begin');
|
|
48
|
+
ok('parseSQL VALUES DEFAULT', parseSQL(applyParams('INSERT INTO t (id, name) VALUES (DEFAULT, ?)', ['x'])).dataRows[0].id._default === true);
|
|
49
|
+
|
|
50
|
+
const srv = createMysqlServer({ port: 0, host: '127.0.0.1', dataDir: ':memory:', noAuth: true });
|
|
51
|
+
await new Promise(r => srv.listen(r));
|
|
52
|
+
const addr = srv.address;
|
|
53
|
+
ok('createMysqlServer listens on ephemeral port', addr && addr.port > 0, addr);
|
|
54
|
+
srv.close();
|
|
55
|
+
|
|
56
|
+
console.log(fail === 0 ? '\nALL SMOKE TESTS PASSED' : `\n${fail} FAILURES`);
|
|
57
|
+
process.exit(fail === 0 ? 0 : 1);
|
|
58
|
+
})().catch(e => { console.error('FATAL', e.message); process.exit(1); });
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSQL-NEO browser (WASM + IndexedDB) — ESM entry `jsql-neo/wasm/browser.mjs`.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type FieldType =
|
|
6
|
+
| 'string' | 'integer' | 'float' | 'boolean' | 'date' | 'datetime' | 'timestamp'
|
|
7
|
+
| 'object' | 'array' | 'any';
|
|
8
|
+
|
|
9
|
+
export interface FieldDef {
|
|
10
|
+
type: FieldType;
|
|
11
|
+
primaryKey?: boolean;
|
|
12
|
+
autoIncrement?: boolean;
|
|
13
|
+
unique?: boolean;
|
|
14
|
+
required?: boolean;
|
|
15
|
+
length?: number;
|
|
16
|
+
default?: unknown;
|
|
17
|
+
computed?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type Schema = Record<string, FieldDef>;
|
|
21
|
+
|
|
22
|
+
export interface Row {
|
|
23
|
+
id: number | string;
|
|
24
|
+
fields?: Record<string, unknown>;
|
|
25
|
+
created_at?: string;
|
|
26
|
+
updated_at?: string;
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface FindOptions {
|
|
31
|
+
limit?: number;
|
|
32
|
+
offset?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type JSQLHook =
|
|
36
|
+
| 'beforeInsert' | 'afterInsert'
|
|
37
|
+
| 'beforeUpdate' | 'afterUpdate'
|
|
38
|
+
| 'beforeDelete' | 'afterDelete'
|
|
39
|
+
| 'beforeFind' | 'afterFind'
|
|
40
|
+
| 'beforeCreateTable' | 'afterCreateTable'
|
|
41
|
+
| 'beforeDropTable' | 'afterDropTable'
|
|
42
|
+
| 'beforeFlush' | 'afterFlush'
|
|
43
|
+
| 'beforeCount' | 'afterCount'
|
|
44
|
+
| 'onStart' | 'onStop';
|
|
45
|
+
|
|
46
|
+
export interface JSQLOptions {
|
|
47
|
+
dbName?: string;
|
|
48
|
+
persistence?: boolean;
|
|
49
|
+
flushThreshold?: number;
|
|
50
|
+
pageSize?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class JSQL {
|
|
54
|
+
constructor(opts?: JSQLOptions);
|
|
55
|
+
on(event: JSQLHook, fn: (...args: any[]) => unknown): this;
|
|
56
|
+
onEvent(fn: (event: string, data: unknown) => void): this;
|
|
57
|
+
start(): Promise<void>;
|
|
58
|
+
stop(): Promise<void>;
|
|
59
|
+
flush(): Promise<void>;
|
|
60
|
+
createTable(name: string, schema: Schema): Promise<unknown>;
|
|
61
|
+
dropTable(name: string): Promise<unknown>;
|
|
62
|
+
insert(table: string, data: Record<string, unknown> | Record<string, unknown>[]): Promise<unknown>;
|
|
63
|
+
findById(table: string, id: number | string): Promise<Row | null>;
|
|
64
|
+
findByIds(table: string, ids: Array<number | string>): Promise<Row[] | null>;
|
|
65
|
+
find(table: string, filter?: Record<string, unknown>, opts?: FindOptions): Promise<Row[]>;
|
|
66
|
+
count(table: string): Promise<number>;
|
|
67
|
+
updateById(table: string, id: number | string, data: Record<string, unknown>): Promise<unknown>;
|
|
68
|
+
updateByIds(table: string, entries: Array<[number | string, Record<string, unknown>]>): Promise<unknown>;
|
|
69
|
+
removeById(table: string, id: number | string): Promise<unknown>;
|
|
70
|
+
removeByIds(table: string, ids: Array<number | string>): Promise<unknown>;
|
|
71
|
+
hasTable(name: string): Promise<boolean>;
|
|
72
|
+
getTables(): Promise<string[]>;
|
|
73
|
+
getTableSchema(name: string): Promise<Schema | null>;
|
|
74
|
+
beginTx(): Promise<void>;
|
|
75
|
+
commitTx(): Promise<void>;
|
|
76
|
+
rollbackTx(): Promise<void>;
|
|
77
|
+
begin(): Promise<void>;
|
|
78
|
+
commit(): Promise<void>;
|
|
79
|
+
rollback(): Promise<void>;
|
|
80
|
+
listDatabases(): Promise<string[]>;
|
|
81
|
+
createDatabase(name: string): Promise<void>;
|
|
82
|
+
dropDatabase(name: string): Promise<void>;
|
|
83
|
+
useDatabase(name: string): Promise<void>;
|
|
84
|
+
executeSQL(sql: string, params?: unknown[]): Promise<unknown>;
|
|
85
|
+
export default?: never;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function init(input?: Uint8Array | ArrayBuffer | Response | WebAssembly.Module | URL | string): Promise<JSQL | void>;
|