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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **Mongo 0-match write (#776)** — `updateOne` / `replaceOne` / `deleteOne` throw when `nMatched` / `nRemoved` is 0 (wrong `_id` type, deleted doc). Inspector and JSON editor surface Save Failed instead of a success toast. An identical `$set` (`nModified == 0`) still counts as a match.
- **Mongo full-document Save (#778)** — JSON editor Save uses `replaceOne` (whole document, `_id` locked) instead of `$set`, so fields deleted in JSON — including nested keys — are removed on the server.
- **Mongo document editor Back (#782)** — Dirty JSON in `MongoDocumentEditor` is registered with `UnsavedWorkRegistry`. Breadcrumb Back, Home, Close, and tree navigation confirm before discarding; Cancel keeps the editor.
- **Mongo JSON filter ObjectId / DateTime (#783)** — Document list filter parses Extended JSON (`$oid`, `$date`) and wraps a 24-character hex `_id` as `ObjectId`. A leftover string `_id` with zero matches shows how to write `{ "_id": { "$oid": "…" } }`.
Expand Down
46 changes: 43 additions & 3 deletions lib/core/database/mongodb_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,31 @@ import 'package:mongo_dart/mongo_dart.dart';
import '../storage/local_db.dart';
import 'mongodb_connection.dart';

/// Throws if a single-document write matched nothing (wrong `_id` type,
/// concurrent delete). Same class of lie as 0-row SQL DML.
///
/// [nModified] is ignored: an identical `$set` still matched the document.
void expectMongoDocumentMatched(int matched, {required String operation}) {
if (matched >= 1) return;
throw StateError(
'$operation failed: matched 0 documents. '
'The document may have been deleted or the _id type does not match.',
);
}

void _throwIfMongoWriteFailed(
WriteResult result, {
required String operation,
required int matched,
}) {
if (result.hasWriteErrors) {
throw StateError(
'$operation failed: ${result.writeError?.errmsg ?? 'write error'}',
);
}
expectMongoDocumentMatched(matched, operation: operation);
}

/// Service for managing MongoDB connections.
class MongoService {
MongoService._();
Expand Down Expand Up @@ -241,7 +266,12 @@ class MongoService {
) async {
return _withDb(connection, database, (db) async {
final coll = db.collection(collection);
await coll.updateOne(filter, update);
final result = await coll.updateOne(filter, update);
_throwIfMongoWriteFailed(
result,
operation: 'updateOne',
matched: result.nMatched,
);
});
}

Expand All @@ -255,7 +285,12 @@ class MongoService {
) async {
return _withDb(connection, database, (db) async {
final coll = db.collection(collection);
await coll.replaceOne(filter, replacement);
final result = await coll.replaceOne(filter, replacement);
_throwIfMongoWriteFailed(
result,
operation: 'replaceOne',
matched: result.nMatched,
);
});
}

Expand All @@ -268,7 +303,12 @@ class MongoService {
) async {
return _withDb(connection, database, (db) async {
final coll = db.collection(collection);
await coll.deleteOne(filter);
final result = await coll.deleteOne(filter);
_throwIfMongoWriteFailed(
result,
operation: 'deleteOne',
matched: result.nRemoved,
);
});
}

Expand Down
25 changes: 25 additions & 0 deletions test/core/database/mongo_service_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,31 @@ import 'package:querya_desktop/core/database/mongodb_service.dart';
import 'package:querya_desktop/core/storage/local_db.dart';

void main() {
group('expectMongoDocumentMatched', () {
test('allows 1+ matched documents', () {
expectMongoDocumentMatched(1, operation: 'updateOne');
expectMongoDocumentMatched(2, operation: 'replaceOne');
});

test('throws on 0 matches so Save is a failure', () {
expect(
() => expectMongoDocumentMatched(0, operation: 'updateOne'),
throwsA(
isA<StateError>().having(
(e) => e.message,
'message',
contains('matched 0 documents'),
),
),
);
});

test('nModified 0 is not a failure when the filter matched', () {
// Identical $set: nMatched=1, nModified=0 — still a successful save.
expectMongoDocumentMatched(1, operation: 'updateOne');
});
});

group('MongoService.createConnection', () {
test('creates MongoConnection from ConnectionRow', () {
const row = ConnectionRow(
Expand Down
Loading