diff --git a/.codecov.yml b/.codecov.yml index 6899e9b4..7009a7cb 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -6,7 +6,7 @@ coverage: status: patch: default: - target: 74 + target: auto changes: false project: default: diff --git a/ParseSwift.playground/Sources/Common.swift b/ParseSwift.playground/Sources/Common.swift index 36dd3c52..0c1bf820 100644 --- a/ParseSwift.playground/Sources/Common.swift +++ b/ParseSwift.playground/Sources/Common.swift @@ -6,7 +6,7 @@ public func initializeParse() { clientKey: "clientKey", masterKey: "masterKey", serverURL: URL(string: "http://localhost:1337/1")!, - useTransactionsInternally: false) + useTransactions: false) } public func initializeParseCustomObjectId() { diff --git a/Sources/ParseSwift/Objects/ParseInstallation+async.swift b/Sources/ParseSwift/Objects/ParseInstallation+async.swift index 7dc99df7..0b5fbcd1 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation+async.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation+async.swift @@ -110,7 +110,7 @@ public extension Sequence where Element: ParseInstallation { the transactions can fail. */ func saveAll(batchLimit limit: Int? = nil, - transaction: Bool = false, + transaction: Bool = ParseSwift.configuration.useTransactions, isIgnoreCustomObjectIdConfig: Bool = false, options: API.Options = []) async throws -> [(Result)] { try await withCheckedThrowingContinuation { continuation in @@ -138,7 +138,7 @@ public extension Sequence where Element: ParseInstallation { the transactions can fail. */ func deleteAll(batchLimit limit: Int? = nil, - transaction: Bool = false, + transaction: Bool = ParseSwift.configuration.useTransactions, options: API.Options = []) async throws -> [(Result)] { try await withCheckedThrowingContinuation { continuation in self.deleteAll(batchLimit: limit, diff --git a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift index 968a31b2..5919a4b8 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift @@ -99,7 +99,7 @@ public extension Sequence where Element: ParseInstallation { the transactions can fail. */ func saveAllPublisher(batchLimit limit: Int? = nil, - transaction: Bool = false, + transaction: Bool = ParseSwift.configuration.useTransactions, isIgnoreCustomObjectIdConfig: Bool = false, options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in @@ -126,7 +126,7 @@ public extension Sequence where Element: ParseInstallation { the transactions can fail. */ func deleteAllPublisher(batchLimit limit: Int? = nil, - transaction: Bool = false, + transaction: Bool = ParseSwift.configuration.useTransactions, options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in self.deleteAll(batchLimit: limit, diff --git a/Sources/ParseSwift/Objects/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift index 34a0dab7..a44c81bc 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation.swift @@ -747,7 +747,7 @@ public extension Sequence where Element: ParseInstallation { desires a different policy, it should be inserted in `options`. */ func saveAll(batchLimit limit: Int? = nil, // swiftlint:disable:this function_body_length - transaction: Bool = false, + transaction: Bool = ParseSwift.configuration.useTransactions, isIgnoreCustomObjectIdConfig: Bool = false, options: API.Options = []) throws -> [(Result)] { var options = options @@ -799,12 +799,8 @@ public extension Sequence where Element: ParseInstallation { let commands = try map { try $0.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) } - let batchLimit: Int! - if transaction { - batchLimit = commands.count - } else { - batchLimit = limit != nil ? limit! : ParseConstants.batchLimit - } + let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + try canSendTransactions(transaction, objectCount: commands.count, batchLimit: batchLimit) let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) try batches.forEach { let currentBatch = try API.Command @@ -850,7 +846,7 @@ public extension Sequence where Element: ParseInstallation { */ func saveAll( // swiftlint:disable:this function_body_length cyclomatic_complexity batchLimit limit: Int? = nil, - transaction: Bool = false, + transaction: Bool = ParseSwift.configuration.useTransactions, isIgnoreCustomObjectIdConfig: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, @@ -874,7 +870,9 @@ public extension Sequence where Element: ParseInstallation { let group = DispatchGroup() group.enter() installation - .ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, parseError) -> Void in + .ensureDeepSave(options: options, + // swiftlint:disable:next line_length + isShouldReturnIfChildObjectsFound: true) { (savedChildObjects, savedChildFiles, parseError) -> Void in //If an error occurs, everything should be skipped if parseError != nil { error = parseError @@ -917,12 +915,8 @@ public extension Sequence where Element: ParseInstallation { let commands = try map { try $0.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) } - let batchLimit: Int! - if transaction { - batchLimit = commands.count - } else { - batchLimit = limit != nil ? limit! : ParseConstants.batchLimit - } + let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + try canSendTransactions(transaction, objectCount: commands.count, batchLimit: batchLimit) let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) var completed = 0 for batch in batches { @@ -1093,18 +1087,14 @@ public extension Sequence where Element: ParseInstallation { desires a different policy, it should be inserted in `options`. */ func deleteAll(batchLimit limit: Int? = nil, - transaction: Bool = false, + transaction: Bool = ParseSwift.configuration.useTransactions, options: API.Options = []) throws -> [(Result)] { var options = options options.insert(.cachePolicy(.reloadIgnoringLocalCacheData)) 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 batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + try canSendTransactions(transaction, objectCount: commands.count, batchLimit: batchLimit) let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) try batches.forEach { let currentBatch = try API.Command)> @@ -1146,7 +1136,7 @@ public extension Sequence where Element: ParseInstallation { */ func deleteAll( batchLimit limit: Int? = nil, - transaction: Bool = false, + transaction: Bool = ParseSwift.configuration.useTransactions, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void @@ -1156,12 +1146,8 @@ public extension Sequence where Element: ParseInstallation { 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 batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + try canSendTransactions(transaction, objectCount: commands.count, batchLimit: batchLimit) let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) var completed = 0 for batch in batches { diff --git a/Sources/ParseSwift/Objects/ParseObject+async.swift b/Sources/ParseSwift/Objects/ParseObject+async.swift index 3482110d..0f73d525 100644 --- a/Sources/ParseSwift/Objects/ParseObject+async.swift +++ b/Sources/ParseSwift/Objects/ParseObject+async.swift @@ -109,7 +109,7 @@ public extension Sequence where Element: ParseObject { the transactions can fail. */ func saveAll(batchLimit limit: Int? = nil, - transaction: Bool = false, + transaction: Bool = ParseSwift.configuration.useTransactions, isIgnoreCustomObjectIdConfig: Bool = false, options: API.Options = []) async throws -> [(Result)] { try await withCheckedThrowingContinuation { continuation in @@ -137,7 +137,7 @@ public extension Sequence where Element: ParseObject { the transactions can fail. */ func deleteAll(batchLimit limit: Int? = nil, - transaction: Bool = false, + transaction: Bool = ParseSwift.configuration.useTransactions, options: API.Options = []) async throws -> [(Result)] { try await withCheckedThrowingContinuation { continuation in self.deleteAll(batchLimit: limit, diff --git a/Sources/ParseSwift/Objects/ParseObject+combine.swift b/Sources/ParseSwift/Objects/ParseObject+combine.swift index cf8da99f..3f66b7e7 100644 --- a/Sources/ParseSwift/Objects/ParseObject+combine.swift +++ b/Sources/ParseSwift/Objects/ParseObject+combine.swift @@ -110,7 +110,7 @@ public extension Sequence where Element: ParseObject { client-side checks are disabled. Developers are responsible for handling such cases. */ func saveAllPublisher(batchLimit limit: Int? = nil, - transaction: Bool = false, + transaction: Bool = ParseSwift.configuration.useTransactions, isIgnoreCustomObjectIdConfig: Bool = false, options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in @@ -137,7 +137,7 @@ public extension Sequence where Element: ParseObject { the transactions can fail. */ func deleteAllPublisher(batchLimit limit: Int? = nil, - transaction: Bool = false, + transaction: Bool = ParseSwift.configuration.useTransactions, options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in self.deleteAll(batchLimit: limit, diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index 36fd6a34..e647c4a1 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -83,6 +83,22 @@ public extension ParseObject { // MARK: Batch Support public extension Sequence where Element: ParseObject { + internal func canSendTransactions(_ isUsingTransactions: Bool, + objectCount: Int, + batchLimit: Int) throws { + if isUsingTransactions { + if objectCount > batchLimit { + let error = ParseError(code: .unknownError, + message: """ +The amount of objects (\(objectCount)) can't exceed the batch size(\(batchLimit)). +Either decrease the amount of objects, increase the batch size, or disable +transactions for this call. +""") + throw error + } + } + } + /** Saves a collection of objects *synchronously* all at once and throws an error if necessary. - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched. @@ -113,7 +129,7 @@ public extension Sequence where Element: ParseObject { desires a different policy, it should be inserted in `options`. */ func saveAll(batchLimit limit: Int? = nil, // swiftlint:disable:this function_body_length - transaction: Bool = false, + transaction: Bool = ParseSwift.configuration.useTransactions, isIgnoreCustomObjectIdConfig: Bool = false, options: API.Options = []) throws -> [(Result)] { var options = options @@ -126,7 +142,9 @@ public extension Sequence where Element: ParseObject { for object in objects { let group = DispatchGroup() group.enter() - object.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, parseError) -> Void in + object.ensureDeepSave(options: options, + // swiftlint:disable:next line_length + isShouldReturnIfChildObjectsFound: true) { (savedChildObjects, savedChildFiles, parseError) -> Void in //If an error occurs, everything should be skipped if parseError != nil { error = parseError @@ -163,12 +181,8 @@ public extension Sequence where Element: ParseObject { var returnBatch = [(Result)]() let commands = try map { try $0.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) } - let batchLimit: Int! - if transaction { - batchLimit = commands.count - } else { - batchLimit = limit ?? ParseConstants.batchLimit - } + let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + try canSendTransactions(transaction, objectCount: commands.count, batchLimit: batchLimit) let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) try batches.forEach { let currentBatch = try API.Command @@ -212,7 +226,7 @@ public extension Sequence where Element: ParseObject { */ func saveAll( // swiftlint:disable:this function_body_length cyclomatic_complexity batchLimit limit: Int? = nil, - transaction: Bool = false, + transaction: Bool = ParseSwift.configuration.useTransactions, isIgnoreCustomObjectIdConfig: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, @@ -236,7 +250,9 @@ public extension Sequence where Element: ParseObject { for object in objects { let group = DispatchGroup() group.enter() - object.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, parseError) -> Void in + object.ensureDeepSave(options: options, + // swiftlint:disable:next line_length + isShouldReturnIfChildObjectsFound: true) { (savedChildObjects, savedChildFiles, parseError) -> Void in //If an error occurs, everything should be skipped if parseError != nil { error = parseError @@ -279,12 +295,8 @@ public extension Sequence where Element: ParseObject { let commands = try map { try $0.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) } - let batchLimit: Int! - if transaction { - batchLimit = commands.count - } else { - batchLimit = limit ?? ParseConstants.batchLimit - } + let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + try canSendTransactions(transaction, objectCount: commands.count, batchLimit: batchLimit) let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) var completed = 0 for batch in batches { @@ -447,18 +459,14 @@ public extension Sequence where Element: ParseObject { desires a different policy, it should be inserted in `options`. */ func deleteAll(batchLimit limit: Int? = nil, - transaction: Bool = false, + transaction: Bool = ParseSwift.configuration.useTransactions, options: API.Options = []) throws -> [(Result)] { var options = options options.insert(.cachePolicy(.reloadIgnoringLocalCacheData)) var returnBatch = [(Result)]() let commands = try map { try $0.deleteCommand() } - let batchLimit: Int! - if transaction { - batchLimit = commands.count - } else { - batchLimit = limit ?? ParseConstants.batchLimit - } + let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + try canSendTransactions(transaction, objectCount: commands.count, batchLimit: batchLimit) let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) try batches.forEach { let currentBatch = try API.Command)> @@ -497,7 +505,7 @@ public extension Sequence where Element: ParseObject { */ func deleteAll( batchLimit limit: Int? = nil, - transaction: Bool = false, + transaction: Bool = ParseSwift.configuration.useTransactions, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void @@ -507,12 +515,8 @@ public extension Sequence where Element: ParseObject { options.insert(.cachePolicy(.reloadIgnoringLocalCacheData)) var returnBatch = [(Result)]() let commands = try map({ try $0.deleteCommand() }) - let batchLimit: Int! - if transaction { - batchLimit = commands.count - } else { - batchLimit = limit ?? ParseConstants.batchLimit - } + let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + try canSendTransactions(transaction, objectCount: commands.count, batchLimit: batchLimit) let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) var completed = 0 for batch in batches { @@ -755,6 +759,7 @@ extension ParseObject { // swiftlint:disable:next function_body_length internal func ensureDeepSave(options: API.Options = [], + isShouldReturnIfChildObjectsFound: Bool = false, completion: @escaping ([String: PointerType], [UUID: ParseFile], ParseError?) -> Void) { let uuid = UUID() @@ -779,7 +784,16 @@ extension ParseObject { filesSavedBeforeThisOne: nil) var waitingToBeSaved = object.unsavedChildren - + if isShouldReturnIfChildObjectsFound && waitingToBeSaved.count > 0 { + let error = ParseError(code: .unknownError, + message: """ + When using transactions, all child ParseObjects have to originally + be saved to the Parse Server. Either save all child objects first + or disable transactions for this call. + """) + completion([String: PointerType](), [UUID: ParseFile](), error) + return + } while waitingToBeSaved.count > 0 { var savableObjects = [ParseType]() var savableFiles = [ParseFile]() @@ -848,7 +862,7 @@ extension ParseObject { // MARK: Savable Encodable Version internal extension ParseType { func saveAll(objects: [ParseType], - transaction: Bool = ParseSwift.configuration.useTransactionsInternally, + transaction: Bool = ParseSwift.configuration.useTransactions, options: API.Options = []) throws -> [(Result)] { try API.NonParseBodyCommand .batch(objects: objects, diff --git a/Sources/ParseSwift/Objects/ParseUser+async.swift b/Sources/ParseSwift/Objects/ParseUser+async.swift index 7a1bd1d5..411d1a53 100644 --- a/Sources/ParseSwift/Objects/ParseUser+async.swift +++ b/Sources/ParseSwift/Objects/ParseUser+async.swift @@ -246,7 +246,7 @@ public extension Sequence where Element: ParseUser { */ @MainActor func saveAll(batchLimit limit: Int? = nil, - transaction: Bool = false, + transaction: Bool = ParseSwift.configuration.useTransactions, isIgnoreCustomObjectIdConfig: Bool = false, options: API.Options = []) async throws -> [(Result)] { try await withCheckedThrowingContinuation { continuation in @@ -275,7 +275,7 @@ public extension Sequence where Element: ParseUser { */ @MainActor func deleteAll(batchLimit limit: Int? = nil, - transaction: Bool = false, + transaction: Bool = ParseSwift.configuration.useTransactions, options: API.Options = []) async throws -> [(Result)] { try await withCheckedThrowingContinuation { continuation in self.deleteAll(batchLimit: limit, diff --git a/Sources/ParseSwift/Objects/ParseUser+combine.swift b/Sources/ParseSwift/Objects/ParseUser+combine.swift index 2d5e62b7..09768c61 100644 --- a/Sources/ParseSwift/Objects/ParseUser+combine.swift +++ b/Sources/ParseSwift/Objects/ParseUser+combine.swift @@ -241,7 +241,7 @@ public extension Sequence where Element: ParseUser { client-side checks are disabled. Developers are responsible for handling such cases. */ func saveAllPublisher(batchLimit limit: Int? = nil, - transaction: Bool = false, + transaction: Bool = ParseSwift.configuration.useTransactions, isIgnoreCustomObjectIdConfig: Bool = false, options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in @@ -268,7 +268,7 @@ public extension Sequence where Element: ParseUser { the transactions can fail. */ func deleteAllPublisher(batchLimit limit: Int? = nil, - transaction: Bool = false, + transaction: Bool = ParseSwift.configuration.useTransactions, options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in self.deleteAll(batchLimit: limit, diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index abc7f840..538a1a4e 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -1141,7 +1141,7 @@ public extension Sequence where Element: ParseUser { desires a different policy, it should be inserted in `options`. */ func saveAll(batchLimit limit: Int? = nil, // swiftlint:disable:this function_body_length - transaction: Bool = false, + transaction: Bool = ParseSwift.configuration.useTransactions, isIgnoreCustomObjectIdConfig: Bool = false, options: API.Options = []) throws -> [(Result)] { var childObjects = [String: PointerType]() @@ -1153,7 +1153,9 @@ public extension Sequence where Element: ParseUser { for user in users { let group = DispatchGroup() group.enter() - user.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, parseError) -> Void in + user.ensureDeepSave(options: options, + // swiftlint:disable:next line_length + isShouldReturnIfChildObjectsFound: true) { (savedChildObjects, savedChildFiles, parseError) -> Void in //If an error occurs, everything should be skipped if parseError != nil { error = parseError @@ -1192,12 +1194,8 @@ public extension Sequence where Element: ParseUser { let commands = try map { try $0.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) } - let batchLimit: Int! - if transaction { - batchLimit = commands.count - } else { - batchLimit = limit != nil ? limit! : ParseConstants.batchLimit - } + let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + try canSendTransactions(transaction, objectCount: commands.count, batchLimit: batchLimit) let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) try batches.forEach { let currentBatch = try API.Command @@ -1243,7 +1241,7 @@ public extension Sequence where Element: ParseUser { */ func saveAll( // swiftlint:disable:this function_body_length cyclomatic_complexity batchLimit limit: Int? = nil, - transaction: Bool = false, + transaction: Bool = ParseSwift.configuration.useTransactions, isIgnoreCustomObjectIdConfig: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, @@ -1266,7 +1264,9 @@ public extension Sequence where Element: ParseUser { for user in users { let group = DispatchGroup() group.enter() - user.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, parseError) -> Void in + user.ensureDeepSave(options: options, + // swiftlint:disable:next line_length + isShouldReturnIfChildObjectsFound: true) { (savedChildObjects, savedChildFiles, parseError) -> Void in //If an error occurs, everything should be skipped if parseError != nil { error = parseError @@ -1309,12 +1309,8 @@ public extension Sequence where Element: ParseUser { let commands = try map { try $0.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) } - let batchLimit: Int! - if transaction { - batchLimit = commands.count - } else { - batchLimit = limit != nil ? limit! : ParseConstants.batchLimit - } + let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + try canSendTransactions(transaction, objectCount: commands.count, batchLimit: batchLimit) let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) var completed = 0 for batch in batches { @@ -1483,18 +1479,14 @@ public extension Sequence where Element: ParseUser { desires a different policy, it should be inserted in `options`. */ func deleteAll(batchLimit limit: Int? = nil, - transaction: Bool = false, + transaction: Bool = ParseSwift.configuration.useTransactions, options: API.Options = []) throws -> [(Result)] { var options = options options.insert(.cachePolicy(.reloadIgnoringLocalCacheData)) 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 batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + try canSendTransactions(transaction, objectCount: commands.count, batchLimit: batchLimit) let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) try batches.forEach { let currentBatch = try API.Command @@ -1535,7 +1527,7 @@ public extension Sequence where Element: ParseUser { */ func deleteAll( batchLimit limit: Int? = nil, - transaction: Bool = false, + transaction: Bool = ParseSwift.configuration.useTransactions, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void @@ -1545,12 +1537,8 @@ public extension Sequence where Element: ParseUser { 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 batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + try canSendTransactions(transaction, objectCount: commands.count, batchLimit: batchLimit) let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) var completed = 0 for batch in batches { diff --git a/Sources/ParseSwift/Parse.swift b/Sources/ParseSwift/Parse.swift index 0323134d..c3e1b72c 100644 --- a/Sources/ParseSwift/Parse.swift +++ b/Sources/ParseSwift/Parse.swift @@ -9,56 +9,56 @@ import FoundationNetworking public struct ParseConfiguration { /// The application id of your Parse application. - var applicationId: String + public internal(set) var applicationId: String /// The master key of your Parse application. - var masterKey: String? // swiftlint:disable:this inclusive_language + public internal(set) var masterKey: String? // swiftlint:disable:this inclusive_language /// The client key of your Parse application. - var clientKey: String? + public internal(set) var clientKey: String? /// The server URL to connect to Parse Server. - var serverURL: URL + public internal(set) var serverURL: URL /// The live query server URL to connect to Parse Server. - var liveQuerysServerURL: URL? + public internal(set) var liveQuerysServerURL: URL? /// Allows objectIds to be created on the client. - var allowCustomObjectId = false + public internal(set) var allowCustomObjectId = false - /// Use transactions inside the Client SDK. - /// - warning: This is experimental and known not to work with mongoDB. - var useTransactionsInternally = false + /// Use transactions when saving/updating multiple objects. + /// - warning: This is experimental. + public internal(set) var useTransactions = false /// The default caching policy for all http requests that determines when to /// return a response from the cache. Defaults to `useProtocolCachePolicy`. /// See Apple's [documentation](https://developer.apple.com/documentation/foundation/url_loading_system/accessing_cached_data) /// for more info. - var requestCachePolicy = URLRequest.CachePolicy.useProtocolCachePolicy + public internal(set) var requestCachePolicy = URLRequest.CachePolicy.useProtocolCachePolicy /// A dictionary of additional headers to send with requests. See Apple's /// [documentation](https://developer.apple.com/documentation/foundation/urlsessionconfiguration/1411532-httpadditionalheaders) /// for more info. - var httpAdditionalHeaders: [String: String]? + public internal(set) var httpAdditionalHeaders: [String: String]? /// The memory capacity of the cache, in bytes. Defaults to 512KB. - var cacheMemoryCapacity = 512_000 + public internal(set) var cacheMemoryCapacity = 512_000 /// The disk capacity of the cache, in bytes. Defaults to 10MB. - var cacheDiskCapacity = 10_000_000 + public internal(set) var cacheDiskCapacity = 10_000_000 /// If your app previously used the iOS Objective-C SDK, setting this value /// to `true` will attempt to migrate relevant data stored in the Keychain to /// ParseSwift. Defaults to `false`. - var migrateFromObjcSDK: Bool = false + public internal(set) var migrateFromObjcSDK: Bool = false /// Deletes the Parse Keychain when the app is running for the first time. /// Defaults to `false`. - var deleteKeychainIfNeeded: Bool = false + public internal(set) var deleteKeychainIfNeeded: Bool = false /// Maximum number of times to try to connect to Parse Server. /// Defaults to 5. - var maxConnectionAttempts: Int = 5 + public internal(set) var maxConnectionAttempts: Int = 5 internal var authentication: ((URLAuthenticationChallenge, (URLSession.AuthChallengeDisposition, @@ -75,7 +75,7 @@ public struct ParseConfiguration { - parameter liveQueryServerURL: The live query server URL to connect to Parse Server. - parameter allowCustomObjectId: Allows objectIds to be created on the client. side for each object. Must be enabled on the server to work. - - parameter useTransactionsInternally: Use transactions inside the Client SDK. + - parameter useTransactions: Use transactions when saving/updating multiple objects. - parameter keyValueStore: A key/value store that conforms to the `ParseKeyValueStore` protocol. Defaults to `nil` in which one will be created an memory, but never persisted. For Linux, this this is the only store available since there is no Keychain. Linux users should replace this store with an @@ -99,7 +99,7 @@ public struct ParseConfiguration { It should have the following argument signature: `(challenge: URLAuthenticationChallenge, completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void`. See Apple's [documentation](https://developer.apple.com/documentation/foundation/urlsessiontaskdelegate/1411595-urlsession) for more for details. - - warning: `useTransactionsInternally` is experimental and known not to work with mongoDB. + - warning: `useTransactions` is experimental. */ public init(applicationId: String, clientKey: String? = nil, @@ -107,7 +107,7 @@ public struct ParseConfiguration { serverURL: URL, liveQueryServerURL: URL? = nil, allowCustomObjectId: Bool = false, - useTransactionsInternally: Bool = false, + useTransactions: Bool = false, keyValueStore: ParseKeyValueStore? = nil, requestCachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy, cacheMemoryCapacity: Int = 512_000, @@ -125,7 +125,7 @@ public struct ParseConfiguration { self.serverURL = serverURL self.liveQuerysServerURL = liveQueryServerURL self.allowCustomObjectId = allowCustomObjectId - self.useTransactionsInternally = useTransactionsInternally + self.useTransactions = useTransactions self.mountPath = "/" + serverURL.pathComponents .filter { $0 != "/" } .joined(separator: "/") @@ -146,7 +146,7 @@ public struct ParseConfiguration { */ public struct ParseSwift { - static var configuration: ParseConfiguration! + public internal(set) static var configuration: ParseConfiguration! static var sessionDelegate: ParseURLSessionDelegate! /** @@ -236,7 +236,7 @@ public struct ParseSwift { - parameter liveQueryServerURL: The live query server URL to connect to Parse Server. - parameter allowCustomObjectId: Allows objectIds to be created on the client. side for each object. Must be enabled on the server to work. - - parameter useTransactionsInternally: Use transactions inside the Client SDK. + - parameter useTransactions: Use transactions when saving/updating multiple objects. - parameter keyValueStore: A key/value store that conforms to the `ParseKeyValueStore` protocol. Defaults to `nil` in which one will be created an memory, but never persisted. For Linux, this this is the only store available since there is no Keychain. Linux users should replace this store with an @@ -258,7 +258,7 @@ public struct ParseSwift { It should have the following argument signature: `(challenge: URLAuthenticationChallenge, completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void`. See Apple's [documentation](https://developer.apple.com/documentation/foundation/urlsessiontaskdelegate/1411595-urlsession) for more for details. - - warning: `useTransactionsInternally` is experimental and known not to work with mongoDB. + - warning: `useTransactions` is experimental. */ static public func initialize( applicationId: String, @@ -267,7 +267,7 @@ public struct ParseSwift { serverURL: URL, liveQueryServerURL: URL? = nil, allowCustomObjectId: Bool = false, - useTransactionsInternally: Bool = false, + useTransactions: Bool = false, keyValueStore: ParseKeyValueStore? = nil, requestCachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy, cacheMemoryCapacity: Int = 512_000, @@ -286,7 +286,7 @@ public struct ParseSwift { serverURL: serverURL, liveQueryServerURL: liveQueryServerURL, allowCustomObjectId: allowCustomObjectId, - useTransactionsInternally: useTransactionsInternally, + useTransactions: useTransactions, keyValueStore: keyValueStore, requestCachePolicy: requestCachePolicy, cacheMemoryCapacity: cacheMemoryCapacity, @@ -304,7 +304,7 @@ public struct ParseSwift { serverURL: URL, liveQueryServerURL: URL? = nil, allowCustomObjectId: Bool = false, - useTransactionsInternally: Bool = false, + useTransactions: Bool = false, keyValueStore: ParseKeyValueStore? = nil, requestCachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy, cacheMemoryCapacity: Int = 512_000, @@ -323,7 +323,7 @@ public struct ParseSwift { serverURL: serverURL, liveQueryServerURL: liveQueryServerURL, allowCustomObjectId: allowCustomObjectId, - useTransactionsInternally: useTransactionsInternally, + useTransactions: useTransactions, keyValueStore: keyValueStore, requestCachePolicy: requestCachePolicy, cacheMemoryCapacity: cacheMemoryCapacity, diff --git a/Tests/ParseSwiftTests/ParseObjectBatchTests.swift b/Tests/ParseSwiftTests/ParseObjectBatchTests.swift index ca4ba2d3..df53bd6c 100644 --- a/Tests/ParseSwiftTests/ParseObjectBatchTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectBatchTests.swift @@ -21,6 +21,7 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le // Custom properties var score: Int = 0 + var other: Game2? //custom initializers init() { @@ -35,6 +36,18 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } } + struct Game2: ParseObject { + //: These are required by ParseObject + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + //: Your own properties + var name = "Hello" + var profilePicture: ParseFile? + } + override func setUpWithError() throws { try super.setUpWithError() guard let url = URL(string: "http://localhost:1337/1") else { @@ -225,6 +238,126 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } } + func testSaveAllTransaction() { // swiftlint:disable:this function_body_length cyclomatic_complexity + let score = GameScore(score: 10) + let score2 = GameScore(score: 20) + + var scoreOnServer = score + scoreOnServer.objectId = "yarr" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt + scoreOnServer.ACL = nil + + var scoreOnServer2 = score2 + scoreOnServer2.objectId = "yolo" + scoreOnServer2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + scoreOnServer2.updatedAt = scoreOnServer2.createdAt + scoreOnServer2.ACL = nil + + let response = [BatchResponseItem(success: scoreOnServer, error: nil), + BatchResponseItem(success: scoreOnServer2, error: nil)] + let encoded: Data! + do { + encoded = try scoreOnServer.getJSONEncoder().encode(response) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().encode(scoreOnServer) + scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded1) + let encoded2 = try ParseCoding.jsonEncoder().encode(scoreOnServer2) + scoreOnServer2 = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded2) + + } catch { + XCTFail("Should have encoded/decoded. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + do { + + let saved = try [score, score2].saveAll(transaction: true) + + XCTAssertEqual(saved.count, 2) + switch saved[0] { + + case .success(let first): + XCTAssert(first.hasSameObjectId(as: scoreOnServer)) + guard let savedCreatedAt = first.createdAt, + let savedUpdatedAt = first.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = scoreOnServer.createdAt, + let originalUpdatedAt = scoreOnServer.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertNil(first.ACL) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + switch saved[1] { + + case .success(let second): + XCTAssert(second.hasSameObjectId(as: scoreOnServer2)) + guard let savedCreatedAt = second.createdAt, + let savedUpdatedAt = second.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = scoreOnServer2.createdAt, + let originalUpdatedAt = scoreOnServer2.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertNil(second.ACL) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + } catch { + XCTFail(error.localizedDescription) + } + } + + func testSaveAllTransactionErrorTooMany() { + let score = GameScore(score: 10) + let score2 = GameScore(score: 20) + do { + _ = try [score, score2].saveAll(batchLimit: 1, transaction: true) + XCTFail("Should have thrown error") + } catch { + guard let parseError = error as? ParseError else { + XCTFail("Error should have casted to ParseError") + return + } + XCTAssertEqual(parseError.code, .unknownError) + XCTAssertTrue(parseError.message.contains("exceed")) + } + } + + func testSaveAllTransactionErrorChild() { + let score = GameScore(score: 10) + var score2 = GameScore(score: 20) + score2.other = Game2() + do { + _ = try [score, score2].saveAll(transaction: true) + XCTFail("Should have thrown error") + } catch { + guard let parseError = error as? ParseError else { + XCTFail("Error should have casted to ParseError") + return + } + XCTAssertEqual(parseError.code, .unknownError) + XCTAssertTrue(parseError.message.contains("originally")) + } + } + func testSaveAllErrorIncorrectServerResponse() { let score = GameScore(score: 10) let score2 = GameScore(score: 20) @@ -601,6 +734,7 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } func saveAllAsync(scores: [GameScore], // swiftlint:disable:this function_body_length cyclomatic_complexity + transaction: Bool = false, scoresOnServer: [GameScore], callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Save object1") @@ -611,7 +745,8 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le return } - scores.saveAll(callbackQueue: callbackQueue) { result in + scores.saveAll(transaction: transaction, + callbackQueue: callbackQueue) { result in switch result { @@ -828,6 +963,80 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le callbackQueue: .main) } + func testSaveAllAsyncTransaction() { // swiftlint:disable:this function_body_length cyclomatic_complexity + let score = GameScore(score: 10) + let score2 = GameScore(score: 20) + + var scoreOnServer = score + scoreOnServer.objectId = "yarr" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt + scoreOnServer.ACL = nil + + var scoreOnServer2 = score2 + scoreOnServer2.objectId = "yolo" + scoreOnServer2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + scoreOnServer2.updatedAt = scoreOnServer2.createdAt + scoreOnServer2.ACL = nil + + let response = [BatchResponseItem(success: scoreOnServer, error: nil), + BatchResponseItem(success: scoreOnServer2, error: nil)] + let encoded: Data! + do { + encoded = try scoreOnServer.getJSONEncoder().encode(response) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().encode(scoreOnServer) + scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded1) + let encoded2 = try ParseCoding.jsonEncoder().encode(scoreOnServer2) + scoreOnServer2 = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded2) + + } catch { + XCTFail("Should have encoded/decoded. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + self.saveAllAsync(scores: [score, score2], + transaction: true, + scoresOnServer: [scoreOnServer, scoreOnServer2], + callbackQueue: .main) + } + + func testSaveAllAsyncTransactionErrorTooMany() { + let score = GameScore(score: 10) + let score2 = GameScore(score: 20) + let expectation1 = XCTestExpectation(description: "Save object1") + [score, score2].saveAll(batchLimit: 1, transaction: true) { result in + if case .failure(let error) = result { + XCTAssertEqual(error.code, .unknownError) + XCTAssertTrue(error.message.contains("exceed")) + } else { + XCTFail("Should have received error") + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testSaveAllAsyncTransactionErrorChild() { + let score = GameScore(score: 10) + var score2 = GameScore(score: 20) + score2.other = Game2() + let expectation1 = XCTestExpectation(description: "Save object1") + [score, score2].saveAll(transaction: true) { result in + if case .failure(let error) = result { + XCTAssertEqual(error.code, .unknownError) + XCTAssertTrue(error.message.contains("originally")) + } else { + XCTFail("Should have received error") + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + /* Note, the current batchCommand for updateAll returns the original object that was updated as opposed to the latestUpdated. The objective c one just returns true/false */ // swiftlint:disable:next function_body_length cyclomatic_complexity @@ -1370,6 +1579,89 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } } + func testDeleteAllTransaction() { + let response = [BatchResponseItem(success: NoBody(), error: nil), + BatchResponseItem(success: NoBody(), error: nil)] + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(response) + } catch { + XCTFail("Should have encoded/decoded. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + 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) + } + + 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) + } + } + + func testDeleteAllTransactionErrorTooMany() { + do { + _ = try [GameScore(objectId: "yarr"), + GameScore(objectId: "yolo")].deleteAll(batchLimit: 1, + transaction: true) + XCTFail("Should have thrown error") + } catch { + guard let parseError = error as? ParseError else { + XCTFail("Error should have casted to ParseError") + return + } + XCTAssertEqual(parseError.code, .unknownError) + XCTAssertTrue(parseError.message.contains("exceed")) + } + } + #if !os(Linux) && !os(Android) && !os(Windows) func testDeleteAllError() { let parseError = ParseError(code: .objectNotFound, message: "Object not found") @@ -1418,13 +1710,13 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } #endif - func deleteAllAsync(callbackQueue: DispatchQueue) { + func deleteAllAsync(transaction: Bool = false, callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Delete object1") let expectation2 = XCTestExpectation(description: "Delete object2") [GameScore(objectId: "yarr"), GameScore(objectId: "yolo")] - .deleteAll(callbackQueue: callbackQueue) { result in + .deleteAll(transaction: transaction, callbackQueue: callbackQueue) { result in switch result { @@ -1510,6 +1802,39 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le self.deleteAllAsync(callbackQueue: .main) } + func testDeleteAllAsyncMainQueueTransaction() { + let response = [BatchResponseItem(success: NoBody(), error: nil), + BatchResponseItem(success: NoBody(), error: nil)] + + do { + let encoded = try ParseCoding.jsonEncoder().encode(response) + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + } catch { + XCTFail("Should have encoded/decoded. Error \(error)") + return + } + + self.deleteAllAsync(transaction: true, callbackQueue: .main) + } + + func testDeleteAllAsyncTransactionErrorTooMany() { + let expectation1 = XCTestExpectation(description: "Save object1") + [GameScore(objectId: "yarr"), + GameScore(objectId: "yolo")].deleteAll(batchLimit: 1, + transaction: true) { result in + if case .failure(let error) = result { + XCTAssertEqual(error.code, .unknownError) + XCTAssertTrue(error.message.contains("exceed")) + } else { + XCTFail("Should have received error") + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + func deleteAllAsyncError(parseError: ParseError, callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Delete object1")