mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 06:00:39 +00:00
@@ -58,6 +58,7 @@ public final class AgentProtocol {
|
||||
public static final String MONGO_METHOD_COUNT_DOCUMENTS = "count_documents";
|
||||
public static final String MONGO_METHOD_SERVER_VERSION = "server_version";
|
||||
public static final String MONGO_METHOD_CREATE_INDEX = "create_index";
|
||||
public static final String MONGO_METHOD_CREATE_USER = "create_user";
|
||||
public static final String MONGO_METHOD_DROP_INDEXES = "drop_indexes";
|
||||
public static final String MONGO_METHOD_DROP_COLLECTION = "drop_collection";
|
||||
public static final String MONGO_METHOD_DROP_DATABASE = "drop_database";
|
||||
@@ -243,6 +244,7 @@ public final class AgentProtocol {
|
||||
MONGO_METHOD_COUNT_DOCUMENTS,
|
||||
MONGO_METHOD_SERVER_VERSION,
|
||||
MONGO_METHOD_CREATE_INDEX,
|
||||
MONGO_METHOD_CREATE_USER,
|
||||
MONGO_METHOD_DROP_INDEXES,
|
||||
MONGO_METHOD_DROP_COLLECTION,
|
||||
MONGO_METHOD_DROP_DATABASE,
|
||||
|
||||
@@ -39,6 +39,6 @@
|
||||
"disconnect",
|
||||
"shutdown"
|
||||
],
|
||||
"mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_one", "explain_find", "aggregate_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "drop_indexes", "drop_collection", "drop_database", "insert_document", "update_document", "update_documents", "delete_document", "delete_documents"],
|
||||
"mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_one", "explain_find", "aggregate_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "create_user", "drop_indexes", "drop_collection", "drop_database", "insert_document", "update_document", "update_documents", "delete_document", "delete_documents"],
|
||||
"kvMethods": ["kv_list_prefix", "kv_get", "kv_put", "kv_delete", "kv_rename", "kv_history", "kv_status", "etcd_compact", "etcd_defrag", "etcd_watch_start", "etcd_watch_poll", "etcd_watch_stop", "etcd_lease_list", "etcd_lease_get", "etcd_lease_grant", "etcd_lease_keepalive_once", "etcd_lease_revoke", "etcd_auth_user_list", "etcd_auth_user_get", "etcd_auth_user_add", "etcd_auth_user_delete", "etcd_auth_user_change_password", "etcd_auth_user_grant_role", "etcd_auth_user_revoke_role", "etcd_auth_role_list", "etcd_auth_role_get", "etcd_auth_role_add", "etcd_auth_role_delete", "etcd_auth_role_grant_permission", "etcd_auth_role_revoke_permission"]
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"disconnect",
|
||||
"shutdown"
|
||||
],
|
||||
"mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_one", "explain_find", "aggregate_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "drop_indexes", "drop_collection", "drop_database", "insert_document", "update_document", "update_documents", "delete_document", "delete_documents"],
|
||||
"mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_one", "explain_find", "aggregate_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "create_user", "drop_indexes", "drop_collection", "drop_database", "insert_document", "update_document", "update_documents", "delete_document", "delete_documents"],
|
||||
"kvMethods": ["kv_list_prefix", "kv_get", "kv_put", "kv_delete", "kv_rename", "kv_history", "kv_status", "etcd_compact", "etcd_defrag", "etcd_watch_start", "etcd_watch_poll", "etcd_watch_stop", "etcd_lease_list", "etcd_lease_get", "etcd_lease_grant", "etcd_lease_keepalive_once", "etcd_lease_revoke", "etcd_auth_user_list", "etcd_auth_user_get", "etcd_auth_user_add", "etcd_auth_user_delete", "etcd_auth_user_change_password", "etcd_auth_user_grant_role", "etcd_auth_user_revoke_role", "etcd_auth_role_list", "etcd_auth_role_get", "etcd_auth_role_add", "etcd_auth_role_delete", "etcd_auth_role_grant_permission", "etcd_auth_role_revoke_permission"],
|
||||
"sessionField": "agentSessionId",
|
||||
"cursorSessionField": "sessionId"
|
||||
|
||||
@@ -1003,6 +1003,32 @@ public final class MongoAgent {
|
||||
return Collections.singletonMap("name", name);
|
||||
}
|
||||
|
||||
private static Object createUser(JsonObject params) {
|
||||
MongoClient client = requireClient();
|
||||
String database = params.get("database").getAsString();
|
||||
client.getDatabase(database).runCommand(buildCreateUserCommand(params));
|
||||
return Collections.singletonMap("affected_rows", 1);
|
||||
}
|
||||
|
||||
static Document buildCreateUserCommand(JsonObject params) {
|
||||
Document user = requiredDocument(params, "user_json", "User document");
|
||||
Object username = user.remove("user");
|
||||
if (!(username instanceof String) || ((String) username).isBlank()) {
|
||||
throw new IllegalArgumentException("MongoDB createUser requires a non-empty user name");
|
||||
}
|
||||
if (user.containsKey("createUser") || user.containsKey("writeConcern")) {
|
||||
throw new IllegalArgumentException("MongoDB createUser user document contains reserved command fields");
|
||||
}
|
||||
|
||||
Document command = new Document("createUser", username);
|
||||
command.putAll(user);
|
||||
Document writeConcern = documentOrNull(params, "write_concern_json");
|
||||
if (writeConcern != null) {
|
||||
command.put("writeConcern", writeConcern);
|
||||
}
|
||||
return command;
|
||||
}
|
||||
|
||||
private static Document requiredDocument(JsonObject params, String key, String label) {
|
||||
Document document = documentOrNull(params, key);
|
||||
if (document == null) {
|
||||
@@ -1562,6 +1588,7 @@ public final class MongoAgent {
|
||||
case AgentProtocol.MONGO_METHOD_COUNT_DOCUMENTS -> countDocuments(params);
|
||||
case AgentProtocol.MONGO_METHOD_SERVER_VERSION -> serverVersion(params);
|
||||
case AgentProtocol.MONGO_METHOD_CREATE_INDEX -> createIndex(params);
|
||||
case AgentProtocol.MONGO_METHOD_CREATE_USER -> createUser(params);
|
||||
case AgentProtocol.MONGO_METHOD_DROP_INDEXES -> dropIndexes(params);
|
||||
case AgentProtocol.MONGO_METHOD_DROP_COLLECTION -> dropCollection(params);
|
||||
case AgentProtocol.MONGO_METHOD_DROP_DATABASE -> dropDatabase(params);
|
||||
|
||||
@@ -521,6 +521,33 @@ class MongoAgentTest {
|
||||
assertTrue(AgentProtocol.MONGO_LEGACY_METHODS.contains(AgentProtocol.MONGO_METHOD_CREATE_INDEX));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createUserMethodIsRecognizedAndBuildsTheExpectedCommand() {
|
||||
String response = MongoAgent.handleRequest(
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":121,\"method\":\"create_user\","
|
||||
+ "\"params\":{\"database\":\"admin\","
|
||||
+ "\"user_json\":\"{\\\"user\\\":\\\"test-db\\\",\\\"pwd\\\":\\\"test-password\\\",\\\"roles\\\":[{\\\"role\\\":\\\"readWrite\\\",\\\"db\\\":\\\"db1\\\"}]}\"}}"
|
||||
);
|
||||
|
||||
JsonObject json = JsonParser.parseString(response).getAsJsonObject();
|
||||
assertEquals(121, json.get("id").getAsInt());
|
||||
assertEquals("Not connected", json.getAsJsonObject("error").get("message").getAsString());
|
||||
assertFalse(json.getAsJsonObject("error").get("message").getAsString().contains("Unknown method"));
|
||||
assertTrue(AgentProtocol.MONGO_LEGACY_METHODS.contains(AgentProtocol.MONGO_METHOD_CREATE_USER));
|
||||
|
||||
JsonObject params = JsonParser.parseString(
|
||||
"{\"database\":\"admin\","
|
||||
+ "\"user_json\":\"{\\\"user\\\":\\\"test-db\\\",\\\"pwd\\\":\\\"test-password\\\",\\\"roles\\\":[{\\\"role\\\":\\\"readWrite\\\",\\\"db\\\":\\\"db1\\\"}]}\","
|
||||
+ "\"write_concern_json\":\"{\\\"w\\\":\\\"majority\\\"}\"}"
|
||||
).getAsJsonObject();
|
||||
Document command = MongoAgent.buildCreateUserCommand(params);
|
||||
assertEquals("createUser", command.keySet().iterator().next());
|
||||
assertEquals("test-db", command.getString("createUser"));
|
||||
assertEquals("test-password", command.getString("pwd"));
|
||||
assertEquals("readWrite", command.getList("roles", Document.class).get(0).getString("role"));
|
||||
assertEquals("majority", command.get("writeConcern", Document.class).getString("w"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultIndexNameMatchesNativeDriverForWholeDoubles() {
|
||||
assertEquals(
|
||||
|
||||
@@ -581,6 +581,7 @@ export const mongoAggregateDocuments = forward("mongoAggregateDocuments");
|
||||
export const mongoDistinct = forward("mongoDistinct");
|
||||
export const mongoCollectionStats = forward("mongoCollectionStats");
|
||||
export const mongoCreateIndex = forward("mongoCreateIndex");
|
||||
export const mongoCreateUser = forward("mongoCreateUser");
|
||||
export const mongoDropIndexes = forward("mongoDropIndexes");
|
||||
export const documentInsertDocument = forward("documentInsertDocument");
|
||||
export const mongoInsertDocument = forward("mongoInsertDocument");
|
||||
|
||||
@@ -3202,6 +3202,15 @@ export async function mongoCreateIndex(connectionId: string, database: string, c
|
||||
});
|
||||
}
|
||||
|
||||
export async function mongoCreateUser(connectionId: string, database: string, userJson: string, writeConcernJson?: string): Promise<{ affected_rows: number }> {
|
||||
return post("/api/mongo/create-user", {
|
||||
connectionId,
|
||||
database,
|
||||
userJson,
|
||||
writeConcernJson,
|
||||
});
|
||||
}
|
||||
|
||||
export async function mongoDropIndexes(connectionId: string, database: string, collection: string, indexesJson?: string, single = false): Promise<MongoDropIndexesResult> {
|
||||
return post("/api/mongo/drop-indexes", {
|
||||
connectionId,
|
||||
|
||||
@@ -3136,6 +3136,15 @@ export async function mongoCreateIndex(connectionId: string, database: string, c
|
||||
});
|
||||
}
|
||||
|
||||
export async function mongoCreateUser(connectionId: string, database: string, userJson: string, writeConcernJson?: string): Promise<{ affected_rows: number }> {
|
||||
return invoke("mongo_create_user", {
|
||||
connectionId,
|
||||
database,
|
||||
userJson,
|
||||
writeConcernJson,
|
||||
});
|
||||
}
|
||||
|
||||
export async function mongoDropIndexes(connectionId: string, database: string, collection: string, indexesJson?: string, single = false): Promise<MongoDropIndexesResult> {
|
||||
return invoke("mongo_drop_indexes", {
|
||||
connectionId,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
normalizeJsonArgument,
|
||||
parseCollectionMethodTarget,
|
||||
parseMongoAggregateCommand,
|
||||
parseMongoObjectArgument,
|
||||
quoteUnquotedObjectKeys,
|
||||
splitTopLevel,
|
||||
type MongoAggregateCommand,
|
||||
@@ -61,6 +62,11 @@ export interface MongoVersionCommand {
|
||||
kind: "version";
|
||||
}
|
||||
|
||||
export interface MongoCreateUserCommand {
|
||||
userJson: string;
|
||||
writeConcernJson?: string;
|
||||
}
|
||||
|
||||
export type MongoCollectionStatsMetric = "stats" | "dataSize" | "storageSize" | "totalIndexSize";
|
||||
|
||||
export interface MongoCollectionStatsCommand {
|
||||
@@ -75,7 +81,7 @@ export interface MongoDistinctCommand {
|
||||
filter?: string;
|
||||
}
|
||||
|
||||
type MongoWriteKind = "insert" | "update" | "delete" | "createIndex" | "dropIndex" | "dropIndexes" | "dropCollection" | "findOneAndUpdate" | "findOneAndReplace" | "findOneAndDelete";
|
||||
type MongoWriteKind = "insert" | "update" | "delete" | "createIndex" | "createUser" | "dropIndex" | "dropIndexes" | "dropCollection" | "findOneAndUpdate" | "findOneAndReplace" | "findOneAndDelete";
|
||||
|
||||
export type MongoCommand =
|
||||
| ({ kind: "find" } & MongoFindCommand)
|
||||
@@ -87,6 +93,7 @@ export type MongoCommand =
|
||||
| ({ kind: "getIndexes" } & MongoGetIndexesCommand)
|
||||
| ({ kind: "collectionStats" } & MongoCollectionStatsCommand)
|
||||
| ({ kind: "use" } & MongoUseCommand)
|
||||
| ({ kind: "createUser" } & MongoCreateUserCommand)
|
||||
| { kind: "insert"; collection: string; docsJson: string }
|
||||
| { kind: "update"; collection: string; filter: string; update: string; options?: string; many: boolean }
|
||||
| { kind: "delete"; collection: string; filter: string; many: boolean }
|
||||
@@ -471,6 +478,27 @@ export function parseMongoVersionCommand(input: string): MongoVersionCommand | n
|
||||
return /^db\s*\.\s*version\s*\(\s*\)$/i.test(source) ? { kind: "version" } : null;
|
||||
}
|
||||
|
||||
export function parseMongoCreateUserCommand(input: string): MongoCreateUserCommand | null {
|
||||
const source = input.trim().replace(/;$/, "").trim();
|
||||
const match = /^db\s*\.\s*createUser\s*\(/i.exec(source);
|
||||
if (!match) return null;
|
||||
const openIndex = source.indexOf("(", match.index);
|
||||
const closeIndex = findMatchingParen(source, openIndex);
|
||||
if (closeIndex < 0 || source.slice(closeIndex + 1).trim()) return null;
|
||||
const args = splitTopLevel(source.slice(openIndex + 1, closeIndex));
|
||||
if (args.length < 1 || args.length > 2) return null;
|
||||
const userJson = parseMongoObjectArgument(args[0]);
|
||||
if (!userJson) return null;
|
||||
const user = JSON.parse(userJson) as Record<string, unknown>;
|
||||
if (typeof user.user !== "string" || !user.user.trim()) return null;
|
||||
const writeConcernJson = args[1]?.trim() ? parseMongoObjectArgument(args[1]) : undefined;
|
||||
if (args[1]?.trim() && !writeConcernJson) return null;
|
||||
return {
|
||||
userJson: JSON.stringify(user),
|
||||
...(writeConcernJson ? { writeConcernJson: JSON.stringify(JSON.parse(writeConcernJson)) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseMongoWriteCommand(input: string): MongoWriteCommand | null {
|
||||
const source = input.trim().replace(/;$/, "").trim();
|
||||
const insertOne = parseCollectionMethodTarget(source, "insertOne");
|
||||
@@ -575,6 +603,10 @@ export function parseMongoCommand(input: string): ParsedMongoCommand | null {
|
||||
const version = parseMongoVersionCommand(source);
|
||||
return version ?? null;
|
||||
},
|
||||
(source) => {
|
||||
const createUser = parseMongoCreateUserCommand(source);
|
||||
return createUser ? { kind: "createUser", ...createUser } : null;
|
||||
},
|
||||
(source) => {
|
||||
// Legacy Mongo shell uses count()/find().count(); keep accepting it
|
||||
// while mapping to DBX's countDocuments-compatible result path.
|
||||
|
||||
@@ -4043,6 +4043,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
case "update":
|
||||
case "delete":
|
||||
case "createIndex":
|
||||
case "createUser":
|
||||
case "dropIndex":
|
||||
case "dropIndexes":
|
||||
case "dropCollection": {
|
||||
@@ -4054,7 +4055,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
traceId,
|
||||
database: currentDatabase,
|
||||
kind: mongoCommand.kind,
|
||||
collection: mongoCommand.collection,
|
||||
...(mongoCommand.kind === "createUser" ? {} : { collection: mongoCommand.collection }),
|
||||
});
|
||||
mongoEditTarget = undefined;
|
||||
if (mongoCommand.kind === "insert") {
|
||||
@@ -4066,6 +4067,9 @@ export const useQueryStore = defineStore("query", () => {
|
||||
} else if (mongoCommand.kind === "createIndex") {
|
||||
const result = await api.mongoCreateIndex(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.keys, mongoCommand.options);
|
||||
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoCreateIndexToQueryResult(result.name, performance.now() - commandStartedAt))));
|
||||
} else if (mongoCommand.kind === "createUser") {
|
||||
const result = await api.mongoCreateUser(executionConnectionId, currentDatabase, mongoCommand.userJson, mongoCommand.writeConcernJson);
|
||||
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoWriteToQueryResult(result.affected_rows, performance.now() - commandStartedAt))));
|
||||
} else if (mongoCommand.kind === "dropIndex" || mongoCommand.kind === "dropIndexes") {
|
||||
try {
|
||||
const result = await api.mongoDropIndexes(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.kind === "dropIndex" ? mongoCommand.index : mongoCommand.indexes, mongoCommand.kind === "dropIndex");
|
||||
@@ -4084,7 +4088,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
traceId,
|
||||
database: currentDatabase,
|
||||
kind: mongoCommand.kind,
|
||||
collection: mongoCommand.collection,
|
||||
...(mongoCommand.kind === "createUser" ? {} : { collection: mongoCommand.collection }),
|
||||
elapsed: elapsed(),
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -39,6 +39,6 @@
|
||||
"disconnect",
|
||||
"shutdown"
|
||||
],
|
||||
"mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_one", "explain_find", "aggregate_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "drop_indexes", "drop_collection", "drop_database", "insert_document", "update_document", "update_documents", "delete_document", "delete_documents"],
|
||||
"mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_one", "explain_find", "aggregate_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "create_user", "drop_indexes", "drop_collection", "drop_database", "insert_document", "update_document", "update_documents", "delete_document", "delete_documents"],
|
||||
"kvMethods": ["kv_list_prefix", "kv_get", "kv_put", "kv_delete", "kv_rename", "kv_history", "kv_status", "etcd_compact", "etcd_defrag", "etcd_watch_start", "etcd_watch_poll", "etcd_watch_stop", "etcd_lease_list", "etcd_lease_get", "etcd_lease_grant", "etcd_lease_keepalive_once", "etcd_lease_revoke", "etcd_auth_user_list", "etcd_auth_user_get", "etcd_auth_user_add", "etcd_auth_user_delete", "etcd_auth_user_change_password", "etcd_auth_user_grant_role", "etcd_auth_user_revoke_role", "etcd_auth_role_list", "etcd_auth_role_get", "etcd_auth_role_add", "etcd_auth_role_delete", "etcd_auth_role_grant_permission", "etcd_auth_role_revoke_permission"]
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"disconnect",
|
||||
"shutdown"
|
||||
],
|
||||
"mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_one", "explain_find", "aggregate_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "drop_indexes", "drop_collection", "drop_database", "insert_document", "update_document", "update_documents", "delete_document", "delete_documents"],
|
||||
"mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_one", "explain_find", "aggregate_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "create_user", "drop_indexes", "drop_collection", "drop_database", "insert_document", "update_document", "update_documents", "delete_document", "delete_documents"],
|
||||
"kvMethods": ["kv_list_prefix", "kv_get", "kv_put", "kv_delete", "kv_rename", "kv_history", "kv_status", "etcd_compact", "etcd_defrag", "etcd_watch_start", "etcd_watch_poll", "etcd_watch_stop", "etcd_lease_list", "etcd_lease_get", "etcd_lease_grant", "etcd_lease_keepalive_once", "etcd_lease_revoke", "etcd_auth_user_list", "etcd_auth_user_get", "etcd_auth_user_add", "etcd_auth_user_delete", "etcd_auth_user_change_password", "etcd_auth_user_grant_role", "etcd_auth_user_revoke_role", "etcd_auth_role_list", "etcd_auth_role_get", "etcd_auth_role_add", "etcd_auth_role_delete", "etcd_auth_role_grant_permission", "etcd_auth_role_revoke_permission"],
|
||||
"sessionField": "agentSessionId",
|
||||
"cursorSessionField": "sessionId"
|
||||
|
||||
@@ -1279,6 +1279,7 @@ pub enum MongoAgentMethod {
|
||||
CountDocuments,
|
||||
ServerVersion,
|
||||
CreateIndex,
|
||||
CreateUser,
|
||||
DropIndexes,
|
||||
DropCollection,
|
||||
DropDatabase,
|
||||
@@ -1290,7 +1291,7 @@ pub enum MongoAgentMethod {
|
||||
}
|
||||
|
||||
impl MongoAgentMethod {
|
||||
pub const ALL: [Self; 18] = [
|
||||
pub const ALL: [Self; 19] = [
|
||||
Self::ListDatabases,
|
||||
Self::ListCollections,
|
||||
Self::FindDocuments,
|
||||
@@ -1301,6 +1302,7 @@ impl MongoAgentMethod {
|
||||
Self::CountDocuments,
|
||||
Self::ServerVersion,
|
||||
Self::CreateIndex,
|
||||
Self::CreateUser,
|
||||
Self::DropIndexes,
|
||||
Self::DropCollection,
|
||||
Self::DropDatabase,
|
||||
@@ -1323,6 +1325,7 @@ impl MongoAgentMethod {
|
||||
Self::CountDocuments => "count_documents",
|
||||
Self::ServerVersion => "server_version",
|
||||
Self::CreateIndex => "create_index",
|
||||
Self::CreateUser => "create_user",
|
||||
Self::DropIndexes => "drop_indexes",
|
||||
Self::DropCollection => "drop_collection",
|
||||
Self::DropDatabase => "drop_database",
|
||||
@@ -2520,6 +2523,13 @@ impl AgentDriverClient {
|
||||
self.call_mongo_method(MongoAgentMethod::CreateIndex, params).await
|
||||
}
|
||||
|
||||
pub async fn mongo_create_user<T: DeserializeOwned + Send + 'static>(
|
||||
&mut self,
|
||||
params: Value,
|
||||
) -> Result<T, String> {
|
||||
self.call_mongo_method(MongoAgentMethod::CreateUser, params).await
|
||||
}
|
||||
|
||||
pub async fn mongo_drop_indexes<T: DeserializeOwned + Send + 'static>(
|
||||
&mut self,
|
||||
params: Value,
|
||||
@@ -4348,6 +4358,7 @@ for line in sys.stdin:
|
||||
assert_eq!(MongoAgentMethod::CountDocuments.as_str(), "count_documents");
|
||||
assert_eq!(MongoAgentMethod::ServerVersion.as_str(), "server_version");
|
||||
assert_eq!(MongoAgentMethod::CreateIndex.as_str(), "create_index");
|
||||
assert_eq!(MongoAgentMethod::CreateUser.as_str(), "create_user");
|
||||
assert_eq!(MongoAgentMethod::DropIndexes.as_str(), "drop_indexes");
|
||||
assert_eq!(MongoAgentMethod::DropCollection.as_str(), "drop_collection");
|
||||
assert_eq!(MongoAgentMethod::DropDatabase.as_str(), "drop_database");
|
||||
|
||||
@@ -1231,6 +1231,48 @@ pub async fn create_index(
|
||||
Ok(name)
|
||||
}
|
||||
|
||||
pub async fn create_user(
|
||||
client: &Client,
|
||||
database: &str,
|
||||
user_json: &str,
|
||||
write_concern_json: Option<&str>,
|
||||
) -> Result<(), String> {
|
||||
let database = validate_mongo_namespace_name(database, "Database")?;
|
||||
let command = create_user_command(user_json, write_concern_json)?;
|
||||
client.database(database).run_command(command).await.map_err(|error| error.kind.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_create_user_request(user_json: &str, write_concern_json: Option<&str>) -> Result<(), String> {
|
||||
create_user_command(user_json, write_concern_json).map(|_| ())
|
||||
}
|
||||
|
||||
fn create_user_command(user_json: &str, write_concern_json: Option<&str>) -> Result<Document, String> {
|
||||
let user_value: serde_json::Value =
|
||||
serde_json::from_str(user_json).map_err(|error| format!("Invalid MongoDB user JSON: {error}"))?;
|
||||
let mut user = json_object_to_document_extended_json(&user_value)
|
||||
.map_err(|error| format!("Invalid MongoDB user document: {error}"))?;
|
||||
let username = user
|
||||
.remove("user")
|
||||
.and_then(|value| value.as_str().map(str::to_string))
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.ok_or("MongoDB createUser requires a non-empty user name")?;
|
||||
if user.contains_key("createUser") || user.contains_key("writeConcern") {
|
||||
return Err("MongoDB createUser user document contains reserved command fields".to_string());
|
||||
}
|
||||
|
||||
let mut command = doc! { "createUser": username };
|
||||
command.extend(user);
|
||||
if let Some(write_concern_json) = write_concern_json.filter(|value| !value.trim().is_empty()) {
|
||||
let write_concern_value: serde_json::Value = serde_json::from_str(write_concern_json)
|
||||
.map_err(|error| format!("Invalid MongoDB write concern JSON: {error}"))?;
|
||||
let write_concern = json_object_to_document_extended_json(&write_concern_value)
|
||||
.map_err(|error| format!("Invalid MongoDB write concern: {error}"))?;
|
||||
command.insert("writeConcern", write_concern);
|
||||
}
|
||||
Ok(command)
|
||||
}
|
||||
|
||||
/// Validate an index request before it reaches either the native driver or the
|
||||
/// Legacy Agent. In particular, `key` belongs to the createIndexes command
|
||||
/// itself and must not be smuggled through the options document.
|
||||
@@ -3013,6 +3055,35 @@ mod tests {
|
||||
assert_eq!(error, "Collection name is required");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_user_command_preserves_roles_and_write_concern() {
|
||||
let command = create_user_command(
|
||||
r#"{"user":"test-db","pwd":"test-password","roles":[{"role":"readWrite","db":"db1"}]}"#,
|
||||
Some(r#"{"w":"majority","wtimeout":5000}"#),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(command.keys().next().map(String::as_str), Some("createUser"));
|
||||
assert_eq!(command.get_str("createUser").unwrap(), "test-db");
|
||||
assert_eq!(command.get_str("pwd").unwrap(), "test-password");
|
||||
let roles = command.get_array("roles").unwrap();
|
||||
assert_eq!(roles[0].as_document().unwrap(), &doc! { "role": "readWrite", "db": "db1" });
|
||||
assert_eq!(command.get_document("writeConcern").unwrap(), &doc! { "w": "majority", "wtimeout": 5000_i32 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_user_command_rejects_missing_names_and_reserved_fields() {
|
||||
for user in [
|
||||
r#"{"pwd":"secret","roles":[]}"#,
|
||||
r#"{"user":"","pwd":"secret","roles":[]}"#,
|
||||
r#"{"user":"app","createUser":"other","pwd":"secret","roles":[]}"#,
|
||||
r#"{"user":"app","writeConcern":{"w":1},"pwd":"secret","roles":[]}"#,
|
||||
] {
|
||||
assert!(create_user_command(user, None).is_err(), "{user}");
|
||||
}
|
||||
assert!(create_user_command(r#"{"user":"app","pwd":"secret","roles":[]}"#, Some("true")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_indexes_command_keeps_raw_options_and_generates_a_name() {
|
||||
let (command, name) = create_indexes_command(
|
||||
|
||||
@@ -466,6 +466,37 @@ pub async fn mongo_create_index_core(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn mongo_create_user_core(
|
||||
state: &AppState,
|
||||
connection_id: &str,
|
||||
database: &str,
|
||||
user_json: &str,
|
||||
write_concern_json: Option<&str>,
|
||||
) -> Result<u64, String> {
|
||||
mongo_driver::validate_mongo_namespace_name(database, "Database")?;
|
||||
mongo_driver::validate_create_user_request(user_json, write_concern_json)?;
|
||||
ensure_document_pool(state, connection_id).await?;
|
||||
let connections = state.connections.read().await;
|
||||
match connections.get(connection_id).ok_or("Not found")? {
|
||||
PoolKind::MongoDb(client) => {
|
||||
mongo_driver::create_user(client, database, user_json, write_concern_json).await?;
|
||||
Ok(1)
|
||||
}
|
||||
PoolKind::Agent(client) => {
|
||||
let mut client = client.lock().await;
|
||||
let result: serde_json::Value = client
|
||||
.mongo_create_user(serde_json::json!({
|
||||
"database": database,
|
||||
"user_json": user_json,
|
||||
"write_concern_json": write_concern_json,
|
||||
}))
|
||||
.await?;
|
||||
Ok(result.get("affected_rows").and_then(serde_json::Value::as_u64).unwrap_or(1))
|
||||
}
|
||||
_ => Err("Not a MongoDB connection".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn mongo_drop_indexes_core(
|
||||
state: &AppState,
|
||||
connection_id: &str,
|
||||
@@ -875,6 +906,12 @@ pub async fn execute_mongo_command_core(
|
||||
mongo_create_index_core(state, connection_id, database, collection, keys, options.as_deref()).await?;
|
||||
Ok(scalar_query_result("name", Value::String(name)))
|
||||
}
|
||||
MongoCommand::CreateUser { user_json, write_concern_json } => {
|
||||
let affected =
|
||||
mongo_create_user_core(state, connection_id, database, user_json, write_concern_json.as_deref())
|
||||
.await?;
|
||||
Ok(affected_query_result(affected))
|
||||
}
|
||||
MongoCommand::DropIndexes { collection, indexes, single } => {
|
||||
let result =
|
||||
mongo_drop_indexes_core(state, connection_id, database, collection, indexes.as_deref(), *single)
|
||||
|
||||
@@ -8,6 +8,13 @@ pub enum MongoCommand {
|
||||
Version,
|
||||
#[serde(rename = "use")]
|
||||
Use { database: String },
|
||||
#[serde(rename = "createUser")]
|
||||
CreateUser {
|
||||
#[serde(rename = "userJson")]
|
||||
user_json: String,
|
||||
#[serde(rename = "writeConcernJson")]
|
||||
write_concern_json: Option<String>,
|
||||
},
|
||||
#[serde(rename = "find")]
|
||||
Find {
|
||||
collection: String,
|
||||
@@ -77,7 +84,8 @@ impl MongoCommand {
|
||||
pub fn is_mutating(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Insert { .. }
|
||||
Self::CreateUser { .. }
|
||||
| Self::Insert { .. }
|
||||
| Self::Update { .. }
|
||||
| Self::Delete { .. }
|
||||
| Self::CreateIndex { .. }
|
||||
@@ -90,7 +98,7 @@ impl MongoCommand {
|
||||
}
|
||||
|
||||
pub fn is_dangerous(&self) -> bool {
|
||||
matches!(self, Self::DropCollection { .. })
|
||||
matches!(self, Self::CreateUser { .. } | Self::DropCollection { .. })
|
||||
|| matches!(self, Self::DropIndexes { indexes: None, single: false, .. })
|
||||
|| matches!(self, Self::Aggregate { pipeline, .. } if aggregate_writes(pipeline))
|
||||
}
|
||||
@@ -348,6 +356,23 @@ pub fn parse(input: &str) -> Result<MongoCommand, String> {
|
||||
if let Some(database) = parse_use_database(source) {
|
||||
return Ok(MongoCommand::Use { database });
|
||||
}
|
||||
if let Some((args, tail)) = database_method_call(source, "createUser") {
|
||||
if !tail.is_empty() || !(1..=2).contains(&args.len()) {
|
||||
return Err("MongoDB createUser() requires a user document and optional write concern.".to_string());
|
||||
}
|
||||
let user_json = normalized_json(&args[0])?;
|
||||
let user = parse_json_value(&user_json)
|
||||
.and_then(|value| value.as_object().cloned())
|
||||
.ok_or("MongoDB createUser() requires a user document.")?;
|
||||
if user.get("user").and_then(Value::as_str).is_none_or(|user| user.trim().is_empty()) {
|
||||
return Err("MongoDB createUser() requires a non-empty user name.".to_string());
|
||||
}
|
||||
let write_concern_json = optional_json_argument(args.get(1))?;
|
||||
if write_concern_json.as_deref().and_then(parse_json_value).is_some_and(|value| !value.is_object()) {
|
||||
return Err("MongoDB createUser() write concern must be a document.".to_string());
|
||||
}
|
||||
return Ok(MongoCommand::CreateUser { user_json, write_concern_json });
|
||||
}
|
||||
let (collection, prefix_end) = parse_collection_prefix(source)?;
|
||||
|
||||
if let Some((args, tail)) = method_call(source, prefix_end, "find") {
|
||||
@@ -651,6 +676,24 @@ fn method_call(source: &str, prefix_end: usize, method: &str) -> Option<(Vec<Str
|
||||
Some((split_top_level(&source[open + 1..close]), source[close + 1..].trim().to_string()))
|
||||
}
|
||||
|
||||
fn database_method_call(source: &str, method: &str) -> Option<(Vec<String>, String)> {
|
||||
if !source.get(..2).is_some_and(|prefix| prefix.eq_ignore_ascii_case("db")) {
|
||||
return None;
|
||||
}
|
||||
let after_db = source.get(2..)?.trim_start();
|
||||
let after_dot = after_db.strip_prefix('.')?.trim_start();
|
||||
if !after_dot.get(..method.len()).is_some_and(|name| name.eq_ignore_ascii_case(method)) {
|
||||
return None;
|
||||
}
|
||||
let after_method = after_dot.get(method.len()..)?.trim_start();
|
||||
if !after_method.starts_with('(') {
|
||||
return None;
|
||||
}
|
||||
let open = source.len() - after_method.len();
|
||||
let close = matching_paren(source, open)?;
|
||||
Some((split_top_level(&source[open + 1..close]), source[close + 1..].trim().to_string()))
|
||||
}
|
||||
|
||||
fn chained_calls(chain: &str) -> Result<Vec<(String, Vec<String>)>, String> {
|
||||
let mut rest = chain.trim();
|
||||
let mut calls = Vec::new();
|
||||
@@ -957,6 +1000,28 @@ mod tests {
|
||||
assert_eq!(validate_safety(&legacy_update, true, false, false), Err(MongoSafetyError::EmptyFilter));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_create_user_as_a_dangerous_write() {
|
||||
let command = parse(
|
||||
r#"db . createUser({user: "test-db", pwd: "test-password", roles: [{role: "readWrite", db: "db1"}]}, {w: "majority"})"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
command,
|
||||
MongoCommand::CreateUser {
|
||||
user_json: r#"{"user":"test-db","pwd":"test-password","roles":[{"role":"readWrite","db":"db1"}]}"#
|
||||
.to_string(),
|
||||
write_concern_json: Some(r#"{"w":"majority"}"#.to_string()),
|
||||
}
|
||||
);
|
||||
assert!(command.is_mutating());
|
||||
assert!(command.is_dangerous());
|
||||
assert_eq!(validate_safety(&command, true, false, false), Err(MongoSafetyError::Dangerous));
|
||||
assert_eq!(validate_safety(&command, true, true, false), Ok(()));
|
||||
assert!(parse(r#"db.createUser({pwd: "missing-user", roles: []})"#).is_err());
|
||||
assert!(parse(r#"db.createUser({user: "test"}, "majority")"#).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn treats_effectively_unbounded_write_filters_as_dangerous() {
|
||||
for command in [
|
||||
@@ -1102,6 +1167,10 @@ mod tests {
|
||||
let count = serde_json::to_value(parse("db.items.count({})").unwrap()).unwrap();
|
||||
assert_eq!(count["kind"], "countDocuments");
|
||||
assert_eq!(count["accurate"], false);
|
||||
let create_user =
|
||||
serde_json::to_value(parse(r#"db.createUser({user: "app", pwd: "secret", roles: []})"#).unwrap()).unwrap();
|
||||
assert_eq!(create_user["kind"], "createUser");
|
||||
assert_eq!(create_user["userJson"], r#"{"user":"app","pwd":"secret","roles":[]}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -34,7 +34,7 @@ pub const QUERY_CANCELED: &str = "Query canceled";
|
||||
/// Fallback when a Mongo connection hits the generic SQL executor instead of the shell path.
|
||||
/// Wording must match packages/mongo-shell `MONGO_SHELL_COMMAND_HINT`
|
||||
/// (desktop/CLI diagnose first; this is only the Rust SQL-executor backstop).
|
||||
const MONGO_SHELL_COMMAND_HINT: &str = "Use MongoDB shell-style commands, for example: db.collection.find({}).limit(100), db.collection.aggregate([]), db.collection.aggregate([], { explain: true }), db.version(), db.collection.countDocuments({}), db.collection.distinct(\"field\"), db.collection.getIndexes(), db.collection.createIndex({...}), or db.collection.insertOne({...}).";
|
||||
const MONGO_SHELL_COMMAND_HINT: &str = "Use MongoDB shell-style commands, for example: db.collection.find({}).limit(100), db.collection.aggregate([]), db.collection.aggregate([], { explain: true }), db.version(), db.collection.countDocuments({}), db.collection.distinct(\"field\"), db.collection.getIndexes(), db.collection.createIndex({...}), db.createUser({...}), or db.collection.insertOne({...}).";
|
||||
const SQL_OMITTED_ERROR_CONTEXT: &str =
|
||||
"SQL text omitted from user-facing error; enable debug SQL diagnostics to inspect the original statement.";
|
||||
|
||||
|
||||
@@ -1138,6 +1138,24 @@ impl DbxBackend for WebBackend {
|
||||
Value::String(value.get("name").and_then(Value::as_str).unwrap_or("").to_string()),
|
||||
))
|
||||
}
|
||||
MongoCommand::CreateUser { user_json, write_concern_json } => {
|
||||
let value: Value = self
|
||||
.request(
|
||||
reqwest::Method::POST,
|
||||
"/api/mongo/create-user",
|
||||
Some(json!({
|
||||
"connectionId": connection_id,
|
||||
"database": database,
|
||||
"userJson": user_json,
|
||||
"writeConcernJson": write_concern_json,
|
||||
})),
|
||||
)
|
||||
.await?
|
||||
.json()
|
||||
.await
|
||||
.map_err(|error| format!("Invalid MongoDB create user response: {error}"))?;
|
||||
Ok(affected_query_result(affected_rows_from_value(&value)))
|
||||
}
|
||||
MongoCommand::DropIndexes { collection, indexes, single } => {
|
||||
let value: Value = self
|
||||
.request(
|
||||
|
||||
@@ -632,6 +632,7 @@ async fn main() {
|
||||
.route("/mongo/aggregate-documents", post(routes::mongo::aggregate_documents))
|
||||
.route("/mongo/distinct", post(routes::mongo::distinct))
|
||||
.route("/mongo/create-index", post(routes::mongo::create_index))
|
||||
.route("/mongo/create-user", post(routes::mongo::create_user))
|
||||
.route("/mongo/drop-indexes", post(routes::mongo::drop_indexes))
|
||||
.route("/mongo/insert-document", post(routes::mongo::insert_document))
|
||||
.route("/mongo/insert-documents", post(routes::mongo::insert_documents))
|
||||
|
||||
@@ -191,6 +191,15 @@ pub struct MongoCreateIndexRequest {
|
||||
pub options_json: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MongoCreateUserRequest {
|
||||
pub connection_id: String,
|
||||
pub database: String,
|
||||
pub user_json: String,
|
||||
pub write_concern_json: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MongoDropIndexesRequest {
|
||||
@@ -616,6 +625,26 @@ pub async fn create_index(
|
||||
Ok(Json(serde_json::json!({ "name": name })))
|
||||
}
|
||||
|
||||
pub async fn create_user(
|
||||
State(state): State<Arc<WebState>>,
|
||||
headers: HeaderMap,
|
||||
Json(req): Json<MongoCreateUserRequest>,
|
||||
) -> Result<Json<serde_json::Value>, AppError> {
|
||||
super::mcp_policy::ensure_dangerous_write(&state, &headers, &req.connection_id, &req.database, "Create user")
|
||||
.await?;
|
||||
ensure_writable(&state.app, &req.connection_id, "Create user").await?;
|
||||
let affected_rows = dbx_core::mongo_ops::mongo_create_user_core(
|
||||
&state.app,
|
||||
&req.connection_id,
|
||||
&req.database,
|
||||
&req.user_json,
|
||||
req.write_concern_json.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
Ok(Json(serde_json::json!({ "affected_rows": affected_rows })))
|
||||
}
|
||||
|
||||
pub async fn drop_indexes(
|
||||
State(state): State<Arc<WebState>>,
|
||||
headers: HeaderMap,
|
||||
|
||||
+36
@@ -16,6 +16,7 @@ import {
|
||||
parseMongoAggregateCommand,
|
||||
parseMongoCollectionStatsCommand,
|
||||
parseMongoCommand,
|
||||
parseMongoCreateUserCommand,
|
||||
parseMongoCountDocumentsCommand,
|
||||
parseMongoDistinctCommand,
|
||||
parseMongoFindCommand,
|
||||
@@ -49,6 +50,41 @@ test("normalizeRustMongoCommand preserves the desktop command contract", () => {
|
||||
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("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",
|
||||
|
||||
+54
@@ -4547,6 +4547,60 @@ test("mongo createIndex execution uses the dedicated create-index endpoint", asy
|
||||
}
|
||||
});
|
||||
|
||||
test("mongo createUser execution follows a preceding use command", async () => {
|
||||
const restoreStorage = installMemoryStorage();
|
||||
setActivePinia(createPinia());
|
||||
const connectionStore = useConnectionStore();
|
||||
const store = useQueryStore();
|
||||
const originalFetch = globalThis.fetch;
|
||||
let createUserBody: any;
|
||||
|
||||
connectionStore.addEphemeralConnection({
|
||||
...conn("mongo-1"),
|
||||
db_type: "mongodb",
|
||||
port: 27017,
|
||||
});
|
||||
|
||||
globalThis.fetch = withConnectionHealthMock(async (input, init) => {
|
||||
if (String(input) === "/api/mongo/create-user") {
|
||||
createUserBody = JSON.parse(String(init?.body ?? "{}"));
|
||||
return Response.json({ affected_rows: 1 });
|
||||
}
|
||||
return new Response("unexpected request", { status: 500 });
|
||||
});
|
||||
|
||||
try {
|
||||
const tabId = store.createTab("mongo-1", "accounting", "Query", "query", "");
|
||||
await store.executeTabSql(
|
||||
tabId,
|
||||
`use admin
|
||||
|
||||
db.createUser({
|
||||
user: "test-db",
|
||||
pwd: "test-password",
|
||||
roles: [{ role: "readWrite", db: "db1" }]
|
||||
})`,
|
||||
);
|
||||
const tab = store.tabs.find((item) => item.id === tabId);
|
||||
|
||||
assert.deepEqual(createUserBody, {
|
||||
connectionId: "mongo-1",
|
||||
database: "admin",
|
||||
userJson: '{"user":"test-db","pwd":"test-password","roles":[{"role":"readWrite","db":"db1"}]}',
|
||||
});
|
||||
assert.equal(tab?.database, "admin");
|
||||
assert.equal(tab?.results?.length, 2);
|
||||
assert.deepEqual(tab?.results?.[0]?.rows, [["switched to db admin"]]);
|
||||
assert.deepEqual(tab?.results?.[1]?.columns, []);
|
||||
assert.deepEqual(tab?.results?.[1]?.rows, []);
|
||||
assert.equal(tab?.results?.[1]?.affected_rows, 1);
|
||||
assert.equal(tab?.results?.[1]?.sourceLabel, undefined);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
restoreStorage();
|
||||
}
|
||||
});
|
||||
|
||||
test("mongo dropIndex execution uses the dedicated drop-indexes endpoint", async () => {
|
||||
const restoreStorage = installMemoryStorage();
|
||||
setActivePinia(createPinia());
|
||||
|
||||
@@ -25,7 +25,8 @@ export const MONGO_SHELL_COMMAND_HINT =
|
||||
"Use MongoDB shell-style commands, for example: db.collection.find({}).limit(100), " +
|
||||
"db.collection.aggregate([]), db.collection.aggregate([], { explain: true }), " +
|
||||
'db.version(), db.collection.countDocuments({}), db.collection.distinct("field"), ' +
|
||||
"db.collection.getIndexes(), db.collection.createIndex({...}), or db.collection.insertOne({...}).";
|
||||
"db.collection.getIndexes(), db.collection.createIndex({...}), db.createUser({...}), " +
|
||||
"or db.collection.insertOne({...}).";
|
||||
|
||||
const PIPELINE_MUST_BE_ARRAY =
|
||||
"MongoDB aggregate pipeline must be a JSON array (for example [{ $match: {} }]).";
|
||||
|
||||
@@ -334,6 +334,36 @@ pub async fn mongo_create_index(
|
||||
Ok(serde_json::json!({ "name": name }))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mongo_create_user(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
database: String,
|
||||
user_json: String,
|
||||
write_concern_json: Option<String>,
|
||||
mcp_request: Option<bool>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
if mcp_request == Some(true) {
|
||||
crate::commands::mcp_bridge::ensure_mcp_dangerous_write_allowed_by_id(
|
||||
state.inner(),
|
||||
&connection_id,
|
||||
&database,
|
||||
"Create user",
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ensure_connection_writable(&state, &connection_id, "Create user").await?;
|
||||
let affected_rows = dbx_core::mongo_ops::mongo_create_user_core(
|
||||
&state,
|
||||
&connection_id,
|
||||
&database,
|
||||
&user_json,
|
||||
write_concern_json.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
Ok(serde_json::json!({ "affected_rows": affected_rows }))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mongo_drop_indexes(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
|
||||
@@ -1894,6 +1894,7 @@ pub fn run() {
|
||||
commands::mongo_cmd::mongo_aggregate_documents,
|
||||
commands::mongo_cmd::mongo_distinct,
|
||||
commands::mongo_cmd::mongo_create_index,
|
||||
commands::mongo_cmd::mongo_create_user,
|
||||
commands::mongo_cmd::mongo_drop_indexes,
|
||||
commands::document_cmd::document_insert_document,
|
||||
commands::mongo_cmd::mongo_insert_document,
|
||||
|
||||
Reference in New Issue
Block a user