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 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": "…" } }`.
- **Postgres open-transaction probe (#787)** — `BEGIN` + `SELECT` does not assign an XID, so `pg_current_xact_id_if_assigned` stayed NULL (SQL-tab badge off; autocommit-off prepended a second `BEGIN`). The session tracks BEGIN/COMMIT/ROLLBACK and otherwise probes `pg_stat_activity.xact_start` (PG 9+). Implicit BEGIN when autocommit is off stays a separate `execute`.
Expand Down
16 changes: 15 additions & 1 deletion lib/core/database/mongodb_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ class MongoService {
});
}

/// Updates a single document matched by [filter].
/// Updates a single document matched by [filter] (`$set` / `$unset` / …).
Future<void> updateDocument(
MongoConnection connection,
String database,
Expand All @@ -245,6 +245,20 @@ class MongoService {
});
}

/// Replaces a single document matched by [filter] (full-document Save).
Future<void> replaceDocument(
MongoConnection connection,
String database,
String collection,
Map<String, dynamic> filter,
Map<String, dynamic> replacement,
) async {
return _withDb(connection, database, (db) async {
final coll = db.collection(collection);
await coll.replaceOne(filter, replacement);
});
}

/// Deletes a single document matched by [filter].
Future<void> deleteDocument(
MongoConnection connection,
Expand Down
14 changes: 8 additions & 6 deletions lib/features/mongodb/mongo_document_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,12 @@ class _MongoDocumentEditorState extends material.State<MongoDocumentEditor> {
return;
}

// Remove _id from the update payload (can't change _id). Filter uses the
// original BSON _id, not a stringified copy from the editor.
final updateDoc = Map<String, dynamic>.from(parsed);
updateDoc.remove('_id');
// replaceOne drops keys the user deleted. `_id` stays the original BSON
// value even if the editor JSON changed it.
final replacement = mongoFullDocumentReplacement(
parsed: parsed,
originalId: id,
);

setState(() {
_saving = true;
Expand All @@ -162,12 +164,12 @@ class _MongoDocumentEditorState extends material.State<MongoDocumentEditor> {
});

try {
await MongoService.instance.updateDocument(
await MongoService.instance.replaceDocument(
widget.connection,
widget.database,
widget.collection,
{'_id': id},
{r'$set': updateDoc},
replacement,
);
if (!mounted) return;
setState(() {
Expand Down
14 changes: 14 additions & 0 deletions lib/features/mongodb/mongo_ejson.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,17 @@ Map<String, dynamic> mongoFilterFromJson(String text) {
bool mongoFilterNeedsObjectIdHint(Map<String, dynamic> filter) {
return filter['_id'] is String;
}

/// Full-document Save payload for [DbCollection.replaceOne].
///
/// Parsed JSON minus any `_id` the user typed, then `_id` locked to
/// [originalId]. Keys absent from [parsed] (including nested ones) are not in
/// the replacement, so they are dropped — unlike `$set`, which only merges.
Map<String, dynamic> mongoFullDocumentReplacement({
required Map<String, dynamic> parsed,
required Object originalId,
}) {
final out = Map<String, dynamic>.from(parsed);
out['_id'] = originalId;
return out;
}
35 changes: 35 additions & 0 deletions test/features/mongodb/mongo_ejson_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,39 @@ void main() {
expect((filter['age'] as Map)[r'$gt'], 5);
});
});

group('mongoFullDocumentReplacement', () {
test('start {a:1,b:2}, save {a:1} drops b and keeps original _id', () {
final id = ObjectId.fromHexString('507f1f77bcf86cd799439011');
final replacement = mongoFullDocumentReplacement(
parsed: <String, dynamic>{'a': 1},
originalId: id,
);
expect(replacement.containsKey('b'), isFalse);
expect(replacement['a'], 1);
expect(replacement['_id'], same(id));
});

test('nested keys deleted in JSON are absent from the replacement', () {
final id = ObjectId.fromHexString('507f191e810c19729de860ea');
final replacement = mongoFullDocumentReplacement(
parsed: <String, dynamic>{
'nested': <String, dynamic>{'x': 1},
},
originalId: id,
);
expect(replacement['nested'], <String, dynamic>{'x': 1});
expect((replacement['nested'] as Map).containsKey('y'), isFalse);
});

test('edited _id in JSON is overwritten with the original BSON id', () {
final id = ObjectId.fromHexString('507f1f77bcf86cd799439011');
final replacement = mongoFullDocumentReplacement(
parsed: <String, dynamic>{'_id': 'tampered', 'a': 1},
originalId: id,
);
expect(replacement['_id'], same(id));
expect(replacement['a'], 1);
});
});
}
Loading