dsh-db-tool 0.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.
Files changed (41) hide show
  1. package/README.md +66 -0
  2. package/client/README.md +53 -0
  3. package/client/client.js +1128 -0
  4. package/cordis.patch.yml +6 -0
  5. package/dist/adapters/dmdb/index.js +326 -0
  6. package/dist/adapters/gaussdb/index.js +32 -0
  7. package/dist/adapters/index.js +41 -0
  8. package/dist/adapters/mongodb/index.js +508 -0
  9. package/dist/adapters/mysql/index.js +222 -0
  10. package/dist/adapters/oracle/index.js +369 -0
  11. package/dist/adapters/postgresql/index.js +15 -0
  12. package/dist/adapters/redis/index.js +492 -0
  13. package/dist/adapters/sql-shared/common.js +69 -0
  14. package/dist/adapters/sql-shared/pg-like.js +170 -0
  15. package/dist/adapters/sqlite/index.js +106 -0
  16. package/dist/adapters/types.js +1 -0
  17. package/dist/guard/index.js +221 -0
  18. package/dist/http/index.js +396 -0
  19. package/dist/index.js +130 -0
  20. package/dist/manager.js +469 -0
  21. package/dist/script/runner.js +147 -0
  22. package/dist/script/worker.cjs +104 -0
  23. package/dist/store/audit.js +41 -0
  24. package/dist/store/connections.js +167 -0
  25. package/dist/store/grants.js +82 -0
  26. package/dist/store/index.js +41 -0
  27. package/dist/store/io.js +66 -0
  28. package/dist/store/normalize.js +22 -0
  29. package/dist/store/secrets.js +42 -0
  30. package/docs/api-contract.md +76 -0
  31. package/docs/apple-redesign-spec.md +80 -0
  32. package/docs/dsh-market-submission/mengqi1436__dsh-db-tool.yml +6 -0
  33. package/docs/install.md +75 -0
  34. package/docs/review-findings.md +95 -0
  35. package/docs/skill.md +48 -0
  36. package/package.json +70 -0
  37. package/scripts/build-gaussdb.ps1 +72 -0
  38. package/scripts/build-gaussdb.sh +53 -0
  39. package/scripts/diag-profile.mjs +21 -0
  40. package/scripts/verify-host.mjs +53 -0
  41. package/skills/db-admin/SKILL.md +138 -0
@@ -0,0 +1,508 @@
1
+ /**
2
+ * MongoDB 适配器(mongodb v7,Server 4.4+)。
3
+ *
4
+ * 设计要点(依据官方文档调研):
5
+ * - new MongoClient(uri, { maxPoolSize: 10, readPreference: 'primary', w: 'majority', family: 4 })
6
+ * + 显式 await connect()。family:4 规避 Node localhost IPv6 DNS 解析坑(官方 README)。
7
+ * - query() 入参为命令 JSON 文档字符串,读白名单:find/aggregate/count/countDocuments/
8
+ * estimatedDocumentCount/distinct/listCollections/dbStats/collStats/indexes。
9
+ * - find 自动 clamp limit ≤ 50,默认按 _id 排序;$where 拒绝(代码注入面)。
10
+ * - execute() 写白名单;updateMany/deleteMany 空 filter 直接拒绝(适配器级防线)。
11
+ * - 错误判定用 instanceof MongoServerError(官方建议不解析 message)。
12
+ * - 只读账号由用户配置官方内置角色 read / readWrite(见文档)。
13
+ */
14
+ import { Binary, Decimal128, MongoClient, MongoServerError, Long, ObjectId, Timestamp, } from 'mongodb';
15
+ /** 危险操作:服务层拦截器用于确认流程。deleteMany/updateMany 仅空 filter 时危险(适配器已拦截);createIndex 在大集合上可能阻塞。createIndexes/dropIndexes 为单数形式别名,renameCollection 影响集合可见性。 */
16
+ export const DANGEROUS_OPS = new Set([
17
+ 'dropDatabase', 'dropCollection', 'renameCollection', 'deleteMany', 'updateMany',
18
+ 'createIndex', 'createIndexes', 'dropIndex', 'dropIndexes', 'shutdown', 'replSetStepDown',
19
+ ]);
20
+ /** 只读操作白名单(guard 层确认分级用,导出) */
21
+ export const READ_OPS = new Set([
22
+ 'find', 'aggregate', 'count', 'countDocuments', 'estimatedDocumentCount',
23
+ 'distinct', 'listCollections', 'dbStats', 'collStats', 'indexes',
24
+ ]);
25
+ const WRITE_OPS = new Set([
26
+ 'insertOne', 'insertMany', 'updateOne', 'updateMany', 'replaceOne',
27
+ 'deleteOne', 'deleteMany', 'createCollection', 'dropCollection',
28
+ 'createIndex', 'createIndexes', 'dropIndex', 'dropIndexes', 'renameCollection',
29
+ ]);
30
+ const FIND_MAX_LIMIT = 50;
31
+ const ROWS_MAX = 500;
32
+ const CELL_TRUNC = 1000;
33
+ function trunc(s, max = CELL_TRUNC) {
34
+ return s.length > max ? `${s.slice(0, max)}…[截断,共${s.length}字符]` : s;
35
+ }
36
+ /** NormalizedCell 规范化:ObjectId/Decimal128/Long/Timestamp → toString();Binary → hex 截断;Date → ISO;嵌套对象/数组 → JSON 截断 1000 */
37
+ export function normalizeMongoCell(v) {
38
+ if (v === null || v === undefined)
39
+ return null;
40
+ if (typeof v === 'number')
41
+ return Number.isFinite(v) ? v : String(v);
42
+ if (typeof v === 'string')
43
+ return trunc(v);
44
+ if (typeof v === 'boolean')
45
+ return v ? 'true' : 'false';
46
+ if (v instanceof Date)
47
+ return v.toISOString();
48
+ if (v instanceof ObjectId || v instanceof Decimal128 || v instanceof Long || v instanceof Timestamp) {
49
+ return trunc(v.toString());
50
+ }
51
+ if (v instanceof Binary)
52
+ return trunc(Buffer.from(v.buffer ?? []).toString('hex') || String(v));
53
+ if (typeof v === 'object') {
54
+ try {
55
+ return trunc(JSON.stringify(v) ?? String(v));
56
+ }
57
+ catch {
58
+ return trunc(String(v));
59
+ }
60
+ }
61
+ return trunc(String(v));
62
+ }
63
+ function isPlainObject(v) {
64
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
65
+ }
66
+ /** 可执行服务端 JS 的键(代码注入面) */
67
+ const JS_INJECTION_KEYS = new Set(['$where', '$function', '$accumulator']);
68
+ /** 有写副作用的聚合阶段(只读 query 通道禁止) */
69
+ const WRITE_STAGE_KEYS = new Set(['$out', '$merge', '$unionWith']);
70
+ /** 递归检测命令文档:拒绝服务端 JS 键($where/$function/$accumulator)与写副作用聚合阶段($out/$merge/$unionWith) */
71
+ function assertNoWhere(node, path = '$') {
72
+ if (Array.isArray(node)) {
73
+ node.forEach((child, i) => assertNoWhere(child, `${path}[${i}]`));
74
+ return;
75
+ }
76
+ if (isPlainObject(node)) {
77
+ for (const [k, v] of Object.entries(node)) {
78
+ if (JS_INJECTION_KEYS.has(k)) {
79
+ throw new Error(`MongoDB 查询拒绝使用 ${k}(${path}.${k}):可执行服务端 JS 代码,存在注入风险;请改用查询运算符`);
80
+ }
81
+ if (WRITE_STAGE_KEYS.has(k)) {
82
+ throw new Error(`MongoDB 查询拒绝聚合阶段 ${k}(${path}.${k}):该阶段有写副作用(写入/创建目标集合),只读通道不允许;如需落盘请走 execute 通道并评估权限`);
83
+ }
84
+ assertNoWhere(v, `${path}.${k}`);
85
+ }
86
+ }
87
+ }
88
+ /** 解析命令文档字符串为对象(首个键为操作名) */
89
+ function parseCommandDoc(input) {
90
+ let doc;
91
+ try {
92
+ doc = JSON.parse(input);
93
+ }
94
+ catch (e) {
95
+ throw new Error(`MongoDB 命令不是合法 JSON:${e instanceof Error ? e.message : String(e)}。示例:{"find":"users","filter":{},"limit":10}`);
96
+ }
97
+ if (!isPlainObject(doc) || Object.keys(doc).length === 0) {
98
+ throw new Error('MongoDB 命令必须为非空 JSON 文档对象,操作名为第一个键,如 {"find":"users"}');
99
+ }
100
+ return doc;
101
+ }
102
+ /** 空 filter 校验:updateMany/deleteMany 必须显式 filter(危险操作确认前的适配器级防线) */
103
+ export function assertExplicitFilter(op, filter) {
104
+ const empty = filter === null || filter === undefined ||
105
+ (isPlainObject(filter) && Object.keys(filter).length === 0);
106
+ if (empty) {
107
+ throw new Error(`MongoDB ${op} 缺少显式 filter:空条件会作用于全集合。如确需全集合操作,请使用显式条件(如 {"_id":{"$exists":true}})并经危险操作确认`);
108
+ }
109
+ }
110
+ function requireObject(v, label) {
111
+ if (!isPlainObject(v))
112
+ throw new Error(`MongoDB ${label} 必须为 JSON 对象`);
113
+ return v;
114
+ }
115
+ export async function createMongoAdapter(conn, opts) {
116
+ const mode = opts?.mode ?? 'rw';
117
+ let client;
118
+ if (opts?.client) {
119
+ client = opts.client;
120
+ }
121
+ else {
122
+ const uri = conn.url ?? buildUri(conn.fields, conn.ssl);
123
+ client = new MongoClient(uri, {
124
+ maxPoolSize: 10,
125
+ readPreference: 'primary',
126
+ w: 'majority',
127
+ family: 4,
128
+ });
129
+ try {
130
+ await client.connect();
131
+ }
132
+ catch (e) {
133
+ throw new Error(`MongoDB 连接失败(${sanitizeUri(uri)}):${e instanceof Error ? e.message : String(e)}`);
134
+ }
135
+ }
136
+ const dbName = typeof conn.meta.database === 'string' && conn.meta.database !== ''
137
+ ? conn.meta.database
138
+ : defaultDbName(conn.fields);
139
+ const db = client.db(dbName);
140
+ function wrap(e) {
141
+ if (e instanceof MongoServerError) {
142
+ return new Error(`MongoDB 服务端错误:${e.message}${e.codeName ? `(${e.codeName})` : ''}`);
143
+ }
144
+ return new Error(`MongoDB 错误:${e instanceof Error ? e.message : String(e)}`);
145
+ }
146
+ function requireRw(action) {
147
+ if (mode === 'ro') {
148
+ throw new Error(`连接为只读(ro)模式,拒绝执行${action};请使用 rw 连接或只发查询(query)`);
149
+ }
150
+ }
151
+ function collOf(cmd, op) {
152
+ const name = cmd[op];
153
+ if (typeof name !== 'string' || name === '') {
154
+ throw new Error(`MongoDB ${op} 需要集合名字符串,如 {"${op}":"users"}`);
155
+ }
156
+ return name;
157
+ }
158
+ const adapter = {
159
+ kind: 'mongodb',
160
+ connId: conn.meta.id,
161
+ async testConnect() {
162
+ try {
163
+ const hello = await client.db('admin').command({ hello: 1 });
164
+ return { ok: true, serverInfo: `MongoDB ${String(hello.version ?? '')}` };
165
+ }
166
+ catch (e) {
167
+ return { ok: false, error: wrap(e).message };
168
+ }
169
+ },
170
+ async query(sql) {
171
+ try {
172
+ const cmd = parseCommandDoc(sql);
173
+ assertNoWhere(cmd);
174
+ const op = Object.keys(cmd)[0];
175
+ if (!READ_OPS.has(op)) {
176
+ throw new Error(`MongoDB query 不支持操作 ${op}。读操作: ${[...READ_OPS].sort().join('/')};写操作请走 execute`);
177
+ }
178
+ switch (op) {
179
+ case 'find': {
180
+ const name = collOf(cmd, 'find');
181
+ const filter = isPlainObject(cmd.filter) ? cmd.filter : {};
182
+ const sort = isPlainObject(cmd.sort) ? cmd.sort : { _id: 1 };
183
+ const limitRaw = typeof cmd.limit === 'number' && cmd.limit > 0 ? Math.floor(cmd.limit) : 20;
184
+ const limit = Math.min(limitRaw, FIND_MAX_LIMIT);
185
+ const skip = typeof cmd.skip === 'number' && cmd.skip > 0 ? Math.floor(cmd.skip) : undefined;
186
+ const projection = isPlainObject(cmd.projection) ? cmd.projection : undefined;
187
+ const cursor = db.collection(name).find(filter, {
188
+ sort: sort,
189
+ limit,
190
+ ...(skip !== undefined ? { skip } : {}),
191
+ ...(projection ? { projection: projection } : {}),
192
+ });
193
+ const docs = await cursor.toArray();
194
+ return docsToResult(docs, false);
195
+ }
196
+ case 'aggregate': {
197
+ let stages;
198
+ if (Array.isArray(cmd.aggregate)) {
199
+ stages = cmd.aggregate;
200
+ }
201
+ else if (Array.isArray(cmd.pipeline)) {
202
+ stages = cmd.pipeline;
203
+ }
204
+ else {
205
+ throw new Error('MongoDB aggregate 需要阶段数组:{"aggregate":"coll","pipeline":[{"$match":{}}]}');
206
+ }
207
+ const name = typeof cmd.aggregate === 'string' ? cmd.aggregate : undefined;
208
+ if (name === undefined)
209
+ throw new Error('MongoDB aggregate 需要集合名:{"aggregate":"coll","pipeline":[...]}');
210
+ const docs = await db.collection(name).aggregate(stages).limit(ROWS_MAX).toArray();
211
+ return docsToResult(docs, docs.length >= ROWS_MAX);
212
+ }
213
+ case 'count': {
214
+ const name = collOf(cmd, 'count');
215
+ const n = await db.collection(name).countDocuments(isPlainObject(cmd.query) ? cmd.query : {});
216
+ return { columns: ['count'], rows: [[n]], rowCount: 1 };
217
+ }
218
+ case 'countDocuments': {
219
+ const name = collOf(cmd, 'countDocuments');
220
+ const n = await db.collection(name).countDocuments(isPlainObject(cmd.filter) ? cmd.filter : {});
221
+ return { columns: ['count'], rows: [[n]], rowCount: 1 };
222
+ }
223
+ case 'estimatedDocumentCount': {
224
+ const name = collOf(cmd, 'estimatedDocumentCount');
225
+ const n = await db.collection(name).estimatedDocumentCount();
226
+ return { columns: ['count'], rows: [[n]], rowCount: 1 };
227
+ }
228
+ case 'distinct': {
229
+ const name = collOf(cmd, 'distinct');
230
+ if (typeof cmd.key !== 'string' || cmd.key === '') {
231
+ throw new Error('MongoDB distinct 需要 key 字段名:{"distinct":"coll","key":"field","query":{}}');
232
+ }
233
+ const vals = await db.collection(name).distinct(cmd.key, isPlainObject(cmd.query) ? cmd.query : {});
234
+ const rows = vals.map((v) => [normalizeMongoCell(v)]);
235
+ return { columns: [cmd.key], rows, rowCount: rows.length, truncated: rows.length >= ROWS_MAX || undefined };
236
+ }
237
+ case 'listCollections': {
238
+ const infos = await db.listCollections().toArray();
239
+ const rows = infos.map((c) => [c.name, c.type ?? 'collection']);
240
+ return { columns: ['name', 'type'], rows, rowCount: rows.length };
241
+ }
242
+ case 'dbStats': {
243
+ const stats = await db.command({ dbStats: 1 });
244
+ return objectToResult(stats);
245
+ }
246
+ case 'collStats': {
247
+ const name = collOf(cmd, 'collStats');
248
+ const stats = await db.command({ collStats: name });
249
+ return objectToResult(stats);
250
+ }
251
+ case 'indexes': {
252
+ const name = collOf(cmd, 'indexes');
253
+ const idxs = await db.collection(name).indexes();
254
+ const rows = idxs.map((ix) => [
255
+ String(ix.name ?? ''),
256
+ normalizeMongoCell(ix.key),
257
+ ix.unique === true ? 'true' : 'false',
258
+ ]);
259
+ return { columns: ['name', 'key', 'unique'], rows, rowCount: rows.length };
260
+ }
261
+ default:
262
+ throw new Error(`MongoDB query 暂未实现操作 ${op}`);
263
+ }
264
+ }
265
+ catch (e) {
266
+ throw wrap(e);
267
+ }
268
+ },
269
+ async execute(statement) {
270
+ requireRw('写操作(execute)');
271
+ try {
272
+ const cmd = parseCommandDoc(statement);
273
+ assertNoWhere(cmd);
274
+ const op = Object.keys(cmd)[0];
275
+ if (!WRITE_OPS.has(op)) {
276
+ throw new Error(`MongoDB execute 不支持操作 ${op}。写操作: ${[...WRITE_OPS].sort().join('/')}`);
277
+ }
278
+ if (op === 'createCollection') {
279
+ const name = collOf(cmd, 'createCollection');
280
+ await db.createCollection(name);
281
+ return { message: `已创建集合 ${name}` };
282
+ }
283
+ if (op === 'dropCollection') {
284
+ const name = collOf(cmd, 'dropCollection');
285
+ const dropped = await db.dropCollection(name);
286
+ return dropped
287
+ ? { message: `已删除集合 ${name}` }
288
+ : { message: `集合 ${name} 不存在,未删除` };
289
+ }
290
+ if (op === 'renameCollection') {
291
+ const from = collOf(cmd, 'renameCollection');
292
+ if (typeof cmd.to !== 'string' || cmd.to === '') {
293
+ throw new Error('MongoDB renameCollection 需要 to 目标名:{"renameCollection":"a","to":"b"}');
294
+ }
295
+ await db.collection(from).rename(cmd.to);
296
+ return { message: `已将集合 ${from} 重命名为 ${cmd.to}` };
297
+ }
298
+ if (op === 'createIndex' || op === 'createIndexes') {
299
+ const name = collOf(cmd, op);
300
+ const specs = isPlainObject(cmd.index)
301
+ ? [cmd.index]
302
+ : Array.isArray(cmd.index)
303
+ ? cmd.index
304
+ : isPlainObject(cmd.indexes) ? [cmd.indexes] : Array.isArray(cmd.indexes) ? cmd.indexes : null;
305
+ if (!specs)
306
+ throw new Error('MongoDB createIndex 需要 index 规格对象或数组:{"createIndex":"coll","index":{"key":{"a":1},"name":"a_1"}}');
307
+ const created = (await db.collection(name).createIndexes(specs));
308
+ return { affectedRows: created.createdNewIndexes ?? undefined, message: `已在集合 ${name} 创建索引(${created.createdNewIndexes ?? '?'} 个新增)` };
309
+ }
310
+ if (op === 'dropIndex' || op === 'dropIndexes') {
311
+ const name = collOf(cmd, op);
312
+ const index = typeof cmd.index === 'string' ? cmd.index : undefined;
313
+ if (index === undefined)
314
+ throw new Error('MongoDB dropIndex 需要 index 名:{"dropIndex":"coll","index":"a_1"}');
315
+ await db.collection(name).dropIndex(index);
316
+ return { message: `已删除集合 ${name} 的索引 ${index}` };
317
+ }
318
+ // ---- 文档级 DML ----
319
+ const name = collOf(cmd, op);
320
+ const c = db.collection(name);
321
+ switch (op) {
322
+ case 'insertOne': {
323
+ // 不设 cmd.insert 别名:该键语义是集合名(insertMany 命令形态),混用会误导报错方向
324
+ const doc = requireObject(cmd.document, 'insertOne.document');
325
+ const r = await c.insertOne(doc);
326
+ return { affectedRows: 1, message: `已插入 1 条文档到 ${name}(_id=${String(r.insertedId)})` };
327
+ }
328
+ case 'insertMany': {
329
+ if (!Array.isArray(cmd.documents ?? cmd.docs) || (cmd.documents ?? cmd.docs).length === 0) {
330
+ throw new Error('MongoDB insertMany 需要非空 documents 数组:{"insertMany":"coll","documents":[{...}]}');
331
+ }
332
+ const docs = (cmd.documents ?? cmd.docs).map((d) => requireObject(d, 'insertMany.documents[]'));
333
+ const r = await c.insertMany(docs);
334
+ return { affectedRows: r.insertedCount, message: `已插入 ${r.insertedCount} 条文档到 ${name}` };
335
+ }
336
+ case 'updateOne':
337
+ case 'updateMany': {
338
+ if (op === 'updateMany')
339
+ assertExplicitFilter(op, cmd.filter);
340
+ const filter = requireObject(cmd.filter, `${op}.filter`);
341
+ const update = requireObject(cmd.update, `${op}.update`);
342
+ const r = op === 'updateOne'
343
+ ? await c.updateOne(filter, update)
344
+ : await c.updateMany(filter, update);
345
+ return {
346
+ affectedRows: r.modifiedCount,
347
+ message: `${op} 完成:匹配 ${r.matchedCount} 条,修改 ${r.modifiedCount} 条(${name})`,
348
+ };
349
+ }
350
+ case 'replaceOne': {
351
+ const filter = requireObject(cmd.filter, 'replaceOne.filter');
352
+ const replacement = requireObject(cmd.replacement ?? cmd.update, 'replaceOne.replacement');
353
+ const r = await c.replaceOne(filter, replacement);
354
+ return {
355
+ affectedRows: r.modifiedCount,
356
+ message: `replaceOne 完成:匹配 ${r.matchedCount} 条,替换 ${r.modifiedCount} 条(${name})`,
357
+ };
358
+ }
359
+ case 'deleteOne':
360
+ case 'deleteMany': {
361
+ if (op === 'deleteMany')
362
+ assertExplicitFilter(op, cmd.filter);
363
+ const filter = requireObject(cmd.filter, `${op}.filter`);
364
+ const r = op === 'deleteOne'
365
+ ? await c.deleteOne(filter)
366
+ : await c.deleteMany(filter);
367
+ return { affectedRows: r.deletedCount, message: `${op} 完成:删除 ${r.deletedCount} 条(${name})` };
368
+ }
369
+ default:
370
+ throw new Error(`MongoDB execute 暂未实现操作 ${op}`);
371
+ }
372
+ }
373
+ catch (e) {
374
+ throw wrap(e);
375
+ }
376
+ },
377
+ async listDatabases() {
378
+ try {
379
+ const infos = await client.db('admin').admin().listDatabases();
380
+ return infos.databases.map((d) => d.name);
381
+ }
382
+ catch (e) {
383
+ throw wrap(e);
384
+ }
385
+ },
386
+ async listTables() {
387
+ try {
388
+ const infos = await db.listCollections().toArray();
389
+ return infos.map((c) => ({ name: c.name, type: c.type === 'view' ? 'view' : 'collection' }));
390
+ }
391
+ catch (e) {
392
+ throw wrap(e);
393
+ }
394
+ },
395
+ async describeTable(name) {
396
+ try {
397
+ const c = db.collection(name);
398
+ const sample = await c.find({}).limit(10).toArray();
399
+ const idxs = await c.indexes();
400
+ const cols = new Map();
401
+ for (const doc of sample) {
402
+ for (const [k, v] of Object.entries(doc)) {
403
+ const entry = cols.get(k) ?? { types: new Set(), present: 0 };
404
+ entry.types.add(inferBsonType(v));
405
+ entry.present += 1;
406
+ cols.set(k, entry);
407
+ }
408
+ }
409
+ const indexByField = new Map();
410
+ for (const ix of idxs) {
411
+ for (const field of Object.keys((ix.key ?? {}))) {
412
+ indexByField.set(field, [...(indexByField.get(field) ?? []), String(ix.name ?? '')]);
413
+ }
414
+ }
415
+ const out = [];
416
+ for (const [field, entry] of cols) {
417
+ const types = [...entry.types].sort().join('|');
418
+ const indexNames = indexByField.get(field);
419
+ out.push({
420
+ name: field,
421
+ dataType: `${types} (inferred)`,
422
+ nullable: sample.length > 0 && entry.present < sample.length,
423
+ ...(field === '_id' ? { key: 'PRI' } : {}),
424
+ comment: indexNames && indexNames.length > 0 ? `index: ${indexNames.join(', ')}` : undefined,
425
+ });
426
+ }
427
+ // _id 未出现在样本中(空集合)也要给出主键提示
428
+ if (!out.some((c) => c.name === '_id')) {
429
+ out.unshift({ name: '_id', dataType: 'ObjectId (inferred)', nullable: false, key: 'PRI', comment: '空集合样本推断' });
430
+ }
431
+ return out;
432
+ }
433
+ catch (e) {
434
+ throw wrap(e);
435
+ }
436
+ },
437
+ async previewRows(name, limit, _database, offset) {
438
+ try {
439
+ const n = Math.max(1, Math.min(Math.floor(limit) || 20, FIND_MAX_LIMIT));
440
+ const skip = Math.max(0, Math.floor(offset ?? 0) || 0);
441
+ const docs = await db.collection(name).find({}).sort({ _id: 1 }).skip(skip).limit(n).toArray();
442
+ return docsToResult(docs, false);
443
+ }
444
+ catch (e) {
445
+ throw wrap(e);
446
+ }
447
+ },
448
+ async close() {
449
+ await client.close();
450
+ },
451
+ };
452
+ /** 文档数组 → QueryResult(列 = 字段并集,保持首次出现顺序) */
453
+ function docsToResult(docs, truncated) {
454
+ const columns = [];
455
+ for (const d of docs) {
456
+ for (const k of Object.keys(d)) {
457
+ if (!columns.includes(k))
458
+ columns.push(k);
459
+ }
460
+ }
461
+ const rows = docs.map((d) => columns.map((c) => normalizeMongoCell(d[c])));
462
+ return { columns, rows, rowCount: rows.length, truncated: truncated || undefined };
463
+ }
464
+ return adapter;
465
+ }
466
+ /** 单层统计对象 → ['name','value'] 两列 */
467
+ function objectToResult(obj) {
468
+ const rows = Object.entries(obj).map(([k, v]) => [k, normalizeMongoCell(v)]);
469
+ return { columns: ['name', 'value'], rows, rowCount: rows.length };
470
+ }
471
+ function inferBsonType(v) {
472
+ if (v === null)
473
+ return 'null';
474
+ if (v instanceof ObjectId)
475
+ return 'ObjectId';
476
+ if (v instanceof Decimal128)
477
+ return 'Decimal128';
478
+ if (v instanceof Long)
479
+ return 'Long';
480
+ if (v instanceof Timestamp)
481
+ return 'Timestamp';
482
+ if (v instanceof Binary)
483
+ return 'Binary';
484
+ if (v instanceof Date)
485
+ return 'date';
486
+ if (Array.isArray(v))
487
+ return 'array';
488
+ if (typeof v === 'object')
489
+ return 'object';
490
+ return typeof v;
491
+ }
492
+ function buildUri(fields, ssl) {
493
+ const host = typeof fields?.host === 'string' && fields.host !== '' ? fields.host : '127.0.0.1';
494
+ const port = typeof fields?.port === 'number' ? fields.port : 27017;
495
+ const user = typeof fields?.user === 'string' ? encodeURIComponent(fields.user) : '';
496
+ const pass = typeof fields?.password === 'string' ? `:${encodeURIComponent(fields.password)}` : '';
497
+ const auth = user !== '' ? `${user}${pass}@` : '';
498
+ const scheme = ssl || fields?.ssl === true || fields?.tls === true ? 'mongodb+srv' : 'mongodb';
499
+ const authDb = typeof fields?.authDatabase === 'string' ? `?authSource=${fields.authDatabase}` : '';
500
+ return `${scheme}://${auth}${host}:${port}${authDb}`;
501
+ }
502
+ function defaultDbName(fields) {
503
+ return typeof fields?.database === 'string' && fields.database !== '' ? fields.database : 'test';
504
+ }
505
+ function sanitizeUri(uri) {
506
+ return uri.replace(/:\/\/[^@/]*@/, '://***:***@');
507
+ }
508
+ export const factory = async (conn) => createMongoAdapter(conn);