Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 37 additions & 6 deletions lib/core/database/mongodb_connection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,41 @@ class MongoConnection {
return buffer.toString();
}

/// Returns a connection URI targeting [databaseName].
///
/// When credentials are present and no explicit `authSource` query parameter
/// exists, the method automatically adds `authSource=<original_db>` (defaults
/// to `admin`) so that authentication succeeds on databases other than the
/// one the user was created in.
String buildUriForDatabase(String databaseName) {
final baseUri = buildConnectionUri();
final uri = Uri.parse(baseUri);

// Determine the authSource that should be used.
// 1) Already present in the query → keep it.
// 2) Not present but credentials exist → use the original path db, or
// fall back to "admin" (Mongo's default authSource).
final existingAuthSource = uri.queryParameters['authSource'];
final hasCredentials =
uri.userInfo.isNotEmpty ||
(username != null && username!.isNotEmpty);

Map<String, String>? newQueryParams;
if (existingAuthSource == null && hasCredentials) {
// Original db from the URI path (strip leading '/')
final origDb = uri.path.replaceFirst('/', '');
final source = (origDb.isNotEmpty) ? origDb : 'admin';
newQueryParams = Map<String, String>.from(uri.queryParameters)
..['authSource'] = source;
}

final newUri = uri.replace(
path: '/$databaseName',
queryParameters: newQueryParams ?? uri.queryParameters,
);
return newUri.toString();
}

/// Connects to MongoDB server.
Future<void> connect() async {
if (_isConnected && _db != null) {
Expand Down Expand Up @@ -122,9 +157,7 @@ class MongoConnection {

try {
// Switch to admin database to list all databases
final baseUri = buildConnectionUri();
final uri = Uri.parse(baseUri);
final adminUri = uri.replace(path: '/admin').toString();
final adminUri = buildUriForDatabase('admin');
final adminDb = await Db.create(adminUri);
await adminDb.open();
try {
Expand Down Expand Up @@ -152,9 +185,7 @@ class MongoConnection {

try {
// Create a new Db connection to the specified database
final baseUri = buildConnectionUri();
final uri = Uri.parse(baseUri);
final dbUri = uri.replace(path: '/$databaseName').toString();
final dbUri = buildUriForDatabase(databaseName);
final db = await Db.create(dbUri);
await db.open();
try {
Expand Down
161 changes: 132 additions & 29 deletions lib/core/database/mongodb_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,10 @@ class MongoService {
throw StateError('Not connected to MongoDB');
}

// Create a new Db connection to the specified database
final baseUri = connection.buildConnectionUri();
final uri = Uri.parse(baseUri);
final dbUri = uri.replace(path: '/$database').toString();
final db = await Db.create(dbUri);
await db.open();
try {
return _withDb(connection, database, (db) async {
final cmd = command.map((k, v) => MapEntry(k, v as Object));
return await db.runCommand(cmd);
} finally {
await db.close();
}
});
}

/// Executes a find query.
Expand All @@ -104,16 +96,10 @@ class MongoService {
throw StateError('Not connected to MongoDB');
}

// Create a new Db connection to the specified database
final baseUri = connection.buildConnectionUri();
final uri = Uri.parse(baseUri);
final dbUri = uri.replace(path: '/$database').toString();
final db = await Db.create(dbUri);
await db.open();
try {
return _withDb(connection, database, (db) async {
final coll = db.collection(collection);
final selector = filter ?? <String, dynamic>{};

final stream = coll.find(selector);
final results = <Map<String, dynamic>>[];
int count = 0;
Expand All @@ -129,9 +115,7 @@ class MongoService {
count++;
}
return results;
} finally {
await db.close();
}
});
}

/// Executes an aggregation pipeline.
Expand All @@ -145,20 +129,139 @@ class MongoService {
throw StateError('Not connected to MongoDB');
}

// Create a new Db connection to the specified database
final baseUri = connection.buildConnectionUri();
final uri = Uri.parse(baseUri);
final dbUri = uri.replace(path: '/$database').toString();
final db = await Db.create(dbUri);
await db.open();
try {
return _withDb(connection, database, (db) async {
final coll = db.collection(collection);
final pipe = pipeline.map((stage) => stage.map((k, v) => MapEntry(k, v as Object))).toList();
final pipe = pipeline
.map((stage) => stage.map((k, v) => MapEntry(k, v as Object)))
.toList();
final result = await coll.aggregate(pipe);
// aggregate returns a Map, wrap it in a List
return [Map<String, dynamic>.from(result)];
});
}

/// Opens a temporary [Db] for the given [database], runs [action], then closes.
Future<T> _withDb<T>(
MongoConnection connection,
String database,
Future<T> Function(Db db) action,
) async {
if (!connection.isConnected) {
throw StateError('Not connected to MongoDB');
}
final dbUri = connection.buildUriForDatabase(database);
final db = await Db.create(dbUri);
await db.open();
try {
return await action(db);
} finally {
await db.close();
}
}

/// Returns the document count for a collection (with optional filter).
Future<int> countDocuments(
MongoConnection connection,
String database,
String collection, {
Map<String, dynamic>? filter,
}) async {
return _withDb(connection, database, (db) async {
final result = await db.runCommand(<String, Object>{
'count': collection,
if (filter != null && filter.isNotEmpty) 'query': filter,
});
final n = result['n'];
if (n is int) return n;
if (n is num) return n.toInt();
return int.tryParse(n.toString()) ?? 0;
});
}

/// Returns `collStats` for a collection.
Future<Map<String, dynamic>> getCollectionStats(
MongoConnection connection,
String database,
String collection,
) async {
return _withDb(connection, database, (db) async {
return await db.runCommand(<String, Object>{'collStats': collection});
});
}

/// Inserts a single document, returns the inserted document (with _id).
Future<Map<String, dynamic>> insertDocument(
MongoConnection connection,
String database,
String collection,
Map<String, dynamic> document,
) async {
return _withDb(connection, database, (db) async {
final coll = db.collection(collection);
await coll.insertOne(document);
return document;
});
}

/// Updates a single document matched by [filter].
Future<void> updateDocument(
MongoConnection connection,
String database,
String collection,
Map<String, dynamic> filter,
Map<String, dynamic> update,
) async {
return _withDb(connection, database, (db) async {
final coll = db.collection(collection);
await coll.updateOne(filter, update);
});
}

/// Deletes a single document matched by [filter].
Future<void> deleteDocument(
MongoConnection connection,
String database,
String collection,
Map<String, dynamic> filter,
) async {
return _withDb(connection, database, (db) async {
final coll = db.collection(collection);
await coll.deleteOne(filter);
});
}

/// Returns index information for a collection.
Future<List<Map<String, dynamic>>> getIndexes(
MongoConnection connection,
String database,
String collection,
) async {
return _withDb(connection, database, (db) async {
final coll = db.collection(collection);
final indexes = await coll.getIndexes();
return indexes.cast<Map<String, dynamic>>();
});
}

/// Creates a new collection.
Future<void> createCollection(
MongoConnection connection,
String database,
String collectionName,
) async {
return _withDb(connection, database, (db) async {
await db.createCollection(collectionName);
});
}

/// Drops a collection.
Future<void> dropCollection(
MongoConnection connection,
String database,
String collectionName,
) async {
return _withDb(connection, database, (db) async {
await db.dropCollection(collectionName);
});
}
}
6 changes: 3 additions & 3 deletions lib/features/main_screen/workspace_panel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'package:flutter/material.dart' as material show Container, EdgeInsets, B
import 'package:querya_desktop/core/storage/local_db.dart';
import 'package:querya_desktop/shared/widgets/widgets.dart';

import 'package:querya_desktop/features/mongodb/mongo_stats_view.dart';
import 'package:querya_desktop/features/mongodb/mongo_explorer_view.dart';
import 'package:querya_desktop/features/redis/redis_view.dart';
import 'query_editor_tab.dart';
import 'results_tab.dart';
Expand Down Expand Up @@ -30,13 +30,13 @@ class _WorkspacePanelState extends State<WorkspacePanel> {
Widget build(BuildContext context) {
final theme = Theme.of(context);

// If a MongoDB connection is selected, show the stats view
// If a MongoDB connection is selected, show the MongoDB explorer
if (widget.activeConnection != null &&
widget.activeConnection!.type == 'mongodb') {
return material.Container(
color: theme.colorScheme.background,
child: material.SizedBox.expand(
child: MongoStatsView(
child: MongoExplorerView(
key: ValueKey(widget.activeConnection!.id),
connectionRow: widget.activeConnection!,
),
Expand Down
Loading