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 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
  ![Engines](https://img.shields.io/badge/engines-Native%20%7C%20WASM%20%7C%20Pure%20JS-7ee787)
7
9
  ![MySQL](https://img.shields.io/badge/protocol-MySQL%20compatible-1f6feb)
10
+ ![PostgreSQL](https://img.shields.io/badge/protocol-PostgreSQL%20compatible-336791)
8
11
  ![Redis](https://img.shields.io/badge/protocol-Redis%20RESP2-f03c15)
9
12
  ![ZERO](https://img.shields.io/badge/dependencies-ZERO-8957e5)
10
13
  ![WASM](https://img.shields.io/badge/runs%20in-Browser%20%28WASM%29-79c0ff)
@@ -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 idx = table._rows.findIndex(r => r === this._resolveId(table, id));
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 };