diff --git a/.codecov.yml b/.codecov.yml index 5f226e7a9..222dc0bd4 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -4,7 +4,7 @@ coverage: status: patch: default: - target: 49 + target: auto changes: false project: default: diff --git a/CHANGELOG.md b/CHANGELOG.md index a57275dc6..e94169f83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,15 @@ # Parse-Swift Changelog ### main -[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.9.6...main) +[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.9.7...main) * _Contributing to this repo? Add info about your change here to be included in the next release_ +### 1.9.7 +[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.9.6...1.9.7) + +__Improvements__ +- Properly allow a mixed custom objectId environment without compromising safety checks using .save(). If a developer wants to ignore the objectId checks, they need to specify isIgnoreCustomObjectIdConfig = true each time ([#222](https://github.com/parse-community/Parse-Swift/pull/222)), thanks to [Corey Baker](https://github.com/cbaker6). + ### 1.9.6 [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.9.5...1.9.6) diff --git a/Sources/ParseSwift/API/API+Commands.swift b/Sources/ParseSwift/API/API+Commands.swift index ab5f82c72..565621ff1 100644 --- a/Sources/ParseSwift/API/API+Commands.swift +++ b/Sources/ParseSwift/API/API+Commands.swift @@ -344,8 +344,9 @@ internal extension API.Command { } // MARK: Saving ParseObjects - static func save(_ object: T) throws -> API.Command where T: ParseObject { - if ParseSwift.configuration.allowCustomObjectId && object.objectId == nil { + static func save(_ object: T, + isIgnoreCustomObjectIdConfig: Bool) throws -> API.Command where T: ParseObject { + if ParseSwift.configuration.allowCustomObjectId && object.objectId == nil && !isIgnoreCustomObjectIdConfig { throw ParseError(code: .missingObjectId, message: "objectId must not be nil") } if object.isSaved { diff --git a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift index f9e7c500d..3836ba4f4 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift @@ -42,9 +42,11 @@ public extension ParseInstallation { - 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. */ - func savePublisher(options: API.Options = []) -> Future { + func savePublisher(isIgnoreCustomObjectIdConfig: Bool = false, + options: API.Options = []) -> Future { Future { promise in - self.save(options: options, + self.save(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, + options: options, completion: promise) } } @@ -102,10 +104,12 @@ public extension Sequence where Element: ParseInstallation { */ func saveAllPublisher(batchLimit limit: Int? = nil, transaction: Bool = false, + isIgnoreCustomObjectIdConfig: Bool = false, options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in self.saveAll(batchLimit: limit, transaction: transaction, + isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, options: options, completion: promise) } diff --git a/Sources/ParseSwift/Objects/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift index 1dd79bf23..eb4f0ad5f 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation.swift @@ -426,6 +426,32 @@ extension ParseInstallation { - important: If an object saved has the same objectId as current, it will automatically update the current. */ public func save(options: API.Options = []) throws -> Self { + try save(isIgnoreCustomObjectIdConfig: false, + options: options) + } + + /** + Saves the `ParseInstallation` *synchronously* and throws an error if there's an issue. + + - parameter isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` + when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed + `objectId` environments. Defaults to false. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - throws: An error of type `ParseError`. + - returns: Returns saved `ParseInstallation`. + - important: If an object saved has the same objectId as current, it will automatically update the current. + - warning: If you are using `ParseConfiguration.allowCustomObjectId = true` + and plan to generate all of your `objectId`'s on the client-side then you should leave + `isIgnoreCustomObjectIdConfig = false`. Setting + `ParseConfiguration.allowCustomObjectId = true` and + `isIgnoreCustomObjectIdConfig = true` means the client will generate `objectId`'s + and the server will generate an `objectId` only when the client does not provide one. This can + increase the probability of colliding `objectId`'s as the client and server `objectId`'s may be generated using + different algorithms. This can also lead to overwriting of `ParseObject`'s by accident as the + client-side checks are disabled. Developers are responsible for handling such cases. + */ + public func save(isIgnoreCustomObjectIdConfig: Bool, + options: API.Options = []) throws -> Self { var options = options options.insert(.cachePolicy(.reloadIgnoringLocalCacheData)) var childObjects: [String: PointerType]? @@ -445,7 +471,7 @@ extension ParseInstallation { throw error } - let result: Self = try saveCommand() + let result: Self = try saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) .execute(options: options, callbackQueue: .main, childObjects: childObjects, @@ -457,13 +483,26 @@ extension ParseInstallation { /** Saves the `ParseInstallation` *asynchronously* and executes the given callback block. + - parameter isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` + when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed + `objectId` environments. Defaults to false. - 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)`. - important: If an object saved has the same objectId as current, it will automatically update the current. + - warning: If you are using `ParseConfiguration.allowCustomObjectId = true` + and plan to generate all of your `objectId`'s on the client-side then you should leave + `isIgnoreCustomObjectIdConfig = false`. Setting + `ParseConfiguration.allowCustomObjectId = true` and + `isIgnoreCustomObjectIdConfig = true` means the client will generate `objectId`'s + and the server will generate an `objectId` only when the client does not provide one. This can + increase the probability of colliding `objectId`'s as the client and server `objectId`'s may be generated using + different algorithms. This can also lead to overwriting of `ParseObject`'s by accident as the + client-side checks are disabled. Developers are responsible for handling such cases. */ public func save( + isIgnoreCustomObjectIdConfig: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void @@ -473,7 +512,7 @@ extension ParseInstallation { self.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, error) in guard let parseError = error else { do { - try self.saveCommand() + try self.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) .executeAsync(options: options, callbackQueue: callbackQueue, childObjects: savedChildObjects, @@ -515,8 +554,8 @@ extension ParseInstallation { } } - func saveCommand() throws -> API.Command { - if ParseSwift.configuration.allowCustomObjectId && objectId == nil { + func saveCommand(isIgnoreCustomObjectIdConfig: Bool = false) throws -> API.Command { + if ParseSwift.configuration.allowCustomObjectId && objectId == nil && !isIgnoreCustomObjectIdConfig { throw ParseError(code: .missingObjectId, message: "objectId must not be nil") } if isSaved { @@ -643,6 +682,9 @@ 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 isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` + when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed + `objectId` environments. Defaults to false. - 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. @@ -653,9 +695,19 @@ public extension Sequence where Element: ParseInstallation { - 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. + - warning: If you are using `ParseConfiguration.allowCustomObjectId = true` + and plan to generate all of your `objectId`'s on the client-side then you should leave + `isIgnoreCustomObjectIdConfig = false`. Setting + `ParseConfiguration.allowCustomObjectId = true` and + `isIgnoreCustomObjectIdConfig = true` means the client will generate `objectId`'s + and the server will generate an `objectId` only when the client does not provide one. This can + increase the probability of colliding `objectId`'s as the client and server `objectId`'s may be generated using + different algorithms. This can also lead to overwriting of `ParseObject`'s by accident as the + client-side checks are disabled. Developers are responsible for handling such cases. */ func saveAll(batchLimit limit: Int? = nil, // swiftlint:disable:this function_body_length transaction: Bool = false, + isIgnoreCustomObjectIdConfig: Bool = false, options: API.Options = []) throws -> [(Result)] { var options = options options.insert(.cachePolicy(.reloadIgnoringLocalCacheData)) @@ -703,7 +755,9 @@ public extension Sequence where Element: ParseInstallation { } var returnBatch = [(Result)]() - let commands = try map { try $0.saveCommand() } + let commands = try map { + try $0.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) + } let batchLimit: Int! if transaction { batchLimit = commands.count @@ -731,6 +785,9 @@ public extension Sequence where Element: ParseInstallation { 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 isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` + when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed + `objectId` environments. Defaults to false. - 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. @@ -739,10 +796,20 @@ public extension Sequence where Element: ParseInstallation { - 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. + - warning: If you are using `ParseConfiguration.allowCustomObjectId = true` + and plan to generate all of your `objectId`'s on the client-side then you should leave + `isIgnoreCustomObjectIdConfig = false`. Setting + `ParseConfiguration.allowCustomObjectId = true` and + `isIgnoreCustomObjectIdConfig = true` means the client will generate `objectId`'s + and the server will generate an `objectId` only when the client does not provide one. This can + increase the probability of colliding `objectId`'s as the client and server `objectId`'s may be generated using + different algorithms. This can also lead to overwriting of `ParseObject`'s by accident as the + client-side checks are disabled. Developers are responsible for handling such cases. */ func saveAll( // swiftlint:disable:this function_body_length cyclomatic_complexity batchLimit limit: Int? = nil, transaction: Bool = false, + isIgnoreCustomObjectIdConfig: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void @@ -805,7 +872,9 @@ public extension Sequence where Element: ParseInstallation { do { var returnBatch = [(Result)]() - let commands = try map { try $0.saveCommand() } + let commands = try map { + try $0.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) + } let batchLimit: Int! if transaction { batchLimit = commands.count diff --git a/Sources/ParseSwift/Objects/ParseObject+combine.swift b/Sources/ParseSwift/Objects/ParseObject+combine.swift index 2ef066b40..c64a59f10 100644 --- a/Sources/ParseSwift/Objects/ParseObject+combine.swift +++ b/Sources/ParseSwift/Objects/ParseObject+combine.swift @@ -39,9 +39,11 @@ public extension ParseObject { - 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. */ - func savePublisher(options: API.Options = []) -> Future { + func savePublisher(isIgnoreCustomObjectIdConfig: Bool = false, + options: API.Options = []) -> Future { Future { promise in - self.save(options: options, + self.save(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, + options: options, completion: promise) } } @@ -89,19 +91,33 @@ public extension Sequence where Element: ParseObject { 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 isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` + when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed + `objectId` environments. Defaults to false. - 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. + - warning: If you are using `ParseConfiguration.allowCustomObjectId = true` + and plan to generate all of your `objectId`'s on the client-side then you should leave + `isIgnoreCustomObjectIdConfig = false`. Setting + `ParseConfiguration.allowCustomObjectId = true` and + `isIgnoreCustomObjectIdConfig = true` means the client will generate `objectId`'s + and the server will generate an `objectId` only when the client does not provide one. This can + increase the probability of colliiding `objectId`'s as the client and server `objectId`'s may be generated using + different algorithms. This can also lead to overwriting of `ParseObject`'s by accident as the + client-side checks are disabled. Developers are responsible for handling such cases. */ func saveAllPublisher(batchLimit limit: Int? = nil, transaction: Bool = false, + isIgnoreCustomObjectIdConfig: Bool = false, options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in self.saveAll(batchLimit: limit, transaction: transaction, + isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, options: options, completion: promise) } diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index 94c2f8b6f..ae1bb64ff 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -67,6 +67,9 @@ public extension Sequence where Element: ParseObject { 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 isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` + when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed + `objectId` environments. Defaults to false. - 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. @@ -74,9 +77,19 @@ public extension Sequence where Element: ParseObject { - 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. + - warning: If you are using `ParseConfiguration.allowCustomObjectId = true` + and plan to generate all of your `objectId`'s on the client-side then you should leave + `isIgnoreCustomObjectIdConfig = false`. Setting + `ParseConfiguration.allowCustomObjectId = true` and + `isIgnoreCustomObjectIdConfig = true` means the client will generate `objectId`'s + and the server will generate an `objectId` only when the client does not provide one. This can + increase the probability of colliiding `objectId`'s as the client and server `objectId`'s may be generated using + different algorithms. This can also lead to overwriting of `ParseObject`'s by accident as the + client-side checks are disabled. Developers are responsible for handling such cases. */ func saveAll(batchLimit limit: Int? = nil, // swiftlint:disable:this function_body_length transaction: Bool = false, + isIgnoreCustomObjectIdConfig: Bool = false, options: API.Options = []) throws -> [(Result)] { var options = options options.insert(.cachePolicy(.reloadIgnoringLocalCacheData)) @@ -124,7 +137,7 @@ public extension Sequence where Element: ParseObject { } var returnBatch = [(Result)]() - let commands = try map { try $0.saveCommand() } + let commands = try map { try $0.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) } let batchLimit: Int! if transaction { batchLimit = commands.count @@ -151,6 +164,9 @@ public extension Sequence where Element: ParseObject { 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 isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` + when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed + `objectId` environments. Defaults to false. - 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. @@ -158,10 +174,20 @@ public extension Sequence where Element: ParseObject { - 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. + - warning: If you are using `ParseConfiguration.allowCustomObjectId = true` + and plan to generate all of your `objectId`'s on the client-side then you should leave + `isIgnoreCustomObjectIdConfig = false`. Setting + `ParseConfiguration.allowCustomObjectId = true` and + `isIgnoreCustomObjectIdConfig = true` means the client will generate `objectId`'s + and the server will generate an `objectId` only when the client does not provide one. This can + increase the probability of colliiding `objectId`'s as the client and server `objectId`'s may be generated using + different algorithms. This can also lead to overwriting of `ParseObject`'s by accident as the + client-side checks are disabled. Developers are responsible for handling such cases. */ func saveAll( // swiftlint:disable:this function_body_length cyclomatic_complexity batchLimit limit: Int? = nil, transaction: Bool = false, + isIgnoreCustomObjectIdConfig: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void @@ -224,7 +250,9 @@ public extension Sequence where Element: ParseObject { do { var returnBatch = [(Result)]() - let commands = try map { try $0.saveCommand() } + let commands = try map { + try $0.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) + } let batchLimit: Int! if transaction { batchLimit = commands.count @@ -580,6 +608,30 @@ extension ParseObject { - returns: Returns saved `ParseObject`. */ public func save(options: API.Options = []) throws -> Self { + try save(isIgnoreCustomObjectIdConfig: false, options: options) + } + + /** + Saves the `ParseObject` *synchronously* and throws an error if there's an issue. + - parameter isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` + when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed + `objectId` environments. Defaults to false. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - throws: An error of type `ParseError`. + + - returns: Returns saved `ParseObject`. + - warning: If you are using `ParseConfiguration.allowCustomObjectId = true` + and plan to generate all of your `objectId`'s on the client-side then you should leave + `isIgnoreCustomObjectIdConfig = false`. Setting + `ParseConfiguration.allowCustomObjectId = true` and + `isIgnoreCustomObjectIdConfig = true` means the client will generate `objectId`'s + and the server will generate an `objectId` only when the client does not provide one. This can + increase the probability of colliiding `objectId`'s as the client and server `objectId`'s may be generated using + different algorithms. This can also lead to overwriting of `ParseObject`'s by accident as the + client-side checks are disabled. Developers are responsible for handling such cases. + */ + public func save(isIgnoreCustomObjectIdConfig: Bool = false, + options: API.Options = []) throws -> Self { var childObjects: [String: PointerType]? var childFiles: [UUID: ParseFile]? var error: ParseError? @@ -599,7 +651,7 @@ extension ParseObject { throw error } - return try saveCommand() + return try saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) .execute(options: options, callbackQueue: .main, childObjects: childObjects, @@ -609,12 +661,25 @@ extension ParseObject { /** Saves the `ParseObject` *asynchronously* and executes the given callback block. + - parameter isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` + when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed + `objectId` environments. Defaults to false. - 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)`. + - warning: If you are using `ParseConfiguration.allowCustomObjectId = true` + and plan to generate all of your `objectId`'s on the client-side then you should leave + `isIgnoreCustomObjectIdConfig = false`. Setting + `ParseConfiguration.allowCustomObjectId = true` and + `isIgnoreCustomObjectIdConfig = true` means the client will generate `objectId`'s + and the server will generate an `objectId` only when the client does not provide one. This can + increase the probability of colliiding `objectId`'s as the client and server `objectId`'s may be generated using + different algorithms. This can also lead to overwriting of `ParseObject`'s by accident as the + client-side checks are disabled. Developers are responsible for handling such cases. */ public func save( + isIgnoreCustomObjectIdConfig: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void @@ -622,10 +687,11 @@ extension ParseObject { self.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, error) in guard let parseError = error else { do { - try self.saveCommand().executeAsync(options: options, - callbackQueue: callbackQueue, - childObjects: savedChildObjects, - childFiles: savedChildFiles) { result in + try self.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) + .executeAsync(options: options, + callbackQueue: callbackQueue, + childObjects: savedChildObjects, + childFiles: savedChildFiles) { result in callbackQueue.async { completion(result) } @@ -647,8 +713,8 @@ extension ParseObject { } } - internal func saveCommand() throws -> API.Command { - try API.Command.save(self) + internal func saveCommand(isIgnoreCustomObjectIdConfig: Bool = false) throws -> API.Command { + try API.Command.save(self, isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) } // swiftlint:disable:next function_body_length diff --git a/Sources/ParseSwift/Objects/ParseUser+combine.swift b/Sources/ParseSwift/Objects/ParseUser+combine.swift index 82e0d68d4..b660f8461 100644 --- a/Sources/ParseSwift/Objects/ParseUser+combine.swift +++ b/Sources/ParseSwift/Objects/ParseUser+combine.swift @@ -160,13 +160,27 @@ public extension ParseUser { /** Saves the `ParseUser` *asynchronously* and publishes when complete. + - parameter isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` + when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed + `objectId` environments. Defaults to false. - 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 you are using `ParseConfiguration.allowCustomObjectId = true` + and plan to generate all of your `objectId`'s on the client-side then you should leave + `isIgnoreCustomObjectIdConfig = false`. Setting + `ParseConfiguration.allowCustomObjectId = true` and + `isIgnoreCustomObjectIdConfig = true` means the client will generate `objectId`'s + and the server will generate an `objectId` only when the client does not provide one. This can + increase the probability of colliiding `objectId`'s as the client and server `objectId`'s may be generated using + different algorithms. This can also lead to overwriting of `ParseObject`'s by accident as the + client-side checks are disabled. Developers are responsible for handling such cases. */ - func savePublisher(options: API.Options = []) -> Future { + func savePublisher(options: API.Options = [], + isIgnoreCustomObjectIdConfig: Bool = false) -> Future { Future { promise in - self.save(options: options, + self.save(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, + options: options, completion: promise) } } @@ -215,19 +229,33 @@ public extension Sequence where Element: ParseUser { 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 isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` + when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed + `objectId` environments. Defaults to false. - 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. + - warning: If you are using `ParseConfiguration.allowCustomObjectId = true` + and plan to generate all of your `objectId`'s on the client-side then you should leave + `isIgnoreCustomObjectIdConfig = false`. Setting + `ParseConfiguration.allowCustomObjectId = true` and + `isIgnoreCustomObjectIdConfig = true` means the client will generate `objectId`'s + and the server will generate an `objectId` only when the client does not provide one. This can + increase the probability of colliiding `objectId`'s as the client and server `objectId`'s may be generated using + different algorithms. This can also lead to overwriting of `ParseObject`'s by accident as the + client-side checks are disabled. Developers are responsible for handling such cases. */ func saveAllPublisher(batchLimit limit: Int? = nil, transaction: Bool = false, + isIgnoreCustomObjectIdConfig: Bool = false, options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in self.saveAll(batchLimit: limit, transaction: transaction, + isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, options: options, completion: promise) } diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index 722d01048..4b85e5243 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -808,6 +808,31 @@ extension ParseUser { - important: If an object saved has the same objectId as current, it will automatically update the current. */ public func save(options: API.Options = []) throws -> Self { + try save(isIgnoreCustomObjectIdConfig: false, options: options) + } + + /** + Saves the `ParseUser` *synchronously* and throws an error if there's an issue. + + - parameter isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` + when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed + `objectId` environments. Defaults to false. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - throws: An error of type `ParseError`. + - returns: Returns saved `ParseUser`. + - important: If an object saved has the same objectId as current, it will automatically update the current. + - warning: If you are using `ParseConfiguration.allowCustomObjectId = true` + and plan to generate all of your `objectId`'s on the client-side then you should leave + `isIgnoreCustomObjectIdConfig = false`. Setting + `ParseConfiguration.allowCustomObjectId = true` and + `isIgnoreCustomObjectIdConfig = true` means the client will generate `objectId`'s + and the server will generate an `objectId` only when the client does not provide one. This can + increase the probability of colliiding `objectId`'s as the client and server `objectId`'s may be generated using + different algorithms. This can also lead to overwriting of `ParseObject`'s by accident as the + client-side checks are disabled. Developers are responsible for handling such cases. + */ + public func save(isIgnoreCustomObjectIdConfig: Bool, + options: API.Options = []) throws -> Self { var childObjects: [String: PointerType]? var childFiles: [UUID: ParseFile]? var error: ParseError? @@ -827,7 +852,7 @@ extension ParseUser { throw error } - let result: Self = try saveCommand() + let result: Self = try saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) .execute(options: options, callbackQueue: .main, childObjects: childObjects, @@ -839,13 +864,26 @@ extension ParseUser { /** Saves the `ParseUser` *asynchronously* and executes the given callback block. + - parameter isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` + when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed + `objectId` environments. Defaults to false. - 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)`. - important: If an object saved has the same objectId as current, it will automatically update the current. + - warning: If you are using `ParseConfiguration.allowCustomObjectId = true` + and plan to generate all of your `objectId`'s on the client-side then you should leave + `isIgnoreCustomObjectIdConfig = false`. Setting + `ParseConfiguration.allowCustomObjectId = true` and + `isIgnoreCustomObjectIdConfig = true` means the client will generate `objectId`'s + and the server will generate an `objectId` only when the client does not provide one. This can + increase the probability of colliiding `objectId`'s as the client and server `objectId`'s may be generated using + different algorithms. This can also lead to overwriting of `ParseObject`'s by accident as the + client-side checks are disabled. Developers are responsible for handling such cases. */ public func save( + isIgnoreCustomObjectIdConfig: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void @@ -855,7 +893,7 @@ extension ParseUser { self.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, error) in guard let parseError = error else { do { - try self.saveCommand() + try self.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) .executeAsync(options: options, callbackQueue: callbackQueue, childObjects: savedChildObjects, @@ -884,8 +922,8 @@ extension ParseUser { } } - func saveCommand() throws -> API.Command { - if ParseSwift.configuration.allowCustomObjectId && objectId == nil { + func saveCommand(isIgnoreCustomObjectIdConfig: Bool = false) throws -> API.Command { + if ParseSwift.configuration.allowCustomObjectId && objectId == nil && !isIgnoreCustomObjectIdConfig { throw ParseError(code: .missingObjectId, message: "objectId must not be nil") } if isSaved { @@ -1004,6 +1042,9 @@ public extension Sequence where Element: ParseUser { 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 isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` + when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed + `objectId` environments. Defaults to false. - 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. @@ -1012,9 +1053,19 @@ public extension Sequence where Element: ParseUser { - 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. + - warning: If you are using `ParseConfiguration.allowCustomObjectId = true` + and plan to generate all of your `objectId`'s on the client-side then you should leave + `isIgnoreCustomObjectIdConfig = false`. Setting + `ParseConfiguration.allowCustomObjectId = true` and + `isIgnoreCustomObjectIdConfig = true` means the client will generate `objectId`'s + and the server will generate an `objectId` only when the client does not provide one. This can + increase the probability of colliiding `objectId`'s as the client and server `objectId`'s may be generated using + different algorithms. This can also lead to overwriting of `ParseObject`'s by accident as the + client-side checks are disabled. Developers are responsible for handling such cases. */ func saveAll(batchLimit limit: Int? = nil, // swiftlint:disable:this function_body_length transaction: Bool = false, + isIgnoreCustomObjectIdConfig: Bool = false, options: API.Options = []) throws -> [(Result)] { var childObjects = [String: PointerType]() var childFiles = [UUID: ParseFile]() @@ -1061,7 +1112,9 @@ public extension Sequence where Element: ParseUser { } var returnBatch = [(Result)]() - let commands = try map { try $0.saveCommand() } + let commands = try map { + try $0.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) + } let batchLimit: Int! if transaction { batchLimit = commands.count @@ -1089,6 +1142,9 @@ public extension Sequence where Element: ParseUser { 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 isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` + when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed + `objectId` environments. Defaults to false. - 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. @@ -1097,10 +1153,20 @@ public extension Sequence where Element: ParseUser { - 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. + - warning: If you are using `ParseConfiguration.allowCustomObjectId = true` + and plan to generate all of your `objectId`'s on the client-side then you should leave + `isIgnoreCustomObjectIdConfig = false`. Setting + `ParseConfiguration.allowCustomObjectId = true` and + `isIgnoreCustomObjectIdConfig = true` means the client will generate `objectId`'s + and the server will generate an `objectId` only when the client does not provide one. This can + increase the probability of colliiding `objectId`'s as the client and server `objectId`'s may be generated using + different algorithms. This can also lead to overwriting of `ParseObject`'s by accident as the + client-side checks are disabled. Developers are responsible for handling such cases. */ func saveAll( // swiftlint:disable:this function_body_length cyclomatic_complexity batchLimit limit: Int? = nil, transaction: Bool = false, + isIgnoreCustomObjectIdConfig: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void @@ -1162,7 +1228,9 @@ public extension Sequence where Element: ParseUser { do { var returnBatch = [(Result)]() - let commands = try map { try $0.saveCommand() } + let commands = try map { + try $0.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) + } let batchLimit: Int! if transaction { batchLimit = commands.count diff --git a/Sources/ParseSwift/Operations/ParseOperation.swift b/Sources/ParseSwift/Operations/ParseOperation.swift index fe1f90b5d..643cc0ca7 100644 --- a/Sources/ParseSwift/Operations/ParseOperation.swift +++ b/Sources/ParseSwift/Operations/ParseOperation.swift @@ -351,7 +351,7 @@ extension ParseOperation { */ public func save(options: API.Options = []) throws -> T { guard let target = self.target else { - throw ParseError(code: .unknownError, message: "Target shouldn't be nil") + throw ParseError(code: .unknownError, message: "Target shouldn't be nil.") } if !target.isSaved { throw ParseError(code: .missingObjectId, message: "ParseObject isn't saved.") @@ -375,7 +375,7 @@ extension ParseOperation { ) { guard let target = self.target else { callbackQueue.async { - let error = ParseError(code: .missingObjectId, message: "ParseObject isn't saved.") + let error = ParseError(code: .unknownError, message: "Target shouldn't be nil.") completion(.failure(error)) } return diff --git a/Sources/ParseSwift/ParseConstants.swift b/Sources/ParseSwift/ParseConstants.swift index 95a109604..b2317af41 100644 --- a/Sources/ParseSwift/ParseConstants.swift +++ b/Sources/ParseSwift/ParseConstants.swift @@ -10,7 +10,7 @@ import Foundation enum ParseConstants { static let sdk = "swift" - static let version = "1.9.6" + static let version = "1.9.7" static let fileManagementDirectory = "parse/" static let fileManagementPrivateDocumentsDirectory = "Private Documents/" static let fileManagementLibraryDirectory = "Library/" diff --git a/Sources/ParseSwift/Storage/KeychainStore.swift b/Sources/ParseSwift/Storage/KeychainStore.swift index 40c2f53a0..5f9acb0dc 100644 --- a/Sources/ParseSwift/Storage/KeychainStore.swift +++ b/Sources/ParseSwift/Storage/KeychainStore.swift @@ -19,7 +19,7 @@ func getKeychainQueryTemplate(forService service: String) -> [String: String] { query[kSecAttrService as String] = service } query[kSecClass as String] = kSecClassGenericPassword as String - query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as String + query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as String return query } diff --git a/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift b/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift index 390fff13a..c3c498613 100644 --- a/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift @@ -432,12 +432,23 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ XCTAssertThrowsError(try score.saveCommand()) } + func testSaveCommandNoObjectIdIgnoreConfig() throws { + let score = GameScore(score: 10) + _ = try score.saveCommand(isIgnoreCustomObjectIdConfig: true) + } + func testUpdateCommandNoObjectId() throws { var score = GameScore(score: 10) score.createdAt = Date() XCTAssertThrowsError(try score.saveCommand()) } + func testUpdateCommandNoObjectIdIgnoreConfig() throws { + var score = GameScore(score: 10) + score.createdAt = Date() + _ = try score.saveCommand(isIgnoreCustomObjectIdConfig: true) + } + func testSaveAllNoObjectIdCommand() throws { let score = GameScore(score: 10) let score2 = GameScore(score: 20) @@ -459,12 +470,23 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ XCTAssertThrowsError(try user.saveCommand()) } + func testUserSaveCommandNoObjectIdIgnoreConfig() throws { + let user = User() + _ = try user.saveCommand(isIgnoreCustomObjectIdConfig: true) + } + func testUserUpdateCommandNoObjectId() throws { var user = User() user.createdAt = Date() XCTAssertThrowsError(try user.saveCommand()) } + func testUserUpdateCommandNoObjectIdIgnoreConfig() throws { + var user = User() + user.createdAt = Date() + _ = try user.saveCommand(isIgnoreCustomObjectIdConfig: true) + } + func testUserSaveAllNoObjectIdCommand() throws { let user = User() let user2 = User() @@ -486,12 +508,23 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ XCTAssertThrowsError(try installation.saveCommand()) } + func testInstallationSaveCommandNoObjectIdIgnoreConfig() throws { + let installation = Installation() + _ = try installation.saveCommand(isIgnoreCustomObjectIdConfig: true) + } + func testInstallationUpdateCommandNoObjectId() throws { var installation = Installation() installation.createdAt = Date() XCTAssertThrowsError(try installation.saveCommand()) } + func testInstallationUpdateCommandNoObjectIdIgnoreConfig() throws { + var installation = Installation() + installation.createdAt = Date() + _ = try installation.saveCommand(isIgnoreCustomObjectIdConfig: true) + } + func testInstallationSaveAllNoObjectIdCommand() throws { let installation = Installation() let installation2 = Installation() @@ -542,6 +575,35 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ XCTAssertThrowsError(try score.save()) } + func testSaveNoObjectIdIgnoreConfig() { // swiftlint:disable:this function_body_length + let score = GameScore(score: 10) + + var scoreOnServer = score + scoreOnServer.objectId = "yarr" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(scoreOnServer) + //Get dates in correct format from ParseDecoding strategy + scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + do { + let saved = try score.save(isIgnoreCustomObjectIdConfig: true) + XCTAssert(saved.hasSameObjectId(as: scoreOnServer)) + } catch { + XCTFail(error.localizedDescription) + } + } + func testUpdate() { var score = GameScore(score: 10) score.objectId = "yarr" @@ -579,12 +641,48 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ XCTAssertThrowsError(try score.save()) } + func testUpdateNoObjectIdIgnoreConfig() { + var score = GameScore(score: 10) + score.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + score.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + score.ACL = nil + + var scoreOnServer = score + scoreOnServer.objectId = "yarr" + scoreOnServer.updatedAt = Date() + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(scoreOnServer) + //Get dates in correct format from ParseDecoding strategy + scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + do { + let saved = try score.save(isIgnoreCustomObjectIdConfig: true) + XCTAssertTrue(saved.hasSameObjectId(as: scoreOnServer)) + } catch { + XCTFail(error.localizedDescription) + } + } + // swiftlint:disable:next function_body_length - func saveAsync(score: GameScore, scoreOnServer: GameScore, callbackQueue: DispatchQueue) { + func saveAsync(score: GameScore, + scoreOnServer: GameScore, + callbackQueue: DispatchQueue, + isIgnoreCustomObjectIdConfig: Bool = false) { let expectation1 = XCTestExpectation(description: "Save object1") - score.save(options: [], callbackQueue: callbackQueue) { result in + score.save(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, + options: [], + callbackQueue: callbackQueue) { result in switch result { @@ -597,7 +695,9 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ } let expectation2 = XCTestExpectation(description: "Save object2") - score.save(options: [.useMasterKey], callbackQueue: callbackQueue) { result in + score.save(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, + options: [.useMasterKey], + callbackQueue: callbackQueue) { result in switch result { @@ -651,26 +751,47 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ wait(for: [expectation1], timeout: 20.0) } - func updateAsync(score: GameScore, scoreOnServer: GameScore, callbackQueue: DispatchQueue) { + func testSaveNoObjectIdIgnoreConfigAsyncMainQueue() { + let score = GameScore(score: 10) + + var scoreOnServer = score + scoreOnServer.objectId = "yarr" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt + scoreOnServer.ACL = nil + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(scoreOnServer) + //Get dates in correct format from ParseDecoding strategy + scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) + } catch { + XCTFail("Should have encoded/decoded: Error: \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + self.saveAsync(score: score, + scoreOnServer: scoreOnServer, + callbackQueue: .main, + isIgnoreCustomObjectIdConfig: true) + } + + func updateAsync(score: GameScore, + scoreOnServer: GameScore, + isIgnoreCustomObjectIdConfig: Bool = false, + callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Update object1") - score.save(options: [], callbackQueue: callbackQueue) { result in + score.save(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, + options: [], + callbackQueue: callbackQueue) { result in switch result { case .success(let saved): - guard let savedUpdatedAt = saved.updatedAt else { - XCTFail("Should unwrap dates") - expectation1.fulfill() - return - } - guard let originalUpdatedAt = score.updatedAt else { - XCTFail("Should unwrap dates") - expectation1.fulfill() - return - } - XCTAssertGreaterThan(savedUpdatedAt, originalUpdatedAt) XCTAssertNil(saved.ACL) case .failure(let error): XCTFail(error.localizedDescription) @@ -679,7 +800,9 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ } let expectation2 = XCTestExpectation(description: "Update object2") - score.save(options: [.useMasterKey], callbackQueue: callbackQueue) { result in + score.save(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, + options: [.useMasterKey], + callbackQueue: callbackQueue) { result in switch result { @@ -734,6 +857,33 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ wait(for: [expectation1], timeout: 20.0) } + func testUpdateNoObjectIdIgnoreConfigAsyncMainQueue() { + var score = GameScore(score: 10) + score.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + score.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + score.ACL = nil + + var scoreOnServer = score + scoreOnServer.objectId = "yarr" + scoreOnServer.updatedAt = Date() + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(scoreOnServer) + //Get dates in correct format from ParseDecoding strategy + scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) + } catch { + XCTFail("Should have encoded/decoded: Error: \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + self.updateAsync(score: score, + scoreOnServer: scoreOnServer, + isIgnoreCustomObjectIdConfig: true, + callbackQueue: .main) + } + func testSaveAll() { // swiftlint:disable:this function_body_length cyclomatic_complexity var score = GameScore(score: 10) score.objectId = "yarr" @@ -801,6 +951,67 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ XCTAssertThrowsError(try [score, score2].saveAll()) } + func testSaveAllNoObjectIdIgnoreConfig() { // 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(isIgnoreCustomObjectIdConfig: true) + + XCTAssertEqual(saved.count, 2) + switch saved[0] { + + case .success(let first): + XCTAssert(first.hasSameObjectId(as: scoreOnServer)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + switch saved[1] { + + case .success(let second): + XCTAssert(second.hasSameObjectId(as: scoreOnServer2)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + } catch { + XCTFail(error.localizedDescription) + } + } + func testSaveAllNoObjectIdAsync() throws { let score = GameScore(score: 10) let score2 = GameScore(score: 20) @@ -939,6 +1150,36 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ XCTAssertThrowsError(try score.save()) } + func testUserSaveNoObjectIdIgnoreConfig() { // swiftlint:disable:this function_body_length + var user = User() + user.ACL = nil + + var userOnServer = user + userOnServer.objectId = "yarr" + userOnServer.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + userOnServer.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(userOnServer) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try userOnServer.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + do { + let saved = try user.save(isIgnoreCustomObjectIdConfig: true) + XCTAssert(saved.hasSameObjectId(as: userOnServer)) + } catch { + XCTFail(error.localizedDescription) + } + } + func testUserUpdate() { var user = User() user.objectId = "yarr" @@ -976,12 +1217,47 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ XCTAssertThrowsError(try user.save()) } + func testUserUpdateNoObjectIdIgnoreConfig() { + var user = User() + user.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + user.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + user.ACL = nil + + var userOnServer = user + userOnServer.objectId = "yarr" + userOnServer.updatedAt = Date() + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(userOnServer) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try userOnServer.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + do { + let saved = try user.save(isIgnoreCustomObjectIdConfig: true) + XCTAssertTrue(saved.hasSameObjectId(as: userOnServer)) + } catch { + XCTFail(error.localizedDescription) + } + } + // swiftlint:disable:next function_body_length - func saveUserAsync(user: User, userOnServer: User, callbackQueue: DispatchQueue) { + func saveUserAsync(user: User, userOnServer: User, + isIgnoreCustomObjectIdConfig: Bool = false, + callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Update object1") - user.save(options: [], callbackQueue: callbackQueue) { result in + user.save(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, + options: [], + callbackQueue: callbackQueue) { result in switch result { @@ -1034,7 +1310,35 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ wait(for: [expectation1], timeout: 20.0) } - func updateUserAsync(user: User, userOnServer: User, callbackQueue: DispatchQueue) { + func testUserSaveNoObjectIdIgnoreConfigAsyncMainQueue() { + var user = User() + user.ACL = nil + + var userOnServer = user + userOnServer.objectId = "yarr" + userOnServer.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + userOnServer.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(userOnServer) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try userOnServer.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should have encoded/decoded: Error: \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + self.saveUserAsync(user: user, + userOnServer: userOnServer, + isIgnoreCustomObjectIdConfig: true, + callbackQueue: .main) + } + + func updateUserAsync(user: User, userOnServer: User, + isIgnoreCustomObjectIdConfig: Bool = false, + callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Update object1") @@ -1246,6 +1550,68 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ XCTAssertThrowsError(try [user, user2].saveAll()) } + // swiftlint:disable:next function_body_length cyclomatic_complexity + func testUserUpdateAllNoObjectIdIgnoreConfig() { + var user = User() + user.createdAt = Date() + var user2 = User() + user2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + + var userOnServer = user + userOnServer.objectId = "yarr" + userOnServer.updatedAt = userOnServer.createdAt + userOnServer.ACL = nil + + var userOnServer2 = user2 + userOnServer2.objectId = "yolo" + userOnServer2.updatedAt = userOnServer2.createdAt + userOnServer2.ACL = nil + + let response = [BatchResponseItem(success: userOnServer, error: nil), + BatchResponseItem(success: userOnServer2, error: nil)] + let encoded: Data! + do { + encoded = try userOnServer.getJSONEncoder().encode(response) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().encode(userOnServer) + userOnServer = try userOnServer.getDecoder().decode(User.self, from: encoded1) + let encoded2 = try ParseCoding.jsonEncoder().encode(userOnServer2) + userOnServer2 = try userOnServer.getDecoder().decode(User.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 [user, user2].saveAll(isIgnoreCustomObjectIdConfig: true) + + XCTAssertEqual(saved.count, 2) + switch saved[0] { + + case .success(let first): + XCTAssert(first.hasSameObjectId(as: userOnServer)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + switch saved[1] { + + case .success(let second): + XCTAssert(second.hasSameObjectId(as: userOnServer2)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + } catch { + XCTFail(error.localizedDescription) + } + } + func testUserUpdateAllNoObjectIdAsync() throws { var user = User() user.createdAt = Date() @@ -1299,6 +1665,36 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ XCTAssertThrowsError(try score.save()) } + func testInstallationSaveNoObjectIdIgnoreConfig() { // swiftlint:disable:this function_body_length + var installation = Installation() + installation.ACL = nil + + var installationOnServer = installation + installationOnServer.objectId = "yarr" + installationOnServer.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + installationOnServer.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(installationOnServer) + //Get dates in correct format from ParseDecoding strategy + installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + do { + let saved = try installation.save(isIgnoreCustomObjectIdConfig: true) + XCTAssert(saved.hasSameObjectId(as: installationOnServer)) + } catch { + XCTFail(error.localizedDescription) + } + } + func testInstallationUpdate() { var installation = Installation() installation.objectId = "yarr" @@ -1336,14 +1732,48 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ XCTAssertThrowsError(try installation.save()) } + func testInstallationUpdateNoObjectIdIgnoreConfig() { + var installation = Installation() + installation.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + installation.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + installation.ACL = nil + + var installationOnServer = installation + installationOnServer.objectId = "yarr" + installationOnServer.updatedAt = Date() + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(installationOnServer) + //Get dates in correct format from ParseDecoding strategy + installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + do { + let saved = try installation.save(isIgnoreCustomObjectIdConfig: true) + XCTAssertTrue(saved.hasSameObjectId(as: installationOnServer)) + } catch { + XCTFail(error.localizedDescription) + } + } + // swiftlint:disable:next function_body_length func saveInstallationAsync(installation: Installation, installationOnServer: Installation, + isIgnoreCustomObjectIdConfig: Bool = false, callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Update object1") - installation.save(options: [], callbackQueue: callbackQueue) { result in + installation.save(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, + options: [], + callbackQueue: callbackQueue) { result in switch result { @@ -1356,7 +1786,9 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ } let expectation2 = XCTestExpectation(description: "Update object2") - installation.save(options: [.useMasterKey], callbackQueue: callbackQueue) { result in + installation.save(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, + options: [.useMasterKey], + callbackQueue: callbackQueue) { result in switch result { @@ -1392,6 +1824,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ } self.saveInstallationAsync(installation: installation, installationOnServer: installationOnServer, + isIgnoreCustomObjectIdConfig: false, callbackQueue: .main) } @@ -1411,13 +1844,42 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ wait(for: [expectation1], timeout: 20.0) } + func testInstallationSaveNoObjectIdIgnoreConfigAsyncMainQueue() { + var installation = Installation() + installation.ACL = nil + + var installationOnServer = installation + installationOnServer.objectId = "yarr" + installationOnServer.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + installationOnServer.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(installationOnServer) + //Get dates in correct format from ParseDecoding strategy + installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded) + } catch { + XCTFail("Should have encoded/decoded: Error: \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + self.saveInstallationAsync(installation: installation, + installationOnServer: installationOnServer, + isIgnoreCustomObjectIdConfig: true, + callbackQueue: .main) + } + func updateInstallationAsync(installation: Installation, installationOnServer: Installation, + isIgnoreCustomObjectIdConfig: Bool = false, callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Update object1") - installation.save(options: [], callbackQueue: callbackQueue) { result in + installation.save(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, + options: [], + callbackQueue: callbackQueue) { result in switch result { @@ -1474,6 +1936,33 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ wait(for: [expectation1], timeout: 20.0) } + func testInstallationUpdateNoObjectIdIgnoreConfigAsyncMainQueue() { + var installation = Installation() + installation.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + installation.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + installation.ACL = nil + + var installationOnServer = installation + installationOnServer.objectId = "yarr" + installationOnServer.updatedAt = Date() + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(installationOnServer) + //Get dates in correct format from ParseDecoding strategy + installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded) + } catch { + XCTFail("Should have encoded/decoded: Error: \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + self.updateInstallationAsync(installation: installation, + installationOnServer: installationOnServer, + isIgnoreCustomObjectIdConfig: true, + callbackQueue: .main) + } + func testInstallationSaveAll() { // swiftlint:disable:this function_body_length cyclomatic_complexity var installation = Installation() installation.objectId = "yarr" @@ -1555,6 +2044,81 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ XCTAssertThrowsError(try [installation, installation2].saveAll()) } + func testInstallationSaveAllIgnoreConfig() { // swiftlint:disable:this function_body_length cyclomatic_complexity + let installation = Installation() + + let installation2 = Installation() + + var installationOnServer = installation + installationOnServer.objectId = "yarr" + installationOnServer.createdAt = Date() + installationOnServer.updatedAt = installationOnServer.createdAt + installationOnServer.ACL = nil + + var installationOnServer2 = installation2 + installationOnServer2.objectId = "yolo" + installationOnServer2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + installationOnServer2.updatedAt = installationOnServer2.createdAt + installationOnServer2.ACL = nil + + let response = [BatchResponseItem(success: installationOnServer, error: nil), + BatchResponseItem(success: installationOnServer2, error: nil)] + let encoded: Data! + do { + encoded = try installationOnServer.getJSONEncoder().encode(response) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().encode(installationOnServer) + installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded1) + let encoded2 = try ParseCoding.jsonEncoder().encode(installationOnServer2) + installationOnServer2 = try installationOnServer.getDecoder().decode(Installation.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 [installation, installation2].saveAll(isIgnoreCustomObjectIdConfig: true) + + XCTAssertEqual(saved.count, 2) + switch saved[0] { + + case .success(let first): + XCTAssert(first.hasSameObjectId(as: installationOnServer)) + guard let savedCreatedAt = first.createdAt, + let savedUpdatedAt = first.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = installationOnServer.createdAt, + let originalUpdatedAt = installationOnServer.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: installationOnServer2)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + } catch { + XCTFail(error.localizedDescription) + } + } + func testInstallationSaveAllNoObjectIdAsync() throws { let installation = Installation() let installation2 = Installation() @@ -1640,6 +2204,68 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ XCTAssertThrowsError(try [installation, installation2].saveAll()) } + // swiftlint:disable:next function_body_length cyclomatic_complexity + func testInstallationUpdateAllNoObjectIdIgnoreConfig() { + var installation = Installation() + installation.createdAt = Date() + var installation2 = Installation() + installation2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + + var installationOnServer = installation + installationOnServer.objectId = "yarr" + installationOnServer.updatedAt = installationOnServer.createdAt + installationOnServer.ACL = nil + + var installationOnServer2 = installation2 + installationOnServer2.objectId = "yolo" + installationOnServer2.updatedAt = installationOnServer2.createdAt + installationOnServer2.ACL = nil + + let response = [BatchResponseItem(success: installationOnServer, error: nil), + BatchResponseItem(success: installationOnServer2, error: nil)] + let encoded: Data! + do { + encoded = try installationOnServer.getJSONEncoder().encode(response) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().encode(installationOnServer) + installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded1) + let encoded2 = try ParseCoding.jsonEncoder().encode(installationOnServer2) + installationOnServer2 = try installationOnServer.getDecoder().decode(Installation.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 [installation, installation2].saveAll(isIgnoreCustomObjectIdConfig: true) + + XCTAssertEqual(saved.count, 2) + switch saved[0] { + + case .success(let first): + XCTAssert(first.hasSameObjectId(as: installationOnServer)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + switch saved[1] { + + case .success(let second): + XCTAssert(second.hasSameObjectId(as: installationOnServer2)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + } catch { + XCTFail(error.localizedDescription) + } + } + func testInstallationUpdateAllNoObjectIdAsync() throws { var installation = Installation() installation.createdAt = Date()