From 72f94dd96a5fda9079dc5348dcd22ae7ccfc318f Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Fri, 25 Sep 2026 13:47:35 +0300 Subject: [PATCH] fix(mongo): refuse inspector $set of dotted top-level field names (Closes #872) The field inspector Save issued $set: { field: value } with the card's key verbatim. MongoDB reads "a.b" as a nested path, so editing a top-level key literally named "a.b" rewrote (or created) a: { b: ... } while the UI still showed the dotted name. $set has no escape for dots, so mongoAssertFieldEditable now rejects such names with an explicit error pointing to the whole-document JSON editor, which uses replaceOne and keeps literal keys. The field stays viewable in the inspector; only Save is blocked. --- lib/features/mongodb/mongo_field_codec.dart | 12 ++++++++ .../mongodb/mongo_field_codec_test.dart | 28 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/lib/features/mongodb/mongo_field_codec.dart b/lib/features/mongodb/mongo_field_codec.dart index 401cb3b..7691761 100644 --- a/lib/features/mongodb/mongo_field_codec.dart +++ b/lib/features/mongodb/mongo_field_codec.dart @@ -6,6 +6,11 @@ import 'package:querya_desktop/features/mongodb/mongo_ejson.dart'; /// Mongo forbids `$set` of `_id` on an existing document. bool mongoFieldIsReadOnly(String field) => field == '_id'; +/// True when `$set` would treat [field] as a nested path (`a.b`), not as a +/// literal top-level key. `$set` has no escape for dots, so such a field cannot +/// be updated on its own without rewriting a different subtree. +bool mongoFieldNameIsDottedPath(String field) => field.contains('.'); + /// Throws if [field] cannot be updated with `$set` on an existing document. void mongoAssertFieldEditable(String field) { if (mongoFieldIsReadOnly(field)) { @@ -13,6 +18,13 @@ void mongoAssertFieldEditable(String field) { 'MongoDB forbids \$set of _id on an existing document', ); } + if (mongoFieldNameIsDottedPath(field)) { + throw StateError( + 'Field "$field" contains a dot, so a single-field update would be ' + 'treated as a nested path and change a different value. ' + 'Edit the whole document in the JSON editor instead.', + ); + } } /// Display string for a MongoDB document field in the Cell Inspector. diff --git a/test/features/mongodb/mongo_field_codec_test.dart b/test/features/mongodb/mongo_field_codec_test.dart index 3fd2114..c0e1d27 100644 --- a/test/features/mongodb/mongo_field_codec_test.dart +++ b/test/features/mongodb/mongo_field_codec_test.dart @@ -112,4 +112,32 @@ void main() { expect(() => mongoAssertFieldEditable('name'), returnsNormally); }); }); + group('dotted top-level field names', () { + test('are detected as nested paths', () { + expect(mongoFieldNameIsDottedPath('a.b'), isTrue); + expect(mongoFieldNameIsDottedPath('.a'), isTrue); + expect(mongoFieldNameIsDottedPath('name'), isFalse); + expect(mongoFieldNameIsDottedPath('a_b'), isFalse); + }); + + test('single-field save is refused with an explicit error', () { + expect( + () => mongoAssertFieldEditable('a.b'), + throwsA( + isA().having( + (e) => e.message, + 'message', + allOf(contains('"a.b"'), contains('JSON editor')), + ), + ), + ); + expect(() => mongoAssertFieldEditable('ab'), returnsNormally); + }); + + test('dotted key is still readable in the inspector', () { + // Only Save is blocked; the dotted field itself is not read-only. + expect(mongoFieldIsReadOnly('a.b'), isFalse); + expect(mongoFieldToDisplay(1), '1'); + }); + }); }