From d11c0a7efca721a531c5991f433d022eefe13157 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 13 Oct 2026 16:12:52 +0700 Subject: [PATCH] fix(plugin-firebase): show runnable examples instead of {...} Signed-off-by: Ngo Quoc Dat --- .../Firestore/FirestoreQuery.swift | 15 +++++++-------- .../Wire/FirebaseStatement.swift | 12 +++++++++--- .../FirebaseSettingsAndStatementTests.swift | 11 +++++++++++ .../FirestoreCellAndQueryTests.swift | 13 +++++++++++++ TablePro/Resources/Localizable.xcstrings | 4 ++-- 5 files changed, 42 insertions(+), 13 deletions(-) diff --git a/Packages/TableProCore/Sources/TableProFirebaseCore/Firestore/FirestoreQuery.swift b/Packages/TableProCore/Sources/TableProFirebaseCore/Firestore/FirestoreQuery.swift index 661a9db8a4..8760f9b3f7 100644 --- a/Packages/TableProCore/Sources/TableProFirebaseCore/Firestore/FirestoreQuery.swift +++ b/Packages/TableProCore/Sources/TableProFirebaseCore/Firestore/FirestoreQuery.swift @@ -130,6 +130,8 @@ public enum FirestoreQueryBuilder { localized: "Firestore cannot filter with %@. It supports =, !=, <, <=, >, >=, IN, NOT IN, BETWEEN, STARTS WITH, IS NULL, and CONTAINS on an array field." ) public static let rawFilterColumn = "__RAW__" + /// Kept out of the localized key, so a translator never edits JSON and a test can run it. + static let rawFilterExample = #"{"unaryFilter": {"field": {"fieldPath": "email"}, "op": "IS_NULL"}}"# /// `typeOf` answers the type a field was last seen holding, so `zip = 30` compares against the /// string "30" when zip is a string field. @@ -347,15 +349,12 @@ public enum FirestoreQueryBuilder { /// The raw filter row takes a REST `Filter` object as written in the Firestore API reference, /// which is the only way to reach a nested field or a composite the filter bar cannot draw. private static func rawFilter(_ text: String) throws -> FirebaseJSON { - let json: FirebaseJSON - do { - json = try FirebaseJSON.parse(text) - } catch { - throw FirebaseError.configuration(String(localized: "A raw Firestore filter is a JSON Filter object, such as {\"fieldFilter\": {...}}.")) - } - guard let members = json.objectValue, members.count == 1, + let refusal = FirebaseError.configuration( + String(format: String(localized: "A raw Firestore filter is a JSON Filter object, such as %@."), rawFilterExample) + ) + guard let json = try? FirebaseJSON.parse(text), let members = json.objectValue, members.count == 1, let key = members.keys.first, ["fieldFilter", "unaryFilter", "compositeFilter"].contains(key) else { - throw FirebaseError.configuration(String(localized: "A raw Firestore filter is a JSON Filter object, such as {\"fieldFilter\": {...}}.")) + throw refusal } return json } diff --git a/Packages/TableProCore/Sources/TableProFirebaseCore/Wire/FirebaseStatement.swift b/Packages/TableProCore/Sources/TableProFirebaseCore/Wire/FirebaseStatement.swift index 7226e3f290..2309df4730 100644 --- a/Packages/TableProCore/Sources/TableProFirebaseCore/Wire/FirebaseStatement.swift +++ b/Packages/TableProCore/Sources/TableProFirebaseCore/Wire/FirebaseStatement.swift @@ -112,9 +112,15 @@ public struct FirebaseRequest: Sendable, Equatable { return line } - static let usage = String( - localized: "Enter a request such as GET users/alice, QUERY {\"structuredQuery\": {...}}, PATCH users/alice {\"age\": 31} or DELETE users/alice." - ) + /// Kept out of the localized key, so a translator never edits a request and a test can run each one. + static let usageExamples = [ + "GET users/alice", + #"QUERY {"structuredQuery": {"from": [{"collectionId": "users"}]}}"#, + #"PATCH users/alice {"age": 31}"#, + "DELETE users/alice" + ] + + static let usage = String(format: String(localized: "Enter a request such as %@, %@, %@ or %@."), arguments: usageExamples) private static let escapedScalars = CharacterSet.whitespacesAndNewlines .union(.controlCharacters) diff --git a/Packages/TableProCore/Tests/TableProFirebaseCoreTests/FirebaseSettingsAndStatementTests.swift b/Packages/TableProCore/Tests/TableProFirebaseCoreTests/FirebaseSettingsAndStatementTests.swift index 09b115658c..7c9fde705c 100644 --- a/Packages/TableProCore/Tests/TableProFirebaseCoreTests/FirebaseSettingsAndStatementTests.swift +++ b/Packages/TableProCore/Tests/TableProFirebaseCoreTests/FirebaseSettingsAndStatementTests.swift @@ -89,6 +89,17 @@ struct FirebaseStatementTests { #expect(throws: FirebaseError.self) { try FirebaseRequest.parse("QUERY users") } } + @Test("Every request the syntax message suggests parses, and its QUERY carries a structuredQuery") + func usageExamples() throws { + for example in FirebaseRequest.usageExamples { + #expect(FirebaseRequest.usage.contains(example)) + let request = try FirebaseRequest.parse(example) + if request.method == .query { + #expect(try FirebaseJSON.parse(request.body ?? "")["structuredQuery"] != nil) + } + } + } + @Test("The request text reads back as the same request") func text() throws { let request = FirebaseRequest(method: .patch, path: "users/alice", query: [URLQueryItem(name: "updateMask", value: "age")], body: #"{"age":31}"#) diff --git a/Packages/TableProCore/Tests/TableProFirebaseCoreTests/FirestoreCellAndQueryTests.swift b/Packages/TableProCore/Tests/TableProFirebaseCoreTests/FirestoreCellAndQueryTests.swift index dd4b2c27d5..ae33dccb1b 100644 --- a/Packages/TableProCore/Tests/TableProFirebaseCoreTests/FirestoreCellAndQueryTests.swift +++ b/Packages/TableProCore/Tests/TableProFirebaseCoreTests/FirestoreCellAndQueryTests.swift @@ -148,6 +148,19 @@ struct FirestoreQueryBuilderTests { } } + @Test("A raw filter that is not a Filter object is refused with an example the raw filter row accepts") + func rawFilterExample() throws { + let raw = FirestoreQueryBuilder.rawFilterColumn + let example = FirestoreQueryBuilder.rawFilterExample + #expect(try filterJSON(FirebaseFilterSpec(column: raw, op: "", value: example)).contains("IS_NULL")) + do { + _ = try filterJSON(FirebaseFilterSpec(column: raw, op: "", value: #"{"age": 3}"#)) + Issue.record("A JSON object that is not a Filter was accepted") + } catch let FirebaseError.configuration(message) { + #expect(message.contains(example)) + } + } + @Test("Counting drops the page but keeps the order, which leaves out documents without the field") func count() throws { let spec = FirebaseBrowseSpec(table: "*/orders", sorts: [FirebaseSortSpec(column: "a", ascending: true)], limit: 10, offset: 5) diff --git a/TablePro/Resources/Localizable.xcstrings b/TablePro/Resources/Localizable.xcstrings index f65b8498fd..514da3e4b8 100644 --- a/TablePro/Resources/Localizable.xcstrings +++ b/TablePro/Resources/Localizable.xcstrings @@ -15929,7 +15929,7 @@ } } }, - "A raw Firestore filter is a JSON Filter object, such as {\"fieldFilter\": {...}}." : { + "A raw Firestore filter is a JSON Filter object, such as %@." : { "extractionState" : "manual", "generatesSymbol" : false }, @@ -70003,7 +70003,7 @@ } } }, - "Enter a request such as GET users/alice, QUERY {\"structuredQuery\": {...}}, PATCH users/alice {\"age\": 31} or DELETE users/alice." : { + "Enter a request such as %@, %@, %@ or %@." : { "extractionState" : "manual", "generatesSymbol" : false },