From 96b26a508209bb7789c3b412fd68fbe01bc9d35f Mon Sep 17 00:00:00 2001 From: Corey Date: Fri, 26 Mar 2021 23:13:57 -0400 Subject: [PATCH] Fix allowCustomObjectId (#101) * Doc nits * Fix allowCustomObjectId * increase codecov * Prepare for release and doc nits * nits --- CHANGELOG.md | 7 +- ParseSwift.podspec | 2 +- ParseSwift.xcodeproj/project.pbxproj | 32 +- Scripts/jazzy.sh | 2 +- Sources/ParseSwift/API/API+Commands.swift | 16 +- .../Internal/ParseAnonymous.swift | 8 +- .../ParseSwift/LiveQuery/ParseLiveQuery.swift | 8 +- .../Objects/ParseInstallation.swift | 121 ++++--- Sources/ParseSwift/Objects/ParseObject.swift | 96 +++--- .../Objects/ParseUser+combine.swift | 2 +- Sources/ParseSwift/Objects/ParseUser.swift | 119 ++++--- Sources/ParseSwift/ParseConstants.swift | 2 +- Sources/ParseSwift/Protocols/Objectable.swift | 8 + Sources/ParseSwift/Types/ParseGeoPoint.swift | 2 +- .../ParseInstallationTests.swift | 4 +- .../ParseObjectBatchTests.swift | 4 +- ...t => ParseObjectCustomObjectIdTests.swift} | 322 ++++++++++++++++-- Tests/ParseSwiftTests/ParseObjectTests.swift | 4 +- Tests/ParseSwiftTests/ParseUserTests.swift | 4 +- 19 files changed, 574 insertions(+), 189 deletions(-) rename Tests/ParseSwiftTests/{ParseObjectCustomObjectId.swift => ParseObjectCustomObjectIdTests.swift} (81%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 343d37d2f..ce790b8c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,14 @@ # Parse-Swift Changelog ### main -[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.2.2...main) +[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.2.3...main) * _Contributing to this repo? Add info about your change here to be included in the next release_ +### 1.2.3 +[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.2.2...1.2.3) + +__Fixes__ +- Fixed a bug that prevented custom objectIds from working ([#101](https://github.com/parse-community/Parse-Swift/pull/101)), thanks to [Corey Baker](https://github.com/cbaker6). ### 1.2.2 [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.2.1...1.2.2) diff --git a/ParseSwift.podspec b/ParseSwift.podspec index 455ccb4ef..810d25531 100644 --- a/ParseSwift.podspec +++ b/ParseSwift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "ParseSwift" - s.version = "1.2.2" + s.version = "1.2.3" s.summary = "Parse Pure Swift SDK" s.homepage = "https://github.com/parse-community/Parse-Swift" s.authors = { diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index 96ee5180d..05f4fe674 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -197,9 +197,9 @@ 70647E9D259E3A9A004C1004 /* ParseType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70647E9B259E3A9A004C1004 /* ParseType.swift */; }; 70647E9E259E3A9A004C1004 /* ParseType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70647E9B259E3A9A004C1004 /* ParseType.swift */; }; 70647E9F259E3A9A004C1004 /* ParseType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70647E9B259E3A9A004C1004 /* ParseType.swift */; }; - 70732C5A2606CCAD000CAB81 /* ParseObjectCustomObjectId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70732C592606CCAD000CAB81 /* ParseObjectCustomObjectId.swift */; }; - 70732C5B2606CCAD000CAB81 /* ParseObjectCustomObjectId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70732C592606CCAD000CAB81 /* ParseObjectCustomObjectId.swift */; }; - 70732C5C2606CCAD000CAB81 /* ParseObjectCustomObjectId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70732C592606CCAD000CAB81 /* ParseObjectCustomObjectId.swift */; }; + 70732C5A2606CCAD000CAB81 /* ParseObjectCustomObjectIdTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70732C592606CCAD000CAB81 /* ParseObjectCustomObjectIdTests.swift */; }; + 70732C5B2606CCAD000CAB81 /* ParseObjectCustomObjectIdTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70732C592606CCAD000CAB81 /* ParseObjectCustomObjectIdTests.swift */; }; + 70732C5C2606CCAD000CAB81 /* ParseObjectCustomObjectIdTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70732C592606CCAD000CAB81 /* ParseObjectCustomObjectIdTests.swift */; }; 707A3BF125B0A4F0000D215C /* ParseAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3BF025B0A4F0000D215C /* ParseAuthentication.swift */; }; 707A3BF225B0A4F0000D215C /* ParseAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3BF025B0A4F0000D215C /* ParseAuthentication.swift */; }; 707A3BF325B0A4F0000D215C /* ParseAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3BF025B0A4F0000D215C /* ParseAuthentication.swift */; }; @@ -602,7 +602,7 @@ 705D950725BE4C08003EF6F8 /* SubscriptionCallback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionCallback.swift; sourceTree = ""; }; 70647E8D259E3375004C1004 /* LocallyIdentifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocallyIdentifiable.swift; sourceTree = ""; }; 70647E9B259E3A9A004C1004 /* ParseType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseType.swift; sourceTree = ""; }; - 70732C592606CCAD000CAB81 /* ParseObjectCustomObjectId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseObjectCustomObjectId.swift; sourceTree = ""; }; + 70732C592606CCAD000CAB81 /* ParseObjectCustomObjectIdTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseObjectCustomObjectIdTests.swift; sourceTree = ""; }; 707A3BF025B0A4F0000D215C /* ParseAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAuthentication.swift; sourceTree = ""; }; 707A3C1025B0A8E8000D215C /* ParseAnonymous.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAnonymous.swift; sourceTree = ""; }; 707A3C1F25B14BCF000D215C /* ParseApple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseApple.swift; sourceTree = ""; }; @@ -834,7 +834,7 @@ 7003963A25A288100052CB31 /* ParseLiveQueryTests.swift */, 70C7DC2024D20F190050419B /* ParseObjectBatchTests.swift */, 7044C1DE25C5C70D0011F6E7 /* ParseObjectCombine.swift */, - 70732C592606CCAD000CAB81 /* ParseObjectCustomObjectId.swift */, + 70732C592606CCAD000CAB81 /* ParseObjectCustomObjectIdTests.swift */, 911DB13524C4FC100027F3C7 /* ParseObjectTests.swift */, 7044C1EB25C5CC930011F6E7 /* ParseOperationCombineTests.swift */, 70C5508425B4A68700B5DBC2 /* ParseOperationTests.swift */, @@ -1665,7 +1665,7 @@ 89899D772603CF66002E2043 /* ParseFacebookTests.swift in Sources */, 70386A4625D99C8B0048EC1B /* ParseLDAPTests.swift in Sources */, 911DB12E24C4837E0027F3C7 /* APICommandTests.swift in Sources */, - 70732C5A2606CCAD000CAB81 /* ParseObjectCustomObjectId.swift in Sources */, + 70732C5A2606CCAD000CAB81 /* ParseObjectCustomObjectIdTests.swift in Sources */, 911DB12C24C3F7720027F3C7 /* MockURLResponse.swift in Sources */, 7044C24325C5EA360011F6E7 /* ParseAppleCombineTests.swift in Sources */, 7044C1DF25C5C70D0011F6E7 /* ParseObjectCombine.swift in Sources */, @@ -1816,7 +1816,7 @@ 89899D822603CF67002E2043 /* ParseFacebookTests.swift in Sources */, 70386A4825D99C8B0048EC1B /* ParseLDAPTests.swift in Sources */, 709B984C2556ECAA00507778 /* APICommandTests.swift in Sources */, - 70732C5C2606CCAD000CAB81 /* ParseObjectCustomObjectId.swift in Sources */, + 70732C5C2606CCAD000CAB81 /* ParseObjectCustomObjectIdTests.swift in Sources */, 709B984D2556ECAA00507778 /* AnyDecodableTests.swift in Sources */, 7044C24525C5EA360011F6E7 /* ParseAppleCombineTests.swift in Sources */, 7044C1E125C5C70D0011F6E7 /* ParseObjectCombine.swift in Sources */, @@ -1870,7 +1870,7 @@ 89899D812603CF67002E2043 /* ParseFacebookTests.swift in Sources */, 70386A4725D99C8B0048EC1B /* ParseLDAPTests.swift in Sources */, 70F2E2B5254F283000B2EA5C /* ParseEncoderTests.swift in Sources */, - 70732C5B2606CCAD000CAB81 /* ParseObjectCustomObjectId.swift in Sources */, + 70732C5B2606CCAD000CAB81 /* ParseObjectCustomObjectIdTests.swift in Sources */, 70F2E2C2254F283000B2EA5C /* APICommandTests.swift in Sources */, 7044C24425C5EA360011F6E7 /* ParseAppleCombineTests.swift in Sources */, 7044C1E025C5C70D0011F6E7 /* ParseObjectCombine.swift in Sources */, @@ -2321,7 +2321,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.2.2; + MARKETING_VERSION = 1.2.3; PRODUCT_BUNDLE_IDENTIFIER = com.parse.ParseSwift; PRODUCT_NAME = ParseSwift; SKIP_INSTALL = YES; @@ -2345,7 +2345,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.2.2; + MARKETING_VERSION = 1.2.3; PRODUCT_BUNDLE_IDENTIFIER = com.parse.ParseSwift; PRODUCT_NAME = ParseSwift; SKIP_INSTALL = YES; @@ -2411,7 +2411,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 1.2.2; + MARKETING_VERSION = 1.2.3; PRODUCT_BUNDLE_IDENTIFIER = com.parse.ParseSwift; PRODUCT_NAME = ParseSwift; SDKROOT = macosx; @@ -2437,7 +2437,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 1.2.2; + MARKETING_VERSION = 1.2.3; PRODUCT_BUNDLE_IDENTIFIER = com.parse.ParseSwift; PRODUCT_NAME = ParseSwift; SDKROOT = macosx; @@ -2584,7 +2584,7 @@ INFOPLIST_FILE = "ParseSwift-watchOS/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.2.2; + MARKETING_VERSION = 1.2.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.parse.ParseSwift-watchOS"; @@ -2613,7 +2613,7 @@ INFOPLIST_FILE = "ParseSwift-watchOS/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.2.2; + MARKETING_VERSION = 1.2.3; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.parse.ParseSwift-watchOS"; PRODUCT_NAME = ParseSwift; @@ -2640,7 +2640,7 @@ INFOPLIST_FILE = "ParseSwift-tvOS/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.2.2; + MARKETING_VERSION = 1.2.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.parse.ParseSwift-tvOS"; @@ -2668,7 +2668,7 @@ INFOPLIST_FILE = "ParseSwift-tvOS/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.2.2; + MARKETING_VERSION = 1.2.3; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.parse.ParseSwift-tvOS"; PRODUCT_NAME = ParseSwift; diff --git a/Scripts/jazzy.sh b/Scripts/jazzy.sh index 3427498ca..0de90cccf 100755 --- a/Scripts/jazzy.sh +++ b/Scripts/jazzy.sh @@ -5,7 +5,7 @@ bundle exec jazzy \ --author_url http://parseplatform.org \ --github_url https://github.com/parse-community/Parse-Swift \ --root-url http://parseplatform.org/Parse-Swift/api/ \ - --module-version 1.2.2 \ + --module-version 1.2.3 \ --theme fullwidth \ --skip-undocumented \ --output ./docs/api \ diff --git a/Sources/ParseSwift/API/API+Commands.swift b/Sources/ParseSwift/API/API+Commands.swift index bcaf81495..8dfa0095d 100644 --- a/Sources/ParseSwift/API/API+Commands.swift +++ b/Sources/ParseSwift/API/API+Commands.swift @@ -313,7 +313,10 @@ internal extension API.Command { } // MARK: Saving ParseObjects - static func saveCommand(_ object: T) -> API.Command where T: ParseObject { + static func saveCommand(_ object: T) throws -> API.Command where T: ParseObject { + if ParseConfiguration.allowCustomObjectId && object.objectId == nil { + throw ParseError(code: .missingObjectId, message: "objectId must not be nil") + } if object.isSaved { return updateCommand(object) } @@ -326,7 +329,7 @@ internal extension API.Command { try ParseCoding.jsonDecoder().decode(SaveResponse.self, from: data).apply(to: object) } return API.Command(method: .POST, - path: object.endpoint, + path: object.endpoint(.POST), body: object, mapper: mapper) } @@ -346,6 +349,9 @@ internal extension API.Command { guard let objectable = object as? Objectable else { throw ParseError(code: .unknownError, message: "Not able to cast to objectable. Not saving") } + if ParseConfiguration.allowCustomObjectId && objectable.objectId == nil { + throw ParseError(code: .missingObjectId, message: "objectId must not be nil") + } if objectable.isSaved { return try updateCommand(object) } else { @@ -364,9 +370,9 @@ internal extension API.Command { return try objectable.toPointer() } return API.Command(method: .POST, - path: objectable.endpoint, - body: object, - mapper: mapper) + path: objectable.endpoint(.POST), + body: object, + mapper: mapper) } private static func updateCommand(_ object: T) throws -> API.Command where T: Encodable { diff --git a/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift b/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift index cfaf691ec..47a0be07d 100644 --- a/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift +++ b/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift @@ -19,14 +19,14 @@ import Combine - Once logged out, an anonymous user cannot be recovered. - When the current user is anonymous, the following methods can be used to switch to a different user or convert the anonymous user into a regular one: - - *signup* converts an anonymous user to a standard user with the given username and password. + - *signup* converts an anonymous user to a standard user with the given username and password. Data associated with the anonymous user is retained. - - *login* switches users without converting the anonymous user. + - *login* switches users without converting the anonymous user. Data associated with the anonymous user will be lost. - - Service *login* (e.g. Apple, Facebook, Twitter) will attempt to convert + - Service *login* (e.g. Apple, Facebook, Twitter) will attempt to convert the anonymous user into a standard user by linking it to the service. If a user already exists that is linked to the service, it will instead switch to the existing user. - - Service linking (e.g. Apple, Facebook, Twitter) will convert the anonymous user + - Service linking (e.g. Apple, Facebook, Twitter) will convert the anonymous user into a standard user by linking it to the service. */ public struct ParseAnonymous: ParseAuthentication { diff --git a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift index f57bc947c..fb88f1e00 100644 --- a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift +++ b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift @@ -37,10 +37,12 @@ import FoundationNetworking The above creates a `ParseLiveQuery` using either the `liveQueryServerURL` (if it has been set) or `serverURL` when using `ParseSwift.initialize`. All additional queries will be created in the same way. The times you will want to initialize a new `ParseLiveQuery` instance - are: 1) If you want to become a `ParseLiveQueryDelegate` to respond to authentification challenges + are: + 1. If you want to become a `ParseLiveQueryDelegate` to respond to authentification challenges and/or receive metrics and error messages for a `ParseLiveQuery`client. - 2) You have specific LiveQueries that need to subscribe to a server that have a different url than - the default. 3) You want to change the default url for all LiveQuery connections when the app is already + 2. You have specific LiveQueries that need to subscribe to a server that have a different url than + the default. + 3. You want to change the default url for all LiveQuery connections when the app is already running. Initializing new instances will create a new task/connection to the `ParseLiveQuery` server. When an instance is deinitialized it will automatically close it's connection gracefully. */ diff --git a/Sources/ParseSwift/Objects/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift index 3e3bff532..d14a4407c 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation.swift @@ -97,6 +97,14 @@ extension ParseInstallation { return .installations } + func endpoint(_ method: API.Method) -> API.Endpoint { + if !ParseConfiguration.allowCustomObjectId || method != .POST { + return endpoint + } else { + return .installations + } + } + /** Sets the device token string property from an `Data`-encoded token. - parameter data: A token that identifies the device. @@ -478,19 +486,29 @@ extension ParseInstallation { ) { self.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, error) in guard let parseError = error else { - self.saveCommand() - .executeAsync(options: options, - callbackQueue: callbackQueue, - childObjects: savedChildObjects, - childFiles: savedChildFiles) { result in - callbackQueue.async { - if case .success(let foundResults) = result { - Self.updateKeychainIfNeeded([foundResults]) - } + do { + try self.saveCommand() + .executeAsync(options: options, + callbackQueue: callbackQueue, + childObjects: savedChildObjects, + childFiles: savedChildFiles) { result in + callbackQueue.async { + if case .success(let foundResults) = result { + Self.updateKeychainIfNeeded([foundResults]) + } - completion(result) + completion(result) + } + } + } catch { + callbackQueue.async { + if let parseError = error as? ParseError { + completion(.failure(parseError)) + } else { + completion(.failure(.init(code: .unknownError, message: error.localizedDescription))) } } + } return } callbackQueue.async { @@ -499,7 +517,10 @@ extension ParseInstallation { } } - func saveCommand() -> API.Command { + func saveCommand() throws -> API.Command { + if ParseConfiguration.allowCustomObjectId && objectId == nil { + throw ParseError(code: .missingObjectId, message: "objectId must not be nil") + } if isSaved { return updateCommand() } @@ -512,9 +533,9 @@ extension ParseInstallation { try ParseCoding.jsonDecoder().decode(SaveResponse.self, from: data).apply(to: self) } return API.Command(method: .POST, - path: endpoint, - body: self, - mapper: mapper) + path: endpoint(.POST), + body: self, + mapper: mapper) } private func updateCommand() -> API.Command { @@ -668,7 +689,7 @@ public extension Sequence where Element: ParseInstallation { } var returnBatch = [(Result)]() - let commands = map { $0.saveCommand() } + let commands = try map { try $0.saveCommand() } let batchLimit: Int! if transaction { batchLimit = commands.count @@ -762,39 +783,49 @@ public extension Sequence where Element: ParseInstallation { } } - var returnBatch = [(Result)]() - let commands = map { $0.saveCommand() } - let batchLimit: Int! - if transaction { - batchLimit = commands.count - } else { - batchLimit = limit != nil ? limit! : ParseConstants.batchLimit - } - let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) - var completed = 0 - for batch in batches { - API.Command - .batch(commands: batch, transaction: transaction) - .executeAsync(options: options, - callbackQueue: callbackQueue, - childObjects: childObjects, - childFiles: childFiles) { results in - switch results { - - case .success(let saved): - returnBatch.append(contentsOf: saved) - if completed == (batches.count - 1) { + do { + var returnBatch = [(Result)]() + let commands = try map { try $0.saveCommand() } + let batchLimit: Int! + if transaction { + batchLimit = commands.count + } else { + batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + } + let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) + var completed = 0 + for batch in batches { + API.Command + .batch(commands: batch, transaction: transaction) + .executeAsync(options: options, + callbackQueue: callbackQueue, + childObjects: childObjects, + childFiles: childFiles) { results in + switch results { + + case .success(let saved): + returnBatch.append(contentsOf: saved) + if completed == (batches.count - 1) { + callbackQueue.async { + Self.Element.updateKeychainIfNeeded(returnBatch.compactMap {try? $0.get()}) + completion(.success(returnBatch)) + } + } + completed += 1 + case .failure(let error): callbackQueue.async { - Self.Element.updateKeychainIfNeeded(returnBatch.compactMap {try? $0.get()}) - completion(.success(returnBatch)) + completion(.failure(error)) } + return } - completed += 1 - case .failure(let error): - callbackQueue.async { - completion(.failure(error)) - } - return + } + } + } catch { + callbackQueue.async { + if let parseError = error as? ParseError { + completion(.failure(parseError)) + } else { + completion(.failure(.init(code: .unknownError, message: error.localizedDescription))) } } } diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index 7c1a5ac37..a2c476c4a 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -21,7 +21,7 @@ import Foundation - warning: If you plan to use "reference types" (classes), you are using at your risk as this SDK is not designed for reference types and may have unexpected behavior when it comes to threading. You will also need to implement your own `==` method to conform to `Equatable` along with with the `hash` method to conform to `Hashable`. - It is important to note that for unsaved ParseObject`s, you won't be able to rely on `objectId` for + It is important to note that for unsaved `ParseObject`'s, you won't be able to rely on `objectId` for `Equatable` and `Hashable` as your unsaved objects won't have this value yet and is nil. A possible way to address this is by creating a `UUID` for your objects locally and relying on that for `Equatable` and `Hashable`, otherwise it's possible you will get "circular dependency errors" depending on your implementation. @@ -121,7 +121,7 @@ public extension Sequence where Element: ParseObject { } var returnBatch = [(Result)]() - let commands = map { $0.saveCommand() } + let commands = try map { try $0.saveCommand() } let batchLimit: Int! if transaction { batchLimit = commands.count @@ -213,38 +213,48 @@ public extension Sequence where Element: ParseObject { } } - var returnBatch = [(Result)]() - let commands = map { $0.saveCommand() } - let batchLimit: Int! - if transaction { - batchLimit = commands.count - } else { - batchLimit = limit != nil ? limit! : ParseConstants.batchLimit - } - let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) - var completed = 0 - for batch in batches { - API.Command - .batch(commands: batch, transaction: transaction) - .executeAsync(options: options, - callbackQueue: callbackQueue, - childObjects: childObjects, - childFiles: childFiles) { results in - switch results { - - case .success(let saved): - returnBatch.append(contentsOf: saved) - if completed == (batches.count - 1) { + do { + var returnBatch = [(Result)]() + let commands = try map { try $0.saveCommand() } + let batchLimit: Int! + if transaction { + batchLimit = commands.count + } else { + batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + } + let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) + var completed = 0 + for batch in batches { + API.Command + .batch(commands: batch, transaction: transaction) + .executeAsync(options: options, + callbackQueue: callbackQueue, + childObjects: childObjects, + childFiles: childFiles) { results in + switch results { + + case .success(let saved): + returnBatch.append(contentsOf: saved) + if completed == (batches.count - 1) { + callbackQueue.async { + completion(.success(returnBatch)) + } + } + completed += 1 + case .failure(let error): callbackQueue.async { - completion(.success(returnBatch)) + completion(.failure(error)) } + return } - completed += 1 - case .failure(let error): - callbackQueue.async { - completion(.failure(error)) - } - return + } + } + } catch { + callbackQueue.async { + if let parseError = error as? ParseError { + completion(.failure(parseError)) + } else { + completion(.failure(.init(code: .unknownError, message: error.localizedDescription))) } } } @@ -589,12 +599,22 @@ extension ParseObject { ) { self.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, error) in guard let parseError = error else { - self.saveCommand().executeAsync(options: options, - callbackQueue: callbackQueue, - childObjects: savedChildObjects, - childFiles: savedChildFiles) { result in + do { + try self.saveCommand().executeAsync(options: options, + callbackQueue: callbackQueue, + childObjects: savedChildObjects, + childFiles: savedChildFiles) { result in + callbackQueue.async { + completion(result) + } + } + } catch { callbackQueue.async { - completion(result) + if let parseError = error as? ParseError { + completion(.failure(parseError)) + } else { + completion(.failure(.init(code: .unknownError, message: error.localizedDescription))) + } } } return @@ -605,8 +625,8 @@ extension ParseObject { } } - internal func saveCommand() -> API.Command { - API.Command.saveCommand(self) + internal func saveCommand() throws -> API.Command { + try API.Command.saveCommand(self) } // swiftlint:disable:next function_body_length diff --git a/Sources/ParseSwift/Objects/ParseUser+combine.swift b/Sources/ParseSwift/Objects/ParseUser+combine.swift index 17f91bbc2..77b7e92a5 100644 --- a/Sources/ParseSwift/Objects/ParseUser+combine.swift +++ b/Sources/ParseSwift/Objects/ParseUser+combine.swift @@ -13,7 +13,7 @@ import Combine @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public extension ParseUser { - // MARK: Signing Out - Combine + // MARK: Signing Up - Combine /** Signs up the user *asynchronously* and publishes value. diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index a7bf2ac0d..b0384f7c0 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -69,6 +69,14 @@ extension ParseUser { return .users } + func endpoint(_ method: API.Method) -> API.Endpoint { + if !ParseConfiguration.allowCustomObjectId || method != .POST { + return endpoint + } else { + return .users + } + } + static func deleteCurrentKeychain() { deleteCurrentContainerFromKeychain() BaseParseInstallation.deleteCurrentContainerFromKeychain() @@ -814,17 +822,27 @@ extension ParseUser { ) { self.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, error) in guard let parseError = error else { - self.saveCommand() - .executeAsync(options: options, - callbackQueue: callbackQueue, - childObjects: savedChildObjects, - childFiles: savedChildFiles) { result in - callbackQueue.async { - if case .success(let foundResults) = result { - try? Self.updateKeychainIfNeeded([foundResults]) + do { + try self.saveCommand() + .executeAsync(options: options, + callbackQueue: callbackQueue, + childObjects: savedChildObjects, + childFiles: savedChildFiles) { result in + callbackQueue.async { + if case .success(let foundResults) = result { + try? Self.updateKeychainIfNeeded([foundResults]) + } + completion(result) } - completion(result) + } + } catch { + callbackQueue.async { + if let parseError = error as? ParseError { + completion(.failure(parseError)) + } else { + completion(.failure(.init(code: .unknownError, message: error.localizedDescription))) } + } } return } @@ -834,7 +852,10 @@ extension ParseUser { } } - func saveCommand() -> API.Command { + func saveCommand() throws -> API.Command { + if ParseConfiguration.allowCustomObjectId && objectId == nil { + throw ParseError(code: .missingObjectId, message: "objectId must not be nil") + } if isSaved { return updateCommand() } @@ -847,9 +868,9 @@ extension ParseUser { try ParseCoding.jsonDecoder().decode(SaveResponse.self, from: data).apply(to: self) } return API.Command(method: .POST, - path: endpoint, - body: self, - mapper: mapper) + path: endpoint(.POST), + body: self, + mapper: mapper) } private func updateCommand() -> API.Command { @@ -1002,7 +1023,7 @@ public extension Sequence where Element: ParseUser { } var returnBatch = [(Result)]() - let commands = map { $0.saveCommand() } + let commands = try map { try $0.saveCommand() } let batchLimit: Int! if transaction { batchLimit = commands.count @@ -1095,39 +1116,49 @@ public extension Sequence where Element: ParseUser { } } - var returnBatch = [(Result)]() - let commands = map { $0.saveCommand() } - let batchLimit: Int! - if transaction { - batchLimit = commands.count - } else { - batchLimit = limit != nil ? limit! : ParseConstants.batchLimit - } - let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) - var completed = 0 - for batch in batches { - API.Command - .batch(commands: batch, transaction: transaction) - .executeAsync(options: options, - callbackQueue: callbackQueue, - childObjects: childObjects, - childFiles: childFiles) { results in - switch results { - - case .success(let saved): - returnBatch.append(contentsOf: saved) - if completed == (batches.count - 1) { + do { + var returnBatch = [(Result)]() + let commands = try map { try $0.saveCommand() } + let batchLimit: Int! + if transaction { + batchLimit = commands.count + } else { + batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + } + let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) + var completed = 0 + for batch in batches { + API.Command + .batch(commands: batch, transaction: transaction) + .executeAsync(options: options, + callbackQueue: callbackQueue, + childObjects: childObjects, + childFiles: childFiles) { results in + switch results { + + case .success(let saved): + returnBatch.append(contentsOf: saved) + if completed == (batches.count - 1) { + callbackQueue.async { + try? Self.Element.updateKeychainIfNeeded(returnBatch.compactMap {try? $0.get()}) + completion(.success(returnBatch)) + } + } + completed += 1 + case .failure(let error): callbackQueue.async { - try? Self.Element.updateKeychainIfNeeded(returnBatch.compactMap {try? $0.get()}) - completion(.success(returnBatch)) + completion(.failure(error)) } + return } - completed += 1 - case .failure(let error): - callbackQueue.async { - completion(.failure(error)) - } - return + } + } + } catch { + callbackQueue.async { + if let parseError = error as? ParseError { + completion(.failure(parseError)) + } else { + completion(.failure(.init(code: .unknownError, message: error.localizedDescription))) } } } diff --git a/Sources/ParseSwift/ParseConstants.swift b/Sources/ParseSwift/ParseConstants.swift index e402bed63..6dbfe056c 100644 --- a/Sources/ParseSwift/ParseConstants.swift +++ b/Sources/ParseSwift/ParseConstants.swift @@ -9,7 +9,7 @@ import Foundation enum ParseConstants { - static let parseVersion = "1.2.2" + static let parseVersion = "1.2.3" static let hashingKey = "parseSwift" static let fileManagementDirectory = "parse/" static let fileManagementPrivateDocumentsDirectory = "Private Documents/" diff --git a/Sources/ParseSwift/Protocols/Objectable.swift b/Sources/ParseSwift/Protocols/Objectable.swift index 62e138cff..f2d1c22b5 100644 --- a/Sources/ParseSwift/Protocols/Objectable.swift +++ b/Sources/ParseSwift/Protocols/Objectable.swift @@ -85,6 +85,14 @@ extension Objectable { func toPointer() throws -> PointerType { return try PointerType(self) } + + func endpoint(_ method: API.Method) -> API.Endpoint { + if !ParseConfiguration.allowCustomObjectId || method != .POST { + return endpoint + } else { + return .objects(className: className) + } + } } internal struct UniqueObject: Encodable, Decodable, Hashable { diff --git a/Sources/ParseSwift/Types/ParseGeoPoint.swift b/Sources/ParseSwift/Types/ParseGeoPoint.swift index 10b0a4672..224d01603 100644 --- a/Sources/ParseSwift/Types/ParseGeoPoint.swift +++ b/Sources/ParseSwift/Types/ParseGeoPoint.swift @@ -5,7 +5,7 @@ import CoreLocation /** `ParseGeoPoint` is used to embed a latitude / longitude point as the value for a key in a `ParseObject`. - It could be used to perform queries in a geospatial manner using `ParseQuery.-whereKey:nearGeoPoint:`. + It could be used to perform queries in a geospatial manner using `ParseQuery.whereKey:nearGeoPoint:`. Currently, instances of `ParseObject` may only have one key associated with a `ParseGeoPoint` type. */ public struct ParseGeoPoint: Codable, Hashable { diff --git a/Tests/ParseSwiftTests/ParseInstallationTests.swift b/Tests/ParseSwiftTests/ParseInstallationTests.swift index f5d6521f0..37c2c01c1 100644 --- a/Tests/ParseSwiftTests/ParseInstallationTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationTests.swift @@ -925,7 +925,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l func testSaveCommand() throws { let installation = Installation() - let command = installation.saveCommand() + let command = try installation.saveCommand() XCTAssertNotNil(command) XCTAssertEqual(command.path.urlComponent, "/installations") XCTAssertEqual(command.method, API.Method.POST) @@ -938,7 +938,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l let objectId = "yarr" installation.objectId = objectId - let command = installation.saveCommand() + let command = try installation.saveCommand() XCTAssertNotNil(command) XCTAssertEqual(command.path.urlComponent, "/installations/\(objectId)") XCTAssertEqual(command.method, API.Method.PUT) diff --git a/Tests/ParseSwiftTests/ParseObjectBatchTests.swift b/Tests/ParseSwiftTests/ParseObjectBatchTests.swift index 261479995..f34bae05a 100644 --- a/Tests/ParseSwiftTests/ParseObjectBatchTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectBatchTests.swift @@ -73,7 +73,7 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le scoreOnServer2.ACL = nil let objects = [score, score2] - let commands = objects.map { $0.saveCommand() } + let commands = try objects.map { try $0.saveCommand() } let body = BatchCommand(requests: commands, transaction: false) // swiftlint:disable:next line_length let expected = "{\"requests\":[{\"path\":\"\\/classes\\/GameScore\",\"method\":\"POST\",\"body\":{\"score\":10}},{\"path\":\"\\/classes\\/GameScore\",\"method\":\"POST\",\"body\":{\"score\":20}}],\"transaction\":false}" @@ -284,7 +284,7 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le score2.updatedAt = score2.createdAt let objects = [score, score2] - let initialCommands = objects.map { $0.saveCommand() } + let initialCommands = try objects.map { try $0.saveCommand() } let commands = initialCommands.compactMap { (command) -> API.Command? in let path = ParseConfiguration.mountPath + command.path.urlComponent guard let body = command.body else { diff --git a/Tests/ParseSwiftTests/ParseObjectCustomObjectId.swift b/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift similarity index 81% rename from Tests/ParseSwiftTests/ParseObjectCustomObjectId.swift rename to Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift index 8f1c893c3..5b252d147 100644 --- a/Tests/ParseSwiftTests/ParseObjectCustomObjectId.swift +++ b/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift @@ -1,5 +1,5 @@ // -// ParseObjectCustomObjectId.swift +// ParseObjectCustomObjectIdTests.swift // ParseSwift // // Created by Corey Baker on 3/20/21. @@ -10,7 +10,7 @@ import Foundation import XCTest @testable import ParseSwift -class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_body_length +class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this type_body_length struct Level: ParseObject { var objectId: String? @@ -149,9 +149,9 @@ class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_bod score.objectId = objectId let className = score.className - let command = score.saveCommand() + let command = try score.saveCommand() XCTAssertNotNil(command) - XCTAssertEqual(command.path.urlComponent, "/classes/\(className)/\(objectId)") + XCTAssertEqual(command.path.urlComponent, "/classes/\(className)") XCTAssertEqual(command.method, API.Method.POST) XCTAssertNil(command.params) XCTAssertNotNil(command.data) @@ -178,7 +178,7 @@ class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_bod score.createdAt = Date() score.updatedAt = score.createdAt - let command = score.saveCommand() + let command = try score.saveCommand() XCTAssertNotNil(command) XCTAssertEqual(command.path.urlComponent, "/classes/\(className)/\(objectId)") XCTAssertEqual(command.method, API.Method.PUT) @@ -206,10 +206,10 @@ class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_bod score2.objectId = "yolo" let objects = [score, score2] - let commands = objects.map { $0.saveCommand() } + let commands = try objects.map { try $0.saveCommand() } let body = BatchCommand(requests: commands, transaction: false) // swiftlint:disable:next line_length - let expected = "{\"requests\":[{\"path\":\"\\/classes\\/GameScore\\/yarr\",\"method\":\"POST\",\"body\":{\"score\":10,\"player\":\"Jen\",\"objectId\":\"yarr\"}},{\"path\":\"\\/classes\\/GameScore\\/yolo\",\"method\":\"POST\",\"body\":{\"score\":20,\"player\":\"Jen\",\"objectId\":\"yolo\"}}],\"transaction\":false}" + let expected = "{\"requests\":[{\"path\":\"\\/classes\\/GameScore\",\"method\":\"POST\",\"body\":{\"score\":10,\"player\":\"Jen\",\"objectId\":\"yarr\"}},{\"path\":\"\\/classes\\/GameScore\",\"method\":\"POST\",\"body\":{\"score\":20,\"player\":\"Jen\",\"objectId\":\"yolo\"}}],\"transaction\":false}" let encoded = try ParseCoding.parseEncoder() .encode(body, collectChildren: false, objectsSavedBeforeThisOne: nil, @@ -227,7 +227,7 @@ class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_bod score2.createdAt = Date() let objects = [score, score2] - let commands = objects.map { $0.saveCommand() } + let commands = try objects.map { try $0.saveCommand() } let body = BatchCommand(requests: commands, transaction: false) // swiftlint:disable:next line_length let expected = "{\"requests\":[{\"path\":\"\\/classes\\/GameScore\\/yarr\",\"method\":\"PUT\",\"body\":{\"score\":10,\"player\":\"Jen\",\"objectId\":\"yarr\"}},{\"path\":\"\\/classes\\/GameScore\\/yolo\",\"method\":\"PUT\",\"body\":{\"score\":20,\"player\":\"Jen\",\"objectId\":\"yolo\"}}],\"transaction\":false}" @@ -244,9 +244,9 @@ class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_bod var user = User() user.objectId = objectId - let command = user.saveCommand() + let command = try user.saveCommand() XCTAssertNotNil(command) - XCTAssertEqual(command.path.urlComponent, "/users/\(objectId)") + XCTAssertEqual(command.path.urlComponent, "/users") XCTAssertEqual(command.method, API.Method.POST) XCTAssertNil(command.params) XCTAssertNotNil(command.data) @@ -271,7 +271,7 @@ class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_bod user.objectId = objectId user.createdAt = Date() - let command = user.saveCommand() + let command = try user.saveCommand() XCTAssertNotNil(command) XCTAssertEqual(command.path.urlComponent, "/users/\(objectId)") XCTAssertEqual(command.method, API.Method.PUT) @@ -299,10 +299,10 @@ class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_bod user2.objectId = "yolo" let objects = [user, user2] - let commands = objects.map { $0.saveCommand() } + let commands = try objects.map { try $0.saveCommand() } let body = BatchCommand(requests: commands, transaction: false) // swiftlint:disable:next line_length - let expected = "{\"requests\":[{\"path\":\"\\/users\\/yarr\",\"method\":\"POST\",\"body\":{\"objectId\":\"yarr\"}},{\"path\":\"\\/users\\/yolo\",\"method\":\"POST\",\"body\":{\"objectId\":\"yolo\"}}],\"transaction\":false}" + let expected = "{\"requests\":[{\"path\":\"\\/users\",\"method\":\"POST\",\"body\":{\"objectId\":\"yarr\"}},{\"path\":\"\\/users\",\"method\":\"POST\",\"body\":{\"objectId\":\"yolo\"}}],\"transaction\":false}" let encoded = try ParseCoding.parseEncoder() .encode(body, collectChildren: false, objectsSavedBeforeThisOne: nil, @@ -320,7 +320,7 @@ class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_bod user2.createdAt = Date() let objects = [user, user2] - let commands = objects.map { $0.saveCommand() } + let commands = try objects.map { try $0.saveCommand() } let body = BatchCommand(requests: commands, transaction: false) // swiftlint:disable:next line_length let expected = "{\"requests\":[{\"path\":\"\\/users\\/yarr\",\"method\":\"PUT\",\"body\":{\"objectId\":\"yarr\"}},{\"path\":\"\\/users\\/yolo\",\"method\":\"PUT\",\"body\":{\"objectId\":\"yolo\"}}],\"transaction\":false}" @@ -337,9 +337,9 @@ class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_bod var installation = Installation() installation.objectId = objectId - let command = installation.saveCommand() + let command = try installation.saveCommand() XCTAssertNotNil(command) - XCTAssertEqual(command.path.urlComponent, "/installations/\(objectId)") + XCTAssertEqual(command.path.urlComponent, "/installations") XCTAssertEqual(command.method, API.Method.POST) XCTAssertNil(command.params) XCTAssertNotNil(command.data) @@ -364,7 +364,7 @@ class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_bod installation.objectId = objectId installation.createdAt = Date() - let command = installation.saveCommand() + let command = try installation.saveCommand() XCTAssertNotNil(command) XCTAssertEqual(command.path.urlComponent, "/installations/\(objectId)") XCTAssertEqual(command.method, API.Method.PUT) @@ -392,10 +392,10 @@ class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_bod installation2.objectId = "yolo" let objects = [installation, installation2] - let commands = objects.map { $0.saveCommand() } + let commands = try objects.map { try $0.saveCommand() } let body = BatchCommand(requests: commands, transaction: false) // swiftlint:disable:next line_length - let expected = "{\"requests\":[{\"path\":\"\\/installations\\/yarr\",\"method\":\"POST\",\"body\":{\"objectId\":\"yarr\"}},{\"path\":\"\\/installations\\/yolo\",\"method\":\"POST\",\"body\":{\"objectId\":\"yolo\"}}],\"transaction\":false}" + let expected = "{\"requests\":[{\"path\":\"\\/installations\",\"method\":\"POST\",\"body\":{\"objectId\":\"yarr\"}},{\"path\":\"\\/installations\",\"method\":\"POST\",\"body\":{\"objectId\":\"yolo\"}}],\"transaction\":false}" let encoded = try ParseCoding.parseEncoder() .encode(body, collectChildren: false, objectsSavedBeforeThisOne: nil, @@ -413,7 +413,7 @@ class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_bod installation2.createdAt = Date() let objects = [installation, installation2] - let commands = objects.map { $0.saveCommand() } + let commands = try objects.map { try $0.saveCommand() } let body = BatchCommand(requests: commands, transaction: false) // swiftlint:disable:next line_length let expected = "{\"requests\":[{\"path\":\"\\/installations\\/yarr\",\"method\":\"PUT\",\"body\":{\"objectId\":\"yarr\"}},{\"path\":\"\\/installations\\/yolo\",\"method\":\"PUT\",\"body\":{\"objectId\":\"yolo\"}}],\"transaction\":false}" @@ -426,6 +426,87 @@ class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_bod } #endif + func testSaveCommandNoObjectId() throws { + let score = GameScore(score: 10) + XCTAssertThrowsError(try score.saveCommand()) + } + + func testUpdateCommandNoObjectId() throws { + var score = GameScore(score: 10) + score.createdAt = Date() + XCTAssertThrowsError(try score.saveCommand()) + } + + func testSaveAllNoObjectIdCommand() throws { + let score = GameScore(score: 10) + let score2 = GameScore(score: 20) + let objects = [score, score2] + XCTAssertThrowsError(try objects.map { try $0.saveCommand() }) + } + + func testUpdateAllNoObjectIdCommand() throws { + var score = GameScore(score: 10) + score.createdAt = Date() + var score2 = GameScore(score: 20) + score2.createdAt = Date() + let objects = [score, score2] + XCTAssertThrowsError(try objects.map { try $0.saveCommand() }) + } + + func testUserSaveCommandNoObjectId() throws { + let user = User() + XCTAssertThrowsError(try user.saveCommand()) + } + + func testUserUpdateCommandNoObjectId() throws { + var user = User() + user.createdAt = Date() + XCTAssertThrowsError(try user.saveCommand()) + } + + func testUserSaveAllNoObjectIdCommand() throws { + let user = User() + let user2 = User() + let objects = [user, user2] + XCTAssertThrowsError(try objects.map { try $0.saveCommand() }) + } + + func testUserUpdateAllNoObjectIdCommand() throws { + var user = GameScore(score: 10) + user.createdAt = Date() + var user2 = GameScore(score: 20) + user2.createdAt = Date() + let objects = [user, user2] + XCTAssertThrowsError(try objects.map { try $0.saveCommand() }) + } + + func testInstallationSaveCommandNoObjectId() throws { + let installation = Installation() + XCTAssertThrowsError(try installation.saveCommand()) + } + + func testInstallationUpdateCommandNoObjectId() throws { + var installation = Installation() + installation.createdAt = Date() + XCTAssertThrowsError(try installation.saveCommand()) + } + + func testInstallationSaveAllNoObjectIdCommand() throws { + let installation = Installation() + let installation2 = Installation() + let objects = [installation, installation2] + XCTAssertThrowsError(try objects.map { try $0.saveCommand() }) + } + + func testInstallationUpdateAllNoObjectIdCommand() throws { + var score = GameScore(score: 10) + score.createdAt = Date() + var score2 = GameScore(score: 20) + score2.createdAt = Date() + let objects = [score, score2] + XCTAssertThrowsError(try objects.map { try $0.saveCommand() }) + } + func testSave() { // swiftlint:disable:this function_body_length var score = GameScore(score: 10) score.objectId = "yarr" @@ -553,6 +634,22 @@ class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_bod self.saveAsync(score: score, scoreOnServer: scoreOnServer, callbackQueue: .main) } + func testSaveNoObjectIdAsyncMainQueue() throws { + let score = GameScore(score: 10) + XCTAssertThrowsError(try score.save()) + + let expectation1 = XCTestExpectation(description: "Save object2") + score.save { result in + if case let .failure(error) = result { + XCTAssertTrue(error.message.contains("objectId")) + } else { + XCTFail("Should have failed") + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + func updateAsync(score: GameScore, scoreOnServer: GameScore, callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Update object1") @@ -619,6 +716,23 @@ class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_bod self.updateAsync(score: score, scoreOnServer: scoreOnServer, callbackQueue: .main) } + func testUpdateNoObjectIdAsyncMainQueue() throws { + var score = GameScore(score: 10) + score.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + XCTAssertThrowsError(try score.save()) + + let expectation1 = XCTestExpectation(description: "Save object2") + score.save { result in + if case let .failure(error) = result { + XCTAssertTrue(error.message.contains("objectId")) + } else { + XCTFail("Should have failed") + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + func testSaveAll() { // swiftlint:disable:this function_body_length cyclomatic_complexity var score = GameScore(score: 10) score.objectId = "yarr" @@ -686,6 +800,22 @@ class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_bod XCTAssertThrowsError(try [score, score2].saveAll()) } + func testSaveAllNoObjectIdAsync() throws { + let score = GameScore(score: 10) + let score2 = GameScore(score: 20) + + let expectation1 = XCTestExpectation(description: "Save object2") + [score, score2].saveAll { result in + if case let .failure(error) = result { + XCTAssertTrue(error.message.contains("objectId")) + } else { + XCTFail("Should have failed") + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + func testUpdateAll() { // swiftlint:disable:this function_body_length cyclomatic_complexity var score = GameScore(score: 10) score.objectId = "yarr" @@ -755,6 +885,24 @@ class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_bod XCTAssertThrowsError(try [score, score2].saveAll()) } + func testUpdateAllNoObjectIdAsync() throws { + var score = GameScore(score: 10) + score.createdAt = Date() + var score2 = GameScore(score: 20) + score2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + + let expectation1 = XCTestExpectation(description: "Save object2") + [score, score2].saveAll { result in + if case let .failure(error) = result { + XCTAssertTrue(error.message.contains("objectId")) + } else { + XCTFail("Should have failed") + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + func testUserSave() { // swiftlint:disable:this function_body_length var user = User() user.objectId = "yarr" @@ -869,6 +1017,22 @@ class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_bod self.saveUserAsync(user: user, userOnServer: userOnServer, callbackQueue: .main) } + func testUserSaveNoObjectIdAsyncMainQueue() throws { + let user = User() + XCTAssertThrowsError(try user.save()) + + let expectation1 = XCTestExpectation(description: "Save object2") + user.save { result in + if case let .failure(error) = result { + XCTAssertTrue(error.message.contains("objectId")) + } else { + XCTFail("Should have failed") + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + func updateUserAsync(user: User, userOnServer: User, callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Update object1") @@ -911,6 +1075,23 @@ class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_bod self.updateUserAsync(user: user, userOnServer: userOnServer, callbackQueue: .main) } + func testUserUpdateNoObjectIdAsyncMainQueue() throws { + var user = User() + user.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + XCTAssertThrowsError(try user.save()) + + let expectation1 = XCTestExpectation(description: "Save object2") + user.save { result in + if case let .failure(error) = result { + XCTAssertTrue(error.message.contains("objectId")) + } else { + XCTFail("Should have failed") + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + func testUserSaveAll() { // swiftlint:disable:this function_body_length cyclomatic_complexity var user = User() user.objectId = "yarr" @@ -979,6 +1160,22 @@ class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_bod XCTAssertThrowsError(try [user, user2].saveAll()) } + func testUserSaveAllNoObjectIdAsync() throws { + let user = User() + let user2 = User() + + let expectation1 = XCTestExpectation(description: "SaveAll user") + [user, user2].saveAll { result in + if case let .failure(error) = result { + XCTAssertTrue(error.message.contains("objectId")) + } else { + XCTFail("Should have failed") + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + func testUserUpdateAll() { // swiftlint:disable:this function_body_length cyclomatic_complexity var user = User() user.objectId = "yarr" @@ -1048,6 +1245,24 @@ class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_bod XCTAssertThrowsError(try [user, user2].saveAll()) } + func testUserUpdateAllNoObjectIdAsync() throws { + var user = User() + user.createdAt = Date() + var user2 = User() + user2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + + let expectation1 = XCTestExpectation(description: "UpdateAll user") + [user, user2].saveAll { result in + if case let .failure(error) = result { + XCTAssertTrue(error.message.contains("objectId")) + } else { + XCTFail("Should have failed") + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + func testInstallationSave() { // swiftlint:disable:this function_body_length var installation = Installation() installation.objectId = "yarr" @@ -1179,6 +1394,22 @@ class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_bod callbackQueue: .main) } + func testInstallationSaveNoObjectIdAsyncMainQueue() throws { + let installation = Installation() + XCTAssertThrowsError(try installation.save()) + + let expectation1 = XCTestExpectation(description: "Save object2") + installation.save { result in + if case let .failure(error) = result { + XCTAssertTrue(error.message.contains("objectId")) + } else { + XCTFail("Should have failed") + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + func updateInstallationAsync(installation: Installation, installationOnServer: Installation, callbackQueue: DispatchQueue) { @@ -1225,6 +1456,23 @@ class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_bod callbackQueue: .main) } + func testInstallationUpdateNoObjectIdAsyncMainQueue() throws { + var installation = Installation() + installation.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + XCTAssertThrowsError(try installation.save()) + + let expectation1 = XCTestExpectation(description: "Save object2") + installation.save { result in + if case let .failure(error) = result { + XCTAssertTrue(error.message.contains("objectId")) + } else { + XCTFail("Should have failed") + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + func testInstallationSaveAll() { // swiftlint:disable:this function_body_length cyclomatic_complexity var installation = Installation() installation.objectId = "yarr" @@ -1306,6 +1554,22 @@ class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_bod XCTAssertThrowsError(try [installation, installation2].saveAll()) } + func testInstallationSaveAllNoObjectIdAsync() throws { + let installation = Installation() + let installation2 = Installation() + + let expectation1 = XCTestExpectation(description: "SaveAll installation") + [installation, installation2].saveAll { result in + if case let .failure(error) = result { + XCTAssertTrue(error.message.contains("objectId")) + } else { + XCTFail("Should have failed") + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + func testInstallationUpdateAll() { // swiftlint:disable:this function_body_length cyclomatic_complexity var installation = Installation() installation.objectId = "yarr" @@ -1374,4 +1638,22 @@ class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_bod installation2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) XCTAssertThrowsError(try [installation, installation2].saveAll()) } + + func testInstallationUpdateAllNoObjectIdAsync() throws { + var installation = Installation() + installation.createdAt = Date() + var installation2 = Installation() + installation2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + + let expectation1 = XCTestExpectation(description: "UpdateAll installation") + [installation, installation2].saveAll { result in + if case let .failure(error) = result { + XCTAssertTrue(error.message.contains("objectId")) + } else { + XCTFail("Should have failed") + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } } diff --git a/Tests/ParseSwiftTests/ParseObjectTests.swift b/Tests/ParseSwiftTests/ParseObjectTests.swift index a7e4c62fa..7162dc9f8 100644 --- a/Tests/ParseSwiftTests/ParseObjectTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectTests.swift @@ -502,7 +502,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length let score = GameScore(score: 10) let className = score.className - let command = score.saveCommand() + let command = try score.saveCommand() XCTAssertNotNil(command) XCTAssertEqual(command.path.urlComponent, "/classes/\(className)") XCTAssertEqual(command.method, API.Method.POST) @@ -531,7 +531,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length score.createdAt = Date() score.updatedAt = score.createdAt - let command = score.saveCommand() + let command = try score.saveCommand() XCTAssertNotNil(command) XCTAssertEqual(command.path.urlComponent, "/classes/\(className)/\(objectId)") XCTAssertEqual(command.method, API.Method.PUT) diff --git a/Tests/ParseSwiftTests/ParseUserTests.swift b/Tests/ParseSwiftTests/ParseUserTests.swift index f7dd06ce0..638febb3b 100644 --- a/Tests/ParseSwiftTests/ParseUserTests.swift +++ b/Tests/ParseSwiftTests/ParseUserTests.swift @@ -432,7 +432,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length func testSaveCommand() throws { let user = User() - let command = user.saveCommand() + let command = try user.saveCommand() XCTAssertNotNil(command) XCTAssertEqual(command.path.urlComponent, "/users") XCTAssertEqual(command.method, API.Method.POST) @@ -446,7 +446,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length let objectId = "yarr" user.objectId = objectId - let command = user.saveCommand() + let command = try user.saveCommand() XCTAssertNotNil(command) XCTAssertEqual(command.path.urlComponent, "/users/\(objectId)") XCTAssertEqual(command.method, API.Method.PUT)