diff --git a/CHANGELOG.md b/CHANGELOG.md index bc2653c83..b36dad1ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.8.0...main) * _Contributing to this repo? Add info about your change here to be included in the next release_ +__Improvements__ +- Append instead of replace when using query select, exclude, include, and fields ([#155](https://github.com/parse-community/Parse-Swift/pull/155)), thanks to [Corey Baker](https://github.com/cbaker6). + ### 1.8.0 [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.7.2...1.8.0) diff --git a/Sources/ParseSwift/LiveQuery/Messages.swift b/Sources/ParseSwift/LiveQuery/Messages.swift index e1d4c5be5..41436f475 100644 --- a/Sources/ParseSwift/LiveQuery/Messages.swift +++ b/Sources/ParseSwift/LiveQuery/Messages.swift @@ -38,7 +38,7 @@ struct StandardMessage: LiveQueryable, Codable { struct SubscribeQuery: Encodable { let className: String let `where`: QueryWhere - let fields: [String]? + let fields: Set? } struct SubscribeMessage: LiveQueryable, Encodable { diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 31770d24b..2e6494ed3 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -633,20 +633,20 @@ public struct Query: Encodable, Equatable where T: ParseObject { private let method: String = "GET" internal var limit: Int = 100 internal var skip: Int = 0 - internal var keys: [String]? - internal var include: [String]? + internal var keys: Set? + internal var include: Set? internal var order: [Order]? internal var isCount: Bool? internal var explain: Bool? internal var hint: AnyEncodable? internal var `where` = QueryWhere() - internal var excludeKeys: [String]? + internal var excludeKeys: Set? internal var readPreference: String? internal var includeReadPreference: String? internal var subqueryReadPreference: String? internal var distinct: String? internal var pipeline: [[String: AnyEncodable]]? - internal var fields: [String]? + internal var fields: Set? /** An enum that determines the order to sort the results based on a given key. @@ -768,21 +768,31 @@ public struct Query: Encodable, Equatable where T: ParseObject { /** Make the query include `ParseObject`s that have a reference stored at the provided keys. + If this is called multiple times, then all of the keys specified in each of the calls will be included. - parameter keys: A variadic list of keys to load child `ParseObject`s for. */ public func include(_ keys: String...) -> Query { var mutableQuery = self - mutableQuery.include = keys + if mutableQuery.include != nil { + mutableQuery.include = mutableQuery.include?.union(keys) + } else { + mutableQuery.include = Set(keys) + } return mutableQuery } /** Make the query include `ParseObject`s that have a reference stored at the provided keys. + If this is called multiple times, then all of the keys specified in each of the calls will be included. - parameter keys: An array of keys to load child `ParseObject`s for. */ public func include(_ keys: [String]) -> Query { var mutableQuery = self - mutableQuery.include = keys + if mutableQuery.include != nil { + mutableQuery.include = mutableQuery.include?.union(keys) + } else { + mutableQuery.include = Set(keys) + } return mutableQuery } @@ -797,24 +807,34 @@ public struct Query: Encodable, Equatable where T: ParseObject { } /** - Exclude specific keys for a `ParseObject`. Default is to nil. + Exclude specific keys for a `ParseObject`. + If this is called multiple times, then all of the keys specified in each of the calls will be excluded. - parameter keys: A variadic list of keys include in the result. - warning: Requires Parse Server > 4.5.0. */ public func exclude(_ keys: String...) -> Query { var mutableQuery = self - mutableQuery.excludeKeys = keys + if mutableQuery.excludeKeys != nil { + mutableQuery.excludeKeys = mutableQuery.excludeKeys?.union(keys) + } else { + mutableQuery.excludeKeys = Set(keys) + } return mutableQuery } /** - Exclude specific keys for a `ParseObject`. Default is to nil. - - parameter keys: An array of keys to exclude. + Exclude specific keys for a `ParseObject`. + If this is called multiple times, then all of the keys specified in each of the calls will be excluded. + - parameter keys: An array of keys to exclude in the result. - warning: Requires Parse Server > 4.5.0. */ public func exclude(_ keys: [String]) -> Query { var mutableQuery = self - mutableQuery.excludeKeys = keys + if mutableQuery.excludeKeys != nil { + mutableQuery.excludeKeys = mutableQuery.excludeKeys?.union(keys) + } else { + mutableQuery.excludeKeys = Set(keys) + } return mutableQuery } @@ -826,7 +846,11 @@ public struct Query: Encodable, Equatable where T: ParseObject { */ public func select(_ keys: String...) -> Query { var mutableQuery = self - mutableQuery.keys = keys + if mutableQuery.keys != nil { + mutableQuery.keys = mutableQuery.keys?.union(keys) + } else { + mutableQuery.keys = Set(keys) + } return mutableQuery } @@ -838,7 +862,11 @@ public struct Query: Encodable, Equatable where T: ParseObject { */ public func select(_ keys: [String]) -> Query { var mutableQuery = self - mutableQuery.keys = keys + if mutableQuery.keys != nil { + mutableQuery.keys = mutableQuery.keys?.union(keys) + } else { + mutableQuery.keys = Set(keys) + } return mutableQuery } @@ -859,13 +887,18 @@ public struct Query: Encodable, Equatable where T: ParseObject { If you are only interested in the change of the name field, you can set `query.fields` to "name". In this situation, when the change of a Player `ParseObject` fulfills the subscription, only the name field will be sent to the clients instead of the full Player `ParseObject`. + If this is called multiple times, then all of the keys specified in each of the calls will be received. - warning: This is only for `ParseLiveQuery`. - parameter keys: A variadic list of fields to receive back instead of the whole `ParseObject`. */ @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public func fields(_ keys: String...) -> Query { var mutableQuery = self - mutableQuery.fields = keys + if mutableQuery.fields != nil { + mutableQuery.fields = mutableQuery.fields?.union(keys) + } else { + mutableQuery.fields = Set(keys) + } return mutableQuery } @@ -876,13 +909,18 @@ public struct Query: Encodable, Equatable where T: ParseObject { If you are only interested in the change of the name field, you can set `query.fields` to "name". In this situation, when the change of a Player `ParseObject` fulfills the subscription, only the name field will be sent to the clients instead of the full Player `ParseObject`. + If this is called multiple times, then all of the keys specified in each of the calls will be received. - warning: This is only for `ParseLiveQuery`. - parameter keys: An array of fields to receive back instead of the whole `ParseObject`. */ @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public func fields(_ keys: [String]) -> Query { var mutableQuery = self - mutableQuery.fields = keys + if mutableQuery.fields != nil { + mutableQuery.fields = mutableQuery.fields?.union(keys) + } else { + mutableQuery.fields = Set(keys) + } return mutableQuery } diff --git a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift index 47d555ce5..c91f77706 100644 --- a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift @@ -193,7 +193,7 @@ class ParseLiveQueryTests: XCTestCase { // swiftlint:disable:next line_length let expected = "{\"op\":\"subscribe\",\"requestId\":1,\"query\":{\"className\":\"GameScore\",\"where\":{\"score\":{\"$gt\":9}},\"fields\":[\"score\"]}}" let query = GameScore.query("score" > 9) - .fields("score") + .fields(["score"]) let message = SubscribeMessage(operation: .subscribe, requestId: RequestId(value: 1), query: query, @@ -204,6 +204,32 @@ class ParseLiveQueryTests: XCTestCase { XCTAssertEqual(decoded, expected) } + func testFieldKeys() throws { + let query = GameScore.query() + XCTAssertNil(query.keys) + + var query2 = GameScore.query().fields(["yolo"]) + XCTAssertEqual(query2.fields?.count, 1) + XCTAssertEqual(query2.fields?.first, "yolo") + + query2 = query2.fields(["hello", "wow"]) + XCTAssertEqual(query2.fields?.count, 3) + XCTAssertEqual(query2.fields, ["yolo", "hello", "wow"]) + } + + func testFieldKeysVariadic() throws { + let query = GameScore.query() + XCTAssertNil(query.keys) + + var query2 = GameScore.query().fields("yolo") + XCTAssertEqual(query2.fields?.count, 1) + XCTAssertEqual(query2.fields?.first, "yolo") + + query2 = query2.fields("hello", "wow") + XCTAssertEqual(query2.fields?.count, 3) + XCTAssertEqual(query2.fields, ["yolo", "hello", "wow"]) + } + func testRedirectResponseDecoding() throws { guard let url = URL(string: "http://parse.org") else { XCTFail("Should have url") diff --git a/Tests/ParseSwiftTests/ParseQueryTests.swift b/Tests/ParseSwiftTests/ParseQueryTests.swift index 92a1f512f..6e00c38f8 100644 --- a/Tests/ParseSwiftTests/ParseQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryTests.swift @@ -157,15 +157,23 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length func testIncludeKeys() { let query = GameScore.query() XCTAssertNil(query.include) - var query2 = GameScore.query().include("yolo") + var query2 = GameScore.query().include(["yolo"]) XCTAssertEqual(query2.include?.count, 1) XCTAssertEqual(query2.include?.first, "yolo") - query2 = query2.include("yolo", "wow") - XCTAssertEqual(query2.include?.count, 2) - XCTAssertEqual(query2.include, ["yolo", "wow"]) - query2 = query2.include(["yolo"]) + query2 = query2.include(["hello", "wow"]) + XCTAssertEqual(query2.include?.count, 3) + XCTAssertEqual(query2.include, Set(["yolo", "hello", "wow"])) + } + + func testIncludeKeysVariadic() { + let query = GameScore.query() + XCTAssertNil(query.include) + var query2 = GameScore.query().include("yolo") XCTAssertEqual(query2.include?.count, 1) - XCTAssertEqual(query2.include, ["yolo"]) + XCTAssertEqual(query2.include?.first, "yolo") + query2 = query2.include("hello", "wow") + XCTAssertEqual(query2.include?.count, 3) + XCTAssertEqual(query2.include, Set(["yolo", "hello", "wow"])) } func testIncludeAllKeys() { @@ -179,8 +187,33 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length func testExcludeKeys() throws { let query = GameScore.query() XCTAssertNil(query.excludeKeys) - var query2 = GameScore.query().exclude("yolo") + var query2 = GameScore.query().exclude(["yolo"]) XCTAssertEqual(query2.excludeKeys, ["yolo"]) + let encoded = try ParseCoding.jsonEncoder().encode(query2) + let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) + guard let decodedKeys = decodedDictionary["excludeKeys"], + let decodedValues = decodedKeys.value as? [String] else { + XCTFail("Should have casted") + return + } + XCTAssertEqual(decodedValues, ["yolo"]) + + query2 = query2.exclude(["hello", "wow"]) + XCTAssertEqual(query2.excludeKeys, ["yolo", "hello", "wow"]) + let encoded2 = try ParseCoding.jsonEncoder().encode(query2) + let decodedDictionary2 = try JSONDecoder().decode([String: AnyCodable].self, from: encoded2) + guard let decodedKeys2 = decodedDictionary2["excludeKeys"], + let decodedValues2 = decodedKeys2.value as? [String] else { + XCTFail("Should have casted") + return + } + XCTAssertEqual(Set(decodedValues2), Set(["yolo", "hello", "wow"])) + } + + func testExcludeKeysVariadic() throws { + let query = GameScore.query() + XCTAssertNil(query.excludeKeys) + var query2 = GameScore.query().exclude("yolo") XCTAssertEqual(query2.excludeKeys, ["yolo"]) let encoded = try ParseCoding.jsonEncoder().encode(query2) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) @@ -191,9 +224,8 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } XCTAssertEqual(decodedValues, ["yolo"]) - query2 = GameScore.query().exclude(["yolo", "wow"]) - XCTAssertEqual(query2.excludeKeys, ["yolo", "wow"]) - XCTAssertEqual(query2.excludeKeys, ["yolo", "wow"]) + query2 = query2.exclude("hello", "wow") + XCTAssertEqual(query2.excludeKeys, ["yolo", "hello", "wow"]) let encoded2 = try ParseCoding.jsonEncoder().encode(query2) let decodedDictionary2 = try JSONDecoder().decode([String: AnyCodable].self, from: encoded2) guard let decodedKeys2 = decodedDictionary2["excludeKeys"], @@ -201,13 +233,42 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length XCTFail("Should have casted") return } - XCTAssertEqual(decodedValues2, ["yolo", "wow"]) + XCTAssertEqual(Set(decodedValues2), Set(["yolo", "hello", "wow"])) } func testSelectKeys() throws { let query = GameScore.query() XCTAssertNil(query.keys) + var query2 = GameScore.query().select(["yolo"]) + XCTAssertEqual(query2.keys?.count, 1) + XCTAssertEqual(query2.keys?.first, "yolo") + let encoded = try ParseCoding.jsonEncoder().encode(query2) + let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) + guard let decodedKeys = decodedDictionary["keys"], + let decodedValues = decodedKeys.value as? [String] else { + XCTFail("Should have casted") + return + } + XCTAssertEqual(decodedValues, ["yolo"]) + + query2 = query2.select(["hello", "wow"]) + XCTAssertEqual(query2.keys?.count, 3) + XCTAssertEqual(query2.keys, ["yolo", "hello", "wow"]) + let encoded2 = try ParseCoding.jsonEncoder().encode(query2) + let decodedDictionary2 = try JSONDecoder().decode([String: AnyCodable].self, from: encoded2) + guard let decodedKeys2 = decodedDictionary2["keys"], + let decodedValues2 = decodedKeys2.value as? [String] else { + XCTFail("Should have casted") + return + } + XCTAssertEqual(Set(decodedValues2), Set(["yolo", "hello", "wow"])) + } + + func testSelectKeysVariadic() throws { + let query = GameScore.query() + XCTAssertNil(query.keys) + var query2 = GameScore.query().select("yolo") XCTAssertEqual(query2.keys?.count, 1) XCTAssertEqual(query2.keys?.first, "yolo") @@ -220,9 +281,9 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } XCTAssertEqual(decodedValues, ["yolo"]) - query2 = query2.select(["yolo", "wow"]) - XCTAssertEqual(query2.keys?.count, 2) - XCTAssertEqual(query2.keys, ["yolo", "wow"]) + query2 = query2.select("hello", "wow") + XCTAssertEqual(query2.keys?.count, 3) + XCTAssertEqual(query2.keys, ["yolo", "hello", "wow"]) let encoded2 = try ParseCoding.jsonEncoder().encode(query2) let decodedDictionary2 = try JSONDecoder().decode([String: AnyCodable].self, from: encoded2) guard let decodedKeys2 = decodedDictionary2["keys"], @@ -230,7 +291,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length XCTFail("Should have casted") return } - XCTAssertEqual(decodedValues2, ["yolo", "wow"]) + XCTAssertEqual(Set(decodedValues2), Set(["yolo", "hello", "wow"])) } func testAddingConstraints() {