Files
dbx/packages/node-core/tests/mongo-query.test.ts
T
徐文彬andClaude Opus 4.7 c56bcb8e2d feat(mongodb): 支持聚合查询、批量写入和 MCP 集成
- 桌面端:解析 db.<集合>.aggregate([...]) shell 语法,新增 mongoAggregateDocuments
  接口(Tauri + HTTP);mongo 入口统一补 ensureConnected;ObjectId(...) 自动
  转换为 {"\$oid":"..."} 扩展 JSON 表示。
- dbx-core:新增 aggregate_documents、insert/update/delete_documents bulk
  操作;扩展 JSON 解析识别 \$oid;schema list_tables_core 补 MongoDB/Elasticsearch
  分支返回 COLLECTION/INDEX。
- dbx-web / Tauri 命令 / MCP bridge:注册 4 个新 mongo 路由
  (aggregate/insert/update/delete documents);MCP bridge 新增 mongo HTTP 入口
  和 lazy 连接复用。
- node-core (MCP 后端):mongodb 走专属 executeQuery / listTables /
  describeTable;read-only 默认 + DBX_MCP_ALLOW_WRITES /
  DBX_MCP_ALLOW_DANGEROUS_SQL 两级开关;aggregate 含 \$out/\$merge 走同样开关;
  describeTable 抽样 20 条文档推断字段类型,_id 自动标记主键。
- mcp-server:mongodb 跳过 SQL safety evaluator(mongo shell 与 SQL 语法
  不兼容),改由 backend 做命令级 gating;附注释。
- AiAssistant 同时 emit replaceSql + executeSql 让编辑器和结果保持一致;
  App.vue ensureQueryTab 在无上下文时回退到连接默认库;aiMessageRender
  识别 mongodb / mongo 代码块。
- Review 修复:合并两份重复的 evaluateMongo*Safety 实现(同时修复 web-backend
  用字符串比较导致空 filter 检查可被空格绕过的 bug);ObjectId 正则兼容
  单/双引号;mongo_ops 错误文案 "MCP" 改为 "bulk";补 aggregate 解析测试用例。

测试:pnpm test 911 通过;vue-tsc 通过;cargo check -p dbx-core 通过。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 21:25:43 +08:00

153 lines
5.0 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import {
executeQuery,
inferMongoColumns,
mongoAggregateWriteStage,
mongoDocumentsToQueryResult,
parseMongoAggregateCommand,
parseMongoCountDocumentsCommand,
parseMongoFindCommand,
parseMongoWriteCommand,
} from "../src/database.js";
test("parseMongoFindCommand accepts shell-style find commands", () => {
assert.deepEqual(parseMongoFindCommand('db.getCollection("operation_logs").find({"level":"info"}).sort({"ts":-1}).skip(5).limit(10)'), {
collection: "operation_logs",
filter: '{"level":"info"}',
skip: 5,
limit: 10,
sort: '{"ts":-1}',
});
});
test("parseMongoCountDocumentsCommand accepts shell-style count commands", () => {
assert.deepEqual(parseMongoCountDocumentsCommand('db.projects.countDocuments({"active":true})'), {
collection: "projects",
filter: '{"active":true}',
});
});
test("parseMongoAggregateCommand accepts aggregate pipelines", () => {
assert.deepEqual(parseMongoAggregateCommand('db.projects.aggregate([{"$match":{"active":true}},{"$group":{"_id":"$owner","total":{"$sum":1}}}])'), {
collection: "projects",
pipeline: '[{"$match":{"active":true}},{"$group":{"_id":"$owner","total":{"$sum":1}}}]',
});
});
test("mongoAggregateWriteStage detects write stages", () => {
assert.equal(mongoAggregateWriteStage('[{"$match":{"active":true}}]'), null);
assert.equal(mongoAggregateWriteStage('[{"$match":{}},{"$out":"projects_dump"}]'), "$out");
assert.equal(mongoAggregateWriteStage('[{"$merge":{"into":"projects_dump"}}]'), "$merge");
});
test("mongodb executeQuery blocks aggregate write stages without both env flags", async () => {
const oldAllowWrites = process.env.DBX_MCP_ALLOW_WRITES;
const oldAllowDangerous = process.env.DBX_MCP_ALLOW_DANGEROUS_SQL;
delete process.env.DBX_MCP_ALLOW_WRITES;
delete process.env.DBX_MCP_ALLOW_DANGEROUS_SQL;
const config = {
id: "mongo",
name: "mongo",
db_type: "mongodb",
host: "127.0.0.1",
port: 27017,
username: "",
password: "",
database: "app",
ssh_enabled: false,
ssl: false,
} as const;
await assert.rejects(executeQuery(config, 'db.projects.aggregate([{"$out":"projects_dump"}])'), /DBX_MCP_ALLOW_WRITES=1/);
process.env.DBX_MCP_ALLOW_WRITES = "1";
await assert.rejects(
executeQuery(config, 'db.projects.aggregate([{"$merge":{"into":"projects_dump"}}])'),
/DBX_MCP_ALLOW_DANGEROUS_SQL=1/,
);
if (oldAllowWrites === undefined) delete process.env.DBX_MCP_ALLOW_WRITES;
else process.env.DBX_MCP_ALLOW_WRITES = oldAllowWrites;
if (oldAllowDangerous === undefined) delete process.env.DBX_MCP_ALLOW_DANGEROUS_SQL;
else process.env.DBX_MCP_ALLOW_DANGEROUS_SQL = oldAllowDangerous;
});
test("parseMongoWriteCommand accepts supported write commands", () => {
assert.deepEqual(parseMongoWriteCommand('db.projects.insertOne({"name":"demo"})'), {
kind: "insert",
collection: "projects",
docsJson: '{"name":"demo"}',
});
assert.deepEqual(parseMongoWriteCommand('db.projects.updateOne({"_id":"1"},{"$set":{"name":"next"}})'), {
kind: "update",
collection: "projects",
filter: '{"_id":"1"}',
update: '{"$set":{"name":"next"}}',
many: false,
});
assert.deepEqual(parseMongoWriteCommand('db.projects.deleteMany({"stale":true})'), {
kind: "delete",
collection: "projects",
filter: '{"stale":true}',
many: true,
});
});
test("mongodb executeQuery blocks writes without the write env flag", async () => {
const oldAllowWrites = process.env.DBX_MCP_ALLOW_WRITES;
delete process.env.DBX_MCP_ALLOW_WRITES;
await assert.rejects(
executeQuery(
{
id: "mongo",
name: "mongo",
db_type: "mongodb",
host: "127.0.0.1",
port: 27017,
username: "",
password: "",
database: "app",
ssh_enabled: false,
ssl: false,
},
'db.projects.insertOne({"name":"demo"})',
),
/DBX_MCP_ALLOW_WRITES=1/,
);
if (oldAllowWrites === undefined) delete process.env.DBX_MCP_ALLOW_WRITES;
else process.env.DBX_MCP_ALLOW_WRITES = oldAllowWrites;
});
test("mongoDocumentsToQueryResult turns documents into rows", () => {
assert.deepEqual(mongoDocumentsToQueryResult([{ _id: "1", nested: { ok: true } }, { _id: "2", name: "demo" }], 2), {
columns: ["_id", "nested", "name"],
rows: [
{ _id: "1", nested: '{"ok":true}', name: undefined },
{ _id: "2", nested: undefined, name: "demo" },
],
row_count: 2,
});
});
test("inferMongoColumns marks _id as primary and reports observed types", () => {
assert.deepEqual(inferMongoColumns([{ _id: "1", active: true }, { _id: "2", active: null }]), [
{
name: "_id",
data_type: "string",
is_nullable: false,
column_default: null,
is_primary_key: true,
comment: null,
},
{
name: "active",
data_type: "boolean | null",
is_nullable: true,
column_default: null,
is_primary_key: false,
comment: null,
},
]);
});