diff --git a/CHANGELOG.md b/CHANGELOG.md index 70cc72597..33bbdd29f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,15 @@ # Parse-Swift Changelog ### main -[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.8.2...main), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/main/documentation/parseswift) +[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.9.0...main), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/main/documentation/parseswift) * _Contributing to this repo? Add info about your change here to be included in the next release_ +### 5.9.0 +[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.8.2...5.9.0), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/5.9.0/documentation/parseswift) + +__New features__ +* Add fetchAll method to array of Parse Pointer Object's ([#141](https://github.com/netreconlab/Parse-Swift/pull/141)), thanks to [Corey Baker](https://github.com/cbaker6). + ### 5.8.2 [Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.8.1...5.8.2), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/5.8.2/documentation/parseswift) diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index 10e277f1a..401e246f9 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -23,6 +23,8 @@ 7003972A25A3B0140052CB31 /* ParseURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7003972925A3B0130052CB31 /* ParseURLSessionDelegate.swift */; }; 7004C22025B63C7A005E0AD9 /* ParseRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7004C21F25B63C7A005E0AD9 /* ParseRelation.swift */; }; 7004C24D25B69207005E0AD9 /* ParseRoleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7004C22D25B69077005E0AD9 /* ParseRoleTests.swift */; }; + 700A8A662B4CC1E40087ADBE /* ParsePointerable+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 700A8A652B4CC1E40087ADBE /* ParsePointerable+async.swift */; }; + 700A8A682B4CC2700087ADBE /* ParsePointerable+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 700A8A672B4CC2700087ADBE /* ParsePointerable+combine.swift */; }; 700AFE03289C3508006C1CD9 /* ParseQueryCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 700AFE02289C3508006C1CD9 /* ParseQueryCacheTests.swift */; }; 70110D52250680140091CC1D /* ParseConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70110D51250680140091CC1D /* ParseConstants.swift */; }; 70110D572506CE890091CC1D /* BaseParseInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70110D562506CE890091CC1D /* BaseParseInstallation.swift */; }; @@ -278,6 +280,7 @@ 918CED592684C74000CFDC83 /* ParseLiveQuery+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918CED582684C74000CFDC83 /* ParseLiveQuery+combine.swift */; }; 918CED5E268618C600CFDC83 /* ParseLiveQueryCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918CED5D268618C600CFDC83 /* ParseLiveQueryCombineTests.swift */; }; 9194657824F16E330070296B /* ParseACLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9194657724F16E330070296B /* ParseACLTests.swift */; }; + 919823652B3A134000E9591A /* ParsePointerable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 919823642B3A134000E9591A /* ParsePointerable.swift */; }; 91B40651267A66ED00B129CD /* ParseErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B40650267A66ED00B129CD /* ParseErrorTests.swift */; }; 91B79AC326EE3A4E00073F2C /* API+NonParseBodyCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B79AC226EE3A4E00073F2C /* API+NonParseBodyCommand.swift */; }; 91B79AC826EE3C5D00073F2C /* API+BatchCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B79AC726EE3C5D00073F2C /* API+BatchCommand.swift */; }; @@ -364,6 +367,8 @@ 7003972925A3B0130052CB31 /* ParseURLSessionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseURLSessionDelegate.swift; sourceTree = ""; }; 7004C21F25B63C7A005E0AD9 /* ParseRelation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseRelation.swift; sourceTree = ""; }; 7004C22D25B69077005E0AD9 /* ParseRoleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseRoleTests.swift; sourceTree = ""; }; + 700A8A652B4CC1E40087ADBE /* ParsePointerable+async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParsePointerable+async.swift"; sourceTree = ""; }; + 700A8A672B4CC2700087ADBE /* ParsePointerable+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParsePointerable+combine.swift"; sourceTree = ""; }; 700AFE02289C3508006C1CD9 /* ParseQueryCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseQueryCacheTests.swift; sourceTree = ""; }; 70110D51250680140091CC1D /* ParseConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseConstants.swift; sourceTree = ""; }; 70110D562506CE890091CC1D /* BaseParseInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseParseInstallation.swift; sourceTree = ""; }; @@ -625,6 +630,7 @@ 918CED582684C74000CFDC83 /* ParseLiveQuery+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseLiveQuery+combine.swift"; sourceTree = ""; }; 918CED5D268618C600CFDC83 /* ParseLiveQueryCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseLiveQueryCombineTests.swift; sourceTree = ""; }; 9194657724F16E330070296B /* ParseACLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseACLTests.swift; sourceTree = ""; }; + 919823642B3A134000E9591A /* ParsePointerable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParsePointerable.swift; sourceTree = ""; }; 91B40650267A66ED00B129CD /* ParseErrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseErrorTests.swift; sourceTree = ""; }; 91B79AC226EE3A4E00073F2C /* API+NonParseBodyCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "API+NonParseBodyCommand.swift"; sourceTree = ""; }; 91B79AC726EE3C5D00073F2C /* API+BatchCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "API+BatchCommand.swift"; sourceTree = ""; }; @@ -942,6 +948,9 @@ 705025EF2851542D008D6624 /* ParsePushFirebasePayloadable.swift */, 705025CB284CE4C2008D6624 /* ParsePushPayloadable.swift */, 70A98D812794AB3C009B58F2 /* ParseQueryScorable.swift */, + 919823642B3A134000E9591A /* ParsePointerable.swift */, + 700A8A652B4CC1E40087ADBE /* ParsePointerable+async.swift */, + 700A8A672B4CC2700087ADBE /* ParsePointerable+combine.swift */, 916E206F29D8C83100C21EC6 /* ParseRelationOperationable.swift */, 70CE0ABB285F8FF900DAEA86 /* ParseTypeable.swift */, F97B45C824D9C6F200F4A88B /* Queryable.swift */, @@ -1410,7 +1419,7 @@ attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1430; - LastUpgradeCheck = 1500; + LastUpgradeCheck = 1520; ORGANIZATIONNAME = "Network Reconnaissance Lab"; TargetAttributes = { 4AB8B4F31F254AE10070F682 = { @@ -1557,6 +1566,7 @@ 703B094E26BF47E3005A112F /* ParseTwitter+combine.swift in Sources */, 70386A3825D998D90048EC1B /* ParseLDAP.swift in Sources */, 709A14A02839CABD00BF85E5 /* ParseCLP.swift in Sources */, + 700A8A662B4CC1E40087ADBE /* ParsePointerable+async.swift in Sources */, 700395F225A171320052CB31 /* LiveQueryable.swift in Sources */, 70F03A252780BDF700E5AFB4 /* ParseGoogle+async.swift in Sources */, F97B45F224D9C6F200F4A88B /* Pointer.swift in Sources */, @@ -1603,6 +1613,7 @@ 7045769326BD8F8100F86F71 /* ParseInstallation+async.swift in Sources */, 7C55F9E72860CD6B002A352D /* ParseSpotify.swift in Sources */, 7034B9FF2A46391200395CBC /* ParseHookFunction.swift in Sources */, + 700A8A682B4CC2700087ADBE /* ParsePointerable+combine.swift in Sources */, 7003960925A184EF0052CB31 /* ParseLiveQuery.swift in Sources */, 7044C17525C4ECFF0011F6E7 /* ParseCloudable+combine.swift in Sources */, 705025B32845C302008D6624 /* ParsePushStatus.swift in Sources */, @@ -1654,6 +1665,7 @@ F97B45F624D9C6F200F4A88B /* ParseError.swift in Sources */, 7045769D26BD934000F86F71 /* ParseFile+async.swift in Sources */, F97B463324D9C74400F4A88B /* URLSession.swift in Sources */, + 919823652B3A134000E9591A /* ParsePointerable.swift in Sources */, F97B464E24D9C78B00F4A88B /* ParseOperationAdd.swift in Sources */, 70D41D8028B520E200613510 /* ParseKeychainAccessGroup.swift in Sources */, 70385E762858E1000084D306 /* ParseHookFunctionable.swift in Sources */, @@ -1848,6 +1860,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -1917,6 +1930,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; diff --git a/ParseSwift.xcodeproj/xcshareddata/xcschemes/ParseSwift.xcscheme b/ParseSwift.xcodeproj/xcshareddata/xcschemes/ParseSwift.xcscheme index a5a95ce64..1bb827bea 100644 --- a/ParseSwift.xcodeproj/xcshareddata/xcschemes/ParseSwift.xcscheme +++ b/ParseSwift.xcodeproj/xcshareddata/xcschemes/ParseSwift.xcscheme @@ -1,6 +1,6 @@ [(Result)] { + try await withCheckedThrowingContinuation { continuation in + self.fetchAll( + includeKeys: includeKeys, + options: options, + completion: continuation.resume + ) + } + } + +} diff --git a/Sources/ParseSwift/Protocols/ParsePointerable+combine.swift b/Sources/ParseSwift/Protocols/ParsePointerable+combine.swift new file mode 100644 index 000000000..17fd71b8c --- /dev/null +++ b/Sources/ParseSwift/Protocols/ParsePointerable+combine.swift @@ -0,0 +1,41 @@ +// +// ParsePointerable+combine.swift +// ParseSwift +// +// Created by Corey Baker on 1/8/24. +// Copyright © 2024 Network Reconnaissance Lab. All rights reserved. +// + +#if canImport(Combine) + +import Foundation +import Combine + +// MARK: Batch Support +public extension Sequence where Element: ParsePointerObject { + + /** + Fetches a collection of objects *aynchronously* with the current data from the server and sets + an error if one occurs. Publishes when complete. + - parameter includeKeys: The name(s) of the key(s) to include that are + `ParseObject`s. Use `["*"]` to include all keys one level deep. This is similar to `include` and + `includeAll` for `Query`. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces an an array of Result enums with the object if a fetch was + successful or a `ParseError` if it failed. + */ + func fetchAllPublisher( + includeKeys: [String]? = nil, + options: API.Options = []) -> Future<[(Result)], ParseError> { + Future { promise in + self.fetchAll( + includeKeys: includeKeys, + options: options, + completion: promise + ) + } + } + +} + +#endif diff --git a/Sources/ParseSwift/Protocols/ParsePointerable.swift b/Sources/ParseSwift/Protocols/ParsePointerable.swift new file mode 100644 index 000000000..6edcdd6e6 --- /dev/null +++ b/Sources/ParseSwift/Protocols/ParsePointerable.swift @@ -0,0 +1,134 @@ +// +// ParsePointerable.swift +// ParseSwift +// +// Created by Corey Baker on 12/25/23. +// Copyright © 2023 Network Reconnaissance Lab. All rights reserved. +// + +import Foundation + +public protocol ParsePointer: Encodable { + + var __type: String { get } // swiftlint:disable:this identifier_name + + var className: String { get } + + var objectId: String { get set } +} + +extension ParsePointer { + /** + Determines if two objects have the same objectId. + - parameter as: Object to compare. + - returns: Returns a **true** if the other object has the same `objectId` or **false** if unsuccessful. + */ + func hasSameObjectId(as other: any ParsePointer) -> Bool { + return other.className == className && other.objectId == objectId + } +} + +public protocol ParsePointerObject: ParsePointer, ParseTypeable, Fetchable, Hashable { + associatedtype Object: ParseObject +} + +extension ParsePointerObject { + + /** + Convert a Pointer to its respective `ParseObject`. + - returns: A `ParseObject` created from this Pointer. + */ + func toObject() -> Object { + var object = Object() + object.objectId = self.objectId + return object + } + + /** + Determines if a `ParseObject` and `Pointer`have the same `objectId`. + - parameter as: `ParseObject` to compare. + - returns: Returns a **true** if the other object has the same `objectId` or **false** if unsuccessful. + */ + func hasSameObjectId(as other: Object) -> Bool { + return other.className == className && other.objectId == objectId + } + + /** + Determines if two `Pointer`'s have the same `objectId`. + - parameter as: `Pointer` to compare. + - returns: Returns a **true** if the other object has the same `objectId` or **false** if unsuccessful. + */ + func hasSameObjectId(as other: Self) -> Bool { + return other.className == className && other.objectId == objectId + } + + /** + Fetches the `ParseObject` *asynchronously* and executes the given callback block. + - parameter includeKeys: The name(s) of the key(s) to include. Use `["*"]` to include + all keys. + - 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 when completed. + It should have the following argument signature: `(Result)`. + - note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer + desires a different policy, it should be inserted in `options`. + */ + func fetch(includeKeys: [String]? = nil, + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void) { + Task { + var options = options + options.insert(.cachePolicy(.reloadIgnoringLocalCacheData)) + + let method = API.Method.GET + let path = API.Endpoint.object(className: className, objectId: objectId) + let params: [String: String]? = { + guard let includeKeys = includeKeys else { + return nil + } + return ["include": "\(Set(includeKeys))"] + }() + let mapper = { (data) -> Object in + try ParseCoding.jsonDecoder().decode(Object.self, from: data) + } + await API.NonParseBodyCommand(method: method, path: path, params: params, mapper: mapper) + .execute(options: options, + callbackQueue: callbackQueue, + completion: completion) + } + } +} + +// MARK: Batch Support +public extension Sequence where Element: ParsePointerObject { + + /** + Fetches a collection of objects all at once *asynchronously* and executes the completion block when done. + - parameter includeKeys: The name(s) of the key(s) to include that are + `ParseObject`s. Use `["*"]` to include all keys one level deep. This is similar to `include` and + `includeAll` for `Query`. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - parameter callbackQueue: The queue to return to after completion. Default value of .main. + - parameter completion: The block to execute. + It should have the following argument signature: `(Result<[(Result)], ParseError>)`. + - warning: The order in which objects are returned are not guarenteed. You should not expect results in + any particular order. + */ + func fetchAll( + includeKeys: [String]? = nil, + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result<[(Result)], ParseError>) -> Void + ) { + let objects = Set(compactMap { $0.toObject() }) + objects.fetchAll( + includeKeys: includeKeys, + options: options, + callbackQueue: callbackQueue, + completion: completion + ) + } + +} diff --git a/Sources/ParseSwift/Types/Pointer.swift b/Sources/ParseSwift/Types/Pointer.swift index 9930ecd49..5e96cbbc8 100644 --- a/Sources/ParseSwift/Types/Pointer.swift +++ b/Sources/ParseSwift/Types/Pointer.swift @@ -1,25 +1,5 @@ import Foundation -protocol ParsePointer: Encodable { - - var __type: String { get } // swiftlint:disable:this identifier_name - - var className: String { get } - - var objectId: String { get set } -} - -extension ParsePointer { - /** - Determines if two objects have the same objectId. - - parameter as: Object to compare. - - returns: Returns a **true** if the other object has the same `objectId` or **false** if unsuccessful. - */ - func hasSameObjectId(as other: ParsePointer) -> Bool { - return other.className == className && other.objectId == objectId - } -} - private func getObjectId(target: Objectable) throws -> String { guard let objectId = target.objectId else { throw ParseError(code: .missingObjectId, message: "Cannot set a pointer to an unsaved object") @@ -28,9 +8,10 @@ private func getObjectId(target: Objectable) throws -> String { } /// A Pointer referencing a ParseObject. -public struct Pointer: ParsePointer, ParseTypeable, Fetchable, Hashable { +public struct Pointer: ParsePointerObject { - internal let __type: String = "Pointer" // swiftlint:disable:this identifier_name + public typealias Object = T + public let __type: String = "Pointer" // swiftlint:disable:this identifier_name /** The class name of the object. @@ -76,65 +57,6 @@ public struct Pointer: ParsePointer, ParseTypeable, Fetchable, H } } -public extension Pointer { - - /** - Determines if a `ParseObject` and `Pointer`have the same `objectId`. - - parameter as: `ParseObject` to compare. - - returns: Returns a **true** if the other object has the same `objectId` or **false** if unsuccessful. - */ - func hasSameObjectId(as other: T) -> Bool { - return other.className == className && other.objectId == objectId - } - - /** - Determines if two `Pointer`'s have the same `objectId`. - - parameter as: `Pointer` to compare. - - returns: Returns a **true** if the other object has the same `objectId` or **false** if unsuccessful. - */ - func hasSameObjectId(as other: Self) -> Bool { - return other.className == className && other.objectId == objectId - } - - /** - Fetches the `ParseObject` *asynchronously* and executes the given callback block. - - parameter includeKeys: The name(s) of the key(s) to include. Use `["*"]` to include - all keys. - - 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 when completed. - It should have the following argument signature: `(Result)`. - - note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer - desires a different policy, it should be inserted in `options`. - */ - func fetch(includeKeys: [String]? = nil, - options: API.Options = [], - callbackQueue: DispatchQueue = .main, - completion: @escaping (Result) -> Void) { - Task { - var options = options - options.insert(.cachePolicy(.reloadIgnoringLocalCacheData)) - - let method = API.Method.GET - let path = API.Endpoint.object(className: className, objectId: objectId) - let params: [String: String]? = { - guard let includeKeys = includeKeys else { - return nil - } - return ["include": "\(Set(includeKeys))"] - }() - let mapper = { (data) -> T in - try ParseCoding.jsonDecoder().decode(T.self, from: data) - } - await API.NonParseBodyCommand(method: method, path: path, params: params, mapper: mapper) - .execute(options: options, - callbackQueue: callbackQueue, - completion: completion) - } - } -} - internal struct PointerType: ParsePointer, Codable { var __type: String = "Pointer" // swiftlint:disable:this identifier_name var className: String diff --git a/Tests/ParseSwiftTests/BatchUtilsTests.swift b/Tests/ParseSwiftTests/BatchUtilsTests.swift index a15d449a4..260a49abc 100644 --- a/Tests/ParseSwiftTests/BatchUtilsTests.swift +++ b/Tests/ParseSwiftTests/BatchUtilsTests.swift @@ -11,13 +11,6 @@ import XCTest @testable import ParseSwift class BatchUtilsTests: XCTestCase { - override func setUp() async throws { - try await super.setUp() - } - - override func tearDown() async throws { - try await super.tearDown() - } func testSplitArrayLessSegments() throws { let array = [1, 2] diff --git a/Tests/ParseSwiftTests/ParsePointerCombineTests.swift b/Tests/ParseSwiftTests/ParsePointerCombineTests.swift index ed8e6d39c..c20965f00 100644 --- a/Tests/ParseSwiftTests/ParsePointerCombineTests.swift +++ b/Tests/ParseSwiftTests/ParsePointerCombineTests.swift @@ -113,6 +113,115 @@ class ParsePointerCombineTests: XCTestCase { wait(for: [expectation1], timeout: 20.0) } + + // swiftlint:disable:next cyclomatic_complexity function_body_length + func testFetchAll() throws { + var current = Set() + let expectation1 = XCTestExpectation(description: "Fetch") + + let score = GameScore(points: 10) + let score2 = GameScore(points: 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: -2), to: Date()) + scoreOnServer2.updatedAt = scoreOnServer2.createdAt + scoreOnServer2.ACL = nil + + let response = QueryResponse(results: [scoreOnServer, scoreOnServer2], count: 2) + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().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) + } + + let pointers = try [ + GameScore(objectId: "yarr").toPointer(), + GameScore(objectId: "yolo").toPointer() + ] + + let publisher = pointers.fetchAllPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + + XCTAssertEqual(fetched.count, 2) + guard let firstObject = try? fetched.first(where: {try $0.get().objectId == "yarr"}), + let secondObject = try? fetched.first(where: {try $0.get().objectId == "yolo"}) else { + XCTFail("Should unwrap") + return + } + + switch firstObject { + + case .success(let first): + XCTAssert(first.hasSameObjectId(as: scoreOnServer)) + guard let fetchedCreatedAt = first.createdAt, + let fetchedUpdatedAt = first.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = scoreOnServer.createdAt, + let originalUpdatedAt = scoreOnServer.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) + XCTAssertEqual(fetchedUpdatedAt, originalUpdatedAt) + XCTAssertNil(first.ACL) + XCTAssertEqual(first.points, scoreOnServer.points) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + switch secondObject { + + case .success(let second): + XCTAssert(second.hasSameObjectId(as: scoreOnServer2)) + guard let savedCreatedAt = second.createdAt, + let savedUpdatedAt = second.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = scoreOnServer2.createdAt, + let originalUpdatedAt = scoreOnServer2.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertNil(second.ACL) + XCTAssertEqual(second.points, scoreOnServer2.points) + case .failure(let error): + XCTFail(error.localizedDescription) + } + }) + publisher.store(in: ¤t) + + wait(for: [expectation1], timeout: 20.0) + } } #endif diff --git a/Tests/ParseSwiftTests/ParsePointerTests.swift b/Tests/ParseSwiftTests/ParsePointerTests.swift index 3a1d60e58..772d551a1 100644 --- a/Tests/ParseSwiftTests/ParsePointerTests.swift +++ b/Tests/ParseSwiftTests/ParsePointerTests.swift @@ -199,6 +199,29 @@ class ParsePointerTests: XCTestCase { XCTFail(error.localizedDescription) } + do { + let fetched = try await pointer.fetch( + includeKeys: [], + options: [] + ) + XCTAssert(fetched.hasSameObjectId(as: scoreOnServer)) + guard let fetchedCreatedAt = fetched.createdAt, + let fetchedUpdatedAt = fetched.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = scoreOnServer.createdAt, + let originalUpdatedAt = scoreOnServer.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) + XCTAssertEqual(fetchedUpdatedAt, originalUpdatedAt) + XCTAssertNil(fetched.ACL) + } catch { + XCTFail(error.localizedDescription) + } + do { let fetched = try await pointer.fetch(options: [.usePrimaryKey]) XCTAssert(fetched.hasSameObjectId(as: scoreOnServer)) @@ -371,4 +394,98 @@ class ParsePointerTests: XCTestCase { } self.fetchAsync(score: pointer, scoreOnServer: scoreOnServer, callbackQueue: .main) } + + @MainActor + func testFetchAll() async throws { // swiftlint:disable:this function_body_length + let score = GameScore(points: 10) + let score2 = GameScore(points: 20) + + var scoreOnServer = score + scoreOnServer.objectId = "yarr" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt + scoreOnServer.ACL = nil + let scoreOnServerImmutable: GameScore! + let scoreOnServer2Immutable: GameScore! + var scoreOnServer2 = score2 + scoreOnServer2.objectId = "yolo" + scoreOnServer2.createdAt = Calendar.current.date(byAdding: .init(day: -2), to: Date()) + scoreOnServer2.updatedAt = scoreOnServer2.createdAt + scoreOnServer2.ACL = nil + + let response = QueryResponse(results: [scoreOnServer, scoreOnServer2], count: 2) + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(response) + // Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().encode(scoreOnServer) + scoreOnServerImmutable = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded1) + let encoded2 = try ParseCoding.jsonEncoder().encode(scoreOnServer2) + scoreOnServer2Immutable = 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) + } + + let fetched = try await [ + GameScore(objectId: "yarr").toPointer(), + GameScore(objectId: "yolo").toPointer() + ].fetchAll() + + XCTAssertEqual(fetched.count, 2) + guard let firstObject = try? fetched.first(where: {try $0.get().objectId == "yarr"}), + let secondObject = try? fetched.first(where: {try $0.get().objectId == "yolo"}) else { + XCTFail("Should unwrap") + return + } + + switch firstObject { + + case .success(let first): + XCTAssert(first.hasSameObjectId(as: scoreOnServerImmutable)) + guard let fetchedCreatedAt = first.createdAt, + let fetchedUpdatedAt = first.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = scoreOnServerImmutable.createdAt, + let originalUpdatedAt = scoreOnServerImmutable.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) + XCTAssertEqual(fetchedUpdatedAt, originalUpdatedAt) + XCTAssertNil(first.ACL) + XCTAssertEqual(first.points, scoreOnServerImmutable.points) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + switch secondObject { + + case .success(let second): + XCTAssert(second.hasSameObjectId(as: scoreOnServer2Immutable)) + guard let savedCreatedAt = second.createdAt, + let savedUpdatedAt = second.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = scoreOnServer2Immutable.createdAt, + let originalUpdatedAt = scoreOnServer2Immutable.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertNil(second.ACL) + XCTAssertEqual(second.points, scoreOnServer2Immutable.points) + case .failure(let error): + XCTFail(error.localizedDescription) + } + } + }