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
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}"#)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions TablePro/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down Expand Up @@ -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
},
Expand Down
Loading