From 3ed5c85b6f4787a0a8e3760b8ad8b2c398e9d871 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Tue, 31 Aug 2021 13:09:09 -0400 Subject: [PATCH 01/13] Allow mixed custom objectId environment --- Sources/ParseSwift/API/API+Commands.swift | 5 +- .../Objects/ParseInstallation+combine.swift | 8 +- .../Objects/ParseInstallation.swift | 81 ++- .../Objects/ParseObject+combine.swift | 20 +- Sources/ParseSwift/Objects/ParseObject.swift | 84 ++- .../Objects/ParseUser+combine.swift | 32 +- Sources/ParseSwift/Objects/ParseUser.swift | 80 ++- .../Operations/ParseOperation.swift | 4 +- .../ParseObjectCustomObjectIdTests.swift | 672 +++++++++++++++++- 9 files changed, 932 insertions(+), 54 deletions(-) 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..9150549a9 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 lead + to collisions of `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 lead + to collisions of `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 lead + to collisions of `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 lead + to collisions of `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..f80c19335 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 lead + to collisions of `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..5b84c797d 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 lead + to collisions of `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 lead + to collisions of `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 lead + to collisions of `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 lead + to collisions of `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..80a12b002 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 lead + to collisions of `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 lead + to collisions of `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..9c876e289 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 lead + to collisions of `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 lead + to collisions of `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 lead + to collisions of `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 lead + to collisions of `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/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() From f77526391838dfc908351d9c13aa88d524a77819 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Tue, 31 Aug 2021 13:18:36 -0400 Subject: [PATCH 02/13] Update change log --- CHANGELOG.md | 8 +++++++- Sources/ParseSwift/ParseConstants.swift | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) 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/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/" From 6514aa3ca4e1500366eedeb234b4673b8f303320 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Tue, 31 Aug 2021 20:24:39 -0400 Subject: [PATCH 03/13] nit on warning --- .../ParseSwift/Objects/ParseInstallation.swift | 16 ++++++++-------- .../ParseSwift/Objects/ParseObject+combine.swift | 4 ++-- Sources/ParseSwift/Objects/ParseObject.swift | 16 ++++++++-------- .../ParseSwift/Objects/ParseUser+combine.swift | 8 ++++---- Sources/ParseSwift/Objects/ParseUser.swift | 16 ++++++++-------- Sources/ParseSwift/Storage/KeychainStore.swift | 2 +- 6 files changed, 31 insertions(+), 31 deletions(-) diff --git a/Sources/ParseSwift/Objects/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift index 9150549a9..6845f2e7d 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation.swift @@ -445,8 +445,8 @@ extension ParseInstallation { `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 lead - to collisions of `objectId`'s as the client and server `objectId`'s may be generated using + 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. */ @@ -496,8 +496,8 @@ extension ParseInstallation { `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 lead - to collisions of `objectId`'s as the client and server `objectId`'s may be generated using + 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. */ @@ -700,8 +700,8 @@ public extension Sequence where Element: ParseInstallation { `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 lead - to collisions of `objectId`'s as the client and server `objectId`'s may be generated using + 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. */ @@ -801,8 +801,8 @@ public extension Sequence where Element: ParseInstallation { `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 lead - to collisions of `objectId`'s as the client and server `objectId`'s may be generated using + 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. */ diff --git a/Sources/ParseSwift/Objects/ParseObject+combine.swift b/Sources/ParseSwift/Objects/ParseObject+combine.swift index f80c19335..c64a59f10 100644 --- a/Sources/ParseSwift/Objects/ParseObject+combine.swift +++ b/Sources/ParseSwift/Objects/ParseObject+combine.swift @@ -105,8 +105,8 @@ public extension Sequence where Element: ParseObject { `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 lead - to collisions of `objectId`'s as the client and server `objectId`'s may be generated using + 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. */ diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index 5b84c797d..ae1bb64ff 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -82,8 +82,8 @@ public extension Sequence where Element: ParseObject { `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 lead - to collisions of `objectId`'s as the client and server `objectId`'s may be generated using + 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. */ @@ -179,8 +179,8 @@ public extension Sequence where Element: ParseObject { `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 lead - to collisions of `objectId`'s as the client and server `objectId`'s may be generated using + 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. */ @@ -625,8 +625,8 @@ extension ParseObject { `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 lead - to collisions of `objectId`'s as the client and server `objectId`'s may be generated using + 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. */ @@ -673,8 +673,8 @@ extension ParseObject { `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 lead - to collisions of `objectId`'s as the client and server `objectId`'s may be generated using + 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. */ diff --git a/Sources/ParseSwift/Objects/ParseUser+combine.swift b/Sources/ParseSwift/Objects/ParseUser+combine.swift index 80a12b002..b660f8461 100644 --- a/Sources/ParseSwift/Objects/ParseUser+combine.swift +++ b/Sources/ParseSwift/Objects/ParseUser+combine.swift @@ -171,8 +171,8 @@ public extension ParseUser { `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 lead - to collisions of `objectId`'s as the client and server `objectId`'s may be generated using + 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. */ @@ -243,8 +243,8 @@ public extension Sequence where Element: ParseUser { `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 lead - to collisions of `objectId`'s as the client and server `objectId`'s may be generated using + 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. */ diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index 9c876e289..4b85e5243 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -826,8 +826,8 @@ extension ParseUser { `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 lead - to collisions of `objectId`'s as the client and server `objectId`'s may be generated using + 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. */ @@ -877,8 +877,8 @@ extension ParseUser { `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 lead - to collisions of `objectId`'s as the client and server `objectId`'s may be generated using + 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. */ @@ -1058,8 +1058,8 @@ public extension Sequence where Element: ParseUser { `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 lead - to collisions of `objectId`'s as the client and server `objectId`'s may be generated using + 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. */ @@ -1158,8 +1158,8 @@ public extension Sequence where Element: ParseUser { `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 lead - to collisions of `objectId`'s as the client and server `objectId`'s may be generated using + 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. */ 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 } From adb3c0f8af14c19335a87fdbd7ad9e95d8c17381 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Tue, 31 Aug 2021 20:26:49 -0400 Subject: [PATCH 04/13] nit --- Sources/ParseSwift/Objects/ParseInstallation.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ParseSwift/Objects/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift index 6845f2e7d..eb4f0ad5f 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation.swift @@ -446,7 +446,7 @@ extension ParseInstallation { `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 + 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. */ @@ -497,7 +497,7 @@ extension ParseInstallation { `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 + 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. */ @@ -701,7 +701,7 @@ public extension Sequence where Element: ParseInstallation { `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 + 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. */ @@ -802,7 +802,7 @@ public extension Sequence where Element: ParseInstallation { `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 + 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. */ From f9be817809485b26b86b27b03cded1e3c53e4386 Mon Sep 17 00:00:00 2001 From: Corey Date: Tue, 31 Aug 2021 21:06:48 -0400 Subject: [PATCH 05/13] Update .codecov.yml --- .codecov.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: From 7652cce41e8e3423c482c6cd35a8ca3e7229e341 Mon Sep 17 00:00:00 2001 From: Lukas Smilek Date: Mon, 6 Sep 2021 12:18:21 +0200 Subject: [PATCH 06/13] Added "createWithCustomObjectId" parameter to save functions and `isSaved` checks Added test cases --- CHANGELOG.md | 1 + Sources/ParseSwift/API/API+Commands.swift | 6 +- .../Objects/ParseInstallation+combine.swift | 32 + .../Objects/ParseInstallation.swift | 29 +- .../Objects/ParseObject+combine.swift | 11 + Sources/ParseSwift/Objects/ParseObject.swift | 26 +- .../Objects/ParseUser+combine.swift | 9 +- Sources/ParseSwift/Objects/ParseUser.swift | 30 +- .../ParseObjectCustomObjectIdTests.swift | 568 ++++++++++++++++++ 9 files changed, 684 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e94169f83..7199d06a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +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). +- Added createWithCustomObjectId parameter in .save() functions to remove the need of setting isIgnoreCustomObjectIdConfig = true and allowCustomObjectId = true for a mixed custom objectId environment ### 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 565621ff1..f8062803b 100644 --- a/Sources/ParseSwift/API/API+Commands.swift +++ b/Sources/ParseSwift/API/API+Commands.swift @@ -345,11 +345,11 @@ internal extension API.Command { // MARK: Saving ParseObjects static func save(_ object: T, - isIgnoreCustomObjectIdConfig: Bool) throws -> API.Command where T: ParseObject { - if ParseSwift.configuration.allowCustomObjectId && object.objectId == nil && !isIgnoreCustomObjectIdConfig { + isIgnoreCustomObjectIdConfig: Bool, createWithCustomObjectId: Bool) throws -> API.Command where T: ParseObject { + if (ParseSwift.configuration.allowCustomObjectId && object.objectId == nil && !isIgnoreCustomObjectIdConfig) || (object.objectId == nil && createWithCustomObjectId) { throw ParseError(code: .missingObjectId, message: "objectId must not be nil") } - if object.isSaved { + if object.isSaved && !createWithCustomObjectId { return update(object) } return create(object) diff --git a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift index 3836ba4f4..050daf168 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift @@ -38,14 +38,30 @@ public extension ParseInstallation { /** Saves the `ParseInstallation` *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 createWithCustomObjectId: Ignore checking `ParseConfiguration.allowCustomObjectId = true` + and try to create object with given custom `objectId` 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(isIgnoreCustomObjectIdConfig: Bool = false, + createWithCustomObjectId: Bool = false, options: API.Options = []) -> Future { Future { promise in self.save(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, + createWithCustomObjectId: createWithCustomObjectId, options: options, completion: promise) } @@ -95,21 +111,37 @@ 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 createWithCustomObjectId: Ignore checking `ParseConfiguration.allowCustomObjectId = true` + and try to create object with given custom `objectId` 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, + createWithCustomObjectId: Bool = false, options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in self.saveAll(batchLimit: limit, transaction: transaction, isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, + createWithCustomObjectId: createWithCustomObjectId, options: options, completion: promise) } diff --git a/Sources/ParseSwift/Objects/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift index eb4f0ad5f..84a0474b2 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation.swift @@ -427,6 +427,7 @@ extension ParseInstallation { */ public func save(options: API.Options = []) throws -> Self { try save(isIgnoreCustomObjectIdConfig: false, + createWithCustomObjectId: false, options: options) } @@ -436,6 +437,8 @@ extension ParseInstallation { - parameter isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed `objectId` environments. Defaults to false. + - parameter createWithCustomObjectId: Ignore checking `ParseConfiguration.allowCustomObjectId = true` + and try to create object with given custom `objectId` 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`. @@ -450,7 +453,8 @@ extension ParseInstallation { 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, + public func save(isIgnoreCustomObjectIdConfig: Bool = false, + createWithCustomObjectId: Bool = false, options: API.Options = []) throws -> Self { var options = options options.insert(.cachePolicy(.reloadIgnoringLocalCacheData)) @@ -471,7 +475,7 @@ extension ParseInstallation { throw error } - let result: Self = try saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) + let result: Self = try saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, createWithCustomObjectId: createWithCustomObjectId) .execute(options: options, callbackQueue: .main, childObjects: childObjects, @@ -486,6 +490,8 @@ extension ParseInstallation { - parameter isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed `objectId` environments. Defaults to false. + - parameter createWithCustomObjectId: Ignore checking `ParseConfiguration.allowCustomObjectId = true` + and try to create object with given custom `objectId` 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. @@ -503,6 +509,7 @@ extension ParseInstallation { */ public func save( isIgnoreCustomObjectIdConfig: Bool = false, + createWithCustomObjectId: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void @@ -512,7 +519,7 @@ extension ParseInstallation { self.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, error) in guard let parseError = error else { do { - try self.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) + try self.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, createWithCustomObjectId: createWithCustomObjectId) .executeAsync(options: options, callbackQueue: callbackQueue, childObjects: savedChildObjects, @@ -554,11 +561,11 @@ extension ParseInstallation { } } - func saveCommand(isIgnoreCustomObjectIdConfig: Bool = false) throws -> API.Command { - if ParseSwift.configuration.allowCustomObjectId && objectId == nil && !isIgnoreCustomObjectIdConfig { + func saveCommand(isIgnoreCustomObjectIdConfig: Bool = false, createWithCustomObjectId: Bool = false) throws -> API.Command { + if (ParseSwift.configuration.allowCustomObjectId && objectId == nil && !isIgnoreCustomObjectIdConfig) || (object.objectId == nil && createWithCustomObjectId) { throw ParseError(code: .missingObjectId, message: "objectId must not be nil") } - if isSaved { + if isSaved && !createWithCustomObjectId{ return updateCommand() } return createCommand() @@ -685,6 +692,8 @@ public extension Sequence where Element: ParseInstallation { - parameter isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed `objectId` environments. Defaults to false. + - parameter createWithCustomObjectId: Ignore checking `ParseConfiguration.allowCustomObjectId = true` + and try to create object with given custom `objectId` 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. @@ -708,6 +717,7 @@ public extension Sequence where Element: ParseInstallation { func saveAll(batchLimit limit: Int? = nil, // swiftlint:disable:this function_body_length transaction: Bool = false, isIgnoreCustomObjectIdConfig: Bool = false, + createWithCustomObjectId: Bool = false, options: API.Options = []) throws -> [(Result)] { var options = options options.insert(.cachePolicy(.reloadIgnoringLocalCacheData)) @@ -756,7 +766,7 @@ public extension Sequence where Element: ParseInstallation { var returnBatch = [(Result)]() let commands = try map { - try $0.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) + try $0.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, createWithCustomObjectId: createWithCustomObjectId) } let batchLimit: Int! if transaction { @@ -788,6 +798,8 @@ public extension Sequence where Element: ParseInstallation { - parameter isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed `objectId` environments. Defaults to false. + - parameter createWithCustomObjectId: Ignore checking `ParseConfiguration.allowCustomObjectId = true` + and try to create object with given custom `objectId` 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. @@ -810,6 +822,7 @@ public extension Sequence where Element: ParseInstallation { batchLimit limit: Int? = nil, transaction: Bool = false, isIgnoreCustomObjectIdConfig: Bool = false, + createWithCustomObjectId: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void @@ -873,7 +886,7 @@ public extension Sequence where Element: ParseInstallation { do { var returnBatch = [(Result)]() let commands = try map { - try $0.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) + try $0.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, createWithCustomObjectId: createWithCustomObjectId) } let batchLimit: Int! if transaction { diff --git a/Sources/ParseSwift/Objects/ParseObject+combine.swift b/Sources/ParseSwift/Objects/ParseObject+combine.swift index c64a59f10..88b10bc4b 100644 --- a/Sources/ParseSwift/Objects/ParseObject+combine.swift +++ b/Sources/ParseSwift/Objects/ParseObject+combine.swift @@ -35,14 +35,21 @@ public extension ParseObject { /** Saves the `ParseObject` *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 createWithCustomObjectId: Ignore checking `ParseConfiguration.allowCustomObjectId = true` + and try to create object with given custom `objectId` 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. */ func savePublisher(isIgnoreCustomObjectIdConfig: Bool = false, + createWithCustomObjectId: Bool = false, options: API.Options = []) -> Future { Future { promise in self.save(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, + createWithCustomObjectId: createWithCustomObjectId, options: options, completion: promise) } @@ -94,6 +101,8 @@ public extension Sequence where Element: ParseObject { - parameter isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed `objectId` environments. Defaults to false. + - parameter createWithCustomObjectId: Ignore checking `ParseConfiguration.allowCustomObjectId = true` + and try to create object with given custom `objectId` 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. @@ -113,11 +122,13 @@ public extension Sequence where Element: ParseObject { func saveAllPublisher(batchLimit limit: Int? = nil, transaction: Bool = false, isIgnoreCustomObjectIdConfig: Bool = false, + createWithCustomObjectId: Bool = false, options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in self.saveAll(batchLimit: limit, transaction: transaction, isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, + createWithCustomObjectId: createWithCustomObjectId, options: options, completion: promise) } diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index ae1bb64ff..ea71183a0 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -70,6 +70,8 @@ public extension Sequence where Element: ParseObject { - parameter isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed `objectId` environments. Defaults to false. + - parameter createWithCustomObjectId: Ignore checking `ParseConfiguration.allowCustomObjectId = true` + and try to create object with given custom `objectId` 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. @@ -90,6 +92,7 @@ public extension Sequence where Element: ParseObject { func saveAll(batchLimit limit: Int? = nil, // swiftlint:disable:this function_body_length transaction: Bool = false, isIgnoreCustomObjectIdConfig: Bool = false, + createWithCustomObjectId: Bool = false, options: API.Options = []) throws -> [(Result)] { var options = options options.insert(.cachePolicy(.reloadIgnoringLocalCacheData)) @@ -137,7 +140,7 @@ public extension Sequence where Element: ParseObject { } var returnBatch = [(Result)]() - let commands = try map { try $0.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) } + let commands = try map { try $0.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, createWithCustomObjectId: createWithCustomObjectId) } let batchLimit: Int! if transaction { batchLimit = commands.count @@ -167,6 +170,8 @@ public extension Sequence where Element: ParseObject { - parameter isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed `objectId` environments. Defaults to false. + - parameter createWithCustomObjectId: Ignore checking `ParseConfiguration.allowCustomObjectId = true` + and try to create object with given custom `objectId` 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. @@ -188,6 +193,7 @@ public extension Sequence where Element: ParseObject { batchLimit limit: Int? = nil, transaction: Bool = false, isIgnoreCustomObjectIdConfig: Bool = false, + createWithCustomObjectId: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void @@ -251,7 +257,7 @@ public extension Sequence where Element: ParseObject { do { var returnBatch = [(Result)]() let commands = try map { - try $0.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) + try $0.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, createWithCustomObjectId: createWithCustomObjectId) } let batchLimit: Int! if transaction { @@ -608,7 +614,7 @@ extension ParseObject { - returns: Returns saved `ParseObject`. */ public func save(options: API.Options = []) throws -> Self { - try save(isIgnoreCustomObjectIdConfig: false, options: options) + try save(isIgnoreCustomObjectIdConfig: false, createWithCustomObjectId: false, options: options) } /** @@ -616,6 +622,8 @@ extension ParseObject { - parameter isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed `objectId` environments. Defaults to false. + - parameter createWithCustomObjectId: Ignore checking `ParseConfiguration.allowCustomObjectId = true` + and try to create object with given custom `objectId` 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`. @@ -631,6 +639,7 @@ extension ParseObject { client-side checks are disabled. Developers are responsible for handling such cases. */ public func save(isIgnoreCustomObjectIdConfig: Bool = false, + createWithCustomObjectId: Bool = false, options: API.Options = []) throws -> Self { var childObjects: [String: PointerType]? var childFiles: [UUID: ParseFile]? @@ -651,7 +660,7 @@ extension ParseObject { throw error } - return try saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) + return try saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, createWithCustomObjectId: createWithCustomObjectId) .execute(options: options, callbackQueue: .main, childObjects: childObjects, @@ -664,6 +673,8 @@ extension ParseObject { - parameter isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed `objectId` environments. Defaults to false. + - parameter createWithCustomObjectId: Ignore checking `ParseConfiguration.allowCustomObjectId = true` + and try to create object with given custom `objectId` 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. @@ -680,6 +691,7 @@ extension ParseObject { */ public func save( isIgnoreCustomObjectIdConfig: Bool = false, + createWithCustomObjectId: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void @@ -687,7 +699,7 @@ extension ParseObject { self.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, error) in guard let parseError = error else { do { - try self.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) + try self.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, createWithCustomObjectId: createWithCustomObjectId) .executeAsync(options: options, callbackQueue: callbackQueue, childObjects: savedChildObjects, @@ -713,8 +725,8 @@ extension ParseObject { } } - internal func saveCommand(isIgnoreCustomObjectIdConfig: Bool = false) throws -> API.Command { - try API.Command.save(self, isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) + internal func saveCommand(isIgnoreCustomObjectIdConfig: Bool = false, createWithCustomObjectId: Bool = false) throws -> API.Command { + try API.Command.save(self, isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, createWithCustomObjectId: createWithCustomObjectId) } // swiftlint:disable:next function_body_length diff --git a/Sources/ParseSwift/Objects/ParseUser+combine.swift b/Sources/ParseSwift/Objects/ParseUser+combine.swift index b660f8461..72259855e 100644 --- a/Sources/ParseSwift/Objects/ParseUser+combine.swift +++ b/Sources/ParseSwift/Objects/ParseUser+combine.swift @@ -163,6 +163,8 @@ public extension ParseUser { - parameter isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed `objectId` environments. Defaults to false. + - parameter createWithCustomObjectId: Ignore checking `ParseConfiguration.allowCustomObjectId = true` + and try to create object with given custom `objectId` 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. @@ -177,9 +179,10 @@ public extension ParseUser { client-side checks are disabled. Developers are responsible for handling such cases. */ func savePublisher(options: API.Options = [], - isIgnoreCustomObjectIdConfig: Bool = false) -> Future { + isIgnoreCustomObjectIdConfig: Bool = false, createWithCustomObjectId: Bool = false) -> Future { Future { promise in self.save(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, + createWithCustomObjectId: createWithCustomObjectId, options: options, completion: promise) } @@ -232,6 +235,8 @@ public extension Sequence where Element: ParseUser { - parameter isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed `objectId` environments. Defaults to false. + - parameter createWithCustomObjectId: Ignore checking `ParseConfiguration.allowCustomObjectId = true` + and try to create object with given custom `objectId` 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. @@ -251,11 +256,13 @@ public extension Sequence where Element: ParseUser { func saveAllPublisher(batchLimit limit: Int? = nil, transaction: Bool = false, isIgnoreCustomObjectIdConfig: Bool = false, + createWithCustomObjectId: Bool = false, options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in self.saveAll(batchLimit: limit, transaction: transaction, isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, + createWithCustomObjectId: createWithCustomObjectId, options: options, completion: promise) } diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index 4b85e5243..5f33ed845 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -808,7 +808,7 @@ 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) + try save(isIgnoreCustomObjectIdConfig: false, createWithCustomObjectId: false, options: options) } /** @@ -817,6 +817,8 @@ extension ParseUser { - parameter isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed `objectId` environments. Defaults to false. + - parameter createWithCustomObjectId: Ignore checking `ParseConfiguration.allowCustomObjectId = true` + and try to create object with given custom `objectId` 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`. @@ -831,7 +833,8 @@ extension ParseUser { 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, + public func save(isIgnoreCustomObjectIdConfig: Bool = false, + createWithCustomObjectId: Bool = false, options: API.Options = []) throws -> Self { var childObjects: [String: PointerType]? var childFiles: [UUID: ParseFile]? @@ -852,7 +855,7 @@ extension ParseUser { throw error } - let result: Self = try saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) + let result: Self = try saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, createWithCustomObjectId: createWithCustomObjectId) .execute(options: options, callbackQueue: .main, childObjects: childObjects, @@ -867,6 +870,8 @@ extension ParseUser { - parameter isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed `objectId` environments. Defaults to false. + - parameter createWithCustomObjectId: Ignore checking `ParseConfiguration.allowCustomObjectId = true` + and try to create object with given custom `objectId` 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. @@ -884,6 +889,7 @@ extension ParseUser { */ public func save( isIgnoreCustomObjectIdConfig: Bool = false, + createWithCustomObjectId: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void @@ -893,7 +899,7 @@ extension ParseUser { self.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, error) in guard let parseError = error else { do { - try self.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) + try self.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, createWithCustomObjectId: createWithCustomObjectId) .executeAsync(options: options, callbackQueue: callbackQueue, childObjects: savedChildObjects, @@ -922,11 +928,11 @@ extension ParseUser { } } - func saveCommand(isIgnoreCustomObjectIdConfig: Bool = false) throws -> API.Command { - if ParseSwift.configuration.allowCustomObjectId && objectId == nil && !isIgnoreCustomObjectIdConfig { + func saveCommand(isIgnoreCustomObjectIdConfig: Bool = false, createWithCustomObjectId: Bool = false) throws -> API.Command { + if (ParseSwift.configuration.allowCustomObjectId && objectId == nil && !isIgnoreCustomObjectIdConfig) || (object.objectId == nil && createWithCustomObjectId) { throw ParseError(code: .missingObjectId, message: "objectId must not be nil") } - if isSaved { + if isSaved && !createWithCustomObjectId { return updateCommand() } return createCommand() @@ -1045,6 +1051,8 @@ public extension Sequence where Element: ParseUser { - parameter isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed `objectId` environments. Defaults to false. + - parameter createWithCustomObjectId: Ignore checking `ParseConfiguration.allowCustomObjectId = true` + and try to create object with given custom `objectId` 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. @@ -1066,6 +1074,7 @@ public extension Sequence where Element: ParseUser { func saveAll(batchLimit limit: Int? = nil, // swiftlint:disable:this function_body_length transaction: Bool = false, isIgnoreCustomObjectIdConfig: Bool = false, + createWithCustomObjectId: Bool = false, options: API.Options = []) throws -> [(Result)] { var childObjects = [String: PointerType]() var childFiles = [UUID: ParseFile]() @@ -1113,7 +1122,7 @@ public extension Sequence where Element: ParseUser { var returnBatch = [(Result)]() let commands = try map { - try $0.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) + try $0.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, createWithCustomObjectId: createWithCustomObjectId) } let batchLimit: Int! if transaction { @@ -1145,6 +1154,8 @@ public extension Sequence where Element: ParseUser { - parameter isIgnoreCustomObjectIdConfig: Ignore checking for `objectId` when `ParseConfiguration.allowCustomObjectId = true` to allow for mixed `objectId` environments. Defaults to false. + - parameter createWithCustomObjectId: Ignore checking `ParseConfiguration.allowCustomObjectId = true` + and try to create object with given custom `objectId` 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. @@ -1167,6 +1178,7 @@ public extension Sequence where Element: ParseUser { batchLimit limit: Int? = nil, transaction: Bool = false, isIgnoreCustomObjectIdConfig: Bool = false, + createWithCustomObjectId: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void @@ -1229,7 +1241,7 @@ public extension Sequence where Element: ParseUser { do { var returnBatch = [(Result)]() let commands = try map { - try $0.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) + try $0.saveCommand(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, createWithCustomObjectId: createWithCustomObjectId) } let batchLimit: Int! if transaction { diff --git a/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift b/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift index c3c498613..2a14108b0 100644 --- a/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift @@ -436,12 +436,34 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ let score = GameScore(score: 10) _ = try score.saveCommand(isIgnoreCustomObjectIdConfig: true) } + + func testSaveCommandNoObjectIdCreateWithCustomId() throws { + let score = GameScore(score: 10) + _ = try score.saveCommand(createWithCustomObjectId: true) + } + + func testSaveCommandNoObjectIdIgnoreConfigCreateWithCustomId() throws { + let score = GameScore(score: 10) + _ = try score.saveCommand(isIgnoreCustomObjectIdConfig: true, createWithCustomObjectId: true) + } func testUpdateCommandNoObjectId() throws { var score = GameScore(score: 10) score.createdAt = Date() XCTAssertThrowsError(try score.saveCommand()) } + + func testUpdateCommandNoObjectIdCreateWithCustomId() throws { + var score = GameScore(score: 10) + score.createdAt = Date() + _ = try score.saveCommand(createWithCustomObjectId: true) + } + + func testUpdateCommandNoObjectIdIgnoreConfigCreateWithCustomId() throws { + var score = GameScore(score: 10) + score.createdAt = Date() + _ = try score.saveCommand(isIgnoreCustomObjectIdConfig: true, createWithCustomObjectId: true) + } func testUpdateCommandNoObjectIdIgnoreConfig() throws { var score = GameScore(score: 10) @@ -474,6 +496,16 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ let user = User() _ = try user.saveCommand(isIgnoreCustomObjectIdConfig: true) } + + func testUserSaveCommandNoObjectIdCreateWithCustomId() throws { + let user = User() + _ = try user.saveCommand(createWithCustomObjectId: true) + } + + func testUserSaveCommandNoObjectIdIgnoreConfigCreateWithCustomId() throws { + let user = User() + _ = try user.saveCommand(isIgnoreCustomObjectIdConfig: true, createWithCustomObjectId: true) + } func testUserUpdateCommandNoObjectId() throws { var user = User() @@ -486,6 +518,18 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ user.createdAt = Date() _ = try user.saveCommand(isIgnoreCustomObjectIdConfig: true) } + + func testUserUpdateCommandNoObjectIdCreateWithCustomId() throws { + var user = User() + user.createdAt = Date() + _ = try user.saveCommand(createWithCustomObjectId: true) + } + + func testUserUpdateCommandNoObjectIdIgnoreConfigCreateWithCustomId() throws { + var user = User() + user.createdAt = Date() + _ = try user.saveCommand(isIgnoreCustomObjectIdConfig: true, createWithCustomObjectId: true) + } func testUserSaveAllNoObjectIdCommand() throws { let user = User() @@ -512,6 +556,16 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ let installation = Installation() _ = try installation.saveCommand(isIgnoreCustomObjectIdConfig: true) } + + func testInstallationSaveCommandNoObjectIdCreateWithCustomId() throws { + let installation = Installation() + _ = try installation.saveCommand(createWithCustomObjectId: true) + } + + func testInstallationSaveCommandNoObjectIdIgnoreConfigCreateWithCustomId() throws { + let installation = Installation() + _ = try installation.saveCommand(isIgnoreCustomObjectIdConfig: true, createWithCustomObjectId: true) + } func testInstallationUpdateCommandNoObjectId() throws { var installation = Installation() @@ -524,6 +578,18 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ installation.createdAt = Date() _ = try installation.saveCommand(isIgnoreCustomObjectIdConfig: true) } + + func testInstallationUpdateCommandNoObjectIdCreateWithCustomId() throws { + var installation = Installation() + installation.createdAt = Date() + _ = try installation.saveCommand(createWithCustomObjectId: true) + } + + func testInstallationUpdateCommandNoObjectIdIgnoreConfigCreateWithCustomId() throws { + var installation = Installation() + installation.createdAt = Date() + _ = try installation.saveCommand(isIgnoreCustomObjectIdConfig: true, createWithCustomObjectId: true) + } func testInstallationSaveAllNoObjectIdCommand() throws { let installation = Installation() @@ -603,6 +669,66 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ XCTFail(error.localizedDescription) } } + + func testSaveNoObjectIdCreateWithCustomId() { // 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(createWithCustomObjectId: true) + //objectId should be generated by server and new object created + XCTAssert(!saved.hasSameObjectId(as: scoreOnServer)) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testSaveNoObjectIdIgnoreConfigCreateWithCustomId() { // 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, createWithCustomObjectId: true) + //objectId should be generated by server and new object created + XCTAssert(!saved.hasSameObjectId(as: scoreOnServer)) + } catch { + XCTFail(error.localizedDescription) + } + } func testUpdate() { var score = GameScore(score: 10) @@ -671,6 +797,70 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ XCTFail(error.localizedDescription) } } + + func testUpdateNoObjectIdCreateWithCustomId() { + 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(createWithCustomObjectId: true) + //objectId should be generated by server and new object created + XCTAssertTrue(!saved.hasSameObjectId(as: scoreOnServer)) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testUpdateNoObjectIdIgnoreConfigCreateWithCustomId() { + 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, createWithCustomObjectId: true) + //objectId should be generated by server and new object created + XCTAssertTrue(!saved.hasSameObjectId(as: scoreOnServer)) + } catch { + XCTFail(error.localizedDescription) + } + } // swiftlint:disable:next function_body_length func saveAsync(score: GameScore, @@ -777,6 +967,61 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ callbackQueue: .main, isIgnoreCustomObjectIdConfig: true) } + + func testSaveNoObjectIdCreateWithCustomIdAsyncMainQueue() { + 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, + createWithCustomObjectId: true) + } + + func testSaveNoObjectIdIgnoreConfigCreateWithCustomIdAsyncMainQueue() { + 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, + createWithCustomObjectId: true) + } func updateAsync(score: GameScore, scoreOnServer: GameScore, @@ -883,6 +1128,61 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ isIgnoreCustomObjectIdConfig: true, callbackQueue: .main) } + + func testUpdateNoObjectIdCreateWithCustomIdAsyncMainQueue() { + 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, + createWithCustomObjectId: true, + callbackQueue: .main) + } + + func testUpdateNoObjectIdIgnoreConfigCreateWithCustomIdAsyncMainQueue() { + 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, + createWithCustomObjectId: true, + callbackQueue: .main) + } func testSaveAll() { // swiftlint:disable:this function_body_length cyclomatic_complexity var score = GameScore(score: 10) @@ -944,6 +1244,67 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ XCTFail(error.localizedDescription) } } + + func testSaveAllCreateWithCustomId() { // swiftlint:disable:this function_body_length cyclomatic_complexity + var score = GameScore(score: 10) + score.objectId = "yarr" + var score2 = GameScore(score: 20) + score2.objectId = "yolo" + + var scoreOnServer = score + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt + scoreOnServer.ACL = nil + + var scoreOnServer2 = score2 + 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(createWithCustomObjectId: true) + + XCTAssertEqual(saved.count, 0) + switch saved[0] { + + case .success(let first): + XCTFail("Duplicate error should be thrown") + case .failure(let error): + XCTAssert(error.localizedDescription) + } + + switch saved[1] { + + case .success(let second): + XCTFail("Duplicate error should be thrown") + case .failure(let error): + XCTAssert(error.localizedDescription) + } + + } catch { + XCTFail(error.localizedDescription) + } + } func testSaveAllNoObjectId() throws { let score = GameScore(score: 10) @@ -1011,6 +1372,36 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ XCTFail(error.localizedDescription) } } + + func testSaveAllNoObjectIdCreateWithCustomId() { // swiftlint:disable:this function_body_length cyclomatic_complexity + let score = GameScore(score: 10) + let score2 = GameScore(score: 20) + + do { + + let saved = try [score, score2].saveAll(createWithCustomObjectId: true) + + XCTAssertEqual(saved.count, 0) + switch saved[0] { + + case .success(let first): + XCTFail("ObjectId should not be nil before save") + case .failure(let error): + XCTAssert(error.localizedDescription) + } + + switch saved[1] { + + case .success(let second): + XCTFail("ObjectId should not be nil before save") + case .failure(let error): + XCTAssert(error.localizedDescription) + } + + } catch { + XCTFail(error.localizedDescription) + } + } func testSaveAllNoObjectIdAsync() throws { let score = GameScore(score: 10) @@ -1216,6 +1607,12 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ user.createdAt = Date() XCTAssertThrowsError(try user.save()) } + + func testUserUpdateNoObjectIdCreateWithCustomId() throws { + var user = User() + user.createdAt = Date() + XCTAssertThrowsError(try user.save(createWithCustomObjectId: true)) + } func testUserUpdateNoObjectIdIgnoreConfig() { var user = User() @@ -1335,6 +1732,32 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ isIgnoreCustomObjectIdConfig: true, callbackQueue: .main) } + + func testUserSaveNoObjectIdCreateWithCustomIdAsyncMainQueue() { + 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) + } + XCTAssertThrowsError(self.saveUserAsync(user: user, + userOnServer: userOnServer, + createWithCustomObjectId: true, + callbackQueue: .main)) + } func updateUserAsync(user: User, userOnServer: User, isIgnoreCustomObjectIdConfig: Bool = false, @@ -1611,6 +2034,39 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ XCTFail(error.localizedDescription) } } + + // swiftlint:disable:next function_body_length cyclomatic_complexity + func testUserUpdateAllNoObjectIdCreateWithCustomId() { + var user = User() + user.createdAt = Date() + var user2 = User() + user2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + + do { + + let saved = try [user, user2].saveAll(createWithCustomObjectId: true) + + XCTAssertEqual(saved.count, 2) + switch saved[0] { + + case .success(let first): + XCTFail("ObjectId should not be nil before save") + case .failure(let error): + XCTAssert(error.localizedDescription) + } + + switch saved[1] { + + case .success(let second): + XCTFail("ObjectId should not be nil before save") + case .failure(let error): + XCTAssert(error.localizedDescription) + } + + } catch { + XCTFail(error.localizedDescription) + } + } func testUserUpdateAllNoObjectIdAsync() throws { var user = User() @@ -1664,6 +2120,11 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ let score = GameScore(score: 10) XCTAssertThrowsError(try score.save()) } + + func testInstallationSaveNoObjectIdCreateWithCustomId() throws { + let score = GameScore(score: 10) + XCTAssertThrowsError(try score.save(createWithCustomObjectId: true)) + } func testInstallationSaveNoObjectIdIgnoreConfig() { // swiftlint:disable:this function_body_length var installation = Installation() @@ -1762,6 +2223,19 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ XCTFail(error.localizedDescription) } } + + func testInstallationUpdateNoObjectIdCreateWithCustomId() { + 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 + + do { + XCTAssertThrowsError(try installation.save(createWithCustomObjectId: true)) + } catch { + XCTFail(error.localizedDescription) + } + } // swiftlint:disable:next function_body_length func saveInstallationAsync(installation: Installation, @@ -2037,6 +2511,39 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ XCTFail(error.localizedDescription) } } + + func testInstallationSaveAllCreateWithCustomId() { // swiftlint:disable:this function_body_length cyclomatic_complexity + var installation = Installation() + installation.objectId = "yarr" + + var installation2 = Installation() + installation2.objectId = "yolo" + + do { + + let saved = try [installation, installation2].saveAll(createWithCustomObjectId: true) + + XCTAssertEqual(saved.count, 2) + switch saved[0] { + + case .success(let first): + XCTAssert(first.hasSameObjectId(as: installation)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + switch saved[1] { + + case .success(let second): + XCTAssert(second.hasSameObjectId(as: installation2)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + } catch { + XCTFail(error.localizedDescription) + } + } func testInstallationSaveAllNoObjectId() throws { let installation = Installation() @@ -2195,6 +2702,67 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ XCTFail(error.localizedDescription) } } + + func testInstallationUpdateAllCreateWithCustomId() { // swiftlint:disable:this function_body_length cyclomatic_complexity + var installation = Installation() + installation.objectId = "yarr" + installation.createdAt = Date() + var installation2 = Installation() + installation2.objectId = "yolo" + installation2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + + var installationOnServer = installation + installationOnServer.updatedAt = installationOnServer.createdAt + installationOnServer.ACL = nil + + var installationOnServer2 = installation2 + 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(createWithCustomObjectId: true) + + XCTAssertEqual(saved.count, 0) + switch saved[0] { + + case .success(let first): + XCTFail("Duplicate error should be thrown") + case .failure(let error): + XCTAssert(error.localizedDescription) + } + + switch saved[1] { + + case .success(let second): + XCTFail("Duplicate error should be thrown") + case .failure(let error): + XCTAssert(error.localizedDescription) + } + + } catch { + XCTFail(error.localizedDescription) + } + } func testInstallationUpdateAllNoObjectId() throws { var installation = Installation() From a9ec7a2c5814134d94fca7650c0e727fdb3bdfa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Smilek?= <41491201+lsmilek1@users.noreply.github.com> Date: Mon, 6 Sep 2021 18:48:54 +0200 Subject: [PATCH 07/13] Update CHANGELOG.md correcting **Improvements** block wrongly assigned to already released version 1.9.8 --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 018743e1a..64dd758ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,15 +4,15 @@ [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.9.8...main) * _Contributing to this repo? Add info about your change here to be included in the next release_ +__Improvements__ +- Added createWithCustomObjectId parameter in .save() functions to remove the need of setting isIgnoreCustomObjectIdConfig = true and allowCustomObjectId = true for a mixed custom objectId environment. ([#225](https://github.com/parse-community/Parse-Swift/pull/225)), thanks to [Lukas Smilek](https://github.com/lsmilek1) + ### 1.9.8 [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.9.7...1.9.8) __Fixes__ - Use a seperate Keychain for each app bundleId. This only effects macOS apps as their Keychain is handled by the OS differently. For macOS app developers only, the user who logged in last to your app will have their Keychain upgraded to the patched version. Other users/apps will either need to login again or logout then login again ([#224](https://github.com/parse-community/Parse-Swift/pull/224)), thanks to [Corey Baker](https://github.com/cbaker6). -__Improvements__ -- Added createWithCustomObjectId parameter in .save() functions to remove the need of setting isIgnoreCustomObjectIdConfig = true and allowCustomObjectId = true for a mixed custom objectId environment. ([#225](https://github.com/parse-community/Parse-Swift/pull/225)), thanks to [Lukas Smilek](https://github.com/lsmilek1) - ### 1.9.7 [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.9.6...1.9.7) From 9be8930f25477d1f504ac3d74db276d0f286bd67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Smilek?= <41491201+lsmilek1@users.noreply.github.com> Date: Mon, 6 Sep 2021 18:53:54 +0200 Subject: [PATCH 08/13] Update ParseInstallation.swift Correcting wrong objectId reference --- Sources/ParseSwift/Objects/ParseInstallation.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ParseSwift/Objects/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift index 84a0474b2..65e6f16f3 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation.swift @@ -562,7 +562,7 @@ extension ParseInstallation { } func saveCommand(isIgnoreCustomObjectIdConfig: Bool = false, createWithCustomObjectId: Bool = false) throws -> API.Command { - if (ParseSwift.configuration.allowCustomObjectId && objectId == nil && !isIgnoreCustomObjectIdConfig) || (object.objectId == nil && createWithCustomObjectId) { + if (ParseSwift.configuration.allowCustomObjectId && objectId == nil && !isIgnoreCustomObjectIdConfig) || (objectId == nil && createWithCustomObjectId) { throw ParseError(code: .missingObjectId, message: "objectId must not be nil") } if isSaved && !createWithCustomObjectId{ From ccd5dea8665edb6bd066956ba8803dac64d4f7e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Smilek?= <41491201+lsmilek1@users.noreply.github.com> Date: Mon, 6 Sep 2021 18:54:47 +0200 Subject: [PATCH 09/13] Update ParseUser.swift Correcting wrong objectId reference --- Sources/ParseSwift/Objects/ParseUser.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index 5f33ed845..cebbb5f60 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -929,7 +929,7 @@ extension ParseUser { } func saveCommand(isIgnoreCustomObjectIdConfig: Bool = false, createWithCustomObjectId: Bool = false) throws -> API.Command { - if (ParseSwift.configuration.allowCustomObjectId && objectId == nil && !isIgnoreCustomObjectIdConfig) || (object.objectId == nil && createWithCustomObjectId) { + if (ParseSwift.configuration.allowCustomObjectId && objectId == nil && !isIgnoreCustomObjectIdConfig) || (objectId == nil && createWithCustomObjectId) { throw ParseError(code: .missingObjectId, message: "objectId must not be nil") } if isSaved && !createWithCustomObjectId { From 3c48fe3e7933d962cae2d3741532a18ffecf6522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Smilek?= <41491201+lsmilek1@users.noreply.github.com> Date: Mon, 6 Sep 2021 19:12:35 +0200 Subject: [PATCH 10/13] Update ParseObjectCustomObjectIdTests.swift Correcting Test cases --- .../ParseObjectCustomObjectIdTests.swift | 152 ++---------------- 1 file changed, 9 insertions(+), 143 deletions(-) diff --git a/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift b/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift index 475bec3ec..0494f9883 100644 --- a/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift @@ -471,12 +471,6 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ _ = try score.saveCommand(isIgnoreCustomObjectIdConfig: true) } - 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) @@ -1296,7 +1290,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ case .success(let first): XCTFail("Duplicate error should be thrown") case .failure(let error): - XCTAssert(error.localizedDescription) + XCTAssert(error == ParseError(code: .missingObjectId, message: "objectId must not be nil")) } switch saved[1] { @@ -1304,7 +1298,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ case .success(let second): XCTFail("Duplicate error should be thrown") case .failure(let error): - XCTAssert(error.localizedDescription) + XCTAssert(error == ParseError(code: .missingObjectId, message: "objectId must not be nil")) } } catch { @@ -1393,7 +1387,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ case .success(let first): XCTFail("ObjectId should not be nil before save") case .failure(let error): - XCTAssert(error.localizedDescription) + XCTAssert(error == ParseError(code: .missingObjectId, message: "objectId must not be nil")) } switch saved[1] { @@ -1401,74 +1395,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ case .success(let second): XCTFail("ObjectId should not be nil before save") case .failure(let error): - XCTAssert(error.localizedDescription) - } - - } catch { - XCTFail(error.localizedDescription) - } - } - - func testSaveAllNoObjectId() throws { - let score = GameScore(score: 10) - let score2 = GameScore(score: 20) - 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) + XCTAssert(error == ParseError(code: .missingObjectId, message: "objectId must not be nil")) } } catch { @@ -1718,37 +1645,6 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ } } - 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, isIgnoreCustomObjectIdConfig: Bool = false, @@ -2156,7 +2052,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ case .success(let first): XCTFail("ObjectId should not be nil before save") case .failure(let error): - XCTAssert(error.localizedDescription) + XCTAssert(error == ParseError(code: .missingObjectId, message: "objectId must not be nil")) } switch saved[1] { @@ -2164,7 +2060,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ case .success(let second): XCTFail("ObjectId should not be nil before save") case .failure(let error): - XCTAssert(error.localizedDescription) + XCTAssert(error == ParseError(code: .missingObjectId, message: "objectId must not be nil")) } } catch { @@ -2229,37 +2125,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ let score = GameScore(score: 10) XCTAssertThrowsError(try score.save(createWithCustomObjectId: true)) } - - 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 testInstallationSaveNoObjectIdIgnoreConfig() { // swiftlint:disable:this function_body_length var installation = Installation() installation.ACL = nil @@ -2882,7 +2748,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ case .success(let first): XCTFail("Duplicate error should be thrown") case .failure(let error): - XCTAssert(error.localizedDescription) + XCTAssert(error == ParseError(code: .missingObjectId, message: "objectId must not be nil")) } switch saved[1] { @@ -2890,7 +2756,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ case .success(let second): XCTFail("Duplicate error should be thrown") case .failure(let error): - XCTAssert(error.localizedDescription) + XCTAssert(error == ParseError(code: .missingObjectId, message: "objectId must not be nil")) } } catch { From 40795c548dfa096a01d2e73bc9200eb7cdc06606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Smilek?= <41491201+lsmilek1@users.noreply.github.com> Date: Mon, 6 Sep 2021 19:21:25 +0200 Subject: [PATCH 11/13] Update ParseObjectCustomObjectIdTests.swift Correcting failed test cases --- .../ParseObjectCustomObjectIdTests.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift b/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift index 0494f9883..713f98399 100644 --- a/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift @@ -1290,7 +1290,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ case .success(let first): XCTFail("Duplicate error should be thrown") case .failure(let error): - XCTAssert(error == ParseError(code: .missingObjectId, message: "objectId must not be nil")) + XCTAssert(error.localizedDescription.contains("objectId must not be nil")) } switch saved[1] { @@ -1298,7 +1298,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ case .success(let second): XCTFail("Duplicate error should be thrown") case .failure(let error): - XCTAssert(error == ParseError(code: .missingObjectId, message: "objectId must not be nil")) + XCTAssert(error.localizedDescription.contains("objectId must not be nil")) } } catch { @@ -1387,7 +1387,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ case .success(let first): XCTFail("ObjectId should not be nil before save") case .failure(let error): - XCTAssert(error == ParseError(code: .missingObjectId, message: "objectId must not be nil")) + XCTAssert(error.localizedDescription.contains("objectId must not be nil")) } switch saved[1] { @@ -1395,7 +1395,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ case .success(let second): XCTFail("ObjectId should not be nil before save") case .failure(let error): - XCTAssert(error == ParseError(code: .missingObjectId, message: "objectId must not be nil")) + XCTAssert(error.localizedDescription.contains("objectId must not be nil")) } } catch { @@ -2052,7 +2052,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ case .success(let first): XCTFail("ObjectId should not be nil before save") case .failure(let error): - XCTAssert(error == ParseError(code: .missingObjectId, message: "objectId must not be nil")) + XCTAssert(error.localizedDescription.contains("objectId must not be nil")) } switch saved[1] { @@ -2060,7 +2060,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ case .success(let second): XCTFail("ObjectId should not be nil before save") case .failure(let error): - XCTAssert(error == ParseError(code: .missingObjectId, message: "objectId must not be nil")) + XCTAssert(error.localizedDescription.contains("objectId must not be nil")) } } catch { @@ -2748,7 +2748,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ case .success(let first): XCTFail("Duplicate error should be thrown") case .failure(let error): - XCTAssert(error == ParseError(code: .missingObjectId, message: "objectId must not be nil")) + XCTAssert(error.localizedDescription.contains("objectId must not be nil")) } switch saved[1] { @@ -2756,7 +2756,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ case .success(let second): XCTFail("Duplicate error should be thrown") case .failure(let error): - XCTAssert(error == ParseError(code: .missingObjectId, message: "objectId must not be nil")) + XCTAssert(error.localizedDescription.contains("objectId must not be nil")) } } catch { From d830982af9a9414dacacd239dd65c46b3b380551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Smilek?= <41491201+lsmilek1@users.noreply.github.com> Date: Mon, 6 Sep 2021 19:49:47 +0200 Subject: [PATCH 12/13] Update ParseObjectCustomObjectIdTests.swift Correcting failing test cases --- .../ParseSwiftTests/ParseObjectCustomObjectIdTests.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift b/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift index 713f98399..dac84776d 100644 --- a/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift @@ -866,11 +866,13 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ func saveAsync(score: GameScore, scoreOnServer: GameScore, callbackQueue: DispatchQueue, - isIgnoreCustomObjectIdConfig: Bool = false) { + isIgnoreCustomObjectIdConfig: Bool = false + createWithCustomObjectId: Bool = false) { let expectation1 = XCTestExpectation(description: "Save object1") score.save(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, + createWithCustomObjectId: createWithCustomObjectId, options: [], callbackQueue: callbackQueue) { result in @@ -1026,11 +1028,13 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ func updateAsync(score: GameScore, scoreOnServer: GameScore, isIgnoreCustomObjectIdConfig: Bool = false, + createWithCustomObjectId: Bool = false, callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Update object1") score.save(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, + createWithCustomObjectId: createWithCustomObjectId, options: [], callbackQueue: callbackQueue) { result in @@ -1648,11 +1652,13 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ // swiftlint:disable:next function_body_length func saveUserAsync(user: User, userOnServer: User, isIgnoreCustomObjectIdConfig: Bool = false, + createWithCustomObjectId: Bool = false, callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Update object1") user.save(isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig, + createWithCustomObjectId: createWithCustomObjectId, options: [], callbackQueue: callbackQueue) { result in From 70bae8d7b95e717b0867e64f51deefa48ff10c4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Smilek?= <41491201+lsmilek1@users.noreply.github.com> Date: Mon, 6 Sep 2021 19:53:30 +0200 Subject: [PATCH 13/13] Update ParseObjectCustomObjectIdTests.swift --- Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift b/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift index dac84776d..5998a385e 100644 --- a/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift @@ -866,7 +866,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ func saveAsync(score: GameScore, scoreOnServer: GameScore, callbackQueue: DispatchQueue, - isIgnoreCustomObjectIdConfig: Bool = false + isIgnoreCustomObjectIdConfig: Bool = false, createWithCustomObjectId: Bool = false) { let expectation1 = XCTestExpectation(description: "Save object1")