jsql-neo 5.2.0 → 5.2.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 +29 -1
- package/lib/database.js +20 -1
- package/lib/mongo_server.js +483 -0
- package/lib/multiserver.js +141 -0
- package/lib/mysql_server.js +1 -1
- package/lib/pg_server.js +864 -0
- package/lib/redis_server.js +24 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# JSQL-NEO
|
|
2
2
|
|
|
3
3
|
> **One engine to rule them all** — a Rust-powered embedded database that speaks your language:
|
|
4
|
-
> MySQL protocol. Redis protocol. SQL. TypeScript. The browser. **And it fits in one npm package.**
|
|
4
|
+
> MySQL protocol. PostgreSQL protocol. Redis protocol. SQL. TypeScript. The browser. **And it fits in one npm package.**
|
|
5
|
+
|
|
6
|
+
> **v5.2.1** — official release build · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo)
|
|
5
7
|
|
|
6
8
|

|
|
7
9
|

|
|
10
|
+

|
|
8
11
|

|
|
9
12
|

|
|
10
13
|

|
|
@@ -20,6 +23,7 @@ database protocols in the world**.
|
|
|
20
23
|
- ⚡ **Rust core** — N-API native addon, ~2× faster than better-sqlite3 (see [Benchmark](#benchmark))
|
|
21
24
|
- 🧩 **WASM build** — the *same engine* runs in Node.js **and any browser**, zero native deps
|
|
22
25
|
- 🐘 **MySQL protocol** — Sequelize, Knex, TypeORM, mysql2, phpMyAdmin … **just work**, no plugin
|
|
26
|
+
- 🐘 **PostgreSQL protocol** — node-postgres, psql, pgAdmin — SCRAM-SHA-256 auth, prepared statements, JSONB, ILIKE, `ON CONFLICT`, SERIAL/BIGSERIAL auto-increment
|
|
23
27
|
- 🐇 **Redis protocol** — ioredis, node-redis, redis-cli — strings, hashes, lists, sets, TTL, snapshots
|
|
24
28
|
- 🌐 **Built-in Web UI** — a zero-dependency management console ships with the package
|
|
25
29
|
- 🗃️ **Three storage modes** — memory-first, hybrid (LRU + async flush), and disk
|
|
@@ -170,6 +174,30 @@ createMysqlServer({
|
|
|
170
174
|
|
|
171
175
|
---
|
|
172
176
|
|
|
177
|
+
## Speak PostgreSQL
|
|
178
|
+
|
|
179
|
+
A PostgreSQL wire-protocol (v3) server that accepts **standard PostgreSQL clients** — `node-postgres`,
|
|
180
|
+
`psql`, `pgAdmin` — with SCRAM-SHA-256 authentication and per-database ACL.
|
|
181
|
+
|
|
182
|
+
| Feature | Support |
|
|
183
|
+
|---------|---------|
|
|
184
|
+
| Wire protocol v3 (simple + extended query) | ✅ |
|
|
185
|
+
| SCRAM-SHA-256 / MD5 / cleartext authentication | ✅ |
|
|
186
|
+
| Prepared statements (Parse / Bind / Describe / Execute / Sync) | ✅ |
|
|
187
|
+
| Transactions (BEGIN / COMMIT / ROLLBACK) | ✅ |
|
|
188
|
+
| JSONB columns | ✅ |
|
|
189
|
+
| `ILIKE` (case-insensitive LIKE) | ✅ |
|
|
190
|
+
| `ON CONFLICT DO NOTHING` | ✅ |
|
|
191
|
+
| `SERIAL` / `BIGSERIAL` → auto-increment | ✅ |
|
|
192
|
+
| SQLSTATE error mapping(缺表 → `42P01` 等) | ✅ |
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
jsql serve --pg -p 5432 --data-dir ./data
|
|
196
|
+
psql -h 127.0.0.1 -p 5432 -U postgres # any PostgreSQL client, now
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
173
201
|
## Speak Redis
|
|
174
202
|
|
|
175
203
|
A RESP2 server that plays perfectly with `ioredis`, `node-redis`, and `redis-cli`:
|
package/lib/database.js
CHANGED
|
@@ -1350,7 +1350,8 @@ class Database {
|
|
|
1350
1350
|
for (const id of ids) removed += table.removeById(id);
|
|
1351
1351
|
} else {
|
|
1352
1352
|
for (const id of ids) {
|
|
1353
|
-
const
|
|
1353
|
+
const row = this._resolveId(table, id);
|
|
1354
|
+
const idx = row ? table._rows.findIndex(r => r === row) : -1;
|
|
1354
1355
|
if (idx !== -1) {
|
|
1355
1356
|
table._rows.splice(idx, 1);
|
|
1356
1357
|
removed++;
|
|
@@ -1366,6 +1367,24 @@ class Database {
|
|
|
1366
1367
|
return { ok: true, count: removed };
|
|
1367
1368
|
}
|
|
1368
1369
|
|
|
1370
|
+
removeWhere(tableName, filter) {
|
|
1371
|
+
const table = this._ensureTable(tableName);
|
|
1372
|
+
if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
|
|
1373
|
+
if (!this._runHooks('beforeDelete', [tableName, [filter]])) return { ok: true, count: 0 };
|
|
1374
|
+
const before = table._rows.length;
|
|
1375
|
+
const matched = table._applyFilter(table._rows, filter || {});
|
|
1376
|
+
const matchSet = new Set(matched);
|
|
1377
|
+
table._rows = table._rows.filter(r => !matchSet.has(r));
|
|
1378
|
+
const removed = before - table._rows.length;
|
|
1379
|
+
table._rebuildPKIndex();
|
|
1380
|
+
table._rebuildAllBTrees();
|
|
1381
|
+
for (const field of Object.keys(table._indexes)) table.createIndex(field);
|
|
1382
|
+
this._emit('delete', { table: tableName, ids: [], result: { ok: true, count: removed } });
|
|
1383
|
+
this._runHooks('afterDelete', [tableName, [filter], { ok: true, count: removed }]);
|
|
1384
|
+
this._markDirty(tableName);
|
|
1385
|
+
return { ok: true, count: removed };
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1369
1388
|
insertMany(tableName, items) {
|
|
1370
1389
|
const table = this._ensureTable(tableName);
|
|
1371
1390
|
if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* MongoDB wire protocol (OP_MSG, 3.6+) server for JSQL-NEO.
|
|
3
|
+
*
|
|
4
|
+
* Lets the official `mongodb` npm driver (and mongosh / Compass) connect to
|
|
5
|
+
* jsql-neo as if it were a MongoDB server. Documents are stored as rows of a
|
|
6
|
+
* JSQL table named after the MongoDB collection.
|
|
7
|
+
*
|
|
8
|
+
* Commands supported (minimal but practical):
|
|
9
|
+
* hello / isMaster / ping / buildInfo / getParameter / endSessions
|
|
10
|
+
* listDatabases / listCollections / create / drop
|
|
11
|
+
* insert / find / getMore / update / delete / count / aggregate($match,$count)
|
|
12
|
+
*/
|
|
13
|
+
const net = require('net');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const zlib = require('zlib');
|
|
16
|
+
const Database = require('./database');
|
|
17
|
+
|
|
18
|
+
const OP_MSG = 2013;
|
|
19
|
+
const OP_COMPRESSED = 2012;
|
|
20
|
+
const OP_QUERY = 2004;
|
|
21
|
+
const OP_REPLY = 1;
|
|
22
|
+
|
|
23
|
+
/* ------------------------------------------------------------------ */
|
|
24
|
+
/* BSON */
|
|
25
|
+
/* ------------------------------------------------------------------ */
|
|
26
|
+
|
|
27
|
+
function appendBytes(buf, b) {
|
|
28
|
+
const out = Buffer.concat([buf, b]);
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function bsonDocument(obj) {
|
|
33
|
+
// 简易 BSON 文档编码(定长后修正 size)
|
|
34
|
+
let body = Buffer.alloc(4); // size placeholder
|
|
35
|
+
for (const [k, v] of Object.entries(obj || {})) {
|
|
36
|
+
const name = Buffer.from(k, 'utf8');
|
|
37
|
+
if (v === null || v === undefined) {
|
|
38
|
+
body = Buffer.concat([body, Buffer.from([0x0A]), name, Buffer.from([0])]);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (typeof v === 'number') {
|
|
42
|
+
const b = Buffer.alloc(8);
|
|
43
|
+
b.writeDoubleLE(v, 0);
|
|
44
|
+
body = Buffer.concat([body, Buffer.from([0x01]), name, Buffer.from([0]), b]);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (v && v.$long !== undefined) {
|
|
48
|
+
const b = Buffer.alloc(8);
|
|
49
|
+
b.writeBigInt64LE(BigInt(Math.trunc(Number(v.$long))), 0);
|
|
50
|
+
body = Buffer.concat([body, Buffer.from([0x12]), name, Buffer.from([0]), b]);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (typeof v === 'boolean') {
|
|
54
|
+
body = Buffer.concat([body, Buffer.from([0x08]), name, Buffer.from([0]), Buffer.from([v ? 1 : 0])]);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (typeof v === 'string') {
|
|
58
|
+
const str = Buffer.from(v, 'utf8');
|
|
59
|
+
const lenBuf = Buffer.alloc(4);
|
|
60
|
+
lenBuf.writeInt32LE(str.length + 1, 0);
|
|
61
|
+
body = Buffer.concat([body, Buffer.from([0x02]), name, Buffer.from([0]), lenBuf, str, Buffer.from([0])]);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (Buffer.isBuffer(v)) {
|
|
65
|
+
const lenBuf = Buffer.alloc(4);
|
|
66
|
+
lenBuf.writeInt32LE(v.length, 0);
|
|
67
|
+
body = Buffer.concat([body, Buffer.from([0x05]), name, Buffer.from([0]), lenBuf, Buffer.from([0, v.buffer ? 0 : 0]), v]);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (v instanceof Date) {
|
|
71
|
+
const b = Buffer.alloc(8);
|
|
72
|
+
b.writeBigInt64LE(BigInt(v.getTime()), 0);
|
|
73
|
+
body = Buffer.concat([body, Buffer.from([0x09]), name, Buffer.from([0]), b]);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(v)) {
|
|
77
|
+
const arrObj = {};
|
|
78
|
+
v.forEach((x, i) => { arrObj[String(i)] = x; });
|
|
79
|
+
const sub = bsonDocument(arrObj);
|
|
80
|
+
body = Buffer.concat([body, Buffer.from([0x04]), name, Buffer.from([0]), sub]);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (typeof v === 'object') {
|
|
84
|
+
const sub = bsonDocument(v);
|
|
85
|
+
body = Buffer.concat([body, Buffer.from([0x03]), name, Buffer.from([0]), sub]);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// fallback: int32
|
|
89
|
+
const b = Buffer.alloc(4);
|
|
90
|
+
b.writeInt32LE(Number(v) || 0, 0);
|
|
91
|
+
body = Buffer.concat([body, Buffer.from([0x10]), name, Buffer.from([0]), b]);
|
|
92
|
+
}
|
|
93
|
+
body = Buffer.concat([body, Buffer.from([0])]);
|
|
94
|
+
body.writeInt32LE(body.length, 0);
|
|
95
|
+
return body;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function bsonReadDocument(buf, pos) {
|
|
99
|
+
const start = pos;
|
|
100
|
+
const size = buf.readInt32LE(pos); pos += 4;
|
|
101
|
+
const end = start + size;
|
|
102
|
+
const obj = {};
|
|
103
|
+
while (pos < end - 1) {
|
|
104
|
+
const type = buf[pos++];
|
|
105
|
+
// cstring field name
|
|
106
|
+
const nameEnd = buf.indexOf(0, pos);
|
|
107
|
+
const name = buf.toString('utf8', pos, nameEnd);
|
|
108
|
+
pos = nameEnd + 1;
|
|
109
|
+
const r = bsonReadValue(buf, pos, type, end);
|
|
110
|
+
obj[name] = r.value;
|
|
111
|
+
pos = r.pos;
|
|
112
|
+
}
|
|
113
|
+
return { value: obj, pos: end };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function bsonReadValue(buf, pos, type, end) {
|
|
117
|
+
switch (type) {
|
|
118
|
+
case 0x01: { const v = buf.readDoubleLE(pos); return { value: v, pos: pos + 8 }; }
|
|
119
|
+
case 0x02: {
|
|
120
|
+
const len = buf.readInt32LE(pos); pos += 4;
|
|
121
|
+
const v = buf.toString('utf8', pos, pos + len - 1);
|
|
122
|
+
return { value: v, pos: pos + len };
|
|
123
|
+
}
|
|
124
|
+
case 0x03: { const r = bsonReadDocument(buf, pos); return { value: r.value, pos: r.pos }; }
|
|
125
|
+
case 0x04: {
|
|
126
|
+
const r = bsonReadDocument(buf, pos);
|
|
127
|
+
const arr = [];
|
|
128
|
+
for (let i = 0; i < Object.keys(r.value).length; i++) arr.push(r.value[String(i)]);
|
|
129
|
+
return { value: arr, pos: r.pos };
|
|
130
|
+
}
|
|
131
|
+
case 0x05: { const len = buf.readInt32LE(pos); return { value: buf.slice(pos + 5, pos + 5 + len), pos: pos + 5 + len }; }
|
|
132
|
+
case 0x07: { return { value: buf.slice(pos, pos + 12).toString('hex'), pos: pos + 12 }; }
|
|
133
|
+
case 0x08: { return { value: buf[pos] !== 0, pos: pos + 1 }; }
|
|
134
|
+
case 0x09: { return { value: new Date(Number(buf.readBigInt64LE(pos))), pos: pos + 8 }; }
|
|
135
|
+
case 0x0A: { return { value: null, pos }; }
|
|
136
|
+
case 0x10: { return { value: buf.readInt32LE(pos), pos: pos + 4 }; }
|
|
137
|
+
case 0x11: { return { value: Number(buf.readBigInt64LE(pos)), pos: pos + 8 }; }
|
|
138
|
+
case 0x12: { return { value: Number(buf.readBigInt64LE(pos)), pos: pos + 8 }; }
|
|
139
|
+
case 0x13: { return { value: buf.readDoubleLE(pos + 1), pos: pos + 17 }; }
|
|
140
|
+
default: return { value: null, pos: end };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* ------------------------------------------------------------------ */
|
|
145
|
+
/* Server */
|
|
146
|
+
/* ------------------------------------------------------------------ */
|
|
147
|
+
|
|
148
|
+
class MongoServer {
|
|
149
|
+
constructor(options = {}) {
|
|
150
|
+
this.options = options;
|
|
151
|
+
this.port = options.port || 27017;
|
|
152
|
+
this.host = options.host || '127.0.0.1';
|
|
153
|
+
this.dataDir = options.dataDir || null;
|
|
154
|
+
this._engine = null;
|
|
155
|
+
this._server = null;
|
|
156
|
+
this._sockets = new Set();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async _getEngine() {
|
|
160
|
+
if (this._engine) return this._engine;
|
|
161
|
+
if (this.dataDir && this.dataDir !== ':memory:') {
|
|
162
|
+
this._engine = new Database(path.join(this.dataDir, 'default'), { autoSave: true });
|
|
163
|
+
} else {
|
|
164
|
+
this._engine = new Database(':memory:', { autoSave: false });
|
|
165
|
+
}
|
|
166
|
+
return this._engine;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async _ensureCollection(name) {
|
|
170
|
+
const engine = await this._getEngine();
|
|
171
|
+
if (!engine.getTableSchema(name)) {
|
|
172
|
+
engine.createTable(name, {});
|
|
173
|
+
}
|
|
174
|
+
return engine;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
listen(cb) {
|
|
178
|
+
this._server = net.createServer((socket) => this._handleSocket(socket));
|
|
179
|
+
this._server.listen(this.port, this.host, cb || (() => {}));
|
|
180
|
+
return this;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_handleSocket(socket, existing) {
|
|
184
|
+
this._sockets.add(socket);
|
|
185
|
+
socket.on('close', () => this._sockets.delete(socket));
|
|
186
|
+
let buf = Buffer.isBuffer(existing) ? Buffer.from(existing) : Buffer.alloc(0);
|
|
187
|
+
socket.on('error', () => {});
|
|
188
|
+
const process = async () => {
|
|
189
|
+
for (;;) {
|
|
190
|
+
if (buf.length < 4) break;
|
|
191
|
+
const len = buf.readInt32LE(0);
|
|
192
|
+
if (len < 16 || buf.length < len) break;
|
|
193
|
+
const msg = buf.slice(0, len);
|
|
194
|
+
buf = buf.slice(len);
|
|
195
|
+
try {
|
|
196
|
+
await this._handleMessage(socket, msg);
|
|
197
|
+
} catch (e) {
|
|
198
|
+
try { socket.write(this._wrapMsg(bsonDocument({ ok: 0, errmsg: e.message || String(e) }), this._reqId(msg))); } catch (_) {}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
socket.on('data', async (chunk) => {
|
|
203
|
+
buf = Buffer.concat([buf, chunk]);
|
|
204
|
+
await process();
|
|
205
|
+
});
|
|
206
|
+
if (buf.length > 0) process();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
_reqId(msg) {
|
|
210
|
+
return msg.readInt32LE(4);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
_wrapMsg(payload, responseTo) {
|
|
214
|
+
const id = Math.floor(Math.random() * 0x7fffffff);
|
|
215
|
+
const head = Buffer.alloc(16);
|
|
216
|
+
head.writeInt32LE(16 + 4 + 1 + payload.length, 0); // messageLength
|
|
217
|
+
head.writeInt32LE(id, 4); // requestID
|
|
218
|
+
head.writeInt32LE(responseTo, 8); // responseTo
|
|
219
|
+
head.writeInt32LE(OP_MSG, 12); // opCode
|
|
220
|
+
const flags = Buffer.alloc(4);
|
|
221
|
+
return Buffer.concat([head, flags, Buffer.from([0x00]), payload]); // flagBits + kind0
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
_wrapReply(payload, requestId) {
|
|
225
|
+
// OP_REPLY: header + responseFlags + cursorID + startingFrom + numberReturned + documents
|
|
226
|
+
const n = 16 + 4 + 8 + 4 + 4 + payload.length;
|
|
227
|
+
const head = Buffer.alloc(16);
|
|
228
|
+
head.writeInt32LE(n, 0);
|
|
229
|
+
head.writeInt32LE(requestId + 1000, 4);
|
|
230
|
+
head.writeInt32LE(requestId, 8);
|
|
231
|
+
head.writeInt32LE(OP_REPLY, 12);
|
|
232
|
+
const rest = Buffer.alloc(4 + 8 + 4 + 4); // responseFlags=0, cursorID=0, startingFrom=0, numberReturned=1
|
|
233
|
+
rest.writeInt32LE(1, 4 + 8 + 4);
|
|
234
|
+
return Buffer.concat([head, rest, payload]);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async _handleMessage(socket, msg) {
|
|
238
|
+
const op = msg.readInt32LE(12);
|
|
239
|
+
if (op === OP_QUERY) {
|
|
240
|
+
let p = 20; // header + flags
|
|
241
|
+
while (p < msg.length && msg[p] !== 0) p++;
|
|
242
|
+
const ns = msg.toString('utf8', 20, p);
|
|
243
|
+
p = p + 1 + 8; // null terminator + numberToSkip + numberToReturn
|
|
244
|
+
if (p >= msg.length || !ns.endsWith('.$cmd')) {
|
|
245
|
+
socket.write(this._wrapReply(bsonDocument({ ok: 0, errmsg: 'legacy op only supports $cmd' }), this._reqId(msg)));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
const body = bsonReadDocument(msg, p).value;
|
|
250
|
+
const command = this._commandName(body);
|
|
251
|
+
const reply = await this._dispatch(command, body);
|
|
252
|
+
socket.write(this._wrapReply(bsonDocument(reply), this._reqId(msg)));
|
|
253
|
+
} catch (e) {
|
|
254
|
+
socket.write(this._wrapReply(bsonDocument({ ok: 0, errmsg: e.message || String(e) }), this._reqId(msg)));
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (op === OP_COMPRESSED) {
|
|
259
|
+
msg = this._decompress(msg);
|
|
260
|
+
}
|
|
261
|
+
if (!msg) {
|
|
262
|
+
socket.write(this._wrapMsg(bsonDocument({ ok: 0, errmsg: 'compression not supported' }), this._reqId(msg || Buffer.alloc(16))));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const op2 = msg.readInt32LE(12);
|
|
266
|
+
if (op2 !== OP_MSG) {
|
|
267
|
+
socket.write(this._wrapMsg(bsonDocument({ ok: 0, errmsg: `opCode ${op2} not supported` }), this._reqId(msg)));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
let pos = 16 + 4; // header + flagBits
|
|
271
|
+
const kind = msg[pos]; pos++; // section kind: 0 = single BSON doc
|
|
272
|
+
if (kind !== 0) {
|
|
273
|
+
socket.write(this._wrapMsg(bsonDocument({ ok: 0, errmsg: `section kind ${kind} not supported` }), this._reqId(msg)));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const body = bsonReadDocument(msg, pos).value;
|
|
277
|
+
const command = this._commandName(body);
|
|
278
|
+
const reply = await this._dispatch(command, body);
|
|
279
|
+
socket.write(this._wrapMsg(bsonDocument(reply), this._reqId(msg)));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
_commandName(doc) {
|
|
283
|
+
for (const k of Object.keys(doc)) {
|
|
284
|
+
if (k === '$db' || k === 'lsid' || k === '$clusterTime' || k === '$readPreference' || k === 'readConcern' || k === 'writeConcern') continue;
|
|
285
|
+
return k;
|
|
286
|
+
}
|
|
287
|
+
return 'ping';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
_decompress(msg) {
|
|
291
|
+
try {
|
|
292
|
+
const origOp = msg.readInt32LE(16);
|
|
293
|
+
const uncompSize = msg.readInt32LE(20);
|
|
294
|
+
const compId = msg[24];
|
|
295
|
+
const data = msg.slice(25);
|
|
296
|
+
let out;
|
|
297
|
+
if (compId === 2) out = zlib.inflateSync(data); // zlib
|
|
298
|
+
else if (compId === 3) { /* zstd 无内置支持 */ return null; }
|
|
299
|
+
else if (compId === 1) { /* snappy 无内置支持 */ return null; }
|
|
300
|
+
else return null;
|
|
301
|
+
const head = Buffer.from(msg.slice(0, 16));
|
|
302
|
+
head.writeInt32LE(origOp, 12);
|
|
303
|
+
return Buffer.concat([head, out]);
|
|
304
|
+
} catch (e) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
_dbOf(doc, fallback = 'test') {
|
|
310
|
+
return doc.$db || fallback;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async _dispatch(cmd, doc) {
|
|
314
|
+
|
|
315
|
+
switch (cmd) {
|
|
316
|
+
case 'hello':
|
|
317
|
+
case 'isMaster':
|
|
318
|
+
case 'ismaster':
|
|
319
|
+
return {
|
|
320
|
+
ok: 1, isWritablePrimary: true, helloOk: true, maxWireVersion: 17, minWireVersion: 0,
|
|
321
|
+
maxBsonObjectSize: 16777216, maxMessageSizeBytes: 48000000, maxWriteBatchSize: 100000,
|
|
322
|
+
localTime: new Date(), logicalSessionTimeoutMinutes: 30, connectionId: 1,
|
|
323
|
+
};
|
|
324
|
+
case 'ping': return { ok: 1 };
|
|
325
|
+
case 'buildInfo': return { ok: 1, version: '5.2.1-jsql-neo', gitVersion: 'jsql-neo', versionArray: [5, 2, 1, 0] };
|
|
326
|
+
case 'getParameter': return { ok: 1 };
|
|
327
|
+
case 'endSessions': return { ok: 1 };
|
|
328
|
+
case 'listDatabases':
|
|
329
|
+
return { ok: 1, databases: [{ name: this._dbOf(doc), sizeOnDisk: 1, empty: false }], totalSize: 1 };
|
|
330
|
+
case 'listCollections': {
|
|
331
|
+
const engine = await this._getEngine();
|
|
332
|
+
const tables = Object.keys(engine._meta && engine._meta.tables ? engine._meta.tables : (engine._tables || {}));
|
|
333
|
+
const firstBatch = tables.map((t) => ({ name: t, type: 'collection', options: {}, info: { readOnly: false, uuid: '' } }));
|
|
334
|
+
return { ok: 1, cursor: { id: { $long: 0 }, ns: `${this._dbOf(doc)}.$cmd.listCollections`, firstBatch } };
|
|
335
|
+
}
|
|
336
|
+
case 'create': {
|
|
337
|
+
const engine = await this._ensureCollection(String(doc.create));
|
|
338
|
+
return { ok: 1, ns: engine ? `${this._dbOf(doc)}.${doc.create}` : '' };
|
|
339
|
+
}
|
|
340
|
+
case 'drop': {
|
|
341
|
+
try {
|
|
342
|
+
const engine = await this._getEngine();
|
|
343
|
+
engine.dropTable(String(doc.drop));
|
|
344
|
+
return { ok: 1, ns: `${this._dbOf(doc)}.${doc.drop}` };
|
|
345
|
+
} catch (e) {
|
|
346
|
+
return { ok: 1, ns: `${this._dbOf(doc)}.${doc.drop}` }; // Mongo 对不存在的 collection drop 也返回 ok
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
case 'insert': {
|
|
350
|
+
const coll = String(doc.insert);
|
|
351
|
+
const engine = await this._ensureCollection(coll);
|
|
352
|
+
const docs = doc.documents || [];
|
|
353
|
+
const ids = [];
|
|
354
|
+
for (const d of docs) {
|
|
355
|
+
const doc2 = { ...d };
|
|
356
|
+
ids.push(engine.insert(coll, doc2));
|
|
357
|
+
}
|
|
358
|
+
return { ok: 1, n: docs.length };
|
|
359
|
+
}
|
|
360
|
+
case 'find': {
|
|
361
|
+
const coll = String(doc.find);
|
|
362
|
+
const engine = await this._ensureCollection(coll);
|
|
363
|
+
const rows = engine.find(coll, doc.filter || {}, {
|
|
364
|
+
limit: typeof doc.limit === 'number' ? doc.limit : 0,
|
|
365
|
+
offset: typeof doc.skip === 'number' ? doc.skip : 0,
|
|
366
|
+
});
|
|
367
|
+
return { ok: 1, cursor: { id: { $long: 0 }, ns: `${this._dbOf(doc)}.${coll}`, firstBatch: rows } };
|
|
368
|
+
}
|
|
369
|
+
case 'getMore':
|
|
370
|
+
return { ok: 1, cursor: { id: { $long: 0 }, ns: String(doc.collection || ''), nextBatch: [] } };
|
|
371
|
+
case 'update': {
|
|
372
|
+
const coll = String(doc.update);
|
|
373
|
+
const engine = await this._ensureCollection(coll);
|
|
374
|
+
let n = 0;
|
|
375
|
+
const updates = doc.updates || [];
|
|
376
|
+
for (const u of updates) {
|
|
377
|
+
const q = u.q || {};
|
|
378
|
+
const set = u.u && u.u.$set ? u.u.$set : (u.u || {});
|
|
379
|
+
// 单文档更新
|
|
380
|
+
let target = engine.find(coll, q, { limit: 1 })[0] || null;
|
|
381
|
+
if (!target && u.upsert) {
|
|
382
|
+
const merged = { ...q, ...set };
|
|
383
|
+
engine.insert(coll, merged);
|
|
384
|
+
n++;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (target) {
|
|
388
|
+
engine.update(coll, q, set);
|
|
389
|
+
n++;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return { ok: 1, n, nModified: n };
|
|
393
|
+
}
|
|
394
|
+
case 'delete': {
|
|
395
|
+
const coll = String(doc.delete);
|
|
396
|
+
const engine = await this._ensureCollection(coll);
|
|
397
|
+
let n = 0;
|
|
398
|
+
for (const d of (doc.deletes || [])) {
|
|
399
|
+
const res = engine.removeWhere(coll, d.q || {});
|
|
400
|
+
n += res.count;
|
|
401
|
+
}
|
|
402
|
+
return { ok: 1, n };
|
|
403
|
+
}
|
|
404
|
+
case 'count': {
|
|
405
|
+
const coll = String(doc.count);
|
|
406
|
+
const engine = await this._ensureCollection(coll);
|
|
407
|
+
const rows = engine.find(coll, doc.query || {});
|
|
408
|
+
return { ok: 1, n: Number(doc.limit) > 0 ? Math.min(rows.length, doc.limit) : rows.length };
|
|
409
|
+
}
|
|
410
|
+
case 'aggregate': {
|
|
411
|
+
const coll = String(doc.aggregate);
|
|
412
|
+
const engine = await this._ensureCollection(coll);
|
|
413
|
+
const pipeline = doc.pipeline || [];
|
|
414
|
+
let rows = engine.find(coll, {});
|
|
415
|
+
for (const stage of pipeline) {
|
|
416
|
+
if (stage.$match) rows = rows.filter((r) => this._match(r, stage.$match));
|
|
417
|
+
else if (stage.$count) rows = [{ [stage.$count]: rows.length }];
|
|
418
|
+
else if (stage.$limit) rows = rows.slice(0, stage.$limit);
|
|
419
|
+
else if (stage.$group) {
|
|
420
|
+
const acc = {};
|
|
421
|
+
for (const [k, v] of Object.entries(stage.$group)) {
|
|
422
|
+
if (v && v.$sum === 1 && k !== '_id') acc[k] = rows.length;
|
|
423
|
+
}
|
|
424
|
+
rows = [{ _id: 1, ...acc }];
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return { ok: 1, cursor: { id: { $long: 0 }, ns: `${this._dbOf(doc)}.${coll}`, firstBatch: rows } };
|
|
428
|
+
}
|
|
429
|
+
default:
|
|
430
|
+
return { ok: 1, n: 0 };
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
_idOf(row) {
|
|
435
|
+
return row._id != null ? row._id : row.id;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
_match(row, filter) {
|
|
439
|
+
for (const [k, cond] of Object.entries(filter || {})) {
|
|
440
|
+
if (k === '$or') {
|
|
441
|
+
if (!cond.some((f) => this._match(row, f))) return false;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (cond && typeof cond === 'object' && !Array.isArray(cond)) {
|
|
445
|
+
for (const [op, v] of Object.entries(cond)) {
|
|
446
|
+
const rv = row[k];
|
|
447
|
+
if (op === '$gt' && !(rv > v)) return false;
|
|
448
|
+
if (op === '$gte' && !(rv >= v)) return false;
|
|
449
|
+
if (op === '$lt' && !(rv < v)) return false;
|
|
450
|
+
if (op === '$lte' && !(rv <= v)) return false;
|
|
451
|
+
if (op === '$ne' && !(rv !== v)) return false;
|
|
452
|
+
if (op === '$in' && !(v.includes(rv))) return false;
|
|
453
|
+
if (op === '$nin' && v.includes(rv)) return false;
|
|
454
|
+
if (op === '$exists' && (v ? (rv === undefined) : (rv !== undefined))) return false;
|
|
455
|
+
}
|
|
456
|
+
} else if (row[k] !== cond) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
get address() {
|
|
464
|
+
return this._server ? this._server.address() : null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
close(cb) {
|
|
468
|
+
for (const s of this._sockets) s.destroy();
|
|
469
|
+
const done = () => {
|
|
470
|
+
if (this._server) { this._server.close(cb || (() => {})); this._server = null; }
|
|
471
|
+
else if (cb) cb();
|
|
472
|
+
};
|
|
473
|
+
if (this._engine && typeof this._engine.stop === 'function') this._engine.stop().then(done).catch(done);
|
|
474
|
+
else done();
|
|
475
|
+
return this;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function createMongoServer(options) {
|
|
480
|
+
return new MongoServer(options || {});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
module.exports = { MongoServer, createMongoServer, bsonDocument, bsonReadDocument };
|