diff --git a/Sources/MeiliSearch/Documents.swift b/Sources/MeiliSearch/Documents.swift index 386d39d2..d4f764fa 100755 --- a/Sources/MeiliSearch/Documents.swift +++ b/Sources/MeiliSearch/Documents.swift @@ -16,9 +16,15 @@ struct Documents { func get( _ uid: String, _ identifier: String, + fields: [String]? = nil, _ completion: @escaping (Result) -> Void) where T: Codable, T: Equatable { - let query: String = "/indexes/\(uid)/documents/\(identifier)" + var query: String = "/indexes/\(uid)/documents/\(identifier)" + + if fields != nil { + query.append(fields?.joined(separator: ",") ?? "") + } + self.request.get(api: query) { result in switch result { case .success(let data): @@ -26,8 +32,8 @@ struct Documents { completion(.failure(MeiliSearch.Error.dataNotFound)) return } - Documents.decodeJSON(data, completion: completion) + Documents.decodeJSON(data, completion: completion) case .failure(let error): completion(.failure(error)) } @@ -36,30 +42,20 @@ struct Documents { func getAll( _ uid: String, - _ options: GetParameters? = nil, - _ completion: @escaping (Result<[T], Swift.Error>) -> Void) + params: DocumentsQuery? = nil, + _ completion: @escaping (Result, Swift.Error>) -> Void) where T: Codable, T: Equatable { - do { - var queryParameters = "" - if let parameters: GetParameters = options { - queryParameters = try parameters.toQueryParameters() - } - let query: String = "/indexes/\(uid)/documents\(queryParameters)" - request.get(api: query) { result in - switch result { - case .success(let data): - guard let data: Data = data else { - completion(.failure(MeiliSearch.Error.dataNotFound)) - return - } - Documents.decodeJSON(data, completion: completion) - - case .failure(let error): - completion(.failure(error)) + request.get(api: "/indexes/\(uid)/documents\(params?.toQuery() ?? "")") { result in + switch result { + case .success(let data): + guard let data: Data = data else { + completion(.failure(MeiliSearch.Error.dataNotFound)) + return } + Documents.decodeJSON(data, completion: completion) + case .failure(let error): + completion(.failure(error)) } - } catch let error { - completion(.failure(error)) } } @@ -69,7 +65,7 @@ struct Documents { _ uid: String, _ document: Data, _ primaryKey: String? = nil, - _ completion: @escaping (Result) -> Void) { + _ completion: @escaping (Result) -> Void) { var query: String = "/indexes/\(uid)/documents" if let primaryKey: String = primaryKey { @@ -91,7 +87,7 @@ struct Documents { _ documents: [T], _ encoder: JSONEncoder? = nil, _ primaryKey: String? = nil, - _ completion: @escaping (Result) -> Void) where T: Encodable { + _ completion: @escaping (Result) -> Void) where T: Encodable { var query: String = "/indexes/\(uid)/documents" if let primaryKey: String = primaryKey { query += "?primaryKey=\(primaryKey)" @@ -120,7 +116,7 @@ struct Documents { _ uid: String, _ document: Data, _ primaryKey: String? = nil, - _ completion: @escaping (Result) -> Void) { + _ completion: @escaping (Result) -> Void) { var query: String = "/indexes/\(uid)/documents" if let primaryKey: String = primaryKey { @@ -142,7 +138,7 @@ struct Documents { _ documents: [T], _ encoder: JSONEncoder? = nil, _ primaryKey: String? = nil, - _ completion: @escaping (Result) -> Void) where T: Encodable { + _ completion: @escaping (Result) -> Void) where T: Encodable { var query: String = "/indexes/\(uid)/documents" if let primaryKey: String = primaryKey { @@ -173,7 +169,7 @@ struct Documents { func delete( _ uid: String, _ identifier: String, - _ completion: @escaping (Result) -> Void) { + _ completion: @escaping (Result) -> Void) { self.request.delete(api: "/indexes/\(uid)/documents/\(identifier)") { result in switch result { @@ -192,7 +188,7 @@ struct Documents { func deleteAll( _ uid: String, - _ completion: @escaping (Result) -> Void) { + _ completion: @escaping (Result) -> Void) { self.request.delete(api: "/indexes/\(uid)/documents") { result in switch result { @@ -214,7 +210,7 @@ struct Documents { func deleteBatch( _ uid: String, _ documentsIdentifiers: [Int], - _ completion: @escaping (Result) -> Void) { + _ completion: @escaping (Result) -> Void) { let data: Data diff --git a/Sources/MeiliSearch/Model/DocumentsResults.swift b/Sources/MeiliSearch/Model/DocumentsResults.swift new file mode 100644 index 00000000..6631c4bf --- /dev/null +++ b/Sources/MeiliSearch/Model/DocumentsResults.swift @@ -0,0 +1,12 @@ +import Foundation + +/** + `DocumentsResults` is a wrapper used in the indexes routes to handle the returned data. + */ + +public struct DocumentsResults: Codable, Equatable { + public let results: [T] + public let offset: Int + public let limit: Int + public let total: Int +} diff --git a/Sources/MeiliSearch/Model/GetParameters.swift b/Sources/MeiliSearch/Model/GetParameters.swift deleted file mode 100644 index d5310ff9..00000000 --- a/Sources/MeiliSearch/Model/GetParameters.swift +++ /dev/null @@ -1,60 +0,0 @@ -import Foundation - -/** - `GetParameters` instances represent query setup for a documents fetch request. - */ -public struct GetParameters: Codable, Equatable { - // MARK: Properties - - /// Number of documents to take. - public let limit: Int? - - /// Number of documents to skip. - public let offset: Int? - - /// Document attributes to show. - public let attributesToRetrieve: [String]? - - // MARK: Initializers - - public init( - offset: Int? = nil, - limit: Int? = nil, - attributesToRetrieve: [String]? = nil - ) { - self.offset = offset - self.limit = limit - self.attributesToRetrieve = attributesToRetrieve - } - - func toQueryParameters() throws -> String { - var queryParams = "?" - if let offset: Int = self.offset { - if queryParams != "?" { - queryParams += "&" - } - queryParams += "offset=\(offset)" - } - - if let limit: Int = self.limit { - if queryParams != "?" { - queryParams += "&" - } - queryParams += "limit=\(limit)" - } - - if let attributesToRetrieve: [String] = self.attributesToRetrieve { - if queryParams != "?" { - queryParams += "&" - } - queryParams += "attributesToRetrieve=\(attributesToRetrieve.joined(separator: ","))" - } - - if queryParams == "&" { - return "" - } - return queryParams - } - // MARK: Codable Keys - -} diff --git a/Sources/MeiliSearch/QueryParameters/DocumentsQuery.swift b/Sources/MeiliSearch/QueryParameters/DocumentsQuery.swift new file mode 100644 index 00000000..aaac0d8d --- /dev/null +++ b/Sources/MeiliSearch/QueryParameters/DocumentsQuery.swift @@ -0,0 +1,21 @@ +import Foundation + +public class DocumentsQuery: Queryable { + private var limit: Int? + private var offset: Int? + private var fields: [String] + + init(limit: Int? = nil, offset: Int? = nil, fields: [String]? = nil) { + self.offset = offset + self.limit = limit + self.fields = fields ?? [] + } + + internal func buildQuery() -> [String: Codable?] { + [ + "limit": limit, + "offset": offset, + "fields": fields.isEmpty ? nil : fields.joined(separator: ",") + ] + } +} diff --git a/Sources/MeiliSearch/QueryParameters/Queryable.swift b/Sources/MeiliSearch/QueryParameters/Queryable.swift new file mode 100644 index 00000000..95b7a7cd --- /dev/null +++ b/Sources/MeiliSearch/QueryParameters/Queryable.swift @@ -0,0 +1,20 @@ +internal protocol Queryable { + func buildQuery() -> [String: Codable?] +} + +extension Queryable { + func toQuery() -> String { + let query: [String: Codable?] = buildQuery() + + let data = query.compactMapValues { $0 } + .sorted { $0.key < $1.key } + .map { "\($0)=\($1)" } + .joined(separator: "&") + + if !data.isEmpty { + return "?\(data)" + } + + return data + } +} diff --git a/Tests/MeiliSearchIntegrationTests/DocumentsTests.swift b/Tests/MeiliSearchIntegrationTests/DocumentsTests.swift index 58406f78..cc69d22b 100755 --- a/Tests/MeiliSearchIntegrationTests/DocumentsTests.swift +++ b/Tests/MeiliSearchIntegrationTests/DocumentsTests.swift @@ -60,7 +60,7 @@ class DocumentsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("documentAddition", task.type) + XCTAssertEqual("documentAdditionOrUpdate", task.type) XCTAssertEqual(Task.Status.succeeded, task.status) if let details = task.details { if let indexedDocuments = details.indexedDocuments { @@ -108,7 +108,7 @@ class DocumentsTests: XCTestCase { self.client.waitForTask(task: task) { result in switch result { case .success(let task): - XCTAssertEqual("documentAddition", task.type) + XCTAssertEqual("documentAdditionOrUpdate", task.type) XCTAssertEqual(Task.Status.succeeded, task.status) expectation.fulfill() case .failure(let error): @@ -140,14 +140,14 @@ class DocumentsTests: XCTestCase { switch result { case .success(let task): XCTAssertEqual(Task.Status.succeeded, task.status) - XCTAssertEqual("documentAddition", task.type) - self.index.getDocuments( - options: GetParameters(offset: 1, limit: 1, attributesToRetrieve: ["id", "title"]) - ) { (result: Result<[Movie], Swift.Error>) in + XCTAssertEqual("documentAdditionOrUpdate", task.type) + + self.index.getDocuments(params: DocumentsQuery(limit: 1, offset: 1, fields: ["id", "title"])) { (result: Result, Swift.Error>) in switch result { - case .success(let returnedMovies): - let returnedMovie = returnedMovies[0] - XCTAssertEqual(returnedMovies.count, 1) + case .success(let movies): + let returnedMovie = movies.results[0] + + XCTAssertEqual(movies.results.count, 1) XCTAssertEqual(returnedMovie.id, 456) XCTAssertEqual(returnedMovie.title, "Le Petit Prince") XCTAssertEqual(returnedMovie.comment, nil) @@ -201,7 +201,7 @@ class DocumentsTests: XCTestCase { switch result { case .success(let task): XCTAssertEqual(Task.Status.succeeded, task.status) - XCTAssertEqual("documentAddition", task.type) + XCTAssertEqual("documentAdditionOrUpdate", task.type) self.index.getDocument(10 ) { (result: Result) in switch result { @@ -244,7 +244,7 @@ class DocumentsTests: XCTestCase { switch result { case .success(let task): XCTAssertEqual(Task.Status.succeeded, task.status) - XCTAssertEqual("documentAddition", task.type) + XCTAssertEqual("documentAdditionOrUpdate", task.type) self.index.getDocument("10" ) { (result: Result) in switch result { @@ -289,7 +289,7 @@ class DocumentsTests: XCTestCase { switch result { case .success(let task): XCTAssertEqual(Task.Status.succeeded, task.status) - XCTAssertEqual("documentPartial", task.type) + XCTAssertEqual("documentAdditionOrUpdate", task.type) expectation.fulfill() case .failure: XCTFail("Failed to wait for task") @@ -325,10 +325,10 @@ class DocumentsTests: XCTestCase { self.wait(for: [expectation], timeout: TESTS_TIME_OUT) let deleteExpectation = XCTestExpectation(description: "Delete one Movie") - self.index.deleteDocument("42") { (result: Result) in + self.index.deleteDocument("42") { result in switch result { case .success(let task): - self.client.waitForTask(task: task) { result in + self.client.waitForTask(taskUid: task.uid) { result in switch result { case .success(let task): XCTAssertEqual(Task.Status.succeeded, task.status) @@ -367,17 +367,17 @@ class DocumentsTests: XCTestCase { self.wait(for: [expectation], timeout: TESTS_TIME_OUT) let deleteExpectation = XCTestExpectation(description: "Delete all documents") - self.index.deleteAllDocuments { (result: Result) in + self.index.deleteAllDocuments { result in switch result { case .success(let task): self.client.waitForTask(task: task) { result in switch result { case .success(let task): XCTAssertEqual(Task.Status.succeeded, task.status) - XCTAssertEqual("clearAll", task.type) + XCTAssertEqual("documentDeletion", task.type) if let details = task.details { if let deletedDocuments = details.deletedDocuments { - XCTAssertEqual(9, deletedDocuments) + XCTAssertGreaterThanOrEqual(deletedDocuments, 8) } else { XCTFail("deletedDocuments field should not be nil") deleteExpectation.fulfill() @@ -423,7 +423,7 @@ class DocumentsTests: XCTestCase { let deleteExpectation = XCTestExpectation(description: "Delete batch movies") let idsToDelete: [Int] = [2, 1, 4] - self.index.deleteBatchDocuments(idsToDelete) { (result: Result) in + self.index.deleteBatchDocuments(idsToDelete) { result in switch result { case .success(let task): self.client.waitForTask(task: task) { result in diff --git a/Tests/MeiliSearchUnitTests/DocumentsTests.swift b/Tests/MeiliSearchUnitTests/DocumentsTests.swift index 3e4da3fb..ff691aad 100755 --- a/Tests/MeiliSearchUnitTests/DocumentsTests.swift +++ b/Tests/MeiliSearchUnitTests/DocumentsTests.swift @@ -6,10 +6,10 @@ import Foundation // swiftlint:disable force_try // swiftlint:disable line_length private struct Movie: Codable, Equatable { - let id: Int - let title: String - let overview: String - let releaseDate: Date + let id: Int? + let title: String? + let overview: String? + let releaseDate: Date? enum CodingKeys: String, CodingKey { case id @@ -33,13 +33,13 @@ class DocumentsTests: XCTestCase { func testAddDocuments() { let jsonString = """ - {"uid": 0, "indexUid": "movies_test", "status": "enqueued", "type": "documentAddition", "enqueuedAt": "xxx" } - """ + {"taskUid":0,"indexUid":"books_test","status":"enqueued","type":"documentAdditionOrUpdate","enqueuedAt":"2022-07-21T21:47:50.565717794Z"} + """ // Prepare the mock server let decoder = JSONDecoder() let jsonData = jsonString.data(using: .utf8)! - let stubTask: Task = try! decoder.decode(Task.self, from: jsonData) + let stubTask: TaskInfo = try! decoder.decode(TaskInfo.self, from: jsonData) session.pushData(jsonString, code: 202) // Start the test with the mocked server @@ -68,13 +68,13 @@ class DocumentsTests: XCTestCase { func testAddDataDocuments() { let jsonString = """ - {"uid": 0, "indexUid": "movies_test", "status": "enqueued", "type": "documentAddition", "enqueuedAt": "xxx" } + {"taskUid":0,"indexUid":"books_test","status":"enqueued","type":"documentAdditionOrUpdate","enqueuedAt":"2022-07-21T21:47:50.565717794Z"} """ // Prepare the mock server let decoder = JSONDecoder() let jsonData = jsonString.data(using: .utf8)! - let stubTask: Task = try! decoder.decode(Task.self, from: jsonData) + let stubTask: TaskInfo = try! decoder.decode(TaskInfo.self, from: jsonData) session.pushData(jsonString, code: 202) let documentJsonString = """ @@ -111,13 +111,13 @@ class DocumentsTests: XCTestCase { func testUpdateDataDocuments() { let jsonString = """ - {"uid": 0, "indexUid": "movies_test", "status": "enqueued", "type": "documentAddition", "enqueuedAt": "xxx" } + {"taskUid":0,"indexUid":"books_test","status":"enqueued","type":"documentAdditionOrUpdate","enqueuedAt":"2022-07-21T21:47:50.565717794Z"} """ // Prepare the mock server let decoder = JSONDecoder() let jsonData = jsonString.data(using: .utf8)! - let stubTask: Task = try! decoder.decode(Task.self, from: jsonData) + let stubTask: TaskInfo = try! decoder.decode(TaskInfo.self, from: jsonData) session.pushData(jsonString, code: 202) let documentJsonString = """ [{ @@ -148,13 +148,13 @@ class DocumentsTests: XCTestCase { func testUpdateDocuments() { let jsonString = """ - {"uid": 0, "indexUid": "movies_test", "status": "enqueued", "type": "documentAddition", "enqueuedAt": "xxx" } + {"taskUid":0,"indexUid":"books_test","status":"enqueued","type":"documentAdditionOrUpdate","enqueuedAt":"2022-07-21T21:47:50.565717794Z"} """ // Prepare the mock server let decoder = JSONDecoder() let jsonData = jsonString.data(using: .utf8)! - let stubTask: Task = try! decoder.decode(Task.self, from: jsonData) + let stubTask: TaskInfo = try! decoder.decode(TaskInfo.self, from: jsonData) session.pushData(jsonString, code: 202) let movie = Movie( @@ -217,21 +217,98 @@ class DocumentsTests: XCTestCase { self.wait(for: [expectation], timeout: TESTS_TIME_OUT) } - func testGetDocuments() { + func testGetDocumentWithSparseFieldsets() { let jsonString = """ - [{ + { "id": 25684, - "release_date": "2020-04-04T19:59:49.259572Z", - "poster": "https://image.tmdb.org/t/p/w1280/iuAQVI4mvjI83wnirpD8GVNRVuY.jpg", - "title": "American Ninja 5", - "overview": "When a scientists daughter is kidnapped, American Ninja, attempts to find her, but this time he teams up with a youngster he has trained in the ways of the ninja." - },{ - "id": 468219, - "title": "Dead in a Week (Or Your Money Back)", - "release_date": "2020-04-04T19:59:49.259572Z", - "poster": "https://image.tmdb.org/t/p/w1280/f4ANVEuEaGy2oP5M0Y2P1dwxUNn.jpg", - "overview": "William has failed to kill himself so many times that he outsources his suicide to aging assassin Leslie. But with the contract signed and death assured within a week (or his money back), William suddenly discovers reasons to live... However Leslie is under pressure from his boss to make sure the contract is completed." - }] + "title": "American Ninja 5" + } + """ + + // Prepare the mock server + session.pushData(jsonString, code: 200) + + do { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .formatted(Formatter.iso8601) + let data = jsonString.data(using: .utf8)! + let stubMovie: Movie = try decoder.decode(Movie.self, from: data) + let identifier: String = "25684" + + // Start the test with the mocked server + let expectation = XCTestExpectation(description: "Get Movies document") + + self.index.getDocument(identifier, fields: ["title", "id"]) { (result: Result) in + switch result { + case .success(let movie): + XCTAssertEqual(stubMovie, movie) + case .failure: + XCTFail("Failed to get Movies document") + } + expectation.fulfill() + } + } catch { + XCTFail("Failed to parse document") + } + +// self.wait(for: [expectation], timeout: TESTS_TIME_OUT) + } + + func testGetDocumentsWithParameters() { + let jsonString = """ + { + "results": [], + "offset": 10, + "limit": 2, + "total": 0 + } + """ + + // Prepare the mock server + session.pushData(jsonString) + + // Start the test with the mocked server + let expectation = XCTestExpectation(description: "Get documents with parameters") + + self.index.getDocuments(params: DocumentsQuery(limit: 2, offset: 10)) { (result: Result, Swift.Error>) in + switch result { + case .success: + XCTAssertEqual(self.session.nextDataTask.request?.url?.query, "limit=2&offset=10") + + expectation.fulfill() + case .failure(let error): + dump(error) + XCTFail("Failed to get all Indexes") + expectation.fulfill() + } + } + + self.wait(for: [expectation], timeout: TESTS_TIME_OUT) + } + + func testGetDocuments() { + let jsonString = """ + { + "results": [ + { + "id": 25684, + "release_date": "2020-04-04T19:59:49.259572Z", + "poster": "https://image.tmdb.org/t/p/w1280/iuAQVI4mvjI83wnirpD8GVNRVuY.jpg", + "title": "American Ninja 5", + "overview": "When a scientists daughter is kidnapped, American Ninja, attempts to find her, but this time he teams up with a youngster he has trained in the ways of the ninja." + }, + { + "id": 468219, + "title": "Dead in a Week (Or Your Money Back)", + "release_date": "2020-04-04T19:59:49.259572Z", + "poster": "https://image.tmdb.org/t/p/w1280/f4ANVEuEaGy2oP5M0Y2P1dwxUNn.jpg", + "overview": "William has failed to kill himself so many times that he outsources his suicide to aging assassin Leslie. But with the contract signed and death assured within a week (or his money back), William suddenly discovers reasons to live... However Leslie is under pressure from his boss to make sure the contract is completed." + } + ], + "limit": 2, + "offset": 0, + "total": 10 + } """ // Prepare the mock server @@ -239,12 +316,12 @@ class DocumentsTests: XCTestCase { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .formatted(Formatter.iso8601) let data = jsonString.data(using: .utf8)! - let stubMovies: [Movie] = try! decoder.decode([Movie].self, from: data) + let stubMovies: DocumentsResults = try! decoder.decode(DocumentsResults.self, from: data) // Start the test with the mocked server let expectation = XCTestExpectation(description: "Get Movies documents") - self.index.getDocuments { (result: Result<[Movie], Swift.Error>) in + self.index.getDocuments { (result: Result, Swift.Error>) in switch result { case .success(let movies): XCTAssertEqual(stubMovies, movies) @@ -258,13 +335,13 @@ class DocumentsTests: XCTestCase { func testDeleteDocument() { let jsonString = """ - {"uid": 0, "indexUid": "movies_test", "status": "enqueued", "type": "documentAddition", "enqueuedAt": "xxx" } - """ + {"taskUid":0,"indexUid":"books_test","status":"enqueued","type":"documentAdditionOrUpdate","enqueuedAt":"2022-07-21T21:47:50.565717794Z"} + """ // Prepare the mock server let decoder = JSONDecoder() let jsonData = jsonString.data(using: .utf8)! - let stubTask: Task = try! decoder.decode(Task.self, from: jsonData) + let stubTask: TaskInfo = try! decoder.decode(TaskInfo.self, from: jsonData) session.pushData(jsonString, code: 202) let identifier: String = "25684" @@ -286,13 +363,13 @@ class DocumentsTests: XCTestCase { func testDeleteAllDocuments() { let jsonString = """ - {"uid": 0, "indexUid": "movies_test", "status": "enqueued", "type": "documentAddition", "enqueuedAt": "xxx" } + {"taskUid":0,"indexUid":"books_test","status":"enqueued","type":"documentAdditionOrUpdate","enqueuedAt":"2022-07-21T21:47:50.565717794Z"} """ // Prepare the mock server let decoder = JSONDecoder() let jsonData = jsonString.data(using: .utf8)! - let stubTask: Task = try! decoder.decode(Task.self, from: jsonData) + let stubTask: TaskInfo = try! decoder.decode(TaskInfo.self, from: jsonData) session.pushData(jsonString, code: 202) // Start the test with the mocked server @@ -312,13 +389,13 @@ class DocumentsTests: XCTestCase { func testDeleteBatchDocuments() { let jsonString = """ - {"uid": 0, "indexUid": "movies_test", "status": "enqueued", "type": "documentAddition", "enqueuedAt": "xxx" } - """ + {"taskUid":0,"indexUid":"books_test","status":"enqueued","type":"documentAdditionOrUpdate","enqueuedAt":"2022-07-21T21:47:50.565717794Z"} + """ // Prepare the mock server let decoder = JSONDecoder() let jsonData = jsonString.data(using: .utf8)! - let stubTask: Task = try! decoder.decode(Task.self, from: jsonData) + let stubTask: TaskInfo = try! decoder.decode(TaskInfo.self, from: jsonData) session.pushData(jsonString, code: 202) let documentsIdentifiers: [Int] = [23488, 153738, 437035, 363869] diff --git a/Tests/MeiliSearchUnitTests/QueryParameters/DocumentsQueryTests.swift b/Tests/MeiliSearchUnitTests/QueryParameters/DocumentsQueryTests.swift new file mode 100644 index 00000000..f3915e6b --- /dev/null +++ b/Tests/MeiliSearchUnitTests/QueryParameters/DocumentsQueryTests.swift @@ -0,0 +1,20 @@ +@testable import MeiliSearch + +import XCTest + +class DocumentsQueryTests: XCTestCase { + func testRenderedQuery() { + let data: [[String: DocumentsQuery]] = [ + ["?limit=2": DocumentsQuery(limit: 2)], + ["?fields=name,title&limit=2&offset=99": DocumentsQuery(limit: 2, offset: 99, fields: ["name", "title"])], + ["?limit=2": DocumentsQuery(limit: 2, offset: nil)], + ["?offset=2": DocumentsQuery(offset: 2)], + ["?limit=10&offset=0": DocumentsQuery(limit: 10, offset: 0)], + ["": DocumentsQuery()] + ] + + data.forEach { dict in + XCTAssertEqual(dict.first?.value.toQuery(), dict.first?.key) + } + } +}