diff --git a/apps/desktop/src/lib/backend/api.ts b/apps/desktop/src/lib/backend/api.ts index e3f3dfeb81..bbfde0a9d9 100644 --- a/apps/desktop/src/lib/backend/api.ts +++ b/apps/desktop/src/lib/backend/api.ts @@ -434,6 +434,7 @@ export const mongoDropDatabase = forward("mongoDropDatabase"); export const mongoDropCollection = forward("mongoDropCollection"); export const documentFindDocuments = forward("documentFindDocuments"); export const mongoFindDocuments = forward("mongoFindDocuments"); +export const mongoFindOne = forward("mongoFindOne"); export const mongoCountDocuments = forward("mongoCountDocuments"); export const mongoServerVersion = forward("mongoServerVersion"); export const mongoAggregateDocuments = forward("mongoAggregateDocuments"); @@ -449,6 +450,9 @@ export const mongoUpdateDocuments = forward("mongoUpdateDocuments"); export const documentDeleteDocument = forward("documentDeleteDocument"); export const mongoDeleteDocument = forward("mongoDeleteDocument"); export const mongoDeleteDocuments = forward("mongoDeleteDocuments"); +export const mongoFindOneAndUpdate = forward("mongoFindOneAndUpdate"); +export const mongoFindOneAndReplace = forward("mongoFindOneAndReplace"); +export const mongoFindOneAndDelete = forward("mongoFindOneAndDelete"); // Elasticsearch export const elasticsearchListIndices = forward("elasticsearchListIndices"); diff --git a/apps/desktop/src/lib/backend/http.ts b/apps/desktop/src/lib/backend/http.ts index a0a3216b21..34f87cb715 100644 --- a/apps/desktop/src/lib/backend/http.ts +++ b/apps/desktop/src/lib/backend/http.ts @@ -2015,6 +2015,10 @@ export async function mongoFindDocuments(connectionId: string, database: string, return documentFindDocuments(connectionId, database, collection, skip, limit, filter, projection, sort, executionId); } +export async function mongoFindOne(connectionId: string, database: string, collection: string, filter?: string, projection?: string, options?: string, executionId?: string): Promise { + return post("/api/mongo/find-one", { connectionId, database, collection, filter, projection, options, executionId }); +} + export async function documentFindDocuments(connectionId: string, database: string, collection: string, skip: number, limit: number, filter?: string, projection?: string, sort?: string, executionId?: string): Promise { return post("/api/document-store/find-documents", { connectionId, database, collection, skip, limit, filter, projection, sort, executionId }); } @@ -2128,6 +2132,18 @@ export async function mongoDeleteDocuments(connectionId: string, database: strin return post("/api/mongo/delete-documents", { connectionId, database, collection, filterJson, many }); } +export async function mongoFindOneAndUpdate(connectionId: string, database: string, collection: string, filterJson: string, updateJson: string, optionsJson?: string): Promise { + return post("/api/mongo/find-one-and-update", { connectionId, database, collection, filterJson, updateJson, optionsJson }); +} + +export async function mongoFindOneAndReplace(connectionId: string, database: string, collection: string, filterJson: string, replacementJson: string, optionsJson?: string): Promise { + return post("/api/mongo/find-one-and-replace", { connectionId, database, collection, filterJson, replacementJson, optionsJson }); +} + +export async function mongoFindOneAndDelete(connectionId: string, database: string, collection: string, filterJson: string, optionsJson?: string): Promise { + return post("/api/mongo/find-one-and-delete", { connectionId, database, collection, filterJson, optionsJson }); +} + // --------------------------------------------------------------------------- // History // --------------------------------------------------------------------------- diff --git a/apps/desktop/src/lib/backend/tauri.ts b/apps/desktop/src/lib/backend/tauri.ts index 57297e2bf4..9eee39be24 100644 --- a/apps/desktop/src/lib/backend/tauri.ts +++ b/apps/desktop/src/lib/backend/tauri.ts @@ -1804,6 +1804,10 @@ export async function mongoFindDocuments(connectionId: string, database: string, return documentFindDocuments(connectionId, database, collection, skip, limit, filter, projection, sort, executionId); } +export async function mongoFindOne(connectionId: string, database: string, collection: string, filter?: string, projection?: string, options?: string, executionId?: string): Promise { + return invoke("mongo_find_one", { connectionId, database, collection, filter, projection, options, executionId }); +} + export async function documentFindDocuments(connectionId: string, database: string, collection: string, skip: number, limit: number, filter?: string, projection?: string, sort?: string, executionId?: string): Promise { return invoke("document_find_documents", { connectionId, database, collection, skip, limit, filter, projection, sort, executionId }); } @@ -1921,6 +1925,18 @@ export async function mongoDeleteDocuments(connectionId: string, database: strin return { affected_rows: affectedRows }; } +export async function mongoFindOneAndUpdate(connectionId: string, database: string, collection: string, filterJson: string, updateJson: string, optionsJson?: string): Promise { + return invoke("mongo_find_one_and_update", { connectionId, database, collection, filterJson, updateJson, optionsJson }); +} + +export async function mongoFindOneAndReplace(connectionId: string, database: string, collection: string, filterJson: string, replacementJson: string, optionsJson?: string): Promise { + return invoke("mongo_find_one_and_replace", { connectionId, database, collection, filterJson, replacementJson, optionsJson }); +} + +export async function mongoFindOneAndDelete(connectionId: string, database: string, collection: string, filterJson: string, optionsJson?: string): Promise { + return invoke("mongo_find_one_and_delete", { connectionId, database, collection, filterJson, optionsJson }); +} + // --- History --- export interface HistoryEntry { id: string; diff --git a/apps/desktop/src/lib/mongo/mongoCompletion.ts b/apps/desktop/src/lib/mongo/mongoCompletion.ts index 211bbd262c..ae25aa5305 100644 --- a/apps/desktop/src/lib/mongo/mongoCompletion.ts +++ b/apps/desktop/src/lib/mongo/mongoCompletion.ts @@ -38,6 +38,9 @@ const COLLECTION_METHODS = [ { label: "updateMany", detail: "Update all matching documents", apply: "updateMany({}, { $set: {} })" }, { label: "deleteOne", detail: "Delete one matching document", apply: "deleteOne({})" }, { label: "deleteMany", detail: "Delete all matching documents", apply: "deleteMany({})" }, + { label: "findOneAndUpdate", detail: "Atomically update and return a document", apply: "findOneAndUpdate({}, { $set: {} })" }, + { label: "findOneAndReplace", detail: "Atomically replace and return a document", apply: "findOneAndReplace({}, {})" }, + { label: "findOneAndDelete", detail: "Atomically delete and return a document", apply: "findOneAndDelete({})" }, { label: "getIndexes", detail: "List collection indexes", apply: "getIndexes()" }, { label: "stats", detail: "Show collection statistics", apply: "stats()" }, { label: "dataSize", detail: "Total size of documents in bytes", apply: "dataSize()" }, @@ -72,7 +75,7 @@ const FIELD_SNIPPETS = [ const QUERY_OPERATORS = ["$eq", "$ne", "$gt", "$gte", "$lt", "$lte", "$in", "$nin", "$exists", "$regex", "$and", "$or", "$nor", "$not", "$elemMatch"]; const UPDATE_OPERATORS = ["$set", "$unset", "$inc", "$push", "$pull", "$addToSet", "$rename", "$currentDate", "$setOnInsert"]; const PIPELINE_STAGES = ["$match", "$project", "$group", "$sort", "$limit", "$skip", "$unwind", "$lookup", "$addFields", "$count", "$facet"]; -const QUERY_METHODS = ["find", "findOne", "countDocuments", "updateOne", "updateMany", "deleteOne", "deleteMany", "dropIndex", "dropIndexes", "sort"]; +const QUERY_METHODS = ["find", "findOne", "findOneAndUpdate", "findOneAndReplace", "findOneAndDelete", "countDocuments", "updateOne", "updateMany", "deleteOne", "deleteMany", "dropIndex", "dropIndexes", "sort"]; export function getMongoCompletionContext(text: string, cursor: number): MongoCompletionContext { const safeCursor = Math.max(0, Math.min(cursor, text.length)); @@ -365,7 +368,7 @@ function mongoFieldApplyText(field: string, prefix: string): string { } function findInnermostMongoCall(beforeCursor: string): { method: string; openParenIndex: number } | null { - const callPattern = /\.(find|findOne|countDocuments|updateOne|updateMany|deleteOne|deleteMany|aggregate|sort)\s*\(/g; + const callPattern = /\.(find|findOne|findOneAndUpdate|findOneAndReplace|findOneAndDelete|countDocuments|updateOne|updateMany|deleteOne|deleteMany|aggregate|sort)\s*\(/g; let match: RegExpExecArray | null; let result: { method: string; openParenIndex: number } | null = null; while ((match = callPattern.exec(beforeCursor))) { diff --git a/apps/desktop/src/lib/mongo/mongoShellCommand.ts b/apps/desktop/src/lib/mongo/mongoShellCommand.ts index 76dac780df..d3b0c3210f 100644 --- a/apps/desktop/src/lib/mongo/mongoShellCommand.ts +++ b/apps/desktop/src/lib/mongo/mongoShellCommand.ts @@ -10,6 +10,13 @@ export interface MongoFindCommand { sort?: string; } +export interface MongoFindOneCommand { + collection: string; + filter: string; + projection?: string; + options?: string; +} + export interface MongoCountDocumentsCommand { collection: string; filter: string; @@ -41,10 +48,11 @@ export interface MongoCollectionStatsCommand { scale?: number; } -type MongoWriteKind = "insert" | "update" | "delete" | "createIndex" | "dropIndex" | "dropIndexes" | "dropCollection"; +type MongoWriteKind = "insert" | "update" | "delete" | "createIndex" | "dropIndex" | "dropIndexes" | "dropCollection" | "findOneAndUpdate" | "findOneAndReplace" | "findOneAndDelete"; export type MongoCommand = | ({ kind: "find" } & MongoFindCommand) + | ({ kind: "findOne" } & MongoFindOneCommand) | MongoVersionCommand | ({ kind: "countDocuments" } & MongoCountDocumentsCommand) | ({ kind: "aggregate" } & MongoAggregateCommand) @@ -57,7 +65,10 @@ export type MongoCommand = | { kind: "createIndex"; collection: string; keys: string; options?: string } | { kind: "dropIndex"; collection: string; index: string } | { kind: "dropIndexes"; collection: string; indexes?: string } - | { kind: "dropCollection"; collection: string }; + | { kind: "dropCollection"; collection: string } + | { kind: "findOneAndUpdate"; collection: string; filter: string; update: string; options?: string } + | { kind: "findOneAndReplace"; collection: string; filter: string; replacement: string; options?: string } + | { kind: "findOneAndDelete"; collection: string; filter: string; options?: string }; export type MongoWriteCommand = Extract; @@ -124,6 +135,103 @@ export function parseMongoFindCommand(input: string): MongoFindCommand | null { }; } +export function parseMongoFindOneCommand(input: string): MongoFindOneCommand | null { + const source = input.trim().replace(/;$/, "").trim(); + const target = parseCollectionMethodTarget(source, "findOne"); + if (!target) return null; + + const args = parseMethodArgs(source, target.methodCallIndex); + if (!args) return null; + if (args.length > 3 && args.slice(3).some((arg) => arg.trim())) return null; + + const filter = normalizeJsonArgument(args[0] || "{}"); + if (!filter) return null; + + let projection: string | undefined; + if (args[1]?.trim()) { + const parsedProjection = normalizeJsonArgument(args[1]); + if (!parsedProjection) return null; + projection = parsedProjection; + } + + const options = args[2]?.trim() ? normalizeJsonArgument(args[2]) : undefined; + if (args[2]?.trim() && !options) return null; + + return { + collection: target.collection, + filter, + ...(projection ? { projection } : {}), + ...(options ? { options } : {}), + }; +} + +export interface MongoFindOneAndUpdateCommand { + collection: string; + filter: string; + update: string; + options?: string; +} + +export interface MongoFindOneAndReplaceCommand { + collection: string; + filter: string; + replacement: string; + options?: string; +} + +export interface MongoFindOneAndDeleteCommand { + collection: string; + filter: string; + options?: string; +} + +export function parseMongoFindOneAndUpdateCommand(input: string): MongoFindOneAndUpdateCommand | null { + const source = input.trim().replace(/;$/, "").trim(); + const target = parseCollectionMethodTarget(source, "findOneAndUpdate"); + if (!target) return null; + + const args = parseMethodArgs(source, target.methodCallIndex); + if (!args || args.length < 2 || args.length > 3) return null; + const filter = normalizeJsonArgument(args[0] || "{}"); + const update = normalizeJsonArgument(args[1]); + if (!filter || !update) return null; + const options = args[2]?.trim() ? normalizeJsonArgument(args[2]) : undefined; + if (args[2]?.trim() && !options) return null; + + return { collection: target.collection, filter, update, ...(options ? { options } : {}) }; +} + +export function parseMongoFindOneAndReplaceCommand(input: string): MongoFindOneAndReplaceCommand | null { + const source = input.trim().replace(/;$/, "").trim(); + const target = parseCollectionMethodTarget(source, "findOneAndReplace"); + if (!target) return null; + + const args = parseMethodArgs(source, target.methodCallIndex); + if (!args || args.length < 2 || args.length > 3) return null; + const filter = normalizeJsonArgument(args[0] || "{}"); + const replacement = normalizeJsonArgument(args[1]); + if (!filter || !replacement) return null; + const options = args[2]?.trim() ? normalizeJsonArgument(args[2]) : undefined; + if (args[2]?.trim() && !options) return null; + + return { collection: target.collection, filter, replacement, ...(options ? { options } : {}) }; +} + +export function parseMongoFindOneAndDeleteCommand(input: string): MongoFindOneAndDeleteCommand | null { + const source = input.trim().replace(/;$/, "").trim(); + const target = parseCollectionMethodTarget(source, "findOneAndDelete"); + if (!target) return null; + + const args = parseMethodArgs(source, target.methodCallIndex); + if (!args || args.length < 1 || args.length > 2) return null; + const filter = normalizeJsonArgument(args[0] || "{}"); + if (!filter) return null; + const options = args[1]?.trim() ? normalizeJsonArgument(args[1]) : undefined; + if (args[1]?.trim() && !options) return null; + + return { collection: target.collection, filter, ...(options ? { options } : {}) }; +} + export function applyMongoFindSort(input: string, column: string, direction: "asc" | "desc"): string | null { const source = input.trim().replace(/;$/, "").trim(); const parsed = parseMongoFindCommand(source); @@ -376,6 +484,22 @@ export function parseMongoCommand(input: string): ParsedMongoCommand | null { const find = parseMongoFindCommand(source); return find ? { kind: "find", ...find } : null; }, + (source) => { + const findOne = parseMongoFindOneCommand(source); + return findOne ? { kind: "findOne", ...findOne } : null; + }, + (source) => { + const findOneAndUpdate = parseMongoFindOneAndUpdateCommand(source); + return findOneAndUpdate ? { kind: "findOneAndUpdate", ...findOneAndUpdate } : null; + }, + (source) => { + const findOneAndReplace = parseMongoFindOneAndReplaceCommand(source); + return findOneAndReplace ? { kind: "findOneAndReplace", ...findOneAndReplace } : null; + }, + (source) => { + const findOneAndDelete = parseMongoFindOneAndDeleteCommand(source); + return findOneAndDelete ? { kind: "findOneAndDelete", ...findOneAndDelete } : null; + }, (source) => { const aggregate = parseMongoAggregateCommand(source); return aggregate ? { kind: "aggregate", ...aggregate } : null; @@ -427,7 +551,7 @@ export function evaluateMongoWriteSafety(command: MongoWriteCommand, options: Mo reason: "MCP MongoDB execution is read-only by default. Set DBX_MCP_ALLOW_WRITES=1 to allow write commands.", }; } - if (!options.allowDangerous && (command.kind === "update" || command.kind === "delete") && isEmptyJsonObject(command.filter)) { + if (!options.allowDangerous && (command.kind === "update" || command.kind === "delete" || command.kind === "findOneAndUpdate" || command.kind === "findOneAndReplace" || command.kind === "findOneAndDelete") && isEmptyJsonObject(command.filter)) { return { allowed: false, reason: "MongoDB update/delete commands must include a non-empty filter unless DBX_MCP_ALLOW_DANGEROUS_SQL=1 is set.", diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index fb0d0d5c56..506f437c95 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -2782,6 +2782,21 @@ export const useQueryStore = defineStore("query", () => { }); break; } + case "findOne": { + console.info("[DBX][executeTabSql:mongo-find-one:start]", { traceId, collection: mongoCommand.collection, database: currentDatabase }); + const result = await api.mongoFindOne(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.projection, mongoCommand.options, executionId); + const queryResult = markQueryResultRowsRaw(annotateMongoResult(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total))); + allResults.push(queryResult); + mongoEditTarget = mongoCommands.length === 1 && queryResult.columns.includes("_id") ? { collection: mongoCommand.collection, idColumn: "_id" } : undefined; + console.info("[DBX][executeTabSql:mongo-find-one:done]", { + traceId, + collection: mongoCommand.collection, + database: currentDatabase, + rowCount: result.documents.length, + elapsed: elapsed(), + }); + break; + } case "version": { console.info("[DBX][executeTabSql:mongo-version:start]", { traceId, database: currentDatabase }); const version = await api.mongoServerVersion(tab.connectionId, currentDatabase, executionId); @@ -2862,6 +2877,37 @@ export const useQueryStore = defineStore("query", () => { }); break; } + case "findOneAndUpdate": + case "findOneAndReplace": + case "findOneAndDelete": { + if (options?.mongoSafety) { + const safety = evaluateMongoWriteSafety(mongoCommand, options.mongoSafety); + if (!safety.allowed) throw new Error(safety.reason); + } + console.info("[DBX][executeTabSql:mongo-find-and-modify:start]", { + traceId, + kind: mongoCommand.kind, + collection: mongoCommand.collection, + database: currentDatabase, + }); + const result = + mongoCommand.kind === "findOneAndUpdate" + ? await api.mongoFindOneAndUpdate(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.update, mongoCommand.options) + : mongoCommand.kind === "findOneAndReplace" + ? await api.mongoFindOneAndReplace(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.replacement, mongoCommand.options) + : await api.mongoFindOneAndDelete(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.options); + allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total)))); + mongoEditTarget = undefined; + console.info("[DBX][executeTabSql:mongo-find-and-modify:done]", { + traceId, + kind: mongoCommand.kind, + collection: mongoCommand.collection, + database: currentDatabase, + rowCount: result.documents.length, + elapsed: elapsed(), + }); + break; + } case "insert": case "update": case "delete": diff --git a/crates/dbx-core/src/db/mongo_driver.rs b/crates/dbx-core/src/db/mongo_driver.rs index 49aa7ccc93..4a4bb0e2d7 100644 --- a/crates/dbx-core/src/db/mongo_driver.rs +++ b/crates/dbx-core/src/db/mongo_driver.rs @@ -592,6 +592,61 @@ pub async fn find_documents( Ok(MongoDocumentResult { documents, total }) } +#[derive(Debug, Default, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct MongoFindOneOptions { + sort: Option, +} + +pub async fn find_one( + client: &Client, + database: &str, + collection: &str, + filter: Option<&str>, + projection: Option<&str>, + options: Option<&str>, +) -> Result { + let filter_doc = parse_optional_filter_document(filter)?.unwrap_or_default(); + let projection_doc = parse_optional_json_document(projection, "projection")?; + let options = parse_find_one_options(options)?; + let sort_doc = parse_optional_document(options.sort.as_ref(), "sort")?; + + let col = client.database(database).collection::(collection); + let mut action = col.find_one(filter_doc); + if let Some(projection) = projection_doc { + action = action.projection(projection); + } + if let Some(sort) = sort_doc { + action = action.sort(sort); + } + + let result = action.await.map_err(|e| e.to_string())?; + Ok(single_document_result(result)) +} + +fn parse_optional_filter_document(value: Option<&str>) -> Result, String> { + let Some(raw) = value.map(str::trim).filter(|value| !value.is_empty()) else { + return Ok(None); + }; + let json: serde_json::Value = serde_json::from_str(raw).map_err(|e| format!("Invalid filter JSON: {e}"))?; + json_filter_to_document(&json).map(Some).map_err(|e| format!("Invalid filter: {e}")) +} + +fn parse_find_one_options(options: Option<&str>) -> Result { + match options.map(str::trim).filter(|value| !value.is_empty()) { + Some(raw) => serde_json::from_str(raw).map_err(|e| format!("Invalid findOne options: {e}")), + None => Ok(MongoFindOneOptions::default()), + } +} + +fn parse_optional_json_document(value: Option<&str>, label: &str) -> Result, String> { + let Some(raw) = value.map(str::trim).filter(|value| !value.is_empty()) else { + return Ok(None); + }; + let json: serde_json::Value = serde_json::from_str(raw).map_err(|e| format!("Invalid {label} JSON: {e}"))?; + json_object_to_document(&json).map(Some).map_err(|e| format!("Invalid {label}: {e}")) +} + pub async fn count_documents( client: &Client, database: &str, @@ -979,6 +1034,180 @@ fn parse_update_array_filters(options_json: Option<&str>) -> Result, + return_new_document: Option, + new: Option, + upsert: Option, + projection: Option, + sort: Option, + array_filters: Option>, +} + +#[derive(Debug, Default, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct MongoFindOneAndReplaceOptions { + return_document: Option, + return_new_document: Option, + new: Option, + upsert: Option, + projection: Option, + sort: Option, +} + +#[derive(Debug, Default, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct MongoFindOneAndDeleteOptions { + projection: Option, + sort: Option, +} + +fn parse_find_and_modify_options(options_json: Option<&str>, command: &str) -> Result +where + T: Default + for<'de> Deserialize<'de>, +{ + match options_json.map(str::trim).filter(|value| !value.is_empty()) { + Some(raw) => serde_json::from_str(raw).map_err(|e| format!("Invalid {command} options: {e}")), + None => Ok(T::default()), + } +} + +fn find_and_modify_returns_after( + return_document: Option<&str>, + return_new_document: Option, + new: Option, +) -> Result { + if let Some(return_document) = return_document { + if return_document.eq_ignore_ascii_case("after") { + return Ok(true); + } + if return_document.eq_ignore_ascii_case("before") { + return Ok(false); + } + return Err("returnDocument must be either 'before' or 'after'".to_string()); + } + Ok(return_new_document.or(new).unwrap_or(false)) +} + +fn parse_optional_document(field: Option<&serde_json::Value>, label: &str) -> Result, String> { + field.map(|value| json_object_to_document(value).map_err(|e| format!("Invalid {label}: {e}"))).transpose() +} + +fn find_and_modify_array_filters( + array_filters: Option<&Vec>, +) -> Result>, String> { + array_filters + .map(|filters| { + filters + .iter() + .map(json_filter_to_document) + .collect::, _>>() + .map_err(|e| format!("Invalid arrayFilters: {e}")) + }) + .transpose() +} + +fn single_document_result(document: Option) -> MongoDocumentResult { + match document { + Some(document) => MongoDocumentResult { documents: vec![bson_to_json(&Bson::Document(document))], total: 1 }, + None => MongoDocumentResult { documents: Vec::new(), total: 0 }, + } +} + +pub async fn find_one_and_update( + client: &Client, + database: &str, + collection: &str, + filter_json: &str, + update_json: &str, + options_json: Option<&str>, +) -> Result { + let filter_value: serde_json::Value = + serde_json::from_str(filter_json).map_err(|e| format!("Invalid filter JSON: {e}"))?; + let update_value: serde_json::Value = + serde_json::from_str(update_json).map_err(|e| format!("Invalid update JSON: {e}"))?; + let filter = json_filter_to_document(&filter_value).map_err(|e| format!("Invalid filter: {e}"))?; + let update = json_object_to_document(&update_value).map_err(|e| format!("Invalid update: {e}"))?; + let options: MongoFindOneAndUpdateOptions = parse_find_and_modify_options(options_json, "findOneAndUpdate")?; + let col = client.database(database).collection::(collection); + let mut action = col.find_one_and_update(filter, update); + if find_and_modify_returns_after(options.return_document.as_deref(), options.return_new_document, options.new)? { + action = action.return_document(mongodb::options::ReturnDocument::After); + } + if let Some(upsert) = options.upsert { + action = action.upsert(upsert); + } + if let Some(projection) = parse_optional_document(options.projection.as_ref(), "projection")? { + action = action.projection(projection); + } + if let Some(sort) = parse_optional_document(options.sort.as_ref(), "sort")? { + action = action.sort(sort); + } + if let Some(array_filters) = find_and_modify_array_filters(options.array_filters.as_ref())? { + action = action.array_filters(array_filters); + } + let result = action.await.map_err(|e| e.to_string())?; + Ok(single_document_result(result)) +} + +pub async fn find_one_and_replace( + client: &Client, + database: &str, + collection: &str, + filter_json: &str, + replacement_json: &str, + options_json: Option<&str>, +) -> Result { + let filter_value: serde_json::Value = + serde_json::from_str(filter_json).map_err(|e| format!("Invalid filter JSON: {e}"))?; + let replacement_value: serde_json::Value = + serde_json::from_str(replacement_json).map_err(|e| format!("Invalid replacement JSON: {e}"))?; + let filter = json_filter_to_document(&filter_value).map_err(|e| format!("Invalid filter: {e}"))?; + let replacement = json_object_to_document(&replacement_value).map_err(|e| format!("Invalid replacement: {e}"))?; + let options: MongoFindOneAndReplaceOptions = parse_find_and_modify_options(options_json, "findOneAndReplace")?; + let col = client.database(database).collection::(collection); + let mut action = col.find_one_and_replace(filter, replacement); + if find_and_modify_returns_after(options.return_document.as_deref(), options.return_new_document, options.new)? { + action = action.return_document(mongodb::options::ReturnDocument::After); + } + if let Some(upsert) = options.upsert { + action = action.upsert(upsert); + } + if let Some(projection) = parse_optional_document(options.projection.as_ref(), "projection")? { + action = action.projection(projection); + } + if let Some(sort) = parse_optional_document(options.sort.as_ref(), "sort")? { + action = action.sort(sort); + } + let result = action.await.map_err(|e| e.to_string())?; + Ok(single_document_result(result)) +} + +pub async fn find_one_and_delete( + client: &Client, + database: &str, + collection: &str, + filter_json: &str, + options_json: Option<&str>, +) -> Result { + let filter_value: serde_json::Value = + serde_json::from_str(filter_json).map_err(|e| format!("Invalid filter JSON: {e}"))?; + let filter = json_filter_to_document(&filter_value).map_err(|e| format!("Invalid filter: {e}"))?; + let options: MongoFindOneAndDeleteOptions = parse_find_and_modify_options(options_json, "findOneAndDelete")?; + let col = client.database(database).collection::(collection); + let mut action = col.find_one_and_delete(filter); + if let Some(projection) = parse_optional_document(options.projection.as_ref(), "projection")? { + action = action.projection(projection); + } + if let Some(sort) = parse_optional_document(options.sort.as_ref(), "sort")? { + action = action.sort(sort); + } + let result = action.await.map_err(|e| e.to_string())?; + Ok(single_document_result(result)) +} + pub async fn delete_document(client: &Client, database: &str, collection: &str, id: &str) -> Result { let col = client.database(database).collection::(collection); for filter in document_id_filters(id) { @@ -1910,6 +2139,57 @@ mod tests { assert!(!is_update_operator_document(&Document::new())); } + #[test] + fn find_one_options_accept_sort_and_reject_unimplemented_fields() { + let options = parse_find_one_options(Some(r#"{"sort":{"createdAt":-1}}"#)).unwrap(); + assert_eq!(options.sort, Some(serde_json::json!({ "createdAt": -1 }))); + + let error = parse_find_one_options(Some(r#"{"hint":{"createdAt":1}}"#)).unwrap_err(); + assert!(error.contains("unknown field `hint`")); + } + + #[test] + fn find_and_modify_options_reject_fields_not_applied_by_each_command() { + let update_error = parse_find_and_modify_options::( + Some(r#"{"hint":{"name":1}}"#), + "findOneAndUpdate", + ) + .unwrap_err(); + assert!(update_error.contains("unknown field `hint`")); + + let replace_error = parse_find_and_modify_options::( + Some(r#"{"arrayFilters":[{"item.active":true}]}"#), + "findOneAndReplace", + ) + .unwrap_err(); + assert!(replace_error.contains("unknown field `arrayFilters`")); + + let delete_error = parse_find_and_modify_options::( + Some(r#"{"returnDocument":"after"}"#), + "findOneAndDelete", + ) + .unwrap_err(); + assert!(delete_error.contains("unknown field `returnDocument`")); + } + + #[test] + fn find_and_modify_return_document_rejects_invalid_values() { + assert!(find_and_modify_returns_after(Some("after"), None, None).unwrap()); + assert!(!find_and_modify_returns_after(Some("before"), None, None).unwrap()); + assert!(find_and_modify_returns_after(Some("newest"), None, None).is_err()); + } + + #[test] + fn single_document_result_has_zero_or_one_metadata() { + let empty = single_document_result(None); + assert!(empty.documents.is_empty()); + assert_eq!(empty.total, 0); + + let one = single_document_result(Some(doc! { "_id": 1, "name": "Ada" })); + assert_eq!(one.documents.len(), 1); + assert_eq!(one.total, 1); + } + #[test] fn json_object_to_document_preserves_unchanged_bson_date_fields() { let date = DateTime::parse_rfc3339_str("2026-06-10T13:59:31.287Z").unwrap(); diff --git a/crates/dbx-core/src/mongo_ops.rs b/crates/dbx-core/src/mongo_ops.rs index 8388998784..f27f0cd9d6 100644 --- a/crates/dbx-core/src/mongo_ops.rs +++ b/crates/dbx-core/src/mongo_ops.rs @@ -121,6 +121,27 @@ pub async fn mongo_find_documents_core( .await } +pub async fn mongo_find_one_core( + state: &AppState, + connection_id: &str, + database: &str, + collection: &str, + filter: Option<&str>, + projection: Option<&str>, + options: Option<&str>, +) -> Result { + 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::find_one(client, database, collection, filter, projection, options).await + } + // The legacy agent only exposes paginated find, which also performs a count. + PoolKind::Agent(_) => Err("MongoDB legacy agent does not support the bounded findOne path".to_string()), + _ => Err("Not a MongoDB connection".to_string()), + } +} + pub async fn mongo_count_documents_core( state: &AppState, connection_id: &str, @@ -426,3 +447,71 @@ pub async fn mongo_delete_documents_core( _ => Err("Not a MongoDB connection".to_string()), } } + +pub async fn mongo_find_one_and_update_core( + state: &AppState, + connection_id: &str, + database: &str, + collection: &str, + filter_json: &str, + update_json: &str, + options_json: Option<&str>, +) -> Result { + 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::find_one_and_update(client, database, collection, filter_json, update_json, options_json) + .await + } + PoolKind::Agent(_) => Err("MongoDB legacy agent does not support findOneAndUpdate".to_string()), + _ => Err("Not a MongoDB connection".to_string()), + } +} + +pub async fn mongo_find_one_and_replace_core( + state: &AppState, + connection_id: &str, + database: &str, + collection: &str, + filter_json: &str, + replacement_json: &str, + options_json: Option<&str>, +) -> Result { + 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::find_one_and_replace( + client, + database, + collection, + filter_json, + replacement_json, + options_json, + ) + .await + } + PoolKind::Agent(_) => Err("MongoDB legacy agent does not support findOneAndReplace".to_string()), + _ => Err("Not a MongoDB connection".to_string()), + } +} + +pub async fn mongo_find_one_and_delete_core( + state: &AppState, + connection_id: &str, + database: &str, + collection: &str, + filter_json: &str, + options_json: Option<&str>, +) -> Result { + 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::find_one_and_delete(client, database, collection, filter_json, options_json).await + } + PoolKind::Agent(_) => Err("MongoDB legacy agent does not support findOneAndDelete".to_string()), + _ => Err("Not a MongoDB connection".to_string()), + } +} diff --git a/crates/dbx-core/tests/live_mongodb_find_one.rs b/crates/dbx-core/tests/live_mongodb_find_one.rs new file mode 100644 index 0000000000..0b220467f6 --- /dev/null +++ b/crates/dbx-core/tests/live_mongodb_find_one.rs @@ -0,0 +1,36 @@ +use std::time::Duration; + +use dbx_core::db::mongo_driver; + +#[tokio::test] +#[ignore = "requires DBX_LIVE_MONGODB_URL pointing at a writable MongoDB database"] +async fn find_one_returns_only_the_sorted_document() { + let url = std::env::var("DBX_LIVE_MONGODB_URL").expect("DBX_LIVE_MONGODB_URL"); + let client = mongo_driver::connect(&url, Duration::from_secs(10), Duration::from_secs(60)).await.unwrap(); + let database = "dbx_live_find_one"; + let collection = format!("items_{}", std::process::id()); + + mongo_driver::insert_documents( + &client, + database, + &collection, + r#"[{"name":"old","rank":1},{"name":"new","rank":2}]"#, + ) + .await + .unwrap(); + + let result = mongo_driver::find_one( + &client, + database, + &collection, + Some("{}"), + Some(r#"{"_id":0,"name":1}"#), + Some(r#"{"sort":{"rank":-1}}"#), + ) + .await + .unwrap(); + + assert_eq!(result.total, 1); + assert_eq!(result.documents, vec![serde_json::json!({ "name": "new" })]); + mongo_driver::drop_collection(&client, database, &collection).await.unwrap(); +} diff --git a/crates/dbx-web/src/main.rs b/crates/dbx-web/src/main.rs index 502f004951..76229f5cc3 100644 --- a/crates/dbx-web/src/main.rs +++ b/crates/dbx-web/src/main.rs @@ -476,6 +476,7 @@ async fn main() { .route("/document-store/update-document", post(routes::document_store::update_document)) .route("/document-store/delete-document", post(routes::document_store::delete_document)) .route("/mongo/find-documents", post(routes::mongo::find_documents)) + .route("/mongo/find-one", post(routes::mongo::find_one)) .route("/mongo/count-documents", post(routes::mongo::count_documents)) .route("/mongo/server-version", post(routes::mongo::server_version)) .route("/mongo/collection-stats", post(routes::mongo::collection_stats)) @@ -488,6 +489,9 @@ async fn main() { .route("/mongo/update-documents", post(routes::mongo::update_documents)) .route("/mongo/delete-document", post(routes::mongo::delete_document)) .route("/mongo/delete-documents", post(routes::mongo::delete_documents)) + .route("/mongo/find-one-and-update", post(routes::mongo::find_one_and_update)) + .route("/mongo/find-one-and-replace", post(routes::mongo::find_one_and_replace)) + .route("/mongo/find-one-and-delete", post(routes::mongo::find_one_and_delete)) // History .route("/history", get(routes::history::load_history).delete(routes::history::clear_history)) .route("/history/save", post(routes::history::save_history)) diff --git a/crates/dbx-web/src/routes/mongo.rs b/crates/dbx-web/src/routes/mongo.rs index 5ec057efa6..709d65d25f 100644 --- a/crates/dbx-web/src/routes/mongo.rs +++ b/crates/dbx-web/src/routes/mongo.rs @@ -78,6 +78,18 @@ pub struct MongoFindRequest { pub execution_id: Option, } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MongoFindOneRequest { + pub connection_id: String, + pub database: String, + pub collection: String, + pub filter: Option, + pub projection: Option, + pub options: Option, + pub execution_id: Option, +} + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct MongoCountRequest { @@ -178,6 +190,38 @@ pub struct MongoDeleteDocumentsRequest { pub many: bool, } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MongoFindOneAndUpdateRequest { + pub connection_id: String, + pub database: String, + pub collection: String, + pub filter_json: String, + pub update_json: String, + pub options_json: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MongoFindOneAndReplaceRequest { + pub connection_id: String, + pub database: String, + pub collection: String, + pub filter_json: String, + pub replacement_json: String, + pub options_json: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MongoFindOneAndDeleteRequest { + pub connection_id: String, + pub database: String, + pub collection: String, + pub filter_json: String, + pub options_json: Option, +} + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct MongoUpdateRequest { @@ -297,6 +341,27 @@ pub async fn find_documents( Ok(Json(serde_json::to_value(result).map_err(|e| AppError(e.to_string()))?)) } +pub async fn find_one( + State(state): State>, + Json(req): Json, +) -> Result, AppError> { + let result = run_cancellable( + &state, + req.execution_id, + dbx_core::mongo_ops::mongo_find_one_core( + &state.app, + &req.connection_id, + &req.database, + &req.collection, + req.filter.as_deref(), + req.projection.as_deref(), + req.options.as_deref(), + ), + ) + .await?; + Ok(Json(serde_json::to_value(result).map_err(|e| AppError(e.to_string()))?)) +} + pub async fn count_documents( State(state): State>, Json(req): Json, @@ -478,6 +543,62 @@ pub async fn update_documents( Ok(Json(serde_json::json!({ "affected_rows": result }))) } +pub async fn find_one_and_update( + State(state): State>, + Json(req): Json, +) -> Result, AppError> { + ensure_writable(&state.app, &req.connection_id, "Update").await?; + let result = dbx_core::mongo_ops::mongo_find_one_and_update_core( + &state.app, + &req.connection_id, + &req.database, + &req.collection, + &req.filter_json, + &req.update_json, + req.options_json.as_deref(), + ) + .await + .map_err(AppError)?; + Ok(Json(serde_json::to_value(result).map_err(|e| AppError(e.to_string()))?)) +} + +pub async fn find_one_and_replace( + State(state): State>, + Json(req): Json, +) -> Result, AppError> { + ensure_writable(&state.app, &req.connection_id, "Update").await?; + let result = dbx_core::mongo_ops::mongo_find_one_and_replace_core( + &state.app, + &req.connection_id, + &req.database, + &req.collection, + &req.filter_json, + &req.replacement_json, + req.options_json.as_deref(), + ) + .await + .map_err(AppError)?; + Ok(Json(serde_json::to_value(result).map_err(|e| AppError(e.to_string()))?)) +} + +pub async fn find_one_and_delete( + State(state): State>, + Json(req): Json, +) -> Result, AppError> { + ensure_writable(&state.app, &req.connection_id, "Delete").await?; + let result = dbx_core::mongo_ops::mongo_find_one_and_delete_core( + &state.app, + &req.connection_id, + &req.database, + &req.collection, + &req.filter_json, + req.options_json.as_deref(), + ) + .await + .map_err(AppError)?; + Ok(Json(serde_json::to_value(result).map_err(|e| AppError(e.to_string()))?)) +} + pub async fn delete_document( State(state): State>, Json(req): Json, diff --git a/packages/app-tests/mongoShellCommand.test.ts b/packages/app-tests/mongoShellCommand.test.ts index e44f845de1..1bc0d9a87a 100644 --- a/packages/app-tests/mongoShellCommand.test.ts +++ b/packages/app-tests/mongoShellCommand.test.ts @@ -13,12 +13,17 @@ import { parseMongoCommand, parseMongoCountDocumentsCommand, parseMongoFindCommand, + parseMongoFindOneCommand, + parseMongoFindOneAndUpdateCommand, + parseMongoFindOneAndReplaceCommand, + parseMongoFindOneAndDeleteCommand, parseMongoGetIndexesCommand, parseMongoVersionCommand, 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 } from "../../apps/desktop/src/lib/mongo/mongoDocumentValues.ts"; test("parseMongoFindCommand parses db collection find with an empty JSON filter", () => { @@ -129,6 +134,91 @@ test("parseMongoFindCommand rejects unsupported mongo shell commands", () => { 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("parseMongoVersionCommand parses db.version", () => { assert.deepEqual(parseMongoVersionCommand("db.version();"), { kind: "version" }); assert.equal(parseMongoVersionCommand("db.jobs.version()"), null); diff --git a/src-tauri/src/commands/mongo_cmd.rs b/src-tauri/src/commands/mongo_cmd.rs index 316237fe99..df3e3c10fc 100644 --- a/src-tauri/src/commands/mongo_cmd.rs +++ b/src-tauri/src/commands/mongo_cmd.rs @@ -110,6 +110,35 @@ pub async fn mongo_find_documents( .await } +#[tauri::command] +#[allow(clippy::too_many_arguments)] +pub async fn mongo_find_one( + state: State<'_, Arc>, + connection_id: String, + database: String, + collection: String, + filter: Option, + projection: Option, + options: Option, + execution_id: Option, +) -> Result { + let app = state.inner().clone(); + run_cancellable( + &app, + execution_id, + dbx_core::mongo_ops::mongo_find_one_core( + &app, + &connection_id, + &database, + &collection, + filter.as_deref(), + projection.as_deref(), + options.as_deref(), + ), + ) + .await +} + #[tauri::command] pub async fn mongo_count_documents( state: State<'_, Arc>, @@ -331,3 +360,70 @@ pub async fn mongo_delete_documents( dbx_core::mongo_ops::mongo_delete_documents_core(&state, &connection_id, &database, &collection, &filter_json, many) .await } + +#[tauri::command] +pub async fn mongo_find_one_and_update( + state: State<'_, Arc>, + connection_id: String, + database: String, + collection: String, + filter_json: String, + update_json: String, + options_json: Option, +) -> Result { + ensure_connection_writable(&state, &connection_id, "Update").await?; + dbx_core::mongo_ops::mongo_find_one_and_update_core( + &state, + &connection_id, + &database, + &collection, + &filter_json, + &update_json, + options_json.as_deref(), + ) + .await +} + +#[tauri::command] +pub async fn mongo_find_one_and_replace( + state: State<'_, Arc>, + connection_id: String, + database: String, + collection: String, + filter_json: String, + replacement_json: String, + options_json: Option, +) -> Result { + ensure_connection_writable(&state, &connection_id, "Update").await?; + dbx_core::mongo_ops::mongo_find_one_and_replace_core( + &state, + &connection_id, + &database, + &collection, + &filter_json, + &replacement_json, + options_json.as_deref(), + ) + .await +} + +#[tauri::command] +pub async fn mongo_find_one_and_delete( + state: State<'_, Arc>, + connection_id: String, + database: String, + collection: String, + filter_json: String, + options_json: Option, +) -> Result { + ensure_connection_writable(&state, &connection_id, "Delete").await?; + dbx_core::mongo_ops::mongo_find_one_and_delete_core( + &state, + &connection_id, + &database, + &collection, + &filter_json, + options_json.as_deref(), + ) + .await +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5386998bc1..51c3d03fba 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1286,6 +1286,7 @@ pub fn run() { commands::document_cmd::document_upload_gridfs_file, commands::document_cmd::document_delete_gridfs_file, commands::mongo_cmd::mongo_find_documents, + commands::mongo_cmd::mongo_find_one, commands::mongo_cmd::mongo_count_documents, commands::mongo_cmd::mongo_server_version, commands::mongo_cmd::mongo_collection_stats, @@ -1301,6 +1302,9 @@ pub fn run() { commands::document_cmd::document_delete_document, commands::mongo_cmd::mongo_delete_document, commands::mongo_cmd::mongo_delete_documents, + commands::mongo_cmd::mongo_find_one_and_update, + commands::mongo_cmd::mongo_find_one_and_replace, + commands::mongo_cmd::mongo_find_one_and_delete, #[cfg(feature = "mq-admin")] commands::mq_cmd::mq_test_connection, #[cfg(feature = "mq-admin")]