Skip to content
Merged

Dev #12

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
2 changes: 1 addition & 1 deletion .flutter-plugins-dependencies
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"path_provider_foundation","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.6.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\sqflite_darwin-2.4.2\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"path_provider_android","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_android-2.2.22\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_android","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\sqflite_android-2.4.2+2\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"bitsdojo_window_macos","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\bitsdojo_window_macos-0.1.4\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.6.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\sqflite_darwin-2.4.2\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"bitsdojo_window_linux","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\bitsdojo_window_linux-0.1.4\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_linux-2.2.1\\\\","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"bitsdojo_window_windows","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\bitsdojo_window_windows-0.1.6\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_windows-2.3.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false}],"web":[]},"dependencyGraph":[{"name":"bitsdojo_window","dependencies":["bitsdojo_window_windows","bitsdojo_window_macos","bitsdojo_window_linux"]},{"name":"bitsdojo_window_linux","dependencies":[]},{"name":"bitsdojo_window_macos","dependencies":[]},{"name":"bitsdojo_window_windows","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]}],"date_created":"2026-03-05 22:16:33.139337","version":"3.38.5","swift_package_manager_enabled":{"ios":false,"macos":false}}
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"path_provider_foundation","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.6.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\sqflite_darwin-2.4.2\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"path_provider_android","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_android-2.2.22\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_android","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\sqflite_android-2.4.2+2\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"bitsdojo_window_macos","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\bitsdojo_window_macos-0.1.4\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.6.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\sqflite_darwin-2.4.2\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"bitsdojo_window_linux","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\bitsdojo_window_linux-0.1.4\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_linux-2.2.1\\\\","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"bitsdojo_window_windows","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\bitsdojo_window_windows-0.1.6\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"C:\\\\Users\\\\junte\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_windows-2.3.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false}],"web":[]},"dependencyGraph":[{"name":"bitsdojo_window","dependencies":["bitsdojo_window_windows","bitsdojo_window_macos","bitsdojo_window_linux"]},{"name":"bitsdojo_window_linux","dependencies":[]},{"name":"bitsdojo_window_macos","dependencies":[]},{"name":"bitsdojo_window_windows","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]}],"date_created":"2026-03-07 10:36:50.253535","version":"3.38.5","swift_package_manager_enabled":{"ios":false,"macos":false}}
50 changes: 44 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 All @@ -98,10 +133,13 @@ class MongoConnection {

/// Disconnects from MongoDB server.
Future<void> disconnect() async {
if (_db != null && _isConnected) {
await _db!.close();
_db = null;
_isConnected = false;
_isConnected = false;
final db = _db;
_db = null;
try {
await db?.close();
} catch (_) {
// Connection may already be closed — ignore.
}
}

Expand All @@ -119,7 +157,7 @@ class MongoConnection {

try {
// Switch to admin database to list all databases
final adminUri = buildConnectionUri().replaceAll(RegExp(r'/[^/?]*(\?|$)'), '/admin\$1');
final adminUri = buildUriForDatabase('admin');
final adminDb = await Db.create(adminUri);
await adminDb.open();
try {
Expand Down Expand Up @@ -147,7 +185,7 @@ class MongoConnection {

try {
// Create a new Db connection to the specified database
final dbUri = buildConnectionUri().replaceAll(RegExp(r'/[^/?]*(\?|$)'), '/$databaseName\$1');
final dbUri = buildUriForDatabase(databaseName);
final db = await Db.create(dbUri);
await db.open();
try {
Expand Down
168 changes: 143 additions & 25 deletions lib/core/database/mongodb_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,23 @@ class MongoService {

final Map<int, MongoConnection> _connections = {};

/// Creates a MongoDB connection from ConnectionRow.
/// Creates (or replaces) a [MongoConnection] for the given [ConnectionRow].
/// If a connection with the same ID already exists it is disconnected first.
MongoConnection createConnection(ConnectionRow row) {
if (row.type != 'mongodb') {
throw ArgumentError('Connection type must be mongodb');
}

final id = row.id ?? 0;

// Disconnect previous connection for this ID, if any.
final existing = _connections[id];
if (existing != null) {
existing.disconnect(); // fire-and-forget; disconnect is safe
}

final connection = MongoConnection(
id: row.id ?? 0,
id: id,
name: row.name,
host: row.host ?? 'localhost',
port: row.port ?? 27017,
Expand Down Expand Up @@ -66,16 +75,10 @@ class MongoService {
throw StateError('Not connected to MongoDB');
}

// Create a new Db connection to the specified database
final dbUri = connection.buildConnectionUri().replaceAll(RegExp(r'/[^/?]*(\?|$)'), '/$database\$1');
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 @@ -93,14 +96,10 @@ class MongoService {
throw StateError('Not connected to MongoDB');
}

// Create a new Db connection to the specified database
final dbUri = connection.buildConnectionUri().replaceAll(RegExp(r'/[^/?]*(\?|$)'), '/$database\$1');
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 @@ -116,9 +115,7 @@ class MongoService {
count++;
}
return results;
} finally {
await db.close();
}
});
}

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

// Create a new Db connection to the specified database
final dbUri = connection.buildConnectionUri().replaceAll(RegExp(r'/[^/?]*(\?|$)'), '/$database\$1');
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);
});
}
}
38 changes: 32 additions & 6 deletions lib/features/connections/connections_panel.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'package:flutter/material.dart' as material show Padding, Container, BoxDecoration, Border, BorderSide, InkWell, Icon, Icons, IconData, Image, EdgeInsets, BorderRadius, CrossAxisAlignment, MainAxisSize, MouseRegion, SystemMouseCursors, DefaultTextStyle, TextStyle, CustomScrollView, SliverToBoxAdapter, SliverFillRemaining, SliverPadding, GestureDetector, HitTestBehavior, SizedBox, Column, AnimatedRotation, Row, BoxFit;
import 'package:flutter/material.dart' as material show Padding, Container, BoxDecoration, Border, BorderSide, InkWell, Icon, Icons, IconData, Image, EdgeInsets, BorderRadius, CrossAxisAlignment, MainAxisSize, MouseRegion, SystemMouseCursors, DefaultTextStyle, TextStyle, CustomScrollView, SliverToBoxAdapter, SliverFillRemaining, SliverPadding, GestureDetector, HitTestBehavior, SizedBox, Column, AnimatedRotation, Row, BoxFit, Text, TextOverflow, Expanded;
import 'package:querya_desktop/core/storage/folders_storage.dart';
import 'package:querya_desktop/core/storage/local_db.dart';
import 'package:querya_desktop/shared/widgets/widgets.dart';
Expand Down Expand Up @@ -337,14 +337,30 @@ class _ConnectionTile extends StatelessWidget {
children: [
iconWidget,
const Gap(8),
Expanded(
child: Column(
material.Expanded(
child: material.Column(
crossAxisAlignment: material.CrossAxisAlignment.start,
mainAxisSize: material.MainAxisSize.min,
children: [
Text(connection.name).small(),
material.Text(
connection.name,
overflow: material.TextOverflow.ellipsis,
maxLines: 1,
style: material.TextStyle(
fontSize: 13,
color: theme.colorScheme.foreground,
),
),
if (connection.host != null)
Text('${connection.host}:${connection.port ?? ''}').muted().xSmall(),
material.Text(
'${connection.host}:${connection.port ?? ''}',
overflow: material.TextOverflow.ellipsis,
maxLines: 1,
style: material.TextStyle(
fontSize: 11,
color: theme.colorScheme.mutedForeground,
),
),
],
),
),
Expand Down Expand Up @@ -424,7 +440,17 @@ class _FolderTile extends StatelessWidget {
const Gap(2),
material.Icon(material.Icons.folder_rounded, size: 18, color: theme.colorScheme.primary),
const Gap(8),
Expanded(child: Text(name).small()),
material.Expanded(
child: material.Text(
name,
overflow: material.TextOverflow.ellipsis,
maxLines: 1,
style: material.TextStyle(
fontSize: 13,
color: theme.colorScheme.foreground,
),
),
),
],
),
),
Expand Down
Loading