diff --git a/.codecov.yml b/.codecov.yml index 7009a7cb2..6899e9b41 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -6,7 +6,7 @@ coverage: status: patch: default: - target: auto + target: 74 changes: false project: default: diff --git a/ParseSwift.playground/Pages/12 - Roles and Relations.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/12 - Roles and Relations.xcplaygroundpage/Contents.swift index 180d385a5..8c5565483 100644 --- a/ParseSwift.playground/Pages/12 - Roles and Relations.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/12 - Roles and Relations.xcplaygroundpage/Contents.swift @@ -47,6 +47,31 @@ struct Role: ParseRole { } } +//: Create your own value typed `ParseObject`. +struct GameScore: ParseObject, ParseObjectMutable { + //: These are required by ParseObject + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + //: Your own properties. + var score: Int = 0 +} + +//: It's recommended to place custom initializers in an extension +//: to preserve the convenience initializer. +extension GameScore { + + init(score: Int) { + self.score = score + } + + init(objectId: String?) { + self.objectId = objectId + } +} + //: Roles can provide additional access/security to your apps. //: This variable will store the saved role. @@ -218,15 +243,71 @@ do { print(error) } +//: Using this relation, you can create one-to-many relationships with other `ParseObjecs`, +//: similar to `users` and `roles`. //: All `ParseObject`s have a `ParseRelation` attribute that be used on instances. //: For example, the User has: -let relation = User.current!.relation +var relation = User.current!.relation +let score1 = GameScore(score: 53) +let score2 = GameScore(score: 57) -//: Example: relation.add(<#T##users: [ParseUser]##[ParseUser]#>) -//: Example: relation.remove(<#T##key: String##String#>, objects: <#T##[ParseObject]#>) +//: Add new child relationships. +[score1, score2].saveAll { result in + switch result { + case .success(let savedScores): + //: Make an array of all scores that were properly saved. + let scores = savedScores.compactMap { try? $0.get() } + do { + let newRelations = try relation.add("scores", objects: scores) + newRelations.save { result in + switch result { + case .success(let saved): + print("The relation saved successfully: \(saved)") + print("Check \"scores\" field in your \"_User\" class in Parse Dashboard.") + + case .failure(let error): + print("Error saving role: \(error)") + } + } + } catch { + print(error) + } + case .failure(let error): + print("Couldn't save scores. \(error)") + } +} -//: Using this relation, you can create many-to-many relationships with other `ParseObjecs`, -//: similar to `users` and `roles`. +let specificRelation = User.current!.relation("scores", child: score1) +//: You can also do +// let specificRelation = User.current!.relation("scores", className: "GameScore") +do { + try specificRelation.query(score1).find { result in + switch result { + case .success(let scores): + print("Found related scores: \(scores)") + case .failure(let error): + print("Error finding scores: \(error)") + } + } +} catch { + print(error) +} + +do { + //: You can also leverage the child to find scores related to the parent. + try score1.relation.query("scores", parent: User.current!).find { result in + switch result { + case .success(let scores): + print("Found related scores: \(scores)") + case .failure(let error): + print("Error finding scores: \(error)") + } + } +} catch { + print(error) +} + +//: Example: try relation.remove(<#T##key: String##String#>, objects: <#T##[ParseObject]#>) PlaygroundPage.current.finishExecution() //: [Next](@next) diff --git a/Sources/ParseSwift/Types/ParseOperation.swift b/Sources/ParseSwift/Types/ParseOperation.swift index 0985390c3..29212dfd5 100644 --- a/Sources/ParseSwift/Types/ParseOperation.swift +++ b/Sources/ParseSwift/Types/ParseOperation.swift @@ -18,7 +18,7 @@ import Foundation */ public struct ParseOperation: Savable where T: ParseObject { - var target: T? + var target: T var operations = [String: Encodable]() public init(target: T) { @@ -34,13 +34,10 @@ public struct ParseOperation: Savable where T: ParseObject { */ public func set(_ key: (String, WritableKeyPath), value: W) throws -> Self where W: Encodable { - guard let target = self.target else { - throw ParseError(code: .unknownError, message: "Target shouldn't be nil") - } var mutableOperation = self if !target[keyPath: key.1].isEqual(value) { mutableOperation.operations[key.0] = value - mutableOperation.target?[keyPath: key.1] = value + mutableOperation.target[keyPath: key.1] = value } return mutableOperation } @@ -54,12 +51,9 @@ public struct ParseOperation: Savable where T: ParseObject { */ public func forceSet(_ key: (String, WritableKeyPath), value: W) throws -> Self where W: Encodable { - guard self.target != nil else { - throw ParseError(code: .unknownError, message: "Target shouldn't be nil") - } var mutableOperation = self mutableOperation.operations[key.0] = value - mutableOperation.target?[keyPath: key.1] = value + mutableOperation.target[keyPath: key.1] = value return mutableOperation } @@ -100,14 +94,11 @@ public struct ParseOperation: Savable where T: ParseObject { */ public func addUnique(_ key: (String, WritableKeyPath), objects: [V]) throws -> Self where V: Encodable, V: Hashable { - guard let target = self.target else { - throw ParseError(code: .unknownError, message: "Target shouldn't be nil") - } var mutableOperation = self mutableOperation.operations[key.0] = AddUnique(objects: objects) var values = target[keyPath: key.1] values.append(contentsOf: objects) - mutableOperation.target?[keyPath: key.1] = Array(Set(values)) + mutableOperation.target[keyPath: key.1] = Array(Set(values)) return mutableOperation } @@ -121,14 +112,11 @@ public struct ParseOperation: Savable where T: ParseObject { */ public func addUnique(_ key: (String, WritableKeyPath), objects: [V]) throws -> Self where V: Encodable, V: Hashable { - guard let target = self.target else { - throw ParseError(code: .unknownError, message: "Target shouldn't be nil") - } var mutableOperation = self mutableOperation.operations[key.0] = AddUnique(objects: objects) var values = target[keyPath: key.1] ?? [] values.append(contentsOf: objects) - mutableOperation.target?[keyPath: key.1] = Array(Set(values)) + mutableOperation.target[keyPath: key.1] = Array(Set(values)) return mutableOperation } @@ -154,14 +142,11 @@ public struct ParseOperation: Savable where T: ParseObject { */ public func add(_ key: (String, WritableKeyPath), objects: [V]) throws -> Self where V: Encodable { - guard let target = self.target else { - throw ParseError(code: .unknownError, message: "Target shouldn't be nil") - } var mutableOperation = self mutableOperation.operations[key.0] = Add(objects: objects) var values = target[keyPath: key.1] values.append(contentsOf: objects) - mutableOperation.target?[keyPath: key.1] = values + mutableOperation.target[keyPath: key.1] = values return mutableOperation } @@ -174,14 +159,11 @@ public struct ParseOperation: Savable where T: ParseObject { */ public func add(_ key: (String, WritableKeyPath), objects: [V]) throws -> Self where V: Encodable { - guard let target = self.target else { - throw ParseError(code: .unknownError, message: "Target shouldn't be nil") - } var mutableOperation = self mutableOperation.operations[key.0] = Add(objects: objects) var values = target[keyPath: key.1] ?? [] values.append(contentsOf: objects) - mutableOperation.target?[keyPath: key.1] = values + mutableOperation.target[keyPath: key.1] = values return mutableOperation } @@ -207,14 +189,11 @@ public struct ParseOperation: Savable where T: ParseObject { */ public func addRelation(_ key: (String, WritableKeyPath), objects: [V]) throws -> Self where V: ParseObject { - guard let target = self.target else { - throw ParseError(code: .unknownError, message: "Target shouldn't be nil") - } var mutableOperation = self mutableOperation.operations[key.0] = try AddRelation(objects: objects) var values = target[keyPath: key.1] values.append(contentsOf: objects) - mutableOperation.target?[keyPath: key.1] = values + mutableOperation.target[keyPath: key.1] = values return mutableOperation } @@ -227,14 +206,11 @@ public struct ParseOperation: Savable where T: ParseObject { */ public func addRelation(_ key: (String, WritableKeyPath), objects: [V]) throws -> Self where V: ParseObject { - guard let target = self.target else { - throw ParseError(code: .unknownError, message: "Target shouldn't be nil") - } var mutableOperation = self mutableOperation.operations[key.0] = try AddRelation(objects: objects) var values = target[keyPath: key.1] ?? [] values.append(contentsOf: objects) - mutableOperation.target?[keyPath: key.1] = values + mutableOperation.target[keyPath: key.1] = values return mutableOperation } @@ -262,9 +238,6 @@ public struct ParseOperation: Savable where T: ParseObject { */ public func remove(_ key: (String, WritableKeyPath), objects: [V]) throws -> Self where V: Encodable, V: Hashable { - guard let target = self.target else { - throw ParseError(code: .unknownError, message: "Target shouldn't be nil") - } var mutableOperation = self mutableOperation.operations[key.0] = Remove(objects: objects) let values = target[keyPath: key.1] @@ -272,7 +245,7 @@ public struct ParseOperation: Savable where T: ParseObject { objects.forEach { set.remove($0) } - mutableOperation.target?[keyPath: key.1] = Array(set) + mutableOperation.target[keyPath: key.1] = Array(set) return mutableOperation } @@ -286,9 +259,6 @@ public struct ParseOperation: Savable where T: ParseObject { */ public func remove(_ key: (String, WritableKeyPath), objects: [V]) throws -> Self where V: Encodable, V: Hashable { - guard let target = self.target else { - throw ParseError(code: .unknownError, message: "Target shouldn't be nil") - } var mutableOperation = self mutableOperation.operations[key.0] = Remove(objects: objects) let values = target[keyPath: key.1] @@ -296,7 +266,7 @@ public struct ParseOperation: Savable where T: ParseObject { objects.forEach { set.remove($0) } - mutableOperation.target?[keyPath: key.1] = Array(set) + mutableOperation.target[keyPath: key.1] = Array(set) return mutableOperation } @@ -309,9 +279,6 @@ public struct ParseOperation: Savable where T: ParseObject { - returns: The updated operations. */ public func removeRelation(_ key: String, objects: [W]) throws -> Self where W: ParseObject { - guard self.target != nil else { - throw ParseError(code: .unknownError, message: "Target shouldn't be nil") - } var mutableOperation = self mutableOperation.operations[key] = try RemoveRelation(objects: objects) return mutableOperation @@ -327,9 +294,6 @@ public struct ParseOperation: Savable where T: ParseObject { */ public func removeRelation(_ key: (String, WritableKeyPath), objects: [V]) throws -> Self where V: ParseObject { - guard let target = self.target else { - throw ParseError(code: .unknownError, message: "Target shouldn't be nil") - } var mutableOperation = self mutableOperation.operations[key.0] = try RemoveRelation(objects: objects) let values = target[keyPath: key.1] @@ -337,7 +301,7 @@ public struct ParseOperation: Savable where T: ParseObject { objects.forEach { set.remove($0) } - mutableOperation.target?[keyPath: key.1] = Array(set) + mutableOperation.target[keyPath: key.1] = Array(set) return mutableOperation } @@ -351,9 +315,6 @@ public struct ParseOperation: Savable where T: ParseObject { */ public func removeRelation(_ key: (String, WritableKeyPath), objects: [V]) throws -> Self where V: ParseObject { - guard let target = self.target else { - throw ParseError(code: .unknownError, message: "Target shouldn't be nil") - } var mutableOperation = self mutableOperation.operations[key.0] = try RemoveRelation(objects: objects) let values = target[keyPath: key.1] @@ -361,7 +322,7 @@ public struct ParseOperation: Savable where T: ParseObject { objects.forEach { set.remove($0) } - mutableOperation.target?[keyPath: key.1] = Array(set) + mutableOperation.target[keyPath: key.1] = Array(set) return mutableOperation } @@ -385,7 +346,7 @@ public struct ParseOperation: Savable where T: ParseObject { public func unset(_ key: (String, WritableKeyPath)) -> Self where V: Encodable { var mutableOperation = self mutableOperation.operations[key.0] = Delete() - mutableOperation.target?[keyPath: key.1] = nil + mutableOperation.target[keyPath: key.1] = nil return mutableOperation } @@ -410,9 +371,6 @@ extension ParseOperation { - returns: Returns saved `ParseObject`. */ public func save(options: API.Options = []) throws -> T { - guard let target = self.target else { - throw ParseError(code: .unknownError, message: "Target shouldn't be nil.") - } if !target.isSaved { throw ParseError(code: .missingObjectId, message: "ParseObject isn't saved.") } @@ -433,13 +391,6 @@ extension ParseOperation { callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void ) { - guard let target = self.target else { - callbackQueue.async { - let error = ParseError(code: .unknownError, message: "Target shouldn't be nil.") - completion(.failure(error)) - } - return - } if !target.isSaved { callbackQueue.async { let error = ParseError(code: .missingObjectId, message: "ParseObject isn't saved.") @@ -461,11 +412,8 @@ extension ParseOperation { } func saveCommand() throws -> API.NonParseBodyCommand, T> { - guard let target = self.target else { - throw ParseError(code: .unknownError, message: "Target shouldn't be nil") - } - return API.NonParseBodyCommand(method: .PUT, path: target.endpoint, body: self) { - try ParseCoding.jsonDecoder().decode(UpdateResponse.self, from: $0).apply(to: target) + API.NonParseBodyCommand(method: .PUT, path: target.endpoint, body: self) { + try ParseCoding.jsonDecoder().decode(UpdateResponse.self, from: $0).apply(to: self.target) } } } diff --git a/Sources/ParseSwift/Types/ParseRelation.swift b/Sources/ParseSwift/Types/ParseRelation.swift index e2ea98015..b9e958b48 100644 --- a/Sources/ParseSwift/Types/ParseRelation.swift +++ b/Sources/ParseSwift/Types/ParseRelation.swift @@ -12,14 +12,14 @@ import Foundation The `ParseRelation` class that is used to access all of the children of a many-to-many relationship. Each instance of `ParseRelation` is associated with a particular parent object and key. - In most cases, you do not need to create an instance of `ParseOperation` directly as it can be + In most cases, you do not need to create an instance of `ParseRelation` directly as it can be indirectly created from any `ParseObject` by using the respective `relation` property. */ -public struct ParseRelation: Codable, Hashable where T: ParseObject { +public struct ParseRelation: Encodable, Hashable where T: ParseObject { internal let __type: String = "Relation" // swiftlint:disable:this identifier_name /// The parent `ParseObject` - public var parent: T? + public var parent: T /// The name of the class of the target child objects. public var className: String? @@ -28,28 +28,39 @@ public struct ParseRelation: Codable, Hashable where T: ParseObject { /** Create a `ParseRelation` with a specific parent and key. + - parameters: + - parent: The parent `ParseObject`. + - key: The key for the relation. + */ + public init(parent: T, key: String? = nil) { + self.parent = parent + self.key = key + } + + /** + Create a `ParseRelation` with a specific parent, key, and className. - parameters: - parent: The parent `ParseObject`. - key: The key for the relation. - className: The name of the child class for the relation. */ - public init(parent: T, key: String? = nil, className: String? = nil) { + public init(parent: T, key: String? = nil, className: String) { self.parent = parent self.key = key self.className = className } /** - Create a `ParseRelation` with a specific parent and child. + Create a `ParseRelation` with a specific parent, key, and child object. - parameters: - parent: The parent `ParseObject`. - key: The key for the relation. - child: The child `ParseObject`. */ - public init(parent: T, key: String? = nil, child: U? = nil) where U: ParseObject { + public init(parent: T, key: String? = nil, child: U) where U: ParseObject { self.parent = parent self.key = key - self.className = child?.className + self.className = child.className } enum CodingKeys: String, CodingKey { @@ -65,9 +76,6 @@ public struct ParseRelation: Codable, Hashable where T: ParseObject { - throws: An error of type `ParseError`. */ public func add(_ key: String, objects: [U]) throws -> ParseOperation where U: ParseObject { - guard let parent = parent else { - throw ParseError(code: .unknownError, message: "ParseRelation must have the parent set before removing.") - } if let currentKey = self.key { if currentKey != key { throw ParseError(code: .unknownError, message: "All objects have be related to the same key.") @@ -80,6 +88,20 @@ public struct ParseRelation: Codable, Hashable where T: ParseObject { return try parent.operation.addRelation(key, objects: objects) } + /** + Adds a relation to the respective `ParseObject`'s with using the `key` for this `ParseRelation`. + - parameters: + - objects: An array of `ParseObject`'s to add relation to. + - throws: An error of type `ParseError`. + */ + public func add(_ objects: [U]) throws -> ParseOperation where U: ParseObject { + guard let key = self.key else { + throw ParseError(code: .unknownError, + message: "ParseRelation must have the key set before querying.") + } + return try add(key, objects: objects) + } + /** Removes a relation to the respective objects. - parameters: @@ -88,9 +110,6 @@ public struct ParseRelation: Codable, Hashable where T: ParseObject { - throws: An error of type `ParseError`. */ public func remove(_ key: String, objects: [U]) throws -> ParseOperation where U: ParseObject { - guard let parent = parent else { - throw ParseError(code: .unknownError, message: "ParseRelation must have the parent set before removing.") - } if let currentKey = self.key { if currentKey != key { throw ParseError(code: .unknownError, message: "All objects have be related to the same key.") @@ -103,17 +122,37 @@ public struct ParseRelation: Codable, Hashable where T: ParseObject { } /** - Returns a `Query` that is limited to objects in this relation. + Removes a relation to the respective objects using the `key` for this `ParseRelation`. + - parameters: + - objects: An array of `ParseObject`'s to add relation to. + - throws: An error of type `ParseError`. + */ + public func remove(_ objects: [U]) throws -> ParseOperation where U: ParseObject { + guard let key = self.key else { + throw ParseError(code: .unknownError, + message: "ParseRelation must have the key set before querying.") + } + return try remove(key, objects: objects) + } + + /** + Returns a `Query` that is limited to objects for a specific `key` and `parent` in this relation. + - parameter key: The key for the relation. + - parameter parent: The child class for the relation. + - throws: An error of type `ParseError`. + - returns: A relation query. + */ + public func query(_ key: String, parent: U) throws -> Query where U: ParseObject { + Query(related(key: key, object: try parent.toPointer())) + } + + /** + Returns a `Query` that is limited to the key and objects in this relation. - parameter child: The child class for the relation. - throws: An error of type `ParseError`. - returns: A relation query. */ public func query(_ child: U) throws -> Query where U: ParseObject { - - guard let parent = self.parent else { - throw ParseError(code: .unknownError, - message: "ParseRelation must have the parent set before querying.") - } guard let key = self.key else { throw ParseError(code: .unknownError, message: "ParseRelation must have the key set before querying.") @@ -125,16 +164,25 @@ public struct ParseRelation: Codable, Hashable where T: ParseObject { return Query(related(key: key, object: try parent.toPointer())) } + /** + Returns a `Query` that is limited to objects for a specific `key` and `child` in this relation. + - parameter key: The key for the relation. + - parameter child: The child class for the relation. + - throws: An error of type `ParseError`. + - returns: A relation query. + */ + public func query(_ key: String, child: U) throws -> Query where U: ParseObject { + try Self(parent: parent, key: key).query(child) + } + func isSameClass(_ objects: [U]) -> Bool where U: ParseObject { guard let first = objects.first?.className else { - return true + return false } if className != nil { if className != first { return false } - } else { - return false } let sameClassObjects = objects.filter({ $0.className == first }) return sameClassObjects.count == objects.count @@ -211,7 +259,7 @@ public extension ParseObject { - parameter className: The name of the child class for the relation. - returns: A new `ParseRelation`. */ - func relation(_ key: String, className: String? = nil) -> ParseRelation { + func relation(_ key: String, className: String) -> ParseRelation { ParseRelation(parent: self, key: key, className: className) } @@ -221,7 +269,7 @@ public extension ParseObject { - parameter child: The child `ParseObject`. - returns: A new `ParseRelation`. */ - func relation(_ key: String, child: U? = nil) -> ParseRelation where U: ParseObject { + func relation(_ key: String, child: U) -> ParseRelation where U: ParseObject { ParseRelation(parent: self, key: key, child: child) } } diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index a280338cd..7e3dbb09a 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -480,7 +480,7 @@ public func polygonContains(key: String, point: ParseGeoPoint) -> QueryConstrain string using Full Text Search. - parameter key: The key to be constrained. - parameter text: The substring that the value must contain. - - returns: The same instance of `Query` as the receiver. + - returns: The resulting `QueryConstraint`. */ public func matchesText(key: String, text: String) -> QueryConstraint { let dictionary = [QueryConstraint.Comparator.search.rawValue: [QueryConstraint.Comparator.term.rawValue: text]] @@ -495,7 +495,7 @@ public func matchesText(key: String, text: String) -> QueryConstraint { - parameter modifiers: Any of the following supported PCRE modifiers (defaults to nil): - `i` - Case insensitive search - `m` - Search across multiple lines of input - - returns: The same instance of `Query` as the receiver. + - returns: The resulting `QueryConstraint`. */ public func matchesRegex(key: String, regex: String, modifiers: String? = nil) -> QueryConstraint { @@ -523,7 +523,7 @@ private func regexStringForString(_ inputString: String) -> String { - parameter modifiers: Any of the following supported PCRE modifiers (defaults to nil): - `i` - Case insensitive search - `m` - Search across multiple lines of input - - returns: The same instance of `Query` as the receiver. + - returns: The resulting `QueryConstraint`. */ public func containsString(key: String, substring: String, modifiers: String? = nil) -> QueryConstraint { let regex = regexStringForString(substring) @@ -538,7 +538,7 @@ public func containsString(key: String, substring: String, modifiers: String? = - parameter modifiers: Any of the following supported PCRE modifiers (defaults to nil): - `i` - Case insensitive search - `m` - Search across multiple lines of input - - returns: The same instance of `Query` as the receiver. + - returns: The resulting `QueryConstraint`. */ public func hasPrefix(key: String, prefix: String, modifiers: String? = nil) -> QueryConstraint { let regex = "^\(regexStringForString(prefix))" @@ -553,7 +553,7 @@ public func hasPrefix(key: String, prefix: String, modifiers: String? = nil) -> - parameter modifiers: Any of the following supported PCRE modifiers (defaults to nil): - `i` - Case insensitive search - `m` - Search across multiple lines of input - - returns: The same instance of `Query` as the receiver. + - returns: The resulting `QueryConstraint`. */ public func hasSuffix(key: String, suffix: String, modifiers: String? = nil) -> QueryConstraint { let regex = "\(regexStringForString(suffix))$" @@ -563,7 +563,7 @@ public func hasSuffix(key: String, suffix: String, modifiers: String? = nil) -> /** Add a constraint that requires a particular key exists. - parameter key: The key that should exist. - - returns: The same instance of `Query` as the receiver. + - returns: The resulting `QueryConstraint`. */ public func exists(key: String) -> QueryConstraint { .init(key: key, value: true, comparator: .exists) @@ -572,12 +572,20 @@ public func exists(key: String) -> QueryConstraint { /** Add a constraint that requires a key not exist. - parameter key: The key that should not exist. - - returns: The same instance of `Query` as the receiver. + - returns: The resulting `QueryConstraint`. */ public func doesNotExist(key: String) -> QueryConstraint { .init(key: key, value: false, comparator: .exists) } +internal struct RelatedKeyCondition: Encodable { + let key: String +} + +internal struct RelatedObjectCondition : Encodable where T: ParseObject { + let object: Pointer +} + internal struct RelatedCondition : Encodable where T: ParseObject { let object: Pointer let key: String @@ -587,7 +595,7 @@ internal struct RelatedCondition : Encodable where T: ParseObject { Add a constraint that requires a key is related. - parameter key: The key that should be related. - parameter object: The object that should be related. - - returns: The same instance of `Query` as the receiver. + - returns: The resulting `QueryConstraint`. - throws: An error of type `ParseError`. */ public func related (key: String, object: T) throws -> QueryConstraint where T: ParseObject { @@ -600,13 +608,46 @@ public func related (key: String, object: T) throws -> QueryConstraint where Add a constraint that requires a key is related. - parameter key: The key that should be related. - parameter object: The pointer object that should be related. - - returns: The same instance of `Query` as the receiver. + - returns: The resulting `QueryConstraint`. */ public func related (key: String, object: Pointer) -> QueryConstraint where T: ParseObject { let condition = RelatedCondition(object: object, key: key) return .init(key: QueryConstraint.Comparator.relatedTo.rawValue, value: condition) } +/** + Add a constraint that requires a key is related. + - parameter key: The key that should be related. + - returns: The resulting `QueryConstraint`. + - throws: An error of type `ParseError`. + */ +public func related(key: String) -> QueryConstraint { + let condition = RelatedKeyCondition(key: key) + return .init(key: QueryConstraint.Comparator.relatedTo.rawValue, value: condition) +} + +/** + Add a constraint that requires a key is related. + - parameter object: The object that should be related. + - returns: The resulting `QueryConstraint`. + - throws: An error of type `ParseError`. + */ +public func related (object: T) throws -> QueryConstraint where T: ParseObject { + let pointer = try object.toPointer() + let condition = RelatedObjectCondition(object: pointer) + return .init(key: QueryConstraint.Comparator.relatedTo.rawValue, value: condition) +} + +/** + Add a constraint that requires a key is related. + - parameter object: The pointer object that should be related. + - returns: The resulting `QueryConstraint`. + */ +public func related (object: Pointer) -> QueryConstraint where T: ParseObject { + let condition = RelatedObjectCondition(object: object) + return .init(key: QueryConstraint.Comparator.relatedTo.rawValue, value: condition) +} + internal struct QueryWhere: Encodable, Equatable { var constraints = [String: [QueryConstraint]]() diff --git a/Tests/ParseSwiftTests/ParseOperationTests.swift b/Tests/ParseSwiftTests/ParseOperationTests.swift index eaf01aa0a..3ec55baab 100644 --- a/Tests/ParseSwiftTests/ParseOperationTests.swift +++ b/Tests/ParseSwiftTests/ParseOperationTests.swift @@ -502,7 +502,7 @@ class ParseOperationTests: XCTestCase { .encode(operations) let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) XCTAssertEqual(decoded, expected) - XCTAssertEqual(operations.target?.score, 15) + XCTAssertEqual(operations.target.score, 15) var level = Level(level: 12) level.members = ["hello", "world"] let operations2 = try score.operation.set(("previous", \.previous), value: [level]) @@ -511,7 +511,7 @@ class ParseOperationTests: XCTestCase { .encode(operations2) let decoded2 = try XCTUnwrap(String(data: encoded2, encoding: .utf8)) XCTAssertEqual(decoded2, expected2) - XCTAssertEqual(operations2.target?.previous, [level]) + XCTAssertEqual(operations2.target.previous, [level]) } func testObjectIdSet() throws { @@ -524,7 +524,7 @@ class ParseOperationTests: XCTestCase { .encode(operations) let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) XCTAssertEqual(decoded, expected) - XCTAssertEqual(operations.target?.objectId, "test") + XCTAssertEqual(operations.target.objectId, "test") var level = Level(level: 12) level.members = ["hello", "world"] score.previous = [level] @@ -534,7 +534,7 @@ class ParseOperationTests: XCTestCase { .encode(operations2) let decoded2 = try XCTUnwrap(String(data: encoded2, encoding: .utf8)) XCTAssertEqual(decoded2, expected2) - XCTAssertEqual(operations2.target?.previous, [level]) + XCTAssertEqual(operations2.target.previous, [level]) } #endif diff --git a/Tests/ParseSwiftTests/ParseRelationTests.swift b/Tests/ParseSwiftTests/ParseRelationTests.swift index 7aa763c36..8be8c4b3d 100644 --- a/Tests/ParseSwiftTests/ParseRelationTests.swift +++ b/Tests/ParseSwiftTests/ParseRelationTests.swift @@ -194,6 +194,25 @@ class ParseRelationTests: XCTestCase { XCTAssertEqual(decoded, expected) } + func testAddOperationsNoKey() throws { + var score = GameScore(score: 10) + let objectId = "hello" + score.objectId = objectId + var relation = score.relation + var level = Level(level: 1) + level.objectId = "nice" + relation.className = level.className + + XCTAssertThrowsError(try relation.add([level])) + relation.key = "level" + let operation = try relation.add([level]) + // swiftlint:disable:next line_length + let expected = "{\"level\":{\"objects\":[{\"__type\":\"Pointer\",\"className\":\"Level\",\"objectId\":\"nice\"}],\"__op\":\"AddRelation\"}}" + let encoded = try ParseCoding.jsonEncoder().encode(operation) + let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) + XCTAssertEqual(decoded, expected) + } + func testAddOperationsKeyCheck() throws { var score = GameScore(score: 10) let objectId = "hello" @@ -252,6 +271,25 @@ class ParseRelationTests: XCTestCase { XCTAssertEqual(decoded, expected) } + func testRemoveOperationsNoKey() throws { + var score = GameScore(score: 10) + let objectId = "hello" + score.objectId = objectId + var relation = score.relation + var level = Level(level: 1) + level.objectId = "nice" + relation.className = level.className + + XCTAssertThrowsError(try relation.remove([level])) + relation.key = "level" + let operation = try relation.remove([level]) + // swiftlint:disable:next line_length + let expected = "{\"level\":{\"objects\":[{\"__type\":\"Pointer\",\"className\":\"Level\",\"objectId\":\"nice\"}],\"__op\":\"RemoveRelation\"}}" + let encoded = try ParseCoding.jsonEncoder().encode(operation) + let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) + XCTAssertEqual(decoded, expected) + } + func testRemoveOperationsKeyCheck() throws { var score = GameScore(score: 10) let objectId = "hello" @@ -279,20 +317,38 @@ class ParseRelationTests: XCTestCase { level.objectId = "nice" relation.className = level.className - //No Key, this should throw + // No Key, this should throw XCTAssertThrowsError(try relation.query(level)) - //Wrong child for the relation, should throw + // No child for the relation, should throw XCTAssertThrowsError(try relation.query(score)) - relation.key = "level" + // Wrong child for the relation, should throw + relation.key = "naw" + XCTAssertThrowsError(try relation.query(score)) + + relation.key = "levels" let query = try relation.query(level) // swiftlint:disable:next line_length - let expected = "{\"limit\":100,\"skip\":0,\"_method\":\"GET\",\"where\":{\"$relatedTo\":{\"key\":\"level\",\"object\":{\"__type\":\"Pointer\",\"className\":\"GameScore\",\"objectId\":\"hello\"}}}}" + let expected = "{\"limit\":100,\"skip\":0,\"_method\":\"GET\",\"where\":{\"$relatedTo\":{\"key\":\"levels\",\"object\":{\"__type\":\"Pointer\",\"className\":\"GameScore\",\"objectId\":\"hello\"}}}}" let encoded = try ParseCoding.jsonEncoder().encode(query) let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) XCTAssertEqual(decoded, expected) + + let query2 = try relation.query("wow", child: level) + // swiftlint:disable:next line_length + let expected2 = "{\"limit\":100,\"skip\":0,\"_method\":\"GET\",\"where\":{\"$relatedTo\":{\"key\":\"wow\",\"object\":{\"__type\":\"Pointer\",\"className\":\"GameScore\",\"objectId\":\"hello\"}}}}" + let encoded2 = try ParseCoding.jsonEncoder().encode(query2) + let decoded2 = try XCTUnwrap(String(data: encoded2, encoding: .utf8)) + XCTAssertEqual(decoded2, expected2) + + let query3 = try level.relation.query("levels", parent: score) + // swiftlint:disable:next line_length + let expected3 = "{\"limit\":100,\"skip\":0,\"_method\":\"GET\",\"where\":{\"$relatedTo\":{\"key\":\"levels\",\"object\":{\"__type\":\"Pointer\",\"className\":\"GameScore\",\"objectId\":\"hello\"}}}}" + let encoded3 = try ParseCoding.jsonEncoder().encode(query3) + let decoded3 = try XCTUnwrap(String(data: encoded3, encoding: .utf8)) + XCTAssertEqual(decoded3, expected3) } } #endif diff --git a/Tests/ParseSwiftTests/ParseRoleTests.swift b/Tests/ParseSwiftTests/ParseRoleTests.swift index cae2e6599..dfa5b3b5f 100644 --- a/Tests/ParseSwiftTests/ParseRoleTests.swift +++ b/Tests/ParseSwiftTests/ParseRoleTests.swift @@ -181,6 +181,31 @@ class ParseRoleTests: XCTestCase { let decoded2 = try XCTUnwrap(String(data: encoded2, encoding: .utf8)) XCTAssertEqual(decoded2, expected2) } + + func testUserAddOperationNoKey() throws { + var acl = ParseACL() + acl.publicWrite = false + acl.publicRead = true + + let role = try Role(name: "Administrator", acl: acl) + var userRoles = role.users + userRoles.key = nil + let expected = "{\"className\":\"_User\",\"__type\":\"Relation\"}" + let encoded = try ParseCoding.jsonEncoder().encode(userRoles) + let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) + XCTAssertEqual(decoded, expected) + XCTAssertNil(userRoles.key) + + var user = User() + user.objectId = "heel" + let operation = try userRoles.add([user]) + + // swiftlint:disable:next line_length + let expected2 = "{\"users\":{\"objects\":[{\"__type\":\"Pointer\",\"className\":\"_User\",\"objectId\":\"heel\"}],\"__op\":\"AddRelation\"}}" + let encoded2 = try ParseCoding.jsonEncoder().encode(operation) + let decoded2 = try XCTUnwrap(String(data: encoded2, encoding: .utf8)) + XCTAssertEqual(decoded2, expected2) + } #endif func testUserRemoveIncorrectClassKeyError() throws { @@ -233,6 +258,31 @@ class ParseRoleTests: XCTestCase { let decoded2 = try XCTUnwrap(try XCTUnwrap(String(data: encoded2, encoding: .utf8))) XCTAssertEqual(decoded2, expected2) } + + func testUserRemoveOperationNoKey() throws { + var acl = ParseACL() + acl.publicWrite = false + acl.publicRead = true + + let role = try Role(name: "Administrator", acl: acl) + var userRoles = role.users + userRoles.key = nil + let expected = "{\"className\":\"_User\",\"__type\":\"Relation\"}" + let encoded = try ParseCoding.jsonEncoder().encode(userRoles) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + XCTAssertNil(userRoles.key) + + var user = User() + user.objectId = "heel" + let operation = try userRoles.remove([user]) + + // swiftlint:disable:next line_length + let expected2 = "{\"users\":{\"objects\":[{\"__type\":\"Pointer\",\"className\":\"_User\",\"objectId\":\"heel\"}],\"__op\":\"RemoveRelation\"}}" + let encoded2 = try ParseCoding.jsonEncoder().encode(operation) + let decoded2 = try XCTUnwrap(try XCTUnwrap(String(data: encoded2, encoding: .utf8))) + XCTAssertEqual(decoded2, expected2) + } #endif func testRoleAddIncorrectClassKeyError() throws { @@ -285,6 +335,31 @@ class ParseRoleTests: XCTestCase { let decoded2 = try XCTUnwrap(String(data: encoded2, encoding: .utf8)) XCTAssertEqual(decoded2, expected2) } + + func testRoleAddOperationNoKey() throws { + var acl = ParseACL() + acl.publicWrite = false + acl.publicRead = true + + let role = try Role(name: "Administrator", acl: acl) + var roles = role.roles + roles.key = nil + let expected = "{\"className\":\"_Role\",\"__type\":\"Relation\"}" + let encoded = try ParseCoding.jsonEncoder().encode(roles) + let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) + XCTAssertEqual(decoded, expected) + XCTAssertNil(roles.key) + + var newRole = try Role(name: "Moderator", acl: acl) + newRole.objectId = "heel" + let operation = try roles.add([newRole]) + + // swiftlint:disable:next line_length + let expected2 = "{\"roles\":{\"objects\":[{\"__type\":\"Pointer\",\"className\":\"_Role\",\"objectId\":\"heel\"}],\"__op\":\"AddRelation\"}}" + let encoded2 = try ParseCoding.jsonEncoder().encode(operation) + let decoded2 = try XCTUnwrap(String(data: encoded2, encoding: .utf8)) + XCTAssertEqual(decoded2, expected2) + } #endif func testRoleRemoveIncorrectClassKeyError() throws { @@ -338,6 +413,31 @@ class ParseRoleTests: XCTestCase { XCTAssertEqual(decoded2, expected2) } + func testRoleRemoveOperationNoKey() throws { + var acl = ParseACL() + acl.publicWrite = false + acl.publicRead = true + + let role = try Role(name: "Administrator", acl: acl) + var roles = role.roles + roles.key = nil + let expected = "{\"className\":\"_Role\",\"__type\":\"Relation\"}" + let encoded = try ParseCoding.jsonEncoder().encode(roles) + let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) + XCTAssertEqual(decoded, expected) + XCTAssertNil(roles.key) + + var newRole = try Role(name: "Moderator", acl: acl) + newRole.objectId = "heel" + let operation = try roles.remove([newRole]) + + // swiftlint:disable:next line_length + let expected2 = "{\"roles\":{\"objects\":[{\"__type\":\"Pointer\",\"className\":\"_Role\",\"objectId\":\"heel\"}],\"__op\":\"RemoveRelation\"}}" + let encoded2 = try ParseCoding.jsonEncoder().encode(operation) + let decoded2 = try XCTUnwrap(String(data: encoded2, encoding: .utf8)) + XCTAssertEqual(decoded2, expected2) + } + func testUserQuery() throws { var acl = ParseACL() acl.publicWrite = false