From 8939e47495033ce036da7d403cc86eedd14b43b7 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Mon, 8 Mar 2021 12:15:02 -0500 Subject: [PATCH 01/11] Add batch transactions --- CHANGELOG.md | 4 + .../Contents.swift | 23 +++ ParseSwift.podspec | 2 +- ParseSwift.xcodeproj/project.pbxproj | 16 +- Scripts/jazzy.sh | 2 +- Sources/ParseSwift/API/API+Commands.swift | 9 +- Sources/ParseSwift/API/API.swift | 2 +- Sources/ParseSwift/API/BatchUtils.swift | 2 + .../Objects/ParseInstallation+combine.swift | 35 +++- .../Objects/ParseInstallation.swift | 60 ++++++- .../Objects/ParseObject+combine.swift | 35 +++- Sources/ParseSwift/Objects/ParseObject.swift | 60 ++++++- .../Objects/ParseUser+combine.swift | 35 +++- Sources/ParseSwift/Objects/ParseUser.swift | 60 ++++++- Sources/ParseSwift/ParseConstants.swift | 2 +- .../ParseInstallationTests.swift | 157 +++++++++++++++- .../ParseObjectBatchTests.swift | 107 +++++++++-- Tests/ParseSwiftTests/ParseUserTests.swift | 170 ++++++++++++++++-- 18 files changed, 690 insertions(+), 91 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24551e7e0..4fe8e7a79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,11 @@ [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.1.6...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..5426fa30d 100644 --- a/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift @@ -101,6 +101,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 { 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..710c8e9cc 100644 --- a/Sources/ParseSwift/API/API+Commands.swift +++ b/Sources/ParseSwift/API/API+Commands.swift @@ -410,7 +410,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 +452,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 +491,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) } } 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..a54107f36 100644 --- a/Sources/ParseSwift/API/BatchUtils.swift +++ b/Sources/ParseSwift/API/BatchUtils.swift @@ -25,10 +25,12 @@ 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 } struct BatchUtils { 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..7664e815b 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 { 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/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..fb4270cb8 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() + 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 @@ -1769,7 +1887,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } do { - let deleted = try [user].deleteAll() + let deleted = try [user].deleteAll(transaction: true) deleted.forEach { if case let .failure(error) = $0 { XCTFail("Should have deleted: \(error.localizedDescription)") @@ -1780,8 +1898,21 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } expectation1.fulfill() + + do { + let deleted = try [user].deleteAll() + 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 { 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() { From 264925a883e1f9db9c3d9f5fd274d26c4f4bef93 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Mon, 8 Mar 2021 13:11:56 -0500 Subject: [PATCH 02/11] hit all codecov --- Tests/ParseSwiftTests/ParseUserTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/ParseSwiftTests/ParseUserTests.swift b/Tests/ParseSwiftTests/ParseUserTests.swift index fb4270cb8..9e235ee41 100644 --- a/Tests/ParseSwiftTests/ParseUserTests.swift +++ b/Tests/ParseSwiftTests/ParseUserTests.swift @@ -1652,7 +1652,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length expectation1.fulfill() do { - let saved = try [user].saveAll() + let saved = try [user].saveAll(transaction: true) saved.forEach { switch $0 { case .success(let saved): @@ -1887,7 +1887,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } do { - let deleted = try [user].deleteAll(transaction: true) + let deleted = try [user].deleteAll() deleted.forEach { if case let .failure(error) = $0 { XCTFail("Should have deleted: \(error.localizedDescription)") @@ -1900,7 +1900,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length expectation1.fulfill() do { - let deleted = try [user].deleteAll() + let deleted = try [user].deleteAll(transaction: true) deleted.forEach { if case let .failure(error) = $0 { XCTFail("Should have deleted: \(error.localizedDescription)") @@ -1960,7 +1960,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length expectation1.fulfill() } - [user].deleteAll { results in + [user].deleteAll(transaction: true) { results in switch results { case .success(let deleted): From 82127252089d796c607d57119f9aa07699d77cea Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Mon, 8 Mar 2021 18:56:13 -0500 Subject: [PATCH 03/11] Move around playgrounds --- .../Contents.swift | 36 ++++++++-------- .../5 - ACL.xcplaygroundpage/Contents.swift | 43 +++++++++++++++++++ 2 files changed, 61 insertions(+), 18 deletions(-) diff --git a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift index 6cd3d0f98..5a289840b 100644 --- a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift @@ -47,24 +47,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() @@ -94,6 +76,24 @@ 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 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)") + } +} + //: 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. diff --git a/ParseSwift.playground/Pages/5 - ACL.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/5 - ACL.xcplaygroundpage/Contents.swift index 24b6bb7f0..97bf543ff 100644 --- a/ParseSwift.playground/Pages/5 - ACL.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/5 - ACL.xcplaygroundpage/Contents.swift @@ -7,6 +7,49 @@ import ParseSwift PlaygroundPage.current.needsIndefiniteExecution = true initializeParse() +struct User: ParseUser { + //: These are required for `ParseObject`. + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + //: These are required for `ParseUser`. + var username: String? + var email: String? + var password: String? + var authData: [String: [String: String]?]? +} + +//: Logging out - synchronously +do { + try User.logout() + print("Successfully logged out") +} catch let error { + print("Error logging out: \(error)") +} + +/*: Login - asynchronously - Performs work on background + queue and returns to designated on designated callbackQueue. + If no callbackQueue is specified it returns to main queue. +*/ +User.login(username: "hello", password: "world") { results in + + switch results { + case .success(let user): + + guard let currentUser = User.current else { + assertionFailure("Error: current user currently not stored locally") + return + } + assert(currentUser.hasSameObjectId(as: user)) + print("Successfully logged in as user: \(user)") + + case .failure(let error): + print("Error logging in: \(error)") + } +} + do { var acl = ParseACL() acl.publicRead = true From 7d25508084e55eff45783aef6479ba703643ea71 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Mon, 8 Mar 2021 19:40:06 -0500 Subject: [PATCH 04/11] Add build notes at top of playground files --- .../Contents.swift | 6 +++ .../Contents.swift | 6 +++ .../Contents.swift | 7 +++ .../Contents.swift | 6 +++ .../Contents.swift | 6 +++ .../Contents.swift | 6 +++ .../Contents.swift | 6 +++ .../Contents.swift | 6 +++ .../Contents.swift | 6 +++ .../5 - ACL.xcplaygroundpage/Contents.swift | 49 +++---------------- .../Contents.swift | 6 +++ .../Contents.swift | 6 +++ .../Contents.swift | 6 +++ .../9 - Files.xcplaygroundpage/Contents.swift | 6 +++ 14 files changed, 85 insertions(+), 43 deletions(-) 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 5426fa30d..d3a46ce45 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 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/10 - Cloud Code.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/10 - Cloud Code.xcplaygroundpage/Contents.swift index e4a6ee0a5..5c80b2fbe 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 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..244e596e9 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 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..095204826 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 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..3029c00c8 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 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..77082a23a 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 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..85eb2f205 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 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/3 - User - Sign Up.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift index 367bb02d7..9bc519dc2 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 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 diff --git a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift index 5a289840b..fe3a5ff2b 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 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/5 - ACL.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/5 - ACL.xcplaygroundpage/Contents.swift index 97bf543ff..97f78610c 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 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 @@ -7,49 +13,6 @@ import ParseSwift PlaygroundPage.current.needsIndefiniteExecution = true initializeParse() -struct User: ParseUser { - //: These are required for `ParseObject`. - var objectId: String? - var createdAt: Date? - var updatedAt: Date? - var ACL: ParseACL? - - //: These are required for `ParseUser`. - var username: String? - var email: String? - var password: String? - var authData: [String: [String: String]?]? -} - -//: Logging out - synchronously -do { - try User.logout() - print("Successfully logged out") -} catch let error { - print("Error logging out: \(error)") -} - -/*: Login - asynchronously - Performs work on background - queue and returns to designated on designated callbackQueue. - If no callbackQueue is specified it returns to main queue. -*/ -User.login(username: "hello", password: "world") { results in - - switch results { - case .success(let user): - - guard let currentUser = User.current else { - assertionFailure("Error: current user currently not stored locally") - return - } - assert(currentUser.hasSameObjectId(as: user)) - print("Successfully logged in as user: \(user)") - - case .failure(let error): - print("Error logging in: \(error)") - } -} - do { var acl = ParseACL() acl.publicRead = true diff --git a/ParseSwift.playground/Pages/6 - Installation.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/6 - Installation.xcplaygroundpage/Contents.swift index a50403a1d..b4db74d1f 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 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/7 - GeoPoint.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift index 97dd63c9f..06228f083 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 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/8 - Pointers.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/8 - Pointers.xcplaygroundpage/Contents.swift index ff1fb4415..1fd07dedf 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 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/9 - Files.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/9 - Files.xcplaygroundpage/Contents.swift index f156a3980..90ffae253 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 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 From 3d1aea7fa547cae6b7acf6f32d5ac731a18fd66d Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Mon, 8 Mar 2021 19:53:02 -0500 Subject: [PATCH 05/11] Switch playgrounds to default to macOS --- ParseSwift.playground/contents.xcplayground | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 @@ - + From c1756668c2b8f997b2de14eadbe5305753f77022 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Mon, 8 Mar 2021 22:26:47 -0500 Subject: [PATCH 06/11] Only send idempotent id in header on POST and PUT --- Sources/ParseSwift/API/API+Commands.swift | 10 +- Tests/ParseSwiftTests/APICommandTests.swift | 121 ++++++++++++++++++++ 2 files changed, 129 insertions(+), 2 deletions(-) diff --git a/Sources/ParseSwift/API/API+Commands.swift b/Sources/ParseSwift/API/API+Commands.swift index 710c8e9cc..71a0e3ead 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! @@ -615,7 +618,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 { 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) + } } } From cf72311a3dba0091649ca36f57b6cfe6d72ff7a2 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Tue, 9 Mar 2021 15:58:35 -0500 Subject: [PATCH 07/11] Improve pointer example in playgrounds by adding include and includeAll --- .../Contents.swift | 14 ++--- .../Contents.swift | 63 ++++++++++++++++--- 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift index 85eb2f205..895c40283 100644 --- a/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift @@ -37,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 designated on designated 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): @@ -56,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 @@ -65,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 designated on designated 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/8 - Pointers.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/8 - Pointers.xcplaygroundpage/Contents.swift index 1fd07dedf..367f86f30 100644 --- a/ParseSwift.playground/Pages/8 - Pointers.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/8 - Pointers.xcplaygroundpage/Contents.swift @@ -22,7 +22,7 @@ struct Book: ParseObject { var ACL: ParseACL? //: Your own properties. - var title: String + var title: String? init(title: String) { self.title = title @@ -58,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)") @@ -81,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) From 04bc020cb238e6d2e3442a1298c8b04d8ea0c91a Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Tue, 9 Mar 2021 22:55:38 -0500 Subject: [PATCH 08/11] minor improvement to child saving --- Sources/ParseSwift/API/API+Commands.swift | 52 ++++++++++++++++++-- Sources/ParseSwift/API/BatchUtils.swift | 4 +- Sources/ParseSwift/Objects/ParseObject.swift | 42 ++++++++++------ 3 files changed, 78 insertions(+), 20 deletions(-) diff --git a/Sources/ParseSwift/API/API+Commands.swift b/Sources/ParseSwift/API/API+Commands.swift index 71a0e3ead..f5bc460ff 100644 --- a/Sources/ParseSwift/API/API+Commands.swift +++ b/Sources/ParseSwift/API/API+Commands.swift @@ -341,6 +341,49 @@ internal extension API.Command { mapper: mapper) } + // MARK: Saving ParseObjects - Encodable + static func saveCommand(object: T) throws -> API.Command where T: Encodable { + 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: object) + } else { + return try createCommand(object: object) + } + } + + // MARK: Saving ParseObjects - [Encodable] - private + private static func createCommand(object: T) throws -> API.Command where T: Encodable { + 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.Command(method: .POST, + path: objectable.endpoint, + body: object, + mapper: mapper) + } + + private static func updateCommand(object: T) throws -> API.Command where T: Encodable { + 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.Command(method: .PUT, + path: objectable.endpoint, + body: object, + mapper: mapper) + } + // MARK: Saving ParseObjects - Encodable static func saveCommand(_ object: T) throws -> API.Command where T: Encodable { guard let objectable = object as? Objectable else { @@ -501,7 +544,7 @@ extension API.Command where T: ParseObject { //This has been disabled, looking into getting it working in the future. //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 { @@ -510,7 +553,8 @@ extension API.Command where T: ParseType { return try? ParseCoding.jsonEncoder().encode(body) } - static func batch(commands: [API.Command]) -> RESTBatchCommandTypeEncodable { + static func batch(commands: [API.Command], + transaction: Bool) -> RESTBatchCommandTypeEncodable { let commands = commands.compactMap { (command) -> API.Command? in let path = ParseConfiguration.mountPath + command.path.urlComponent guard let body = command.body else { @@ -551,10 +595,10 @@ 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 { diff --git a/Sources/ParseSwift/API/BatchUtils.swift b/Sources/ParseSwift/API/BatchUtils.swift index a54107f36..4366b4b4d 100644 --- a/Sources/ParseSwift/API/BatchUtils.swift +++ b/Sources/ParseSwift/API/BatchUtils.swift @@ -16,13 +16,13 @@ typealias RESTBatchCommandType = API.Command, Pars typealias ParseObjectBatchCommandNoBody = BatchCommandNoBody typealias ParseObjectBatchResponseNoBody = [(Result)] typealias RESTBatchCommandNoBodyType = API.NonParseBodyCommand, ParseObjectBatchResponseNoBody> where T: Encodable -/* + typealias ParseObjectBatchCommandEncodable = BatchCommand where T: ParseType typealias ParseObjectBatchResponseEncodable = [(Result)] // swiftlint:disable line_length typealias RESTBatchCommandTypeEncodable = API.Command, ParseObjectBatchResponseEncodable> where T: ParseType // swiftlint:enable line_length -*/ + internal struct BatchCommand: ParseType where T: ParseType { let requests: [API.Command] var transaction: Bool diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index 7664e815b..e3c565315 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -477,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 saveAll(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) } }*/ @@ -669,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 @@ -706,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 { @@ -747,11 +755,17 @@ internal extension ParseType { 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) + static func saveAll(objects: [ParseType], + transaction: Bool = true, + options: API.Options = []) throws -> [(Result)] { + let commands = try objects.map { + try API.Command.saveCommand(object: $0) + } + return try API.Command + .batch(commands: commands, + transaction: transaction) + .execute(options: options, + callbackQueue: .main) }*/ } From 0d9cf1ee86d7cecdecb2bdb510a74e2bdd7c8dbe Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Wed, 10 Mar 2021 17:48:50 -0500 Subject: [PATCH 09/11] Update commented code on child batching --- Sources/ParseSwift/API/API+Commands.swift | 135 ++++++++++++------- Sources/ParseSwift/API/BatchUtils.swift | 11 +- Sources/ParseSwift/Objects/ParseObject.swift | 14 +- Sources/ParseSwift/Types/Query.swift | 1 + 4 files changed, 99 insertions(+), 62 deletions(-) diff --git a/Sources/ParseSwift/API/API+Commands.swift b/Sources/ParseSwift/API/API+Commands.swift index f5bc460ff..6a414cf43 100644 --- a/Sources/ParseSwift/API/API+Commands.swift +++ b/Sources/ParseSwift/API/API+Commands.swift @@ -341,49 +341,6 @@ internal extension API.Command { mapper: mapper) } - // MARK: Saving ParseObjects - Encodable - static func saveCommand(object: T) throws -> API.Command where T: Encodable { - 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: object) - } else { - return try createCommand(object: object) - } - } - - // MARK: Saving ParseObjects - [Encodable] - private - private static func createCommand(object: T) throws -> API.Command where T: Encodable { - 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.Command(method: .POST, - path: objectable.endpoint, - body: object, - mapper: mapper) - } - - private static func updateCommand(object: T) throws -> API.Command where T: Encodable { - 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.Command(method: .PUT, - path: objectable.endpoint, - body: object, - mapper: mapper) - } - // MARK: Saving ParseObjects - Encodable static func saveCommand(_ object: T) throws -> API.Command where T: Encodable { guard let objectable = object as? Objectable else { @@ -544,26 +501,26 @@ extension API.Command where T: ParseObject { //This has been disabled, looking into getting it working in the future. //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], - transaction: Bool) -> 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 } @@ -599,7 +556,7 @@ extension API.Command where T: ParseType { return RESTBatchCommandTypeEncodable(method: .POST, path: .batch, body: batchCommand, mapper: mapper) } } - +*/ // MARK: API.NonParseBodyCommand internal extension API { struct NonParseBodyCommand: Encodable where T: Encodable { @@ -718,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/BatchUtils.swift b/Sources/ParseSwift/API/BatchUtils.swift index 4366b4b4d..4c2c39db6 100644 --- a/Sources/ParseSwift/API/BatchUtils.swift +++ b/Sources/ParseSwift/API/BatchUtils.swift @@ -16,13 +16,13 @@ typealias RESTBatchCommandType = API.Command, Pars typealias ParseObjectBatchCommandNoBody = BatchCommandNoBody typealias ParseObjectBatchResponseNoBody = [(Result)] typealias RESTBatchCommandNoBodyType = API.NonParseBodyCommand, ParseObjectBatchResponseNoBody> where T: Encodable - +/* typealias ParseObjectBatchCommandEncodable = BatchCommand where T: ParseType typealias ParseObjectBatchResponseEncodable = [(Result)] // swiftlint:disable line_length typealias RESTBatchCommandTypeEncodable = API.Command, ParseObjectBatchResponseEncodable> where T: ParseType // swiftlint:enable line_length - +*/ internal struct BatchCommand: ParseType where T: ParseType { let requests: [API.Command] var transaction: Bool @@ -32,7 +32,12 @@ 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/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index e3c565315..d607da5ee 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -477,7 +477,7 @@ 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(transaction: Bool = true, + func saveAllParseTypes(transaction: Bool = true, options: API.Options = []) throws -> [(Result)] { let commands = try map { try $0.saveCommand() } return try API.Command @@ -754,14 +754,14 @@ internal extension ParseType { func saveCommand() throws -> API.Command { try API.Command.saveCommand(self) } -/* - static func saveAll(objects: [ParseType], - transaction: Bool = true, - options: API.Options = []) throws -> [(Result)] { + /* + func saveAll(objects: [ParseType], + transaction: Bool = true, + options: API.Options = []) throws -> [(Result)] { let commands = try objects.map { - try API.Command.saveCommand(object: $0) + try API.ChildCommand.saveCommand($0) } - return try API.Command + return try API.ChildCommand .batch(commands: commands, transaction: transaction) .execute(options: options, 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) From d3f15d17eed0a7b64bbcf725cb8bcc2b6e8220bc Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Fri, 12 Mar 2021 21:59:31 -0500 Subject: [PATCH 10/11] nits --- CHANGELOG.md | 2 +- .../Contents.swift | 4 ++-- .../10 - Cloud Code.xcplaygroundpage/Contents.swift | 2 +- .../11 - LiveQuery.xcplaygroundpage/Contents.swift | 2 +- .../Contents.swift | 2 +- .../13 - Operations.xcplaygroundpage/Contents.swift | 2 +- .../14 - Config.xcplaygroundpage/Contents.swift | 2 +- .../Contents.swift | 6 +++--- .../Contents.swift | 4 ++-- .../Contents.swift | 12 ++++++------ .../Pages/5 - ACL.xcplaygroundpage/Contents.swift | 4 ++-- .../6 - Installation.xcplaygroundpage/Contents.swift | 4 ++-- .../7 - GeoPoint.xcplaygroundpage/Contents.swift | 4 ++-- .../8 - Pointers.xcplaygroundpage/Contents.swift | 2 +- .../Pages/9 - Files.xcplaygroundpage/Contents.swift | 4 ++-- 15 files changed, 28 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fe8e7a79..39fc970f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # 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 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 d3a46ce45..e3e53d87e 100644 --- a/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift @@ -1,4 +1,4 @@ -//: For this page, make sure build target is set to ParseSwift (macOS) and targeting +//: 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 @@ -44,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 diff --git a/ParseSwift.playground/Pages/10 - Cloud Code.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/10 - Cloud Code.xcplaygroundpage/Contents.swift index 5c80b2fbe..01b5fd0fd 100644 --- a/ParseSwift.playground/Pages/10 - Cloud Code.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/10 - Cloud Code.xcplaygroundpage/Contents.swift @@ -1,6 +1,6 @@ //: [Previous](@previous) -//: For this page, make sure build target is set to ParseSwift (macOS) and targeting +//: 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 diff --git a/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift index 244e596e9..c09cb9ab5 100644 --- a/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift @@ -1,6 +1,6 @@ //: [Previous](@previous) -//: For this page, make sure build target is set to ParseSwift (iOS) and targeting +//: 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 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 095204826..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,6 +1,6 @@ //: [Previous](@previous) -//: For this page, make sure build target is set to ParseSwift (macOS) and targeting +//: 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 diff --git a/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift index 3029c00c8..1c17e44f2 100644 --- a/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift @@ -1,6 +1,6 @@ //: [Previous](@previous) -//: For this page, make sure build target is set to ParseSwift (macOS) and targeting +//: 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 diff --git a/ParseSwift.playground/Pages/14 - Config.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/14 - Config.xcplaygroundpage/Contents.swift index 77082a23a..fa2429d51 100644 --- a/ParseSwift.playground/Pages/14 - Config.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/14 - Config.xcplaygroundpage/Contents.swift @@ -1,6 +1,6 @@ //: [Previous](@previous) -//: For this page, make sure build target is set to ParseSwift (macOS) and targeting +//: 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 diff --git a/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift index 895c40283..951229c4c 100644 --- a/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift @@ -1,6 +1,6 @@ //: [Previous](@previous) -//: For this page, make sure build target is set to ParseSwift (macOS) and targeting +//: 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 @@ -38,7 +38,7 @@ var query = GameScore.query("score" > 50, .order([.descending("score")]) //: Query 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. query.limit(2).find(callbackQueue: .main) { results in switch results { @@ -66,7 +66,7 @@ results.forEach { (score) in } //: Query first 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. query.first { results in switch results { 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 9bc519dc2..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,6 +1,6 @@ //: [Previous](@previous) -//: For this page, make sure build target is set to ParseSwift (macOS) and targeting +//: 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 @@ -31,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 fe3a5ff2b..da4e380d7 100644 --- a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift @@ -1,6 +1,6 @@ //: [Previous](@previous) -//: For this page, make sure build target is set to ParseSwift (macOS) and targeting +//: 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 @@ -62,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 @@ -82,9 +82,9 @@ User.login(username: "hello", password: "world") { results in } } -/*: Save your first customKey value to your `ParseUser` +/*: Save your first `customKey` value to your `ParseUser` Asynchrounously - 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.current?.customKey = "myCustom" @@ -101,8 +101,8 @@ User.current?.save { results in } //: 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 97f78610c..f37d3cb72 100644 --- a/ParseSwift.playground/Pages/5 - ACL.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/5 - ACL.xcplaygroundpage/Contents.swift @@ -1,6 +1,6 @@ //: [Previous](@previous) -//: For this page, make sure build target is set to ParseSwift (macOS) and targeting +//: 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 @@ -46,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 b4db74d1f..627dd8b44 100644 --- a/ParseSwift.playground/Pages/6 - Installation.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/6 - Installation.xcplaygroundpage/Contents.swift @@ -1,6 +1,6 @@ //: [Previous](@previous) -//: For this page, make sure build target is set to ParseSwift (macOS) and targeting +//: 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 @@ -40,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 06228f083..ed73aeef9 100644 --- a/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift @@ -1,6 +1,6 @@ //: [Previous](@previous) -//: For this page, make sure build target is set to ParseSwift (macOS) and targeting +//: 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 @@ -35,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 367f86f30..ce7af7394 100644 --- a/ParseSwift.playground/Pages/8 - Pointers.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/8 - Pointers.xcplaygroundpage/Contents.swift @@ -1,6 +1,6 @@ //: [Previous](@previous) -//: For this page, make sure build target is set to ParseSwift (macOS) and targeting +//: 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 diff --git a/ParseSwift.playground/Pages/9 - Files.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/9 - Files.xcplaygroundpage/Contents.swift index 90ffae253..5fa5d11b0 100644 --- a/ParseSwift.playground/Pages/9 - Files.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/9 - Files.xcplaygroundpage/Contents.swift @@ -1,6 +1,6 @@ //: [Previous](@previous) -//: For this page, make sure build target is set to ParseSwift (macOS) and targeting +//: 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 @@ -49,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 From 883956ac84ba47215096bc86abe28b97fd1f1398 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Fri, 12 Mar 2021 22:09:40 -0500 Subject: [PATCH 11/11] Test deleteAll transaction in Playgrounds --- .../Pages/1 - Your first Object.xcplaygroundpage/Contents.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e3e53d87e..87949c2a7 100644 --- a/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift @@ -264,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