mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
1980 lines
84 KiB
TypeScript
Vendored
1980 lines
84 KiB
TypeScript
Vendored
import { strict as assert } from "node:assert";
|
||
import { test } from "vitest";
|
||
import {
|
||
describeMongoCommandParseFailure,
|
||
evaluateMongoAggregateSafety,
|
||
evaluateMongoWriteSafety,
|
||
mongoAggregateWriteStage,
|
||
listChainedCalls,
|
||
MONGO_SHELL_COMMAND_HINT,
|
||
parseMongoFindExplainCommand,
|
||
mongoBulkWriteToQueryResult,
|
||
mongoCollectionStatsToQueryResult,
|
||
mongoCountToQueryResult,
|
||
mongoDatabasesToQueryResult,
|
||
mongoDistinctToQueryResult,
|
||
mongoDocumentsToQueryResult,
|
||
mongoDroppedIndexesToQueryResult,
|
||
mongoFindLogicalTotal,
|
||
mongoIndexesToQueryResult,
|
||
normalizeRustMongoCommand,
|
||
parseMongoAggregateCommand,
|
||
parseMongoCollectionStatsCommand,
|
||
parseMongoCommand,
|
||
parseMongoCreateUserCommand,
|
||
parseMongoRunCommand,
|
||
parseMongoCountDocumentsCommand,
|
||
parseMongoDistinctCommand,
|
||
parseMongoFindCommand,
|
||
parseMongoFindOneCommand,
|
||
parseMongoFindOneAndUpdateCommand,
|
||
parseMongoFindOneAndReplaceCommand,
|
||
parseMongoFindOneAndDeleteCommand,
|
||
parseMongoGetIndexesCommand,
|
||
parseMongoVersionCommand,
|
||
planMongoFindPagination,
|
||
mongoFindPageCoversLogicalLimit,
|
||
parseMongoWriteCommand,
|
||
splitMongoCommands,
|
||
splitMongoCommandRanges,
|
||
} from "../../apps/desktop/src/lib/mongo/mongoShellCommand.ts";
|
||
import type { MongoWriteCommand } from "../../apps/desktop/src/lib/mongo/mongoShellCommand.ts";
|
||
import { buildMongoUpdateDocument as buildMongoDocumentUpdate, formatMongoShellLiteral as formatMongoDocumentShellLiteral, mongoDocumentGridDisplayText, mongoDocumentGridEditorText, mongoDocumentGridValue } from "../../apps/desktop/src/lib/mongo/mongoDocumentValues.ts";
|
||
import { normalizeJsonArgument } from "../mongo-shell/src/json.ts";
|
||
|
||
test("parseMongoFindCommand parses db collection find with an empty JSON filter", () => {
|
||
assert.deepEqual(parseMongoFindCommand("db.users.find({})"), {
|
||
collection: "users",
|
||
filter: "{}",
|
||
skip: 0,
|
||
limit: 100,
|
||
sort: undefined,
|
||
});
|
||
});
|
||
|
||
test("normalizeRustMongoCommand preserves the desktop command contract", () => {
|
||
assert.deepEqual(normalizeRustMongoCommand({ kind: "countDocuments", collection: "users", filter: "{}", accurate: false }), { kind: "countDocuments", collection: "users", filter: "{}", mode: "legacy" });
|
||
assert.deepEqual(normalizeRustMongoCommand({ kind: "dropIndexes", collection: "users", indexes: '"email_1"', single: true }), { kind: "dropIndex", collection: "users", index: '"email_1"' });
|
||
assert.deepEqual(normalizeRustMongoCommand({ kind: "findOne", collection: "users", filter: "{}", projection: null, options: null }), { kind: "findOne", collection: "users", filter: "{}" });
|
||
});
|
||
|
||
test("parseMongoCreateUserCommand normalizes user and write concern documents", () => {
|
||
assert.deepEqual(
|
||
parseMongoCreateUserCommand(`db.createUser({
|
||
user: "test-db",
|
||
pwd: "test-password",
|
||
roles: [{ role: "readWrite", db: "db1" }]
|
||
}, { w: "majority", wtimeout: 5000 })`),
|
||
{
|
||
userJson: '{"user":"test-db","pwd":"test-password","roles":[{"role":"readWrite","db":"db1"}]}',
|
||
writeConcernJson: '{"w":"majority","wtimeout":5000}',
|
||
},
|
||
);
|
||
assert.equal(parseMongoCreateUserCommand('db.createUser({pwd: "missing-user", roles: []})'), null);
|
||
assert.equal(parseMongoCreateUserCommand('db.createUser({user: "test"}, "majority")'), null);
|
||
});
|
||
|
||
test("splitMongoCommands executes use before createUser", () => {
|
||
const commands = splitMongoCommands(`use admin
|
||
|
||
db.createUser({
|
||
user: "test-db",
|
||
pwd: "test-password",
|
||
roles: [{ role: "readWrite", db: "db1" }]
|
||
})`);
|
||
|
||
assert.deepEqual(
|
||
commands.map(({ command }) => command.kind),
|
||
["use", "createUser"],
|
||
);
|
||
const createUser = commands[1]?.command;
|
||
assert.equal(createUser?.kind, "createUser");
|
||
if (createUser?.kind === "createUser") {
|
||
assert.equal(JSON.parse(createUser.userJson).user, "test-db");
|
||
assert.match(evaluateMongoWriteSafety(createUser, { allowWrites: true }).reason || "", /high-risk operations/i);
|
||
assert.equal(evaluateMongoWriteSafety(createUser, { allowWrites: true, allowDangerous: true }).allowed, true);
|
||
}
|
||
});
|
||
|
||
test("parseMongoRunCommand normalizes one non-empty command document", () => {
|
||
assert.deepEqual(
|
||
parseMongoRunCommand(`db.runCommand({
|
||
find: "orders",
|
||
filter: {_id: ObjectId("507f1f77bcf86cd799439011")},
|
||
createdAt: ISODate("2025-01-01T00:00:00Z")
|
||
})`),
|
||
{
|
||
commandJson: '{"find":"orders","filter":{"_id":{"$oid":"507f1f77bcf86cd799439011"}},"createdAt":{"$date":"2025-01-01T00:00:00Z"}}',
|
||
},
|
||
);
|
||
|
||
for (const source of ["db.runCommand()", "db.runCommand({})", "db.runCommand('ping')", "db.runCommand({ping: 1}, {readPreference: 'primary'})", "db.runCommand({ping: 1}).valueOf()", "db.runCommand([1, 2, 3])"]) {
|
||
assert.equal(parseMongoRunCommand(source), null, source);
|
||
}
|
||
});
|
||
|
||
test("parseMongoCommand accepts read-only show-databases aliases exactly", () => {
|
||
for (const source of ["show dbs", "SHOW DATABASES;", "/* databases */ show dbs -- list"]) {
|
||
assert.deepEqual(parseMongoCommand(source)?.command, { kind: "showDatabases" }, source);
|
||
}
|
||
|
||
for (const source of ["show dbs extra", "show database", "show collections", "show"]) {
|
||
assert.equal(parseMongoCommand(source), null, source);
|
||
}
|
||
|
||
assert.equal(evaluateMongoWriteSafety(parseMongoCommand("db.runCommand({listDatabases: 1})")!.command as MongoWriteCommand, {}).allowed, false);
|
||
});
|
||
|
||
test("splitMongoCommands keeps show databases after a database switch", () => {
|
||
assert.deepEqual(
|
||
splitMongoCommands("use app\nshow databases").map(({ command }) => command.kind),
|
||
["use", "showDatabases"],
|
||
);
|
||
});
|
||
|
||
test("mongoDatabasesToQueryResult preserves metadata and bounds rows", () => {
|
||
assert.deepEqual(
|
||
mongoDatabasesToQueryResult(
|
||
[
|
||
{
|
||
databases: [
|
||
{ name: "admin", sizeOnDisk: 40960, empty: false },
|
||
{ name: "app", sizeOnDisk: { $numberLong: "8192" }, empty: true },
|
||
{ name: "logs", sizeOnDisk: 1024, empty: false },
|
||
],
|
||
totalSize: 50176,
|
||
ok: 1,
|
||
},
|
||
],
|
||
4.4,
|
||
2,
|
||
),
|
||
{
|
||
columns: ["name", "sizeOnDisk", "empty"],
|
||
rows: [
|
||
["admin", 40960, false],
|
||
["app", "8192", true],
|
||
],
|
||
affected_rows: 3,
|
||
execution_time_ms: 4,
|
||
truncated: true,
|
||
has_more: true,
|
||
},
|
||
);
|
||
|
||
assert.deepEqual(mongoDatabasesToQueryResult([{ databases: [] }], 0, 10), {
|
||
columns: ["name", "sizeOnDisk", "empty"],
|
||
rows: [],
|
||
affected_rows: 0,
|
||
execution_time_ms: 0,
|
||
truncated: false,
|
||
has_more: false,
|
||
});
|
||
assert.throws(() => mongoDatabasesToQueryResult([{ ok: 1 }], 0, 10), /databases array/);
|
||
});
|
||
|
||
test("runCommand support does not accept arbitrary Mongo shell JavaScript", () => {
|
||
assert.deepEqual(
|
||
splitMongoCommands(`for (let i = 0; i < 2; i += 1) {
|
||
db.items.insertOne({ index: i });
|
||
}`),
|
||
[],
|
||
);
|
||
});
|
||
|
||
test("splitMongoCommands keeps runCommand after a database switch", () => {
|
||
const commands = splitMongoCommands(`use admin
|
||
|
||
db.runCommand({ ping: 1 })`);
|
||
|
||
assert.deepEqual(
|
||
commands.map(({ command }) => command.kind),
|
||
["use", "runCommand"],
|
||
);
|
||
const runCommand = commands[1]?.command;
|
||
assert.equal(runCommand?.kind, "runCommand");
|
||
if (runCommand?.kind === "runCommand") {
|
||
assert.deepEqual(JSON.parse(runCommand.commandJson), { ping: 1 });
|
||
assert.equal(evaluateMongoWriteSafety(runCommand, { allowWrites: true }).allowed, false);
|
||
assert.equal(evaluateMongoWriteSafety(runCommand, { allowWrites: true, allowDangerous: true }).allowed, true);
|
||
}
|
||
});
|
||
|
||
test("parseMongoFindCommand parses getCollection find with chained sort skip and limit", () => {
|
||
assert.deepEqual(parseMongoFindCommand('db.getCollection("audit.logs").find({"level":"warn"}).sort({"createdAt":-1}).skip(20).limit(10)'), {
|
||
collection: "audit.logs",
|
||
filter: '{"level":"warn"}',
|
||
skip: 20,
|
||
limit: 10,
|
||
sort: '{"createdAt":-1}',
|
||
});
|
||
});
|
||
|
||
test("parseMongoFindCommand preserves chained collation for execution and pagination", () => {
|
||
const command = parseMongoFindCommand(`db.t_user.find({name: 'xxx'})
|
||
.collation({ locale: "en", strength: 1 })
|
||
.limit(20)`);
|
||
|
||
assert.ok(command);
|
||
assert.equal(command.collection, "t_user");
|
||
assert.deepEqual(JSON.parse(command.filter), { name: "xxx" });
|
||
assert.deepEqual(JSON.parse(command.collation ?? "null"), { locale: "en", strength: 1 });
|
||
assert.equal(command.skip, 0);
|
||
assert.equal(command.limit, 20);
|
||
});
|
||
|
||
test("planMongoFindPagination pages unbounded find queries", () => {
|
||
const command = parseMongoFindCommand("db.users.find({})");
|
||
assert.ok(command);
|
||
const plan = planMongoFindPagination("db.users.find({})", command, 100, 100);
|
||
|
||
assert.deepEqual(plan, {
|
||
pageOffset: 100,
|
||
pageLimit: 100,
|
||
requestSkip: 100,
|
||
requestLimit: 100,
|
||
logicalSkip: 0,
|
||
logicalLimit: undefined,
|
||
});
|
||
assert.equal(mongoFindLogicalTotal(824, plan!), 824);
|
||
});
|
||
|
||
test("planMongoFindPagination preserves explicit skip and limit bounds", () => {
|
||
const source = "db.users.find({ active: true }).skip(20).limit(150)";
|
||
const command = parseMongoFindCommand(source);
|
||
assert.ok(command);
|
||
const plan = planMongoFindPagination(source, command, 100, 100);
|
||
|
||
assert.deepEqual(plan, {
|
||
pageOffset: 100,
|
||
pageLimit: 100,
|
||
requestSkip: 120,
|
||
requestLimit: 50,
|
||
logicalSkip: 20,
|
||
logicalLimit: 150,
|
||
});
|
||
assert.equal(mongoFindLogicalTotal(824, plan!), 150);
|
||
});
|
||
|
||
test("mongoFindPageCoversLogicalLimit only skips counting when the first page holds every limited row", () => {
|
||
const plan = (source: string, pageOffset: number, pageLimit = 100) => {
|
||
const command = parseMongoFindCommand(source);
|
||
assert.ok(command);
|
||
const result = planMongoFindPagination(source, command, pageOffset, pageLimit);
|
||
assert.ok(result);
|
||
return result;
|
||
};
|
||
|
||
assert.equal(mongoFindPageCoversLogicalLimit(plan("db.events.find({ type: 0 }).sort({ eventTime: -1 }).limit(50)", 0)), true);
|
||
assert.equal(mongoFindPageCoversLogicalLimit(plan("db.events.find({}).skip(20).limit(100)", 0)), true);
|
||
assert.equal(mongoFindPageCoversLogicalLimit(plan("db.events.find({}).limit(150)", 0)), false);
|
||
assert.equal(mongoFindPageCoversLogicalLimit(plan("db.events.find({}).limit(150)", 100)), false);
|
||
assert.equal(mongoFindPageCoversLogicalLimit(plan("db.events.find({})", 0)), false);
|
||
assert.equal(mongoFindPageCoversLogicalLimit(plan("db.events.find({}).limit(0)", 0)), false);
|
||
});
|
||
|
||
test("parseMongoFindCommand accepts line breaks before find and chained calls", () => {
|
||
const command = parseMongoFindCommand(`db.getCollection("accounting_reconciliations")
|
||
.find({
|
||
"_id": ObjectId("68ad51ca84c8127bc7d44cb3")
|
||
})
|
||
.sort({ lineNo: -1 })
|
||
.skip(5)
|
||
.limit(20)`);
|
||
assert.ok(command);
|
||
assert.equal(command.collection, "accounting_reconciliations");
|
||
assert.deepEqual(JSON.parse(command.filter), { _id: { $oid: "68ad51ca84c8127bc7d44cb3" } });
|
||
assert.deepEqual(JSON.parse(command.sort || "{}"), { lineNo: -1 });
|
||
assert.equal(command.skip, 5);
|
||
assert.equal(command.limit, 20);
|
||
});
|
||
|
||
test("parseMongoFindCommand accepts Compass-style unquoted keys and ObjectId", () => {
|
||
const command = parseMongoFindCommand("db.products.find({_id: ObjectId('6a045a92d2971e44243771a1')}).limit(1)");
|
||
assert.ok(command);
|
||
assert.equal(command.collection, "products");
|
||
assert.equal(command.limit, 1);
|
||
assert.deepEqual(JSON.parse(command.filter), { _id: { $oid: "6a045a92d2971e44243771a1" } });
|
||
});
|
||
|
||
test("parseMongoFindCommand does not rewrite NumberLong text inside strings", () => {
|
||
const command = parseMongoFindCommand('db.orders.find({label: "NumberLong(123)"})');
|
||
assert.deepEqual(command, {
|
||
collection: "orders",
|
||
filter: '{"label": "NumberLong(123)"}',
|
||
skip: 0,
|
||
limit: 100,
|
||
sort: undefined,
|
||
});
|
||
});
|
||
|
||
test("parseMongoFindCommand rewrites ISODate into extended JSON $date", () => {
|
||
const command = parseMongoFindCommand(`db.trainingdocuments.find({
|
||
createdAt: { $gte: ISODate("2025-02-25T04:57:39.965Z") }
|
||
})`);
|
||
assert.ok(command);
|
||
assert.equal(command.collection, "trainingdocuments");
|
||
assert.deepEqual(JSON.parse(command.filter), { createdAt: { $gte: { $date: "2025-02-25T04:57:39.965Z" } } });
|
||
});
|
||
|
||
test("parseMongoFindCommand rewrites new Date and single-quoted ISODate", () => {
|
||
const command = parseMongoFindCommand("db.events.find({ at: { $lt: new Date('2025-01-01T00:00:00Z'), $gte: ISODate('2024-01-01T00:00:00Z') } })");
|
||
assert.ok(command);
|
||
assert.deepEqual(JSON.parse(command.filter), {
|
||
at: { $lt: { $date: "2025-01-01T00:00:00Z" }, $gte: { $date: "2024-01-01T00:00:00Z" } },
|
||
});
|
||
});
|
||
|
||
test("parseMongoFindCommand rewrites NumberLong into extended JSON", () => {
|
||
const quoted = parseMongoFindCommand('db.orders.find({_id: NumberLong("2048938405781032962")})');
|
||
const unquoted = parseMongoFindCommand("db.orders.find({snowflake: NumberLong(9007199254740993)})");
|
||
|
||
assert.ok(quoted);
|
||
assert.deepEqual(JSON.parse(quoted.filter), { _id: { $numberLong: "2048938405781032962" } });
|
||
assert.ok(unquoted);
|
||
assert.deepEqual(JSON.parse(unquoted.filter), { snowflake: { $numberLong: "9007199254740993" } });
|
||
});
|
||
|
||
test("parseMongoWriteCommand fills in zero-argument new Date, ISODate and ObjectId", () => {
|
||
const before = Date.now();
|
||
const command = parseMongoWriteCommand(`db.reports.updateOne(
|
||
{ phone_number: "+84905421172", code: "VN" },
|
||
{ $set: { type: ObjectId("5bbc28701f3fd80f00e0211c"), created_at: new Date(), updated_at: ISODate() } },
|
||
{ upsert: true }
|
||
)`);
|
||
assert.ok(command);
|
||
assert.equal(command.kind, "update");
|
||
const update = JSON.parse(command.update) as { $set: Record<string, { $date?: string; $oid?: string }> };
|
||
assert.deepEqual(update.$set.type, { $oid: "5bbc28701f3fd80f00e0211c" });
|
||
for (const field of ["created_at", "updated_at"] as const) {
|
||
const filled = Date.parse(update.$set[field]!.$date!);
|
||
assert.ok(filled >= before && filled <= Date.now(), `${field} is not the current time`);
|
||
}
|
||
assert.match(JSON.parse(parseMongoFindCommand("db.reports.find({_id: ObjectId()})")!.filter)._id.$oid, /^[0-9a-f]{24}$/);
|
||
});
|
||
|
||
test("parseMongoFindCommand rewrites epoch-millisecond and numeric BSON constructors", () => {
|
||
const command = parseMongoFindCommand('db.orders.find({at: new Date(1735689600000), qty: NumberInt(3), total: NumberDecimal("12.34")})');
|
||
assert.ok(command);
|
||
assert.deepEqual(JSON.parse(command.filter), {
|
||
at: { $date: { $numberLong: "1735689600000" } },
|
||
qty: { $numberInt: "3" },
|
||
total: { $numberDecimal: "12.34" },
|
||
});
|
||
});
|
||
|
||
test("parseMongoCommand accepts the mongosh Int32, Long, Double and Decimal128 constructors", () => {
|
||
const insert = parseMongoCommand('db.getCollection("issue_demo").insert({\n name: "demo",\n coins: Int32("15000"),\n amount: Decimal128("100")\n});');
|
||
assert.equal(insert?.command.kind, "insert");
|
||
assert.deepEqual(JSON.parse((insert!.command as { docsJson: string }).docsJson), {
|
||
name: "demo",
|
||
coins: { $numberInt: "15000" },
|
||
amount: { $numberDecimal: "100" },
|
||
});
|
||
|
||
const command = parseMongoFindCommand('db.orders.find({a: Int32(3), b: new Int32(-4), c: Long("9223372036854775807"), d: Long(5), e: Double(1.5), f: Double("2"), g: Decimal128("12.34")})');
|
||
assert.ok(command);
|
||
assert.deepEqual(JSON.parse(command.filter), {
|
||
a: { $numberInt: "3" },
|
||
b: { $numberInt: "-4" },
|
||
c: { $numberLong: "9223372036854775807" },
|
||
d: { $numberLong: "5" },
|
||
e: { $numberDouble: "1.5" },
|
||
f: { $numberDouble: "2" },
|
||
g: { $numberDecimal: "12.34" },
|
||
});
|
||
|
||
for (const source of ["db.orders.find({a: Int32(2147483648)})", 'db.orders.find({a: Long("x")})', 'db.orders.find({a: Double("abc")})', "db.orders.find({a: Decimal128()})", "db.orders.find({a: myLong(1)})"]) {
|
||
assert.equal(parseMongoFindCommand(source), null, source);
|
||
}
|
||
assert.match(describeMongoCommandParseFailure("db.orders.find({a: myLong(1)})"), /Unsupported value myLong\(\.\.\.\)/);
|
||
assert.match(describeMongoCommandParseFailure("db.orders.find({a: Foo(1)})"), /Int32, Long, Double, Decimal128/);
|
||
});
|
||
|
||
test("parseMongoFindCommand rejects out-of-range integer constructor arguments", () => {
|
||
assert.equal(parseMongoFindCommand("db.orders.find({a: NumberLong(9223372036854775808)})"), null);
|
||
assert.equal(parseMongoFindCommand("db.orders.find({a: NumberLong(-9223372036854775809)})"), null);
|
||
assert.equal(parseMongoFindCommand("db.orders.find({a: NumberInt(2147483648)})"), null);
|
||
assert.equal(parseMongoFindCommand("db.orders.find({a: new Date(99999999999999999999)})"), null);
|
||
const atBounds = parseMongoFindCommand("db.orders.find({a: NumberLong(9223372036854775807), b: NumberInt(-2147483648)})");
|
||
assert.ok(atBounds);
|
||
assert.deepEqual(JSON.parse(atBounds.filter), { a: { $numberLong: "9223372036854775807" }, b: { $numberInt: "-2147483648" } });
|
||
});
|
||
|
||
test("parseMongoFindCommand does not rewrite constructor text inside strings", () => {
|
||
const command = parseMongoFindCommand(`db.orders.find({label: "new Date()", note: 'ObjectId()'})`);
|
||
assert.ok(command);
|
||
assert.deepEqual(JSON.parse(command.filter), { label: "new Date()", note: "ObjectId()" });
|
||
});
|
||
|
||
test("parseMongoCommand wraps db.getSiblingDB() commands with the target database", () => {
|
||
// The exact command from #3936.
|
||
const parsed = parseMongoCommand(`db.getSiblingDB("iam_account").getCollection("user")
|
||
.find({_id: NumberLong('144115205316939462')})
|
||
.sort({phone: 1})
|
||
.limit(21);`);
|
||
assert.ok(parsed && parsed.command.kind === "inDatabase");
|
||
assert.equal(parsed.command.database, "iam_account");
|
||
assert.equal(parsed.command.command.kind, "find");
|
||
assert.equal((parsed.command.command as { limit: number }).limit, 21);
|
||
assert.equal((parsed.command.command as { collection: string }).collection, "user");
|
||
|
||
assert.equal(parseMongoCommand("db.getSiblingDB('other').orders.updateOne({a: 1}, {$set: {b: 2}})")?.command.kind, "inDatabase");
|
||
assert.equal(parseMongoCommand('db . getSiblingDB( "x" ) . stats()')?.command.kind, "inDatabase");
|
||
// A sibling command is one statement in a batch, and a plain one after it is unaffected.
|
||
assert.deepEqual(
|
||
splitMongoCommandRanges('db.getSiblingDB("a").c.find({});\ndb.d.find({})').map((range) => range.command.kind),
|
||
["inDatabase", "find"],
|
||
);
|
||
// A command that parses is not a failure, even when asked directly.
|
||
assert.equal(describeMongoCommandParseFailure('db.getSiblingDB("other").c.find({})'), MONGO_SHELL_COMMAND_HINT);
|
||
});
|
||
|
||
test("parseMongoCommand rejects malformed db.getSiblingDB() with a specific reason", () => {
|
||
for (const [source, expected] of [
|
||
['db.getSiblingDB("x")', /requires a database name string followed by a command/],
|
||
['db.getSiblingDB("").c.find({})', /requires a database name string/],
|
||
["db.getSiblingDB(1).c.find({})", /requires a database name string/],
|
||
['db.getSiblingDB("x", "y").c.find({})', /requires a database name string/],
|
||
['db.getSiblingDB("x").getSiblingDB("y").c.find({})', /cannot be chained/],
|
||
// Errors inside the wrapped command are reported as they would be without the prefix.
|
||
['db.getSiblingDB("x").c.replaceOne({a: 1}, {$set: {b: 2}})', /must not contain update operators such as \$set/],
|
||
['db.getSiblingDB("x").c.bogus()', /Collection method bogus\(\) is not supported/],
|
||
] as const) {
|
||
assert.equal(parseMongoCommand(source), null, source);
|
||
assert.match(describeMongoCommandParseFailure(source), expected, source);
|
||
}
|
||
});
|
||
|
||
test('parseMongoCommand accepts db["name"] bracket collection accessors', () => {
|
||
const find = parseMongoFindCommand('db["orders-2024"].find({a: 1})');
|
||
assert.ok(find);
|
||
assert.equal(find.collection, "orders-2024");
|
||
assert.deepEqual(JSON.parse(find.filter), { a: 1 });
|
||
|
||
// Single quotes and a dotted name behave like db.<name>.
|
||
assert.equal(parseMongoFindCommand("db['audit.logs'].find({})")?.collection, "audit.logs");
|
||
|
||
assert.deepEqual(parseMongoWriteCommand('db["orders-2024"].updateOne({a: 1}, {$set: {b: 2}}, {upsert: true})'), {
|
||
kind: "update",
|
||
collection: "orders-2024",
|
||
filter: '{"a": 1}',
|
||
update: '{"$set": {"b": 2}}',
|
||
options: '{"upsert": true}',
|
||
many: false,
|
||
});
|
||
|
||
for (const source of ["db[].find({})", 'db["x"]find({})', 'db["x"]']) {
|
||
assert.equal(parseMongoCommand(source), null, source);
|
||
}
|
||
});
|
||
|
||
test("parseMongoFindCommand rewrites UUID, BinData, Timestamp and MinKey/MaxKey", () => {
|
||
const command = parseMongoFindCommand(`db.c.find({
|
||
u: UUID("3b241101-e2bb-4255-8caf-4136c566a962"),
|
||
b: BinData(128, "AQID"),
|
||
t: Timestamp(1735689600, 7),
|
||
lo: MinKey,
|
||
hi: MaxKey(),
|
||
range: {$gt: MinKey(), $lt: MaxKey},
|
||
list: [MinKey, MaxKey]
|
||
})`);
|
||
assert.ok(command);
|
||
assert.deepEqual(JSON.parse(command.filter), {
|
||
u: { $uuid: "3b241101-e2bb-4255-8caf-4136c566a962" },
|
||
b: { $binary: { base64: "AQID", subType: "80" } },
|
||
t: { $timestamp: { t: 1735689600, i: 7 } },
|
||
lo: { $minKey: 1 },
|
||
hi: { $maxKey: 1 },
|
||
range: { $gt: { $minKey: 1 }, $lt: { $maxKey: 1 } },
|
||
list: [{ $minKey: 1 }, { $maxKey: 1 }],
|
||
});
|
||
|
||
const generated = JSON.parse(parseMongoFindCommand("db.c.find({u: UUID()})")!.filter).u.$uuid;
|
||
assert.match(generated, /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
|
||
});
|
||
|
||
test("parseMongoFindCommand leaves MinKey/MaxKey alone outside value positions", () => {
|
||
const command = parseMongoFindCommand('db.c.find({MinKey: 1, MaxKey: 2, label: "MinKey", nested: {MaxKey: true}})');
|
||
assert.ok(command);
|
||
assert.deepEqual(JSON.parse(command.filter), { MinKey: 1, MaxKey: 2, label: "MinKey", nested: { MaxKey: true } });
|
||
});
|
||
|
||
test("parseMongoFindCommand rejects malformed UUID, BinData, Timestamp and MinKey/MaxKey", () => {
|
||
for (const source of [
|
||
'db.c.find({b: BinData(256, "x")})',
|
||
"db.c.find({b: BinData(0)})",
|
||
'db.c.find({b: BinData("00", "x")})',
|
||
"db.c.find({t: Timestamp(1)})",
|
||
"db.c.find({t: Timestamp(-1, 0)})",
|
||
"db.c.find({t: Timestamp(4294967296, 0)})",
|
||
"db.c.find({u: UUID(1)})",
|
||
"db.c.find({a: MinKey(1)})",
|
||
'db.c.find({a: MaxKey("x")})',
|
||
]) {
|
||
assert.equal(parseMongoFindCommand(source), null, source);
|
||
}
|
||
});
|
||
|
||
test("parseMongoFindCommand validates UUID strings at parse time", () => {
|
||
// mongosh requires the canonical 8-4-4-4-12 hex form: dashes at fixed
|
||
// positions, hex digits elsewhere.
|
||
for (const source of [
|
||
'db.c.find({u: UUID("3b241101e2bb42558caf4136c566a962")})',
|
||
'db.c.find({u: UUID("3b241101-e2bb-4255-8caf-4136c566a96")})',
|
||
'db.c.find({u: UUID("3b241101-e2bb-4255-8caf-4136c566a9620")})',
|
||
'db.c.find({u: UUID("zb241101-e2bb-4255-8caf-4136c566a962")})',
|
||
'db.c.find({u: UUID("3b241101_e2bb_4255_8caf_4136c566a962")})',
|
||
]) {
|
||
assert.equal(parseMongoFindCommand(source), null, source);
|
||
}
|
||
|
||
// Upper-case hex is accepted and preserved verbatim.
|
||
const command = parseMongoFindCommand('db.c.find({u: UUID("3B241101-E2BB-4255-8CAF-4136C566A962")})');
|
||
assert.ok(command);
|
||
assert.deepEqual(JSON.parse(command.filter), { u: { $uuid: "3B241101-E2BB-4255-8CAF-4136C566A962" } });
|
||
});
|
||
|
||
test("parseMongoFindExplainCommand reads a final explain() with the find it wraps", () => {
|
||
assert.deepEqual(parseMongoFindExplainCommand('db.c.find({a: 1}, {b: 1}).sort({b: -1}).skip(2).limit(5).explain("allPlansExecution")'), {
|
||
collection: "c",
|
||
filter: '{"a": 1}',
|
||
projection: '{"b": 1}',
|
||
skip: 2,
|
||
limit: 5,
|
||
sort: '{"b": -1}',
|
||
verbosity: "allPlansExecution",
|
||
});
|
||
assert.equal(parseMongoFindExplainCommand("db.c.find({a: 1}).explain()")?.verbosity, "queryPlanner");
|
||
assert.equal(parseMongoFindExplainCommand("db.c.find({a: 1})\n .sort({b: 1})\n .explain('executionStats');")?.verbosity, "executionStats");
|
||
assert.equal(parseMongoFindExplainCommand('db["x-y"].find({}).explain()')?.collection, "x-y");
|
||
assert.equal(parseMongoCommand("db.c.find({}).explain()")?.command.kind, "findExplain");
|
||
// A plain find is not an explain, and vice versa.
|
||
assert.equal(parseMongoFindExplainCommand("db.c.find({a: 1}).limit(1)"), null);
|
||
assert.equal(parseMongoFindCommand("db.c.find({a: 1}).explain()"), null);
|
||
});
|
||
|
||
test("parseMongoFindExplainCommand rejects a misplaced explain or an unknown verbosity", () => {
|
||
for (const [source, expected] of [
|
||
["db.c.find({}).explain('verbose')", /verbosity must be "queryPlanner", "executionStats" or "allPlansExecution", got 'verbose'/],
|
||
["db.c.find({}).explain(1)", /verbosity must be .*, got 1/],
|
||
["db.c.find({}).explain().limit(5)", /explain\(\) must be the final chain method/],
|
||
["db.c.find({}).explain", /Unexpected text after find\(\.\.\.\): "\.explain"/],
|
||
] as const) {
|
||
assert.equal(parseMongoCommand(source), null, source);
|
||
assert.match(describeMongoCommandParseFailure(source), expected, source);
|
||
}
|
||
});
|
||
|
||
test("parseMongoFindCommand rejects chained methods it would otherwise silently drop", () => {
|
||
// Each of these used to parse as a plain find and run a different query than written.
|
||
for (const [source, expected] of [
|
||
["db.c.find({}).hint({a: 1})", /find\(\)\.hint\(\) is not supported yet\. Supported after find\(\): sort, skip, limit, collation, count, explain, toArray, pretty\./],
|
||
["db.c.find({}).batchSize(10)", /batchSize\(\) is not supported yet/],
|
||
["db.c.find({}).sort({a: 1}).maxTimeMS(100)", /maxTimeMS\(\) is not supported yet/],
|
||
["db.c.find({}).itcount()", /itcount\(\) is not supported; use find\(\)\.count\(\) or countDocuments\(\)/],
|
||
["db.c.find({}).size()", /size\(\) is not supported; use find\(\)\.count\(\)/],
|
||
["db.c.find({}).forEach(d => print(d))", /forEach\(\) runs JavaScript, which the editor does not execute/],
|
||
["db.c.find({}).map(d => d.a)", /map\(\) runs JavaScript/],
|
||
["db.c.find({}).toArray(1)", /toArray\(\) takes no arguments/],
|
||
["db.c.find({}).count", /Unexpected text after find\(\.\.\.\): "\.count"/],
|
||
] as const) {
|
||
assert.equal(parseMongoFindCommand(source), null, source);
|
||
assert.equal(parseMongoCommand(source), null, source);
|
||
assert.match(describeMongoCommandParseFailure(source), expected, source);
|
||
}
|
||
|
||
// Non-cursor methods explain why nothing can follow them.
|
||
assert.match(describeMongoCommandParseFailure("db.c.findOne({}).limit(1)"), /findOne\(\) returns a result, not a cursor/);
|
||
assert.match(describeMongoCommandParseFailure("db.c.countDocuments({}).limit(1)"), /countDocuments\(\) returns a result, not a cursor/);
|
||
});
|
||
|
||
test("parseMongoFindCommand still accepts the chain methods it executes", () => {
|
||
assert.equal(parseMongoFindCommand("db.c.find({a: 1}).sort({b: -1}).skip(2).limit(5)")?.limit, 5);
|
||
assert.ok(parseMongoFindCommand("db.c.find({}).collation({locale: 'en'}).limit(1)"));
|
||
assert.ok(parseMongoFindCommand("db.c.find({a: 1}).sort({b: 1}).toArray()"));
|
||
assert.ok(parseMongoFindCommand("db.c.find({a: 1}).pretty()"));
|
||
assert.ok(parseMongoFindCommand("db.c.find({a: 1})\n .sort({b: 1})\n .limit(10)"));
|
||
assert.equal(parseMongoCommand("db.c.find({a: 1}).count()")?.command.kind, "countDocuments");
|
||
assert.deepEqual(
|
||
listChainedCalls(".sort({b: 1}) . limit( 10 ).toArray()")?.map((call) => [call.name, call.args.trim()]),
|
||
[
|
||
["sort", "{b: 1}"],
|
||
["limit", "10"],
|
||
["toArray", ""],
|
||
],
|
||
);
|
||
assert.equal(listChainedCalls(".count"), null);
|
||
});
|
||
|
||
test("parseMongoFindCommand accepts single-quoted string values and unquoted sort keys", () => {
|
||
const command = parseMongoFindCommand("db.products.find({category: 'Electronics'}).sort({price: -1}).limit(2)");
|
||
assert.ok(command);
|
||
assert.equal(command.collection, "products");
|
||
assert.equal(command.limit, 2);
|
||
assert.deepEqual(JSON.parse(command.filter), { category: "Electronics" });
|
||
assert.deepEqual(JSON.parse(command.sort || "{}"), { price: -1 });
|
||
});
|
||
|
||
test("parseMongoFindCommand parses projection arguments", () => {
|
||
const command = parseMongoFindCommand(`db.jobs.find({ status: "open" }, {
|
||
title: 1,
|
||
_id: 0
|
||
}).sort({ title: 1 })`);
|
||
assert.ok(command);
|
||
assert.equal(command.collection, "jobs");
|
||
assert.deepEqual(JSON.parse(command.filter), { status: "open" });
|
||
assert.deepEqual(JSON.parse(command.projection || "{}"), { title: 1, _id: 0 });
|
||
assert.deepEqual(JSON.parse(command.sort || "{}"), { title: 1 });
|
||
});
|
||
|
||
test("parseMongoFindCommand rejects unsupported mongo shell commands", () => {
|
||
assert.equal(parseMongoFindCommand("db.users.drop()"), null);
|
||
assert.equal(parseMongoFindCommand("db.users.find({}, {}, { hint: { name: 1 } })"), null);
|
||
});
|
||
|
||
test("parseMongoFindOneCommand parses a dedicated findOne command", () => {
|
||
assert.deepEqual(parseMongoFindOneCommand("db.users.findOne({email:'a@b.com'})"), {
|
||
collection: "users",
|
||
filter: '{"email":"a@b.com"}',
|
||
});
|
||
});
|
||
|
||
test("parseMongoFindOneCommand defaults an empty filter and parses projection", () => {
|
||
assert.deepEqual(parseMongoFindOneCommand("db.users.findOne()"), {
|
||
collection: "users",
|
||
filter: "{}",
|
||
});
|
||
const withProjection = parseMongoFindOneCommand('db.getCollection("audit.logs").findOne({ level: "warn" }, { message: 1, _id: 0 })');
|
||
assert.ok(withProjection);
|
||
assert.equal(withProjection.collection, "audit.logs");
|
||
assert.deepEqual(JSON.parse(withProjection.projection || "{}"), { message: 1, _id: 0 });
|
||
});
|
||
|
||
test("parseMongoFindOneCommand accepts documented projection and options arguments", () => {
|
||
const command = parseMongoFindOneCommand("db.users.findOne({ active: true }, { name: 1 }, { sort: { createdAt: -1 } })");
|
||
assert.ok(command);
|
||
assert.deepEqual(JSON.parse(command.filter), { active: true });
|
||
assert.deepEqual(JSON.parse(command.projection || "{}"), { name: 1 });
|
||
assert.deepEqual(JSON.parse(command.options || "{}"), { sort: { createdAt: -1 } });
|
||
});
|
||
|
||
test("parseMongoFindOneCommand rejects cursor chaining and does not collide with find", () => {
|
||
assert.equal(parseMongoFindOneCommand("db.users.findOne({}).limit(5)"), null);
|
||
assert.equal(parseMongoFindOneCommand("db.users.find({})"), null);
|
||
});
|
||
|
||
test("parseMongoCommand tags findOne with its own kind", () => {
|
||
const parsed = parseMongoCommand("db.users.findOne({active:true})");
|
||
assert.deepEqual(parsed?.command, {
|
||
kind: "findOne",
|
||
collection: "users",
|
||
filter: '{"active":true}',
|
||
});
|
||
});
|
||
|
||
test("parseMongoFindOneAndUpdateCommand parses filter, update and options", () => {
|
||
assert.deepEqual(parseMongoFindOneAndUpdateCommand("db.users.findOneAndUpdate({_id:1},{$set:{active:true}},{returnDocument:'after'})"), {
|
||
collection: "users",
|
||
filter: '{"_id":1}',
|
||
update: '{"$set":{"active":true}}',
|
||
options: '{"returnDocument":"after"}',
|
||
});
|
||
// requires both filter and update
|
||
assert.equal(parseMongoFindOneAndUpdateCommand("db.users.findOneAndUpdate({_id:1})"), null);
|
||
});
|
||
|
||
test("parseMongoFindOneAndReplaceCommand parses filter and replacement", () => {
|
||
assert.deepEqual(parseMongoFindOneAndReplaceCommand("db.users.findOneAndReplace({_id:1},{name:'a'})"), {
|
||
collection: "users",
|
||
filter: '{"_id":1}',
|
||
replacement: '{"name":"a"}',
|
||
});
|
||
});
|
||
|
||
test("parseMongoFindOneAndDeleteCommand parses filter and defaults empty", () => {
|
||
assert.deepEqual(parseMongoFindOneAndDeleteCommand("db.users.findOneAndDelete({_id:1})"), {
|
||
collection: "users",
|
||
filter: '{"_id":1}',
|
||
});
|
||
assert.deepEqual(parseMongoFindOneAndDeleteCommand("db.users.findOneAndDelete()"), {
|
||
collection: "users",
|
||
filter: "{}",
|
||
});
|
||
});
|
||
|
||
test("parseMongoCommand tags find-and-modify commands with their kind", () => {
|
||
assert.equal(parseMongoCommand("db.users.findOneAndUpdate({_id:1},{$set:{a:1}})")?.command.kind, "findOneAndUpdate");
|
||
assert.equal(parseMongoCommand("db.users.findOneAndReplace({_id:1},{a:1})")?.command.kind, "findOneAndReplace");
|
||
assert.equal(parseMongoCommand("db.users.findOneAndDelete({_id:1})")?.command.kind, "findOneAndDelete");
|
||
// findOne must not be swallowed by the find-and-modify parsers
|
||
assert.equal(parseMongoCommand("db.users.findOne({_id:1})")?.command.kind, "findOne");
|
||
});
|
||
|
||
test("evaluateMongoWriteSafety blocks empty-filter find-and-modify unless dangerous", () => {
|
||
const command = parseMongoCommand("db.users.findOneAndDelete({})")?.command as MongoWriteCommand;
|
||
assert.ok(command);
|
||
assert.equal(evaluateMongoWriteSafety(command, { allowWrites: true, allowDangerous: false }).allowed, false);
|
||
assert.equal(evaluateMongoWriteSafety(command, { allowWrites: true, allowDangerous: true }).allowed, true);
|
||
});
|
||
|
||
test("evaluateMongoWriteSafety fails closed for opaque or effectively unbounded filters", () => {
|
||
for (const source of [
|
||
'db.users.deleteMany({"$nor":[{"$expr":false}]})',
|
||
'db.users.deleteMany({"$or":[{"id":{"$exists":true}},{"id":{"$exists":false}}]})',
|
||
'db.users.deleteMany({"$or":[{"id":{"$exists":true}},{"id":{"$not":{"$exists":true}}}]})',
|
||
'db.users.deleteMany({"$or":[{"id":{"$eq":1}},{"id":{"$ne":1}}]})',
|
||
'db.users.deleteMany({"$or":[{"$and":[{"id":{"$eq":1}}]},{"id":{"$ne":1}}]})',
|
||
'db.users.deleteMany({"$or":[{"$and":[{"id":{"$eq":1}},{}]},{"id":{"$ne":1}}]})',
|
||
'db.users.deleteMany({"$or":[{"$and":[{"id":{"$eq":1}},{"x":{"$exists":true}}]},{"id":{"$ne":1}},{"x":{"$exists":false}}]})',
|
||
'db.users.deleteMany({"$or":[{"id":1},{"id":{"$ne":1}}]})',
|
||
'db.users.deleteMany({"$or":[{"id":{"$gt":1}},{"id":{"$lte":1}}]})',
|
||
'db.users.deleteMany({"$or":[{"id":{"$gte":1}},{"id":{"$lt":1}}]})',
|
||
'db.users.deleteMany({"$or":[{"id":{"$in":[1,2]}},{"id":{"$nin":[2,1]}}]})',
|
||
'db.users.deleteMany({"_id":{"$exists":true}})',
|
||
'db.users.deleteMany({"id":{"$nin":[]}})',
|
||
'db.users.deleteMany({"id":{"$elemMatch":{"value":1}}})',
|
||
'db.users.deleteMany({"_id":{"$oid":"not-an-object-id"}})',
|
||
'db.users.deleteMany({"sequence":{"$numberLong":"9223372036854775808"}})',
|
||
'db.users.deleteMany({"created_at":{"$date":"2026-02-30T00:00:00Z"}})',
|
||
'db.users.deleteMany({"name":{"$regex":".*"}})',
|
||
'db.users.deleteMany({"$or":[{"_id":{"$oid":"507f1f77bcf86cd799439011"}},{"_id":{"$ne":{"$oid":"507f1f77bcf86cd799439011"}}}]})',
|
||
'db.users.updateMany({"$where":"true"},{"$set":{"active":false}})',
|
||
'db.users.deleteMany({"$or":[]})',
|
||
'db.users.deleteMany({"$opaque":[{"id":1}]})',
|
||
]) {
|
||
const command = parseMongoWriteCommand(source);
|
||
assert.ok(command, source);
|
||
assert.equal(evaluateMongoWriteSafety(command, { allowWrites: true, allowDangerous: false }).allowed, false, source);
|
||
assert.equal(evaluateMongoWriteSafety(command, { allowWrites: true, allowDangerous: true }).allowed, true, source);
|
||
}
|
||
|
||
for (const source of [
|
||
'db.users.deleteMany({"tenant_id":1})',
|
||
'db.users.updateMany({"created_at":{"$gte":"2026-01-01"}},{"$set":{"active":false}})',
|
||
'db.users.deleteMany({"$or":[{"tenant_id":1},{"tenant_id":2}]})',
|
||
'db.users.deleteMany({"id":{"$ne":1}})',
|
||
'db.users.deleteMany({"id":{"$in":[1,2]}})',
|
||
'db.users.deleteMany({"id":{"$exists":true}})',
|
||
'db.users.updateOne({_id:ObjectId("507f1f77bcf86cd799439011")},{"$set":{"active":true}})',
|
||
'db.users.deleteMany({"sequence":NumberLong("9223372036854775807")})',
|
||
'db.users.deleteMany({"created_at":ISODate("2026-01-01T00:00:00.000Z")})',
|
||
'db.users.deleteMany({"tenant_id":1,"id":{"$nin":[]}})',
|
||
]) {
|
||
const command = parseMongoWriteCommand(source);
|
||
assert.ok(command, source);
|
||
assert.equal(evaluateMongoWriteSafety(command, { allowWrites: true, allowDangerous: false }).allowed, true, source);
|
||
}
|
||
});
|
||
|
||
test("parseMongoVersionCommand parses db.version", () => {
|
||
assert.deepEqual(parseMongoVersionCommand("db.version();"), { kind: "version" });
|
||
assert.equal(parseMongoVersionCommand("db.jobs.version()"), null);
|
||
});
|
||
|
||
test("parseMongoCommand normalizes outer comments around a command", () => {
|
||
const parsed = parseMongoCommand(`
|
||
// current database
|
||
use accounting;
|
||
// keep working here
|
||
`);
|
||
assert.deepEqual(parsed, {
|
||
text: "use accounting;",
|
||
command: {
|
||
kind: "use",
|
||
database: "accounting",
|
||
},
|
||
});
|
||
});
|
||
|
||
test("parseMongoCommand strips SQL-style -- comments the editor inserts", () => {
|
||
// The editor runs Mongo through its SQL language mode, whose line comment is `--`.
|
||
assert.equal(parseMongoCommand("-- current database\ndb.users.find({})")?.command.kind, "find");
|
||
assert.equal(parseMongoCommand("db.users.find({}) -- run this one")?.command.kind, "find");
|
||
assert.equal(parseMongoCommand("/* header */ db.users.find({}) -- trailer")?.command.kind, "find");
|
||
// Native // comments still work alongside --.
|
||
assert.equal(parseMongoCommand("// keep\ndb.users.find({})")?.command.kind, "find");
|
||
});
|
||
|
||
test("parseMongoCommand keeps comment markers that live inside string values", () => {
|
||
// A `--` or `//` inside a string must not be trimmed as a trailing comment.
|
||
const dash = parseMongoFindCommand('db.users.find({ note: "a--b" })');
|
||
assert.ok(dash);
|
||
assert.deepEqual(JSON.parse(dash.filter), { note: "a--b" });
|
||
const slash = parseMongoFindCommand('db.users.find({ url: "http://x" })');
|
||
assert.ok(slash);
|
||
assert.deepEqual(JSON.parse(slash.filter), { url: "http://x" });
|
||
});
|
||
|
||
test("splitMongoCommands splits statements separated by -- comment lines", () => {
|
||
const commands = splitMongoCommands(`
|
||
-- seed two rows
|
||
db.users.insertOne({ name: "A" });
|
||
db.users.insertOne({ name: "B" }) -- second
|
||
`);
|
||
assert.deepEqual(
|
||
commands.map(({ command }) => command.kind),
|
||
["insert", "insert"],
|
||
);
|
||
});
|
||
|
||
test("parseMongoWriteCommand accepts unquoted insert and update commands", () => {
|
||
assert.deepEqual(parseMongoWriteCommand("db.products.insertOne({name: 'demo', price: 1})"), {
|
||
kind: "insert",
|
||
collection: "products",
|
||
docsJson: '{"name": "demo", "price": 1}',
|
||
});
|
||
assert.deepEqual(parseMongoWriteCommand("db.products.updateOne({_id: ObjectId('507f1f77bcf86cd799439011')}, {$set: {stock: 3}})"), {
|
||
kind: "update",
|
||
collection: "products",
|
||
filter: '{"_id": {"$oid":"507f1f77bcf86cd799439011"}}',
|
||
update: '{"$set": {"stock": 3}}',
|
||
many: false,
|
||
});
|
||
});
|
||
|
||
test("parseMongoWriteCommand reads renameCollection", () => {
|
||
assert.deepEqual(parseMongoWriteCommand('db.orders.renameCollection("orders_2024");'), { kind: "renameCollection", collection: "orders", newName: "orders_2024" });
|
||
assert.equal(parseMongoWriteCommand("db[\"a-b\"].renameCollection('c')")?.kind, "renameCollection");
|
||
assert.equal(parseMongoWriteCommand('db.getCollection("a").renameCollection("b")')?.kind, "renameCollection");
|
||
|
||
for (const [source, expected] of [
|
||
["db.orders.renameCollection()", /expects the new collection name/],
|
||
["db.orders.renameCollection(1)", /expects the new collection name/],
|
||
['db.orders.renameCollection("")', /requires a valid collection name/],
|
||
['db.orders.renameCollection("a$b")', /requires a valid collection name/],
|
||
['db.orders.renameCollection("orders")', /must differ from the current name/],
|
||
['db.orders.renameCollection("x", true)', /dropTarget is not supported; drop the target collection first/],
|
||
['db.orders.renameCollection("x").y()', /Unexpected text after renameCollection/],
|
||
] as const) {
|
||
assert.equal(parseMongoCommand(source), null, source);
|
||
assert.match(describeMongoCommandParseFailure(source), expected, source);
|
||
}
|
||
});
|
||
|
||
test("parseMongoWriteCommand reads bulkWrite with every operation kind", () => {
|
||
const command = parseMongoWriteCommand(`db.orders.bulkWrite([
|
||
{ insertOne: { document: { sku: "A1", stock: 1 } } },
|
||
{ updateOne: { filter: { sku: "A1" }, update: { $inc: { stock: 1 } }, upsert: true } },
|
||
{ updateMany: { filter: { archived: true }, update: [{ $set: { stock: 0 } }] } },
|
||
{ replaceOne: { filter: { sku: "B2" }, replacement: { sku: "B2", stock: 9 } } },
|
||
{ deleteOne: { filter: { sku: "C3" } } },
|
||
{ deleteMany: { filter: { stock: { $lt: 0 } } } }
|
||
], { ordered: false })`);
|
||
assert.ok(command && command.kind === "bulkWrite");
|
||
assert.equal(command.collection, "orders");
|
||
assert.equal(command.options, '{ "ordered": false }');
|
||
assert.deepEqual(
|
||
(JSON.parse(command.operations) as Array<Record<string, unknown>>).map((entry) => Object.keys(entry)[0]),
|
||
["insertOne", "updateOne", "updateMany", "replaceOne", "deleteOne", "deleteMany"],
|
||
);
|
||
assert.equal(parseMongoWriteCommand('db["my-coll"].bulkWrite([{ deleteOne: { filter: { a: 1 } } }]);')?.kind, "bulkWrite");
|
||
});
|
||
|
||
test("bulkWrite operations are validated and the problem is named", () => {
|
||
for (const [source, expected] of [
|
||
["db.orders.bulkWrite()", /array of operations and optional options/],
|
||
["db.orders.bulkWrite({})", /requires an array of operations/],
|
||
["db.orders.bulkWrite([])", /at least one operation/],
|
||
["db.orders.bulkWrite([1])", /operation 1 must be a document/],
|
||
["db.orders.bulkWrite([{ insertOne: {}, deleteOne: {} }])", /exactly one operation key/],
|
||
["db.orders.bulkWrite([{ upsertOne: { document: {} } }])", /unsupported operation upsertOne/],
|
||
["db.orders.bulkWrite([{ insertOne: { doc: {} } }])", /unsupported field doc/],
|
||
["db.orders.bulkWrite([{ insertOne: {} }])", /requires a document document/],
|
||
["db.orders.bulkWrite([{ updateOne: { filter: {}, update: { a: 1 } } }])", /must use operators such as \$set/],
|
||
["db.orders.bulkWrite([{ updateOne: { filter: {}, update: { $set: { a: 1 } }, upsert: 1 } }])", /upsert must be a boolean/],
|
||
["db.orders.bulkWrite([{ updateOne: { filter: {}, update: { $set: { a: 1 } }, collation: {} } }])", /unsupported field collation/],
|
||
["db.orders.bulkWrite([{ replaceOne: { filter: {}, replacement: { $set: { a: 1 } } } }])", /must not contain update operators such as \$set/],
|
||
["db.orders.bulkWrite([{ deleteOne: { filter: [] } }])", /field filter must be a document/],
|
||
["db.orders.bulkWrite([{ deleteOne: { filter: {} } }], { ordered: 1 })", /ordered option must be a boolean/],
|
||
["db.orders.bulkWrite([{ deleteOne: { filter: {} } }], { writeConcern: {} })", /Unsupported bulkWrite\(\) option: writeConcern/],
|
||
] as const) {
|
||
assert.equal(parseMongoWriteCommand(source), null, source);
|
||
assert.match(describeMongoCommandParseFailure(source), expected, source);
|
||
}
|
||
});
|
||
|
||
test("evaluateMongoWriteSafety treats a bulkWrite as risky as its least-bounded operation", () => {
|
||
const policy = { allowWrites: true, allowDangerous: false } as Parameters<typeof evaluateMongoWriteSafety>[1];
|
||
const bounded = parseMongoWriteCommand("db.orders.bulkWrite([{ insertOne: { document: { a: 1 } } }, { deleteOne: { filter: { sku: 'X' } } }])")!;
|
||
const unbounded = parseMongoWriteCommand("db.orders.bulkWrite([{ updateOne: { filter: { sku: 'X' }, update: { $set: { a: 1 } } } }, { deleteMany: { filter: {} } }])")!;
|
||
assert.equal(evaluateMongoWriteSafety(bounded, policy).allowed, true);
|
||
assert.equal(evaluateMongoWriteSafety(unbounded, policy).allowed, false);
|
||
assert.equal(evaluateMongoWriteSafety(unbounded, { ...policy, allowDangerous: true }).allowed, true);
|
||
});
|
||
|
||
test("mongoBulkWriteToQueryResult renders one row of counts", () => {
|
||
const result = mongoBulkWriteToQueryResult({ inserted_count: 2, matched_count: 3, modified_count: 1, deleted_count: 4, upserted_count: 1 }, 12.6);
|
||
assert.deepEqual(result.columns, ["insertedCount", "matchedCount", "modifiedCount", "deletedCount", "upsertedCount"]);
|
||
assert.deepEqual(result.rows, [[2, 3, 1, 4, 1]]);
|
||
assert.equal(result.affected_rows, 8);
|
||
assert.equal(result.execution_time_ms, 13);
|
||
});
|
||
|
||
test("parseMongoWriteCommand reads replaceOne as a filtered replace", () => {
|
||
assert.deepEqual(parseMongoWriteCommand('db.orders.replaceOne({_id: ObjectId("507f1f77bcf86cd799439011")}, {name: "new", tags: []}, {upsert: true})'), {
|
||
kind: "replace",
|
||
collection: "orders",
|
||
filter: '{"_id": {"$oid":"507f1f77bcf86cd799439011"}}',
|
||
replacement: '{"name": "new", "tags": []}',
|
||
options: '{"upsert": true}',
|
||
});
|
||
assert.deepEqual(parseMongoWriteCommand('db["my-coll"].replaceOne({a: 1}, {b: 2});'), {
|
||
kind: "replace",
|
||
collection: "my-coll",
|
||
filter: '{"a": 1}',
|
||
replacement: '{"b": 2}',
|
||
});
|
||
|
||
for (const source of ["db.orders.replaceOne({a: 1})", "db.orders.replaceOne({a: 1}, {b: 2}, {upsert: true}, 4)", "db.orders.replaceOne({a: 1}, [{b: 2}])", "db.orders.replaceOne({a: 1}, {$set: {b: 2}})"]) {
|
||
assert.equal(parseMongoWriteCommand(source), null, source);
|
||
}
|
||
assert.match(describeMongoCommandParseFailure("db.orders.replaceOne({a: 1}, {$set: {b: 2}})"), /must not contain update operators such as \$set; use updateOne\(\)/);
|
||
});
|
||
|
||
test("evaluateMongoWriteSafety guards an unbounded replaceOne like an update", () => {
|
||
const bounded = parseMongoWriteCommand("db.orders.replaceOne({a: 1}, {b: 2})")!;
|
||
const unbounded = parseMongoWriteCommand("db.orders.replaceOne({}, {b: 2})")!;
|
||
const policy = { allowWrites: true, allowDangerous: false } as Parameters<typeof evaluateMongoWriteSafety>[1];
|
||
assert.equal(evaluateMongoWriteSafety(bounded, policy).allowed, true);
|
||
assert.equal(evaluateMongoWriteSafety(unbounded, policy).allowed, false);
|
||
assert.equal(evaluateMongoWriteSafety(bounded, { ...policy, allowWrites: false }).allowed, false);
|
||
});
|
||
|
||
test("normalizeRustMongoCommand passes a replace command through unchanged", () => {
|
||
assert.deepEqual(normalizeRustMongoCommand({ kind: "replace", collection: "orders", filter: '{"a":1}', replacement: '{"b":2}', options: null }), {
|
||
kind: "replace",
|
||
collection: "orders",
|
||
filter: '{"a":1}',
|
||
replacement: '{"b":2}',
|
||
});
|
||
});
|
||
|
||
test("parseMongoWriteCommand unwraps EJSON.deserialize values", () => {
|
||
assert.deepEqual(parseMongoWriteCommand('db.products.updateOne({_id: ObjectId("507f1f77bcf86cd799439011")}, {$set: {price: EJSON.deserialize({"$numberDecimal":"12.34"}), payload: EJSON.deserialize({"$binary":{"base64":"AQI=","subType":"00"}})}})'), {
|
||
kind: "update",
|
||
collection: "products",
|
||
filter: '{"_id": {"$oid":"507f1f77bcf86cd799439011"}}',
|
||
update: '{"$set": {"price": {"$numberDecimal":"12.34"}, "payload": {"$binary":{"base64":"AQI=","subType":"00"}}}}',
|
||
many: false,
|
||
});
|
||
});
|
||
|
||
test("parseMongoWriteCommand accepts the copied insert form that wraps BSON values in EJSON.deserialize", () => {
|
||
// "Copy -> SQL Inserts" formats values extended JSON cannot spell directly
|
||
// through `EJSON.deserialize({...})`. The statement is re-parsed by the Rust
|
||
// parser before execution, so both parsers must accept this exact shape
|
||
// (#11666).
|
||
assert.deepEqual(
|
||
parseMongoWriteCommand(`db.getCollection("test").insert({
|
||
"type": EJSON.deserialize({
|
||
"$numberInt": "999999"
|
||
}),
|
||
"refid": "27507"
|
||
})`),
|
||
{
|
||
kind: "insert",
|
||
collection: "test",
|
||
docsJson: `{
|
||
"type": {
|
||
"$numberInt": "999999"
|
||
},
|
||
"refid": "27507"
|
||
}`,
|
||
},
|
||
);
|
||
});
|
||
|
||
test("parseMongoWriteCommand accepts legacy insert commands", () => {
|
||
assert.deepEqual(
|
||
parseMongoWriteCommand(`db.getCollection("accounting_reconciliations").insert({
|
||
"accountId": 999,
|
||
"status": "done"
|
||
});`),
|
||
{
|
||
kind: "insert",
|
||
collection: "accounting_reconciliations",
|
||
docsJson: '{\n "accountId": 999,\n "status": "done"\n }',
|
||
},
|
||
);
|
||
assert.deepEqual(parseMongoWriteCommand("db.products.insert([{name: 'first'}, {name: 'second'}])"), {
|
||
kind: "insert",
|
||
collection: "products",
|
||
docsJson: '[{"name": "first"}, {"name": "second"}]',
|
||
});
|
||
assert.deepEqual(parseMongoWriteCommand("db.products.insertMany([{name: 'first'}, {name: 'second'}])"), {
|
||
kind: "insert",
|
||
collection: "products",
|
||
docsJson: '[{"name": "first"}, {"name": "second"}]',
|
||
});
|
||
assert.equal(parseMongoWriteCommand("db.products.insert({name: 'demo'}, {writeConcern: {w: 1}})"), null);
|
||
assert.equal(parseMongoWriteCommand("db.products.insert()"), null);
|
||
assert.equal(parseMongoWriteCommand("db.products.insert('demo')"), null);
|
||
});
|
||
|
||
test("parseMongoWriteCommand accepts updateMany arrayFilters options", () => {
|
||
assert.deepEqual(
|
||
parseMongoWriteCommand(`db.issue_3231.updateMany(
|
||
{ msgType: 3, "order.orderId": { $in: [12345] } },
|
||
{ $set: { "order.$[orderElem].bcorderproducts.$[prodElem].pankouType": "双双2" } },
|
||
{ arrayFilters: [
|
||
{ "orderElem.orderId": { $in: [12345] } },
|
||
{ "prodElem.id": 322678 }
|
||
] }
|
||
)`),
|
||
{
|
||
kind: "update",
|
||
collection: "issue_3231",
|
||
filter: '{ "msgType": 3, "order.orderId": { "$in": [12345] } }',
|
||
update: '{ "$set": { "order.$[orderElem].bcorderproducts.$[prodElem].pankouType": "双双2" } }',
|
||
options: '{ "arrayFilters": [\n { "orderElem.orderId": { "$in": [12345] } },\n { "prodElem.id": 322678 }\n ] }',
|
||
many: true,
|
||
},
|
||
);
|
||
});
|
||
|
||
test("parseMongoWriteCommand accepts Mongo shell regex literals", () => {
|
||
const command = parseMongoWriteCommand(`db.code_info.updateMany(
|
||
{ level: "SECOND_LAYER", position: "B", packagingRatio: /^[^::]*盒/, type: { $ne: "PACK_IN" } },
|
||
{ $set: { type: "PACK_IN" }, $currentDate: { lastUpdateTime: true } }
|
||
)`);
|
||
|
||
assert.equal(command?.kind, "update");
|
||
if (command?.kind !== "update") return;
|
||
assert.equal(command.collection, "code_info");
|
||
assert.equal(command.many, true);
|
||
assert.deepEqual(JSON.parse(command.filter), {
|
||
level: "SECOND_LAYER",
|
||
position: "B",
|
||
packagingRatio: { $regularExpression: { pattern: "^[^::]*盒", options: "" } },
|
||
type: { $ne: "PACK_IN" },
|
||
});
|
||
});
|
||
|
||
test("parseMongoWriteCommand ignores delimiters inside Mongo shell regex literals", () => {
|
||
const command = parseMongoWriteCommand(String.raw`db.items.updateMany({ value: /a\),b/ }, { $set: { matched: true } })`);
|
||
|
||
assert.equal(command?.kind, "update");
|
||
if (command?.kind !== "update") return;
|
||
assert.deepEqual(JSON.parse(command.filter), {
|
||
value: { $regularExpression: { pattern: "a\\),b", options: "" } },
|
||
});
|
||
});
|
||
|
||
test("normalizeJsonArgument handles regex escapes, character classes, flags, strings, and comments", () => {
|
||
const normalized = normalizeJsonArgument(`{
|
||
pattern: /a\\/b[/::]/mi,
|
||
url: "https://example.com/a/b",
|
||
note: "// remains text",
|
||
// slash comments remain comments
|
||
active: true
|
||
}`);
|
||
|
||
assert.ok(normalized);
|
||
assert.deepEqual(JSON.parse(normalized), {
|
||
pattern: { $regularExpression: { pattern: "a\\/b[/::]", options: "im" } },
|
||
url: "https://example.com/a/b",
|
||
note: "// remains text",
|
||
active: true,
|
||
});
|
||
});
|
||
|
||
test("normalizeJsonArgument accepts a regex literal after comments", () => {
|
||
const normalized = normalizeJsonArgument(`{
|
||
block: /* explain the pattern */ /block/i,
|
||
line: // explain the next pattern
|
||
/line/m
|
||
}`);
|
||
|
||
assert.ok(normalized);
|
||
assert.deepEqual(JSON.parse(normalized), {
|
||
block: { $regularExpression: { pattern: "block", options: "i" } },
|
||
line: { $regularExpression: { pattern: "line", options: "m" } },
|
||
});
|
||
});
|
||
|
||
test("normalizeJsonArgument drops JS-only regex literal flags", () => {
|
||
const normalized = normalizeJsonArgument(`{
|
||
global: /value/g,
|
||
sticky: /value/yd,
|
||
verbose: /value/givs
|
||
}`);
|
||
|
||
assert.ok(normalized);
|
||
assert.deepEqual(JSON.parse(normalized), {
|
||
global: { $regularExpression: { pattern: "value", options: "" } },
|
||
sticky: { $regularExpression: { pattern: "value", options: "" } },
|
||
verbose: { $regularExpression: { pattern: "value", options: "is" } },
|
||
});
|
||
});
|
||
|
||
test("normalizeJsonArgument rejects malformed Mongo shell regex literals", () => {
|
||
for (const source of ["{pattern: /unterminated}", "{pattern: /value/ii}", "{pattern: /value/q}"]) {
|
||
assert.equal(normalizeJsonArgument(source), null, source);
|
||
}
|
||
});
|
||
|
||
test("parseMongoWriteCommand parses createIndex with optional options", () => {
|
||
assert.deepEqual(parseMongoWriteCommand("db.users.createIndex({email: 1}, {unique: true, name: 'users_email_unique'})"), {
|
||
kind: "createIndex",
|
||
collection: "users",
|
||
keys: '{"email": 1}',
|
||
options: '{"unique": true, "name": "users_email_unique"}',
|
||
});
|
||
});
|
||
|
||
test("parseMongoWriteCommand parses dropIndex and dropIndexes variants", () => {
|
||
assert.deepEqual(parseMongoWriteCommand('db.users.dropIndex("users_email_unique")'), {
|
||
kind: "dropIndex",
|
||
collection: "users",
|
||
index: '"users_email_unique"',
|
||
});
|
||
assert.deepEqual(parseMongoWriteCommand("db.users.dropIndex({email: 1})"), {
|
||
kind: "dropIndex",
|
||
collection: "users",
|
||
index: '{"email": 1}',
|
||
});
|
||
assert.deepEqual(parseMongoWriteCommand("db.users.dropIndexes()"), {
|
||
kind: "dropIndexes",
|
||
collection: "users",
|
||
});
|
||
assert.deepEqual(parseMongoWriteCommand("db.users.dropIndexes({email: 1})"), {
|
||
kind: "dropIndexes",
|
||
collection: "users",
|
||
indexes: '{"email": 1}',
|
||
});
|
||
assert.deepEqual(parseMongoWriteCommand('db.users.dropIndexes("*")'), {
|
||
kind: "dropIndexes",
|
||
collection: "users",
|
||
indexes: '"*"',
|
||
});
|
||
assert.deepEqual(parseMongoWriteCommand('db.users.dropIndexes(["a_1", "b_1"])'), {
|
||
kind: "dropIndexes",
|
||
collection: "users",
|
||
indexes: '["a_1", "b_1"]',
|
||
});
|
||
});
|
||
|
||
test("parseMongoWriteCommand parses collection drop commands", () => {
|
||
assert.deepEqual(parseMongoWriteCommand("db.users.drop()"), {
|
||
kind: "dropCollection",
|
||
collection: "users",
|
||
});
|
||
assert.deepEqual(parseMongoWriteCommand('db.getCollection("audit.logs").drop();'), {
|
||
kind: "dropCollection",
|
||
collection: "audit.logs",
|
||
});
|
||
assert.deepEqual(parseMongoCommand("db.users.drop()")?.command, {
|
||
kind: "dropCollection",
|
||
collection: "users",
|
||
});
|
||
});
|
||
|
||
test("parseMongoWriteCommand rejects collection drop arguments", () => {
|
||
assert.equal(parseMongoWriteCommand("db.users.drop({ writeConcern: 1 })"), null);
|
||
});
|
||
|
||
test("parseMongoWriteCommand rejects invalid dropIndex/dropIndexes variants", () => {
|
||
assert.equal(parseMongoWriteCommand("db.users.dropIndex()"), null);
|
||
assert.equal(parseMongoWriteCommand('db.users.dropIndex("*")'), null);
|
||
assert.equal(parseMongoWriteCommand('db.users.dropIndex(["a_1"])'), null);
|
||
assert.equal(parseMongoWriteCommand('db.users.dropIndexes([{"a":1}])'), null);
|
||
});
|
||
|
||
test("evaluateMongoWriteSafety blocks collection drop unless dangerous writes are enabled", () => {
|
||
const dropCollection = parseMongoWriteCommand("db.users.drop()");
|
||
assert.ok(dropCollection);
|
||
assert.match(evaluateMongoWriteSafety(dropCollection, { allowWrites: true }).reason || "", /high-risk operations.*DBX MCP settings/i);
|
||
assert.equal(evaluateMongoWriteSafety(dropCollection, { allowWrites: true, allowDangerous: true }).allowed, true);
|
||
});
|
||
|
||
test("evaluateMongoWriteSafety requires high-risk permission for schema changes", () => {
|
||
const dropAll = parseMongoWriteCommand("db.users.dropIndexes()");
|
||
assert.ok(dropAll);
|
||
assert.match(evaluateMongoWriteSafety(dropAll, { allowWrites: true }).reason || "", /high-risk operations.*DBX MCP settings/i);
|
||
|
||
const dropOne = parseMongoWriteCommand('db.users.dropIndexes("users_email_unique")');
|
||
assert.ok(dropOne);
|
||
assert.equal(evaluateMongoWriteSafety(dropOne, { allowWrites: true }).allowed, false);
|
||
assert.equal(evaluateMongoWriteSafety(dropOne, { allowWrites: true, allowDangerous: true }).allowed, true);
|
||
});
|
||
|
||
test("parseMongoCountDocumentsCommand parses db collection countDocuments", () => {
|
||
assert.deepEqual(parseMongoCountDocumentsCommand("db.products.countDocuments({})"), {
|
||
collection: "products",
|
||
filter: "{}",
|
||
mode: "accurate",
|
||
});
|
||
});
|
||
|
||
test("parseMongoCountDocumentsCommand parses legacy count helpers", () => {
|
||
assert.deepEqual(parseMongoCountDocumentsCommand("db.products.count({ active: true })"), {
|
||
collection: "products",
|
||
filter: '{ "active": true }',
|
||
mode: "legacy",
|
||
});
|
||
assert.deepEqual(parseMongoCountDocumentsCommand('db.getCollection("audit.logs").count()'), {
|
||
collection: "audit.logs",
|
||
filter: "{}",
|
||
mode: "legacy",
|
||
});
|
||
assert.deepEqual(parseMongoCountDocumentsCommand("db.products.find({ active: true }).count()"), {
|
||
collection: "products",
|
||
filter: '{ "active": true }',
|
||
mode: "legacy",
|
||
});
|
||
assert.equal(parseMongoFindCommand("db.products.find({ active: true }).count()"), null);
|
||
assert.deepEqual(parseMongoCommand("db.products.find({ active: true }).count()")?.command, {
|
||
kind: "countDocuments",
|
||
collection: "products",
|
||
filter: '{ "active": true }',
|
||
mode: "legacy",
|
||
});
|
||
});
|
||
|
||
test("parseMongoAggregateCommand parses db collection aggregate", () => {
|
||
assert.deepEqual(parseMongoAggregateCommand('db.products.aggregate([{"$match":{"active":true}},{"$count":"total"}])'), {
|
||
collection: "products",
|
||
pipeline: '[{"$match":{"active":true}},{"$count":"total"}]',
|
||
});
|
||
});
|
||
|
||
test("parseMongoAggregateCommand drops the no-op cursor methods mongosh and Compass append", () => {
|
||
const expected = { collection: "orders", pipeline: '[{"$match": {"a": 1}}]' };
|
||
for (const source of [
|
||
"db.orders.aggregate([{$match: {a: 1}}]).toArray()",
|
||
"db.orders.aggregate([{$match: {a: 1}}]).pretty()",
|
||
"db.orders.aggregate([{$match: {a: 1}}]).toArray().pretty()",
|
||
"db.orders.aggregate([{$match: {a: 1}}])\n .toArray()",
|
||
"db.orders.aggregate([{$match: {a: 1}}]).toArray();",
|
||
]) {
|
||
assert.deepEqual(parseMongoAggregateCommand(source), expected, source);
|
||
}
|
||
assert.deepEqual(parseMongoAggregateCommand("db.orders.aggregate([], {allowDiskUse: true}).toArray()"), {
|
||
collection: "orders",
|
||
pipeline: "[]",
|
||
options: '{"allowDiskUse": true}',
|
||
});
|
||
});
|
||
|
||
test("parseMongoAggregateCommand still rejects cursor methods that would change the result", () => {
|
||
for (const source of ["db.orders.aggregate([]).limit(5)", "db.orders.aggregate([]).sort({a: 1})", "db.orders.aggregate([]).toArray().limit(5)", "db.orders.aggregate([]).toArray(1)"]) {
|
||
assert.equal(parseMongoAggregateCommand(source), null, source);
|
||
assert.match(describeMongoCommandParseFailure(source), /Chaining .* is not supported/, source);
|
||
}
|
||
});
|
||
|
||
test("parseMongoAggregateCommand accepts an empty pipeline", () => {
|
||
assert.deepEqual(parseMongoAggregateCommand("db.products.aggregate([])"), {
|
||
collection: "products",
|
||
pipeline: "[]",
|
||
});
|
||
});
|
||
|
||
test("parseMongoAggregateCommand accepts Mongo Shell trailing commas", () => {
|
||
const command = parseMongoAggregateCommand(`db.AdressInfo.aggregate([
|
||
{
|
||
$match: {
|
||
IsDelete: 0,
|
||
DataSource: { $ne: 'XC' },
|
||
TypeName: 1,
|
||
},
|
||
},
|
||
{
|
||
$project: {
|
||
MainId: '$_id',
|
||
labels: ['primary', 'backup',],
|
||
},
|
||
},
|
||
{ $out: 'IBMBiititle' },
|
||
], {
|
||
allowDiskUse: true,
|
||
})`);
|
||
|
||
assert.ok(command);
|
||
assert.deepEqual(JSON.parse(command.pipeline), [
|
||
{
|
||
$match: {
|
||
IsDelete: 0,
|
||
DataSource: { $ne: "XC" },
|
||
TypeName: 1,
|
||
},
|
||
},
|
||
{
|
||
$project: {
|
||
MainId: "$_id",
|
||
labels: ["primary", "backup"],
|
||
},
|
||
},
|
||
{ $out: "IBMBiititle" },
|
||
]);
|
||
assert.deepEqual(JSON.parse(command.options ?? "null"), { allowDiskUse: true });
|
||
});
|
||
|
||
test("parseMongoAggregateCommand preserves trailing-comma text inside strings", () => {
|
||
const command = parseMongoAggregateCommand(`db.logs.aggregate([
|
||
{
|
||
$project: {
|
||
objectText: "literal,}",
|
||
arrayText: "literal,]",
|
||
escapedQuote: "literal\\",]",
|
||
},
|
||
},
|
||
])`);
|
||
|
||
assert.ok(command);
|
||
assert.deepEqual(JSON.parse(command.pipeline), [
|
||
{
|
||
$project: {
|
||
objectText: "literal,}",
|
||
arrayText: "literal,]",
|
||
escapedQuote: 'literal",]',
|
||
},
|
||
},
|
||
]);
|
||
});
|
||
|
||
test("parseMongoAggregateCommand ignores comments inside aggregate pipelines", () => {
|
||
const command = parseMongoAggregateCommand(`
|
||
db.cash.aggregate([
|
||
{
|
||
$match: {
|
||
portfolio_id: "b6f6ec62-8571-11f1-bbfb-000c29caf77f",
|
||
date: { $gte: "20260604" }
|
||
}
|
||
},
|
||
{ $unwind: "$cashs" },
|
||
{
|
||
$project: {
|
||
_id: 0,
|
||
date: 1,
|
||
trans_currency_cd: "$cashs.trans_currency_cd",
|
||
cash_local: { $multiply: ["$cashs.cash", "$cashs.fx_rate"] }
|
||
}
|
||
},
|
||
{
|
||
$group: {
|
||
_id: { date: "$date", currency: "$trans_currency_cd" },
|
||
cash_local: { $sum: "$cash_local" }
|
||
}
|
||
},
|
||
// 第二步:按 date 分组,将不同币种转为字段
|
||
{
|
||
$group: {
|
||
_id: "$_id.date",
|
||
cny: {
|
||
$sum: {
|
||
$cond: [{ $eq: ["$_id.currency", "CNY"] }, "$cash_local", 0]
|
||
}
|
||
},
|
||
hkd: {
|
||
$sum: {
|
||
$cond: [{ $eq: ["$_id.currency", "HKD"] }, "$cash_local", 0]
|
||
}
|
||
},
|
||
total_cash: { $sum: "$cash_local" }
|
||
}
|
||
},
|
||
{ $sort: { _id: 1 } },
|
||
/* 可选:将 _id 重命名为 date */
|
||
{
|
||
$project: {
|
||
_id: 0,
|
||
date: "$_id",
|
||
cny: 1,
|
||
hkd: 1,
|
||
total_cash: 1
|
||
}
|
||
}
|
||
])
|
||
`);
|
||
|
||
assert.ok(command);
|
||
assert.equal(command.collection, "cash");
|
||
const pipeline = JSON.parse(command.pipeline);
|
||
assert.equal(pipeline.length, 7);
|
||
assert.deepEqual(pipeline[4].$group.total_cash, { $sum: "$cash_local" });
|
||
});
|
||
|
||
test("parseMongoAggregateCommand keeps comment markers inside string values", () => {
|
||
const command = parseMongoAggregateCommand(`db.logs.aggregate([
|
||
{ $match: { url: "https://example.com/a//b", note: "literal /* text */" } },
|
||
// comment with closing delimiters )]}
|
||
{ $project: { url: 1, note: 1 } }
|
||
])`);
|
||
|
||
assert.ok(command);
|
||
assert.deepEqual(JSON.parse(command.pipeline)[0].$match, {
|
||
url: "https://example.com/a//b",
|
||
note: "literal /* text */",
|
||
});
|
||
});
|
||
|
||
test("parseMongoAggregateCommand accepts every JavaScript line terminator after comments", () => {
|
||
for (const lineTerminator of ["\n", "\r", "\r\n", "\u2028", "\u2029"]) {
|
||
const command = parseMongoAggregateCommand(`db.logs.aggregate([// pipeline${lineTerminator}{ $match: { active: true } }])`);
|
||
|
||
assert.ok(command, `expected parser result for ${JSON.stringify(lineTerminator)}`);
|
||
assert.deepEqual(JSON.parse(command.pipeline), [{ $match: { active: true } }]);
|
||
}
|
||
});
|
||
|
||
test("parseMongoAggregateCommand accepts official aggregate options document", () => {
|
||
assert.deepEqual(parseMongoAggregateCommand("db.products.aggregate([], {})"), {
|
||
collection: "products",
|
||
pipeline: "[]",
|
||
options: "{}",
|
||
});
|
||
const withExplain = parseMongoAggregateCommand("db.uc_user.aggregate([], {explain: true})");
|
||
assert.equal(withExplain?.collection, "uc_user");
|
||
assert.equal(withExplain?.pipeline, "[]");
|
||
assert.deepEqual(JSON.parse(withExplain?.options ?? "null"), { explain: true });
|
||
|
||
// Official aggregate options are parsed as a free-form object and forwarded to the server.
|
||
const fullOptions = parseMongoAggregateCommand(`db.products.aggregate(
|
||
[{"$match":{"active":true}}],
|
||
{
|
||
allowDiskUse: true,
|
||
cursor: { batchSize: 50 },
|
||
maxTimeMS: 1000,
|
||
collation: { locale: "en" },
|
||
hint: "status_1",
|
||
comment: "agg-test",
|
||
let: { year: 2024 }
|
||
}
|
||
)`);
|
||
assert.equal(fullOptions?.collection, "products");
|
||
assert.deepEqual(JSON.parse(fullOptions?.pipeline ?? "null"), [{ $match: { active: true } }]);
|
||
assert.deepEqual(JSON.parse(fullOptions?.options ?? "null"), {
|
||
allowDiskUse: true,
|
||
cursor: { batchSize: 50 },
|
||
maxTimeMS: 1000,
|
||
collation: { locale: "en" },
|
||
hint: "status_1",
|
||
comment: "agg-test",
|
||
let: { year: 2024 },
|
||
});
|
||
});
|
||
|
||
test("parseMongoAggregateCommand rejects non-array pipelines and invalid options", () => {
|
||
assert.equal(parseMongoAggregateCommand('db.products.aggregate({"$match":{}})'), null);
|
||
assert.equal(parseMongoAggregateCommand("db.products.aggregate([], [])"), null);
|
||
assert.equal(parseMongoAggregateCommand("db.products.aggregate([], {explain: true"), null);
|
||
assert.equal(parseMongoAggregateCommand("db.products.aggregate([]).limit(10)"), null);
|
||
assert.equal(parseMongoAggregateCommand("db.products.aggregate([], {}, true)"), null);
|
||
});
|
||
|
||
test("parseMongoCountDocumentsCommand reads estimatedDocumentCount as a metadata-backed count", () => {
|
||
// The driver already takes the metadata fast path for a filterless legacy count().
|
||
const expected = { collection: "orders", filter: "{}", mode: "legacy" };
|
||
for (const source of ["db.orders.estimatedDocumentCount()", 'db["orders"].estimatedDocumentCount()', "db.getCollection('orders').estimatedDocumentCount();"]) {
|
||
assert.deepEqual(parseMongoCountDocumentsCommand(source), expected, source);
|
||
}
|
||
|
||
assert.equal(parseMongoCountDocumentsCommand("db.orders.estimatedDocumentCount({a: 1})"), null);
|
||
assert.equal(parseMongoCountDocumentsCommand("db.orders.estimatedDocumentCount().limit(5)"), null);
|
||
// count() and countDocuments() keep their own modes.
|
||
assert.equal(parseMongoCountDocumentsCommand("db.orders.countDocuments({})")?.mode, "accurate");
|
||
assert.equal(parseMongoCountDocumentsCommand("db.orders.count()")?.mode, "legacy");
|
||
});
|
||
|
||
test("parseMongoRunCommand reads db.dropDatabase() and db.createCollection() as run commands", () => {
|
||
assert.deepEqual(parseMongoRunCommand("db . dropDatabase ( ) ;"), { commandJson: '{"dropDatabase":1}' });
|
||
assert.deepEqual(parseMongoRunCommand('db.createCollection("events")'), { commandJson: '{"create":"events"}' });
|
||
assert.deepEqual(JSON.parse(parseMongoRunCommand("db.createCollection('logs', {capped: true, size: 1048576, max: 1000})")!.commandJson), {
|
||
create: "logs",
|
||
capped: true,
|
||
size: 1048576,
|
||
max: 1000,
|
||
});
|
||
assert.equal(parseMongoCommand("db.dropDatabase()")?.command.kind, "runCommand");
|
||
|
||
for (const [source, expected] of [
|
||
["db.dropDatabase(1)", /dropDatabase\(\) expects no arguments/],
|
||
["db.createCollection()", /createCollection\(\) expects a collection name and optional options/],
|
||
["db.createCollection(1)", /expects a collection name/],
|
||
['db.createCollection("a$b")', /expects a collection name/],
|
||
['db.createCollection("a", {create: "b"})', /expects a collection name/],
|
||
['db.createCollection("a").x()', /Unexpected text after createCollection/],
|
||
] as const) {
|
||
assert.equal(parseMongoCommand(source), null, source);
|
||
assert.match(describeMongoCommandParseFailure(source), expected, source);
|
||
}
|
||
});
|
||
|
||
test("parseMongoRunCommand reads db.createView() as a create command with viewOn and pipeline", () => {
|
||
assert.deepEqual(JSON.parse(parseMongoRunCommand('db.createView("active_users", "users", [{ $match: { active: true } }])')!.commandJson), {
|
||
create: "active_users",
|
||
viewOn: "users",
|
||
pipeline: [{ $match: { active: true } }],
|
||
});
|
||
assert.deepEqual(JSON.parse(parseMongoRunCommand("db.createView('v', 'users', [], { collation: { locale: 'fr' } });")!.commandJson), {
|
||
create: "v",
|
||
viewOn: "users",
|
||
pipeline: [],
|
||
collation: { locale: "fr" },
|
||
});
|
||
assert.equal(parseMongoCommand('db.createView("v", "users", [])')?.command.kind, "runCommand");
|
||
|
||
for (const [source, expected] of [
|
||
['db.createView("v", "users")', /createView\(\) expects a view name, a source collection name, a pipeline/],
|
||
['db.createView("v", "users", {})', /createView\(\) expects/],
|
||
['db.createView("v", 1, [])', /createView\(\) expects/],
|
||
['db.createView("v", "users", [], { viewOn: "other" })', /createView\(\) expects/],
|
||
['db.createView("v", "users", [{ $match: { a: Foo() } }])', /Unsupported value Foo\(\.\.\.\) in the pipeline argument of createView\(\)/],
|
||
] as const) {
|
||
assert.equal(parseMongoCommand(source), null, source);
|
||
assert.match(describeMongoCommandParseFailure(source), expected, source);
|
||
}
|
||
});
|
||
|
||
test("parseMongoRunCommand reads user and role helpers as usersInfo and rolesInfo", () => {
|
||
for (const [source, expected] of [
|
||
["show users", { usersInfo: 1 }],
|
||
["SHOW ROLES;", { rolesInfo: 1 }],
|
||
["db.getUsers()", { usersInfo: 1 }],
|
||
["db.getUsers({ showCredentials: false, filter: { roles: { $size: 0 } } })", { usersInfo: 1, showCredentials: false, filter: { roles: { $size: 0 } } }],
|
||
['db.getUser("app")', { usersInfo: "app" }],
|
||
["db.getUser('app', { showPrivileges: true })", { usersInfo: "app", showPrivileges: true }],
|
||
["db.getRoles({ showBuiltinRoles: true })", { rolesInfo: 1, showBuiltinRoles: true }],
|
||
['db.getRole("reader")', { rolesInfo: "reader" }],
|
||
] as const) {
|
||
assert.deepEqual(JSON.parse(parseMongoRunCommand(source)?.commandJson ?? "null"), expected, source);
|
||
assert.equal(parseMongoCommand(source)?.command.kind, "runCommand", source);
|
||
}
|
||
|
||
for (const [source, expected] of [
|
||
["db.getUser()", /getUser\(\) expects a user name/],
|
||
["db.getUser(1)", /getUser\(\) expects a user name/],
|
||
["db.getUsers({ usersInfo: 2 })", /getUsers\(\) expects optional options/],
|
||
["db.getRoles({}, {})", /getRoles\(\) expects optional options/],
|
||
['db.getRole("a").x()', /Unexpected text after getRole/],
|
||
] as const) {
|
||
assert.equal(parseMongoCommand(source), null, source);
|
||
assert.match(describeMongoCommandParseFailure(source), expected, source);
|
||
}
|
||
assert.equal(parseMongoCommand("show users extra"), null);
|
||
assert.deepEqual(
|
||
splitMongoCommands("use admin\nshow users\nshow roles").map(({ command }) => command.kind),
|
||
["use", "runCommand", "runCommand"],
|
||
);
|
||
});
|
||
|
||
test("parseMongoRunCommand reads db.stats() and db.serverStatus() as run commands", () => {
|
||
assert.deepEqual(parseMongoRunCommand("db.stats()"), { commandJson: '{"dbStats":1}' });
|
||
assert.deepEqual(parseMongoRunCommand("db . serverStatus ( ) ;"), { commandJson: '{"serverStatus":1}' });
|
||
assert.deepEqual(parseMongoRunCommand("DB.STATS()"), { commandJson: '{"dbStats":1}' });
|
||
|
||
for (const source of ["db.stats(1)", "db.serverStatus({})"]) {
|
||
assert.equal(parseMongoRunCommand(source), null, source);
|
||
assert.match(describeMongoCommandParseFailure(source), /takes no arguments|expects no arguments/, source);
|
||
}
|
||
|
||
// db.collection.stats() is still collection stats, not a run command.
|
||
assert.equal(parseMongoRunCommand("db.orders.stats()"), null);
|
||
assert.equal(parseMongoCommand("db.orders.stats()")?.command.kind, "collectionStats");
|
||
});
|
||
|
||
test("describeMongoCommandParseFailure names an unsupported value constructor and where it is", () => {
|
||
const message = describeMongoCommandParseFailure('db.reports.updateOne({a: 1}, {$set: {t: Foo("x")}}, {upsert: true})');
|
||
assert.match(message, /Unsupported value Foo\(\.\.\.\) in the update argument of updateOne\(\)/);
|
||
assert.match(message, /ObjectId, ISODate, new Date, NumberLong/);
|
||
|
||
assert.match(describeMongoCommandParseFailure("db.c.find({a: new Bar()})"), /Unsupported value new Bar\(\.\.\.\) in the filter argument of find\(\)/);
|
||
// Text inside a string is not a constructor call.
|
||
assert.match(describeMongoCommandParseFailure('db.c.find({a: "Foo(1)", b: })'), /filter argument of find\(\) is not a valid document/);
|
||
});
|
||
|
||
test("describeMongoCommandParseFailure explains the argument shape a known method expects", () => {
|
||
assert.equal(describeMongoCommandParseFailure("db.c.insertOne({a: 1}, {writeConcern: {w: 1}})"), "insertOne() expects one document.");
|
||
assert.equal(describeMongoCommandParseFailure("db.c.deleteOne({a: 1}, {collation: {locale: 'en'}})"), "deleteOne() expects a filter.");
|
||
assert.equal(describeMongoCommandParseFailure("db.c.updateOne({a: 1})"), "updateOne() expects a filter, an update, and optional options.");
|
||
assert.equal(describeMongoCommandParseFailure("db.runCommand()"), "runCommand() expects one command document.");
|
||
assert.equal(describeMongoCommandParseFailure("db.version(1)"), "version() expects no arguments.");
|
||
assert.equal(describeMongoCommandParseFailure("db.c.find({a: 1}).count"), 'Unexpected text after find(...): ".count".');
|
||
});
|
||
|
||
test("describeMongoCommandParseFailure names unsupported methods and points at alternatives", () => {
|
||
const mapReduce = describeMongoCommandParseFailure("db.c.mapReduce()");
|
||
assert.match(mapReduce, /^Collection method mapReduce\(\) is not supported\. Supported collection methods: find, findOne/);
|
||
// Bracket and getCollection targets are recognised too.
|
||
assert.match(describeMongoCommandParseFailure('db["my-coll"].validate()'), /validate\(\) is not supported/);
|
||
assert.match(describeMongoCommandParseFailure('db.getCollection("my-coll").watch()'), /watch\(\) is not supported/);
|
||
|
||
assert.match(describeMongoCommandParseFailure("db.getCollectionNames()"), /db\.getCollectionNames\(\) is not supported; collections are listed in the sidebar/);
|
||
assert.match(describeMongoCommandParseFailure("db.currentOp()"), /db\.currentOp\(\) is not supported/);
|
||
assert.match(describeMongoCommandParseFailure("db.adminCommand({ping: 1})"), /use db\.runCommand/);
|
||
assert.match(describeMongoCommandParseFailure("show collections"), /listed in the sidebar/);
|
||
|
||
// Leading comments do not hide the command shape.
|
||
assert.match(describeMongoCommandParseFailure("// note\ndb.c.mapReduce()"), /mapReduce\(\) is not supported/);
|
||
});
|
||
|
||
test("describeMongoCommandParseFailure reports unclosed delimiters and shell hints", () => {
|
||
const unclosed = describeMongoCommandParseFailure("db.uc_user.aggregate([], {explain: true");
|
||
assert.match(unclosed, /unclosed/i);
|
||
|
||
const chained = describeMongoCommandParseFailure("db.products.aggregate([]).limit(10)");
|
||
assert.match(chained, /chaining|not supported/i);
|
||
|
||
const nonArrayPipeline = describeMongoCommandParseFailure('db.products.aggregate({"$match":{}})');
|
||
assert.match(nonArrayPipeline, /pipeline must be a JSON array/i);
|
||
|
||
const badOptions = describeMongoCommandParseFailure("db.products.aggregate([], [])");
|
||
assert.match(badOptions, /options must be a JSON object/i);
|
||
|
||
const generic = describeMongoCommandParseFailure("SELECT 1");
|
||
assert.match(generic, /MongoDB shell-style commands/i);
|
||
});
|
||
|
||
test("parseMongoAggregateCommand normalises ObjectId arguments with either quote style", () => {
|
||
const oid = "507f1f77bcf86cd799439011";
|
||
for (const quote of ['"', "'"]) {
|
||
const command = parseMongoAggregateCommand(`db.orders.aggregate([{"$match":{"_id":ObjectId(${quote}${oid}${quote})}}])`);
|
||
assert.ok(command, `quote=${quote} should parse`);
|
||
assert.equal(command.collection, "orders");
|
||
assert.deepEqual(JSON.parse(command.pipeline), [{ $match: { _id: { $oid: oid } } }]);
|
||
}
|
||
});
|
||
|
||
test("parseMongoDistinctCommand parses a field with an optional filter", () => {
|
||
assert.deepEqual(parseMongoDistinctCommand('db.products.distinct("category")'), {
|
||
collection: "products",
|
||
field: "category",
|
||
});
|
||
assert.deepEqual(parseMongoDistinctCommand("db.products.distinct('category', { active: true })"), {
|
||
collection: "products",
|
||
field: "category",
|
||
filter: '{ "active": true }',
|
||
});
|
||
assert.deepEqual(parseMongoDistinctCommand('db.getCollection("audit.logs").distinct("level");'), {
|
||
collection: "audit.logs",
|
||
field: "level",
|
||
});
|
||
});
|
||
|
||
test("parseMongoDistinctCommand normalises shell constructors in its filter", () => {
|
||
const command = parseMongoDistinctCommand("db.orders.distinct(\"status\", { _id: ObjectId('507f1f77bcf86cd799439011') })");
|
||
assert.ok(command);
|
||
assert.deepEqual(JSON.parse(command.filter || "{}"), { _id: { $oid: "507f1f77bcf86cd799439011" } });
|
||
});
|
||
|
||
test("parseMongoDistinctCommand requires a non-empty string field", () => {
|
||
assert.equal(parseMongoDistinctCommand("db.products.distinct()"), null);
|
||
assert.equal(parseMongoDistinctCommand('db.products.distinct("")'), null);
|
||
assert.equal(parseMongoDistinctCommand("db.products.distinct({ category: 1 })"), null);
|
||
assert.equal(parseMongoDistinctCommand("db.products.distinct(category)"), null);
|
||
// Cursor chaining and extra arguments are rejected rather than silently ignored.
|
||
assert.equal(parseMongoDistinctCommand('db.products.distinct("category").limit(5)'), null);
|
||
assert.equal(parseMongoDistinctCommand('db.products.distinct("category", {}, {})'), null);
|
||
});
|
||
|
||
test("parseMongoCommand tags distinct with its own kind", () => {
|
||
assert.deepEqual(parseMongoCommand('db.products.distinct("category")')?.command, {
|
||
kind: "distinct",
|
||
collection: "products",
|
||
field: "category",
|
||
});
|
||
});
|
||
|
||
test("mongoDistinctToQueryResult names the column after the field", () => {
|
||
assert.deepEqual(mongoDistinctToQueryResult("category", ["books", "toys", null], 4), {
|
||
columns: ["category"],
|
||
rows: [["books"], ["toys"], [null]],
|
||
affected_rows: 3,
|
||
execution_time_ms: 4,
|
||
});
|
||
// ObjectId values are displayed, not dumped as raw extended JSON.
|
||
assert.deepEqual(mongoDistinctToQueryResult("owner", [{ $oid: "507f1f77bcf86cd799439011" }], 0).rows, [["507f1f77bcf86cd799439011"]]);
|
||
});
|
||
|
||
test("parseMongoGetIndexesCommand parses collection index commands", () => {
|
||
assert.deepEqual(parseMongoGetIndexesCommand("db.web_log.getIndexes();"), {
|
||
collection: "web_log",
|
||
});
|
||
assert.deepEqual(parseMongoGetIndexesCommand('db.getCollection("audit.logs").getIndexes()'), {
|
||
collection: "audit.logs",
|
||
});
|
||
assert.equal(parseMongoGetIndexesCommand("db.web_log.getIndexes({})"), null);
|
||
});
|
||
|
||
test("parseMongoCollectionStatsCommand parses collection stats commands", () => {
|
||
assert.deepEqual(parseMongoCollectionStatsCommand("db.users.stats()"), {
|
||
collection: "users",
|
||
metric: "stats",
|
||
});
|
||
assert.deepEqual(parseMongoCollectionStatsCommand("db.users.dataSize();"), {
|
||
collection: "users",
|
||
metric: "dataSize",
|
||
});
|
||
assert.deepEqual(parseMongoCollectionStatsCommand("db.users.storageSize(1024)"), {
|
||
collection: "users",
|
||
metric: "storageSize",
|
||
scale: 1024,
|
||
});
|
||
assert.deepEqual(parseMongoCollectionStatsCommand("db.users.totalIndexSize()"), {
|
||
collection: "users",
|
||
metric: "totalIndexSize",
|
||
});
|
||
assert.deepEqual(parseMongoCollectionStatsCommand('db.getCollection("audit.logs").stats()'), {
|
||
collection: "audit.logs",
|
||
metric: "stats",
|
||
});
|
||
// A non-numeric argument is rejected rather than silently ignored.
|
||
assert.equal(parseMongoCollectionStatsCommand('db.users.storageSize("big")'), null);
|
||
});
|
||
|
||
test("parseMongoCommand tags collection stats commands with the collectionStats kind", () => {
|
||
const parsed = parseMongoCommand("db.users.stats()");
|
||
assert.ok(parsed);
|
||
assert.deepEqual(parsed.command, { kind: "collectionStats", collection: "users", metric: "stats" });
|
||
});
|
||
|
||
test("mongoCollectionStatsToQueryResult formats stats and single-metric results", () => {
|
||
const stats = {
|
||
count: 12,
|
||
size: 4096,
|
||
avgObjSize: 341,
|
||
storageSize: 8192,
|
||
totalIndexSize: 2048,
|
||
nindexes: 3,
|
||
};
|
||
assert.deepEqual(mongoCollectionStatsToQueryResult("stats", stats, 5), {
|
||
columns: ["count", "size", "avgObjSize", "storageSize", "totalIndexSize", "nindexes"],
|
||
rows: [[12, 4096, 341, 8192, 2048, 3]],
|
||
affected_rows: 1,
|
||
execution_time_ms: 5,
|
||
});
|
||
assert.deepEqual(mongoCollectionStatsToQueryResult("dataSize", stats, 0), {
|
||
columns: ["dataSize"],
|
||
rows: [[4096]],
|
||
affected_rows: 1,
|
||
execution_time_ms: 0,
|
||
});
|
||
assert.deepEqual(mongoCollectionStatsToQueryResult("totalIndexSize", {}, 0), {
|
||
columns: ["totalIndexSize"],
|
||
rows: [[null]],
|
||
affected_rows: 1,
|
||
execution_time_ms: 0,
|
||
});
|
||
});
|
||
|
||
test("splitMongoCommands keeps semicolon-separated mongo commands in order", () => {
|
||
const commands = splitMongoCommands(`
|
||
db.users.insertOne({ name: "A" });
|
||
db.users.insertOne({ name: "B" });
|
||
`);
|
||
assert.deepEqual(
|
||
commands.map(({ text, command }) => ({ kind: command.kind, text })),
|
||
[
|
||
{ kind: "insert", text: 'db.users.insertOne({ name: "A" })' },
|
||
{ kind: "insert", text: 'db.users.insertOne({ name: "B" })' },
|
||
],
|
||
);
|
||
});
|
||
|
||
test("splitMongoCommands splits top-level line starts without semicolons", () => {
|
||
const commands = splitMongoCommands(`
|
||
use accounting
|
||
db.getCollection("entries")
|
||
.find({ status: "open" })
|
||
.limit(5)
|
||
`);
|
||
assert.deepEqual(
|
||
commands.map(({ text, command }) => ({ kind: command.kind, text })),
|
||
[
|
||
{ kind: "use", text: "use accounting" },
|
||
{ kind: "find", text: 'db.getCollection("entries")\n .find({ status: "open" })\n .limit(5)' },
|
||
],
|
||
);
|
||
});
|
||
|
||
test("splitMongoCommandRanges preserve document offsets for newline-separated commands", () => {
|
||
const source = `
|
||
use accounting
|
||
db.getCollection("entries")
|
||
.find({ status: "open" })
|
||
.limit(5)
|
||
`;
|
||
const commands = splitMongoCommandRanges(source);
|
||
|
||
assert.deepEqual(
|
||
commands.map(({ from, to, text, command }) => ({
|
||
from,
|
||
to,
|
||
text,
|
||
kind: command.kind,
|
||
})),
|
||
[
|
||
{
|
||
from: source.indexOf("use accounting"),
|
||
to: source.indexOf("use accounting") + "use accounting".length,
|
||
text: "use accounting",
|
||
kind: "use",
|
||
},
|
||
{
|
||
from: source.indexOf('db.getCollection("entries")'),
|
||
to: source.indexOf(" .limit(5)") + " .limit(5)".length,
|
||
text: 'db.getCollection("entries")\n .find({ status: "open" })\n .limit(5)',
|
||
kind: "find",
|
||
},
|
||
],
|
||
);
|
||
});
|
||
|
||
test("evaluateMongoAggregateSafety follows the DBX-managed MCP permission level", () => {
|
||
const out = parseMongoAggregateCommand('db.products.aggregate([{"$out":"products_copy"}])');
|
||
assert.ok(out);
|
||
assert.equal(mongoAggregateWriteStage(out.pipeline), "$out");
|
||
assert.match(evaluateMongoAggregateSafety(out, {}).reason || "", /DBX MCP read-only policy/i);
|
||
|
||
const merge = parseMongoAggregateCommand('db.products.aggregate([{"$merge":{"into":"products_copy"}}])');
|
||
assert.ok(merge);
|
||
assert.equal(mongoAggregateWriteStage(merge.pipeline), "$merge");
|
||
assert.match(evaluateMongoAggregateSafety(merge, { allowWrites: true }).reason || "", /high-risk operations.*DBX MCP settings/i);
|
||
assert.equal(evaluateMongoAggregateSafety(merge, { allowWrites: true, allowDangerous: true }).allowed, true);
|
||
});
|
||
|
||
test("mongoIndexesToQueryResult formats index metadata", () => {
|
||
assert.deepEqual(
|
||
mongoIndexesToQueryResult(
|
||
[
|
||
{
|
||
name: "_id_",
|
||
columns: ["_id"],
|
||
is_unique: false,
|
||
is_primary: true,
|
||
index_type: "_id: 1",
|
||
filter: null,
|
||
},
|
||
],
|
||
7,
|
||
),
|
||
{
|
||
columns: ["name", "columns", "unique", "primary", "type", "filter"],
|
||
rows: [["_id_", "_id", false, true, "_id: 1", null]],
|
||
affected_rows: 1,
|
||
execution_time_ms: 7,
|
||
},
|
||
);
|
||
});
|
||
|
||
test("mongoCountToQueryResult returns a single count row", () => {
|
||
assert.deepEqual(mongoCountToQueryResult(42, 5), {
|
||
columns: ["count"],
|
||
rows: [[42]],
|
||
affected_rows: 42,
|
||
execution_time_ms: 5,
|
||
});
|
||
});
|
||
|
||
test("mongoDocumentsToQueryResult turns mongo documents into grid rows", () => {
|
||
const result = mongoDocumentsToQueryResult(
|
||
[
|
||
{ _id: "1", name: "Ada", profile: { role: "admin" } },
|
||
{ _id: "2", active: true, name: "Lin" },
|
||
],
|
||
5,
|
||
12,
|
||
);
|
||
|
||
assert.deepEqual(result.columns, ["_id", "name", "profile", "active"]);
|
||
assert.deepEqual(result.rows, [
|
||
["1", "Ada", mongoDocumentGridValue({ role: "admin" }), null],
|
||
["2", "Lin", null, true],
|
||
]);
|
||
assert.deepEqual(result.mongo_documents, [
|
||
{ _id: "1", name: "Ada", profile: { role: "admin" } },
|
||
{ _id: "2", active: true, name: "Lin" },
|
||
]);
|
||
assert.equal(result.affected_rows, 12);
|
||
assert.equal(result.execution_time_ms, 5);
|
||
assert.equal(result.truncated, true);
|
||
});
|
||
|
||
test("mongoDocumentsToQueryResult displays array structures with shell-style BSON literals", () => {
|
||
const document = {
|
||
_id: "1",
|
||
history: [{ at: 'ISODate("2026-06-10T13:59:31.287Z")', by: { $oid: "507f1f77bcf86cd799439011" } }],
|
||
};
|
||
const result = mongoDocumentsToQueryResult([document], 5, 1);
|
||
const historyCell = result.rows[0]?.[1];
|
||
|
||
assert.equal(typeof historyCell, "string");
|
||
assert.equal(mongoDocumentGridDisplayText(historyCell), '[{"at": ISODate("2026-06-10T13:59:31.287Z"), "by": ObjectId("507f1f77bcf86cd799439011")}]');
|
||
assert.equal(mongoDocumentGridEditorText(historyCell), JSON.stringify(document.history));
|
||
});
|
||
|
||
test("mongoDocumentsToQueryResult keeps aligned extended documents for copying", () => {
|
||
const documents = [{ _id: { $oid: "6743e4bfa3f6f84bc3fff6c8" }, createdAt: 'ISODate("2026-07-24T00:00:00Z")' }];
|
||
const copyDocuments = [{ _id: { $oid: "6743e4bfa3f6f84bc3fff6c8" }, createdAt: { $date: "2026-07-24T00:00:00Z" } }];
|
||
|
||
const result = mongoDocumentsToQueryResult(documents, 5, 1, copyDocuments);
|
||
|
||
assert.deepEqual(result.mongo_documents, documents);
|
||
assert.deepEqual(result.mongo_copy_documents, copyDocuments);
|
||
assert.equal(mongoDocumentsToQueryResult(documents, 5, 1, []).mongo_copy_documents, undefined);
|
||
});
|
||
|
||
test("mongoDroppedIndexesToQueryResult exposes partial failures", () => {
|
||
const result = mongoDroppedIndexesToQueryResult(["email_1"], 5, [{ name: "missing_1", message: "index not found" }]);
|
||
|
||
assert.deepEqual(result.columns, ["name", "status", "message"]);
|
||
assert.deepEqual(result.rows, [
|
||
["email_1", "dropped", null],
|
||
["missing_1", "failed", "index not found"],
|
||
]);
|
||
assert.equal(result.affected_rows, 1);
|
||
});
|
||
|
||
test("mongoDocumentsToQueryResult preserves an inexact total marker", () => {
|
||
const result = mongoDocumentsToQueryResult([{ _id: "1" }], 5, 10_000_000, undefined, false);
|
||
|
||
assert.equal(result.total_is_exact, false);
|
||
assert.equal(result.affected_rows, 10_000_000);
|
||
assert.equal(result.truncated, true);
|
||
assert.equal(mongoDocumentsToQueryResult([{ _id: "1" }], 5, 1).total_is_exact, undefined);
|
||
});
|
||
|
||
test("mongoDocumentsToQueryResult displays ids without losing raw type metadata", () => {
|
||
const documents = [
|
||
{ _id: { $oid: "6743e4bfa3f6f84bc3fff6c8" }, name: "object id" },
|
||
{ _id: { $numberLong: "2048938405781032962" }, name: "int64" },
|
||
{ _id: 42, name: "int" },
|
||
{ _id: 42.5, name: "double" },
|
||
{ _id: "customer-42", name: "string" },
|
||
];
|
||
const result = mongoDocumentsToQueryResult(documents, documents.length, documents.length);
|
||
|
||
assert.deepEqual(result.rows, [
|
||
["6743e4bfa3f6f84bc3fff6c8", "object id"],
|
||
["2048938405781032962", "int64"],
|
||
[42, "int"],
|
||
[42.5, "double"],
|
||
["customer-42", "string"],
|
||
]);
|
||
assert.deepEqual(result.mongo_documents, documents);
|
||
});
|
||
|
||
test("buildMongoUpdateDocument ignores _id and preserves typed values", () => {
|
||
const changes = new Map<number, string | number | boolean | null>([
|
||
[0, "other-id"],
|
||
[1, "42"],
|
||
[2, '{"role":"admin"}'],
|
||
[3, null],
|
||
]);
|
||
|
||
const update = buildMongoDocumentUpdate(changes, ["_id", "age", "profile", "nickname"]);
|
||
|
||
assert.deepEqual(update, {
|
||
$set: {
|
||
age: 42,
|
||
profile: { role: "admin" },
|
||
},
|
||
$unset: {
|
||
nickname: "",
|
||
},
|
||
});
|
||
assert.equal(formatMongoDocumentShellLiteral(update), '{"$set":{"age":42,"profile":{"role":"admin"}},"$unset":{"nickname":""}}');
|
||
});
|