diff --git a/CHANGELOG.md b/CHANGELOG.md index 24551e7e0..39fc970f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,14 @@ # Parse-Swift Changelog ### main -[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.1.6...main) +[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.1.7...main) * _Contributing to this repo? Add info about your change here to be included in the next release_ +### 1.1.7 +[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.1.6...1.1.7) + __New features__ +- Add transaction support to batch saveAll and deleteAll ([#89](https://github.com/parse-community/Parse-Swift/pull/89)), thanks to [Corey Baker](https://github.com/cbaker6). - Add modifiers to containsString, hasPrefix, hasSuffix ([#85](https://github.com/parse-community/Parse-Swift/pull/85)), thanks to [Corey Baker](https://github.com/cbaker6). __Improvements__ diff --git a/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift index 98581d432..87949c2a7 100644 --- a/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift @@ -1,3 +1,9 @@ +//: For this page, make sure your build target is set to ParseSwift (macOS) and targeting +//: `My Mac` or whatever the name of your mac is. Also be sure your `Playground Settings` +//: in the `File Inspector` is `Platform = macOS`. This is because +//: Keychain in iOS Playgrounds behaves differently. Every page in Playgrounds should +//: be set to build for `macOS` unless specified. + import PlaygroundSupport import Foundation import ParseSwift @@ -38,7 +44,7 @@ let score = GameScore(score: 10) let score2 = GameScore(score: 3) /*: Save asynchronously (preferred way) - Performs work on background - queue and returns to designated on designated callbackQueue. + queue and returns to specified callbackQueue. If no callbackQueue is specified it returns to main queue. */ score.save { result in @@ -101,6 +107,29 @@ var score2ForFetchedLater: GameScore? } } +//: Saving multiple GameScores at once using a transaction. +[score, score2].saveAll(transaction: true) { results in + switch results { + case .success(let otherResults): + var index = 0 + otherResults.forEach { otherResult in + switch otherResult { + case .success(let savedScore): + print("Saved \"\(savedScore.className)\" with score \(savedScore.score) successfully") + if index == 1 { + score2ForFetchedLater = savedScore + } + index += 1 + case .failure(let error): + assertionFailure("Error saving: \(error)") + } + } + + case .failure(let error): + assertionFailure("Error saving: \(error)") + } +} + //: Save synchronously (not preferred - all operations on main queue). let savedScore: GameScore? do { @@ -235,7 +264,7 @@ do { } //: Asynchronously (preferred way) deleteAll GameScores based on it's objectId alone. -[scoreToFetch, score2ToFetch].deleteAll { result in +[scoreToFetch, score2ToFetch].deleteAll(transaction: true) { result in switch result { case .success(let deletedScores): deletedScores.forEach { result in diff --git a/ParseSwift.playground/Pages/10 - Cloud Code.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/10 - Cloud Code.xcplaygroundpage/Contents.swift index e4a6ee0a5..01b5fd0fd 100644 --- a/ParseSwift.playground/Pages/10 - Cloud Code.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/10 - Cloud Code.xcplaygroundpage/Contents.swift @@ -1,5 +1,11 @@ //: [Previous](@previous) +//: For this page, make sure your build target is set to ParseSwift (macOS) and targeting +//: `My Mac` or whatever the name of your mac is. Also be sure your `Playground Settings` +//: in the `File Inspector` is `Platform = macOS`. This is because +//: Keychain in iOS Playgrounds behaves differently. Every page in Playgrounds should +//: be set to build for `macOS` unless specified. + import PlaygroundSupport import Foundation import ParseSwift diff --git a/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift index f8820a8e7..c09cb9ab5 100644 --- a/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift @@ -1,5 +1,12 @@ //: [Previous](@previous) +//: For this page, make sure your build target is set to ParseSwift (iOS) and targeting +//: an iPhone, iPod, or iPad. Also be sure your `Playground Settings` +//: in the `File Inspector` is `Platform = iOS`. This is because +//: SwiftUI in macOS Playgrounds doesn't seem to build correctly +//: Be sure to switch your target and `Playground Settings` back to +//: macOS after leaving this page. + import PlaygroundSupport import Foundation import ParseSwift 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 43eac5f0c..38c306b17 100644 --- a/ParseSwift.playground/Pages/12 - Roles and Relations.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/12 - Roles and Relations.xcplaygroundpage/Contents.swift @@ -1,5 +1,11 @@ //: [Previous](@previous) +//: For this page, make sure your build target is set to ParseSwift (macOS) and targeting +//: `My Mac` or whatever the name of your mac is. Also be sure your `Playground Settings` +//: in the `File Inspector` is `Platform = macOS`. This is because +//: Keychain in iOS Playgrounds behaves differently. Every page in Playgrounds should +//: be set to build for `macOS` unless specified. + import PlaygroundSupport import Foundation import ParseSwift diff --git a/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift index f0fd4c544..1c17e44f2 100644 --- a/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift @@ -1,5 +1,11 @@ //: [Previous](@previous) +//: For this page, make sure your build target is set to ParseSwift (macOS) and targeting +//: `My Mac` or whatever the name of your mac is. Also be sure your `Playground Settings` +//: in the `File Inspector` is `Platform = macOS`. This is because +//: Keychain in iOS Playgrounds behaves differently. Every page in Playgrounds should +//: be set to build for `macOS` unless specified. + import PlaygroundSupport import Foundation import ParseSwift diff --git a/ParseSwift.playground/Pages/14 - Config.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/14 - Config.xcplaygroundpage/Contents.swift index 996847efa..fa2429d51 100644 --- a/ParseSwift.playground/Pages/14 - Config.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/14 - Config.xcplaygroundpage/Contents.swift @@ -1,5 +1,11 @@ //: [Previous](@previous) +//: For this page, make sure your build target is set to ParseSwift (macOS) and targeting +//: `My Mac` or whatever the name of your mac is. Also be sure your `Playground Settings` +//: in the `File Inspector` is `Platform = macOS`. This is because +//: Keychain in iOS Playgrounds behaves differently. Every page in Playgrounds should +//: be set to build for `macOS` unless specified. + import PlaygroundSupport import Foundation import ParseSwift diff --git a/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift index 88f450996..951229c4c 100644 --- a/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift @@ -1,5 +1,11 @@ //: [Previous](@previous) +//: For this page, make sure your build target is set to ParseSwift (macOS) and targeting +//: `My Mac` or whatever the name of your mac is. Also be sure your `Playground Settings` +//: in the `File Inspector` is `Platform = macOS`. This is because +//: Keychain in iOS Playgrounds behaves differently. Every page in Playgrounds should +//: be set to build for `macOS` unless specified. + import PlaygroundSupport import Foundation import ParseSwift @@ -31,9 +37,9 @@ var query = GameScore.query("score" > 50, "createdAt" > afterDate) .order([.descending("score")]) -// Query asynchronously (preferred way) - Performs work on background -// queue and returns to designated on designated callbackQueue. -// If no callbackQueue is specified it returns to main queue. +//: Query asynchronously (preferred way) - Performs work on background +//: queue and returns to specified callbackQueue. +//: If no callbackQueue is specified it returns to main queue. query.limit(2).find(callbackQueue: .main) { results in switch results { case .success(let scores): @@ -50,7 +56,7 @@ query.limit(2).find(callbackQueue: .main) { results in } } -// Query synchronously (not preferred - all operations on main queue). +//: Query synchronously (not preferred - all operations on main queue). let results = try query.find() assert(results.count >= 1) results.forEach { (score) in @@ -59,9 +65,9 @@ results.forEach { (score) in print("Found score: \(score)") } -// Query first asynchronously (preferred way) - Performs work on background -// queue and returns to designated on designated callbackQueue. -// If no callbackQueue is specified it returns to main queue. +//: Query first asynchronously (preferred way) - Performs work on background +//: queue and returns to specified callbackQueue. +//: If no callbackQueue is specified it returns to main queue. query.first { results in switch results { case .success(let score): diff --git a/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift index 367bb02d7..b1742bc1e 100644 --- a/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift @@ -1,5 +1,11 @@ //: [Previous](@previous) +//: For this page, make sure your build target is set to ParseSwift (macOS) and targeting +//: `My Mac` or whatever the name of your mac is. Also be sure your `Playground Settings` +//: in the `File Inspector` is `Platform = macOS`. This is because +//: Keychain in iOS Playgrounds behaves differently. Every page in Playgrounds should +//: be set to build for `macOS` unless specified. + import PlaygroundSupport import Foundation PlaygroundPage.current.needsIndefiniteExecution = true @@ -25,7 +31,7 @@ struct User: ParseUser { } /*: Sign up user asynchronously - Performs work on background - queue and returns to designated on designated callbackQueue. + queue and returns to specified callbackQueue. If no callbackQueue is specified it returns to main queue. */ User.signup(username: "hello", password: "world") { results in diff --git a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift index 6cd3d0f98..da4e380d7 100644 --- a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift @@ -1,5 +1,11 @@ //: [Previous](@previous) +//: For this page, make sure your build target is set to ParseSwift (macOS) and targeting +//: `My Mac` or whatever the name of your mac is. Also be sure your `Playground Settings` +//: in the `File Inspector` is `Platform = macOS`. This is because +//: Keychain in iOS Playgrounds behaves differently. Every page in Playgrounds should +//: be set to build for `macOS` unless specified. + import PlaygroundSupport import Foundation import ParseSwift @@ -47,24 +53,6 @@ struct GameScore: ParseObject { } } -/*: Save your first customKey value to your `ParseUser` - Asynchrounously - Performs work on background - queue and returns to designated on designated callbackQueue. - If no callbackQueue is specified it returns to main queue. -*/ -User.current?.customKey = "myCustom" -User.current?.score = GameScore(score: 12) -User.current?.targetScore = GameScore(score: 100) -User.current?.save { results in - - switch results { - case .success(let updatedUser): - print("Successfully save custom fields of User to ParseServer: \(updatedUser)") - case .failure(let error): - print("Failed to update user: \(error)") - } -} - //: Logging out - synchronously do { try User.logout() @@ -74,7 +62,7 @@ do { } /*: Login - asynchronously - Performs work on background - queue and returns to designated on designated callbackQueue. + queue and returns to specified callbackQueue. If no callbackQueue is specified it returns to main queue. */ User.login(username: "hello", password: "world") { results in @@ -94,9 +82,27 @@ User.login(username: "hello", password: "world") { results in } } +/*: Save your first `customKey` value to your `ParseUser` + Asynchrounously - Performs work on background + queue and returns to specified callbackQueue. + If no callbackQueue is specified it returns to main queue. +*/ +User.current?.customKey = "myCustom" +User.current?.score = GameScore(score: 12) +User.current?.targetScore = GameScore(score: 100) +User.current?.save { results in + + switch results { + case .success(let updatedUser): + print("Successfully save custom fields of User to ParseServer: \(updatedUser)") + case .failure(let error): + print("Failed to update user: \(error)") + } +} + //: Looking at the output of user from the previous login, it only has -//: a pointer to the `score`and `targetScore` fields. You can fetch using `include` to -//: get the score. +//: a pointer to the `score` and `targetScore` fields. You can +//: fetch using `include` to get the score. User.current?.fetch(includeKeys: ["score"]) { result in switch result { case .success: diff --git a/ParseSwift.playground/Pages/5 - ACL.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/5 - ACL.xcplaygroundpage/Contents.swift index 24b6bb7f0..f37d3cb72 100644 --- a/ParseSwift.playground/Pages/5 - ACL.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/5 - ACL.xcplaygroundpage/Contents.swift @@ -1,5 +1,11 @@ //: [Previous](@previous) +//: For this page, make sure your build target is set to ParseSwift (macOS) and targeting +//: `My Mac` or whatever the name of your mac is. Also be sure your `Playground Settings` +//: in the `File Inspector` is `Platform = macOS`. This is because +//: Keychain in iOS Playgrounds behaves differently. Every page in Playgrounds should +//: be set to build for `macOS` unless specified. + import PlaygroundSupport import Foundation import ParseSwift @@ -40,7 +46,7 @@ var score = GameScore(score: 40) score.ACL = try? ParseACL.defaultACL() /*: Save asynchronously (preferred way) - Performs work on background - queue and returns to designated on designated callbackQueue. + queue and returns to specified callbackQueue. If no callbackQueue is specified it returns to main queue. */ score.save { result in diff --git a/ParseSwift.playground/Pages/6 - Installation.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/6 - Installation.xcplaygroundpage/Contents.swift index a50403a1d..627dd8b44 100644 --- a/ParseSwift.playground/Pages/6 - Installation.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/6 - Installation.xcplaygroundpage/Contents.swift @@ -1,5 +1,11 @@ //: [Previous](@previous) +//: For this page, make sure your build target is set to ParseSwift (macOS) and targeting +//: `My Mac` or whatever the name of your mac is. Also be sure your `Playground Settings` +//: in the `File Inspector` is `Platform = macOS`. This is because +//: Keychain in iOS Playgrounds behaves differently. Every page in Playgrounds should +//: be set to build for `macOS` unless specified. + import PlaygroundSupport import Foundation import ParseSwift @@ -34,7 +40,7 @@ struct Installation: ParseInstallation { //: WARNING: All calls on Installation need to be done on the main queue DispatchQueue.main.async { - /*: Save your first customKey value to your `ParseInstallation`. + /*: Save your first `customKey` value to your `ParseInstallation`. Performs work on background queue and returns to designated on designated callbackQueue. If no callbackQueue is specified it returns to main queue. diff --git a/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift index 97dd63c9f..ed73aeef9 100644 --- a/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift @@ -1,5 +1,11 @@ //: [Previous](@previous) +//: For this page, make sure your build target is set to ParseSwift (macOS) and targeting +//: `My Mac` or whatever the name of your mac is. Also be sure your `Playground Settings` +//: in the `File Inspector` is `Platform = macOS`. This is because +//: Keychain in iOS Playgrounds behaves differently. Every page in Playgrounds should +//: be set to build for `macOS` unless specified. + import PlaygroundSupport import Foundation import ParseSwift @@ -29,7 +35,7 @@ var score = GameScore(score: 10) score.location = ParseGeoPoint(latitude: 40.0, longitude: -30.0) /*: Save asynchronously (preferred way) - performs work on background - queue and returns to designated on designated callbackQueue. + queue and returns to specified callbackQueue. If no callbackQueue is specified it returns to main queue. */ score.save { result in diff --git a/ParseSwift.playground/Pages/8 - Pointers.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/8 - Pointers.xcplaygroundpage/Contents.swift index ff1fb4415..ce7af7394 100644 --- a/ParseSwift.playground/Pages/8 - Pointers.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/8 - Pointers.xcplaygroundpage/Contents.swift @@ -1,5 +1,11 @@ //: [Previous](@previous) +//: For this page, make sure your build target is set to ParseSwift (macOS) and targeting +//: `My Mac` or whatever the name of your mac is. Also be sure your `Playground Settings` +//: in the `File Inspector` is `Platform = macOS`. This is because +//: Keychain in iOS Playgrounds behaves differently. Every page in Playgrounds should +//: be set to build for `macOS` unless specified. + import PlaygroundSupport import Foundation import ParseSwift @@ -16,7 +22,7 @@ struct Book: ParseObject { var ACL: ParseACL? //: Your own properties. - var title: String + var title: String? init(title: String) { self.title = title @@ -52,9 +58,6 @@ author.save { result in assert(savedAuthorAndBook.updatedAt != nil) assert(savedAuthorAndBook.ACL == nil) - /*: To modify, need to make it a var as the value type - was initialized as immutable. - */ print("Saved \(savedAuthorAndBook)") case .failure(let error): assertionFailure("Error saving: \(error)") @@ -75,14 +78,66 @@ author2.save { result in assert(savedAuthorAndBook.ACL == nil) assert(savedAuthorAndBook.otherBooks?.count == 2) - /*: To modify, need to make it a var as the value type - was initialized as immutable. - */ print("Saved \(savedAuthorAndBook)") case .failure(let error): assertionFailure("Error saving: \(error)") } } +//: Query for your new saved author +let query1 = Author.query("name" == "Bruce") + +query1.first { results in + switch results { + case .success(let author): + print("Found author: \(author)") + + case .failure(let error): + assertionFailure("Error querying: \(error)") + } +} + +/*: You will notice in the query above, the fields `book` and `otherBooks` only contain + arrays consisting of key/value pairs of `objectId`. These are called Pointers + in `Parse`. + + If you want to retrieve the complete object pointed to in `book`, you need to add + the field names containing the objects specifically in `include` in your query. +*/ + +/*: Here, we include `book`. If you wanted `book` and `otherBook`, you + could have used: `.include(["book", "otherBook"])`. +*/ +let query2 = Author.query("name" == "Bruce") + .include("book") + +query2.first { results in + switch results { + case .success(let author): + print("Found author and included \"book\": \(author)") + + case .failure(let error): + assertionFailure("Error querying: \(error)") + } +} + +/*: When you have many fields that are pointing to objects, it may become tedious + to add all of them to the list. You can quickly retreive all pointer objects by + using `includeAll`. You can also use `include("*")` to retrieve all pointer + objects. +*/ +let query3 = Author.query("name" == "Bruce") + .includeAll() + +query3.first { results in + switch results { + case .success(let author): + print("Found author and included all: \(author)") + + case .failure(let error): + assertionFailure("Error querying: \(error)") + } +} + PlaygroundPage.current.finishExecution() //: [Next](@next) diff --git a/ParseSwift.playground/Pages/9 - Files.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/9 - Files.xcplaygroundpage/Contents.swift index f156a3980..5fa5d11b0 100644 --- a/ParseSwift.playground/Pages/9 - Files.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/9 - Files.xcplaygroundpage/Contents.swift @@ -1,5 +1,11 @@ //: [Previous](@previous) +//: For this page, make sure your build target is set to ParseSwift (macOS) and targeting +//: `My Mac` or whatever the name of your mac is. Also be sure your `Playground Settings` +//: in the `File Inspector` is `Platform = macOS`. This is because +//: Keychain in iOS Playgrounds behaves differently. Every page in Playgrounds should +//: be set to build for `macOS` unless specified. + import PlaygroundSupport import Foundation import ParseSwift @@ -43,7 +49,7 @@ let profilePic = ParseFile(name: "profile.svg", cloudURL: linkToFile) score.profilePicture = profilePic /*: Save asynchronously (preferred way) - Performs work on background - queue and returns to designated on designated callbackQueue. + queue and returns to specified callbackQueue. If no callbackQueue is specified it returns to main queue. */ score.save { result in diff --git a/ParseSwift.playground/contents.xcplayground b/ParseSwift.playground/contents.xcplayground index b482d7b97..ab9557970 100644 --- a/ParseSwift.playground/contents.xcplayground +++ b/ParseSwift.playground/contents.xcplayground @@ -1,5 +1,5 @@ - + diff --git a/ParseSwift.podspec b/ParseSwift.podspec index 297c84519..8e4c51a08 100644 --- a/ParseSwift.podspec +++ b/ParseSwift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "ParseSwift" - s.version = "1.1.6" + s.version = "1.1.7" s.summary = "Parse Pure Swift SDK" s.homepage = "https://github.com/parse-community/Parse-Swift" s.authors = { diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index dd64a6cb7..ccf92eb34 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -2261,7 +2261,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.1.6; + MARKETING_VERSION = 1.1.7; PRODUCT_BUNDLE_IDENTIFIER = com.parse.ParseSwift; PRODUCT_NAME = ParseSwift; SKIP_INSTALL = YES; @@ -2285,7 +2285,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.1.6; + MARKETING_VERSION = 1.1.7; PRODUCT_BUNDLE_IDENTIFIER = com.parse.ParseSwift; PRODUCT_NAME = ParseSwift; SKIP_INSTALL = YES; @@ -2351,7 +2351,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 1.1.6; + MARKETING_VERSION = 1.1.7; PRODUCT_BUNDLE_IDENTIFIER = com.parse.ParseSwift; PRODUCT_NAME = ParseSwift; SDKROOT = macosx; @@ -2377,7 +2377,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 1.1.6; + MARKETING_VERSION = 1.1.7; PRODUCT_BUNDLE_IDENTIFIER = com.parse.ParseSwift; PRODUCT_NAME = ParseSwift; SDKROOT = macosx; @@ -2524,7 +2524,7 @@ INFOPLIST_FILE = "ParseSwift-watchOS/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.1.6; + MARKETING_VERSION = 1.1.7; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.parse.ParseSwift-watchOS"; @@ -2553,7 +2553,7 @@ INFOPLIST_FILE = "ParseSwift-watchOS/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.1.6; + MARKETING_VERSION = 1.1.7; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.parse.ParseSwift-watchOS"; PRODUCT_NAME = ParseSwift; @@ -2580,7 +2580,7 @@ INFOPLIST_FILE = "ParseSwift-tvOS/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.1.6; + MARKETING_VERSION = 1.1.7; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.parse.ParseSwift-tvOS"; @@ -2608,7 +2608,7 @@ INFOPLIST_FILE = "ParseSwift-tvOS/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.1.6; + MARKETING_VERSION = 1.1.7; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.parse.ParseSwift-tvOS"; PRODUCT_NAME = ParseSwift; diff --git a/Scripts/jazzy.sh b/Scripts/jazzy.sh index b3a7cd5f5..3a0a8df81 100755 --- a/Scripts/jazzy.sh +++ b/Scripts/jazzy.sh @@ -5,7 +5,7 @@ bundle exec jazzy \ --author_url http://parseplatform.org \ --github_url https://github.com/parse-community/Parse-Swift \ --root-url http://parseplatform.org/Parse-Swift/api/ \ - --module-version 1.1.6 \ + --module-version 1.1.7 \ --theme fullwidth \ --skip-undocumented \ --output ./docs/api \ diff --git a/Sources/ParseSwift/API/API+Commands.swift b/Sources/ParseSwift/API/API+Commands.swift index 809c22c71..6a414cf43 100644 --- a/Sources/ParseSwift/API/API+Commands.swift +++ b/Sources/ParseSwift/API/API+Commands.swift @@ -203,7 +203,10 @@ internal extension API { childObjects: [String: PointerType]? = nil, childFiles: [UUID: ParseFile]? = nil) -> Result { let params = self.params?.getQueryItems() - let headers = API.getHeaders(options: options) + var headers = API.getHeaders(options: options) + if !(method == .POST) && !(method == .PUT) { + headers.removeValue(forKey: "X-Parse-Request-Id") + } let url = parseURL == nil ? ParseConfiguration.serverURL.appendingPathComponent(path.urlComponent) : parseURL! @@ -410,7 +413,7 @@ extension API.Command where T: ParseObject { return try? body.getEncoder().encode(body, skipKeys: .object) } - static func batch(commands: [API.Command]) -> RESTBatchCommandType { + static func batch(commands: [API.Command], transaction: Bool) -> RESTBatchCommandType { let commands = commands.compactMap { (command) -> API.Command? in let path = ParseConfiguration.mountPath + command.path.urlComponent guard let body = command.body else { @@ -452,12 +455,13 @@ extension API.Command where T: ParseObject { } } - let batchCommand = BatchCommand(requests: commands) + let batchCommand = BatchCommand(requests: commands, transaction: transaction) return RESTBatchCommandType(method: .POST, path: .batch, body: batchCommand, mapper: mapper) } // MARK: Batch - Deleting - static func batch(commands: [API.NonParseBodyCommand]) -> RESTBatchCommandNoBodyType { + static func batch(commands: [API.NonParseBodyCommand], + transaction: Bool) -> RESTBatchCommandNoBodyType { let commands = commands.compactMap { (command) -> API.NonParseBodyCommand? in let path = ParseConfiguration.mountPath + command.path.urlComponent return API.NonParseBodyCommand( @@ -490,7 +494,7 @@ extension API.Command where T: ParseObject { } } - let batchCommand = BatchCommandNoBody(requests: commands) + let batchCommand = BatchCommandNoBody(requests: commands, transaction: transaction) return RESTBatchCommandNoBodyType(method: .POST, path: .batch, body: batchCommand, mapper: mapper) } } @@ -499,23 +503,24 @@ extension API.Command where T: ParseObject { //It's only needed for sending batches of childObjects which currently isn't being used. /* // MARK: Batch - Child Objects -extension API.Command where T: ParseType { +extension API.ChildCommand { internal var data: Data? { guard let body = body else { return nil } return try? ParseCoding.jsonEncoder().encode(body) } - static func batch(commands: [API.Command]) -> RESTBatchCommandTypeEncodable { - let commands = commands.compactMap { (command) -> API.Command? in + static func batch(commands: [API.ChildCommand], + transaction: Bool) -> RESTBatchCommandTypeEncodable { + let commands = commands.compactMap { (command) -> API.ChildCommand? in let path = ParseConfiguration.mountPath + command.path.urlComponent guard let body = command.body else { return nil } - return API.Command(method: command.method, path: .any(path), + return API.ChildCommand(method: command.method, path: .any(path), body: body, mapper: command.mapper) } - let bodies = commands.compactMap { (command) -> (body: T, command: API.Method)? in + let bodies = commands.compactMap { (command) -> (body: ParseType, command: API.Method)? in guard let body = command.body else { return nil } @@ -547,11 +552,11 @@ extension API.Command where T: ParseType { return [(.failure(parseError))] } } - let batchCommand = BatchCommand(requests: commands) + let batchCommand = BatchCommand(requests: commands, transaction: transaction) return RESTBatchCommandTypeEncodable(method: .POST, path: .batch, body: batchCommand, mapper: mapper) } -}*/ - +} +*/ // MARK: API.NonParseBodyCommand internal extension API { struct NonParseBodyCommand: Encodable where T: Encodable { @@ -614,7 +619,10 @@ internal extension API { // MARK: URL Preperation func prepareURLRequest(options: API.Options) -> Result { let params = self.params?.getQueryItems() - let headers = API.getHeaders(options: options) + var headers = API.getHeaders(options: options) + if !(method == .POST) && !(method == .PUT) { + headers.removeValue(forKey: "X-Parse-Request-Id") + } let url = ParseConfiguration.serverURL.appendingPathComponent(path.urlComponent) guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { @@ -667,4 +675,78 @@ internal extension API.NonParseBodyCommand { } } } -} // swiftlint:disable:this file_length +} +/* +// MARK: API.Command +internal extension API { + struct ChildCommand: ParseType { + typealias ReturnType = U + let method: API.Method + let path: API.Endpoint + let body: ParseType? + let mapper: ((Data) throws -> U) + let params: [String: String?]? + + init(method: API.Method, + path: API.Endpoint, + params: [String: String]? = nil, + body: ParseType? = nil, + mapper: @escaping ((Data) throws -> U)) { + self.method = method + self.path = path + self.body = body + self.mapper = mapper + self.params = params + } + } + + enum CodingKeys: String, CodingKey { + case method, body, path + } +} + +internal extension API.ChildCommand { + // MARK: Saving ParseObjects - Encodable + static func saveCommand(_ object: ParseType) throws -> API.ChildCommand { + guard let objectable = object as? Objectable else { + throw ParseError(code: .unknownError, message: "Not able to cast to objectable. Not saving") + } + if objectable.isSaved { + return try updateCommand(object) + } else { + return try createCommand(object) + } + } + + // MARK: Saving ParseObjects - Encodable - private + private static func createCommand(_ object: ParseType) throws -> API.ChildCommand { + guard var objectable = object as? Objectable else { + throw ParseError(code: .unknownError, message: "Not able to cast to objectable. Not saving") + } + let mapper = { (data: Data) -> PointerType in + let baseObjectable = try ParseCoding.jsonDecoder().decode(BaseObjectable.self, from: data) + objectable.objectId = baseObjectable.objectId + return try objectable.toPointer() + } + return API.ChildCommand(method: .POST, + path: objectable.endpoint, + body: object, + mapper: mapper) + } + + private static func updateCommand(_ object: ParseType) throws -> API.ChildCommand { + guard var objectable = object as? Objectable else { + throw ParseError(code: .unknownError, message: "Not able to cast to objectable. Not saving") + } + let mapper = { (data: Data) -> PointerType in + let baseObjectable = try ParseCoding.jsonDecoder().decode(BaseObjectable.self, from: data) + objectable.objectId = baseObjectable.objectId + return try objectable.toPointer() + } + return API.ChildCommand(method: .PUT, + path: objectable.endpoint, + body: object, + mapper: mapper) + } +}// swiftlint:disable:this file_length +*/ diff --git a/Sources/ParseSwift/API/API.swift b/Sources/ParseSwift/API/API.swift index f6088e118..13f434362 100644 --- a/Sources/ParseSwift/API/API.swift +++ b/Sources/ParseSwift/API/API.swift @@ -114,7 +114,7 @@ public struct API { /// Specify metadata. /// - note: This is typically used indirectly by `ParseFile`. case metadata([String: String]) - // Specify tags. + /// Specify tags. /// - note: This is typically used indirectly by `ParseFile`. case tags([String: String]) diff --git a/Sources/ParseSwift/API/BatchUtils.swift b/Sources/ParseSwift/API/BatchUtils.swift index 4d01208d6..4c2c39db6 100644 --- a/Sources/ParseSwift/API/BatchUtils.swift +++ b/Sources/ParseSwift/API/BatchUtils.swift @@ -25,12 +25,19 @@ typealias RESTBatchCommandTypeEncodable = API.Command: ParseType where T: ParseType { let requests: [API.Command] + var transaction: Bool } internal struct BatchCommandNoBody: Encodable where T: Encodable { let requests: [API.NonParseBodyCommand] + var transaction: Bool } - +/* +internal struct BatchChildCommand: ParseType { + let requests: [API.ChildCommand] + var transaction: Bool +} +*/ struct BatchUtils { static func splitArray(_ array: [U], valuesPerSegment: Int) -> [[U]] { if array.count < valuesPerSegment { diff --git a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift index 1493f8c88..e0f65c307 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift @@ -88,28 +88,51 @@ public extension Sequence where Element: ParseInstallation { /** Saves a collection of installations *asynchronously* and publishes when complete. - + - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched + is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. + Defaults to 50. + - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that + prevents the transaction from completing, then none of the objects are committed to the Parse Server database. - 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. - important: If an object saved has the same objectId as current, it will automatically update the current. + - warning: If `transaction = true`, then `batchLimit` will be automatically be set to the amount of the + objects in the transaction. The developer should ensure their respective Parse Servers can handle the limit or else + the transactions can fail. */ - func saveAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { + func saveAllPublisher(batchLimit limit: Int? = nil, + transaction: Bool = false, + options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in - self.saveAll(options: options, + self.saveAll(batchLimit: limit, + transaction: transaction, + options: options, completion: promise) } } /** Deletes a collection of installations *asynchronously* and publishes when complete. - + - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched + is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. + Defaults to 50. + - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that + prevents the transaction from completing, then none of the objects are committed to the Parse Server database. - 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. - important: If an object deleted has the same objectId as current, it will automatically update the current. + - warning: If `transaction = true`, then `batchLimit` will be automatically be set to the amount of the + objects in the transaction. The developer should ensure their respective Parse Servers can handle the limit or else + the transactions can fail. */ - func deleteAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { + func deleteAllPublisher(batchLimit limit: Int? = nil, + transaction: Bool = false, + options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in - self.deleteAll(options: options, completion: promise) + self.deleteAll(batchLimit: limit, + transaction: transaction, + options: options, + completion: promise) } } } diff --git a/Sources/ParseSwift/Objects/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift index 61a646838..9929c364b 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation.swift @@ -611,14 +611,19 @@ public extension Sequence where Element: ParseInstallation { is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. Defaults to 50. - parameter options: A set of header options sent to the server. Defaults to an empty set. + - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that + prevents the transaction from completing, then none of the objects are committed to the Parse Server database. - returns: Returns a Result enum with the object if a save was successful or a `ParseError` if it failed. - throws: `ParseError` - important: If an object saved has the same objectId as current, it will automatically update the current. + - warning: If `transaction = true`, then `batchLimit` will be automatically be set to the amount of the + objects in the transaction. The developer should ensure their respective Parse Servers can handle the limit or else + the transactions can fail. */ func saveAll(batchLimit limit: Int? = nil, // swiftlint:disable:this function_body_length + transaction: Bool = false, options: API.Options = []) throws -> [(Result)] { - let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit var childObjects = [String: PointerType]() var childFiles = [UUID: ParseFile]() var error: ParseError? @@ -664,10 +669,16 @@ public extension Sequence where Element: ParseInstallation { var returnBatch = [(Result)]() let commands = map { $0.saveCommand() } + let batchLimit: Int! + if transaction { + batchLimit = commands.count + } else { + batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + } let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) try batches.forEach { let currentBatch = try API.Command - .batch(commands: $0) + .batch(commands: $0, transaction: transaction) .execute(options: options, callbackQueue: .main, childObjects: childObjects, @@ -683,14 +694,20 @@ public extension Sequence where Element: ParseInstallation { - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. Defaults to 50. + - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that + prevents the transaction from completing, then none of the objects are committed to the Parse Server database. - 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<[(Result)], ParseError>)`. - important: If an object saved has the same objectId as current, it will automatically update the current. + - warning: If `transaction = true`, then `batchLimit` will be automatically be set to the amount of the + objects in the transaction. The developer should ensure their respective Parse Servers can handle the limit or else + the transactions can fail. */ func saveAll( // swiftlint:disable:this function_body_length cyclomatic_complexity batchLimit limit: Int? = nil, + transaction: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void @@ -698,7 +715,6 @@ public extension Sequence where Element: ParseInstallation { let queue = DispatchQueue(label: "com.parse.saveAll", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil) queue.sync { - let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit var childObjects = [String: PointerType]() var childFiles = [UUID: ParseFile]() var error: ParseError? @@ -748,11 +764,17 @@ public extension Sequence where Element: ParseInstallation { var returnBatch = [(Result)]() let commands = map { $0.saveCommand() } + let batchLimit: Int! + if transaction { + batchLimit = commands.count + } else { + batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + } let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) var completed = 0 for batch in batches { API.Command - .batch(commands: batch) + .batch(commands: batch, transaction: transaction) .executeAsync(options: options, callbackQueue: callbackQueue, childObjects: childObjects, @@ -888,6 +910,8 @@ public extension Sequence where Element: ParseInstallation { - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. Defaults to 50. + - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that + prevents the transaction from completing, then none of the objects are committed to the Parse Server database. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: Returns `nil` if the delete successful or a `ParseError` if it failed. @@ -900,16 +924,25 @@ public extension Sequence where Element: ParseInstallation { instance, a connection failure in the middle of the delete). - throws: `ParseError` - important: If an object deleted has the same objectId as current, it will automatically update the current. + - warning: If `transaction = true`, then `batchLimit` will be automatically be set to the amount of the + objects in the transaction. The developer should ensure their respective Parse Servers can handle the limit or else + the transactions can fail. */ func deleteAll(batchLimit limit: Int? = nil, + transaction: Bool = false, options: API.Options = []) throws -> [(Result)] { - let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit var returnBatch = [(Result)]() let commands = try map { try $0.deleteCommand() } + let batchLimit: Int! + if transaction { + batchLimit = commands.count + } else { + batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + } let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) try batches.forEach { let currentBatch = try API.Command)> - .batch(commands: $0) + .batch(commands: $0, transaction: transaction) .execute(options: options) returnBatch.append(contentsOf: currentBatch) } @@ -923,6 +956,8 @@ public extension Sequence where Element: ParseInstallation { - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. Defaults to 50. + - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that + prevents the transaction from completing, then none of the objects are committed to the Parse Server database. - 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. @@ -936,22 +971,31 @@ public extension Sequence where Element: ParseInstallation { caused the delete operation to be aborted partway through (for instance, a connection failure in the middle of the delete). - important: If an object deleted has the same objectId as current, it will automatically update the current. + - warning: If `transaction = true`, then `batchLimit` will be automatically be set to the amount of the + objects in the transaction. The developer should ensure their respective Parse Servers can handle the limit or else + the transactions can fail. */ func deleteAll( batchLimit limit: Int? = nil, + transaction: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void ) { - let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit do { var returnBatch = [(Result)]() let commands = try map({ try $0.deleteCommand() }) + let batchLimit: Int! + if transaction { + batchLimit = commands.count + } else { + batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + } let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) var completed = 0 for batch in batches { API.Command - .batch(commands: batch) + .batch(commands: batch, transaction: transaction) .executeAsync(options: options) { results in switch results { diff --git a/Sources/ParseSwift/Objects/ParseObject+combine.swift b/Sources/ParseSwift/Objects/ParseObject+combine.swift index 918f4455c..3abb33e04 100644 --- a/Sources/ParseSwift/Objects/ParseObject+combine.swift +++ b/Sources/ParseSwift/Objects/ParseObject+combine.swift @@ -85,28 +85,51 @@ public extension Sequence where Element: ParseObject { /** Saves a collection of objects *asynchronously* and publishes when complete. - + - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched + is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. + Defaults to 50. + - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that + prevents the transaction from completing, then none of the objects are committed to the Parse Server database. - 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. - important: If an object saved has the same objectId as current, it will automatically update the current. + - warning: If `transaction = true`, then `batchLimit` will be automatically be set to the amount of the + objects in the transaction. The developer should ensure their respective Parse Servers can handle the limit or else + the transactions can fail. */ - func saveAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { + func saveAllPublisher(batchLimit limit: Int? = nil, + transaction: Bool = false, + options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in - self.saveAll(options: options, + self.saveAll(batchLimit: limit, + transaction: transaction, + options: options, completion: promise) } } /** Deletes a collection of objects *asynchronously* and publishes when complete. - + - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched + is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. + Defaults to 50. + - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that + prevents the transaction from completing, then none of the objects are committed to the Parse Server database. - 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. - important: If an object deleted has the same objectId as current, it will automatically update the current. + - warning: If `transaction = true`, then `batchLimit` will be automatically be set to the amount of the + objects in the transaction. The developer should ensure their respective Parse Servers can handle the limit or else + the transactions can fail. */ - func deleteAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { + func deleteAllPublisher(batchLimit limit: Int? = nil, + transaction: Bool = false, + options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in - self.deleteAll(options: options, completion: promise) + self.deleteAll(batchLimit: limit, + transaction: transaction, + options: options, + completion: promise) } } } diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index aad1e618c..d607da5ee 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -62,14 +62,19 @@ public extension Sequence where Element: ParseObject { - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. Defaults to 50. + - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that + prevents the transaction from completing, then none of the objects are committed to the Parse Server database. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: Returns a Result enum with the object if a save was successful or a `ParseError` if it failed. - throws: `ParseError` + - warning: If `transaction = true`, then `batchLimit` will be automatically be set to the amount of the + objects in the transaction. The developer should ensure their respective Parse Servers can handle the limit or else + the transactions can fail. */ func saveAll(batchLimit limit: Int? = nil, // swiftlint:disable:this function_body_length + transaction: Bool = false, options: API.Options = []) throws -> [(Result)] { - let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit var childObjects = [String: PointerType]() var childFiles = [UUID: ParseFile]() var error: ParseError? @@ -115,10 +120,16 @@ public extension Sequence where Element: ParseObject { var returnBatch = [(Result)]() let commands = map { $0.saveCommand() } + let batchLimit: Int! + if transaction { + batchLimit = commands.count + } else { + batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + } let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) try batches.forEach { let currentBatch = try API.Command - .batch(commands: $0) + .batch(commands: $0, transaction: transaction) .execute(options: options, callbackQueue: .main, childObjects: childObjects, @@ -133,13 +144,19 @@ public extension Sequence where Element: ParseObject { - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. Defaults to 50. + - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that + prevents the transaction from completing, then none of the objects are committed to the Parse Server database. - 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<[(Result)], ParseError>)`. + - warning: If `transaction = true`, then `batchLimit` will be automatically be set to the amount of the + objects in the transaction. The developer should ensure their respective Parse Servers can handle the limit or else + the transactions can fail. */ func saveAll( // swiftlint:disable:this function_body_length cyclomatic_complexity batchLimit limit: Int? = nil, + transaction: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void @@ -148,7 +165,6 @@ public extension Sequence where Element: ParseObject { attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil) queue.sync { - let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit var childObjects = [String: PointerType]() var childFiles = [UUID: ParseFile]() var error: ParseError? @@ -197,11 +213,17 @@ public extension Sequence where Element: ParseObject { var returnBatch = [(Result)]() let commands = map { $0.saveCommand() } + let batchLimit: Int! + if transaction { + batchLimit = commands.count + } else { + batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + } let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) var completed = 0 for batch in batches { API.Command - .batch(commands: batch) + .batch(commands: batch, transaction: transaction) .executeAsync(options: options, callbackQueue: callbackQueue, childObjects: childObjects, @@ -330,6 +352,8 @@ public extension Sequence where Element: ParseObject { - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. Defaults to 50. + - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that + prevents the transaction from completing, then none of the objects are committed to the Parse Server database. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: Returns `nil` if the delete successful or a `ParseError` if it failed. @@ -341,16 +365,25 @@ public extension Sequence where Element: ParseObject { caused the delete operation to be aborted partway through (for instance, a connection failure in the middle of the delete). - throws: `ParseError` + - warning: If `transaction = true`, then `batchLimit` will be automatically be set to the amount of the + objects in the transaction. The developer should ensure their respective Parse Servers can handle the limit or else + the transactions can fail. */ func deleteAll(batchLimit limit: Int? = nil, + transaction: Bool = false, options: API.Options = []) throws -> [(Result)] { - let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit var returnBatch = [(Result)]() let commands = try map { try $0.deleteCommand() } + let batchLimit: Int! + if transaction { + batchLimit = commands.count + } else { + batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + } let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) try batches.forEach { let currentBatch = try API.Command)> - .batch(commands: $0) + .batch(commands: $0, transaction: transaction) .execute(options: options) returnBatch.append(contentsOf: currentBatch) } @@ -362,6 +395,8 @@ public extension Sequence where Element: ParseObject { - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. Defaults to 50. + - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that + prevents the transaction from completing, then none of the objects are committed to the Parse Server database. - 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. @@ -374,22 +409,31 @@ public extension Sequence where Element: ParseObject { 2. A non-aggregate Parse.Error. This indicates a serious error that caused the delete operation to be aborted partway through (for instance, a connection failure in the middle of the delete). + - warning: If `transaction = true`, then `batchLimit` will be automatically be set to the amount of the + objects in the transaction. The developer should ensure their respective Parse Servers can handle the limit or else + the transactions can fail. */ func deleteAll( batchLimit limit: Int? = nil, + transaction: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void ) { - let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit do { var returnBatch = [(Result)]() let commands = try map({ try $0.deleteCommand() }) + let batchLimit: Int! + if transaction { + batchLimit = commands.count + } else { + batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + } let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) var completed = 0 for batch in batches { API.Command - .batch(commands: batch) + .batch(commands: batch, transaction: transaction) .executeAsync(options: options) { results in switch results { @@ -433,11 +477,12 @@ public extension Sequence where Element: ParseObject { - returns: Returns a Result enum with the object if a save was successful or a `ParseError` if it failed. - throws: `ParseError` */ - func saveAll(options: API.Options = []) throws -> [(Result)] { + func saveAllParseTypes(transaction: Bool = true, + options: API.Options = []) throws -> [(Result)] { let commands = try map { try $0.saveCommand() } return try API.Command - .batch(commands: commands) - .execute(options: options) + .batch(commands: commands, transaction: transaction) + .execute(options: options, callbackQueue: .main) } }*/ @@ -625,7 +670,7 @@ extension ParseObject { var waitingToBeSaved = object.unsavedChildren while waitingToBeSaved.count > 0 { - var savableObjects = [Encodable]() + var savableObjects = [ParseType]() var savableFiles = [ParseFile]() var nextBatch = [ParseType]() try waitingToBeSaved.forEach { parseType in @@ -662,14 +707,21 @@ extension ParseObject { } //Currently, batch isn't working for Encodable - /*if let parseTypes = savableObjects as? [ParseType] { - let savedChildObjects = try self.saveAll(options: options, objects: parseTypes) + /*let savedChildObjects = try Self.saveAll(objects: savableObjects, + options: options) + let savedChildPointers = try savedChildObjects.compactMap { try $0.get() } + if savedChildPointers.count != savableObjects.count { + throw ParseError(code: .unknownError, message: "Couldn't save all child objects") + } + for (index, object) in savableObjects.enumerated() { + let hash = try BaseObjectable.createHash(object) + objectsFinishedSaving[hash] = savedChildPointers[index] }*/ + + //Saving children individually try savableObjects.forEach { let hash = try BaseObjectable.createHash($0) - if let parseType = $0 as? ParseType { - objectsFinishedSaving[hash] = try parseType.save(options: options) - } + objectsFinishedSaving[hash] = try $0.save(options: options) } try savableFiles.forEach { @@ -702,12 +754,18 @@ internal extension ParseType { func saveCommand() throws -> API.Command { try API.Command.saveCommand(self) } -/* - func saveAll(options: API.Options = [], objects: [T]) throws -> [(Result)] { - let commands = try objects.map { try API.Command.saveCommand($0) } - return try API.Command - .batch(commands: commands) - .execute(options: options) + /* + func saveAll(objects: [ParseType], + transaction: Bool = true, + options: API.Options = []) throws -> [(Result)] { + let commands = try objects.map { + try API.ChildCommand.saveCommand($0) + } + return try API.ChildCommand + .batch(commands: commands, + transaction: transaction) + .execute(options: options, + callbackQueue: .main) }*/ } diff --git a/Sources/ParseSwift/Objects/ParseUser+combine.swift b/Sources/ParseSwift/Objects/ParseUser+combine.swift index 619ff287b..17f91bbc2 100644 --- a/Sources/ParseSwift/Objects/ParseUser+combine.swift +++ b/Sources/ParseSwift/Objects/ParseUser+combine.swift @@ -210,28 +210,51 @@ public extension Sequence where Element: ParseUser { /** Saves a collection of users *asynchronously* and publishes when complete. - + - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched + is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. + Defaults to 50. + - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that + prevents the transaction from completing, then none of the objects are committed to the Parse Server database. - 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. - important: If an object saved has the same objectId as current, it will automatically update the current. + - warning: If `transaction = true`, then `batchLimit` will be automatically be set to the amount of the + objects in the transaction. The developer should ensure their respective Parse Servers can handle the limit or else + the transactions can fail. */ - func saveAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { + func saveAllPublisher(batchLimit limit: Int? = nil, + transaction: Bool = false, + options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in - self.saveAll(options: options, + self.saveAll(batchLimit: limit, + transaction: transaction, + options: options, completion: promise) } } /** Deletes a collection of users *asynchronously* and publishes when complete. - + - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched + is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. + Defaults to 50. + - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that + prevents the transaction from completing, then none of the objects are committed to the Parse Server database. - 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. - important: If an object deleted has the same objectId as current, it will automatically update the current. + - warning: If `transaction = true`, then `batchLimit` will be automatically be set to the amount of the + objects in the transaction. The developer should ensure their respective Parse Servers can handle the limit or else + the transactions can fail. */ - func deleteAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { + func deleteAllPublisher(batchLimit limit: Int? = nil, + transaction: Bool = false, + options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in - self.deleteAll(options: options, completion: promise) + self.deleteAll(batchLimit: limit, + transaction: transaction, + options: options, + completion: promise) } } } diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index 03cbbf414..ff53f872f 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -920,15 +920,20 @@ public extension Sequence where Element: ParseUser { - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. Defaults to 50. + - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that + prevents the transaction from completing, then none of the objects are committed to the Parse Server database. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: Returns a Result enum with the object if a save was successful or a `ParseError` if it failed. - throws: `ParseError` - important: If an object saved has the same objectId as current, it will automatically update the current. + - warning: If `transaction = true`, then `batchLimit` will be automatically be set to the amount of the + objects in the transaction. The developer should ensure their respective Parse Servers can handle the limit or else + the transactions can fail. */ func saveAll(batchLimit limit: Int? = nil, // swiftlint:disable:this function_body_length + transaction: Bool = false, options: API.Options = []) throws -> [(Result)] { - let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit var childObjects = [String: PointerType]() var childFiles = [UUID: ParseFile]() var error: ParseError? @@ -973,10 +978,16 @@ public extension Sequence where Element: ParseUser { var returnBatch = [(Result)]() let commands = map { $0.saveCommand() } + let batchLimit: Int! + if transaction { + batchLimit = commands.count + } else { + batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + } let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) try batches.forEach { let currentBatch = try API.Command - .batch(commands: $0) + .batch(commands: $0, transaction: transaction) .execute(options: options, callbackQueue: .main, childObjects: childObjects, @@ -992,14 +1003,20 @@ public extension Sequence where Element: ParseUser { - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. Defaults to 50. + - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that + prevents the transaction from completing, then none of the objects are committed to the Parse Server database. - 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<[(Result)], ParseError>)`. - important: If an object saved has the same objectId as current, it will automatically update the current. + - warning: If `transaction = true`, then `batchLimit` will be automatically be set to the amount of the + objects in the transaction. The developer should ensure their respective Parse Servers can handle the limit or else + the transactions can fail. */ func saveAll( // swiftlint:disable:this function_body_length cyclomatic_complexity batchLimit limit: Int? = nil, + transaction: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void @@ -1007,7 +1024,6 @@ public extension Sequence where Element: ParseUser { let queue = DispatchQueue(label: "com.parse.saveAll", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil) queue.sync { - let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit var childObjects = [String: PointerType]() var childFiles = [UUID: ParseFile]() var error: ParseError? @@ -1056,11 +1072,17 @@ public extension Sequence where Element: ParseUser { var returnBatch = [(Result)]() let commands = map { $0.saveCommand() } + let batchLimit: Int! + if transaction { + batchLimit = commands.count + } else { + batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + } let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) var completed = 0 for batch in batches { API.Command - .batch(commands: batch) + .batch(commands: batch, transaction: transaction) .executeAsync(options: options, callbackQueue: callbackQueue, childObjects: childObjects, @@ -1194,6 +1216,8 @@ public extension Sequence where Element: ParseUser { - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. Defaults to 50. + - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that + prevents the transaction from completing, then none of the objects are committed to the Parse Server database. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: Returns `nil` if the delete successful or a `ParseError` if it failed. @@ -1206,16 +1230,25 @@ public extension Sequence where Element: ParseUser { instance, a connection failure in the middle of the delete). - throws: `ParseError` - important: If an object deleted has the same objectId as current, it will automatically update the current. + - warning: If `transaction = true`, then `batchLimit` will be automatically be set to the amount of the + objects in the transaction. The developer should ensure their respective Parse Servers can handle the limit or else + the transactions can fail. */ func deleteAll(batchLimit limit: Int? = nil, + transaction: Bool = false, options: API.Options = []) throws -> [(Result)] { - let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit var returnBatch = [(Result)]() let commands = try map { try $0.deleteCommand() } + let batchLimit: Int! + if transaction { + batchLimit = commands.count + } else { + batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + } let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) try batches.forEach { let currentBatch = try API.Command - .batch(commands: $0) + .batch(commands: $0, transaction: transaction) .execute(options: options) returnBatch.append(contentsOf: currentBatch) } @@ -1228,6 +1261,8 @@ public extension Sequence where Element: ParseUser { - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. Defaults to 50. + - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that + prevents the transaction from completing, then none of the objects are committed to the Parse Server database. - 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. @@ -1241,22 +1276,31 @@ public extension Sequence where Element: ParseUser { caused the delete operation to be aborted partway through (for instance, a connection failure in the middle of the delete). - important: If an object deleted has the same objectId as current, it will automatically update the current. + - warning: If `transaction = true`, then `batchLimit` will be automatically be set to the amount of the + objects in the transaction. The developer should ensure their respective Parse Servers can handle the limit or else + the transactions can fail. */ func deleteAll( batchLimit limit: Int? = nil, + transaction: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void ) { - let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit do { var returnBatch = [(Result)]() let commands = try map({ try $0.deleteCommand() }) + let batchLimit: Int! + if transaction { + batchLimit = commands.count + } else { + batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + } let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) var completed = 0 for batch in batches { API.Command - .batch(commands: batch) + .batch(commands: batch, transaction: transaction) .executeAsync(options: options) { results in switch results { diff --git a/Sources/ParseSwift/ParseConstants.swift b/Sources/ParseSwift/ParseConstants.swift index d9b61af9d..0e9248139 100644 --- a/Sources/ParseSwift/ParseConstants.swift +++ b/Sources/ParseSwift/ParseConstants.swift @@ -9,7 +9,7 @@ import Foundation enum ParseConstants { - static let parseVersion = "1.1.6" + static let parseVersion = "1.1.7" static let hashingKey = "parseSwift" static let fileManagementDirectory = "parse/" static let fileManagementPrivateDocumentsDirectory = "Private Documents/" diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index e0be2eb7a..d91f242d8 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -267,6 +267,7 @@ public func containedBy (key: String, array: [T]) -> QueryConstraint where T: $lt, $lte, $gt, and $gte operators. - parameter time: The reference time, e.g. "12 days ago". - returns: The same instance of `QueryConstraint` as the receiver. + - warning: This only works with Parse Servers using mongoDB. */ public func relative(key: String, comparator: QueryConstraint.Comparator, time: String) -> QueryConstraint { QueryConstraint(key: key, value: [QueryConstraint.Comparator.relativeTime.rawValue: time], comparator: comparator) diff --git a/Tests/ParseSwiftTests/APICommandTests.swift b/Tests/ParseSwiftTests/APICommandTests.swift index 94c705e18..50e8302a5 100644 --- a/Tests/ParseSwiftTests/APICommandTests.swift +++ b/Tests/ParseSwiftTests/APICommandTests.swift @@ -12,6 +12,18 @@ import XCTest class APICommandTests: XCTestCase { + struct Level: ParseObject { + var objectId: String? + + var createdAt: Date? + + var updatedAt: Date? + + var ACL: ParseACL? + + var name = "First" + } + override func setUp() { super.setUp() guard let url = URL(string: "http://localhost:1337/1") else { @@ -154,5 +166,114 @@ class APICommandTests: XCTestCase { func testIdempodency() { let headers = API.getHeaders(options: []) XCTAssertNotNil(headers["X-Parse-Request-Id"]) + + let post = API.Command(method: .POST, path: .login) { _ in + return nil + } + switch post.prepareURLRequest(options: []) { + + case .success(let request): + if request.allHTTPHeaderFields?["X-Parse-Request-Id"] == nil { + XCTFail("Should contain idempotent header ID") + } + case .failure(let error): + XCTFail(error.localizedDescription) + } + + let put = API.Command(method: .PUT, path: .login) { _ in + return nil + } + switch put.prepareURLRequest(options: []) { + + case .success(let request): + if request.allHTTPHeaderFields?["X-Parse-Request-Id"] == nil { + XCTFail("Should contain idempotent header ID") + } + case .failure(let error): + XCTFail(error.localizedDescription) + } + + let delete = API.Command(method: .DELETE, path: .login) { _ in + return nil + } + switch delete.prepareURLRequest(options: []) { + + case .success(let request): + if request.allHTTPHeaderFields?["X-Parse-Request-Id"] != nil { + XCTFail("Should not contain idempotent header ID") + } + case .failure(let error): + XCTFail(error.localizedDescription) + } + + let get = API.Command(method: .GET, path: .login) { _ in + return nil + } + switch get.prepareURLRequest(options: []) { + + case .success(let request): + if request.allHTTPHeaderFields?["X-Parse-Request-Id"] != nil { + XCTFail("Should not contain idempotent header ID") + } + case .failure(let error): + XCTFail(error.localizedDescription) + } + } + + func testIdempodencyNoParseBody() { + let headers = API.getHeaders(options: []) + XCTAssertNotNil(headers["X-Parse-Request-Id"]) + + let post = API.NonParseBodyCommand(method: .POST, path: .login) { _ in + return nil + } + switch post.prepareURLRequest(options: []) { + + case .success(let request): + if request.allHTTPHeaderFields?["X-Parse-Request-Id"] == nil { + XCTFail("Should contain idempotent header ID") + } + case .failure(let error): + XCTFail(error.localizedDescription) + } + + let put = API.NonParseBodyCommand(method: .PUT, path: .login) { _ in + return nil + } + switch put.prepareURLRequest(options: []) { + + case .success(let request): + if request.allHTTPHeaderFields?["X-Parse-Request-Id"] == nil { + XCTFail("Should contain idempotent header ID") + } + case .failure(let error): + XCTFail(error.localizedDescription) + } + + let delete = API.NonParseBodyCommand(method: .DELETE, path: .login) { _ in + return nil + } + switch delete.prepareURLRequest(options: []) { + + case .success(let request): + if request.allHTTPHeaderFields?["X-Parse-Request-Id"] != nil { + XCTFail("Should not contain idempotent header ID") + } + case .failure(let error): + XCTFail(error.localizedDescription) + } + + let get = API.NonParseBodyCommand(method: .GET, path: .login) { _ in + return nil + } + switch get.prepareURLRequest(options: []) { + + case .success(let request): + if request.allHTTPHeaderFields?["X-Parse-Request-Id"] != nil { + XCTFail("Should not contain idempotent header ID") + } + case .failure(let error): + XCTFail(error.localizedDescription) + } } } diff --git a/Tests/ParseSwiftTests/ParseInstallationTests.swift b/Tests/ParseSwiftTests/ParseInstallationTests.swift index 7826ab017..fea297e30 100644 --- a/Tests/ParseSwiftTests/ParseInstallationTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationTests.swift @@ -953,11 +953,13 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l MockURLProtocol.removeAll() let expectation1 = XCTestExpectation(description: "Fetch installation1") + let expectation2 = XCTestExpectation(description: "Fetch installation2") DispatchQueue.main.async { guard var installation = Installation.current else { XCTFail("Should unwrap dates") expectation1.fulfill() + expectation2.fulfill() return } @@ -974,6 +976,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l } catch { XCTFail("Should encode/decode. Error \(error)") expectation1.fulfill() + expectation2.fulfill() return } MockURLProtocol.mockRequests { _ in @@ -1032,8 +1035,60 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l } expectation1.fulfill() + + do { + let saved2 = try [installation].saveAll(transaction: true) + saved2.forEach { + switch $0 { + case .success(let saved): + XCTAssert(saved.hasSameObjectId(as: installation)) + guard let savedCreatedAt = saved.createdAt, + let savedUpdatedAt = saved.updatedAt else { + XCTFail("Should unwrap dates") + expectation2.fulfill() + return + } + guard let originalCreatedAt = installation.createdAt, + let originalUpdatedAt = installation.updatedAt, + let serverUpdatedAt = installation.updatedAt else { + XCTFail("Should unwrap dates") + expectation2.fulfill() + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertEqual(savedUpdatedAt, serverUpdatedAt) + XCTAssertEqual(Installation.current?.customKey, installation.customKey) + + //Should be updated in memory + guard let updatedCurrentDate = Installation.current?.updatedAt else { + XCTFail("Should unwrap current date") + expectation2.fulfill() + return + } + XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) + + #if !os(Linux) + //Should be updated in Keychain + guard let keychainInstallation: CurrentInstallationContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation), + let keychainUpdatedCurrentDate = keychainInstallation.currentInstallation?.updatedAt else { + XCTFail("Should get object from Keychain") + expectation2.fulfill() + return + } + XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) + #endif + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + } + } catch { + XCTFail(error.localizedDescription) + } + expectation2.fulfill() } - wait(for: [expectation1], timeout: 20.0) + wait(for: [expectation1, expectation2], timeout: 20.0) } // swiftlint:disable:next function_body_length @@ -1042,10 +1097,12 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l MockURLProtocol.removeAll() let expectation1 = XCTestExpectation(description: "Fetch installation1") + let expectation2 = XCTestExpectation(description: "Fetch installation2") DispatchQueue.main.async { guard var installation = Installation.current else { XCTFail("Should unwrap") expectation1.fulfill() + expectation2.fulfill() return } @@ -1062,6 +1119,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l } catch { XCTFail("Should encode/decode. Error \(error)") expectation1.fulfill() + expectation2.fulfill() return } MockURLProtocol.mockRequests { _ in @@ -1122,8 +1180,63 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l } expectation1.fulfill() } + + [installation].saveAll(transaction: true) { results in + switch results { + + case .success(let saved): + saved.forEach { + switch $0 { + case .success(let saved): + XCTAssert(saved.hasSameObjectId(as: installation)) + guard let savedCreatedAt = saved.createdAt, + let savedUpdatedAt = saved.updatedAt else { + XCTFail("Should unwrap dates") + expectation2.fulfill() + return + } + guard let originalCreatedAt = installation.createdAt, + let originalUpdatedAt = installation.updatedAt, + let serverUpdatedAt = installation.updatedAt else { + XCTFail("Should unwrap dates") + expectation2.fulfill() + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertEqual(savedUpdatedAt, serverUpdatedAt) + XCTAssertEqual(Installation.current?.customKey, installation.customKey) + + //Should be updated in memory + guard let updatedCurrentDate = Installation.current?.updatedAt else { + XCTFail("Should unwrap current date") + expectation2.fulfill() + return + } + XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) + #if !os(Linux) + //Should be updated in Keychain + guard let keychainInstallation: CurrentInstallationContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation), + let keychainUpdatedCurrentDate = keychainInstallation + .currentInstallation?.updatedAt else { + XCTFail("Should get object from Keychain") + expectation2.fulfill() + return + } + XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) + #endif + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + } + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + expectation2.fulfill() + } } - wait(for: [expectation1], timeout: 20.0) + wait(for: [expectation1, expectation2], timeout: 20.0) } func testDeleteAll() { @@ -1131,11 +1244,13 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l MockURLProtocol.removeAll() let expectation1 = XCTestExpectation(description: "Delete installation1") + let expectation2 = XCTestExpectation(description: "Delete installation2") DispatchQueue.main.async { guard let installation = Installation.current else { - XCTFail("Should unwrap dates") + XCTFail("Should unwrap dates") expectation1.fulfill() + expectation2.fulfill() return } @@ -1147,6 +1262,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l } catch { XCTFail("Should encode/decode. Error \(error)") expectation1.fulfill() + expectation2.fulfill() return } MockURLProtocol.mockRequests { _ in @@ -1165,8 +1281,21 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l } expectation1.fulfill() + + do { + let deleted = try [installation].deleteAll(transaction: true) + deleted.forEach { + if case let .failure(error) = $0 { + XCTFail("Should have deleted: \(error.localizedDescription)") + } + } + } catch { + XCTFail(error.localizedDescription) + } + + expectation2.fulfill() } - wait(for: [expectation1], timeout: 20.0) + wait(for: [expectation1, expectation2], timeout: 20.0) } func testDeleteAllAsyncMainQueue() { @@ -1174,10 +1303,12 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l MockURLProtocol.removeAll() let expectation1 = XCTestExpectation(description: "Delete installation1") + let expectation2 = XCTestExpectation(description: "Delete installation2") DispatchQueue.main.async { guard let installation = Installation.current else { XCTFail("Should unwrap") expectation1.fulfill() + expectation2.fulfill() return } @@ -1189,6 +1320,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l } catch { XCTFail("Should encode/decode. Error \(error)") expectation1.fulfill() + expectation2.fulfill() return } MockURLProtocol.mockRequests { _ in @@ -1209,8 +1341,23 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l } expectation1.fulfill() } + + [installation].deleteAll(transaction: true) { results in + switch results { + + case .success(let deleted): + deleted.forEach { + if case let .failure(error) = $0 { + XCTFail("Should have deleted: \(error.localizedDescription)") + } + } + case .failure(let error): + XCTFail("Should have deleted: \(error.localizedDescription)") + } + expectation2.fulfill() + } } - wait(for: [expectation1], timeout: 20.0) + wait(for: [expectation1, expectation2], timeout: 20.0) } } // swiftlint:disable:this file_length diff --git a/Tests/ParseSwiftTests/ParseObjectBatchTests.swift b/Tests/ParseSwiftTests/ParseObjectBatchTests.swift index cc17c1a7c..8461f18ce 100644 --- a/Tests/ParseSwiftTests/ParseObjectBatchTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectBatchTests.swift @@ -74,9 +74,9 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le let objects = [score, score2] let commands = objects.map { $0.saveCommand() } - let body = BatchCommand(requests: commands) + let body = BatchCommand(requests: commands, transaction: false) // swiftlint:disable:next line_length - let expected = "{\"requests\":[{\"path\":\"\\/classes\\/GameScore\",\"method\":\"POST\",\"body\":{\"score\":10}},{\"path\":\"\\/classes\\/GameScore\",\"method\":\"POST\",\"body\":{\"score\":20}}]}" + let expected = "{\"requests\":[{\"path\":\"\\/classes\\/GameScore\",\"method\":\"POST\",\"body\":{\"score\":10}},{\"path\":\"\\/classes\\/GameScore\",\"method\":\"POST\",\"body\":{\"score\":20}}],\"transaction\":false}" let encoded = try ParseCoding.parseEncoder() .encode(body, collectChildren: false, objectsSavedBeforeThisOne: nil, @@ -173,7 +173,8 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } do { - let saved = try [score, score2].saveAll(options: [.installationId("hello")]) + let saved = try [score, score2].saveAll(transaction: true, + options: [.installationId("hello")]) XCTAssertEqual(saved.count, 2) switch saved[0] { @@ -258,7 +259,8 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } do { - let saved = try [score, score2].saveAll(options: [.useMasterKey]) + let saved = try [score, score2].saveAll(transaction: true, + options: [.useMasterKey]) XCTAssertEqual(saved.count, 2) XCTAssertThrowsError(try saved[0].get()) @@ -291,9 +293,9 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le return API.Command(method: command.method, path: .any(path), body: body, mapper: command.mapper) } - let body = BatchCommand(requests: commands) + let body = BatchCommand(requests: commands, transaction: false) // swiftlint:disable:next line_length - let expected = "{\"requests\":[{\"path\":\"\\/1\\/classes\\/GameScore\\/yarr\",\"method\":\"PUT\",\"body\":{\"score\":10}},{\"path\":\"\\/1\\/classes\\/GameScore\\/yolo\",\"method\":\"PUT\",\"body\":{\"score\":20}}]}" + let expected = "{\"requests\":[{\"path\":\"\\/1\\/classes\\/GameScore\\/yarr\",\"method\":\"PUT\",\"body\":{\"score\":10}},{\"path\":\"\\/1\\/classes\\/GameScore\\/yolo\",\"method\":\"PUT\",\"body\":{\"score\":20}}],\"transaction\":false}" let encoded = try ParseCoding.parseEncoder() .encode(body, collectChildren: false, @@ -389,7 +391,8 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } do { - let saved = try [score, score2].saveAll(options: [.useMasterKey]) + let saved = try [score, score2].saveAll(transaction: true, + options: [.useMasterKey]) XCTAssertEqual(saved.count, 2) switch saved[0] { @@ -468,7 +471,8 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } do { - let saved = try [score, score2].saveAll(options: [.useMasterKey]) + let saved = try [score, score2].saveAll(transaction: true, + options: [.useMasterKey]) XCTAssertEqual(saved.count, 2) XCTAssertThrowsError(try saved[0].get()) @@ -566,7 +570,8 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } do { - let saved = try [score, score2].saveAll(options: [.useMasterKey]) + let saved = try [score, score2].saveAll(transaction: true, + options: [.useMasterKey]) XCTAssertEqual(saved.count, 2) switch saved[0] { @@ -599,10 +604,11 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le guard let scoreOnServer = scoresOnServer.first, let scoreOnServer2 = scoresOnServer.last else { XCTFail("Should unwrap") + expectation1.fulfill() return } - scores.saveAll(options: [], callbackQueue: callbackQueue) { result in + scores.saveAll(callbackQueue: callbackQueue) { result in switch result { @@ -670,7 +676,9 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } let expectation2 = XCTestExpectation(description: "Save object2") - scores.saveAll(options: [.useMasterKey], callbackQueue: callbackQueue) { result in + scores.saveAll(transaction: true, + options: [.useMasterKey], + callbackQueue: callbackQueue) { result in switch result { @@ -825,7 +833,7 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le let expectation1 = XCTestExpectation(description: "Update object1") - scores.saveAll(options: [], callbackQueue: callbackQueue) { result in + scores.saveAll(callbackQueue: callbackQueue) { result in switch result { @@ -885,7 +893,9 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } let expectation2 = XCTestExpectation(description: "Update object2") - scores.saveAll(options: [.useMasterKey], callbackQueue: callbackQueue) { result in + scores.saveAll(transaction: true, + options: [.useMasterKey], + callbackQueue: callbackQueue) { result in switch result { @@ -1329,6 +1339,32 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } catch { XCTFail(error.localizedDescription) } + + do { + let deleted = try [GameScore(objectId: "yarr"), GameScore(objectId: "yolo")] + .deleteAll(transaction: true) + + XCTAssertEqual(deleted.count, 2) + guard let firstObject = deleted.first else { + XCTFail("Should unwrap") + return + } + + if case let .failure(error) = firstObject { + XCTFail(error.localizedDescription) + } + + guard let lastObject = deleted.last else { + XCTFail("Should unwrap") + return + } + + if case let .failure(error) = lastObject { + XCTFail(error.localizedDescription) + } + } catch { + XCTFail(error.localizedDescription) + } } #if !os(Linux) @@ -1382,9 +1418,10 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le func deleteAllAsync(callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Delete object1") + let expectation2 = XCTestExpectation(description: "Delete object2") - [GameScore(objectId: "yarr"), GameScore(objectId: "yolo")].deleteAll(options: [], - callbackQueue: callbackQueue) { result in + [GameScore(objectId: "yarr"), GameScore(objectId: "yolo")] + .deleteAll(callbackQueue: callbackQueue) { result in switch result { @@ -1416,7 +1453,41 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le expectation1.fulfill() } - wait(for: [expectation1], timeout: 20.0) + [GameScore(objectId: "yarr"), GameScore(objectId: "yolo")] + .deleteAll(transaction: true, + callbackQueue: callbackQueue) { result in + + switch result { + + case .success(let deleted): + XCTAssertEqual(deleted.count, 2) + guard let firstObject = deleted.first else { + XCTFail("Should unwrap") + expectation2.fulfill() + return + } + + if case let .failure(error) = firstObject { + XCTFail(error.localizedDescription) + } + + guard let lastObject = deleted.last else { + XCTFail("Should unwrap") + expectation2.fulfill() + return + } + + if case let .failure(error) = lastObject { + XCTFail(error.localizedDescription) + } + + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation2.fulfill() + } + + wait(for: [expectation1, expectation2], timeout: 20.0) } func testDeleteAllAsyncMainQueue() { @@ -1440,8 +1511,8 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le let expectation1 = XCTestExpectation(description: "Delete object1") - [GameScore(objectId: "yarr"), GameScore(objectId: "yolo")].deleteAll(options: [], - callbackQueue: callbackQueue) { result in + [GameScore(objectId: "yarr"), GameScore(objectId: "yolo")] + .deleteAll(callbackQueue: callbackQueue) { result in switch result { diff --git a/Tests/ParseSwiftTests/ParseUserTests.swift b/Tests/ParseSwiftTests/ParseUserTests.swift index aee6f831e..9e235ee41 100644 --- a/Tests/ParseSwiftTests/ParseUserTests.swift +++ b/Tests/ParseSwiftTests/ParseUserTests.swift @@ -1567,12 +1567,14 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length testLogin() MockURLProtocol.removeAll() - let expectation1 = XCTestExpectation(description: "Fetch user1") + let expectation1 = XCTestExpectation(description: "Save user1") + let expectation2 = XCTestExpectation(description: "Save user2") DispatchQueue.main.async { guard var user = User.current else { XCTFail("Should unwrap dates") expectation1.fulfill() + expectation2.fulfill() return } @@ -1589,6 +1591,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } catch { XCTFail("Should encode/decode. Error \(error)") expectation1.fulfill() + expectation2.fulfill() return } MockURLProtocol.mockRequests { _ in @@ -1647,8 +1650,61 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } expectation1.fulfill() + + do { + let saved = try [user].saveAll(transaction: true) + saved.forEach { + switch $0 { + case .success(let saved): + XCTAssert(saved.hasSameObjectId(as: user)) + guard let savedCreatedAt = saved.createdAt, + let savedUpdatedAt = saved.updatedAt else { + XCTFail("Should unwrap dates") + expectation2.fulfill() + return + } + guard let originalCreatedAt = user.createdAt, + let originalUpdatedAt = user.updatedAt, + let serverUpdatedAt = user.updatedAt else { + XCTFail("Should unwrap dates") + expectation2.fulfill() + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertEqual(savedUpdatedAt, serverUpdatedAt) + XCTAssertEqual(User.current?.customKey, user.customKey) + + //Should be updated in memory + guard let updatedCurrentDate = User.current?.updatedAt else { + XCTFail("Should unwrap current date") + expectation2.fulfill() + return + } + XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) + + #if !os(Linux) + //Should be updated in Keychain + guard let keychainUser: CurrentUserContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser), + let keychainUpdatedCurrentDate = keychainUser.currentUser?.updatedAt else { + XCTFail("Should get object from Keychain") + expectation2.fulfill() + return + } + XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) + #endif + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + } + } catch { + XCTFail(error.localizedDescription) + } + + expectation2.fulfill() } - wait(for: [expectation1], timeout: 20.0) + wait(for: [expectation1, expectation2], timeout: 20.0) } // swiftlint:disable:next function_body_length @@ -1656,11 +1712,14 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length testLogin() MockURLProtocol.removeAll() - let expectation1 = XCTestExpectation(description: "Fetch user1") + let expectation1 = XCTestExpectation(description: "Save user1") + let expectation2 = XCTestExpectation(description: "Save user2") + DispatchQueue.main.async { guard var user = User.current else { XCTFail("Should unwrap") expectation1.fulfill() + expectation2.fulfill() return } @@ -1677,6 +1736,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } catch { XCTFail("Should encode/decode. Error \(error)") expectation1.fulfill() + expectation2.fulfill() return } MockURLProtocol.mockRequests { _ in @@ -1737,8 +1797,63 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } expectation1.fulfill() } + + [user].saveAll(transaction: true) { results in + switch results { + + case .success(let saved): + saved.forEach { + switch $0 { + case .success(let saved): + XCTAssert(saved.hasSameObjectId(as: user)) + guard let savedCreatedAt = saved.createdAt, + let savedUpdatedAt = saved.updatedAt else { + XCTFail("Should unwrap dates") + expectation2.fulfill() + return + } + guard let originalCreatedAt = user.createdAt, + let originalUpdatedAt = user.updatedAt, + let serverUpdatedAt = user.updatedAt else { + XCTFail("Should unwrap dates") + expectation2.fulfill() + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertEqual(savedUpdatedAt, serverUpdatedAt) + XCTAssertEqual(User.current?.customKey, user.customKey) + + //Should be updated in memory + guard let updatedCurrentDate = User.current?.updatedAt else { + XCTFail("Should unwrap current date") + expectation2.fulfill() + return + } + XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) + + #if !os(Linux) + //Should be updated in Keychain + guard let keychainUser: CurrentUserContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser), + let keychainUpdatedCurrentDate = keychainUser.currentUser?.updatedAt else { + XCTFail("Should get object from Keychain") + expectation2.fulfill() + return + } + XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) + #endif + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + } + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + expectation2.fulfill() + } } - wait(for: [expectation1], timeout: 20.0) + wait(for: [expectation1, expectation2], timeout: 20.0) } func testDeleteAll() { @@ -1746,12 +1861,14 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length MockURLProtocol.removeAll() let expectation1 = XCTestExpectation(description: "Delete user1") + let expectation2 = XCTestExpectation(description: "Delete user2") DispatchQueue.main.async { guard let user = User.current else { - XCTFail("Should unwrap dates") - expectation1.fulfill() - return + XCTFail("Should unwrap dates") + expectation1.fulfill() + expectation2.fulfill() + return } let userOnServer = [BatchResponseItem(success: NoBody(), error: nil)] @@ -1762,6 +1879,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } catch { XCTFail("Should encode/decode. Error \(error)") expectation1.fulfill() + expectation2.fulfill() return } MockURLProtocol.mockRequests { _ in @@ -1780,8 +1898,21 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } expectation1.fulfill() + + do { + let deleted = try [user].deleteAll(transaction: true) + deleted.forEach { + if case let .failure(error) = $0 { + XCTFail("Should have deleted: \(error.localizedDescription)") + } + } + } catch { + XCTFail(error.localizedDescription) + } + + expectation2.fulfill() } - wait(for: [expectation1], timeout: 20.0) + wait(for: [expectation1, expectation2], timeout: 20.0) } func testDeleteAllAsyncMainQueue() { @@ -1789,10 +1920,13 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length MockURLProtocol.removeAll() let expectation1 = XCTestExpectation(description: "Delete user1") + let expectation2 = XCTestExpectation(description: "Delete user2") + DispatchQueue.main.async { guard let user = User.current else { XCTFail("Should unwrap") expectation1.fulfill() + expectation2.fulfill() return } @@ -1804,6 +1938,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } catch { XCTFail("Should encode/decode. Error \(error)") expectation1.fulfill() + expectation2.fulfill() return } MockURLProtocol.mockRequests { _ in @@ -1824,8 +1959,23 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } expectation1.fulfill() } + + [user].deleteAll(transaction: true) { results in + switch results { + + case .success(let deleted): + deleted.forEach { + if case let .failure(error) = $0 { + XCTFail("Should have deleted: \(error.localizedDescription)") + } + } + case .failure(let error): + XCTFail("Should have deleted: \(error.localizedDescription)") + } + expectation2.fulfill() + } } - wait(for: [expectation1], timeout: 20.0) + wait(for: [expectation1, expectation2], timeout: 20.0) } func testMeCommand() {