diff --git a/ParseSwift.playground/Pages/8 - Pointers.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/8 - Pointers.xcplaygroundpage/Contents.swift index 713a3aed1..2443840cc 100644 --- a/ParseSwift.playground/Pages/8 - Pointers.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/8 - Pointers.xcplaygroundpage/Contents.swift @@ -113,6 +113,7 @@ let otherBook1 = Book(title: "I like this book") let otherBook2 = Book(title: "I like this book also") var author2 = Author(name: "Bruce", book: newBook) author2.otherBooks = [otherBook1, otherBook2] + author2.save { result in switch result { case .success(let savedAuthorAndBook): @@ -121,7 +122,10 @@ author2.save { result in assert(savedAuthorAndBook.updatedAt != nil) assert(savedAuthorAndBook.otherBooks?.count == 2) - //: Notice the pointer objects haven't been updated on the client. + /*: + Notice the pointer objects haven't been updated on the + client.If you want the latest pointer objects, fetch and include them. + */ print("Saved \(savedAuthorAndBook)") case .failure(let error): @@ -262,5 +266,68 @@ do { print("\(error)") } +//: Batching saves with saved and unsaved pointer items. +var author3 = Author(name: "Logan", book: newBook) +let otherBook3 = Book(title: "I like this book") +let otherBook4 = Book(title: "I like this book also") +author3.otherBooks = [otherBook3, otherBook4] + +[author3].saveAll { result in + switch result { + case .success(let savedAuthorsAndBook): + savedAuthorsAndBook.forEach { eachResult in + switch eachResult { + case .success(let savedAuthorAndBook): + assert(savedAuthorAndBook.objectId != nil) + assert(savedAuthorAndBook.createdAt != nil) + assert(savedAuthorAndBook.updatedAt != nil) + assert(savedAuthorAndBook.otherBooks?.count == 2) + + /*: + Notice the pointer objects haven't been updated on the + client.If you want the latest pointer objects, fetch and include them. + */ + print("Saved \(savedAuthorAndBook)") + case .failure(let error): + assertionFailure("Error saving: \(error)") + } + } + + case .failure(let error): + assertionFailure("Error saving: \(error)") + } +} + +//: Batching saves with unsaved pointer items. +var newBook2 = Book(title: "world") +var author4 = Author(name: "Scott", book: newBook2) +author4.otherBooks = [otherBook3, otherBook4] + +[author4].saveAll { result in + switch result { + case .success(let savedAuthorsAndBook): + savedAuthorsAndBook.forEach { eachResult in + switch eachResult { + case .success(let savedAuthorAndBook): + assert(savedAuthorAndBook.objectId != nil) + assert(savedAuthorAndBook.createdAt != nil) + assert(savedAuthorAndBook.updatedAt != nil) + assert(savedAuthorAndBook.otherBooks?.count == 2) + + /*: + Notice the pointer objects haven't been updated on the + client.If you want the latest pointer objects, fetch and include them. + */ + print("Saved \(savedAuthorAndBook)") + case .failure(let error): + assertionFailure("Error saving: \(error)") + } + } + + case .failure(let error): + assertionFailure("Error saving: \(error)") + } +} + PlaygroundPage.current.finishExecution() //: [Next](@next) diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index 54601fe20..072782501 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -1619,8 +1619,8 @@ 7003963A25A288100052CB31 /* ParseLiveQueryTests.swift */, 703B091A26BDE774005A112F /* ParseObjectAsyncTests.swift */, 70C7DC2024D20F190050419B /* ParseObjectBatchTests.swift */, - 7044C1DE25C5C70D0011F6E7 /* ParseObjectCombineTests.swift */, 70732C592606CCAD000CAB81 /* ParseObjectCustomObjectIdTests.swift */, + 7044C1DE25C5C70D0011F6E7 /* ParseObjectCombineTests.swift */, 911DB13524C4FC100027F3C7 /* ParseObjectTests.swift */, 917BA43D2703E84000F8D747 /* ParseOperationAsyncTests.swift */, 7044C1EB25C5CC930011F6E7 /* ParseOperationCombineTests.swift */, diff --git a/Sources/ParseSwift/API/API+Command.swift b/Sources/ParseSwift/API/API+Command.swift index ebb7f6466..54f1ca134 100644 --- a/Sources/ParseSwift/API/API+Command.swift +++ b/Sources/ParseSwift/API/API+Command.swift @@ -576,7 +576,7 @@ internal extension API.Command where T: ParseObject { } } - let batchCommand = BatchCommandNoBody(requests: commands, transaction: transaction) + let batchCommand = BatchCommandEncodable(requests: commands, transaction: transaction) return RESTBatchCommandNoBodyType(method: .POST, path: .batch, body: batchCommand, mapper: mapper) } } diff --git a/Sources/ParseSwift/API/API+NonParseBodyCommand.swift b/Sources/ParseSwift/API/API+NonParseBodyCommand.swift index 64f94ef7c..2a44427fa 100644 --- a/Sources/ParseSwift/API/API+NonParseBodyCommand.swift +++ b/Sources/ParseSwift/API/API+NonParseBodyCommand.swift @@ -152,7 +152,7 @@ internal extension API.NonParseBodyCommand { internal extension API.NonParseBodyCommand { // MARK: Batch - Child Objects static func batch(objects: [ParseEncodable], - transaction: Bool) throws -> RESTBatchCommandTypeEncodable { + transaction: Bool) throws -> RESTBatchCommandTypeEncodablePointer { let batchCommands = try objects.compactMap { (object) -> API.BatchCommand? in guard var objectable = object as? Objectable else { return nil @@ -193,7 +193,6 @@ internal extension API.NonParseBodyCommand { guard let parseError = response.error else { return .failure(ParseError(code: .unknownError, message: "unknown error")) } - return .failure(parseError) } }) @@ -206,7 +205,7 @@ internal extension API.NonParseBodyCommand { } let batchCommand = BatchChildCommand(requests: batchCommands, transaction: transaction) - return RESTBatchCommandTypeEncodable(method: .POST, + return RESTBatchCommandTypeEncodablePointer(method: .POST, path: .batch, body: batchCommand, mapper: mapper) diff --git a/Sources/ParseSwift/API/BatchUtils.swift b/Sources/ParseSwift/API/BatchUtils.swift index 1c031598c..231442d1f 100644 --- a/Sources/ParseSwift/API/BatchUtils.swift +++ b/Sources/ParseSwift/API/BatchUtils.swift @@ -13,14 +13,14 @@ typealias ParseObjectBatchResponse = [(Result)] // swiftlint:disable line_length typealias RESTBatchCommandType = API.Command, ParseObjectBatchResponse> where T: ParseObject -typealias ParseObjectBatchCommandNoBody = BatchCommandNoBody +typealias ParseObjectBatchCommandNoBody = BatchCommandEncodable typealias ParseObjectBatchResponseNoBody = [(Result)] typealias RESTBatchCommandNoBodyType = API.NonParseBodyCommand, ParseObjectBatchResponseNoBody> where T: Encodable -typealias ParseObjectBatchCommandEncodable = BatchChildCommand where T: Encodable -typealias ParseObjectBatchResponseEncodable = [(Result)] +typealias ParseObjectBatchCommandEncodablePointer = BatchChildCommand where T: Encodable +typealias ParseObjectBatchResponseEncodablePointer = [(Result)] // swiftlint:disable line_length -typealias RESTBatchCommandTypeEncodable = API.NonParseBodyCommand, ParseObjectBatchResponseEncodable> where T: Encodable +typealias RESTBatchCommandTypeEncodablePointer = API.NonParseBodyCommand, ParseObjectBatchResponseEncodablePointer> where T: Encodable // swiftlint:enable line_length internal struct BatchCommand: ParseEncodable where T: ParseEncodable { @@ -28,7 +28,7 @@ internal struct BatchCommand: ParseEncodable where T: ParseEncodable { var transaction: Bool } -internal struct BatchCommandNoBody: Encodable where T: Encodable { +internal struct BatchCommandEncodable: Encodable where T: Encodable { let requests: [API.NonParseBodyCommand] var transaction: Bool } diff --git a/Sources/ParseSwift/Coding/ParseEncoder.swift b/Sources/ParseSwift/Coding/ParseEncoder.swift index 3ecf5b0a3..d59c59863 100644 --- a/Sources/ParseSwift/Coding/ParseEncoder.swift +++ b/Sources/ParseSwift/Coding/ParseEncoder.swift @@ -340,11 +340,7 @@ internal class _ParseEncoder: JSONEncoder, Encoder { throw ParseError(code: .unknownError, message: "Found a circular dependency when encoding.") } - if !self.collectChildren && codingPath.count > 0 { - valueToEncode = value - } else { - valueToEncode = pointer - } + valueToEncode = pointer } else if let object = value as? Objectable { if let pointer = try? PointerType(object) { if let uniquePointer = self.uniquePointer, @@ -352,11 +348,7 @@ internal class _ParseEncoder: JSONEncoder, Encoder { throw ParseError(code: .unknownError, message: "Found a circular dependency when encoding.") } - if !self.collectChildren && codingPath.count > 0 { - valueToEncode = value - } else { - valueToEncode = pointer - } + valueToEncode = pointer } else { var object = object if object.ACL == nil, @@ -959,7 +951,7 @@ extension _ParseEncoder { // swiftlint:disable:next force_cast return (value as! NSDecimalNumber) } else if value is _JSONStringDictionaryEncodableMarker { - //COREY: DON'T remove the force unwrap, it will crash the app + // COREY: DON'T remove the force cast, it will crash the app // swiftlint:disable:next force_cast return try self.box(value as! [String : Encodable]) } else if value is ParsePointer { diff --git a/Sources/ParseSwift/Objects/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift index b50e93ff4..49261cabd 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation.swift @@ -945,14 +945,17 @@ public extension Sequence where Element: ParseInstallation { options.insert(.cachePolicy(.reloadIgnoringLocalCacheData)) var childObjects = [String: PointerType]() var childFiles = [UUID: ParseFile]() + var commands = [API.Command]() var error: ParseError? - let installations = map { $0 } - for installation in installations { + try forEach { + let installation = $0 let group = DispatchGroup() group.enter() - installation.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, parseError) -> Void in - //If an error occurs, everything should be skipped + installation.ensureDeepSave(options: options, + // swiftlint:disable:next line_length + isShouldReturnIfChildObjectsFound: transaction) { (savedChildObjects, savedChildFiles, parseError) -> Void in + // If an error occurs, everything should be skipped if parseError != nil { error = parseError } @@ -984,12 +987,10 @@ public extension Sequence where Element: ParseInstallation { if let error = error { throw error } + commands.append(try installation.saveCommand(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig)) } var returnBatch = [(Result)]() - let commands = try map { - try $0.saveCommand(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig) - } let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit try canSendTransactions(transaction, objectCount: commands.count, batchLimit: batchLimit) let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) @@ -1171,16 +1172,17 @@ public extension Sequence where Element: ParseInstallation { var childObjects = [String: PointerType]() var childFiles = [UUID: ParseFile]() var error: ParseError? - + var commands = [API.Command]() let installations = map { $0 } + for installation in installations { let group = DispatchGroup() group.enter() installation .ensureDeepSave(options: options, // swiftlint:disable:next line_length - isShouldReturnIfChildObjectsFound: true) { (savedChildObjects, savedChildFiles, parseError) -> Void in - //If an error occurs, everything should be skipped + isShouldReturnIfChildObjectsFound: transaction) { (savedChildObjects, savedChildFiles, parseError) -> Void in + // If an error occurs, everything should be skipped if parseError != nil { error = parseError } @@ -1215,23 +1217,34 @@ public extension Sequence where Element: ParseInstallation { } return } + + do { + switch method { + case .save: + commands.append( + try installation.saveCommand(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig) + ) + case .create: + commands.append(installation.createCommand()) + case .replace: + commands.append(try installation.replaceCommand()) + case .update: + commands.append(try installation.updateCommand()) + } + } catch { + callbackQueue.async { + if let parseError = error as? ParseError { + completion(.failure(parseError)) + } else { + completion(.failure(.init(code: .unknownError, message: error.localizedDescription))) + } + } + return + } } do { var returnBatch = [(Result)]() - let commands: [API.Command]! - switch method { - case .save: - commands = try map { - try $0.saveCommand(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig) - } - case .create: - commands = map { $0.createCommand() } - case .replace: - commands = try map { try $0.replaceCommand() } - case .update: - commands = try map { try $0.updateCommand() } - } let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit try canSendTransactions(transaction, objectCount: commands.count, batchLimit: batchLimit) diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index f397cbbe8..23c3cea8c 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -267,14 +267,15 @@ transactions for this call. var childFiles = [UUID: ParseFile]() var error: ParseError? - let objects = map { $0 } - for object in objects { + var commands = [API.Command]() + try forEach { + let object = $0 let group = DispatchGroup() group.enter() object.ensureDeepSave(options: options, // swiftlint:disable:next line_length - isShouldReturnIfChildObjectsFound: true) { (savedChildObjects, savedChildFiles, parseError) -> Void in - //If an error occurs, everything should be skipped + isShouldReturnIfChildObjectsFound: transaction) { (savedChildObjects, savedChildFiles, parseError) -> Void in + // If an error occurs, everything should be skipped if parseError != nil { error = parseError } @@ -306,10 +307,10 @@ transactions for this call. if let error = error { throw error } + commands.append(try object.saveCommand(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig)) } var returnBatch = [(Result)]() - let commands = try map { try $0.saveCommand(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig) } let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit try canSendTransactions(transaction, objectCount: commands.count, batchLimit: batchLimit) let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) @@ -482,19 +483,18 @@ transactions for this call. autoreleaseFrequency: .inherit, target: nil) queue.sync { - var childObjects = [String: PointerType]() var childFiles = [UUID: ParseFile]() var error: ParseError? - + var commands = [API.Command]() let objects = map { $0 } for object in objects { let group = DispatchGroup() group.enter() object.ensureDeepSave(options: options, // swiftlint:disable:next line_length - isShouldReturnIfChildObjectsFound: true) { (savedChildObjects, savedChildFiles, parseError) -> Void in - //If an error occurs, everything should be skipped + isShouldReturnIfChildObjectsFound: transaction) { (savedChildObjects, savedChildFiles, parseError) -> Void in + // If an error occurs, everything should be skipped if parseError != nil { error = parseError } @@ -529,23 +529,34 @@ transactions for this call. } return } + + do { + switch method { + case .save: + commands.append( + try object.saveCommand(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig) + ) + case .create: + commands.append(object.createCommand()) + case .replace: + commands.append(try object.replaceCommand()) + case .update: + commands.append(try object.updateCommand()) + } + } catch { + callbackQueue.async { + if let parseError = error as? ParseError { + completion(.failure(parseError)) + } else { + completion(.failure(.init(code: .unknownError, message: error.localizedDescription))) + } + } + return + } } do { var returnBatch = [(Result)]() - let commands: [API.Command]! - switch method { - case .save: - commands = try map { - try $0.saveCommand(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig) - } - case .create: - commands = map { $0.createCommand() } - case .replace: - commands = try map { try $0.replaceCommand() } - case .update: - commands = try map { try $0.updateCommand() } - } let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit try canSendTransactions(transaction, objectCount: commands.count, batchLimit: batchLimit) diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index 8530c1982..b88e7d6c7 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -1363,15 +1363,17 @@ public extension Sequence where Element: ParseUser { var childObjects = [String: PointerType]() var childFiles = [UUID: ParseFile]() var error: ParseError? + var commands = [API.Command]() var options = options options.insert(.cachePolicy(.reloadIgnoringLocalCacheData)) - let users = map { $0 } - for user in users { + + try forEach { + let user = $0 let group = DispatchGroup() group.enter() user.ensureDeepSave(options: options, // swiftlint:disable:next line_length - isShouldReturnIfChildObjectsFound: true) { (savedChildObjects, savedChildFiles, parseError) -> Void in + isShouldReturnIfChildObjectsFound: transaction) { (savedChildObjects, savedChildFiles, parseError) -> Void in //If an error occurs, everything should be skipped if parseError != nil { error = parseError @@ -1404,12 +1406,10 @@ public extension Sequence where Element: ParseUser { if let error = error { throw error } + commands.append(try user.saveCommand(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig)) } var returnBatch = [(Result)]() - let commands = try map { - try $0.saveCommand(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig) - } let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit try canSendTransactions(transaction, objectCount: commands.count, batchLimit: batchLimit) let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) @@ -1587,19 +1587,20 @@ public extension Sequence where Element: ParseUser { attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil) + let users = map { $0 } queue.sync { var childObjects = [String: PointerType]() var childFiles = [UUID: ParseFile]() var error: ParseError? + var commands = [API.Command]() - let users = map { $0 } for user in users { let group = DispatchGroup() group.enter() user.ensureDeepSave(options: options, // swiftlint:disable:next line_length - isShouldReturnIfChildObjectsFound: true) { (savedChildObjects, savedChildFiles, parseError) -> Void in - //If an error occurs, everything should be skipped + isShouldReturnIfChildObjectsFound: transaction) { (savedChildObjects, savedChildFiles, parseError) -> Void in + // If an error occurs, everything should be skipped if parseError != nil { error = parseError } @@ -1634,24 +1635,33 @@ public extension Sequence where Element: ParseUser { } return } + do { + switch method { + case .save: + commands.append( + try user.saveCommand(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig) + ) + case .create: + commands.append(user.createCommand()) + case .replace: + commands.append(try user.replaceCommand()) + case .update: + commands.append(try user.updateCommand()) + } + } catch { + callbackQueue.async { + if let parseError = error as? ParseError { + completion(.failure(parseError)) + } else { + completion(.failure(.init(code: .unknownError, message: error.localizedDescription))) + } + } + return + } } do { var returnBatch = [(Result)]() - let commands: [API.Command]! - switch method { - case .save: - commands = try map { - try $0.saveCommand(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig) - } - case .create: - commands = map { $0.createCommand() } - case .replace: - commands = try map { try $0.replaceCommand() } - case .update: - commands = try map { try $0.updateCommand() } - } - let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit try canSendTransactions(transaction, objectCount: commands.count, batchLimit: batchLimit) let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) diff --git a/Tests/ParseSwiftTests/ParseObjectBatchTests.swift b/Tests/ParseSwiftTests/ParseObjectBatchTests.swift index 6fdbc5668..c51b382ef 100644 --- a/Tests/ParseSwiftTests/ParseObjectBatchTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectBatchTests.swift @@ -23,6 +23,7 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le // Custom properties var points: Int = 0 var other: Game2? + var otherArray: [Game2]? //custom initializers init() { @@ -118,7 +119,7 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le let encoded: Data! do { encoded = try scoreOnServer.getJSONEncoder().encode(response) - //Get dates in correct format from ParseDecoding strategy + // Get dates in correct format from ParseDecoding strategy let encoded1 = try ParseCoding.jsonEncoder().encode(scoreOnServer) scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded1) let encoded2 = try ParseCoding.jsonEncoder().encode(scoreOnServer2) @@ -213,6 +214,122 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } } + func testSaveAllWithPointer() { // swiftlint:disable:this function_body_length cyclomatic_complexity + var score = GameScore(points: 10) + score.other = Game2() + + var scoreOnServer = score + scoreOnServer.objectId = "yarr" + scoreOnServer.createdAt = Date() + scoreOnServer.ACL = nil + + let response = [BatchResponseItem(success: scoreOnServer, error: nil)] + let encoded: Data! + do { + encoded = try scoreOnServer.getJSONEncoder().encode(response) + // Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().encode(scoreOnServer) + scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded1) + } catch { + XCTFail("Should have encoded/decoded. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + do { + + let saved = try [score].saveAll() + + XCTAssertEqual(saved.count, 1) + switch saved[0] { + + case .success(let first): + XCTAssert(first.hasSameObjectId(as: scoreOnServer)) + guard let savedCreatedAt = first.createdAt, + let savedUpdatedAt = first.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(savedCreatedAt, scoreOnServer.createdAt) + XCTAssertEqual(savedUpdatedAt, scoreOnServer.createdAt) + XCTAssertNil(first.ACL) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + } catch { + XCTFail(error.localizedDescription) + } + + do { + _ = try [score].saveAll(transaction: true, + options: [.installationId("hello")]) + XCTFail("Should have thrown error") + } catch { + XCTAssertTrue(error.localizedDescription.contains("originally")) + } + } + + func testSaveAllWithPointerArray() { // swiftlint:disable:this function_body_length cyclomatic_complexity + var score = GameScore(points: 10) + score.otherArray = [Game2()] + + var scoreOnServer = score + scoreOnServer.objectId = "yarr" + scoreOnServer.createdAt = Date() + scoreOnServer.ACL = nil + + let response = [BatchResponseItem(success: scoreOnServer, error: nil)] + let encoded: Data! + do { + encoded = try scoreOnServer.getJSONEncoder().encode(response) + // Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().encode(scoreOnServer) + scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded1) + } catch { + XCTFail("Should have encoded/decoded. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + do { + + let saved = try [score].saveAll() + + XCTAssertEqual(saved.count, 1) + switch saved[0] { + + case .success(let first): + XCTAssert(first.hasSameObjectId(as: scoreOnServer)) + guard let savedCreatedAt = first.createdAt, + let savedUpdatedAt = first.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(savedCreatedAt, scoreOnServer.createdAt) + XCTAssertEqual(savedUpdatedAt, scoreOnServer.createdAt) + XCTAssertNil(first.ACL) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + } catch { + XCTFail(error.localizedDescription) + } + + do { + _ = try [score].saveAll(transaction: true, + options: [.installationId("hello")]) + XCTFail("Should have thrown error") + } catch { + XCTAssertTrue(error.localizedDescription.contains("originally")) + } + } + func testSaveAllTransaction() { // swiftlint:disable:this function_body_length cyclomatic_complexity let score = GameScore(points: 10) let score2 = GameScore(points: 20) @@ -391,7 +508,7 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } let body = BatchCommand(requests: commands, transaction: false) // swiftlint:disable:next line_length - let expected = "{\"requests\":[{\"body\":{\"points\":10},\"method\":\"PUT\",\"path\":\"\\/1\\/classes\\/GameScore\\/yarr\"},{\"body\":{\"points\":20},\"method\":\"PUT\",\"path\":\"\\/1\\/classes\\/GameScore\\/yolo\"}],\"transaction\":false}" + let expected = "{\"requests\":[{\"body\":{\"__type\":\"Pointer\",\"className\":\"GameScore\",\"objectId\":\"yarr\"},\"method\":\"PUT\",\"path\":\"\\/1\\/classes\\/GameScore\\/yarr\"},{\"body\":{\"__type\":\"Pointer\",\"className\":\"GameScore\",\"objectId\":\"yolo\"},\"method\":\"PUT\",\"path\":\"\\/1\\/classes\\/GameScore\\/yolo\"}],\"transaction\":false}" let encoded = try ParseCoding.parseEncoder() .encode(body, collectChildren: false, @@ -799,6 +916,71 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le wait(for: [expectation1, expectation2], timeout: 20.0) } + func saveAllAsyncPointer(scores: [GameScore], // swiftlint:disable:this function_body_length cyclomatic_complexity + transaction: Bool = false, + scoresOnServer: [GameScore], callbackQueue: DispatchQueue) { + + let expectation1 = XCTestExpectation(description: "Save object1") + guard let scoreOnServer = scoresOnServer.first else { + XCTFail("Should unwrap") + expectation1.fulfill() + return + } + + scores.saveAll(transaction: transaction, + callbackQueue: callbackQueue) { result in + + switch result { + + case .success(let saved): + XCTAssertEqual(saved.count, 1) + guard let firstObject = saved.first else { + XCTFail("Should unwrap") + expectation1.fulfill() + return + } + + switch firstObject { + + case .success(let first): + XCTAssert(first.hasSameObjectId(as: scoreOnServer)) + guard let savedCreatedAt = first.createdAt, + let savedUpdatedAt = first.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + XCTAssertEqual(savedCreatedAt, scoreOnServer.createdAt) + XCTAssertEqual(savedUpdatedAt, scoreOnServer.createdAt) + XCTAssertNil(first.ACL) + + case .failure(let error): + XCTFail(error.localizedDescription) + } + + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + + let expectation2 = XCTestExpectation(description: "Save object2") + scores.saveAll(transaction: true, + options: [.useMasterKey], + callbackQueue: callbackQueue) { result in + + switch result { + + case .success: + XCTFail("Should have thrown error") + case .failure(let error): + XCTAssertTrue(error.localizedDescription.contains("originally")) + } + expectation2.fulfill() + } + wait(for: [expectation1, expectation2], timeout: 20.0) + } + #if !os(Linux) && !os(Android) && !os(Windows) func testThreadSafeSaveAllAsync() { let score = GameScore(points: 10) @@ -876,6 +1058,62 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le callbackQueue: .main) } + func testSaveAllAsyncPointer() { + var score = GameScore(points: 10) + score.other = Game2() + + var scoreOnServer = score + scoreOnServer.objectId = "yarr" + scoreOnServer.createdAt = Date() + scoreOnServer.ACL = nil + + let response = [BatchResponseItem(success: scoreOnServer, error: nil)] + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(response) + // Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().encode(scoreOnServer) + scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded1) + + } catch { + XCTFail("Should have encoded/decoded. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + self.saveAllAsyncPointer(scores: [score], scoresOnServer: [scoreOnServer], + callbackQueue: .main) + } + + func testSaveAllAsyncPointerArray() { + var score = GameScore(points: 10) + score.otherArray = [Game2()] + + var scoreOnServer = score + scoreOnServer.objectId = "yarr" + scoreOnServer.createdAt = Date() + scoreOnServer.ACL = nil + + let response = [BatchResponseItem(success: scoreOnServer, error: nil)] + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(response) + // Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().encode(scoreOnServer) + scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded1) + + } catch { + XCTFail("Should have encoded/decoded. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + self.saveAllAsyncPointer(scores: [score], scoresOnServer: [scoreOnServer], + callbackQueue: .main) + } + func testSaveAllAsyncTransaction() { // swiftlint:disable:this function_body_length cyclomatic_complexity let score = GameScore(points: 10) let score2 = GameScore(points: 20) diff --git a/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift b/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift index 0428a31b4..10c03ff75 100644 --- a/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift @@ -217,7 +217,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ let commands = try objects.map { try $0.saveCommand() } let body = BatchCommand(requests: commands, transaction: false) // swiftlint:disable:next line_length - let expected = "{\"requests\":[{\"body\":{\"objectId\":\"yarr\",\"player\":\"Jen\",\"points\":10},\"method\":\"POST\",\"path\":\"\\/classes\\/GameScore\"},{\"body\":{\"objectId\":\"yolo\",\"player\":\"Jen\",\"points\":20},\"method\":\"POST\",\"path\":\"\\/classes\\/GameScore\"}],\"transaction\":false}" + let expected = "{\"requests\":[{\"body\":{\"__type\":\"Pointer\",\"className\":\"GameScore\",\"objectId\":\"yarr\"},\"method\":\"POST\",\"path\":\"\\/classes\\/GameScore\"},{\"body\":{\"__type\":\"Pointer\",\"className\":\"GameScore\",\"objectId\":\"yolo\"},\"method\":\"POST\",\"path\":\"\\/classes\\/GameScore\"}],\"transaction\":false}" let encoded = try ParseCoding.parseEncoder() .encode(body, collectChildren: false, objectsSavedBeforeThisOne: nil, @@ -238,7 +238,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ let commands = try objects.map { try $0.saveCommand() } let body = BatchCommand(requests: commands, transaction: false) // swiftlint:disable:next line_length - let expected = "{\"requests\":[{\"body\":{\"objectId\":\"yarr\",\"player\":\"Jen\",\"points\":10},\"method\":\"PUT\",\"path\":\"\\/classes\\/GameScore\\/yarr\"},{\"body\":{\"objectId\":\"yolo\",\"player\":\"Jen\",\"points\":20},\"method\":\"PUT\",\"path\":\"\\/classes\\/GameScore\\/yolo\"}],\"transaction\":false}" + let expected = "{\"requests\":[{\"body\":{\"__type\":\"Pointer\",\"className\":\"GameScore\",\"objectId\":\"yarr\"},\"method\":\"PUT\",\"path\":\"\\/classes\\/GameScore\\/yarr\"},{\"body\":{\"__type\":\"Pointer\",\"className\":\"GameScore\",\"objectId\":\"yolo\"},\"method\":\"PUT\",\"path\":\"\\/classes\\/GameScore\\/yolo\"}],\"transaction\":false}" let encoded = try ParseCoding.parseEncoder() .encode(body, collectChildren: false, objectsSavedBeforeThisOne: nil, @@ -308,7 +308,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ let commands = try objects.map { try $0.saveCommand() } let body = BatchCommand(requests: commands, transaction: false) // swiftlint:disable:next line_length - let expected = "{\"requests\":[{\"body\":{\"objectId\":\"yarr\"},\"method\":\"POST\",\"path\":\"\\/users\"},{\"body\":{\"objectId\":\"yolo\"},\"method\":\"POST\",\"path\":\"\\/users\"}],\"transaction\":false}" + let expected = "{\"requests\":[{\"body\":{\"__type\":\"Pointer\",\"className\":\"_User\",\"objectId\":\"yarr\"},\"method\":\"POST\",\"path\":\"\\/users\"},{\"body\":{\"__type\":\"Pointer\",\"className\":\"_User\",\"objectId\":\"yolo\"},\"method\":\"POST\",\"path\":\"\\/users\"}],\"transaction\":false}" let encoded = try ParseCoding.parseEncoder() .encode(body, collectChildren: false, objectsSavedBeforeThisOne: nil, @@ -329,7 +329,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ let commands = try objects.map { try $0.saveCommand() } let body = BatchCommand(requests: commands, transaction: false) // swiftlint:disable:next line_length - let expected = "{\"requests\":[{\"body\":{\"objectId\":\"yarr\"},\"method\":\"PUT\",\"path\":\"\\/users\\/yarr\"},{\"body\":{\"objectId\":\"yolo\"},\"method\":\"PUT\",\"path\":\"\\/users\\/yolo\"}],\"transaction\":false}" + let expected = "{\"requests\":[{\"body\":{\"__type\":\"Pointer\",\"className\":\"_User\",\"objectId\":\"yarr\"},\"method\":\"PUT\",\"path\":\"\\/users\\/yarr\"},{\"body\":{\"__type\":\"Pointer\",\"className\":\"_User\",\"objectId\":\"yolo\"},\"method\":\"PUT\",\"path\":\"\\/users\\/yolo\"}],\"transaction\":false}" let encoded = try ParseCoding.parseEncoder() .encode(body, collectChildren: false, objectsSavedBeforeThisOne: nil, @@ -399,7 +399,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ let commands = try objects.map { try $0.saveCommand() } let body = BatchCommand(requests: commands, transaction: false) // swiftlint:disable:next line_length - let expected = "{\"requests\":[{\"body\":{\"objectId\":\"yarr\"},\"method\":\"POST\",\"path\":\"\\/installations\"},{\"body\":{\"objectId\":\"yolo\"},\"method\":\"POST\",\"path\":\"\\/installations\"}],\"transaction\":false}" + let expected = "{\"requests\":[{\"body\":{\"__type\":\"Pointer\",\"className\":\"_Installation\",\"objectId\":\"yarr\"},\"method\":\"POST\",\"path\":\"\\/installations\"},{\"body\":{\"__type\":\"Pointer\",\"className\":\"_Installation\",\"objectId\":\"yolo\"},\"method\":\"POST\",\"path\":\"\\/installations\"}],\"transaction\":false}" let encoded = try ParseCoding.parseEncoder() .encode(body, collectChildren: false, objectsSavedBeforeThisOne: nil, @@ -420,7 +420,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ let commands = try objects.map { try $0.saveCommand() } let body = BatchCommand(requests: commands, transaction: false) // swiftlint:disable:next line_length - let expected = "{\"requests\":[{\"body\":{\"objectId\":\"yarr\"},\"method\":\"PUT\",\"path\":\"\\/installations\\/yarr\"},{\"body\":{\"objectId\":\"yolo\"},\"method\":\"PUT\",\"path\":\"\\/installations\\/yolo\"}],\"transaction\":false}" + let expected = "{\"requests\":[{\"body\":{\"__type\":\"Pointer\",\"className\":\"_Installation\",\"objectId\":\"yarr\"},\"method\":\"PUT\",\"path\":\"\\/installations\\/yarr\"},{\"body\":{\"__type\":\"Pointer\",\"className\":\"_Installation\",\"objectId\":\"yolo\"},\"method\":\"PUT\",\"path\":\"\\/installations\\/yolo\"}],\"transaction\":false}" let encoded = try ParseCoding.parseEncoder() .encode(body, collectChildren: false, objectsSavedBeforeThisOne: nil,