jsql-neo 5.0.1 → 5.1.1

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 CHANGED
@@ -13,6 +13,8 @@
13
13
 
14
14
  ## Why JSQL-NEO?
15
15
 
16
+ My web : https://jsql.vexify.top/
17
+
16
18
  Most embedded databases make you choose: *native speed*, *portable WASM*, or *a familiar file format*.
17
19
  JSQL-NEO gives you **all three in one install** — plus drop-in compatibility with the **two most popular
18
20
  database protocols in the world**.
@@ -154,9 +156,20 @@ MySQL DDL forms (`int unsigned`, `auto_increment`, `ENGINE=InnoDB`, `DEFAULT CHA
154
156
 
155
157
  ```js
156
158
  const { createMysqlServer } = require('jsql-neo');
159
+ // 开发环境:本地无认证
157
160
  createMysqlServer({ port: 3306, dataDir: './data', noAuth: true }).listen();
161
+
162
+ // 生产环境:用户名/密码 + 每用户数据库白名单(ACL)
163
+ createMysqlServer({
164
+ port: 3306,
165
+ dataDir: './data',
166
+ auth: { app: { password: 's3cret', databases: ['app', 'analytics'] } },
167
+ }).listen();
158
168
  ```
159
169
 
170
+ `auth` 里的 `databases` 数组即该用户的数据库白名单:越权访问(跨库引用、`SHOW TABLES FROM db`、
171
+ `DROP DATABASE`)统一返回 `ER_DBACCESS_DENIED_ERROR` (1044)。省略 `databases` 时不限制库。
172
+
160
173
  ---
161
174
 
162
175
  ## Speak Redis
@@ -176,9 +189,15 @@ Snapshot persistence to `data.rdb.json` — debounced writes (500ms) plus a guar
176
189
 
177
190
  ```js
178
191
  const { createRedisServer } = require('jsql-neo');
192
+ // 本地无认证
179
193
  createRedisServer({ port: 6379, dataDir: './redis-data' }).listen();
194
+
195
+ // 带密码:每连接独立认证,任一客户端 AUTH 成功不影响其它连接
196
+ createRedisServer({ port: 6379, dataDir: './redis-data', password: 's3cret' }).listen();
180
197
  ```
181
198
 
199
+ 设置了 `password` 后,未认证连接上的命令返回 `NOAUTH Authentication required.`;认证状态按连接隔离。
200
+
182
201
  ---
183
202
 
184
203
  ## Toolbox — everything included
@@ -202,6 +221,18 @@ createRedisServer({ port: 6379, dataDir: './redis-data' }).listen();
202
221
  A zero-dependency HTTP management console: browse databases and tables, run SQL in the browser,
203
222
  see results as a table. Perfect for dev tools, admin panels, and demos.
204
223
 
224
+ > **安全默认值(5.1.0)**:默认只监听 `127.0.0.1`(不再暴露到所有网卡)。生产环境请设置
225
+ > `authToken`,所有 `/api/*` 请求需携带 `Authorization: Bearer <token>`,未认证返回 401。
226
+
227
+ ```js
228
+ const { WebUI } = require('jsql-neo');
229
+ const ui = new WebUI({ port: 8080, dataDir: './data', authToken: 'change-me' });
230
+ await ui.start();
231
+ ```
232
+
233
+ 可用选项:`host`(默认 `127.0.0.1`)、`port`、`dataDir`、`readonly`、`authToken`(Bearer 认证)、
234
+ `allowOrigin`(CORS 允许的源;不设置时开启认证仅回显请求 Origin,未开启认证时为 `*`)。
235
+
205
236
  ### Migration tools (`migrate`)
206
237
 
207
238
  ```js
@@ -210,6 +241,11 @@ await importDumpFile(db, './backup.sql', { strict: true }); // real mysqldump
210
241
  await exportToFile(db, 'users', './users.csv'); // CSV round-trip
211
242
  ```
212
243
 
244
+ `importFromJSON` / `importDumpFile` 从 5.1.0 起:
245
+
246
+ - 兼容两种 JSON 形状:整库 `{ "users": { schema, rows } }` 与单表 `{ table, schema, rows }`。
247
+ - 目标表已存在时**默认抛错**,不会静默覆盖;需显式传入 `{ overwrite: true }` 才重建表。
248
+
213
249
  ### Browser playground
214
250
 
215
251
  `examples/playground/` is a self-contained SQL sandbox — the **entire engine runs in your browser**
package/bin/jsql CHANGED
@@ -21,12 +21,12 @@ const manager = new ModuleManager();
21
21
 
22
22
  const RAW_ARGV = process.argv.slice(2);
23
23
 
24
- const cli = yaggs()
24
+ const cli = yaggs({ pkg: require('../package.json') })
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 0.0.0.0)' });
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 || '0.0.0.0',
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
- console.log(`JSQL-NEO web UI on http://${argv.host || '0.0.0.0'}:${port} (data: ${ui.dataDir})`);
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 v3.1.0 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v5.1.1 — 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
- return this.range(min, Infinity);
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
- return this.range(-Infinity, max);
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] = { type: def.toLowerCase() };
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.map(r => ({ ...r }));
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] : (table._rows.indexOf(r) + 1));
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 = 100, offset = 0 } = opts;
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
- for (const id of ids) {
1314
- const idx = table._rows.findIndex(r => r === this._resolveId(table, id));
1315
- if (idx !== -1) {
1316
- table._rows.splice(idx, 1);
1317
- removed++;
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' ? { type: def } : { ...def };
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 tables = typeof data === 'string' ? JSON.parse(data) : data;
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)) await engine.dropTable(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) {
@@ -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
- engine = new Database(':memory:');
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
  }
@@ -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;
@@ -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
- const est = 100 + rows.length * 120;
78
- const buf = Buffer.allocUnsafe(est);
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
- if (nFields >= 1) {
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
 
@@ -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, -new Error('ERR Client sent AUTH, but no password is set'));
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._authed) {
413
- if (cmd === 'AUTH') { const ok = this.execute('AUTH', args); this._authed = ok === 'OK'; return this._send(socket, ok); }
414
- return this._send(socket, -new Error('NOAUTH Authentication required.'));
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, -e);
426
+ this._send(socket, e);
421
427
  }
422
428
  }
423
429
 
@@ -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
- return new RegExp('^' + target.replace(/%/g, '.*').replace(/_/g, '.') + '$', 'i').test(value);
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,10 @@ 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 || '0.0.0.0';
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;
118
121
  this.cache = new Map();
119
122
  }
120
123
 
@@ -154,9 +157,18 @@ class WebUI {
154
157
 
155
158
  async handle(req, res) {
156
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;
157
164
  const send = (code, obj) => {
158
165
  const body = JSON.stringify(obj);
159
- res.writeHead(code, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
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);
160
172
  res.end(body);
161
173
  };
162
174
 
@@ -165,6 +177,25 @@ class WebUI {
165
177
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
166
178
  return res.end(PAGE);
167
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
+ }
168
199
  if (url.pathname === '/api/databases' && req.method === 'GET') {
169
200
  return send(200, this.listDatabases());
170
201
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "5.0.1",
3
+ "version": "5.1.1",
4
4
  "description": "JSQL-NEO — Rust-powered embedded database with WASM, REST API, B-Tree indexes, WAL, crash recovery",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",