jsql-neo 5.0.0 → 5.1.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 +35 -1
- package/bin/jsql +13 -10
- package/index.js +3 -1
- package/lib/btree.js +22 -4
- package/lib/database.js +51 -12
- package/lib/migrate.js +16 -4
- package/lib/mysql_compat.js +2 -1
- package/lib/mysql_server.js +14 -0
- package/lib/native_client.js +51 -128
- package/lib/redis_server.js +11 -5
- package/lib/sqlite_worker.js +1 -1
- package/lib/table.js +83 -1
- package/lib/web_ui.js +26 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -154,9 +154,20 @@ MySQL DDL forms (`int unsigned`, `auto_increment`, `ENGINE=InnoDB`, `DEFAULT CHA
|
|
|
154
154
|
|
|
155
155
|
```js
|
|
156
156
|
const { createMysqlServer } = require('jsql-neo');
|
|
157
|
+
// 开发环境:本地无认证
|
|
157
158
|
createMysqlServer({ port: 3306, dataDir: './data', noAuth: true }).listen();
|
|
159
|
+
|
|
160
|
+
// 生产环境:用户名/密码 + 每用户数据库白名单(ACL)
|
|
161
|
+
createMysqlServer({
|
|
162
|
+
port: 3306,
|
|
163
|
+
dataDir: './data',
|
|
164
|
+
auth: { app: { password: 's3cret', databases: ['app', 'analytics'] } },
|
|
165
|
+
}).listen();
|
|
158
166
|
```
|
|
159
167
|
|
|
168
|
+
`auth` 里的 `databases` 数组即该用户的数据库白名单:越权访问(跨库引用、`SHOW TABLES FROM db`、
|
|
169
|
+
`DROP DATABASE`)统一返回 `ER_DBACCESS_DENIED_ERROR` (1044)。省略 `databases` 时不限制库。
|
|
170
|
+
|
|
160
171
|
---
|
|
161
172
|
|
|
162
173
|
## Speak Redis
|
|
@@ -176,9 +187,15 @@ Snapshot persistence to `data.rdb.json` — debounced writes (500ms) plus a guar
|
|
|
176
187
|
|
|
177
188
|
```js
|
|
178
189
|
const { createRedisServer } = require('jsql-neo');
|
|
190
|
+
// 本地无认证
|
|
179
191
|
createRedisServer({ port: 6379, dataDir: './redis-data' }).listen();
|
|
192
|
+
|
|
193
|
+
// 带密码:每连接独立认证,任一客户端 AUTH 成功不影响其它连接
|
|
194
|
+
createRedisServer({ port: 6379, dataDir: './redis-data', password: 's3cret' }).listen();
|
|
180
195
|
```
|
|
181
196
|
|
|
197
|
+
设置了 `password` 后,未认证连接上的命令返回 `NOAUTH Authentication required.`;认证状态按连接隔离。
|
|
198
|
+
|
|
182
199
|
---
|
|
183
200
|
|
|
184
201
|
## Toolbox — everything included
|
|
@@ -202,6 +219,18 @@ createRedisServer({ port: 6379, dataDir: './redis-data' }).listen();
|
|
|
202
219
|
A zero-dependency HTTP management console: browse databases and tables, run SQL in the browser,
|
|
203
220
|
see results as a table. Perfect for dev tools, admin panels, and demos.
|
|
204
221
|
|
|
222
|
+
> **安全默认值(5.1.0)**:默认只监听 `127.0.0.1`(不再暴露到所有网卡)。生产环境请设置
|
|
223
|
+
> `authToken`,所有 `/api/*` 请求需携带 `Authorization: Bearer <token>`,未认证返回 401。
|
|
224
|
+
|
|
225
|
+
```js
|
|
226
|
+
const { WebUI } = require('jsql-neo');
|
|
227
|
+
const ui = new WebUI({ port: 8080, dataDir: './data', authToken: 'change-me' });
|
|
228
|
+
await ui.start();
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
可用选项:`host`(默认 `127.0.0.1`)、`port`、`dataDir`、`readonly`、`authToken`(Bearer 认证)、
|
|
232
|
+
`allowOrigin`(CORS 允许的源;不设置时开启认证仅回显请求 Origin,未开启认证时为 `*`)。
|
|
233
|
+
|
|
205
234
|
### Migration tools (`migrate`)
|
|
206
235
|
|
|
207
236
|
```js
|
|
@@ -210,6 +239,11 @@ await importDumpFile(db, './backup.sql', { strict: true }); // real mysqldump
|
|
|
210
239
|
await exportToFile(db, 'users', './users.csv'); // CSV round-trip
|
|
211
240
|
```
|
|
212
241
|
|
|
242
|
+
`importFromJSON` / `importDumpFile` 从 5.1.0 起:
|
|
243
|
+
|
|
244
|
+
- 兼容两种 JSON 形状:整库 `{ "users": { schema, rows } }` 与单表 `{ table, schema, rows }`。
|
|
245
|
+
- 目标表已存在时**默认抛错**,不会静默覆盖;需显式传入 `{ overwrite: true }` 才重建表。
|
|
246
|
+
|
|
213
247
|
### Browser playground
|
|
214
248
|
|
|
215
249
|
`examples/playground/` is a self-contained SQL sandbox — the **entire engine runs in your browser**
|
|
@@ -284,6 +318,6 @@ CI (`.github/workflows/ci.yml`): engine smoke tests on Node 18/20/22 + a full OR
|
|
|
284
318
|
|
|
285
319
|
## License
|
|
286
320
|
|
|
287
|
-
|
|
321
|
+
[Apache-2.0](LICENSE) — free to use, modify, and distribute with attribution.
|
|
288
322
|
|
|
289
323
|
*JSQL-NEO: Rust-powered. Protocol-native. One package.*
|
package/bin/jsql
CHANGED
|
@@ -23,10 +23,10 @@ const RAW_ARGV = process.argv.slice(2);
|
|
|
23
23
|
|
|
24
24
|
const cli = yaggs()
|
|
25
25
|
.usage('jsql <command> [options]')
|
|
26
|
-
.option('help', { alias: 'h', type: 'boolean', description: 'Show help' })
|
|
26
|
+
.option('help', { alias: ['h'], type: 'boolean', description: 'Show help' })
|
|
27
27
|
.command('mod', 'Module management', (sub) => {
|
|
28
28
|
sub.option('address', {
|
|
29
|
-
alias: 'a',
|
|
29
|
+
alias: ['a'],
|
|
30
30
|
type: 'string',
|
|
31
31
|
description: 'Path to module file'
|
|
32
32
|
});
|
|
@@ -149,7 +149,7 @@ const cli = yaggs()
|
|
|
149
149
|
} catch (e) { fail(e); }
|
|
150
150
|
})
|
|
151
151
|
.command('serve', 'Run the MySQL-compatible server in the foreground', (sub) => {
|
|
152
|
-
sub.option('port', { alias: 'p', type: 'number', description: 'Listen port (default 3306)' });
|
|
152
|
+
sub.option('port', { alias: ['p'], type: 'number', description: 'Listen port (default 3306)' });
|
|
153
153
|
sub.option('host', { type: 'string', description: 'Listen host (default 127.0.0.1)' });
|
|
154
154
|
sub.option('data-dir', { type: 'string', description: 'Directory to store databases' });
|
|
155
155
|
sub.option('no-auth', { type: 'boolean', description: 'Allow connections without authentication' });
|
|
@@ -194,26 +194,29 @@ const cli = yaggs()
|
|
|
194
194
|
} catch (e) { console.error(`Error: ${e.message}`); process.exitCode = 1; }
|
|
195
195
|
})
|
|
196
196
|
.command('ui', 'Serve the built-in web management console', (sub) => {
|
|
197
|
-
sub.option('port', { alias: 'p', type: 'number', description: 'HTTP port (default 8080)' });
|
|
198
|
-
sub.option('host', { type: 'string', description: 'Listen host (default
|
|
197
|
+
sub.option('port', { alias: ['p'], type: 'number', description: 'HTTP port (default 8080)' });
|
|
198
|
+
sub.option('host', { type: 'string', description: 'Listen host (default 127.0.0.1)' });
|
|
199
199
|
sub.option('data-dir', { type: 'string', description: 'Directory containing *.json databases (default .)' });
|
|
200
|
+
sub.option('auth-token', { type: 'string', description: 'Require this Bearer token for /api/*' });
|
|
200
201
|
sub.option('readonly', { type: 'boolean', description: 'Never write back to disk' });
|
|
201
202
|
}, async (argv) => {
|
|
202
203
|
const { WebUI } = require('../lib/web_ui');
|
|
203
204
|
const ui = new WebUI({
|
|
204
205
|
port: argv.port != null ? argv.port : 8080,
|
|
205
|
-
host: argv.host
|
|
206
|
+
host: argv.host,
|
|
206
207
|
dataDir: argv['data-dir'] || '.',
|
|
208
|
+
authToken: argv['auth-token'],
|
|
207
209
|
readonly: argv.readonly === true,
|
|
208
210
|
});
|
|
209
211
|
try {
|
|
210
212
|
const port = await ui.start();
|
|
211
|
-
|
|
213
|
+
const host = argv.host || ui.host;
|
|
214
|
+
console.log(`JSQL-NEO web UI on http://${host}:${port} (data: ${ui.dataDir})${ui.authToken ? ' [auth]' : ''}`);
|
|
212
215
|
setInterval(() => {}, 1 << 30);
|
|
213
216
|
} catch (e) { console.error(`Error: ${e.message}`); process.exitCode = 1; }
|
|
214
217
|
})
|
|
215
218
|
.command('redis', 'Run the Redis-compatible server', (sub) => {
|
|
216
|
-
sub.option('port', { alias: 'p', type: 'number', description: 'Listen port (default 6379)' });
|
|
219
|
+
sub.option('port', { alias: ['p'], type: 'number', description: 'Listen port (default 6379)' });
|
|
217
220
|
sub.option('host', { type: 'string', description: 'Listen host (default 127.0.0.1)' });
|
|
218
221
|
sub.option('data-dir', { type: 'string', description: 'Directory for data.rdb.json snapshot' });
|
|
219
222
|
sub.option('auth', { type: 'string', description: 'Require this password for AUTH' });
|
|
@@ -235,10 +238,10 @@ const cli = yaggs()
|
|
|
235
238
|
console.log(require('../package.json').version);
|
|
236
239
|
})
|
|
237
240
|
.command('server', 'Run the MySQL-compatible server in background', (sub) => {
|
|
238
|
-
sub.option('port', { alias: 'p', type: 'number', description: 'Listen port (default 3306)' });
|
|
241
|
+
sub.option('port', { alias: ['p'], type: 'number', description: 'Listen port (default 3306)' });
|
|
239
242
|
sub.option('host', { type: 'string', description: 'Listen host (default 127.0.0.1)' });
|
|
240
243
|
sub.option('data-dir', { type: 'string', description: 'Directory to store databases (default in-memory)' });
|
|
241
|
-
sub.option('auth', { alias: 'a', type: 'string', description: 'User credential: user:password[:db1,db2] (repeatable)' });
|
|
244
|
+
sub.option('auth', { alias: ['a'], type: 'string', description: 'User credential: user:password[:db1,db2] (repeatable)' });
|
|
242
245
|
sub.option('config', { type: 'string', description: 'Path to config file (JSON)' });
|
|
243
246
|
sub.option('no-auth', { type: 'boolean', description: 'Allow connections without authentication (local dev only)' });
|
|
244
247
|
}, (argv) => {
|
package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* JSQL-NEO
|
|
2
|
+
* JSQL-NEO v5.1.0 — Rust-Powered Embedded Database (WASM + HTTP)
|
|
3
3
|
*
|
|
4
4
|
* @example
|
|
5
5
|
* const jsql = require('jsql-neo');
|
|
@@ -46,6 +46,8 @@ function enableMySQLCompat() {
|
|
|
46
46
|
if (seen.has(resolved)) return;
|
|
47
47
|
if (!fs.existsSync(resolved)) return;
|
|
48
48
|
seen.add(resolved);
|
|
49
|
+
// 不覆盖已加载的真实 mysql2,避免全局劫持副作用
|
|
50
|
+
if (require.cache[resolved]) return;
|
|
49
51
|
require.cache[resolved] = { exports: mod, id: resolved, filename: resolved, loaded: true, children: [] };
|
|
50
52
|
} catch (e) { /* ignore */ }
|
|
51
53
|
};
|
package/lib/btree.js
CHANGED
|
@@ -153,17 +153,35 @@ class BTree {
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
/**
|
|
156
|
-
*
|
|
156
|
+
* 大于查询(严格开区间,不含 min 本身)
|
|
157
157
|
*/
|
|
158
158
|
greaterThan(min) {
|
|
159
|
-
|
|
159
|
+
const result = [];
|
|
160
|
+
let node = this._findLeaf(min);
|
|
161
|
+
while (node) {
|
|
162
|
+
for (let i = 0; i < node.keys.length; i++) {
|
|
163
|
+
if (node.keys[i] > min) result.push(...node.values[i]);
|
|
164
|
+
}
|
|
165
|
+
node = node.next;
|
|
166
|
+
}
|
|
167
|
+
return result;
|
|
160
168
|
}
|
|
161
169
|
|
|
162
170
|
/**
|
|
163
|
-
*
|
|
171
|
+
* 小于查询(严格开区间,不含 max 本身)
|
|
164
172
|
*/
|
|
165
173
|
lessThan(max) {
|
|
166
|
-
|
|
174
|
+
const result = [];
|
|
175
|
+
let node = this._root;
|
|
176
|
+
while (!node.leaf) node = node.children[0];
|
|
177
|
+
while (node) {
|
|
178
|
+
for (let i = 0; i < node.keys.length; i++) {
|
|
179
|
+
if (node.keys[i] < max) result.push(...node.values[i]);
|
|
180
|
+
else return result;
|
|
181
|
+
}
|
|
182
|
+
node = node.next;
|
|
183
|
+
}
|
|
184
|
+
return result;
|
|
167
185
|
}
|
|
168
186
|
|
|
169
187
|
_findLeaf(key) {
|
package/lib/database.js
CHANGED
|
@@ -12,6 +12,25 @@ const Table = require('./table');
|
|
|
12
12
|
const { createError } = require('./errors');
|
|
13
13
|
const JSQLFormat = require('./jsql_format');
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* 解析字段简写类型字符串,如 'integer primary key'、'string unique'、
|
|
17
|
+
* 'integer primary key auto_increment'、'datetime default now'
|
|
18
|
+
* @param {string} str - 简写字符串(已小写化)
|
|
19
|
+
* @returns {object} { type, primaryKey, unique, autoIncrement, required, ... }
|
|
20
|
+
*/
|
|
21
|
+
function parseFieldShorthand(str) {
|
|
22
|
+
const def = { type: str };
|
|
23
|
+
if (/\bprimary\s+key\b/.test(str)) { def.primaryKey = true; def.type = str.replace(/\bprimary\s+key\b/g, '').trim() || def.type; }
|
|
24
|
+
if (/\bauto_?increment\b/.test(str)) { def.autoIncrement = true; def.type = str.replace(/\bauto_?increment\b/g, '').trim() || def.type; }
|
|
25
|
+
if (/\bnot\s+null\b/.test(str)) { def.required = true; def.type = str.replace(/\bnot\s+null\b/g, '').trim() || def.type; }
|
|
26
|
+
if (/\bunique\b/.test(str)) { def.unique = true; def.type = str.replace(/\bunique\b/g, '').trim() || def.type; }
|
|
27
|
+
if (/\bdefault\s+(\S+)/.test(str)) {
|
|
28
|
+
def.default = RegExp.$1.replace(/^['"]|['"]$/g, '');
|
|
29
|
+
def.type = str.replace(/\bdefault\s+\S+/g, '').trim() || def.type;
|
|
30
|
+
}
|
|
31
|
+
return def;
|
|
32
|
+
}
|
|
33
|
+
|
|
15
34
|
class Database {
|
|
16
35
|
/**
|
|
17
36
|
* @param {string} filePath - 数据库文件/目录路径,传 null 或 ':memory:' 使用内存模式
|
|
@@ -423,7 +442,7 @@ class Database {
|
|
|
423
442
|
}
|
|
424
443
|
const normalized = {};
|
|
425
444
|
for (const [field, def] of Object.entries(schema || {})) {
|
|
426
|
-
if (typeof def === 'string') normalized[field] =
|
|
445
|
+
if (typeof def === 'string') normalized[field] = parseFieldShorthand(def.toLowerCase());
|
|
427
446
|
else normalized[field] = def;
|
|
428
447
|
}
|
|
429
448
|
const table = new Table(name, normalized, this);
|
|
@@ -872,9 +891,9 @@ class Database {
|
|
|
872
891
|
};
|
|
873
892
|
|
|
874
893
|
if (level === 'REPEATABLE_READ') {
|
|
875
|
-
//
|
|
894
|
+
// 快照:保存当前所有数据(深拷贝,保证嵌套对象可回滚)
|
|
876
895
|
for (const [name, table] of Object.entries(this._tables)) {
|
|
877
|
-
this._transaction.rows[name] = table._rows
|
|
896
|
+
this._transaction.rows[name] = JSON.parse(JSON.stringify(table._rows));
|
|
878
897
|
this._transaction.autoIncrements[name] = table._autoIncrement;
|
|
879
898
|
}
|
|
880
899
|
} else {
|
|
@@ -1191,7 +1210,7 @@ class Database {
|
|
|
1191
1210
|
const ids = [];
|
|
1192
1211
|
for (const row of arr) {
|
|
1193
1212
|
const r = table.insert(row);
|
|
1194
|
-
ids.push(pk ? r[pk] :
|
|
1213
|
+
ids.push(pk ? r[pk] : table._rows.length);
|
|
1195
1214
|
}
|
|
1196
1215
|
this._emit('insert', { table: tableName, count: arr.length, ids });
|
|
1197
1216
|
this._runHooks('afterInsert', [tableName, arr, ids]);
|
|
@@ -1221,9 +1240,10 @@ class Database {
|
|
|
1221
1240
|
const table = this._ensureTable(tableName);
|
|
1222
1241
|
if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
|
|
1223
1242
|
if (!this._runHooks('beforeFind', [tableName, { filter, opts }])) return [];
|
|
1224
|
-
const { limit
|
|
1243
|
+
const { limit, offset = 0 } = opts;
|
|
1225
1244
|
let rows = table._applyFilter(table._rows, filter || {});
|
|
1226
|
-
rows = rows.slice(offset, offset + limit);
|
|
1245
|
+
if (limit > 0) rows = rows.slice(offset, offset + limit);
|
|
1246
|
+
else if (offset > 0) rows = rows.slice(offset);
|
|
1227
1247
|
this._touchTable(tableName);
|
|
1228
1248
|
this._runHooks('afterFind', [tableName, { filter, opts }, rows]);
|
|
1229
1249
|
return rows;
|
|
@@ -1296,9 +1316,20 @@ class Database {
|
|
|
1296
1316
|
const table = this._ensureTable(tableName);
|
|
1297
1317
|
if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
|
|
1298
1318
|
if (!this._runHooks('beforeDelete', [tableName, id])) return;
|
|
1319
|
+
if (table._primaryKey) {
|
|
1320
|
+
const removed = table.removeById(id);
|
|
1321
|
+
if (removed === 0) return { ok: false, error: 'not found' };
|
|
1322
|
+
this._emit('delete', { table: tableName, id });
|
|
1323
|
+
this._runHooks('afterDelete', [tableName, id, { ok: true }]);
|
|
1324
|
+
this._markDirty(tableName);
|
|
1325
|
+
return { ok: true };
|
|
1326
|
+
}
|
|
1299
1327
|
const idx = table._rows.findIndex(r => r === this._resolveId(table, id));
|
|
1300
1328
|
if (idx === -1) return { ok: false, error: 'not found' };
|
|
1301
1329
|
table._rows.splice(idx, 1);
|
|
1330
|
+
table._rebuildPKIndex();
|
|
1331
|
+
table._rebuildAllBTrees();
|
|
1332
|
+
for (const field of Object.keys(table._indexes)) table.createIndex(field);
|
|
1302
1333
|
this._emit('delete', { table: tableName, id });
|
|
1303
1334
|
this._runHooks('afterDelete', [tableName, id, { ok: true }]);
|
|
1304
1335
|
this._markDirty(tableName);
|
|
@@ -1310,12 +1341,19 @@ class Database {
|
|
|
1310
1341
|
if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
|
|
1311
1342
|
if (!this._runHooks('beforeDelete', [tableName, ids])) return;
|
|
1312
1343
|
let removed = 0;
|
|
1313
|
-
|
|
1314
|
-
const
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1344
|
+
if (table._primaryKey) {
|
|
1345
|
+
for (const id of ids) removed += table.removeById(id);
|
|
1346
|
+
} else {
|
|
1347
|
+
for (const id of ids) {
|
|
1348
|
+
const idx = table._rows.findIndex(r => r === this._resolveId(table, id));
|
|
1349
|
+
if (idx !== -1) {
|
|
1350
|
+
table._rows.splice(idx, 1);
|
|
1351
|
+
removed++;
|
|
1352
|
+
}
|
|
1318
1353
|
}
|
|
1354
|
+
table._rebuildPKIndex();
|
|
1355
|
+
table._rebuildAllBTrees();
|
|
1356
|
+
for (const field of Object.keys(table._indexes)) table.createIndex(field);
|
|
1319
1357
|
}
|
|
1320
1358
|
this._emit('delete', { table: tableName, ids, result: { ok: true, count: removed } });
|
|
1321
1359
|
this._runHooks('afterDelete', [tableName, ids, { ok: true, count: removed }]);
|
|
@@ -1679,4 +1717,5 @@ class Database {
|
|
|
1679
1717
|
}
|
|
1680
1718
|
}
|
|
1681
1719
|
|
|
1682
|
-
module.exports = Database;
|
|
1720
|
+
module.exports = Database;
|
|
1721
|
+
module.exports.parseFieldShorthand = parseFieldShorthand;
|
package/lib/migrate.js
CHANGED
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const { splitStatements, executeSQL } = require('./sql');
|
|
13
|
+
const { parseFieldShorthand } = require('./database');
|
|
13
14
|
|
|
14
15
|
function normalizeSchema(schema) {
|
|
15
16
|
const out = {};
|
|
16
17
|
for (const [name, def] of Object.entries(schema || {})) {
|
|
17
|
-
const d = typeof def === 'string' ?
|
|
18
|
+
const d = typeof def === 'string' ? parseFieldShorthand(def.toLowerCase()) : { ...def };
|
|
18
19
|
if (!d.type) d.type = typeof d === 'object' ? 'any' : 'string';
|
|
19
20
|
if (d.type === 'int' || d.type === 'bigint' || d.type === 'smallint' || d.type === 'tinyint') d.type = 'integer';
|
|
20
21
|
if (d.type === 'varchar' || d.type === 'text' || d.type === 'char') d.type = 'string';
|
|
@@ -120,13 +121,24 @@ async function exportAllToJSON(engine, tables) {
|
|
|
120
121
|
return out;
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
async function importFromJSON(engine, data) {
|
|
124
|
-
const
|
|
124
|
+
async function importFromJSON(engine, data, opts = {}) {
|
|
125
|
+
const overwrite = !!(opts && opts.overwrite);
|
|
126
|
+
let tables = typeof data === 'string' ? JSON.parse(data) : data;
|
|
127
|
+
// 兼容单表形状:exportTableToJSON 返回 { table, schema, rows }
|
|
128
|
+
if (tables && !Array.isArray(tables) && typeof tables === 'object'
|
|
129
|
+
&& tables.table && tables.schema && !tables[tables.table]) {
|
|
130
|
+
const single = tables;
|
|
131
|
+
tables = {};
|
|
132
|
+
tables[single.table] = single;
|
|
133
|
+
}
|
|
125
134
|
const created = [];
|
|
126
135
|
let inserted = 0;
|
|
127
136
|
for (const [name, t] of Object.entries(tables)) {
|
|
128
137
|
if (!t || !t.schema) continue;
|
|
129
|
-
if (engine.hasTable(name))
|
|
138
|
+
if (engine.hasTable(name)) {
|
|
139
|
+
if (!overwrite) throw new Error(`Table '${name}' already exists; pass { overwrite: true } to replace it`);
|
|
140
|
+
await engine.dropTable(name);
|
|
141
|
+
}
|
|
130
142
|
await engine.createTable(name, normalizeSchema(t.schema));
|
|
131
143
|
created.push(name);
|
|
132
144
|
if (Array.isArray(t.rows) && t.rows.length > 0) {
|
package/lib/mysql_compat.js
CHANGED
|
@@ -241,7 +241,8 @@ class Pool {
|
|
|
241
241
|
const key = database || 'default';
|
|
242
242
|
let engine = this._sharedEngines.get(key);
|
|
243
243
|
if (!engine) {
|
|
244
|
-
|
|
244
|
+
const filename = (this.config && this.config.filename) || null;
|
|
245
|
+
engine = filename ? new Database(filename) : new Database(':memory:');
|
|
245
246
|
if (typeof engine.start === 'function') await engine.start();
|
|
246
247
|
this._sharedEngines.set(key, engine);
|
|
247
248
|
}
|
package/lib/mysql_server.js
CHANGED
|
@@ -843,6 +843,10 @@ class MysqlConnection {
|
|
|
843
843
|
continue;
|
|
844
844
|
}
|
|
845
845
|
case 'dropDatabase': {
|
|
846
|
+
if (!this.server._canAccessDb(this.user, stmt.database)) {
|
|
847
|
+
this._send(errPacket(1044, `Access denied for user '${this.user}' to database '${stmt.database}'`));
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
846
850
|
await this.server.dropDatabase(stmt.database, { ifExists: stmt.ifExists });
|
|
847
851
|
this._send(okPacket());
|
|
848
852
|
continue;
|
|
@@ -949,6 +953,11 @@ class MysqlServer {
|
|
|
949
953
|
// 决定执行引擎,并把 db.table 改写为 table 后交给对应库引擎执行。
|
|
950
954
|
async _route(stmt, sql, currentDb, user) {
|
|
951
955
|
if (stmt.type === 'showTables' && stmt.database) {
|
|
956
|
+
if (user && this.auth && !this._canAccessDb(user, stmt.database)) {
|
|
957
|
+
const err = new Error(`Access denied for user '${user}' to database '${stmt.database}'`);
|
|
958
|
+
err.code = 1044;
|
|
959
|
+
throw err;
|
|
960
|
+
}
|
|
952
961
|
return { engine: await this._getDatabase(stmt.database), sql };
|
|
953
962
|
}
|
|
954
963
|
const tables = [];
|
|
@@ -983,6 +992,11 @@ class MysqlServer {
|
|
|
983
992
|
}
|
|
984
993
|
}
|
|
985
994
|
if (db) {
|
|
995
|
+
if (user && this.auth && !this._canAccessDb(user, db)) {
|
|
996
|
+
const err = new Error(`Access denied for user '${user}' to database '${db}'`);
|
|
997
|
+
err.code = 1044;
|
|
998
|
+
throw err;
|
|
999
|
+
}
|
|
986
1000
|
if (!this._dbExists(db)) await this.createDatabase(db);
|
|
987
1001
|
const engine = await this._getDatabase(db);
|
|
988
1002
|
let s = sql;
|
package/lib/native_client.js
CHANGED
|
@@ -70,12 +70,59 @@ function restoreRow(row, schema) {
|
|
|
70
70
|
return out;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
function _encodedFieldSize(v) {
|
|
74
|
+
if (v === null || v === undefined) return 1;
|
|
75
|
+
if (typeof v === 'number') {
|
|
76
|
+
if (Number.isInteger(v)) return (v >= -2147483648 && v <= 2147483647) ? 1 + 4 : 1 + 8;
|
|
77
|
+
return 1 + 8;
|
|
78
|
+
}
|
|
79
|
+
if (typeof v === 'boolean') return 1 + 1;
|
|
80
|
+
const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
81
|
+
return 1 + 4 + Buffer.byteLength(s, 'utf8');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function encodeField(buf, off, v) {
|
|
85
|
+
if (v === null || v === undefined) {
|
|
86
|
+
buf.writeUInt8(0, off); return off + 1;
|
|
87
|
+
}
|
|
88
|
+
if (typeof v === 'number') {
|
|
89
|
+
if (Number.isInteger(v)) {
|
|
90
|
+
if (v >= -2147483648 && v <= 2147483647) {
|
|
91
|
+
buf.writeUInt8(INT32_TAG, off); buf.writeInt32LE(v, off + 1); return off + 5;
|
|
92
|
+
}
|
|
93
|
+
buf.writeUInt8(INT64_TAG, off); buf.writeBigInt64LE(BigInt(v), off + 1); return off + 9;
|
|
94
|
+
}
|
|
95
|
+
buf.writeUInt8(FLOAT_TAG, off); buf.writeDoubleLE(v, off + 1); return off + 9;
|
|
96
|
+
}
|
|
97
|
+
if (typeof v === 'string') {
|
|
98
|
+
const sl = Buffer.byteLength(v, 'utf8');
|
|
99
|
+
buf.writeUInt8(STR_TAG, off);
|
|
100
|
+
buf.writeUInt32LE(sl, off + 1);
|
|
101
|
+
buf.write(v, off + 5, sl, 'utf8');
|
|
102
|
+
return off + 5 + sl;
|
|
103
|
+
}
|
|
104
|
+
if (typeof v === 'boolean') {
|
|
105
|
+
buf.writeUInt8(BOOL_TAG, off); buf.writeUInt8(v ? 1 : 0, off + 1); return off + 2;
|
|
106
|
+
}
|
|
107
|
+
const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
108
|
+
const sl = Buffer.byteLength(s, 'utf8');
|
|
109
|
+
buf.writeUInt8(STR_TAG, off);
|
|
110
|
+
buf.writeUInt32LE(sl, off + 1);
|
|
111
|
+
buf.write(s, off + 5, sl, 'utf8');
|
|
112
|
+
return off + 5 + sl;
|
|
113
|
+
}
|
|
114
|
+
|
|
73
115
|
function encodeBatch(rows) {
|
|
74
116
|
if (rows.length === 0) return new Uint8Array(0);
|
|
75
117
|
const fieldNames = Object.keys(rows[0]);
|
|
76
118
|
const nFields = fieldNames.length;
|
|
77
|
-
|
|
78
|
-
const
|
|
119
|
+
let size = 1;
|
|
120
|
+
for (const s of fieldNames) size += 1 + Buffer.byteLength(s, 'utf8');
|
|
121
|
+
size += 4;
|
|
122
|
+
for (const row of rows) {
|
|
123
|
+
for (let fi = 0; fi < nFields; fi++) size += _encodedFieldSize(row[fieldNames[fi]]);
|
|
124
|
+
}
|
|
125
|
+
const buf = Buffer.allocUnsafe(size);
|
|
79
126
|
let off = 0;
|
|
80
127
|
|
|
81
128
|
off = buf.writeUInt8(nFields, off);
|
|
@@ -89,132 +136,8 @@ function encodeBatch(rows) {
|
|
|
89
136
|
|
|
90
137
|
for (let ri = 0; ri < rows.length; ri++) {
|
|
91
138
|
const row = rows[ri];
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const v = row[fieldNames[0]];
|
|
95
|
-
if (v === null || v === undefined) { off = buf.writeUInt8(0, off); }
|
|
96
|
-
else if (typeof v === 'number') {
|
|
97
|
-
if (Number.isInteger(v)) { if (v >= -2147483648 && v <= 2147483647) { off = buf.writeUInt8(INT32_TAG, off); off = buf.writeInt32LE(v, off); } else { off = buf.writeUInt8(INT64_TAG, off); off = buf.writeBigInt64LE(BigInt(v), off); } }
|
|
98
|
-
else { off = buf.writeUInt8(FLOAT_TAG, off); off = buf.writeDoubleLE(v, off); }
|
|
99
|
-
} else if (typeof v === 'string') {
|
|
100
|
-
off = buf.writeUInt8(STR_TAG, off);
|
|
101
|
-
const sl = Buffer.byteLength(v, 'utf8');
|
|
102
|
-
off = buf.writeUInt32LE(sl, off);
|
|
103
|
-
off += buf.write(v, off, sl, 'utf8');
|
|
104
|
-
} else if (typeof v === 'boolean') {
|
|
105
|
-
off = buf.writeUInt8(BOOL_TAG, off); off = buf.writeUInt8(v ? 1 : 0, off);
|
|
106
|
-
} else {
|
|
107
|
-
const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
108
|
-
off = buf.writeUInt8(STR_TAG, off);
|
|
109
|
-
const sl = Buffer.byteLength(s, 'utf8');
|
|
110
|
-
off = buf.writeUInt32LE(sl, off);
|
|
111
|
-
off += buf.write(s, off, sl, 'utf8');
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
if (nFields >= 2) {
|
|
115
|
-
const v = row[fieldNames[1]];
|
|
116
|
-
if (v === null || v === undefined) { off = buf.writeUInt8(0, off); }
|
|
117
|
-
else if (typeof v === 'number') {
|
|
118
|
-
if (Number.isInteger(v)) { if (v >= -2147483648 && v <= 2147483647) { off = buf.writeUInt8(INT32_TAG, off); off = buf.writeInt32LE(v, off); } else { off = buf.writeUInt8(INT64_TAG, off); off = buf.writeBigInt64LE(BigInt(v), off); } }
|
|
119
|
-
else { off = buf.writeUInt8(FLOAT_TAG, off); off = buf.writeDoubleLE(v, off); }
|
|
120
|
-
} else if (typeof v === 'string') {
|
|
121
|
-
off = buf.writeUInt8(STR_TAG, off);
|
|
122
|
-
const sl = Buffer.byteLength(v, 'utf8');
|
|
123
|
-
off = buf.writeUInt32LE(sl, off);
|
|
124
|
-
off += buf.write(v, off, sl, 'utf8');
|
|
125
|
-
} else if (typeof v === 'boolean') {
|
|
126
|
-
off = buf.writeUInt8(BOOL_TAG, off); off = buf.writeUInt8(v ? 1 : 0, off);
|
|
127
|
-
} else {
|
|
128
|
-
const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
129
|
-
off = buf.writeUInt8(STR_TAG, off);
|
|
130
|
-
const sl = Buffer.byteLength(s, 'utf8');
|
|
131
|
-
off = buf.writeUInt32LE(sl, off);
|
|
132
|
-
off += buf.write(s, off, sl, 'utf8');
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
if (nFields >= 3) {
|
|
136
|
-
const v = row[fieldNames[2]];
|
|
137
|
-
if (v === null || v === undefined) { off = buf.writeUInt8(0, off); }
|
|
138
|
-
else if (typeof v === 'number') {
|
|
139
|
-
if (Number.isInteger(v)) { if (v >= -2147483648 && v <= 2147483647) { off = buf.writeUInt8(INT32_TAG, off); off = buf.writeInt32LE(v, off); } else { off = buf.writeUInt8(INT64_TAG, off); off = buf.writeBigInt64LE(BigInt(v), off); } }
|
|
140
|
-
else { off = buf.writeUInt8(FLOAT_TAG, off); off = buf.writeDoubleLE(v, off); }
|
|
141
|
-
} else if (typeof v === 'string') {
|
|
142
|
-
off = buf.writeUInt8(STR_TAG, off);
|
|
143
|
-
const sl = Buffer.byteLength(v, 'utf8');
|
|
144
|
-
off = buf.writeUInt32LE(sl, off);
|
|
145
|
-
off += buf.write(v, off, sl, 'utf8');
|
|
146
|
-
} else if (typeof v === 'boolean') {
|
|
147
|
-
off = buf.writeUInt8(BOOL_TAG, off); off = buf.writeUInt8(v ? 1 : 0, off);
|
|
148
|
-
} else {
|
|
149
|
-
const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
150
|
-
off = buf.writeUInt8(STR_TAG, off);
|
|
151
|
-
const sl = Buffer.byteLength(s, 'utf8');
|
|
152
|
-
off = buf.writeUInt32LE(sl, off);
|
|
153
|
-
off += buf.write(s, off, sl, 'utf8');
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
if (nFields >= 4) {
|
|
157
|
-
const v = row[fieldNames[3]];
|
|
158
|
-
if (v === null || v === undefined) { off = buf.writeUInt8(0, off); }
|
|
159
|
-
else if (typeof v === 'number') {
|
|
160
|
-
if (Number.isInteger(v)) { if (v >= -2147483648 && v <= 2147483647) { off = buf.writeUInt8(INT32_TAG, off); off = buf.writeInt32LE(v, off); } else { off = buf.writeUInt8(INT64_TAG, off); off = buf.writeBigInt64LE(BigInt(v), off); } }
|
|
161
|
-
else { off = buf.writeUInt8(FLOAT_TAG, off); off = buf.writeDoubleLE(v, off); }
|
|
162
|
-
} else if (typeof v === 'string') {
|
|
163
|
-
off = buf.writeUInt8(STR_TAG, off);
|
|
164
|
-
const sl = Buffer.byteLength(v, 'utf8');
|
|
165
|
-
off = buf.writeUInt32LE(sl, off);
|
|
166
|
-
off += buf.write(v, off, sl, 'utf8');
|
|
167
|
-
} else if (typeof v === 'boolean') {
|
|
168
|
-
off = buf.writeUInt8(BOOL_TAG, off); off = buf.writeUInt8(v ? 1 : 0, off);
|
|
169
|
-
} else {
|
|
170
|
-
const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
171
|
-
off = buf.writeUInt8(STR_TAG, off);
|
|
172
|
-
const sl = Buffer.byteLength(s, 'utf8');
|
|
173
|
-
off = buf.writeUInt32LE(sl, off);
|
|
174
|
-
off += buf.write(s, off, sl, 'utf8');
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
if (nFields >= 5) {
|
|
178
|
-
const v = row[fieldNames[4]];
|
|
179
|
-
if (v === null || v === undefined) { off = buf.writeUInt8(0, off); }
|
|
180
|
-
else if (typeof v === 'number') {
|
|
181
|
-
if (Number.isInteger(v)) { if (v >= -2147483648 && v <= 2147483647) { off = buf.writeUInt8(INT32_TAG, off); off = buf.writeInt32LE(v, off); } else { off = buf.writeUInt8(INT64_TAG, off); off = buf.writeBigInt64LE(BigInt(v), off); } }
|
|
182
|
-
else { off = buf.writeUInt8(FLOAT_TAG, off); off = buf.writeDoubleLE(v, off); }
|
|
183
|
-
} else if (typeof v === 'string') {
|
|
184
|
-
off = buf.writeUInt8(STR_TAG, off);
|
|
185
|
-
const sl = Buffer.byteLength(v, 'utf8');
|
|
186
|
-
off = buf.writeUInt32LE(sl, off);
|
|
187
|
-
off += buf.write(v, off, sl, 'utf8');
|
|
188
|
-
} else if (typeof v === 'boolean') {
|
|
189
|
-
off = buf.writeUInt8(BOOL_TAG, off); off = buf.writeUInt8(v ? 1 : 0, off);
|
|
190
|
-
} else {
|
|
191
|
-
const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
192
|
-
off = buf.writeUInt8(STR_TAG, off);
|
|
193
|
-
const sl = Buffer.byteLength(s, 'utf8');
|
|
194
|
-
off = buf.writeUInt32LE(sl, off);
|
|
195
|
-
off += buf.write(s, off, sl, 'utf8');
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
for (let fi = 5; fi < nFields; fi++) {
|
|
199
|
-
const v = row[fieldNames[fi]];
|
|
200
|
-
if (v === null || v === undefined) { off = buf.writeUInt8(0, off); }
|
|
201
|
-
else if (typeof v === 'number') {
|
|
202
|
-
if (Number.isInteger(v)) { if (v >= -2147483648 && v <= 2147483647) { off = buf.writeUInt8(INT32_TAG, off); off = buf.writeInt32LE(v, off); } else { off = buf.writeUInt8(INT64_TAG, off); off = buf.writeBigInt64LE(BigInt(v), off); } }
|
|
203
|
-
else { off = buf.writeUInt8(FLOAT_TAG, off); off = buf.writeDoubleLE(v, off); }
|
|
204
|
-
} else if (typeof v === 'string') {
|
|
205
|
-
off = buf.writeUInt8(STR_TAG, off);
|
|
206
|
-
const sl = Buffer.byteLength(v, 'utf8');
|
|
207
|
-
off = buf.writeUInt32LE(sl, off);
|
|
208
|
-
off += buf.write(v, off, sl, 'utf8');
|
|
209
|
-
} else if (typeof v === 'boolean') {
|
|
210
|
-
off = buf.writeUInt8(BOOL_TAG, off); off = buf.writeUInt8(v ? 1 : 0, off);
|
|
211
|
-
} else {
|
|
212
|
-
const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
213
|
-
off = buf.writeUInt8(STR_TAG, off);
|
|
214
|
-
const sl = Buffer.byteLength(s, 'utf8');
|
|
215
|
-
off = buf.writeUInt32LE(sl, off);
|
|
216
|
-
off += buf.write(s, off, sl, 'utf8');
|
|
217
|
-
}
|
|
139
|
+
for (let fi = 0; fi < nFields; fi++) {
|
|
140
|
+
off = encodeField(buf, off, row[fieldNames[fi]]);
|
|
218
141
|
}
|
|
219
142
|
}
|
|
220
143
|
|
package/lib/redis_server.js
CHANGED
|
@@ -39,6 +39,7 @@ class RedisServer {
|
|
|
39
39
|
this.onQuery = opts.onQuery || null;
|
|
40
40
|
this.db = new Map();
|
|
41
41
|
this.snapshotTimer = null;
|
|
42
|
+
this._authedSockets = new WeakMap();
|
|
42
43
|
this._load();
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -359,6 +360,7 @@ class RedisServer {
|
|
|
359
360
|
listen() {
|
|
360
361
|
this.server = net.createServer((socket) => {
|
|
361
362
|
let buf = Buffer.alloc(0);
|
|
363
|
+
this._authedSockets.set(socket, false);
|
|
362
364
|
socket.setEncoding('utf8');
|
|
363
365
|
socket.on('data', (chunk) => {
|
|
364
366
|
buf += chunk;
|
|
@@ -407,17 +409,21 @@ class RedisServer {
|
|
|
407
409
|
try {
|
|
408
410
|
if (this.onQuery) this.onQuery(cmd, args);
|
|
409
411
|
if (cmd === 'AUTH') {
|
|
410
|
-
if (!this.password) return this._send(socket,
|
|
412
|
+
if (!this.password) return this._send(socket, new Error('ERR Client sent AUTH, but no password is set'));
|
|
411
413
|
}
|
|
412
|
-
if (this.password && !this.
|
|
413
|
-
if (cmd === 'AUTH') {
|
|
414
|
-
|
|
414
|
+
if (this.password && !this._authedSockets.get(socket)) {
|
|
415
|
+
if (cmd === 'AUTH') {
|
|
416
|
+
const ok = this.execute('AUTH', args);
|
|
417
|
+
if (ok === 'OK') this._authedSockets.set(socket, true);
|
|
418
|
+
return this._send(socket, ok);
|
|
419
|
+
}
|
|
420
|
+
return this._send(socket, new Error('NOAUTH Authentication required.'));
|
|
415
421
|
}
|
|
416
422
|
if (cmd === 'QUIT') { this._send(socket, 'OK'); socket.end(); return; }
|
|
417
423
|
const r = this.execute(cmd, args);
|
|
418
424
|
this._send(socket, r);
|
|
419
425
|
} catch (e) {
|
|
420
|
-
this._send(socket,
|
|
426
|
+
this._send(socket, e);
|
|
421
427
|
}
|
|
422
428
|
}
|
|
423
429
|
|
package/lib/sqlite_worker.js
CHANGED
|
@@ -208,7 +208,7 @@ async function handleOp(op, args) {
|
|
|
208
208
|
case 'deserialize': {
|
|
209
209
|
const { importFromJSON } = require('./migrate');
|
|
210
210
|
const dump = JSON.parse(Buffer.from(args[0]).toString('utf8'));
|
|
211
|
-
await importFromJSON(db, dump);
|
|
211
|
+
await importFromJSON(db, dump, { overwrite: true });
|
|
212
212
|
if (typeof db.save === 'function') db.save();
|
|
213
213
|
result = { ok: true };
|
|
214
214
|
break;
|
package/lib/table.js
CHANGED
|
@@ -487,6 +487,14 @@ class Table {
|
|
|
487
487
|
tree.insert(val, idx);
|
|
488
488
|
}
|
|
489
489
|
}
|
|
490
|
+
for (const field of Object.keys(this._indexes)) {
|
|
491
|
+
const val = lastRow[field];
|
|
492
|
+
if (val !== undefined && this._indexes[field].has(val)) {
|
|
493
|
+
const positions = this._indexes[field].get(val);
|
|
494
|
+
const pi = positions.indexOf(lastIdx);
|
|
495
|
+
if (pi !== -1) positions[pi] = idx;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
490
498
|
}
|
|
491
499
|
this._rows.pop();
|
|
492
500
|
}
|
|
@@ -534,6 +542,14 @@ class Table {
|
|
|
534
542
|
tree.insert(val, rowIndex);
|
|
535
543
|
}
|
|
536
544
|
}
|
|
545
|
+
for (const field of Object.keys(this._indexes)) {
|
|
546
|
+
const val = lastRow[field];
|
|
547
|
+
if (val !== undefined && this._indexes[field].has(val)) {
|
|
548
|
+
const positions = this._indexes[field].get(val);
|
|
549
|
+
const pi = positions.indexOf(lastIdx);
|
|
550
|
+
if (pi !== -1) positions[pi] = rowIndex;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
537
553
|
}
|
|
538
554
|
this._rows.pop();
|
|
539
555
|
this._dirty = true;
|
|
@@ -834,6 +850,12 @@ class Table {
|
|
|
834
850
|
}
|
|
835
851
|
return [];
|
|
836
852
|
}
|
|
853
|
+
if (this._indexes[field] && this._indexes[field].has(condition)) {
|
|
854
|
+
const indices = this._indexes[field].get(condition);
|
|
855
|
+
const subset = indices.map(i => this._rows[i]).filter(r => r);
|
|
856
|
+
return this._applyFilter(subset, query);
|
|
857
|
+
}
|
|
858
|
+
if (this._indexes[field]) return [];
|
|
837
859
|
}
|
|
838
860
|
// 范围查询用 B-Tree
|
|
839
861
|
if (condition && typeof condition === 'object') {
|
|
@@ -912,8 +934,15 @@ class Table {
|
|
|
912
934
|
case '$nin': return Array.isArray(target) && !target.includes(value);
|
|
913
935
|
case '$like':
|
|
914
936
|
if (typeof value !== 'string' || typeof target !== 'string') return false;
|
|
915
|
-
|
|
937
|
+
{
|
|
938
|
+
const escaped = target
|
|
939
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
940
|
+
.replace(/%/g, '.*')
|
|
941
|
+
.replace(/_/g, '.');
|
|
942
|
+
return new RegExp('^' + escaped + '$', 'i').test(value);
|
|
943
|
+
}
|
|
916
944
|
case '$regex':
|
|
945
|
+
if (target instanceof RegExp) return target.test(String(value));
|
|
917
946
|
if (!isSafeRegex(target)) throw createError('ER_BAD_REGEX', String(target));
|
|
918
947
|
return new RegExp(target).test(String(value));
|
|
919
948
|
case '$exists':
|
|
@@ -992,6 +1021,23 @@ class Table {
|
|
|
992
1021
|
throw createError('ER_BAD_NULL_ERROR', field);
|
|
993
1022
|
}
|
|
994
1023
|
|
|
1024
|
+
// PRIMARY KEY 唯一性
|
|
1025
|
+
if ((def.primaryKey || def.primary === true) && data[field] !== undefined && data[field] !== null) {
|
|
1026
|
+
let dup = false;
|
|
1027
|
+
if (uniqueTracker && uniqueTracker[field]) {
|
|
1028
|
+
dup = uniqueTracker[field].has(data[field]);
|
|
1029
|
+
} else if (this._pkIndex) {
|
|
1030
|
+
const idx = this._pkIndex.get(data[field]);
|
|
1031
|
+
dup = idx !== undefined && idx !== null && idx !== excludeIndex;
|
|
1032
|
+
} else {
|
|
1033
|
+
const idx = this._rows.findIndex(r => r[field] === data[field]);
|
|
1034
|
+
dup = idx !== -1 && idx !== excludeIndex;
|
|
1035
|
+
}
|
|
1036
|
+
if (dup) {
|
|
1037
|
+
throw createError('ER_DUP_ENTRY', String(data[field]), field);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
995
1041
|
// UNIQUE(B-Tree 优化 O(log n))
|
|
996
1042
|
if (def.unique && data[field] !== undefined) {
|
|
997
1043
|
if (uniqueTracker && uniqueTracker[field]) {
|
|
@@ -1088,6 +1134,42 @@ class Table {
|
|
|
1088
1134
|
result[field] = validateDateType(data[field], type, field);
|
|
1089
1135
|
}
|
|
1090
1136
|
}
|
|
1137
|
+
for (const [field, def] of Object.entries(this._schema)) {
|
|
1138
|
+
if (field === '_softDelete') continue;
|
|
1139
|
+
if (partial && data[field] === undefined) continue;
|
|
1140
|
+
const v = data[field];
|
|
1141
|
+
if (v === undefined || v === null) continue;
|
|
1142
|
+
const t = String(def.type || '').toLowerCase();
|
|
1143
|
+
if (t === 'integer' || t === 'int' || t === 'bigint') {
|
|
1144
|
+
if (typeof v === 'boolean') throw createError('ER_TRUNCATED_WRONG_VALUE', t, v);
|
|
1145
|
+
if (typeof v === 'number') {
|
|
1146
|
+
if (Number.isInteger(v)) result[field] = v;
|
|
1147
|
+
else throw createError('ER_TRUNCATED_WRONG_VALUE', t, v);
|
|
1148
|
+
} else if (typeof v === 'string' && v.trim() !== '' && Number.isInteger(Number(v))) {
|
|
1149
|
+
result[field] = Number(v);
|
|
1150
|
+
} else {
|
|
1151
|
+
throw createError('ER_TRUNCATED_WRONG_VALUE', t, v);
|
|
1152
|
+
}
|
|
1153
|
+
} else if (t === 'float' || t === 'double' || t === 'number' || t === 'real') {
|
|
1154
|
+
if (typeof v === 'boolean') throw createError('ER_TRUNCATED_WRONG_VALUE', t, v);
|
|
1155
|
+
if (typeof v === 'number') result[field] = v;
|
|
1156
|
+
else if (typeof v === 'string' && v.trim() !== '' && !Number.isNaN(Number(v))) result[field] = Number(v);
|
|
1157
|
+
else throw createError('ER_TRUNCATED_WRONG_VALUE', t, v);
|
|
1158
|
+
} else if (t === 'boolean' || t === 'bool') {
|
|
1159
|
+
if (typeof v === 'boolean') result[field] = v;
|
|
1160
|
+
else if (v === 0 || v === 1) result[field] = !!v;
|
|
1161
|
+
else if (typeof v === 'string' && ['true', 'false', '1', '0'].includes(v.toLowerCase())) {
|
|
1162
|
+
result[field] = v.toLowerCase() === 'true' || v === '1';
|
|
1163
|
+
} else {
|
|
1164
|
+
throw createError('ER_TRUNCATED_WRONG_VALUE', t, v);
|
|
1165
|
+
}
|
|
1166
|
+
} else if (t === 'string' || t === 'text' || t === 'varchar' || t === 'char') {
|
|
1167
|
+
if (typeof v === 'string') result[field] = v;
|
|
1168
|
+
else if (typeof v === 'number' || typeof v === 'boolean' || v instanceof Date) result[field] = String(v);
|
|
1169
|
+
else if (Buffer.isBuffer(v)) result[field] = v.toString('utf8');
|
|
1170
|
+
else throw createError('ER_TRUNCATED_WRONG_VALUE', t, v);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1091
1173
|
return result;
|
|
1092
1174
|
}
|
|
1093
1175
|
|
package/lib/web_ui.js
CHANGED
|
@@ -114,7 +114,9 @@ class WebUI {
|
|
|
114
114
|
this.port = opts.port || 8080;
|
|
115
115
|
this.dataDir = opts.dataDir || '.';
|
|
116
116
|
this.readonly = !!opts.readonly;
|
|
117
|
-
this.host = opts.host || '
|
|
117
|
+
this.host = opts.host || '127.0.0.1';
|
|
118
|
+
this.authToken = opts.authToken || null;
|
|
119
|
+
this.allowOrigin = opts.allowOrigin || (this.authToken ? null : '*');
|
|
118
120
|
this.cache = new Map();
|
|
119
121
|
}
|
|
120
122
|
|
|
@@ -154,9 +156,14 @@ class WebUI {
|
|
|
154
156
|
|
|
155
157
|
async handle(req, res) {
|
|
156
158
|
const url = new URL(req.url, 'http://x');
|
|
159
|
+
const reqOrigin = req.headers.origin;
|
|
160
|
+
const corsOrigin = this.allowOrigin === null && reqOrigin ? reqOrigin : (this.allowOrigin || '*');
|
|
157
161
|
const send = (code, obj) => {
|
|
158
162
|
const body = JSON.stringify(obj);
|
|
159
|
-
|
|
163
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
164
|
+
if (this.allowOrigin !== null || reqOrigin) headers['Access-Control-Allow-Origin'] = corsOrigin;
|
|
165
|
+
if (this.allowOrigin !== null || reqOrigin) headers['Vary'] = 'Origin';
|
|
166
|
+
res.writeHead(code, headers);
|
|
160
167
|
res.end(body);
|
|
161
168
|
};
|
|
162
169
|
|
|
@@ -165,6 +172,23 @@ class WebUI {
|
|
|
165
172
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
166
173
|
return res.end(PAGE);
|
|
167
174
|
}
|
|
175
|
+
if (req.method === 'OPTIONS' && url.pathname.startsWith('/api/')) {
|
|
176
|
+
res.writeHead(204, {
|
|
177
|
+
'Access-Control-Allow-Origin': corsOrigin,
|
|
178
|
+
'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
|
|
179
|
+
'Access-Control-Allow-Headers': 'Authorization,Content-Type',
|
|
180
|
+
'Vary': 'Origin',
|
|
181
|
+
});
|
|
182
|
+
return res.end();
|
|
183
|
+
}
|
|
184
|
+
if (this.authToken && url.pathname.startsWith('/api/')) {
|
|
185
|
+
const provided = req.headers.authorization || '';
|
|
186
|
+
const token = provided.startsWith('Bearer ') ? provided.slice(7) : '';
|
|
187
|
+
if (token !== this.authToken) {
|
|
188
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
189
|
+
return res.end(JSON.stringify({ ok: false, error: 'unauthorized' }));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
168
192
|
if (url.pathname === '/api/databases' && req.method === 'GET') {
|
|
169
193
|
return send(200, this.listDatabases());
|
|
170
194
|
}
|