From b92667fb177bbefd59fb79f05e5b1707bc7b501b Mon Sep 17 00:00:00 2001 From: Corey Date: Sat, 8 Jan 2022 12:29:36 -0500 Subject: [PATCH] feat: add explain MongoDB queries (#314) * feat: add explain MongoDB queries * nits --- CHANGELOG.md | 8 +- Sources/ParseSwift/API/Responses.swift | 4 + Sources/ParseSwift/ParseConstants.swift | 2 +- Sources/ParseSwift/Types/Query+async.swift | 58 +++- Sources/ParseSwift/Types/Query+combine.swift | 58 +++- Sources/ParseSwift/Types/Query.swift | 267 ++++++++++++++---- .../ParseQueryAsyncTests.swift | 142 +++++++++- .../ParseQueryCombineTests.swift | 6 +- Tests/ParseSwiftTests/ParseQueryTests.swift | 137 ++++++++- 9 files changed, 596 insertions(+), 86 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8f01783f..986c4a87c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,15 @@ ### main -[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/3.0.0...main) +[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/3.1.0...main) * _Contributing to this repo? Add info about your change here to be included in the next release_ +### 3.1.0 +[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/3.0.0...3.1.0) + +__New features__ +- Add the ability to explain MongoDB queries by setting isUsingMongoDB = true for the respective explain query ([#314](https://github.com/parse-community/Parse-Swift/pull/314)), thanks to [Corey Baker](https://github.com/cbaker6). + ### 3.0.0 [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/2.5.1...3.0.0) diff --git a/Sources/ParseSwift/API/Responses.swift b/Sources/ParseSwift/API/Responses.swift index 288b3d21f..f64a797a6 100644 --- a/Sources/ParseSwift/API/Responses.swift +++ b/Sources/ParseSwift/API/Responses.swift @@ -159,6 +159,10 @@ internal struct AnyResultsResponse: Decodable { let results: [U] } +internal struct AnyResultsMongoResponse: Decodable { + let results: U +} + // MARK: ConfigResponse internal struct ConfigFetchResponse: Codable where T: ParseConfig { let params: T diff --git a/Sources/ParseSwift/ParseConstants.swift b/Sources/ParseSwift/ParseConstants.swift index f36c3820c..1bad86624 100644 --- a/Sources/ParseSwift/ParseConstants.swift +++ b/Sources/ParseSwift/ParseConstants.swift @@ -10,7 +10,7 @@ import Foundation enum ParseConstants { static let sdk = "swift" - static let version = "3.0.0" + static let version = "3.1.0" static let fileManagementDirectory = "parse/" static let fileManagementPrivateDocumentsDirectory = "Private Documents/" static let fileManagementLibraryDirectory = "Library/" diff --git a/Sources/ParseSwift/Types/Query+async.swift b/Sources/ParseSwift/Types/Query+async.swift index bd5b48b37..fa909ddb7 100644 --- a/Sources/ParseSwift/Types/Query+async.swift +++ b/Sources/ParseSwift/Types/Query+async.swift @@ -28,6 +28,7 @@ public extension Query { /** Query plan information for finding objects *asynchronously*. + - parameter isUsingMongoDB: Set to **true** if your Parse Server uses MongoDB. Defaults to **false**. - parameter options: A set of header options sent to the server. Defaults to an empty set. - note: An explain query will have many different underlying types. Since Swift is a strongly typed language, a developer should specify the type expected to be decoded which will be @@ -35,10 +36,15 @@ public extension Query { such as the [AnyCodable](https://github.com/Flight-School/AnyCodable) package. - returns: An array of ParseObjects. - throws: An error of type `ParseError`. + - warning: MongoDB's **explain** does not conform to the traditional Parse Server response, so the + `isUsingMongoDB` flag needs to be set for MongoDB users. See more + [here](https://github.com/parse-community/parse-server/pull/7440). */ - func findExplain(options: API.Options = []) async throws -> [U] { + func findExplain(isUsingMongoDB: Bool = false, + options: API.Options = []) async throws -> [U] { try await withCheckedThrowingContinuation { continuation in - self.findExplain(options: options, + self.findExplain(isUsingMongoDB: isUsingMongoDB, + options: options, completion: continuation.resume) } } @@ -80,13 +86,19 @@ public extension Query { typed language, a developer should specify the type expected to be decoded which will be different for MongoDB and PostgreSQL. One way around this is to use a type-erased wrapper such as the [AnyCodable](https://github.com/Flight-School/AnyCodable) package. + - parameter isUsingMongoDB: Set to **true** if your Parse Server uses MongoDB. Defaults to **false**. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: An array of ParseObjects. - throws: An error of type `ParseError`. + - warning: MongoDB's **explain** does not conform to the traditional Parse Server response, so the + `isUsingMongoDB` flag needs to be set for MongoDB users. See more + [here](https://github.com/parse-community/parse-server/pull/7440). */ - func firstExplain(options: API.Options = []) async throws -> U { + func firstExplain(isUsingMongoDB: Bool = false, + options: API.Options = []) async throws -> U { try await withCheckedThrowingContinuation { continuation in - self.firstExplain(options: options, + self.firstExplain(isUsingMongoDB: isUsingMongoDB, + options: options, completion: continuation.resume) } } @@ -110,14 +122,19 @@ public extension Query { typed language, a developer should specify the type expected to be decoded which will be different for MongoDB and PostgreSQL. One way around this is to use a type-erased wrapper such as the [AnyCodable](https://github.com/Flight-School/AnyCodable) package. - - parameter explain: Used to toggle the information on the query plan. + - parameter isUsingMongoDB: Set to **true** if your Parse Server uses MongoDB. Defaults to **false**. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: An array of ParseObjects. - throws: An error of type `ParseError`. + - warning: MongoDB's **explain** does not conform to the traditional Parse Server response, so the + `isUsingMongoDB` flag needs to be set for MongoDB users. See more + [here](https://github.com/parse-community/parse-server/pull/7440). */ - func countExplain(options: API.Options = []) async throws -> [U] { + func countExplain(isUsingMongoDB: Bool = false, + options: API.Options = []) async throws -> [U] { try await withCheckedThrowingContinuation { continuation in - self.countExplain(options: options, + self.countExplain(isUsingMongoDB: isUsingMongoDB, + options: options, completion: continuation.resume) } } @@ -142,14 +159,19 @@ public extension Query { typed language, a developer should specify the type expected to be decoded which will be different for mongoDB and PostgreSQL. One way around this is to use a type-erased wrapper such as the [AnyCodable](https://github.com/Flight-School/AnyCodable) package. - - parameter explain: Used to toggle the information on the query plan. + - parameter isUsingMongoDB: Set to **true** if your Parse Server uses MongoDB. Defaults to **false**. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: An array of ParseObjects. - throws: An error of type `ParseError`. + - warning: MongoDB's **explain** does not conform to the traditional Parse Server response, so the + `isUsingMongoDB` flag needs to be set for MongoDB users. See more + [here](https://github.com/parse-community/parse-server/pull/7440). */ - func withCountExplain(options: API.Options = []) async throws -> [U] { + func withCountExplain(isUsingMongoDB: Bool = false, + options: API.Options = []) async throws -> [U] { try await withCheckedThrowingContinuation { continuation in - self.withCountExplain(options: options, + self.withCountExplain(isUsingMongoDB: isUsingMongoDB, + options: options, completion: continuation.resume) } } @@ -179,16 +201,22 @@ public extension Query { different for MongoDB and PostgreSQL. One way around this is to use a type-erased wrapper such as the [AnyCodable](https://github.com/Flight-School/AnyCodable) package. - parameter pipeline: A pipeline of stages to process query. + - parameter isUsingMongoDB: Set to **true** if your Parse Server uses MongoDB. Defaults to **false**. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: An array of ParseObjects. - throws: An error of type `ParseError`. + - warning: MongoDB's **explain** does not conform to the traditional Parse Server response, so the + `isUsingMongoDB` flag needs to be set for MongoDB users. See more + [here](https://github.com/parse-community/parse-server/pull/7440). */ func aggregateExplain(_ pipeline: [[String: Encodable]], + isUsingMongoDB: Bool = false, options: API.Options = []) async throws -> [U] { try await withCheckedThrowingContinuation { continuation in self.aggregateExplain(pipeline, - options: options, - completion: continuation.resume) + isUsingMongoDB: isUsingMongoDB, + options: options, + completion: continuation.resume) } } @@ -217,14 +245,20 @@ public extension Query { different for MongoDB and PostgreSQL. One way around this is to use a type-erased wrapper such as the [AnyCodable](https://github.com/Flight-School/AnyCodable) package. - parameter key: A field to find distinct values. + - parameter isUsingMongoDB: Set to **true** if your Parse Server uses MongoDB. Defaults to **false**. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: An array of ParseObjects. - throws: An error of type `ParseError`. + - warning: MongoDB's **explain** does not conform to the traditional Parse Server response, so the + `isUsingMongoDB` flag needs to be set for MongoDB users. See more + [here](https://github.com/parse-community/parse-server/pull/7440). */ func distinctExplain(_ key: String, + isUsingMongoDB: Bool = false, options: API.Options = []) async throws -> [U] { try await withCheckedThrowingContinuation { continuation in self.distinctExplain(key, + isUsingMongoDB: isUsingMongoDB, options: options, completion: continuation.resume) } diff --git a/Sources/ParseSwift/Types/Query+combine.swift b/Sources/ParseSwift/Types/Query+combine.swift index aece8494f..02ebd1e37 100644 --- a/Sources/ParseSwift/Types/Query+combine.swift +++ b/Sources/ParseSwift/Types/Query+combine.swift @@ -28,16 +28,22 @@ public extension Query { /** Query plan information for finding objects *asynchronously* and publishes when complete. + - parameter isUsingMongoDB: Set to **true** if your Parse Server uses MongoDB. Defaults to **false**. - parameter options: A set of header options sent to the server. Defaults to an empty set. - note: An explain query will have many different underlying types. Since Swift is a strongly typed language, a developer should specify the type expected to be decoded which will be different for MongoDB and PostgreSQL. One way around this is to use a type-erased wrapper such as the [AnyCodable](https://github.com/Flight-School/AnyCodable) package. - returns: A publisher that eventually produces a single value and then finishes or fails. + - warning: MongoDB's **explain** does not conform to the traditional Parse Server response, so the + `isUsingMongoDB` flag needs to be set for MongoDB users. See more + [here](https://github.com/parse-community/parse-server/pull/7440). */ - func findExplainPublisher(options: API.Options = []) -> Future<[U], ParseError> { + func findExplainPublisher(isUsingMongoDB: Bool = false, + options: API.Options = []) -> Future<[U], ParseError> { Future { promise in - self.findExplain(options: options, + self.findExplain(isUsingMongoDB: isUsingMongoDB, + options: options, completion: promise) } } @@ -78,12 +84,18 @@ public extension Query { typed language, a developer should specify the type expected to be decoded which will be different for MongoDB and PostgreSQL. One way around this is to use a type-erased wrapper such as the [AnyCodable](https://github.com/Flight-School/AnyCodable) package. + - parameter isUsingMongoDB: Set to **true** if your Parse Server uses MongoDB. Defaults to **false**. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. + - warning: MongoDB's **explain** does not conform to the traditional Parse Server response, so the + `isUsingMongoDB` flag needs to be set for MongoDB users. See more + [here](https://github.com/parse-community/parse-server/pull/7440). */ - func firstExplainPublisher(options: API.Options = []) -> Future { + func firstExplainPublisher(isUsingMongoDB: Bool = false, + options: API.Options = []) -> Future { Future { promise in - self.firstExplain(options: options, + self.firstExplain(isUsingMongoDB: isUsingMongoDB, + options: options, completion: promise) } } @@ -106,13 +118,18 @@ public extension Query { typed language, a developer should specify the type expected to be decoded which will be different for MongoDB and PostgreSQL. One way around this is to use a type-erased wrapper such as the [AnyCodable](https://github.com/Flight-School/AnyCodable) package. - - parameter explain: Used to toggle the information on the query plan. + - parameter isUsingMongoDB: Set to **true** if your Parse Server uses MongoDB. Defaults to **false**. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. + - warning: MongoDB's **explain** does not conform to the traditional Parse Server response, so the + `isUsingMongoDB` flag needs to be set for MongoDB users. See more + [here](https://github.com/parse-community/parse-server/pull/7440). */ - func countExplainPublisher(options: API.Options = []) -> Future<[U], ParseError> { + func countExplainPublisher(isUsingMongoDB: Bool = false, + options: API.Options = []) -> Future<[U], ParseError> { Future { promise in - self.countExplain(options: options, + self.countExplain(isUsingMongoDB: isUsingMongoDB, + options: options, completion: promise) } } @@ -137,13 +154,18 @@ public extension Query { typed language, a developer should specify the type expected to be decoded which will be different for mongoDB and PostgreSQL. One way around this is to use a type-erased wrapper such as the [AnyCodable](https://github.com/Flight-School/AnyCodable) package. - - parameter explain: Used to toggle the information on the query plan. + - parameter isUsingMongoDB: Set to **true** if your Parse Server uses MongoDB. Defaults to **false**. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. + - warning: MongoDB's **explain** does not conform to the traditional Parse Server response, so the + `isUsingMongoDB` flag needs to be set for MongoDB users. See more + [here](https://github.com/parse-community/parse-server/pull/7440). */ - func withCountExplainPublisher(options: API.Options = []) -> Future<[U], ParseError> { + func withCountExplainPublisher(isUsingMongoDB: Bool = false, + options: API.Options = []) -> Future<[U], ParseError> { Future { promise in - self.withCountExplain(options: options, + self.withCountExplain(isUsingMongoDB: isUsingMongoDB, + options: options, completion: promise) } } @@ -172,15 +194,21 @@ public extension Query { different for MongoDB and PostgreSQL. One way around this is to use a type-erased wrapper such as the [AnyCodable](https://github.com/Flight-School/AnyCodable) package. - parameter pipeline: A pipeline of stages to process query. + - parameter isUsingMongoDB: Set to **true** if your Parse Server uses MongoDB. Defaults to **false**. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. + - warning: MongoDB's **explain** does not conform to the traditional Parse Server response, so the + `isUsingMongoDB` flag needs to be set for MongoDB users. See more + [here](https://github.com/parse-community/parse-server/pull/7440). */ func aggregateExplainPublisher(_ pipeline: [[String: Encodable]], + isUsingMongoDB: Bool = false, options: API.Options = []) -> Future<[U], ParseError> { Future { promise in self.aggregateExplain(pipeline, - options: options, - completion: promise) + isUsingMongoDB: isUsingMongoDB, + options: options, + completion: promise) } } @@ -208,13 +236,19 @@ public extension Query { different for MongoDB and PostgreSQL. One way around this is to use a type-erased wrapper such as the [AnyCodable](https://github.com/Flight-School/AnyCodable) package. - parameter key: A field to find distinct values. + - parameter isUsingMongoDB: Set to **true** if your Parse Server uses MongoDB. Defaults to **false**. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. + - warning: MongoDB's **explain** does not conform to the traditional Parse Server response, so the + `isUsingMongoDB` flag needs to be set for MongoDB users. See more + [here](https://github.com/parse-community/parse-server/pull/7440). */ func distinctExplainPublisher(_ key: String, + isUsingMongoDB: Bool = false, options: API.Options = []) -> Future<[U], ParseError> { Future { promise in self.distinctExplain(key, + isUsingMongoDB: isUsingMongoDB, options: options, completion: promise) } diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 5f18c8c92..42748110b 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -396,16 +396,24 @@ extension Query: Queryable { typed language, a developer should specify the type expected to be decoded which will be different for MongoDB and PostgreSQL. One way around this is to use a type-erased wrapper such as the [AnyCodable](https://github.com/Flight-School/AnyCodable) package. + - parameter isUsingMongoDB: Set to **true** if your Parse Server uses MongoDB. Defaults to **false**. - parameter options: A set of header options sent to the server. Defaults to an empty set. - throws: An error of type `ParseError`. - - returns: Returns a response of `Decodable` type. + - warning: MongoDB's **explain** does not conform to the traditional Parse Server response, so the + `isUsingMongoDB` flag needs to be set for MongoDB users. See more + [here](https://github.com/parse-community/parse-server/pull/7440). */ - public func findExplain(options: API.Options = []) throws -> [U] { + public func findExplain(isUsingMongoDB: Bool = false, + options: API.Options = []) throws -> [U] { if limit == 0 { return [U]() } - return try findExplainCommand().execute(options: options) + if !isUsingMongoDB { + return try findExplainCommand().execute(options: options) + } else { + return try findExplainMongoCommand().execute(options: options) + } } /** @@ -437,12 +445,17 @@ extension Query: Queryable { typed language, a developer should specify the type expected to be decoded which will be different for MongoDB and PostgreSQL. One way around this is to use a type-erased wrapper such as the [AnyCodable](https://github.com/Flight-School/AnyCodable) package. + - parameter isUsingMongoDB: Set to **true** if your Parse Server uses MongoDB. Defaults to **false**. - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. It should have the following argument signature: `(Result<[Decodable], ParseError>)`. + - warning: MongoDB's **explain** does not conform to the traditional Parse Server response, so the + `isUsingMongoDB` flag needs to be set for MongoDB users. See more + [here](https://github.com/parse-community/parse-server/pull/7440). */ - public func findExplain(options: API.Options = [], + public func findExplain(isUsingMongoDB: Bool = false, + options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[U], ParseError>) -> Void) { if limit == 0 { @@ -451,9 +464,16 @@ extension Query: Queryable { } return } - findExplainCommand().executeAsync(options: options, - callbackQueue: callbackQueue) { result in - completion(result) + if !isUsingMongoDB { + findExplainCommand().executeAsync(options: options, + callbackQueue: callbackQueue) { result in + completion(result) + } + } else { + findExplainMongoCommand().executeAsync(options: options, + callbackQueue: callbackQueue) { result in + completion(result) + } } } @@ -557,17 +577,25 @@ extension Query: Queryable { typed language, a developer should specify the type expected to be decoded which will be different for MongoDB and PostgreSQL. One way around this is to use a type-erased wrapper such as the [AnyCodable](https://github.com/Flight-School/AnyCodable) package. + - parameter isUsingMongoDB: Set to **true** if your Parse Server uses MongoDB. Defaults to **false**. - parameter options: A set of header options sent to the server. Defaults to an empty set. - throws: An error of type `ParseError`. - - returns: Returns a response of `Decodable` type. + - warning: MongoDB's **explain** does not conform to the traditional Parse Server response, so the + `isUsingMongoDB` flag needs to be set for MongoDB users. See more + [here](https://github.com/parse-community/parse-server/pull/7440). */ - public func firstExplain(options: API.Options = []) throws -> U { + public func firstExplain(isUsingMongoDB: Bool = false, + options: API.Options = []) throws -> U { if limit == 0 { throw ParseError(code: .objectNotFound, message: "Object not found on the server.") } - return try firstExplainCommand().execute(options: options) + if !isUsingMongoDB { + return try firstExplainCommand().execute(options: options) + } else { + return try firstExplainMongoCommand().execute(options: options) + } } /** @@ -604,12 +632,17 @@ extension Query: Queryable { typed language, a developer should specify the type expected to be decoded which will be different for MongoDB and PostgreSQL. One way around this is to use a type-erased wrapper such as the [AnyCodable](https://github.com/Flight-School/AnyCodable) package. + - parameter isUsingMongoDB: Set to **true** if your Parse Server uses MongoDB. Defaults to **false**. - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of `.main`. - parameter completion: The block to execute. It should have the following argument signature: `(Result)`. + - warning: MongoDB's **explain** does not conform to the traditional Parse Server response, so the + `isUsingMongoDB` flag needs to be set for MongoDB users. See more + [here](https://github.com/parse-community/parse-server/pull/7440). */ - public func firstExplain(options: API.Options = [], + public func firstExplain(isUsingMongoDB: Bool = false, + options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void) { if limit == 0 { @@ -620,9 +653,16 @@ extension Query: Queryable { } return } - firstExplainCommand().executeAsync(options: options, - callbackQueue: callbackQueue) { result in - completion(result) + if !isUsingMongoDB { + firstExplainCommand().executeAsync(options: options, + callbackQueue: callbackQueue) { result in + completion(result) + } + } else { + firstExplainMongoCommand().executeAsync(options: options, + callbackQueue: callbackQueue) { result in + completion(result) + } } } @@ -647,16 +687,24 @@ extension Query: Queryable { typed language, a developer should specify the type expected to be decoded which will be different for MongoDB and PostgreSQL. One way around this is to use a type-erased wrapper such as the [AnyCodable](https://github.com/Flight-School/AnyCodable) package. + - parameter isUsingMongoDB: Set to **true** if your Parse Server uses MongoDB. Defaults to **false**. - parameter options: A set of header options sent to the server. Defaults to an empty set. - throws: An error of type `ParseError`. - - returns: Returns a response of `Decodable` type. + - warning: MongoDB's **explain** does not conform to the traditional Parse Server response, so the + `isUsingMongoDB` flag needs to be set for MongoDB users. See more + [here](https://github.com/parse-community/parse-server/pull/7440). */ - public func countExplain(options: API.Options = []) throws -> [U] { + public func countExplain(isUsingMongoDB: Bool = false, + options: API.Options = []) throws -> [U] { if limit == 0 { return [U]() } - return try countExplainCommand().execute(options: options) + if !isUsingMongoDB { + return try countExplainCommand().execute(options: options) + } else { + return try countExplainMongoCommand().execute(options: options) + } } /** @@ -688,12 +736,17 @@ extension Query: Queryable { typed language, a developer should specify the type expected to be decoded which will be different for MongoDB and PostgreSQL. One way around this is to use a type-erased wrapper such as the [AnyCodable](https://github.com/Flight-School/AnyCodable) package. + - parameter isUsingMongoDB: Set to **true** if your Parse Server uses MongoDB. Defaults to **false**. - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of `.main`. - parameter completion: The block to execute. It should have the following argument signature: `(Result)`. + - warning: MongoDB's **explain** does not conform to the traditional Parse Server response, so the + `isUsingMongoDB` flag needs to be set for MongoDB users. See more + [here](https://github.com/parse-community/parse-server/pull/7440). */ - public func countExplain(options: API.Options = [], + public func countExplain(isUsingMongoDB: Bool = false, + options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[U], ParseError>) -> Void) { if limit == 0 { @@ -702,9 +755,16 @@ extension Query: Queryable { } return } - countExplainCommand().executeAsync(options: options, - callbackQueue: callbackQueue) { result in - completion(result) + if !isUsingMongoDB { + countExplainCommand().executeAsync(options: options, + callbackQueue: callbackQueue) { result in + completion(result) + } + } else { + countExplainMongoCommand().executeAsync(options: options, + callbackQueue: callbackQueue) { result in + completion(result) + } } } @@ -738,12 +798,17 @@ extension Query: Queryable { typed language, a developer should specify the type expected to be decoded which will be different for mongoDB and PostgreSQL. One way around this is to use a type-erased wrapper such as the [AnyCodable](https://github.com/Flight-School/AnyCodable) package. + - parameter isUsingMongoDB: Set to **true** if your Parse Server uses MongoDB. Defaults to **false**. - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of `.main`. - parameter completion: The block to execute. It should have the following argument signature: `(Result)`. + - warning: MongoDB's **explain** does not conform to the traditional Parse Server response, so the + `isUsingMongoDB` flag needs to be set for MongoDB users. See more + [here](https://github.com/parse-community/parse-server/pull/7440). */ - public func withCountExplain(options: API.Options = [], + public func withCountExplain(isUsingMongoDB: Bool = false, + options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[U], ParseError>) -> Void) { if limit == 0 { @@ -752,9 +817,16 @@ extension Query: Queryable { } return } - withCountExplainCommand().executeAsync(options: options, - callbackQueue: callbackQueue) { result in - completion(result) + if !isUsingMongoDB { + withCountExplainCommand().executeAsync(options: options, + callbackQueue: callbackQueue) { result in + completion(result) + } + } else { + withCountExplainMongoCommand().executeAsync(options: options, + callbackQueue: callbackQueue) { result in + completion(result) + } } } @@ -860,12 +932,16 @@ extension Query: Queryable { different for MongoDB and PostgreSQL. One way around this is to use a type-erased wrapper such as the [AnyCodable](https://github.com/Flight-School/AnyCodable) package. - parameter pipeline: A pipeline of stages to process query. + - parameter isUsingMongoDB: Set to **true** if your Parse Server uses MongoDB. Defaults to **false**. - parameter options: A set of header options sent to the server. Defaults to an empty set. - throws: An error of type `ParseError`. - - warning: This hasn't been tested thoroughly. - returns: Returns the `ParseObject`s that match the query. + - warning: MongoDB's **explain** does not conform to the traditional Parse Server response, so the + `isUsingMongoDB` flag needs to be set for MongoDB users. See more + [here](https://github.com/parse-community/parse-server/pull/7440). */ public func aggregateExplain(_ pipeline: [[String: Encodable]], + isUsingMongoDB: Bool = false, options: API.Options = []) throws -> [U] { if limit == 0 { return [U]() @@ -891,9 +967,13 @@ extension Query: Queryable { } else { query.pipeline = updatedPipeline } - - return try query.aggregateExplainCommand() - .execute(options: options) + if !isUsingMongoDB { + return try query.aggregateExplainCommand() + .execute(options: options) + } else { + return try query.aggregateExplainMongoCommand() + .execute(options: options) + } } /** @@ -904,13 +984,17 @@ extension Query: Queryable { different for MongoDB and PostgreSQL. One way around this is to use a type-erased wrapper such as the [AnyCodable](https://github.com/Flight-School/AnyCodable) package. - parameter pipeline: A pipeline of stages to process query. + - parameter isUsingMongoDB: Set to **true** if your Parse Server uses MongoDB. Defaults to **false**. - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of `.main`. - parameter completion: The block to execute. It should have the following argument signature: `(Result<[ParseObject], ParseError>)`. - - warning: This hasn't been tested thoroughly. + - warning: MongoDB's **explain** does not conform to the traditional Parse Server response, so the + `isUsingMongoDB` flag needs to be set for MongoDB users. See more + [here](https://github.com/parse-community/parse-server/pull/7440). */ public func aggregateExplain(_ pipeline: [[String: Encodable]], + isUsingMongoDB: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[U], ParseError>) -> Void) { @@ -946,10 +1030,16 @@ extension Query: Queryable { } else { query.pipeline = updatedPipeline } - - query.aggregateExplainCommand() - .executeAsync(options: options, callbackQueue: callbackQueue) { result in - completion(result) + if !isUsingMongoDB { + query.aggregateExplainCommand() + .executeAsync(options: options, callbackQueue: callbackQueue) { result in + completion(result) + } + } else { + query.aggregateExplainMongoCommand() + .executeAsync(options: options, callbackQueue: callbackQueue) { result in + completion(result) + } } } @@ -1010,20 +1100,30 @@ extension Query: Queryable { different for MongoDB and PostgreSQL. One way around this is to use a type-erased wrapper such as the [AnyCodable](https://github.com/Flight-School/AnyCodable) package. - parameter key: A field to find distinct values. + - parameter isUsingMongoDB: Set to **true** if your Parse Server uses MongoDB. Defaults to **false**. - parameter options: A set of header options sent to the server. Defaults to an empty set. - throws: An error of type `ParseError`. - warning: This hasn't been tested thoroughly. - returns: Returns the `ParseObject`s that match the query. + - warning: MongoDB's **explain** does not conform to the traditional Parse Server response, so the + `isUsingMongoDB` flag needs to be set for MongoDB users. See more + [here](https://github.com/parse-community/parse-server/pull/7440). */ public func distinctExplain(_ key: String, + isUsingMongoDB: Bool = false, options: API.Options = []) throws -> [U] { if limit == 0 { return [U]() } var options = options options.insert(.useMasterKey) - return try distinctExplainCommand(key: key) - .execute(options: options) + if !isUsingMongoDB { + return try distinctExplainCommand(key: key) + .execute(options: options) + } else { + return try distinctExplainMongoCommand(key: key) + .execute(options: options) + } } /** @@ -1034,13 +1134,17 @@ extension Query: Queryable { different for MongoDB and PostgreSQL. One way around this is to use a type-erased wrapper such as the [AnyCodable](https://github.com/Flight-School/AnyCodable) package. - parameter key: A field to find distinct values. + - parameter isUsingMongoDB: Set to **true** if your Parse Server uses MongoDB. Defaults to **false**. - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of `.main`. - parameter completion: The block to execute. It should have the following argument signature: `(Result<[Decodable], ParseError>)`. - - warning: This hasn't been tested thoroughly. + - warning: MongoDB's **explain** does not conform to the traditional Parse Server response, so the + `isUsingMongoDB` flag needs to be set for MongoDB users. See more + [here](https://github.com/parse-community/parse-server/pull/7440). */ public func distinctExplain(_ key: String, + isUsingMongoDB: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[U], ParseError>) -> Void) { @@ -1052,10 +1156,18 @@ extension Query: Queryable { } var options = options options.insert(.useMasterKey) - distinctExplainCommand(key: key) - .executeAsync(options: options, - callbackQueue: callbackQueue) { result in - completion(result) + if !isUsingMongoDB { + distinctExplainCommand(key: key) + .executeAsync(options: options, + callbackQueue: callbackQueue) { result in + completion(result) + } + } else { + distinctExplainMongoCommand(key: key) + .executeAsync(options: options, + callbackQueue: callbackQueue) { result in + completion(result) + } } } } @@ -1124,7 +1236,7 @@ extension Query { var query = self query.explain = true return API.NonParseBodyCommand(method: .POST, path: query.endpoint, body: query) { - try ParseCoding.jsonDecoder().decode(AnyResultsResponse.self, from: $0).results + try ParseCoding.jsonDecoder().decode(AnyResultsResponse.self, from: $0).results } } @@ -1133,11 +1245,11 @@ extension Query { query.limit = 1 query.explain = true return API.NonParseBodyCommand(method: .POST, path: query.endpoint, body: query) { - if let decoded: U = try ParseCoding.jsonDecoder().decode(AnyResultsResponse.self, from: $0).results.first { + if let decoded = try ParseCoding.jsonDecoder().decode(AnyResultsResponse.self, from: $0).results.first { return decoded } throw ParseError(code: .objectNotFound, - message: "Object not found on the server.") + message: "Object not found on the server.") } } @@ -1147,8 +1259,7 @@ extension Query { query.isCount = true query.explain = true return API.NonParseBodyCommand(method: .POST, path: query.endpoint, body: query) { - let decoded: [U] = try ParseCoding.jsonDecoder().decode(AnyResultsResponse.self, from: $0).results - return decoded + try ParseCoding.jsonDecoder().decode(AnyResultsResponse.self, from: $0).results } } @@ -1157,8 +1268,7 @@ extension Query { query.isCount = true query.explain = true return API.NonParseBodyCommand(method: .POST, path: query.endpoint, body: query) { - let decoded: [U] = try ParseCoding.jsonDecoder().decode(AnyResultsResponse.self, from: $0).results - return decoded + try ParseCoding.jsonDecoder().decode(AnyResultsResponse.self, from: $0).results } } @@ -1180,6 +1290,67 @@ extension Query { try ParseCoding.jsonDecoder().decode(AnyResultsResponse.self, from: $0).results } } + + func findExplainMongoCommand() -> API.NonParseBodyCommand, [U]> { + var query = self + query.explain = true + return API.NonParseBodyCommand(method: .POST, path: query.endpoint, body: query) { + try [ParseCoding.jsonDecoder().decode(AnyResultsMongoResponse.self, from: $0).results] + } + } + + func firstExplainMongoCommand() -> API.NonParseBodyCommand, U> { + var query = self + query.limit = 1 + query.explain = true + return API.NonParseBodyCommand(method: .POST, path: query.endpoint, body: query) { + do { + return try ParseCoding.jsonDecoder().decode(AnyResultsMongoResponse.self, from: $0).results + } catch { + throw ParseError(code: .objectNotFound, + message: "Object not found on the server. Error: \(error)") + } + } + } + + func countExplainMongoCommand() -> API.NonParseBodyCommand, [U]> { + var query = self + query.limit = 1 + query.isCount = true + query.explain = true + return API.NonParseBodyCommand(method: .POST, path: query.endpoint, body: query) { + try [ParseCoding.jsonDecoder().decode(AnyResultsMongoResponse.self, from: $0).results] + } + } + + func withCountExplainMongoCommand() -> API.NonParseBodyCommand, [U]> { + var query = self + query.isCount = true + query.explain = true + return API.NonParseBodyCommand(method: .POST, path: query.endpoint, body: query) { + try [ParseCoding.jsonDecoder().decode(AnyResultsMongoResponse.self, from: $0).results] + } + } + + func aggregateExplainMongoCommand() -> API.NonParseBodyCommand, [U]> { + var query = self + query.explain = true + let body = AggregateBody(query: query) + return API.NonParseBodyCommand(method: .POST, path: .aggregate(className: T.className), body: body) { + try [ParseCoding.jsonDecoder().decode(AnyResultsMongoResponse.self, from: $0).results] + } + } + + // swiftlint:disable:next line_length + func distinctExplainMongoCommand(key: String) -> API.NonParseBodyCommand, [U]> { + var query = self + query.explain = true + query.distinct = key + let body = DistinctBody(query: query) + return API.NonParseBodyCommand(method: .POST, path: .aggregate(className: T.className), body: body) { + try [ParseCoding.jsonDecoder().decode(AnyResultsMongoResponse.self, from: $0).results] + } + } } // MARK: Query diff --git a/Tests/ParseSwiftTests/ParseQueryAsyncTests.swift b/Tests/ParseSwiftTests/ParseQueryAsyncTests.swift index 3de2410d6..1766e3e75 100644 --- a/Tests/ParseSwiftTests/ParseQueryAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryAsyncTests.swift @@ -38,11 +38,11 @@ class ParseQueryAsyncTests: XCTestCase { // swiftlint:disable:this type_body_len } } - struct AnyResultResponse: Codable { - let result: U + struct AnyResultsResponse: Codable { + let results: [U] } - struct AnyResultsResponse: Codable { + struct AnyResultsMongoResponse: Codable { let results: U } @@ -219,6 +219,28 @@ class ParseQueryAsyncTests: XCTestCase { // swiftlint:disable:this type_body_len XCTAssertEqual(queryResult, json.results) } + @MainActor + func testFindExplainMongo() async throws { + + let json = AnyResultsMongoResponse(results: ["yolo": "yarr"]) + + let encoded: Data! + do { + encoded = try JSONEncoder().encode(json) + } catch { + XCTFail("Should encode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let query = GameScore.query() + let queryResult: [[String: String]] = try await query.findExplain(isUsingMongoDB: true) + XCTAssertEqual(queryResult, [json.results]) + } + @MainActor func testWithCountExplain() async throws { @@ -241,6 +263,28 @@ class ParseQueryAsyncTests: XCTestCase { // swiftlint:disable:this type_body_len XCTAssertEqual(queryResult, json.results) } + @MainActor + func testWithCountExplainMongo() async throws { + + let json = AnyResultsMongoResponse(results: ["yolo": "yarr"]) + + let encoded: Data! + do { + encoded = try JSONEncoder().encode(json) + } catch { + XCTFail("Should encode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let query = GameScore.query() + let queryResult: [[String: String]] = try await query.withCountExplain(isUsingMongoDB: true) + XCTAssertEqual(queryResult, [json.results]) + } + @MainActor func testWithCountExplainLimitZero() async throws { @@ -298,6 +342,29 @@ class ParseQueryAsyncTests: XCTestCase { // swiftlint:disable:this type_body_len XCTAssertEqual(queryResult, json.results.first) } + @MainActor + func testFirstExplainMongo() async throws { + + let json = AnyResultsMongoResponse(results: ["yolo": "yarr"]) + + let encoded: Data! + do { + encoded = try JSONEncoder().encode(json) + } catch { + XCTFail("Should encode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let query = GameScore.query() + + let queryResult: [String: String] = try await query.firstExplain(isUsingMongoDB: true) + XCTAssertEqual(queryResult, json.results) + } + @MainActor func testCount() async throws { @@ -346,6 +413,28 @@ class ParseQueryAsyncTests: XCTestCase { // swiftlint:disable:this type_body_len XCTAssertEqual(queryResult, json.results) } + @MainActor + func testCountExplainMongo() async throws { + + let json = AnyResultsMongoResponse(results: ["yolo": "yarr"]) + + let encoded: Data! + do { + encoded = try JSONEncoder().encode(json) + } catch { + XCTFail("Should encode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let query = GameScore.query() + let queryResult: [[String: String]] = try await query.countExplain(isUsingMongoDB: true) + XCTAssertEqual(queryResult, [json.results]) + } + @MainActor func testAggregate() async throws { @@ -398,6 +487,30 @@ class ParseQueryAsyncTests: XCTestCase { // swiftlint:disable:this type_body_len XCTAssertEqual(queryResult, json.results) } + @MainActor + func testAggregateExplainMongo() async throws { + + let json = AnyResultsMongoResponse(results: ["yolo": "yarr"]) + + let encoded: Data! + do { + encoded = try JSONEncoder().encode(json) + } catch { + XCTFail("Should encode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let query = GameScore.query() + let pipeline = [[String: String]]() + let queryResult: [[String: String]] = try await query.aggregateExplain(pipeline, + isUsingMongoDB: true) + XCTAssertEqual(queryResult, [json.results]) + } + @MainActor func testDistinct() async throws { @@ -447,5 +560,28 @@ class ParseQueryAsyncTests: XCTestCase { // swiftlint:disable:this type_body_len let queryResult: [[String: String]] = try await query.distinctExplain("hello") XCTAssertEqual(queryResult, json.results) } + + @MainActor + func testDistinctExplainMongo() async throws { + + let json = AnyResultsMongoResponse(results: ["yolo": "yarr"]) + + let encoded: Data! + do { + encoded = try JSONEncoder().encode(json) + } catch { + XCTFail("Should encode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let query = GameScore.query() + let queryResult: [[String: String]] = try await query.distinctExplain("hello", + isUsingMongoDB: true) + XCTAssertEqual(queryResult, [json.results]) + } } #endif diff --git a/Tests/ParseSwiftTests/ParseQueryCombineTests.swift b/Tests/ParseSwiftTests/ParseQueryCombineTests.swift index 4e1024503..9a943fe44 100644 --- a/Tests/ParseSwiftTests/ParseQueryCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryCombineTests.swift @@ -43,12 +43,8 @@ class ParseQueryCombineTests: XCTestCase { // swiftlint:disable:this type_body_l } } - struct AnyResultResponse: Codable { - let result: U - } - struct AnyResultsResponse: Codable { - let results: U + let results: [U] } override func setUpWithError() throws { diff --git a/Tests/ParseSwiftTests/ParseQueryTests.swift b/Tests/ParseSwiftTests/ParseQueryTests.swift index 94441645b..51bfe2b8e 100644 --- a/Tests/ParseSwiftTests/ParseQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryTests.swift @@ -44,14 +44,14 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length var points: Int? } - struct AnyResultResponse: Codable { - let result: U - } - struct AnyResultsResponse: Codable { let results: [U] } + struct AnyResultsMongoResponse: Codable { + let results: U + } + override func setUpWithError() throws { try super.setUpWithError() guard let url = URL(string: "http://localhost:1337/1") else { @@ -3020,6 +3020,30 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } } + func testExplainMongoFindSynchronous() { + let json = AnyResultsMongoResponse(results: ["yolo": "yarr"]) + + let encoded: Data! + do { + encoded = try JSONEncoder().encode(json) + } catch { + XCTFail("Should encode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let query = GameScore.query() + do { + let queryResult: [[String: String]] = try query.findExplain(isUsingMongoDB: true) + XCTAssertEqual(queryResult, [json.results]) + } catch { + XCTFail("Error: \(error)") + } + } + func testExplainFindLimitSynchronous() { let query = GameScore.query() .limit(0) @@ -3103,6 +3127,30 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } } + func testExplainMongoFirstSynchronous() { + let json = AnyResultsMongoResponse(results: ["yolo": "yarr"]) + + let encoded: Data! + do { + encoded = try JSONEncoder().encode(json) + } catch { + XCTFail("Should encode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let query = GameScore.query() + do { + let queryResult: [String: String] = try query.firstExplain(isUsingMongoDB: true) + XCTAssertEqual(queryResult, json.results) + } catch { + XCTFail("Error: \(error)") + } + } + func testExplainFirstLimitSynchronous() { let query = GameScore.query() .limit(0) @@ -3190,6 +3238,30 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } } + func testExplainMongoCountSynchronous() { + let json = AnyResultsMongoResponse(results: ["yolo": "yarr"]) + + let encoded: Data! + do { + encoded = try JSONEncoder().encode(json) + } catch { + XCTFail("Should encode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let query = GameScore.query() + do { + let queryResult: [[String: String]] = try query.countExplain(isUsingMongoDB: true) + XCTAssertEqual(queryResult, [json.results]) + } catch { + XCTFail("Error: \(error)") + } + } + func testExplainCountLimitSynchronous() { let query = GameScore.query() @@ -3567,6 +3639,35 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } } + func testAggregateExplainMongoWithWhere() { + let json = AnyResultsMongoResponse(results: ["yolo": "yarr"]) + + let encoded: Data! + do { + encoded = try JSONEncoder().encode(json) + } catch { + XCTFail("Should encode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let query = GameScore.query("points" > 9) + do { + let pipeline = [[String: String]]() + guard let score: [String: String] = try query.aggregateExplain(pipeline, + isUsingMongoDB: true).first else { + XCTFail("Should unwrap first object found") + return + } + XCTAssertEqual(score, json.results) + } catch { + XCTFail(error.localizedDescription) + } + } + func testAggregateExplainWithWhereLimit() { let query = GameScore.query("points" > 9) @@ -3804,6 +3905,34 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } } + func testDistinctExplainMongo() { + let json = AnyResultsMongoResponse(results: ["yolo": "yarr"]) + + let encoded: Data! + do { + encoded = try JSONEncoder().encode(json) + } catch { + XCTFail("Should encode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let query = GameScore.query("points" > 9) + do { + guard let score: [String: String] = try query.distinctExplain("hello", + isUsingMongoDB: true).first else { + XCTFail("Should unwrap first object found") + return + } + XCTAssertEqual(score, json.results) + } catch { + XCTFail(error.localizedDescription) + } + } + func testDistinctExplainLimit() { let query = GameScore.query("points" > 9)