diff --git a/.codecov.yml b/.codecov.yml index 8f078c0e1..5148b8498 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -5,10 +5,10 @@ coverage: status: patch: default: - target: 67 + target: auto changes: false project: default: - target: 74 + target: 76 comment: require_changes: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49373b802..94572d073 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,6 @@ on: branches: '*' env: CI_XCODE_VER: '/Applications/Xcode_11.7.app/Contents/Developer' - CI_XCODE_VER_12: '/Applications/Xcode_12.2.app/Contents/Developer' jobs: xcode-test-ios: @@ -98,19 +97,15 @@ jobs: uses: actions/cache@v2 with: path: vendor/bundle - key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} + key: ${{ runner.os }}-gem-v1-${{ hashFiles('**/Gemfile.lock') }} restore-keys: | - ${{ runner.os }}-gem- + ${{ runner.os }}-gem-v1 - name: Install Bundle run: | bundle config path vendor/bundle bundle install - env: - DEVELOPER_DIR: ${{ env.CI_XCODE_VER_12 }} - name: Create Jazzy Docs run: ./Scripts/jazzy.sh - env: - DEVELOPER_DIR: ${{ env.CI_XCODE_VER_12 }} - name: Deploy Jazzy Docs if: github.ref == 'refs/heads/main' uses: peaceiris/actions-gh-pages@v3 diff --git a/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift index 84fab153a..5b2ca0da4 100644 --- a/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift @@ -216,12 +216,15 @@ let score2ToFetch = GameScore(objectId: score2ForFetchedLater?.objectId) } } +var fetchedScore: GameScore! + //: Synchronously fetchAll GameScore's based on it's objectId's alone. do { let fetchedScores = try [scoreToFetch, score2ToFetch].fetchAll() fetchedScores.forEach { result in switch result { case .success(let fetched): + fetchedScore = fetched print("Successfully fetched: \(fetched)") case .failure(let error): print("Error fetching: \(error)") diff --git a/ParseSwift.playground/Pages/12 - Roles and Relations.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/12 - Roles and Relations.xcplaygroundpage/Contents.swift new file mode 100644 index 000000000..68fdf7dd0 --- /dev/null +++ b/ParseSwift.playground/Pages/12 - Roles and Relations.xcplaygroundpage/Contents.swift @@ -0,0 +1,215 @@ +//: [Previous](@previous) + +import PlaygroundSupport +import Foundation +import ParseSwift + +PlaygroundPage.current.needsIndefiniteExecution = true +initializeParse() + +struct User: ParseUser { + //: These are required for ParseObject + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + //: These are required for ParseUser + var username: String? + var email: String? + var password: String? + var authData: [String: [String: String]?]? + + //: Your custom keys + var customKey: String? +} + +struct Role: ParseRole { + + // required by ParseObject + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + // provided by Role + var name: String + + init() { + self.name = "" + } +} + +//: Roles can provide additional access/security to your apps. + +//: This variable will store the saved role +var savedRole: Role? + +//: Now we will create the Role. +if let currentUser = User.current { + + //: Every Role requires an ACL that can't be changed after saving. + var acl = ParseACL() + acl.setReadAccess(user: currentUser, value: true) + acl.setWriteAccess(user: currentUser, value: true) + + do { + //: Create the actual Role with a name and ACL. + var adminRole = try Role(name: "Administrator", acl: acl) + adminRole.save { result in + switch result { + case .success(let saved): + print("The role saved successfully: \(saved)") + print("Check your \"Role\" class in Parse Dashboard.") + + //: Store the saved role so we can use it later... + savedRole = saved + + case .failure(let error): + print("Error saving role: \(error)") + } + } + } catch { + print("Error: \(error)") + } +} + +//: Lets check to see if our Role has saved +if savedRole != nil { + print("We have a saved Role") +} + +//: Users can be added to our previously saved Role. +do { + //: `ParseRoles` have `ParseRelations` that relate them either `ParseUser` and `ParseRole` objects. + //: The `ParseUser` relations can be accessed using `users`. We can then add `ParseUser`'s to the relation. + try savedRole!.users.add([User.current!]).save { result in + switch result { + case .success(let saved): + print("The role saved successfully: \(saved)") + print("Check \"users\" field in your \"Role\" class in Parse Dashboard.") + + case .failure(let error): + print("Error saving role: \(error)") + } + } + +} catch { + print("Error: \(error)") +} + +//: To retrieve the users who are all Administrators, we need to query the relation. +let templateUser = User() +savedRole!.users.query(templateUser).find { result in + switch result { + case .success(let relatedUsers): + print("The following users are part of the \"\(savedRole!.name) role: \(relatedUsers)") + + case .failure(let error): + print("Error saving role: \(error)") + } +} + +//: Of course, you can remove users from the roles as well. +try savedRole!.users.remove([User.current!]).save { result in + switch result { + case .success(let saved): + print("The role removed successfully: \(saved)") + print("Check \"users\" field in your \"Role\" class in Parse Dashboard.") + + case .failure(let error): + print("Error saving role: \(error)") + } +} + +//: Additional roles can be created and tied to already created roles. Lets create a "Member" role. + +//: This variable will store the saved role +var savedRoleModerator: Role? + +//: We need another ACL +var acl = ParseACL() +acl.setReadAccess(user: User.current!, value: true) +acl.setWriteAccess(user: User.current!, value: false) + +do { + //: Create the actual Role with a name and ACL. + var memberRole = try Role(name: "Member", acl: acl) + memberRole.save { result in + switch result { + case .success(let saved): + print("The role saved successfully: \(saved)") + print("Check your \"Role\" class in Parse Dashboard.") + + //: Store the saved role so we can use it later... + savedRoleModerator = saved + + case .failure(let error): + print("Error saving role: \(error)") + } + } +} catch { + print("Error: \(error)") +} + +//: Lets check to see if our Role has saved +if savedRoleModerator != nil { + print("We have a saved Role") +} + +//: Roles can be added to our previously saved Role. +do { + //: `ParseRoles` have `ParseRelations` that relate them either `ParseUser` and `ParseRole` objects. + //: The `ParseUser` relations can be accessed using `users`. We can then add `ParseUser`'s to the relation. + try savedRole!.roles.add([savedRoleModerator!]).save { result in + switch result { + case .success(let saved): + print("The role saved successfully: \(saved)") + print("Check \"roles\" field in your \"Role\" class in Parse Dashboard.") + + case .failure(let error): + print("Error saving role: \(error)") + } + } + +} catch { + print("Error: \(error)") +} + +//: To retrieve the users who are all Administrators, we need to query the relation. +//: This time we will use a helper query from `ParseRole`. +savedRole!.queryRoles?.find { result in + switch result { + case .success(let relatedRoles): + print("The following roles are part of the \"\(savedRole!.name) role: \(relatedRoles)") + + case .failure(let error): + print("Error saving role: \(error)") + } +} + +//: Of course, you can remove users from the roles as well. +try savedRole!.roles.remove([savedRoleModerator!]).save { result in + switch result { + case .success(let saved): + print("The role removed successfully: \(saved)") + print("Check the \"roles\" field in your \"Role\" class in Parse Dashboard.") + + case .failure(let error): + print("Error saving role: \(error)") + } +} + +//: All `ParseObjects` have a `ParseRelation` attribute that be used on instances. +//: For example, the User has: +let relation = User.current!.relation + +//: Example: relation.add(<#T##users: [ParseUser]##[ParseUser]#>) +//: Example: relation.remove(<#T##key: String##String#>, objects: <#T##[ParseObject]#>) + +//: Using this relation, you can create many-to-many relationships with other `ParseObjecs`, +//: similar to `users` and `roles`. + +PlaygroundPage.current.finishExecution() + +//: [Next](@next) diff --git a/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift new file mode 100644 index 000000000..2e6242cf0 --- /dev/null +++ b/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift @@ -0,0 +1,70 @@ +//: [Previous](@previous) + +import PlaygroundSupport +import Foundation +import ParseSwift + +PlaygroundPage.current.needsIndefiniteExecution = true +initializeParse() + +//: Some ValueTypes ParseObject's we will use... +struct GameScore: ParseObject { + //: Those are required for Object + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + //: Your own properties + var score: Int = 0 + + //custom initializer + init(score: Int) { + self.score = score + } + + init(objectId: String?) { + self.objectId = objectId + } +} + +//: You can have the server do operations on your ParseObjects for you. + +//: First lets create another GameScore +let savedScore: GameScore! +do { + savedScore = try GameScore(score: 102).save() +} catch { + savedScore = nil + fatalError("Error saving: \(error)") +} + +//: Then we will increment the score. +let incrementOperation = savedScore + .operation.increment("score", by: 1) + +incrementOperation.save { result in + switch result { + case .success: + print("Original score: \(savedScore). Check the new score on Parse Dashboard.") + case .failure(let error): + assertionFailure("Error saving: \(error)") + } +} + +//: You can increment the score again syncronously. +do { + _ = try incrementOperation.save() + print("Original score: \(savedScore). Check the new score on Parse Dashboard.") +} catch { + print(error) +} + +//: There are other operations: add/remove/delete objects from `ParseObjects`. +//: In fact, the `users` and `roles` relations from `ParseRoles` used the add/remove operations. +let operations = savedScore.operation + +//: Example: operations.add("hello", objects: ["test"]) + +PlaygroundPage.current.finishExecution() +//: [Next](@next) diff --git a/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift index 7feef8b30..5a3f7da5b 100644 --- a/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift @@ -21,7 +21,7 @@ score.score = 200 try score.save() let afterDate = Date().addingTimeInterval(-300) -let query = GameScore.query("score" > 100, "createdAt" > afterDate) +var query = GameScore.query("score" > 100, "createdAt" > afterDate) // Query asynchronously (preferred way) - Performs work on background // queue and returns to designated on designated callbackQueue. diff --git a/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift index 1e31ca843..7829d7d01 100644 --- a/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift @@ -14,10 +14,11 @@ struct User: ParseUser { var updatedAt: Date? var ACL: ParseACL? - // These are required for ParseUser + //: These are required for ParseUser var username: String? var email: String? var password: String? + var authData: [String: [String: String]?]? //: Your custom keys var customKey: String? diff --git a/ParseSwift.playground/contents.xcplayground b/ParseSwift.playground/contents.xcplayground index f0c16eaee..67dd1efef 100644 --- a/ParseSwift.playground/contents.xcplayground +++ b/ParseSwift.playground/contents.xcplayground @@ -12,5 +12,7 @@ + + \ No newline at end of file diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index 6786675cd..c2754bc2e 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -32,10 +32,10 @@ 700395BB25A1470F0052CB31 /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 700395B925A1470F0052CB31 /* Subscription.swift */; }; 700395BC25A1470F0052CB31 /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 700395B925A1470F0052CB31 /* Subscription.swift */; }; 700395BD25A1470F0052CB31 /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 700395B925A1470F0052CB31 /* Subscription.swift */; }; - 700395D125A147BE0052CB31 /* SubscriptionHandlable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 700395D025A147BE0052CB31 /* SubscriptionHandlable.swift */; }; - 700395D225A147BE0052CB31 /* SubscriptionHandlable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 700395D025A147BE0052CB31 /* SubscriptionHandlable.swift */; }; - 700395D325A147BE0052CB31 /* SubscriptionHandlable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 700395D025A147BE0052CB31 /* SubscriptionHandlable.swift */; }; - 700395D425A147BE0052CB31 /* SubscriptionHandlable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 700395D025A147BE0052CB31 /* SubscriptionHandlable.swift */; }; + 700395D125A147BE0052CB31 /* ParseSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 700395D025A147BE0052CB31 /* ParseSubscription.swift */; }; + 700395D225A147BE0052CB31 /* ParseSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 700395D025A147BE0052CB31 /* ParseSubscription.swift */; }; + 700395D325A147BE0052CB31 /* ParseSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 700395D025A147BE0052CB31 /* ParseSubscription.swift */; }; + 700395D425A147BE0052CB31 /* ParseSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 700395D025A147BE0052CB31 /* ParseSubscription.swift */; }; 700395F225A171320052CB31 /* LiveQueryable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 700395F125A171320052CB31 /* LiveQueryable.swift */; }; 700395F325A171320052CB31 /* LiveQueryable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 700395F125A171320052CB31 /* LiveQueryable.swift */; }; 700395F425A171320052CB31 /* LiveQueryable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 700395F125A171320052CB31 /* LiveQueryable.swift */; }; @@ -59,6 +59,13 @@ 7003972B25A3B0140052CB31 /* ParseURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7003972925A3B0130052CB31 /* ParseURLSessionDelegate.swift */; }; 7003972C25A3B0140052CB31 /* ParseURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7003972925A3B0130052CB31 /* ParseURLSessionDelegate.swift */; }; 7003972D25A3B0140052CB31 /* ParseURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7003972925A3B0130052CB31 /* ParseURLSessionDelegate.swift */; }; + 7004C22025B63C7A005E0AD9 /* ParseRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7004C21F25B63C7A005E0AD9 /* ParseRelation.swift */; }; + 7004C22125B63C7A005E0AD9 /* ParseRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7004C21F25B63C7A005E0AD9 /* ParseRelation.swift */; }; + 7004C22225B63C7A005E0AD9 /* ParseRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7004C21F25B63C7A005E0AD9 /* ParseRelation.swift */; }; + 7004C22325B63C7A005E0AD9 /* ParseRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7004C21F25B63C7A005E0AD9 /* ParseRelation.swift */; }; + 7004C24D25B69207005E0AD9 /* ParseRoleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7004C22D25B69077005E0AD9 /* ParseRoleTests.swift */; }; + 7004C25725B6920A005E0AD9 /* ParseRoleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7004C22D25B69077005E0AD9 /* ParseRoleTests.swift */; }; + 7004C26125B6920B005E0AD9 /* ParseRoleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7004C22D25B69077005E0AD9 /* ParseRoleTests.swift */; }; 70110D52250680140091CC1D /* ParseConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70110D51250680140091CC1D /* ParseConstants.swift */; }; 70110D53250680140091CC1D /* ParseConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70110D51250680140091CC1D /* ParseConstants.swift */; }; 70110D54250680140091CC1D /* ParseConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70110D51250680140091CC1D /* ParseConstants.swift */; }; @@ -106,10 +113,10 @@ 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 */; }; - 707A3BF125B0A4F0000D215C /* ParseAuthenticatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3BF025B0A4F0000D215C /* ParseAuthenticatable.swift */; }; - 707A3BF225B0A4F0000D215C /* ParseAuthenticatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3BF025B0A4F0000D215C /* ParseAuthenticatable.swift */; }; - 707A3BF325B0A4F0000D215C /* ParseAuthenticatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3BF025B0A4F0000D215C /* ParseAuthenticatable.swift */; }; - 707A3BF425B0A4F0000D215C /* ParseAuthenticatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3BF025B0A4F0000D215C /* ParseAuthenticatable.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 */; }; + 707A3BF425B0A4F0000D215C /* ParseAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3BF025B0A4F0000D215C /* ParseAuthentication.swift */; }; 707A3C1125B0A8E8000D215C /* ParseAnonymous.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3C1025B0A8E8000D215C /* ParseAnonymous.swift */; }; 707A3C1225B0A8E8000D215C /* ParseAnonymous.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3C1025B0A8E8000D215C /* ParseAnonymous.swift */; }; 707A3C1325B0A8E8000D215C /* ParseAnonymous.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3C1025B0A8E8000D215C /* ParseAnonymous.swift */; }; @@ -135,7 +142,7 @@ 709B98542556ECAA00507778 /* ParseInstallationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70110D5B2506ED0E0091CC1D /* ParseInstallationTests.swift */; }; 709B98552556ECAA00507778 /* ParseQueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C7DC1F24D20F180050419B /* ParseQueryTests.swift */; }; 709B98562556ECAA00507778 /* ParseObjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 911DB13524C4FC100027F3C7 /* ParseObjectTests.swift */; }; - 709B98572556ECAA00507778 /* ACLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9194657724F16E330070296B /* ACLTests.swift */; }; + 709B98572556ECAA00507778 /* ParseACLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9194657724F16E330070296B /* ParseACLTests.swift */; }; 709B98582556ECAA00507778 /* AnyEncodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FFF552B2217E729007C3B4E /* AnyEncodableTests.swift */; }; 709B98592556ECAA00507778 /* MockURLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 911DB12B24C3F7720027F3C7 /* MockURLResponse.swift */; }; 709B985A2556ECAA00507778 /* ParseObjectBatchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C7DC2024D20F190050419B /* ParseObjectBatchTests.swift */; }; @@ -164,6 +171,21 @@ 70C5504625B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5504525B40D5200B5DBC2 /* ParseSessionTests.swift */; }; 70C5504725B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5504525B40D5200B5DBC2 /* ParseSessionTests.swift */; }; 70C5504825B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5504525B40D5200B5DBC2 /* ParseSessionTests.swift */; }; + 70C5507725B49D3A00B5DBC2 /* ParseRole.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5507625B49D3A00B5DBC2 /* ParseRole.swift */; }; + 70C5507825B49D3A00B5DBC2 /* ParseRole.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5507625B49D3A00B5DBC2 /* ParseRole.swift */; }; + 70C5507925B49D3A00B5DBC2 /* ParseRole.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5507625B49D3A00B5DBC2 /* ParseRole.swift */; }; + 70C5507A25B49D3A00B5DBC2 /* ParseRole.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5507625B49D3A00B5DBC2 /* ParseRole.swift */; }; + 70C5508525B4A68700B5DBC2 /* ParseOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5508425B4A68700B5DBC2 /* ParseOperationTests.swift */; }; + 70C5508625B4A68700B5DBC2 /* ParseOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5508425B4A68700B5DBC2 /* ParseOperationTests.swift */; }; + 70C5508725B4A68700B5DBC2 /* ParseOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5508425B4A68700B5DBC2 /* ParseOperationTests.swift */; }; + 70C5509225B4A99100B5DBC2 /* AddRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5509125B4A99100B5DBC2 /* AddRelation.swift */; }; + 70C5509325B4A99100B5DBC2 /* AddRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5509125B4A99100B5DBC2 /* AddRelation.swift */; }; + 70C5509425B4A99100B5DBC2 /* AddRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5509125B4A99100B5DBC2 /* AddRelation.swift */; }; + 70C5509525B4A99100B5DBC2 /* AddRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5509125B4A99100B5DBC2 /* AddRelation.swift */; }; + 70C550A025B4A9F600B5DBC2 /* RemoveRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5509F25B4A9F600B5DBC2 /* RemoveRelation.swift */; }; + 70C550A125B4A9F600B5DBC2 /* RemoveRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5509F25B4A9F600B5DBC2 /* RemoveRelation.swift */; }; + 70C550A225B4A9F600B5DBC2 /* RemoveRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5509F25B4A9F600B5DBC2 /* RemoveRelation.swift */; }; + 70C550A325B4A9F600B5DBC2 /* RemoveRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5509F25B4A9F600B5DBC2 /* RemoveRelation.swift */; }; 70C5655925AA147B00BDD57F /* ParseLiveQueryConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5655825AA147B00BDD57F /* ParseLiveQueryConstants.swift */; }; 70C5655A25AA147B00BDD57F /* ParseLiveQueryConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5655825AA147B00BDD57F /* ParseLiveQueryConstants.swift */; }; 70C5655B25AA147B00BDD57F /* ParseLiveQueryConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5655825AA147B00BDD57F /* ParseLiveQueryConstants.swift */; }; @@ -172,11 +194,14 @@ 70C7DC2124D20F190050419B /* ParseQueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C7DC1F24D20F180050419B /* ParseQueryTests.swift */; }; 70C7DC2224D20F190050419B /* ParseObjectBatchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C7DC2024D20F190050419B /* ParseObjectBatchTests.swift */; }; 70CE1D892545BF730018D572 /* ParsePointerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70CE1D882545BF730018D572 /* ParsePointerTests.swift */; }; + 70D1BD8725B8C37200A42E7C /* ParseRelationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70D1BD8625B8C37200A42E7C /* ParseRelationTests.swift */; }; + 70D1BD8825B8C37200A42E7C /* ParseRelationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70D1BD8625B8C37200A42E7C /* ParseRelationTests.swift */; }; + 70D1BD8925B8C37200A42E7C /* ParseRelationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70D1BD8625B8C37200A42E7C /* ParseRelationTests.swift */; }; 70F2E255254F247000B2EA5C /* ParseSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AFDA7121F26D9A5002AE4FC /* ParseSwift.framework */; }; 70F2E2B3254F283000B2EA5C /* ParseUserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C7DC1D24D20E530050419B /* ParseUserTests.swift */; }; 70F2E2B4254F283000B2EA5C /* ParseQueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C7DC1F24D20F180050419B /* ParseQueryTests.swift */; }; 70F2E2B5254F283000B2EA5C /* ParseEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F971F4F524DE381A006CB79B /* ParseEncoderTests.swift */; }; - 70F2E2B6254F283000B2EA5C /* ACLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9194657724F16E330070296B /* ACLTests.swift */; }; + 70F2E2B6254F283000B2EA5C /* ParseACLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9194657724F16E330070296B /* ParseACLTests.swift */; }; 70F2E2B7254F283000B2EA5C /* ParsePointerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70CE1D882545BF730018D572 /* ParsePointerTests.swift */; }; 70F2E2B8254F283000B2EA5C /* AnyEncodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FFF552B2217E729007C3B4E /* AnyEncodableTests.swift */; }; 70F2E2B9254F283000B2EA5C /* KeychainStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA8076E1F794C1C008CD551 /* KeychainStoreTests.swift */; }; @@ -207,7 +232,7 @@ 91678706259BC5D400BB5B4E /* ParseCloudTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 916786EF259BC59600BB5B4E /* ParseCloudTests.swift */; }; 91678710259BC5D600BB5B4E /* ParseCloudTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 916786EF259BC59600BB5B4E /* ParseCloudTests.swift */; }; 9167871A259BC5D600BB5B4E /* ParseCloudTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 916786EF259BC59600BB5B4E /* ParseCloudTests.swift */; }; - 9194657824F16E330070296B /* ACLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9194657724F16E330070296B /* ACLTests.swift */; }; + 9194657824F16E330070296B /* ParseACLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9194657724F16E330070296B /* ParseACLTests.swift */; }; F971F4F624DE381A006CB79B /* ParseEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F971F4F524DE381A006CB79B /* ParseEncoderTests.swift */; }; F97B45CE24D9C6F200F4A88B /* ParseCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45B424D9C6F200F4A88B /* ParseCoding.swift */; }; F97B45CF24D9C6F200F4A88B /* ParseCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45B424D9C6F200F4A88B /* ParseCoding.swift */; }; @@ -289,10 +314,10 @@ F97B461F24D9C6F200F4A88B /* ParseStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45CC24D9C6F200F4A88B /* ParseStorage.swift */; }; F97B462024D9C6F200F4A88B /* ParseStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45CC24D9C6F200F4A88B /* ParseStorage.swift */; }; F97B462124D9C6F200F4A88B /* ParseStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45CC24D9C6F200F4A88B /* ParseStorage.swift */; }; - F97B462224D9C6F200F4A88B /* PrimitiveObjectStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45CD24D9C6F200F4A88B /* PrimitiveObjectStore.swift */; }; - F97B462324D9C6F200F4A88B /* PrimitiveObjectStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45CD24D9C6F200F4A88B /* PrimitiveObjectStore.swift */; }; - F97B462424D9C6F200F4A88B /* PrimitiveObjectStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45CD24D9C6F200F4A88B /* PrimitiveObjectStore.swift */; }; - F97B462524D9C6F200F4A88B /* PrimitiveObjectStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45CD24D9C6F200F4A88B /* PrimitiveObjectStore.swift */; }; + F97B462224D9C6F200F4A88B /* ParseKeyValueStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45CD24D9C6F200F4A88B /* ParseKeyValueStore.swift */; }; + F97B462324D9C6F200F4A88B /* ParseKeyValueStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45CD24D9C6F200F4A88B /* ParseKeyValueStore.swift */; }; + F97B462424D9C6F200F4A88B /* ParseKeyValueStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45CD24D9C6F200F4A88B /* ParseKeyValueStore.swift */; }; + F97B462524D9C6F200F4A88B /* ParseKeyValueStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45CD24D9C6F200F4A88B /* ParseKeyValueStore.swift */; }; F97B462724D9C72700F4A88B /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B462624D9C72700F4A88B /* API.swift */; }; F97B462824D9C72700F4A88B /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B462624D9C72700F4A88B /* API.swift */; }; F97B462924D9C72700F4A88B /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B462624D9C72700F4A88B /* API.swift */; }; @@ -313,30 +338,30 @@ F97B463C24D9C74400F4A88B /* API+Commands.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B462E24D9C74400F4A88B /* API+Commands.swift */; }; F97B463D24D9C74400F4A88B /* API+Commands.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B462E24D9C74400F4A88B /* API+Commands.swift */; }; F97B463E24D9C74400F4A88B /* API+Commands.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B462E24D9C74400F4A88B /* API+Commands.swift */; }; - F97B464624D9C78B00F4A88B /* ParseMutationContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464024D9C78B00F4A88B /* ParseMutationContainer.swift */; }; - F97B464724D9C78B00F4A88B /* ParseMutationContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464024D9C78B00F4A88B /* ParseMutationContainer.swift */; }; - F97B464824D9C78B00F4A88B /* ParseMutationContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464024D9C78B00F4A88B /* ParseMutationContainer.swift */; }; - F97B464924D9C78B00F4A88B /* ParseMutationContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464024D9C78B00F4A88B /* ParseMutationContainer.swift */; }; - F97B464A24D9C78B00F4A88B /* DeleteOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464124D9C78B00F4A88B /* DeleteOperation.swift */; }; - F97B464B24D9C78B00F4A88B /* DeleteOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464124D9C78B00F4A88B /* DeleteOperation.swift */; }; - F97B464C24D9C78B00F4A88B /* DeleteOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464124D9C78B00F4A88B /* DeleteOperation.swift */; }; - F97B464D24D9C78B00F4A88B /* DeleteOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464124D9C78B00F4A88B /* DeleteOperation.swift */; }; - F97B464E24D9C78B00F4A88B /* AddOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464224D9C78B00F4A88B /* AddOperation.swift */; }; - F97B464F24D9C78B00F4A88B /* AddOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464224D9C78B00F4A88B /* AddOperation.swift */; }; - F97B465024D9C78B00F4A88B /* AddOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464224D9C78B00F4A88B /* AddOperation.swift */; }; - F97B465124D9C78C00F4A88B /* AddOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464224D9C78B00F4A88B /* AddOperation.swift */; }; - F97B465224D9C78C00F4A88B /* AddUniqueOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464324D9C78B00F4A88B /* AddUniqueOperation.swift */; }; - F97B465324D9C78C00F4A88B /* AddUniqueOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464324D9C78B00F4A88B /* AddUniqueOperation.swift */; }; - F97B465424D9C78C00F4A88B /* AddUniqueOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464324D9C78B00F4A88B /* AddUniqueOperation.swift */; }; - F97B465524D9C78C00F4A88B /* AddUniqueOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464324D9C78B00F4A88B /* AddUniqueOperation.swift */; }; - F97B465624D9C78C00F4A88B /* RemoveOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464424D9C78B00F4A88B /* RemoveOperation.swift */; }; - F97B465724D9C78C00F4A88B /* RemoveOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464424D9C78B00F4A88B /* RemoveOperation.swift */; }; - F97B465824D9C78C00F4A88B /* RemoveOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464424D9C78B00F4A88B /* RemoveOperation.swift */; }; - F97B465924D9C78C00F4A88B /* RemoveOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464424D9C78B00F4A88B /* RemoveOperation.swift */; }; - F97B465A24D9C78C00F4A88B /* IncrementOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464524D9C78B00F4A88B /* IncrementOperation.swift */; }; - F97B465B24D9C78C00F4A88B /* IncrementOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464524D9C78B00F4A88B /* IncrementOperation.swift */; }; - F97B465C24D9C78C00F4A88B /* IncrementOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464524D9C78B00F4A88B /* IncrementOperation.swift */; }; - F97B465D24D9C78C00F4A88B /* IncrementOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464524D9C78B00F4A88B /* IncrementOperation.swift */; }; + F97B464624D9C78B00F4A88B /* ParseOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464024D9C78B00F4A88B /* ParseOperation.swift */; }; + F97B464724D9C78B00F4A88B /* ParseOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464024D9C78B00F4A88B /* ParseOperation.swift */; }; + F97B464824D9C78B00F4A88B /* ParseOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464024D9C78B00F4A88B /* ParseOperation.swift */; }; + F97B464924D9C78B00F4A88B /* ParseOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464024D9C78B00F4A88B /* ParseOperation.swift */; }; + F97B464A24D9C78B00F4A88B /* Delete.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464124D9C78B00F4A88B /* Delete.swift */; }; + F97B464B24D9C78B00F4A88B /* Delete.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464124D9C78B00F4A88B /* Delete.swift */; }; + F97B464C24D9C78B00F4A88B /* Delete.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464124D9C78B00F4A88B /* Delete.swift */; }; + F97B464D24D9C78B00F4A88B /* Delete.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464124D9C78B00F4A88B /* Delete.swift */; }; + F97B464E24D9C78B00F4A88B /* Add.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464224D9C78B00F4A88B /* Add.swift */; }; + F97B464F24D9C78B00F4A88B /* Add.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464224D9C78B00F4A88B /* Add.swift */; }; + F97B465024D9C78B00F4A88B /* Add.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464224D9C78B00F4A88B /* Add.swift */; }; + F97B465124D9C78C00F4A88B /* Add.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464224D9C78B00F4A88B /* Add.swift */; }; + F97B465224D9C78C00F4A88B /* AddUnique.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464324D9C78B00F4A88B /* AddUnique.swift */; }; + F97B465324D9C78C00F4A88B /* AddUnique.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464324D9C78B00F4A88B /* AddUnique.swift */; }; + F97B465424D9C78C00F4A88B /* AddUnique.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464324D9C78B00F4A88B /* AddUnique.swift */; }; + F97B465524D9C78C00F4A88B /* AddUnique.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464324D9C78B00F4A88B /* AddUnique.swift */; }; + F97B465624D9C78C00F4A88B /* Remove.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464424D9C78B00F4A88B /* Remove.swift */; }; + F97B465724D9C78C00F4A88B /* Remove.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464424D9C78B00F4A88B /* Remove.swift */; }; + F97B465824D9C78C00F4A88B /* Remove.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464424D9C78B00F4A88B /* Remove.swift */; }; + F97B465924D9C78C00F4A88B /* Remove.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464424D9C78B00F4A88B /* Remove.swift */; }; + F97B465A24D9C78C00F4A88B /* Increment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464524D9C78B00F4A88B /* Increment.swift */; }; + F97B465B24D9C78C00F4A88B /* Increment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464524D9C78B00F4A88B /* Increment.swift */; }; + F97B465C24D9C78C00F4A88B /* Increment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464524D9C78B00F4A88B /* Increment.swift */; }; + F97B465D24D9C78C00F4A88B /* Increment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B464524D9C78B00F4A88B /* Increment.swift */; }; F97B465F24D9C7B500F4A88B /* KeychainStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B465E24D9C7B500F4A88B /* KeychainStore.swift */; }; F97B466024D9C7B500F4A88B /* KeychainStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B465E24D9C7B500F4A88B /* KeychainStore.swift */; }; F97B466124D9C7B500F4A88B /* KeychainStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B465E24D9C7B500F4A88B /* KeychainStore.swift */; }; @@ -407,13 +432,15 @@ 7003959425A10DFC0052CB31 /* Messages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Messages.swift; sourceTree = ""; }; 700395A225A119430052CB31 /* Operations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Operations.swift; sourceTree = ""; }; 700395B925A1470F0052CB31 /* Subscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscription.swift; sourceTree = ""; }; - 700395D025A147BE0052CB31 /* SubscriptionHandlable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionHandlable.swift; sourceTree = ""; }; + 700395D025A147BE0052CB31 /* ParseSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseSubscription.swift; sourceTree = ""; }; 700395F125A171320052CB31 /* LiveQueryable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveQueryable.swift; sourceTree = ""; }; 7003960825A184EF0052CB31 /* ParseLiveQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseLiveQuery.swift; sourceTree = ""; }; 7003963A25A288100052CB31 /* ParseLiveQueryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseLiveQueryTests.swift; sourceTree = ""; }; 700396E925A3892D0052CB31 /* LiveQuerySocketDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveQuerySocketDelegate.swift; sourceTree = ""; }; 700396F725A394AE0052CB31 /* ParseLiveQueryDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseLiveQueryDelegate.swift; sourceTree = ""; }; 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 = ""; }; 70110D51250680140091CC1D /* ParseConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseConstants.swift; sourceTree = ""; }; 70110D562506CE890091CC1D /* BaseParseInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseParseInstallation.swift; sourceTree = ""; }; 70110D5B2506ED0E0091CC1D /* ParseInstallationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseInstallationTests.swift; sourceTree = ""; }; @@ -433,7 +460,7 @@ 705A9A2E25991C1400B3547F /* Fileable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fileable.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 = ""; }; - 707A3BF025B0A4F0000D215C /* ParseAuthenticatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAuthenticatable.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 = ""; }; 708D035125215F9B00646C70 /* Deletable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deletable.swift; sourceTree = ""; }; @@ -447,11 +474,16 @@ 70C5502125B3D8F700B5DBC2 /* ParseAppleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAppleTests.swift; sourceTree = ""; }; 70C5503725B406B800B5DBC2 /* ParseSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseSession.swift; sourceTree = ""; }; 70C5504525B40D5200B5DBC2 /* ParseSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseSessionTests.swift; sourceTree = ""; }; + 70C5507625B49D3A00B5DBC2 /* ParseRole.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseRole.swift; sourceTree = ""; }; + 70C5508425B4A68700B5DBC2 /* ParseOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseOperationTests.swift; sourceTree = ""; }; + 70C5509125B4A99100B5DBC2 /* AddRelation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddRelation.swift; sourceTree = ""; }; + 70C5509F25B4A9F600B5DBC2 /* RemoveRelation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveRelation.swift; sourceTree = ""; }; 70C5655825AA147B00BDD57F /* ParseLiveQueryConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseLiveQueryConstants.swift; sourceTree = ""; }; 70C7DC1D24D20E530050419B /* ParseUserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseUserTests.swift; sourceTree = ""; }; 70C7DC1F24D20F180050419B /* ParseQueryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseQueryTests.swift; sourceTree = ""; }; 70C7DC2024D20F190050419B /* ParseObjectBatchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseObjectBatchTests.swift; sourceTree = ""; }; 70CE1D882545BF730018D572 /* ParsePointerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParsePointerTests.swift; sourceTree = ""; }; + 70D1BD8625B8C37200A42E7C /* ParseRelationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseRelationTests.swift; sourceTree = ""; }; 70F2E23E254F246000B2EA5C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 70F2E250254F247000B2EA5C /* ParseSwiftTestsmacOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ParseSwiftTestsmacOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 70F2E254254F247000B2EA5C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -471,7 +503,7 @@ 9158916A256A07DD0024BE9A /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 916786E1259B7DDA00BB5B4E /* ParseCloud.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseCloud.swift; sourceTree = ""; }; 916786EF259BC59600BB5B4E /* ParseCloudTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseCloudTests.swift; sourceTree = ""; }; - 9194657724F16E330070296B /* ACLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACLTests.swift; sourceTree = ""; }; + 9194657724F16E330070296B /* ParseACLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseACLTests.swift; sourceTree = ""; }; F971F4F524DE381A006CB79B /* ParseEncoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseEncoderTests.swift; sourceTree = ""; }; F97B45B424D9C6F200F4A88B /* ParseCoding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseCoding.swift; sourceTree = ""; }; F97B45B524D9C6F200F4A88B /* AnyDecodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyDecodable.swift; sourceTree = ""; }; @@ -493,18 +525,18 @@ F97B45C724D9C6F200F4A88B /* Savable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Savable.swift; sourceTree = ""; }; F97B45C824D9C6F200F4A88B /* Queryable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Queryable.swift; sourceTree = ""; }; F97B45CC24D9C6F200F4A88B /* ParseStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseStorage.swift; sourceTree = ""; }; - F97B45CD24D9C6F200F4A88B /* PrimitiveObjectStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrimitiveObjectStore.swift; sourceTree = ""; }; + F97B45CD24D9C6F200F4A88B /* ParseKeyValueStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseKeyValueStore.swift; sourceTree = ""; }; F97B462624D9C72700F4A88B /* API.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; F97B462B24D9C74400F4A88B /* BatchUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatchUtils.swift; sourceTree = ""; }; F97B462C24D9C74400F4A88B /* URLSession+extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URLSession+extensions.swift"; sourceTree = ""; }; F97B462D24D9C74400F4A88B /* Responses.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Responses.swift; sourceTree = ""; }; F97B462E24D9C74400F4A88B /* API+Commands.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "API+Commands.swift"; sourceTree = ""; }; - F97B464024D9C78B00F4A88B /* ParseMutationContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseMutationContainer.swift; sourceTree = ""; }; - F97B464124D9C78B00F4A88B /* DeleteOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteOperation.swift; sourceTree = ""; }; - F97B464224D9C78B00F4A88B /* AddOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddOperation.swift; sourceTree = ""; }; - F97B464324D9C78B00F4A88B /* AddUniqueOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddUniqueOperation.swift; sourceTree = ""; }; - F97B464424D9C78B00F4A88B /* RemoveOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoveOperation.swift; sourceTree = ""; }; - F97B464524D9C78B00F4A88B /* IncrementOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IncrementOperation.swift; sourceTree = ""; }; + F97B464024D9C78B00F4A88B /* ParseOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseOperation.swift; sourceTree = ""; }; + F97B464124D9C78B00F4A88B /* Delete.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Delete.swift; sourceTree = ""; }; + F97B464224D9C78B00F4A88B /* Add.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Add.swift; sourceTree = ""; }; + F97B464324D9C78B00F4A88B /* AddUnique.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddUnique.swift; sourceTree = ""; }; + F97B464424D9C78B00F4A88B /* Remove.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Remove.swift; sourceTree = ""; }; + F97B464524D9C78B00F4A88B /* Increment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Increment.swift; sourceTree = ""; }; F97B465E24D9C7B500F4A88B /* KeychainStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainStore.swift; sourceTree = ""; }; F97B466324D9C88600F4A88B /* SecureStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureStorage.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -620,11 +652,11 @@ isa = PBXGroup; children = ( 4AA8076D1F794C1C008CD551 /* Info.plist */, - 9194657724F16E330070296B /* ACLTests.swift */, 911DB12D24C4837E0027F3C7 /* APICommandTests.swift */, 7003957525A0EE770052CB31 /* BatchUtilsTests.swift */, 705726ED2592C91C00F0ADD5 /* HashTests.swift */, 4AA8076E1F794C1C008CD551 /* KeychainStoreTests.swift */, + 9194657724F16E330070296B /* ParseACLTests.swift */, 70A2D86A25B3ADB6001BEB7D /* ParseAnonymousTests.swift */, 70C5502125B3D8F700B5DBC2 /* ParseAppleTests.swift */, 70A2D81E25B36A7D001BEB7D /* ParseAuthenticationTests.swift */, @@ -637,8 +669,11 @@ 7003963A25A288100052CB31 /* ParseLiveQueryTests.swift */, 70C7DC2024D20F190050419B /* ParseObjectBatchTests.swift */, 911DB13524C4FC100027F3C7 /* ParseObjectTests.swift */, + 70C5508425B4A68700B5DBC2 /* ParseOperationTests.swift */, 70CE1D882545BF730018D572 /* ParsePointerTests.swift */, 70C7DC1F24D20F180050419B /* ParseQueryTests.swift */, + 70D1BD8625B8C37200A42E7C /* ParseRelationTests.swift */, + 7004C22D25B69077005E0AD9 /* ParseRoleTests.swift */, 70C5504525B40D5200B5DBC2 /* ParseSessionTests.swift */, 70C7DC1D24D20E530050419B /* ParseUserTests.swift */, 7FFF552A2217E729007C3B4E /* AnyCodableTests */, @@ -694,8 +729,8 @@ F97B45B324D9C6F200F4A88B /* Coding */, 70110D5D250849B30091CC1D /* Internal */, 70510AAA259EE23700FEA700 /* LiveQuery */, - F97B463F24D9C78B00F4A88B /* Mutation Operations */, F97B45C324D9C6F200F4A88B /* Objects */, + F97B463F24D9C78B00F4A88B /* Operations */, 70110D5E25084AF80091CC1D /* Protocols */, F97B45CB24D9C6F200F4A88B /* Storage */, F97B45BA24D9C6F200F4A88B /* Types */, @@ -717,7 +752,7 @@ 700395F125A171320052CB31 /* LiveQueryable.swift */, 700396E925A3892D0052CB31 /* LiveQuerySocketDelegate.swift */, 700396F725A394AE0052CB31 /* ParseLiveQueryDelegate.swift */, - 700395D025A147BE0052CB31 /* SubscriptionHandlable.swift */, + 700395D025A147BE0052CB31 /* ParseSubscription.swift */, ); path = Protocols; sourceTree = ""; @@ -788,7 +823,7 @@ 707A3C1E25B14BAE000D215C /* Protocols */ = { isa = PBXGroup; children = ( - 707A3BF025B0A4F0000D215C /* ParseAuthenticatable.swift */, + 707A3BF025B0A4F0000D215C /* ParseAuthentication.swift */, ); path = Protocols; sourceTree = ""; @@ -891,6 +926,7 @@ F97B45BF24D9C6F200F4A88B /* ParseError.swift */, F97B45C124D9C6F200F4A88B /* ParseFile.swift */, F97B45BC24D9C6F200F4A88B /* ParseGeoPoint.swift */, + 7004C21F25B63C7A005E0AD9 /* ParseRelation.swift */, F97B45BE24D9C6F200F4A88B /* Pointer.swift */, F97B45BB24D9C6F200F4A88B /* Query.swift */, ); @@ -902,6 +938,7 @@ children = ( 70BDA2B2250536FF00FC2237 /* ParseInstallation.swift */, F97B45C624D9C6F200F4A88B /* ParseObject.swift */, + 70C5507625B49D3A00B5DBC2 /* ParseRole.swift */, 70C5503725B406B800B5DBC2 /* ParseSession.swift */, F97B45C424D9C6F200F4A88B /* ParseUser.swift */, ); @@ -927,23 +964,25 @@ F97B465E24D9C7B500F4A88B /* KeychainStore.swift */, 70572670259033A700F0ADD5 /* ParseFileManager.swift */, F97B45CC24D9C6F200F4A88B /* ParseStorage.swift */, - F97B45CD24D9C6F200F4A88B /* PrimitiveObjectStore.swift */, + F97B45CD24D9C6F200F4A88B /* ParseKeyValueStore.swift */, F97B466324D9C88600F4A88B /* SecureStorage.swift */, ); path = Storage; sourceTree = ""; }; - F97B463F24D9C78B00F4A88B /* Mutation Operations */ = { + F97B463F24D9C78B00F4A88B /* Operations */ = { isa = PBXGroup; children = ( - F97B464224D9C78B00F4A88B /* AddOperation.swift */, - F97B464324D9C78B00F4A88B /* AddUniqueOperation.swift */, - F97B464124D9C78B00F4A88B /* DeleteOperation.swift */, - F97B464524D9C78B00F4A88B /* IncrementOperation.swift */, - F97B464024D9C78B00F4A88B /* ParseMutationContainer.swift */, - F97B464424D9C78B00F4A88B /* RemoveOperation.swift */, - ); - path = "Mutation Operations"; + F97B464224D9C78B00F4A88B /* Add.swift */, + 70C5509125B4A99100B5DBC2 /* AddRelation.swift */, + F97B464324D9C78B00F4A88B /* AddUnique.swift */, + F97B464124D9C78B00F4A88B /* Delete.swift */, + F97B464524D9C78B00F4A88B /* Increment.swift */, + F97B464024D9C78B00F4A88B /* ParseOperation.swift */, + F97B464424D9C78B00F4A88B /* Remove.swift */, + 70C5509F25B4A9F600B5DBC2 /* RemoveRelation.swift */, + ); + path = Operations; sourceTree = ""; }; /* End PBXGroup section */ @@ -1347,11 +1386,11 @@ F97B461624D9C6F200F4A88B /* Queryable.swift in Sources */, F97B45DA24D9C6F200F4A88B /* Extensions.swift in Sources */, 70C5503825B406B800B5DBC2 /* ParseSession.swift in Sources */, - 707A3BF125B0A4F0000D215C /* ParseAuthenticatable.swift in Sources */, + 707A3BF125B0A4F0000D215C /* ParseAuthentication.swift in Sources */, F97B465F24D9C7B500F4A88B /* KeychainStore.swift in Sources */, 705726E02592C2A800F0ADD5 /* ParseHash.swift in Sources */, 70110D52250680140091CC1D /* ParseConstants.swift in Sources */, - F97B465224D9C78C00F4A88B /* AddUniqueOperation.swift in Sources */, + F97B465224D9C78C00F4A88B /* AddUnique.swift in Sources */, F97B45D624D9C6F200F4A88B /* ParseEncoder.swift in Sources */, 700395A325A119430052CB31 /* Operations.swift in Sources */, 700395F225A171320052CB31 /* LiveQueryable.swift in Sources */, @@ -1359,23 +1398,26 @@ 70510AAC259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */, F97B461E24D9C6F200F4A88B /* ParseStorage.swift in Sources */, F97B45D224D9C6F200F4A88B /* AnyDecodable.swift in Sources */, + 70C550A025B4A9F600B5DBC2 /* RemoveRelation.swift in Sources */, F97B463B24D9C74400F4A88B /* API+Commands.swift in Sources */, - F97B464624D9C78B00F4A88B /* ParseMutationContainer.swift in Sources */, + F97B464624D9C78B00F4A88B /* ParseOperation.swift in Sources */, 705A9A2F25991C1400B3547F /* Fileable.swift in Sources */, - F97B464A24D9C78B00F4A88B /* DeleteOperation.swift in Sources */, + F97B464A24D9C78B00F4A88B /* Delete.swift in Sources */, 70647E8E259E3375004C1004 /* LocallyIdentifiable.swift in Sources */, F97B460624D9C6F200F4A88B /* ParseUser.swift in Sources */, 700396F825A394AE0052CB31 /* ParseLiveQueryDelegate.swift in Sources */, - F97B465A24D9C78C00F4A88B /* IncrementOperation.swift in Sources */, + F97B465A24D9C78C00F4A88B /* Increment.swift in Sources */, 7003960925A184EF0052CB31 /* ParseLiveQuery.swift in Sources */, F97B45E224D9C6F200F4A88B /* AnyEncodable.swift in Sources */, 700396EA25A3892D0052CB31 /* LiveQuerySocketDelegate.swift in Sources */, 70572671259033A700F0ADD5 /* ParseFileManager.swift in Sources */, 707A3C2025B14BD0000D215C /* ParseApple.swift in Sources */, - F97B462224D9C6F200F4A88B /* PrimitiveObjectStore.swift in Sources */, + F97B462224D9C6F200F4A88B /* ParseKeyValueStore.swift in Sources */, F97B45E624D9C6F200F4A88B /* Query.swift in Sources */, + 70C5509225B4A99100B5DBC2 /* AddRelation.swift in Sources */, 708D035225215F9B00646C70 /* Deletable.swift in Sources */, F97B466424D9C88600F4A88B /* SecureStorage.swift in Sources */, + 7004C22025B63C7A005E0AD9 /* ParseRelation.swift in Sources */, 7003959525A10DFC0052CB31 /* Messages.swift in Sources */, 70C5655925AA147B00BDD57F /* ParseLiveQueryConstants.swift in Sources */, F97B462F24D9C74400F4A88B /* BatchUtils.swift in Sources */, @@ -1384,10 +1426,10 @@ F97B460224D9C6F200F4A88B /* NoBody.swift in Sources */, 700395BA25A1470F0052CB31 /* Subscription.swift in Sources */, 7003972A25A3B0140052CB31 /* ParseURLSessionDelegate.swift in Sources */, - 700395D125A147BE0052CB31 /* SubscriptionHandlable.swift in Sources */, + 700395D125A147BE0052CB31 /* ParseSubscription.swift in Sources */, F97B45F624D9C6F200F4A88B /* ParseError.swift in Sources */, F97B463324D9C74400F4A88B /* URLSession+extensions.swift in Sources */, - F97B464E24D9C78B00F4A88B /* AddOperation.swift in Sources */, + F97B464E24D9C78B00F4A88B /* Add.swift in Sources */, 70BC9890252A5B5C00FF3074 /* Objectable.swift in Sources */, F97B45FE24D9C6F200F4A88B /* ParseFile.swift in Sources */, F97B45EE24D9C6F200F4A88B /* BaseParseUser.swift in Sources */, @@ -1395,7 +1437,7 @@ F97B460E24D9C6F200F4A88B /* ParseObject.swift in Sources */, F97B461224D9C6F200F4A88B /* Savable.swift in Sources */, F97B45CE24D9C6F200F4A88B /* ParseCoding.swift in Sources */, - F97B465624D9C78C00F4A88B /* RemoveOperation.swift in Sources */, + F97B465624D9C78C00F4A88B /* Remove.swift in Sources */, F97B45FA24D9C6F200F4A88B /* ParseACL.swift in Sources */, 70BDA2B3250536FF00FC2237 /* ParseInstallation.swift in Sources */, F97B462724D9C72700F4A88B /* API.swift in Sources */, @@ -1403,6 +1445,7 @@ 707A3C1125B0A8E8000D215C /* ParseAnonymous.swift in Sources */, 70110D572506CE890091CC1D /* BaseParseInstallation.swift in Sources */, F97B45DE24D9C6F200F4A88B /* AnyCodable.swift in Sources */, + 70C5507725B49D3A00B5DBC2 /* ParseRole.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1420,7 +1463,10 @@ 70BC0B33251903D1001556DB /* ParseGeoPointTests.swift in Sources */, 7003957625A0EE770052CB31 /* BatchUtilsTests.swift in Sources */, 705A99F9259807F900B3547F /* ParseFileManagerTests.swift in Sources */, + 70C5508525B4A68700B5DBC2 /* ParseOperationTests.swift in Sources */, + 7004C24D25B69207005E0AD9 /* ParseRoleTests.swift in Sources */, 91678706259BC5D400BB5B4E /* ParseCloudTests.swift in Sources */, + 70D1BD8725B8C37200A42E7C /* ParseRelationTests.swift in Sources */, 7003963B25A288100052CB31 /* ParseLiveQueryTests.swift in Sources */, 7FFF552E2217E72A007C3B4E /* AnyEncodableTests.swift in Sources */, 7FFF55302217E72A007C3B4E /* AnyDecodableTests.swift in Sources */, @@ -1430,7 +1476,7 @@ 70C5502225B3D8F700B5DBC2 /* ParseAppleTests.swift in Sources */, F971F4F624DE381A006CB79B /* ParseEncoderTests.swift in Sources */, 70C7DC2124D20F190050419B /* ParseQueryTests.swift in Sources */, - 9194657824F16E330070296B /* ACLTests.swift in Sources */, + 9194657824F16E330070296B /* ParseACLTests.swift in Sources */, 70A2D86B25B3ADB6001BEB7D /* ParseAnonymousTests.swift in Sources */, 70C7DC1E24D20E530050419B /* ParseUserTests.swift in Sources */, 70A2D81F25B36A7D001BEB7D /* ParseAuthenticationTests.swift in Sources */, @@ -1448,11 +1494,11 @@ F97B461724D9C6F200F4A88B /* Queryable.swift in Sources */, F97B45DB24D9C6F200F4A88B /* Extensions.swift in Sources */, 70C5503925B406B800B5DBC2 /* ParseSession.swift in Sources */, - 707A3BF225B0A4F0000D215C /* ParseAuthenticatable.swift in Sources */, + 707A3BF225B0A4F0000D215C /* ParseAuthentication.swift in Sources */, F97B466024D9C7B500F4A88B /* KeychainStore.swift in Sources */, 705726E12592C2A800F0ADD5 /* ParseHash.swift in Sources */, 70110D53250680140091CC1D /* ParseConstants.swift in Sources */, - F97B465324D9C78C00F4A88B /* AddUniqueOperation.swift in Sources */, + F97B465324D9C78C00F4A88B /* AddUnique.swift in Sources */, F97B45D724D9C6F200F4A88B /* ParseEncoder.swift in Sources */, 700395A425A119430052CB31 /* Operations.swift in Sources */, 700395F325A171320052CB31 /* LiveQueryable.swift in Sources */, @@ -1460,23 +1506,26 @@ 70510AAD259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */, F97B461F24D9C6F200F4A88B /* ParseStorage.swift in Sources */, F97B45D324D9C6F200F4A88B /* AnyDecodable.swift in Sources */, + 70C550A125B4A9F600B5DBC2 /* RemoveRelation.swift in Sources */, F97B463C24D9C74400F4A88B /* API+Commands.swift in Sources */, - F97B464724D9C78B00F4A88B /* ParseMutationContainer.swift in Sources */, + F97B464724D9C78B00F4A88B /* ParseOperation.swift in Sources */, 705A9A3025991C1400B3547F /* Fileable.swift in Sources */, - F97B464B24D9C78B00F4A88B /* DeleteOperation.swift in Sources */, + F97B464B24D9C78B00F4A88B /* Delete.swift in Sources */, 70647E8F259E3375004C1004 /* LocallyIdentifiable.swift in Sources */, F97B460724D9C6F200F4A88B /* ParseUser.swift in Sources */, 700396F925A394AE0052CB31 /* ParseLiveQueryDelegate.swift in Sources */, - F97B465B24D9C78C00F4A88B /* IncrementOperation.swift in Sources */, + F97B465B24D9C78C00F4A88B /* Increment.swift in Sources */, 7003960A25A184EF0052CB31 /* ParseLiveQuery.swift in Sources */, F97B45E324D9C6F200F4A88B /* AnyEncodable.swift in Sources */, 700396EB25A3892D0052CB31 /* LiveQuerySocketDelegate.swift in Sources */, 70572672259033A700F0ADD5 /* ParseFileManager.swift in Sources */, 707A3C2125B14BD0000D215C /* ParseApple.swift in Sources */, - F97B462324D9C6F200F4A88B /* PrimitiveObjectStore.swift in Sources */, + F97B462324D9C6F200F4A88B /* ParseKeyValueStore.swift in Sources */, F97B45E724D9C6F200F4A88B /* Query.swift in Sources */, + 70C5509325B4A99100B5DBC2 /* AddRelation.swift in Sources */, 708D035325215F9B00646C70 /* Deletable.swift in Sources */, F97B466524D9C88600F4A88B /* SecureStorage.swift in Sources */, + 7004C22125B63C7A005E0AD9 /* ParseRelation.swift in Sources */, 7003959625A10DFC0052CB31 /* Messages.swift in Sources */, 70C5655A25AA147B00BDD57F /* ParseLiveQueryConstants.swift in Sources */, F97B463024D9C74400F4A88B /* BatchUtils.swift in Sources */, @@ -1485,10 +1534,10 @@ F97B460324D9C6F200F4A88B /* NoBody.swift in Sources */, 700395BB25A1470F0052CB31 /* Subscription.swift in Sources */, 7003972B25A3B0140052CB31 /* ParseURLSessionDelegate.swift in Sources */, - 700395D225A147BE0052CB31 /* SubscriptionHandlable.swift in Sources */, + 700395D225A147BE0052CB31 /* ParseSubscription.swift in Sources */, F97B45F724D9C6F200F4A88B /* ParseError.swift in Sources */, F97B463424D9C74400F4A88B /* URLSession+extensions.swift in Sources */, - F97B464F24D9C78B00F4A88B /* AddOperation.swift in Sources */, + F97B464F24D9C78B00F4A88B /* Add.swift in Sources */, 70BC9891252A5B5C00FF3074 /* Objectable.swift in Sources */, F97B45FF24D9C6F200F4A88B /* ParseFile.swift in Sources */, F97B45EF24D9C6F200F4A88B /* BaseParseUser.swift in Sources */, @@ -1496,7 +1545,7 @@ F97B460F24D9C6F200F4A88B /* ParseObject.swift in Sources */, F97B461324D9C6F200F4A88B /* Savable.swift in Sources */, F97B45CF24D9C6F200F4A88B /* ParseCoding.swift in Sources */, - F97B465724D9C78C00F4A88B /* RemoveOperation.swift in Sources */, + F97B465724D9C78C00F4A88B /* Remove.swift in Sources */, F97B45FB24D9C6F200F4A88B /* ParseACL.swift in Sources */, 70BDA2B4250536FF00FC2237 /* ParseInstallation.swift in Sources */, F97B462824D9C72700F4A88B /* API.swift in Sources */, @@ -1504,6 +1553,7 @@ 707A3C1225B0A8E8000D215C /* ParseAnonymous.swift in Sources */, 70110D582506CE890091CC1D /* BaseParseInstallation.swift in Sources */, F97B45DF24D9C6F200F4A88B /* AnyCodable.swift in Sources */, + 70C5507825B49D3A00B5DBC2 /* ParseRole.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1525,12 +1575,15 @@ 709B984C2556ECAA00507778 /* APICommandTests.swift in Sources */, 709B984D2556ECAA00507778 /* AnyDecodableTests.swift in Sources */, 70C5504825B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */, - 709B98572556ECAA00507778 /* ACLTests.swift in Sources */, + 709B98572556ECAA00507778 /* ParseACLTests.swift in Sources */, 705727BC2593FF8C00F0ADD5 /* ParseFileTests.swift in Sources */, 709B984F2556ECAA00507778 /* AnyCodableTests.swift in Sources */, 7003957825A0EE770052CB31 /* BatchUtilsTests.swift in Sources */, 705A99FB259807F900B3547F /* ParseFileManagerTests.swift in Sources */, + 70C5508725B4A68700B5DBC2 /* ParseOperationTests.swift in Sources */, + 7004C26125B6920B005E0AD9 /* ParseRoleTests.swift in Sources */, 9167871A259BC5D600BB5B4E /* ParseCloudTests.swift in Sources */, + 70D1BD8925B8C37200A42E7C /* ParseRelationTests.swift in Sources */, 7003963D25A288100052CB31 /* ParseLiveQueryTests.swift in Sources */, 709B98592556ECAA00507778 /* MockURLResponse.swift in Sources */, 709B98522556ECAA00507778 /* ParseUserTests.swift in Sources */, @@ -1553,7 +1606,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 70F2E2B6254F283000B2EA5C /* ACLTests.swift in Sources */, + 70F2E2B6254F283000B2EA5C /* ParseACLTests.swift in Sources */, 70F2E2B7254F283000B2EA5C /* ParsePointerTests.swift in Sources */, 70F2E2B5254F283000B2EA5C /* ParseEncoderTests.swift in Sources */, 70F2E2C2254F283000B2EA5C /* APICommandTests.swift in Sources */, @@ -1563,7 +1616,10 @@ 70F2E2BD254F283000B2EA5C /* AnyDecodableTests.swift in Sources */, 7003957725A0EE770052CB31 /* BatchUtilsTests.swift in Sources */, 705A99FA259807F900B3547F /* ParseFileManagerTests.swift in Sources */, + 70C5508625B4A68700B5DBC2 /* ParseOperationTests.swift in Sources */, + 7004C25725B6920A005E0AD9 /* ParseRoleTests.swift in Sources */, 91678710259BC5D600BB5B4E /* ParseCloudTests.swift in Sources */, + 70D1BD8825B8C37200A42E7C /* ParseRelationTests.swift in Sources */, 7003963C25A288100052CB31 /* ParseLiveQueryTests.swift in Sources */, 70F2E2C1254F283000B2EA5C /* AnyCodableTests.swift in Sources */, 70F2E2B3254F283000B2EA5C /* ParseUserTests.swift in Sources */, @@ -1591,18 +1647,19 @@ F97B45E924D9C6F200F4A88B /* Query.swift in Sources */, F97B463624D9C74400F4A88B /* URLSession+extensions.swift in Sources */, 70C5503B25B406B800B5DBC2 /* ParseSession.swift in Sources */, - 707A3BF425B0A4F0000D215C /* ParseAuthenticatable.swift in Sources */, + 707A3BF425B0A4F0000D215C /* ParseAuthentication.swift in Sources */, F97B460524D9C6F200F4A88B /* NoBody.swift in Sources */, 705726E32592C2A800F0ADD5 /* ParseHash.swift in Sources */, F97B45E124D9C6F200F4A88B /* AnyCodable.swift in Sources */, F97B45E524D9C6F200F4A88B /* AnyEncodable.swift in Sources */, - F97B465D24D9C78C00F4A88B /* IncrementOperation.swift in Sources */, + F97B465D24D9C78C00F4A88B /* Increment.swift in Sources */, 700395A625A119430052CB31 /* Operations.swift in Sources */, 700395F525A171320052CB31 /* LiveQueryable.swift in Sources */, F97B45FD24D9C6F200F4A88B /* ParseACL.swift in Sources */, 70510AAF259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */, - F97B465124D9C78C00F4A88B /* AddOperation.swift in Sources */, + F97B465124D9C78C00F4A88B /* Add.swift in Sources */, F97B461124D9C6F200F4A88B /* ParseObject.swift in Sources */, + 70C550A325B4A9F600B5DBC2 /* RemoveRelation.swift in Sources */, F97B460D24D9C6F200F4A88B /* Fetchable.swift in Sources */, F97B45ED24D9C6F200F4A88B /* ParseGeoPoint.swift in Sources */, 705A9A3225991C1400B3547F /* Fileable.swift in Sources */, @@ -1618,25 +1675,27 @@ 707A3C2325B14BD0000D215C /* ParseApple.swift in Sources */, F97B462124D9C6F200F4A88B /* ParseStorage.swift in Sources */, F97B466724D9C88600F4A88B /* SecureStorage.swift in Sources */, + 70C5509525B4A99100B5DBC2 /* AddRelation.swift in Sources */, 708D035525215F9B00646C70 /* Deletable.swift in Sources */, 70110D55250680140091CC1D /* ParseConstants.swift in Sources */, + 7004C22325B63C7A005E0AD9 /* ParseRelation.swift in Sources */, 7003959825A10DFC0052CB31 /* Messages.swift in Sources */, 70C5655C25AA147B00BDD57F /* ParseLiveQueryConstants.swift in Sources */, 70BDA2B6250536FF00FC2237 /* ParseInstallation.swift in Sources */, - F97B465924D9C78C00F4A88B /* RemoveOperation.swift in Sources */, + F97B465924D9C78C00F4A88B /* Remove.swift in Sources */, 70110D5A2506CE890091CC1D /* BaseParseInstallation.swift in Sources */, F97B45F924D9C6F200F4A88B /* ParseError.swift in Sources */, 700395BD25A1470F0052CB31 /* Subscription.swift in Sources */, 7003972D25A3B0140052CB31 /* ParseURLSessionDelegate.swift in Sources */, - 700395D425A147BE0052CB31 /* SubscriptionHandlable.swift in Sources */, + 700395D425A147BE0052CB31 /* ParseSubscription.swift in Sources */, F97B460124D9C6F200F4A88B /* ParseFile.swift in Sources */, - F97B464924D9C78B00F4A88B /* ParseMutationContainer.swift in Sources */, + F97B464924D9C78B00F4A88B /* ParseOperation.swift in Sources */, F97B45D124D9C6F200F4A88B /* ParseCoding.swift in Sources */, 70BC9893252A5B5C00FF3074 /* Objectable.swift in Sources */, - F97B465524D9C78C00F4A88B /* AddUniqueOperation.swift in Sources */, - F97B464D24D9C78B00F4A88B /* DeleteOperation.swift in Sources */, + F97B465524D9C78C00F4A88B /* AddUnique.swift in Sources */, + F97B464D24D9C78B00F4A88B /* Delete.swift in Sources */, F97B461524D9C6F200F4A88B /* Savable.swift in Sources */, - F97B462524D9C6F200F4A88B /* PrimitiveObjectStore.swift in Sources */, + F97B462524D9C6F200F4A88B /* ParseKeyValueStore.swift in Sources */, F97B466224D9C7B500F4A88B /* KeychainStore.swift in Sources */, F97B463E24D9C74400F4A88B /* API+Commands.swift in Sources */, F97B462A24D9C72700F4A88B /* API.swift in Sources */, @@ -1647,6 +1706,7 @@ 707A3C1425B0A8E8000D215C /* ParseAnonymous.swift in Sources */, 912C9BFD24D302B2009947C3 /* Parse.swift in Sources */, F97B461924D9C6F200F4A88B /* Queryable.swift in Sources */, + 70C5507A25B49D3A00B5DBC2 /* ParseRole.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1659,18 +1719,19 @@ F97B45E824D9C6F200F4A88B /* Query.swift in Sources */, F97B463524D9C74400F4A88B /* URLSession+extensions.swift in Sources */, 70C5503A25B406B800B5DBC2 /* ParseSession.swift in Sources */, - 707A3BF325B0A4F0000D215C /* ParseAuthenticatable.swift in Sources */, + 707A3BF325B0A4F0000D215C /* ParseAuthentication.swift in Sources */, F97B460424D9C6F200F4A88B /* NoBody.swift in Sources */, 705726E22592C2A800F0ADD5 /* ParseHash.swift in Sources */, F97B45E024D9C6F200F4A88B /* AnyCodable.swift in Sources */, F97B45E424D9C6F200F4A88B /* AnyEncodable.swift in Sources */, - F97B465C24D9C78C00F4A88B /* IncrementOperation.swift in Sources */, + F97B465C24D9C78C00F4A88B /* Increment.swift in Sources */, 700395A525A119430052CB31 /* Operations.swift in Sources */, 700395F425A171320052CB31 /* LiveQueryable.swift in Sources */, F97B45FC24D9C6F200F4A88B /* ParseACL.swift in Sources */, 70510AAE259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */, - F97B465024D9C78B00F4A88B /* AddOperation.swift in Sources */, + F97B465024D9C78B00F4A88B /* Add.swift in Sources */, F97B461024D9C6F200F4A88B /* ParseObject.swift in Sources */, + 70C550A225B4A9F600B5DBC2 /* RemoveRelation.swift in Sources */, F97B460C24D9C6F200F4A88B /* Fetchable.swift in Sources */, F97B45EC24D9C6F200F4A88B /* ParseGeoPoint.swift in Sources */, 705A9A3125991C1400B3547F /* Fileable.swift in Sources */, @@ -1686,25 +1747,27 @@ 707A3C2225B14BD0000D215C /* ParseApple.swift in Sources */, F97B462024D9C6F200F4A88B /* ParseStorage.swift in Sources */, F97B466624D9C88600F4A88B /* SecureStorage.swift in Sources */, + 70C5509425B4A99100B5DBC2 /* AddRelation.swift in Sources */, 708D035425215F9B00646C70 /* Deletable.swift in Sources */, 70110D54250680140091CC1D /* ParseConstants.swift in Sources */, + 7004C22225B63C7A005E0AD9 /* ParseRelation.swift in Sources */, 7003959725A10DFC0052CB31 /* Messages.swift in Sources */, 70C5655B25AA147B00BDD57F /* ParseLiveQueryConstants.swift in Sources */, 70BDA2B5250536FF00FC2237 /* ParseInstallation.swift in Sources */, - F97B465824D9C78C00F4A88B /* RemoveOperation.swift in Sources */, + F97B465824D9C78C00F4A88B /* Remove.swift in Sources */, 70110D592506CE890091CC1D /* BaseParseInstallation.swift in Sources */, F97B45F824D9C6F200F4A88B /* ParseError.swift in Sources */, 700395BC25A1470F0052CB31 /* Subscription.swift in Sources */, 7003972C25A3B0140052CB31 /* ParseURLSessionDelegate.swift in Sources */, - 700395D325A147BE0052CB31 /* SubscriptionHandlable.swift in Sources */, + 700395D325A147BE0052CB31 /* ParseSubscription.swift in Sources */, F97B460024D9C6F200F4A88B /* ParseFile.swift in Sources */, - F97B464824D9C78B00F4A88B /* ParseMutationContainer.swift in Sources */, + F97B464824D9C78B00F4A88B /* ParseOperation.swift in Sources */, F97B45D024D9C6F200F4A88B /* ParseCoding.swift in Sources */, 70BC9892252A5B5C00FF3074 /* Objectable.swift in Sources */, - F97B465424D9C78C00F4A88B /* AddUniqueOperation.swift in Sources */, - F97B464C24D9C78B00F4A88B /* DeleteOperation.swift in Sources */, + F97B465424D9C78C00F4A88B /* AddUnique.swift in Sources */, + F97B464C24D9C78B00F4A88B /* Delete.swift in Sources */, F97B461424D9C6F200F4A88B /* Savable.swift in Sources */, - F97B462424D9C6F200F4A88B /* PrimitiveObjectStore.swift in Sources */, + F97B462424D9C6F200F4A88B /* ParseKeyValueStore.swift in Sources */, F97B466124D9C7B500F4A88B /* KeychainStore.swift in Sources */, F97B463D24D9C74400F4A88B /* API+Commands.swift in Sources */, F97B462924D9C72700F4A88B /* API.swift in Sources */, @@ -1715,6 +1778,7 @@ 707A3C1325B0A8E8000D215C /* ParseAnonymous.swift in Sources */, 912C9BE024D302B0009947C3 /* Parse.swift in Sources */, F97B461824D9C6F200F4A88B /* Queryable.swift in Sources */, + 70C5507925B49D3A00B5DBC2 /* ParseRole.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/ParseSwift/API/API+Commands.swift b/Sources/ParseSwift/API/API+Commands.swift index 902be756d..60b94abfa 100644 --- a/Sources/ParseSwift/API/API+Commands.swift +++ b/Sources/ParseSwift/API/API+Commands.swift @@ -359,7 +359,7 @@ internal extension API.Command { let mapper = { (data: Data) -> PointerType in let baseObjectable = try ParseCoding.jsonDecoder().decode(BaseObjectable.self, from: data) objectable.objectId = baseObjectable.objectId - return objectable.toPointer() + return try objectable.toPointer() } return API.Command(method: .POST, path: objectable.endpoint, @@ -374,7 +374,7 @@ internal extension API.Command { let mapper = { (data: Data) -> PointerType in let baseObjectable = try ParseCoding.jsonDecoder().decode(BaseObjectable.self, from: data) objectable.objectId = baseObjectable.objectId - return objectable.toPointer() + return try objectable.toPointer() } return API.Command(method: .PUT, path: objectable.endpoint, diff --git a/Sources/ParseSwift/API/API.swift b/Sources/ParseSwift/API/API.swift index 8b61110ef..6b4c53c9d 100644 --- a/Sources/ParseSwift/API/API.swift +++ b/Sources/ParseSwift/API/API.swift @@ -22,6 +22,10 @@ public struct API { case user(objectId: String) case installations case installation(objectId: String) + case sessions + case session(objectId: String) + case roles + case role(objectId: String) case login case logout case file(fileName: String) @@ -29,6 +33,7 @@ public struct API { case verificationEmailRequest case functions(name: String) case jobs(name: String) + case aggregate(className: String) case any(String) var urlComponent: String { @@ -47,6 +52,14 @@ public struct API { return "/installations" case .installation(let objectId): return "/installations/\(objectId)" + case .sessions: + return "/sessions" + case .session(let objectId): + return "/sessions/\(objectId)" + case .roles: + return "/roles" + case .role(let objectId): + return "/roles/\(objectId)" case .login: return "/login" case .logout: @@ -61,6 +74,8 @@ public struct API { return "/functions/\(name)" case .jobs(name: let name): return "/jobs/\(name)" + case .aggregate(let className): + return "/aggregate/\(className)" case .any(let path): return path } diff --git a/Sources/ParseSwift/API/Responses.swift b/Sources/ParseSwift/API/Responses.swift index c812a138b..f22e6bec7 100644 --- a/Sources/ParseSwift/API/Responses.swift +++ b/Sources/ParseSwift/API/Responses.swift @@ -36,7 +36,7 @@ internal struct PointerSaveResponse: ChildResponse { guard let object = object as? Objectable else { throw ParseError(code: .unknownError, message: "Should have converted encoded object to Pointer") } - var pointer = PointerType(object) + var pointer = try PointerType(object) pointer.objectId = objectId return pointer } diff --git a/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift b/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift index f0885df9e..bf9c3289f 100644 --- a/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift +++ b/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift @@ -15,7 +15,7 @@ import Foundation Be sure your Parse Server is configured for [sign in with Apple](https://docs.parseplatform.org/parse-server/guide/#configuring-parse-server-for-sign-in-with-apple). For information on acquiring Apple sign-in credentials to use with `ParseApple`, refer to [Apple's Documentation](https://developer.apple.com/documentation/authenticationservices/implementing_user_authentication_with_sign_in_with_apple). */ -public struct ParseApple: ParseAuthenticatable { +public struct ParseApple: ParseAuthentication { /// Authentication keys required for Apple authentication. enum AuthenticationKeys: String, Codable { diff --git a/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift b/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift index d8138e17c..523c47969 100644 --- a/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift +++ b/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift @@ -26,7 +26,7 @@ import Foundation - 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: ParseAuthenticatable { +public struct ParseAnonymous: ParseAuthentication { enum AuthenticationKeys: String, Codable { case id // swiftlint:disable:this identifier_name @@ -47,7 +47,7 @@ public extension ParseAnonymous { Login a `ParseUser` *synchronously* using the respective authentication type. - parameter authData: The authData for the respective authentication type. - parameter options: A set of header options sent to the server. Defaults to an empty set. - - throws: `ParseError`. + - throws: An error of type `ParseError`. - returns: the linked `ParseUser`. */ func login(authData: [String: String]? = nil, diff --git a/Sources/ParseSwift/Authentication/Protocols/ParseAuthenticatable.swift b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift similarity index 96% rename from Sources/ParseSwift/Authentication/Protocols/ParseAuthenticatable.swift rename to Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift index 1864f908d..412d17734 100644 --- a/Sources/ParseSwift/Authentication/Protocols/ParseAuthenticatable.swift +++ b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift @@ -1,5 +1,5 @@ // -// ParseAuthenticatable.swift +// ParseAuthentication.swift // ParseSwift // // Created by Corey Baker on 1/14/21. @@ -8,7 +8,13 @@ import Foundation -public protocol ParseAuthenticatable: Codable { +/** + Objects that conform to the `ParseAuthentication` protocol provide + convenience implementations for using 3rd party authentication methods. + The authentication methods supported by the Parse Server can be found + [here](https://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication). + */ +public protocol ParseAuthentication: Codable { associatedtype AuthenticatedUser: ParseUser init() @@ -90,7 +96,7 @@ public protocol ParseAuthenticatable: Codable { } // MARK: Convenience Implementations -public extension ParseAuthenticatable { +public extension ParseAuthentication { var __type: String { // swiftlint:disable:this identifier_name Self.__type diff --git a/Sources/ParseSwift/Coding/ParseEncoder.swift b/Sources/ParseSwift/Coding/ParseEncoder.swift index 600f686a4..b7004d63e 100644 --- a/Sources/ParseSwift/Coding/ParseEncoder.swift +++ b/Sources/ParseSwift/Coding/ParseEncoder.swift @@ -265,7 +265,7 @@ private class _ParseEncoder: JSONEncoder, Encoder { if !self.collectChildren && codingPath.count > 0 { valueToEncode = value } else if !self.collectChildren { - valueToEncode = value.toPointer() + valueToEncode = try value.toPointer() } } else { let hashOfCurrentObject = try BaseObjectable.createHash(value) diff --git a/Sources/ParseSwift/LiveQuery/LiveQuerySocket.swift b/Sources/ParseSwift/LiveQuery/LiveQuerySocket.swift index 2b9c775b5..6d0d86c93 100644 --- a/Sources/ParseSwift/LiveQuery/LiveQuerySocket.swift +++ b/Sources/ParseSwift/LiveQuery/LiveQuerySocket.swift @@ -7,6 +7,9 @@ // import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) final class LiveQuerySocket: NSObject { diff --git a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift index ca00320c4..f89005a2d 100644 --- a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift +++ b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift @@ -7,12 +7,15 @@ // import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif /** The `ParseLiveQuery` class enables two-way communication to a Parse Live Query Server. - In most cases, you won't need to call this class directly as a LiveQuery can be directly + In most cases, you should not call this class directly as a LiveQuery can be indirectly created from `Query` using: // If "Message" is a "ParseObject" @@ -321,7 +324,7 @@ extension ParseLiveQuery: LiveQuerySocketDelegate { return } - //Check if this is a error response + //Check if this is an error response if let error = try? ParseCoding.jsonDecoder().decode(ErrorResponse.self, from: data) { if !error.reconnect { //Treat this as a user disconnect because the server doesn't want to hear from us anymore @@ -565,7 +568,7 @@ extension ParseLiveQuery { var subscribeHandlerClosure: ((Bool) -> Void)? var unsubscribeHandlerClosure: (() -> Void)? - init?(query: Query, message: SubscribeMessage, handler: T) { + init?(query: Query, message: SubscribeMessage, handler: T) { guard let queryData = try? ParseCoding.jsonEncoder().encode(query), let encoded = try? ParseCoding.jsonEncoder().encode(message) else { return nil @@ -612,11 +615,11 @@ extension ParseLiveQuery { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension ParseLiveQuery { - func subscribe(_ query: Query) throws -> Subscription, T> { + func subscribe(_ query: Query) throws -> Subscription { try subscribe(Subscription(query: query)) } - func subscribe(_ handler: T) throws -> T where T: SubscriptionHandlable { + func subscribe(_ handler: T) throws -> T where T: ParseSubscription { let requestId = requestIdGenerator() let message = SubscribeMessage(operation: .subscribe, requestId: requestId, query: handler.query) @@ -642,7 +645,7 @@ extension ParseLiveQuery { try unsubscribe { $0.queryData == unsubscribeQuery } } - func unsubscribe(_ handler: T) throws where T: SubscriptionHandlable { + func unsubscribe(_ handler: T) throws where T: ParseSubscription { let unsubscribeQuery = try ParseCoding.jsonEncoder().encode(handler.query) try unsubscribe { $0.queryData == unsubscribeQuery && $0.subscriptionHandler === handler } } @@ -670,7 +673,7 @@ extension ParseLiveQuery { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension ParseLiveQuery { - func update(_ handler: T) throws where T: SubscriptionHandlable { + func update(_ handler: T) throws where T: ParseSubscription { try subscriptions.forEach {(key, value) -> Void in if value.subscriptionHandler === handler { let message = SubscribeMessage(operation: .update, requestId: key, query: handler.query) @@ -690,7 +693,7 @@ public extension Query { Registers the query for live updates, using the default subscription handler, and the default `ParseLiveQuery` client. */ - var subscribe: Subscription, ResultType>? { + var subscribe: Subscription? { try? ParseLiveQuery.client?.subscribe(self) } @@ -700,7 +703,7 @@ public extension Query { - parameter client: A specific client. - returns: The subscription that has just been registered */ - func subscribe(_ client: ParseLiveQuery) throws -> Subscription, ResultType> { + func subscribe(_ client: ParseLiveQuery) throws -> Subscription { try client.subscribe(Subscription(query: self)) } @@ -709,7 +712,7 @@ public extension Query { - parameter handler: A custom subscription handler. - returns: Your subscription handler, for easy chaining. */ - static func subscribe(_ handler: T) throws -> T { + static func subscribe(_ handler: T) throws -> T { if let client = ParseLiveQuery.client { return try client.subscribe(handler) } else { @@ -723,7 +726,7 @@ public extension Query { - parameter client: A specific client. - returns: Your subscription handler, for easy chaining. */ - static func subscribe(_ handler: T, client: ParseLiveQuery) throws -> T { + static func subscribe(_ handler: T, client: ParseLiveQuery) throws -> T { try client.subscribe(handler) } } @@ -753,7 +756,7 @@ public extension Query { `ParseLiveQuery` client. - parameter handler: The specific handler to unsubscribe from. */ - func unsubscribe(_ handler: T) throws { + func unsubscribe(_ handler: T) throws { try ParseLiveQuery.client?.unsubscribe(handler) } @@ -763,7 +766,7 @@ public extension Query { - parameter handler: The specific handler to unsubscribe from. - parameter client: A specific client. */ - func unsubscribe(_ handler: T, client: ParseLiveQuery) throws { + func unsubscribe(_ handler: T, client: ParseLiveQuery) throws { try client.unsubscribe(handler) } } @@ -776,7 +779,7 @@ public extension Query { Upon completing the registration, the subscribe handler will be called with the new query. - parameter handler: The specific handler to update. */ - func update(_ handler: T) throws { + func update(_ handler: T) throws { try ParseLiveQuery.client?.update(handler) } @@ -786,7 +789,7 @@ public extension Query { - parameter handler: The specific handler to update. - parameter client: A specific client. */ - func update(_ handler: T, client: ParseLiveQuery) throws { + func update(_ handler: T, client: ParseLiveQuery) throws { try client.update(handler) } } diff --git a/Sources/ParseSwift/LiveQuery/Protocols/LiveQuerySocketDelegate.swift b/Sources/ParseSwift/LiveQuery/Protocols/LiveQuerySocketDelegate.swift index 8772ecafa..e8fd36831 100644 --- a/Sources/ParseSwift/LiveQuery/Protocols/LiveQuerySocketDelegate.swift +++ b/Sources/ParseSwift/LiveQuery/Protocols/LiveQuerySocketDelegate.swift @@ -7,6 +7,9 @@ // import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) protocol LiveQuerySocketDelegate: AnyObject { diff --git a/Sources/ParseSwift/LiveQuery/Protocols/ParseLiveQueryDelegate.swift b/Sources/ParseSwift/LiveQuery/Protocols/ParseLiveQueryDelegate.swift index 3ffab9afe..c817d3890 100644 --- a/Sources/ParseSwift/LiveQuery/Protocols/ParseLiveQueryDelegate.swift +++ b/Sources/ParseSwift/LiveQuery/Protocols/ParseLiveQueryDelegate.swift @@ -7,6 +7,9 @@ // import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif // swiftlint:disable line_length diff --git a/Sources/ParseSwift/LiveQuery/Protocols/SubscriptionHandlable.swift b/Sources/ParseSwift/LiveQuery/Protocols/ParseSubscription.swift similarity index 93% rename from Sources/ParseSwift/LiveQuery/Protocols/SubscriptionHandlable.swift rename to Sources/ParseSwift/LiveQuery/Protocols/ParseSubscription.swift index a647865b8..12a317c5e 100644 --- a/Sources/ParseSwift/LiveQuery/Protocols/SubscriptionHandlable.swift +++ b/Sources/ParseSwift/LiveQuery/Protocols/ParseSubscription.swift @@ -1,5 +1,5 @@ // -// SubscriptionHandlable.swift +// ParseSubscription.swift // ParseSwift // // Created by Corey Baker on 1/2/21. @@ -12,7 +12,7 @@ import Foundation This protocol describes the interface for handling events from a `ParseLiveQuery` client. You can use this protocol on any custom class of yours, instead of Subscription, if it fits your use case better. */ -public protocol SubscriptionHandlable: AnyObject { +public protocol ParseSubscription: AnyObject { /// The type of the `ParseObject` that this handler uses. associatedtype Object: ParseObject diff --git a/Sources/ParseSwift/LiveQuery/Subscription.swift b/Sources/ParseSwift/LiveQuery/Subscription.swift index 2ae52b59a..360058d7e 100644 --- a/Sources/ParseSwift/LiveQuery/Subscription.swift +++ b/Sources/ParseSwift/LiveQuery/Subscription.swift @@ -57,9 +57,9 @@ private func == (lhs: Event, rhs: Event) -> Bool { } /** - A default implementation of the SubscriptionHandlable protocol, using closures for callbacks. + A default implementation of the `ParseSubscription` protocol, using closures for callbacks. */ -open class Subscription, T: ParseObject>: SubscriptionHandlable { +open class Subscription: ParseSubscription { //The query subscribed to. public var query: Query //The ParseObject diff --git a/Sources/ParseSwift/Mutation Operations/ParseMutationContainer.swift b/Sources/ParseSwift/Mutation Operations/ParseMutationContainer.swift deleted file mode 100644 index f02a58d35..000000000 --- a/Sources/ParseSwift/Mutation Operations/ParseMutationContainer.swift +++ /dev/null @@ -1,108 +0,0 @@ -// -// ParseMutationContainer.swift -// Parse -// -// Created by Florent Vilmart on 17-07-24. -// Copyright © 2017 Parse. All rights reserved. -// - -import Foundation - -public struct ParseMutationContainer: Encodable where T: ParseObject { - typealias ObjectType = T - - var target: T - private var operations = [String: Encodable]() - - init(target: T) { - self.target = target - } - - public mutating func increment(_ key: String, by amount: Int) { - operations[key] = IncrementOperation(amount: amount) - } - - public mutating func addUnique(_ key: String, objects: [W]) where W: Encodable, W: Hashable { - operations[key] = AddUniqueOperation(objects: objects) - } - - public mutating func addUnique(_ key: (String, WritableKeyPath), - objects: [V]) where V: Encodable, V: Hashable { - operations[key.0] = AddUniqueOperation(objects: objects) - var values = target[keyPath: key.1] - values.append(contentsOf: objects) - target[keyPath: key.1] = Array(Set(values)) - } - - public mutating func addUnique(_ key: (String, WritableKeyPath), - objects: [V]) where V: Encodable, V: Hashable { - operations[key.0] = AddUniqueOperation(objects: objects) - var values = target[keyPath: key.1] ?? [] - values.append(contentsOf: objects) - target[keyPath: key.1] = Array(Set(values)) - } - - public mutating func add(_ key: String, objects: [W]) where W: Encodable { - operations[key] = AddOperation(objects: objects) - } - - public mutating func add(_ key: (String, WritableKeyPath), - objects: [V]) where V: Encodable { - operations[key.0] = AddOperation(objects: objects) - var values = target[keyPath: key.1] - values.append(contentsOf: objects) - target[keyPath: key.1] = values - } - - public mutating func add(_ key: (String, WritableKeyPath), - objects: [V]) where V: Encodable { - operations[key.0] = AddOperation(objects: objects) - var values = target[keyPath: key.1] ?? [] - values.append(contentsOf: objects) - target[keyPath: key.1] = values - } - - public mutating func remove(_ key: String, objects: [W]) where W: Encodable { - operations[key] = RemoveOperation(objects: objects) - } - - public mutating func remove(_ key: (String, WritableKeyPath), - objects: [V]) where V: Encodable, V: Hashable { - operations[key.0] = RemoveOperation(objects: objects) - let values = target[keyPath: key.1] - var set = Set(values) - objects.forEach { - set.remove($0) - } - target[keyPath: key.1] = Array(set) - } - - public mutating func remove(_ key: (String, WritableKeyPath), - objects: [V]) where V: Encodable, V: Hashable { - operations[key.0] = RemoveOperation(objects: objects) - let values = target[keyPath: key.1] - var set = Set(values ?? []) - objects.forEach { - set.remove($0) - } - target[keyPath: key.1] = Array(set) - } - - public mutating func unset(_ key: String) { - operations[key] = DeleteOperation() - } - - public mutating func unset(_ key: (String, WritableKeyPath)) where V: Encodable { - operations[key.0] = DeleteOperation() - target[keyPath: key.1] = nil - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: RawCodingKey.self) - try operations.forEach { pair in - let (key, value) = pair - let encoder = container.superEncoder(forKey: .key(key)) - try value.encode(to: encoder) - } - } -} diff --git a/Sources/ParseSwift/Objects/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift index cefe0b705..c4f65926b 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation.swift @@ -120,6 +120,7 @@ extension ParseInstallation { try? ParseStorage.shared.set(newInstallation, for: ParseStorage.Keys.currentInstallation) return newInstallation } + return installationFromKeyChain #else var newInstallation = CurrentInstallationContainer() let newInstallationId = UUID().uuidString.lowercased() @@ -129,7 +130,6 @@ extension ParseInstallation { try? ParseStorage.shared.set(newInstallation, for: ParseStorage.Keys.currentInstallation) return newInstallation #endif - return installationFromKeyChain } return installationInMemory } @@ -340,7 +340,7 @@ extension ParseInstallation { and sets an error if one occurs. - parameter options: A set of header options sent to the server. Defaults to an empty set. - - throws: An Error of `ParseError` type. + - throws: An error of `ParseError` type. - important: If an object fetched has the same objectId as current, it will automatically update the current. */ public func fetch(options: API.Options = []) throws -> Self { @@ -406,7 +406,7 @@ extension ParseInstallation { Saves the `ParseInstallation` *synchronously* and throws an error if there's an issue. - parameter options: A set of header options sent to the server. Defaults to an empty set. - - throws: A Error of type `ParseError`. + - 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. */ @@ -510,7 +510,7 @@ extension ParseInstallation { and sets an error if one occurs. - parameter options: A set of header options sent to the server. Defaults to an empty set. - - throws: An Error of `ParseError` type. + - throws: An error of `ParseError` type. - important: If an object deleted has the same objectId as current, it will automatically update the current. */ public func delete(options: API.Options = []) throws { diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index a50448b6b..3f8d74836 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -28,7 +28,7 @@ public protocol ParseObject: Objectable, Fetchable, Savable, Deletable, - Equatable, + Hashable, CustomDebugStringConvertible {} // MARK: Default Implementations @@ -49,8 +49,8 @@ extension ParseObject { Gets a Pointer referencing this Object. - returns: Pointer */ - public func toPointer() -> Pointer { - return Pointer(self) + public func toPointer() throws -> Pointer { + return try Pointer(self) } } @@ -447,7 +447,7 @@ extension ParseObject { Fetches the `ParseObject` *synchronously* with the current data from the server and sets an error if one occurs. - parameter options: A set of header options sent to the server. Defaults to an empty set. - - throws: An Error of `ParseError` type. + - throws: An error of `ParseError` type. */ public func fetch(options: API.Options = []) throws -> Self { try fetchCommand().execute(options: options, @@ -492,10 +492,10 @@ extension ParseObject { } } -// MARK: Mutations +// MARK: Operations public extension ParseObject { - var mutationContainer: ParseMutationContainer { - return ParseMutationContainer(target: self) + var operation: ParseOperation { + return ParseOperation(target: self) } } @@ -522,7 +522,7 @@ extension ParseObject { Saves the `ParseObject` *synchronously* and throws an error if there's an issue. - parameter options: A set of header options sent to the server. Defaults to an empty set. - - throws: A Error of type `ParseError`. + - throws: An error of type `ParseError`. - returns: Returns saved `ParseObject`. */ @@ -699,7 +699,7 @@ extension ParseObject { Deletes the `ParseObject` *synchronously* with the current data from the server and sets an error if one occurs. - parameter options: A set of header options sent to the server. Defaults to an empty set. - - throws: An Error of `ParseError` type. + - throws: An error of `ParseError` type. */ public func delete(options: API.Options = []) throws { if let error = try deleteCommand().execute(options: options) { diff --git a/Sources/ParseSwift/Objects/ParseRole.swift b/Sources/ParseSwift/Objects/ParseRole.swift new file mode 100644 index 000000000..0d79601ca --- /dev/null +++ b/Sources/ParseSwift/Objects/ParseRole.swift @@ -0,0 +1,136 @@ +// +// ParseRole.swift +// ParseSwift +// +// Created by Corey Baker on 1/17/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +import Foundation + +/** + Objects that conform to the `ParseRole` protocol represent a Role on the Parse Server. + `ParseRole`'s represent groupings of `ParseUser` objects for the purposes of + granting permissions (e.g. specifying a `ParseACL` for a `ParseObject`). + Roles are specified by their sets of child users and child roles, + all of which are granted any permissions that the parent role has. + Roles must have a name (which cannot be changed after creation of the role), + and must specify a `ParseACL`. + */ +public protocol ParseRole: ParseObject { + + /** + Gets or sets the name for a role. + This value must be set before the role has been saved to the server, + and cannot be set once the role has been saved. + - warning: A role's name can only contain alphanumeric characters, `_`, `-`, and spaces. + */ + var name: String { get set } + + /** + Create a `ParseRole`. It's best to use the provided initializers, `init(name: String)` + or `init(name: String, acl: ParseACL)`. The provided initializers will overwrite + whatever name is specified here, so you can use `self.name = ""` + */ + init() +} + +// MARK: Default Implementations +public extension ParseRole { + static var className: String { + "_Role" + } + + /** + Create a `ParseRole` with a name. The `ParseACL` will still need to be initialized before saving. + - parameter name: The name of the Role to create. + - throws: `ParseError` if the name has invalid characters. + */ + init(name: String) throws { + try Self.checkName(name) + self.init() + } + + /** + Create a `ParseRole` with a name. + - parameter name: The name of the Role to create. + - parameter acl: The `ParseACL` for this role. Roles must have an ACL. + A `ParseRole` is a local representation of a role persisted to the Parse Server. + - throws: `ParseError` if the name has invalid characters. + */ + init(name: String, acl: ParseACL) throws { + try Self.checkName(name) + self.init() + self.name = name + self.ACL = acl + } + + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.name == rhs.name + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.name) + } +} + +// MARK: Convenience +extension ParseRole { + var endpoint: API.Endpoint { + if let objectId = objectId { + return .role(objectId: objectId) + } + return .roles + } + + static func checkName(_ name: String) throws { + // swiftlint:disable:next line_length + let characterset = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_- ") + if name.rangeOfCharacter(from: characterset.inverted) != nil { + throw ParseError(code: .unknownError, + message: "A role's name can be only contain alphanumeric characters, _, '-, and spaces.") + } + } +} + +// MARK: ParseRelation +public extension ParseRole { + + /** + Gets the `ParseRelation` for the `ParseUser` objects that are direct children of this role. + These users are granted any privileges that this role has been granted + (e.g. read or write access through `ParseACL`s). You can add or remove users from + the role through this relation. + */ + var users: ParseRelation { + ParseRelation(parent: self, key: "users", className: "_User") + } + + /** + Gets the `ParseRelation` for the `ParseRole` objects that are direct children of this role. + These roles' users are granted any privileges that this role has been granted + (e.g. read or write access through `ParseACL`s). You can add or remove child roles + from this role through this relation. + */ + var roles: ParseRelation { + ParseRelation(parent: self, key: "roles", className: "_Role") + } + + /** + Query the `ParseRelation` for the `ParseUser`'s that are direct children of this role. + These users are granted any privileges that this role has been granted + (e.g. read or write access through `ParseACL`s). + */ + func queryUsers(_ user: T) throws -> Query where T: ParseUser { + try users.query(user) + } + + /** + Query the `ParseRelation` for the `ParseRole`'s that are direct children of this role. + These users are granted any privileges that this role has been granted + (e.g. read or write access through `ParseACL`s). + */ + var queryRoles: Query? { + try? roles.query(self) + } +} diff --git a/Sources/ParseSwift/Objects/ParseSession.swift b/Sources/ParseSwift/Objects/ParseSession.swift index a65c928b2..5bea69ffa 100644 --- a/Sources/ParseSwift/Objects/ParseSession.swift +++ b/Sources/ParseSwift/Objects/ParseSession.swift @@ -23,6 +23,7 @@ public protocol ParseSession: ParseObject { var user: SessionUser { get } /// Whether the session is restricted. + /// - warning: This will be deprecated in newer versions of Parse Server. var restricted: Bool? { get } /// Information about how the session was created. @@ -43,3 +44,14 @@ public extension ParseSession { "_Session" } } + +// MARK: Convenience +extension ParseSession { + var endpoint: API.Endpoint { + if let objectId = objectId { + return .session(objectId: objectId) + } + + return .sessions + } +} diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index bd6901c87..1d4814eee 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -638,7 +638,7 @@ extension ParseUser { Fetches the `ParseUser` *synchronously* with the current data from the server and sets an error if one occurs. - parameter options: A set of header options sent to the server. Defaults to an empty set. - - throws: An Error of `ParseError` type. + - throws: An error of `ParseError` type. - important: If an object fetched has the same objectId as current, it will automatically update the current. */ public func fetch(options: API.Options = []) throws -> Self { @@ -719,7 +719,7 @@ extension ParseUser { Saves the `ParseUser` *synchronously* and throws an error if there's an issue. - parameter options: A set of header options sent to the server. Defaults to an empty set. - - throws: A Error of type `ParseError`. + - 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. */ @@ -821,7 +821,7 @@ extension ParseUser { Deletes the `ParseUser` *synchronously* with the current data from the server and sets an error if one occurs. - parameter options: A set of header options sent to the server. Defaults to an empty set. - - throws: An Error of `ParseError` type. + - throws: An error of `ParseError` type. - important: If an object deleted has the same objectId as current, it will automatically update the current. */ public func delete(options: API.Options = []) throws { diff --git a/Sources/ParseSwift/Mutation Operations/AddOperation.swift b/Sources/ParseSwift/Operations/Add.swift similarity index 72% rename from Sources/ParseSwift/Mutation Operations/AddOperation.swift rename to Sources/ParseSwift/Operations/Add.swift index a5e468faf..474368925 100644 --- a/Sources/ParseSwift/Mutation Operations/AddOperation.swift +++ b/Sources/ParseSwift/Operations/Add.swift @@ -1,5 +1,5 @@ // -// AddOperation.swift +// Add.swift // Parse // // Created by Florent Vilmart on 17-07-24. @@ -8,7 +8,7 @@ import Foundation -internal struct AddOperation: Encodable where T: Encodable { +internal struct Add: Encodable where T: Encodable { let __op: String = "Add" // swiftlint:disable:this identifier_name let objects: [T] } diff --git a/Sources/ParseSwift/Operations/AddRelation.swift b/Sources/ParseSwift/Operations/AddRelation.swift new file mode 100644 index 000000000..840298ef9 --- /dev/null +++ b/Sources/ParseSwift/Operations/AddRelation.swift @@ -0,0 +1,18 @@ +// +// AddRelation.swift +// ParseSwift +// +// Created by Corey Baker on 1/17/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +import Foundation + +internal struct AddRelation: Encodable where T: ParseObject { + let __op: String = "AddRelation" // swiftlint:disable:this identifier_name + let objects: [Pointer] + + init(objects: [T]) throws { + self.objects = try objects.compactMap { try $0.toPointer() } + } +} diff --git a/Sources/ParseSwift/Mutation Operations/AddUniqueOperation.swift b/Sources/ParseSwift/Operations/AddUnique.swift similarity index 70% rename from Sources/ParseSwift/Mutation Operations/AddUniqueOperation.swift rename to Sources/ParseSwift/Operations/AddUnique.swift index da3a21c80..77606c83e 100644 --- a/Sources/ParseSwift/Mutation Operations/AddUniqueOperation.swift +++ b/Sources/ParseSwift/Operations/AddUnique.swift @@ -1,5 +1,5 @@ // -// AddUniqueOperation.swift +// AddUnique.swift // Parse // // Created by Florent Vilmart on 17-07-24. @@ -8,7 +8,7 @@ import Foundation -internal struct AddUniqueOperation: Encodable where T: Encodable { +internal struct AddUnique: Encodable where T: Encodable { let __op: String = "AddUnique" // swiftlint:disable:this identifier_name let objects: [T] } diff --git a/Sources/ParseSwift/Mutation Operations/DeleteOperation.swift b/Sources/ParseSwift/Operations/Delete.swift similarity index 74% rename from Sources/ParseSwift/Mutation Operations/DeleteOperation.swift rename to Sources/ParseSwift/Operations/Delete.swift index 72d9c43f8..715a4c823 100644 --- a/Sources/ParseSwift/Mutation Operations/DeleteOperation.swift +++ b/Sources/ParseSwift/Operations/Delete.swift @@ -1,5 +1,5 @@ // -// DeleteOperation.swift +// Delete.swift // Parse // // Created by Florent Vilmart on 17-07-24. @@ -8,6 +8,6 @@ import Foundation -internal struct DeleteOperation: Encodable { +internal struct Delete: Encodable { let __op: String = "Delete" // swiftlint:disable:this identifier_name } diff --git a/Sources/ParseSwift/Mutation Operations/IncrementOperation.swift b/Sources/ParseSwift/Operations/Increment.swift similarity index 84% rename from Sources/ParseSwift/Mutation Operations/IncrementOperation.swift rename to Sources/ParseSwift/Operations/Increment.swift index 3bff6b7d2..d904a6e9f 100644 --- a/Sources/ParseSwift/Mutation Operations/IncrementOperation.swift +++ b/Sources/ParseSwift/Operations/Increment.swift @@ -8,7 +8,7 @@ import Foundation -internal struct IncrementOperation: Encodable { +internal struct Increment: Encodable { let __op: String = "Increment" // swiftlint:disable:this identifier_name let amount: Int } diff --git a/Sources/ParseSwift/Operations/ParseOperation.swift b/Sources/ParseSwift/Operations/ParseOperation.swift new file mode 100644 index 000000000..20fc53c7b --- /dev/null +++ b/Sources/ParseSwift/Operations/ParseOperation.swift @@ -0,0 +1,412 @@ +// +// ParseOperation.swift +// Parse +// +// Created by Florent Vilmart on 17-07-24. +// Copyright © 2017 Parse. All rights reserved. +// + +import Foundation + +/** + A `ParseOperation` represents a modification to a value in a `ParseObject`. + For example, setting, deleting, or incrementing a value are all `ParseOperation`'s. + `ParseOperation` themselves can be considered to be immutable. + + In most cases, you should not call this class directly as a `ParseOperation` can be + indirectly created from any `ParseObject` by using its `operation` property. + */ +public struct ParseOperation: Savable where T: ParseObject { + + var target: T? + var operations = [String: Encodable]() + + public init(target: T) { + self.target = target + } + + /** + An operation that increases a numeric field's value by a given amount. + - Parameters: + - key: The key of the object. + - amount: How much to increment by. + */ + public func increment(_ key: String, by amount: Int) -> Self { + var mutableOperation = self + mutableOperation.operations[key] = Increment(amount: amount) + return mutableOperation + } + + /** + An operation that adds a new element to an array field, + only if it wasn't already present. + - Parameters: + - key: The key of the object. + - objects: The field of objects. + */ + public func addUnique(_ key: String, objects: [W]) -> Self where W: Encodable, W: Hashable { + var mutableOperation = self + mutableOperation.operations[key] = AddUnique(objects: objects) + return mutableOperation + } + + /** + An operation that adds a new element to an array field, + only if it wasn't already present. + - Parameters: + - key: A tuple consisting of the key and KeyPath of the object. + - objects: The field of objects. + */ + public func addUnique(_ key: (String, WritableKeyPath), + objects: [V]) throws -> Self where V: Encodable, V: Hashable { + guard let target = self.target else { + throw ParseError(code: .unknownError, message: "Target shouldn't be nil") + } + var mutableOperation = self + mutableOperation.operations[key.0] = AddUnique(objects: objects) + var values = target[keyPath: key.1] + values.append(contentsOf: objects) + mutableOperation.target?[keyPath: key.1] = Array(Set(values)) + return mutableOperation + } + + /** + An operation that adds a new element to an array field, + only if it wasn't already present. + - Parameters: + - key: A tuple consisting of the key and KeyPath of the object. + - objects: The field of objects. + */ + public func addUnique(_ key: (String, WritableKeyPath), + objects: [V]) throws -> Self where V: Encodable, V: Hashable { + guard let target = self.target else { + throw ParseError(code: .unknownError, message: "Target shouldn't be nil") + } + var mutableOperation = self + mutableOperation.operations[key.0] = AddUnique(objects: objects) + var values = target[keyPath: key.1] ?? [] + values.append(contentsOf: objects) + mutableOperation.target?[keyPath: key.1] = Array(Set(values)) + return mutableOperation + } + + /** + An operation that adds a new element to an array field. + - Parameters: + - key: The key of the object. + - objects: The field of objects. + */ + public func add(_ key: String, objects: [W]) -> Self where W: Encodable { + var mutableOperation = self + mutableOperation.operations[key] = Add(objects: objects) + return mutableOperation + } + + /** + An operation that adds a new element to an array field. + - Parameters: + - key: A tuple consisting of the key and KeyPath of the object. + - objects: The field of objects. + */ + public func add(_ key: (String, WritableKeyPath), + objects: [V]) throws -> Self where V: Encodable { + guard let target = self.target else { + throw ParseError(code: .unknownError, message: "Target shouldn't be nil") + } + var mutableOperation = self + mutableOperation.operations[key.0] = Add(objects: objects) + var values = target[keyPath: key.1] + values.append(contentsOf: objects) + mutableOperation.target?[keyPath: key.1] = values + return mutableOperation + } + + /** + An operation that adds a new element to an array field. + - Parameters: + - key: A tuple consisting of the key and KeyPath of the object. + - objects: The field of objects. + */ + public func add(_ key: (String, WritableKeyPath), + objects: [V]) throws -> Self where V: Encodable { + guard let target = self.target else { + throw ParseError(code: .unknownError, message: "Target shouldn't be nil") + } + var mutableOperation = self + mutableOperation.operations[key.0] = Add(objects: objects) + var values = target[keyPath: key.1] ?? [] + values.append(contentsOf: objects) + mutableOperation.target?[keyPath: key.1] = values + return mutableOperation + } + + /** + An operation that adds a new relation to an array field. + - Parameters: + - key: The key of the object. + - objects: The field of objects. + */ + public func addRelation(_ key: String, objects: [W]) throws -> Self where W: ParseObject { + var mutableOperation = self + mutableOperation.operations[key] = try AddRelation(objects: objects) + return mutableOperation + } + + /** + An operation that adds a new relation to an array field. + - Parameters: + - key: A tuple consisting of the key and KeyPath of the object. + - objects: The field of objects. + */ + public func addRelation(_ key: (String, WritableKeyPath), + objects: [V]) throws -> Self where V: ParseObject { + guard let target = self.target else { + throw ParseError(code: .unknownError, message: "Target shouldn't be nil") + } + var mutableOperation = self + mutableOperation.operations[key.0] = try AddRelation(objects: objects) + var values = target[keyPath: key.1] + values.append(contentsOf: objects) + mutableOperation.target?[keyPath: key.1] = values + return mutableOperation + } + + /** + An operation that adds a new relation to an array field. + - Parameters: + - key: A tuple consisting of the key and KeyPath of the object. + - objects: The field of objects. + */ + public func addRelation(_ key: (String, WritableKeyPath), + objects: [V]) throws -> Self where V: ParseObject { + guard let target = self.target else { + throw ParseError(code: .unknownError, message: "Target shouldn't be nil") + } + var mutableOperation = self + mutableOperation.operations[key.0] = try AddRelation(objects: objects) + var values = target[keyPath: key.1] ?? [] + values.append(contentsOf: objects) + mutableOperation.target?[keyPath: key.1] = values + return mutableOperation + } + + /** + An operation that removes every instance of an element from + an array field. + - Parameters: + - key: The key of the object. + - objects: The field of objects. + */ + public func remove(_ key: String, objects: [W]) -> Self where W: Encodable { + var mutableOperation = self + mutableOperation.operations[key] = Remove(objects: objects) + return mutableOperation + } + + /** + An operation that removes every instance of an element from + an array field. + - Parameters: + - key: A tuple consisting of the key and KeyPath of the object. + - objects: The field of objects. + */ + public func remove(_ key: (String, WritableKeyPath), + objects: [V]) throws -> Self where V: Encodable, V: Hashable { + guard let target = self.target else { + throw ParseError(code: .unknownError, message: "Target shouldn't be nil") + } + var mutableOperation = self + mutableOperation.operations[key.0] = Remove(objects: objects) + let values = target[keyPath: key.1] + var set = Set(values) + objects.forEach { + set.remove($0) + } + mutableOperation.target?[keyPath: key.1] = Array(set) + return mutableOperation + } + + /** + An operation that removes every instance of an element from + an array field. + - Parameters: + - key: A tuple consisting of the key and KeyPath of the object. + - objects: The field of objects. + */ + public func remove(_ key: (String, WritableKeyPath), + objects: [V]) throws -> Self where V: Encodable, V: Hashable { + guard let target = self.target else { + throw ParseError(code: .unknownError, message: "Target shouldn't be nil") + } + var mutableOperation = self + mutableOperation.operations[key.0] = Remove(objects: objects) + let values = target[keyPath: key.1] + var set = Set(values ?? []) + objects.forEach { + set.remove($0) + } + mutableOperation.target?[keyPath: key.1] = Array(set) + return mutableOperation + } + + /** + An operation that removes every instance of a relation from + an array field. + - Parameters: + - key: The key of the object. + - objects: The field of objects. + */ + public func removeRelation(_ key: String, objects: [W]) throws -> Self where W: ParseObject { + var mutableOperation = self + mutableOperation.operations[key] = try RemoveRelation(objects: objects) + return mutableOperation + } + + /** + An operation that removes every instance of a relation from + an array field. + - Parameters: + - key: A tuple consisting of the key and KeyPath of the object. + - objects: The field of objects. + */ + public func removeRelation(_ key: (String, WritableKeyPath), + objects: [V]) throws -> Self where V: ParseObject { + guard let target = self.target else { + throw ParseError(code: .unknownError, message: "Target shouldn't be nil") + } + var mutableOperation = self + mutableOperation.operations[key.0] = try RemoveRelation(objects: objects) + let values = target[keyPath: key.1] + var set = Set(values) + objects.forEach { + set.remove($0) + } + mutableOperation.target?[keyPath: key.1] = Array(set) + return mutableOperation + } + + /** + An operation that removes every instance of a relation from + an array field. + - Parameters: + - key: A tuple consisting of the key and KeyPath of the object. + - objects: The field of objects. + */ + public func removeRelation(_ key: (String, WritableKeyPath), + objects: [V]) throws -> Self where V: ParseObject { + guard let target = self.target else { + throw ParseError(code: .unknownError, message: "Target shouldn't be nil") + } + var mutableOperation = self + mutableOperation.operations[key.0] = try RemoveRelation(objects: objects) + let values = target[keyPath: key.1] + var set = Set(values ?? []) + objects.forEach { + set.remove($0) + } + mutableOperation.target?[keyPath: key.1] = Array(set) + return mutableOperation + } + + /** + An operation where a field is deleted from the object. + - parameter key: The key of the object. + */ + public func unset(_ key: String) -> Self { + var mutableOperation = self + mutableOperation.operations[key] = Delete() + return mutableOperation + } + + /** + An operation where a field is deleted from the object. + - Parameters: + - key: A tuple consisting of the key and KeyPath of the object. + */ + public func unset(_ key: (String, WritableKeyPath)) -> Self where V: Encodable { + var mutableOperation = self + mutableOperation.operations[key.0] = Delete() + mutableOperation.target?[keyPath: key.1] = nil + return mutableOperation + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: RawCodingKey.self) + try operations.forEach { pair in + let (key, value) = pair + let encoder = container.superEncoder(forKey: .key(key)) + try value.encode(to: encoder) + } + } +} + +extension ParseOperation { + /** + Saves the operations on the `ParseObject` *synchronously* and throws an error if there's an issue. + + - 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`. + */ + public func save(options: API.Options = []) throws -> T { + guard let target = self.target else { + throw ParseError(code: .unknownError, message: "Target shouldn't be nil") + } + if !target.isSaved { + throw ParseError(code: .missingObjectId, message: "ParseObject isn't saved.") + } + return try saveCommand() + .execute(options: options) + } + + /** + Saves the operations on the `ParseObject` *asynchronously* and executes the given callback block. + + - 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)`. + */ + public func save( + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void + ) { + guard let target = self.target else { + callbackQueue.async { + let error = ParseError(code: .missingObjectId, message: "ParseObject isn't saved.") + completion(.failure(error)) + } + return + } + if !target.isSaved { + callbackQueue.async { + let error = ParseError(code: .missingObjectId, message: "ParseObject isn't saved.") + completion(.failure(error)) + } + return + } + do { + try self.saveCommand().executeAsync(options: options) { result in + callbackQueue.async { + completion(result) + } + } + } catch { + callbackQueue.async { + let error = ParseError(code: .missingObjectId, message: "ParseObject isn't saved.") + completion(.failure(error)) + } + } + } + + func saveCommand() throws -> API.NonParseBodyCommand, T> { + guard let target = self.target else { + throw ParseError(code: .unknownError, message: "Target shouldn't be nil") + } + return API.NonParseBodyCommand(method: .PUT, path: target.endpoint, body: self) { + try ParseCoding.jsonDecoder().decode(UpdateResponse.self, from: $0).apply(to: target) + } + } +} diff --git a/Sources/ParseSwift/Mutation Operations/RemoveOperation.swift b/Sources/ParseSwift/Operations/Remove.swift similarity index 71% rename from Sources/ParseSwift/Mutation Operations/RemoveOperation.swift rename to Sources/ParseSwift/Operations/Remove.swift index 9bb4e8b6b..7022b7c24 100644 --- a/Sources/ParseSwift/Mutation Operations/RemoveOperation.swift +++ b/Sources/ParseSwift/Operations/Remove.swift @@ -1,5 +1,5 @@ // -// RemoveOperation.swift +// Remove.swift // Parse // // Created by Florent Vilmart on 17-07-24. @@ -8,7 +8,7 @@ import Foundation -internal struct RemoveOperation: Encodable where T: Encodable { +internal struct Remove: Encodable where T: Encodable { let __op: String = "Remove" // swiftlint:disable:this identifier_name let objects: [T] } diff --git a/Sources/ParseSwift/Operations/RemoveRelation.swift b/Sources/ParseSwift/Operations/RemoveRelation.swift new file mode 100644 index 000000000..2d9b8e142 --- /dev/null +++ b/Sources/ParseSwift/Operations/RemoveRelation.swift @@ -0,0 +1,18 @@ +// +// RemoveRelation.swift +// ParseSwift +// +// Created by Corey Baker on 1/17/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +import Foundation + +internal struct RemoveRelation: Encodable where T: ParseObject { + let __op: String = "RemoveRelation" // swiftlint:disable:this identifier_name + let objects: [Pointer] + + init(objects: [T]) throws { + self.objects = try objects.compactMap { try $0.toPointer() } + } +} diff --git a/Sources/ParseSwift/Parse.swift b/Sources/ParseSwift/Parse.swift index 8dbeee798..9c483f293 100644 --- a/Sources/ParseSwift/Parse.swift +++ b/Sources/ParseSwift/Parse.swift @@ -1,4 +1,7 @@ import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif // swiftlint:disable line_length @@ -14,63 +17,72 @@ internal struct ParseConfiguration { } /** - `ParseSwift` contains static functions that handle global configuration for the Parse framework. - - parameter applicationId: The application id of your Parse application. - - parameter clientKey: The client key of your Parse application. - - parameter masterKey: The master key of your Parse application. - - parameter serverURL: The server URL to connect to Parse Server. - - parameter liveQueryServerURL: The server URL to connect to Parse Server. - - parameter primitiveObjectStore: A key/value store that conforms to the `PrimitiveObjectStore` - protocol. Defaults to `nil` in which one will be created an memory, but never persisted. - - parameter authentication: A callback block that will be used to receive/accept/decline network challenges. - Defaults to `nil` in which the SDK will use the default OS authentication methods for challenges. - It should have the following argument signature: `(challenge: URLAuthenticationChallenge, - completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void`. - See Apple's [documentation](https://developer.apple.com/documentation/foundation/urlsessiontaskdelegate/1411595-urlsession) for more for details. + `ParseSwift` contains static methods to handle global configuration for the Parse framework. */ -public func initialize( - applicationId: String, - clientKey: String? = nil, - masterKey: String? = nil, - serverURL: URL, - liveQueryServerURL: URL? = nil, - primitiveObjectStore: PrimitiveObjectStore? = nil, - authentication: ((URLAuthenticationChallenge, - (URLSession.AuthChallengeDisposition, - URLCredential?) -> Void) -> Void)? = nil -) { - ParseConfiguration.applicationId = applicationId - ParseConfiguration.clientKey = clientKey - ParseConfiguration.masterKey = masterKey - ParseConfiguration.serverURL = serverURL - ParseConfiguration.liveQuerysServerURL = liveQueryServerURL - ParseConfiguration.mountPath = "/" + serverURL.pathComponents - .filter { $0 != "/" } - .joined(separator: "/") - ParseStorage.shared.use(primitiveObjectStore ?? CodableInMemoryPrimitiveObjectStore()) - ParseConfiguration.sessionDelegate = ParseURLSessionDelegate(callbackQueue: .main, authentication: authentication) - //Prepare installation - DispatchQueue.main.async { - _ = BaseParseInstallation() +public struct ParseSwift { + + /** + Configure the Parse Server. This should only be used when starting your app. Typically in the + `application(... didFinishLaunchingWithOptions launchOptions...)`. + - parameter applicationId: The application id of your Parse application. + - parameter clientKey: The client key of your Parse application. + - parameter masterKey: The master key of your Parse application. + - parameter serverURL: The server URL to connect to Parse Server. + - parameter liveQueryServerURL: The server URL to connect to Parse Server. + - parameter keyValueStore: A key/value store that conforms to the `ParseKeyValueStore` + protocol. Defaults to `nil` in which one will be created an memory, but never persisted. For Linux, this + this is the only store available since there is no Keychain. Linux users should replace this store with an + encrypted one. + - parameter authentication: A callback block that will be used to receive/accept/decline network challenges. + Defaults to `nil` in which the SDK will use the default OS authentication methods for challenges. + It should have the following argument signature: `(challenge: URLAuthenticationChallenge, + completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void`. + See Apple's [documentation](https://developer.apple.com/documentation/foundation/urlsessiontaskdelegate/1411595-urlsession) for more for details. + */ + static public func initialize( + applicationId: String, + clientKey: String? = nil, + masterKey: String? = nil, + serverURL: URL, + liveQueryServerURL: URL? = nil, + keyValueStore: ParseKeyValueStore? = nil, + authentication: ((URLAuthenticationChallenge, + (URLSession.AuthChallengeDisposition, + URLCredential?) -> Void) -> Void)? = nil + ) { + ParseConfiguration.applicationId = applicationId + ParseConfiguration.clientKey = clientKey + ParseConfiguration.masterKey = masterKey + ParseConfiguration.serverURL = serverURL + ParseConfiguration.liveQuerysServerURL = liveQueryServerURL + ParseConfiguration.mountPath = "/" + serverURL.pathComponents + .filter { $0 != "/" } + .joined(separator: "/") + ParseStorage.shared.use(keyValueStore ?? InMemoryKeyValueStore()) + ParseConfiguration.sessionDelegate = ParseURLSessionDelegate(callbackQueue: .main, authentication: authentication) + //Prepare installation + DispatchQueue.main.async { + _ = BaseParseInstallation() + } } -} -internal func initialize(applicationId: String, - clientKey: String? = nil, - masterKey: String? = nil, - serverURL: URL, - liveQueryServerURL: URL? = nil, - primitiveObjectStore: PrimitiveObjectStore? = nil, - testing: Bool = false, - authentication: ((URLAuthenticationChallenge, - (URLSession.AuthChallengeDisposition, - URLCredential?) -> Void) -> Void)? = nil) { - ParseConfiguration.isTestingSDK = testing + internal static func initialize(applicationId: String, + clientKey: String? = nil, + masterKey: String? = nil, + serverURL: URL, + liveQueryServerURL: URL? = nil, + primitiveObjectStore: ParseKeyValueStore? = nil, + testing: Bool = false, + authentication: ((URLAuthenticationChallenge, + (URLSession.AuthChallengeDisposition, + URLCredential?) -> Void) -> Void)? = nil) { + ParseConfiguration.isTestingSDK = testing - initialize(applicationId: applicationId, - clientKey: clientKey, - masterKey: masterKey, - serverURL: serverURL, - liveQueryServerURL: liveQueryServerURL, - authentication: authentication) + initialize(applicationId: applicationId, + clientKey: clientKey, + masterKey: masterKey, + serverURL: serverURL, + liveQueryServerURL: liveQueryServerURL, + authentication: authentication) + } } diff --git a/Sources/ParseSwift/Protocols/Objectable.swift b/Sources/ParseSwift/Protocols/Objectable.swift index 3cb1b9d15..ee697d773 100644 --- a/Sources/ParseSwift/Protocols/Objectable.swift +++ b/Sources/ParseSwift/Protocols/Objectable.swift @@ -56,7 +56,10 @@ extension Objectable { #if !os(Linux) return ParseHash.md5HashFromData(encoded) #else - return String(data: encoded, encoding: .utf8) + guard let hashString = String(data: encoded, encoding: .utf8) else { + throw ParseError(code: .unknownError, message: "Couldn't create hash") + } + return hashString #endif } } @@ -75,8 +78,8 @@ extension Objectable { return objectId != nil } - func toPointer() -> PointerType { - return PointerType(self) + func toPointer() throws -> PointerType { + return try PointerType(self) } } diff --git a/Sources/ParseSwift/Storage/PrimitiveObjectStore.swift b/Sources/ParseSwift/Storage/ParseKeyValueStore.swift similarity index 87% rename from Sources/ParseSwift/Storage/PrimitiveObjectStore.swift rename to Sources/ParseSwift/Storage/ParseKeyValueStore.swift index 1a0127b09..13eb25a75 100644 --- a/Sources/ParseSwift/Storage/PrimitiveObjectStore.swift +++ b/Sources/ParseSwift/Storage/ParseKeyValueStore.swift @@ -1,5 +1,5 @@ // -// PrimitiveObjectStore.swift +// ParseKeyValueStore.swift // // // Created by Pranjal Satija on 7/19/20. @@ -7,12 +7,11 @@ import Foundation -// MARK: PrimitiveObjectStore /** A store that supports key/value storage. It should be able to handle any object that conforms to encodable and decodable. */ -public protocol PrimitiveObjectStore { +public protocol ParseKeyValueStore { /// Delete an object from the store. /// - parameter key: The unique key value of the object. mutating func delete(valueFor key: String) throws @@ -27,10 +26,12 @@ public protocol PrimitiveObjectStore { mutating func set(_ object: T, for key: String) throws } -/// A `PrimitiveObjectStore` that lives in memory for unit testing purposes. +// MARK: InMemoryKeyValueStore + +/// A `ParseKeyValueStore` that lives in memory for unit testing purposes. /// It works by encoding / decoding all values just like a real `Codable` store would /// but it stores all values as `Data` blobs in memory. -struct CodableInMemoryPrimitiveObjectStore: PrimitiveObjectStore { +struct InMemoryKeyValueStore: ParseKeyValueStore { var decoder = JSONDecoder() var encoder = JSONEncoder() var storage = [String: Data]() @@ -54,8 +55,10 @@ struct CodableInMemoryPrimitiveObjectStore: PrimitiveObjectStore { } } -// MARK: KeychainStore + PrimitiveObjectStore -extension KeychainStore: PrimitiveObjectStore { +#if !os(Linux) + +// MARK: KeychainStore + ParseKeyValueStore +extension KeychainStore: ParseKeyValueStore { func delete(valueFor key: String) throws { if !removeObject(forKey: key) { @@ -80,3 +83,5 @@ extension KeychainStore: PrimitiveObjectStore { } } } + +#endif diff --git a/Sources/ParseSwift/Storage/ParseStorage.swift b/Sources/ParseSwift/Storage/ParseStorage.swift index d5f274a06..be73207f1 100644 --- a/Sources/ParseSwift/Storage/ParseStorage.swift +++ b/Sources/ParseSwift/Storage/ParseStorage.swift @@ -9,9 +9,9 @@ public struct ParseStorage { public static var shared = ParseStorage() - private var backingStore: PrimitiveObjectStore! + private var backingStore: ParseKeyValueStore! - mutating func use(_ store: PrimitiveObjectStore) { + mutating func use(_ store: ParseKeyValueStore) { self.backingStore = store } @@ -29,8 +29,8 @@ public struct ParseStorage { } } -// MARK: PrimitiveObjectStore -extension ParseStorage: PrimitiveObjectStore { +// MARK: ParseKeyValueStore +extension ParseStorage: ParseKeyValueStore { public mutating func delete(valueFor key: String) throws { requireBackingStore() return try backingStore.delete(valueFor: key) diff --git a/Sources/ParseSwift/Types/Parse.h b/Sources/ParseSwift/Types/Parse.h new file mode 100644 index 000000000..2c6a817fb --- /dev/null +++ b/Sources/ParseSwift/Types/Parse.h @@ -0,0 +1,19 @@ +// +// Parse.h +// Parse +// +// Created by Florent Vilmart on 17-07-23. +// Copyright © 2017 Parse. All rights reserved. +// + +#import + +//! Project version number for Parse. +FOUNDATION_EXPORT double ParseVersionNumber; + +//! Project version string for Parse. +FOUNDATION_EXPORT const unsigned char ParseVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/Sources/ParseSwift/Types/ParseACL.swift b/Sources/ParseSwift/Types/ParseACL.swift index e7957520d..5a795a412 100644 --- a/Sources/ParseSwift/Types/ParseACL.swift +++ b/Sources/ParseSwift/Types/ParseACL.swift @@ -64,72 +64,140 @@ public struct ParseACL: ParseType, Decodable, Equatable, Hashable { /** Returns true if a particular key has a specific access level. - - parameter key: The `ParseObject.objectId` of the user for which to retrieve access. + - parameter key: The key of the `ParseUser` or `ParseRole` for which to retrieve access. - parameter access: The type of access. - - returns: `true` if the user with this `key` has *explicit* access, otherwise `false`. + - returns: `true` if the `key` has *explicit* access, otherwise `false`. */ - public func get(_ key: String, access: Access) -> Bool { + func get(_ key: String, access: Access) -> Bool { guard let acl = acl else { // no acl, all open! return false } return acl[key]?[access] ?? false } + // MARK: ParseUser /** - Gets whether the given user id is *explicitly* allowed to read this object. + Gets whether the given `objectId` is *explicitly* allowed to read this object. Even if this returns `false`, the user may still be able to access it if `publicReadAccess` returns `true` or if the user belongs to a role that has access. - - parameter userId: The `ParseObject.objectId` of the user for which to retrieve access. + - parameter objectId: The `ParseUser.objectId` of the user for which to retrieve access. - returns: `true` if the user with this `objectId` has *explicit* read access, otherwise `false`. */ - public func getReadAccess(userId: String) -> Bool { - return get(userId, access: .read) + public func getReadAccess(objectId: String) -> Bool { + get(objectId, access: .read) } /** - Gets whether the given user id is *explicitly* allowed to write this object. + Gets whether the given `ParseUser` is *explicitly* allowed to read this object. + Even if this returns `false`, the user may still be able to access it if `publicReadAccess` returns `true` + or if the user belongs to a role that has access. + + - parameter user: The `ParseUser` for which to retrieve access. + - returns: `true` if the user with this `ParseUser` has *explicit* read access, otherwise `false`. + */ + public func getReadAccess(user: T) -> Bool where T: ParseUser { + if let objectId = user.objectId { + return get(objectId, access: .read) + } else { + return false + } + } + + /** + Gets whether the given `objectId` is *explicitly* allowed to write this object. Even if this returns false, the user may still be able to write it if `publicWriteAccess` returns `true` or if the user belongs to a role that has access. - - parameter userId: The `ParseObject.objectId` of the user for which to retrieve access. + - parameter objectId: The `ParseUser.objectId` of the user for which to retrieve access. + - returns: `true` if the user with this `ParseUser.objectId` has *explicit* write access, otherwise `false`. + */ + public func getWriteAccess(objectId: String) -> Bool { + return get(objectId, access: .write) + } + + /** + Gets whether the given `ParseUser` is *explicitly* allowed to write this object. + Even if this returns false, the user may still be able to write it if `publicWriteAccess` returns `true` + or if the user belongs to a role that has access. - - returns: `true` if the user with this `ParseObject.objectId` has *explicit* write access, otherwise `false`. + - parameter user: The `ParseUser` of the user for which to retrieve access. + - returns: `true` if the `ParseUser` has *explicit* write access, otherwise `false`. */ - public func getWriteAccess(userId: String) -> Bool { - return get(userId, access: .write) + public func getWriteAccess(user: T) -> Bool where T: ParseUser { + if let objectId = user.objectId { + return get(objectId, access: .write) + } else { + return false + } } /** - Set whether the given `userId` is allowed to read this object. + Set whether the given `objectId` is allowed to read this object. - - parameter value: Whether the given user can write this object. - - parameter userId: The `ParseObject.objectId` of the user to assign access. + - parameter value: Whether the given user can read this object. + - parameter objectId: The `ParseUser.objectId` of the user to assign access. */ - public mutating func setReadAccess(userId: String, value: Bool) { - set(userId, access: .read, value: value) + public mutating func setReadAccess(objectId: String, value: Bool) { + set(objectId, access: .read, value: value) } /** - Set whether the given `userId` is allowed to write this object. + Set whether the given `ParseUser` is allowed to read this object. - parameter value: Whether the given user can read this object. - - parameter userId: The `ParseObject.objectId` of the user to assign access. + - parameter user: The `ParseUser` to assign access. + */ + public mutating func setReadAccess(user: T, value: Bool) where T: ParseUser { + if let objectId = user.objectId { + set(objectId, access: .read, value: value) + } + } + + /** + Set whether the given `objectId` is allowed to write this object. + + - parameter value: Whether the given user can write this object. + - parameter objectId: The `ParseUser.objectId` of the user to assign access. */ - public mutating func setWriteAccess(userId: String, value: Bool) { - set(userId, access: .write, value: value) + public mutating func setWriteAccess(objectId: String, value: Bool) { + set(objectId, access: .write, value: value) } + /** + Set whether the given `ParseUser` is allowed to write this object. + + - parameter value: Whether the given user can write this object. + - parameter user: The `ParseUser` to assign access. + */ + public mutating func setWriteAccess(user: T, value: Bool) where T: ParseUser { + if let objectId = user.objectId { + set(objectId, access: .write, value: value) + } + } + + // MARK: ParseRole + /** Get whether users belonging to the role with the given name are allowed to read this object. Even if this returns `false`, the role may still be able to read it if a parent role has read access. - parameter roleName: The name of the role. - - returns: `true` if the role has read access, otherwise `false`. */ public func getReadAccess(roleName: String) -> Bool { - return get(toRole(roleName: roleName), access: .read) + get(toRole(roleName: roleName), access: .read) + } + + /** + Get whether users belonging to the role are allowed to read this object. + Even if this returns `false`, the role may still be able to read it if a parent role has read access. + + - parameter role: The `ParseRole` to get access for. + - returns: `true` if the `ParseRole` has read access, otherwise `false`. + */ + public func getReadAccess(role: T) -> Bool where T: ParseRole { + get(toRole(roleName: role.name), access: .read) } /** @@ -137,11 +205,21 @@ public struct ParseACL: ParseType, Decodable, Equatable, Hashable { Even if this returns `false`, the role may still be able to write it if a parent role has write access. - parameter roleName: The name of the role. - - returns: `true` if the role has read access, otherwise `false`. */ public func getWriteAccess(roleName: String) -> Bool { - return get(toRole(roleName: roleName), access: .write) + get(toRole(roleName: roleName), access: .write) + } + + /** + Get whether users belonging to the role are allowed to write this object. + Even if this returns `false`, the role may still be able to write it if a parent role has write access. + + - parameter role: The `ParseRole` to get access for. + - returns: `true` if the role has read access, otherwise `false`. + */ + public func getWriteAccess(role: T) -> Bool where T: ParseRole { + get(toRole(roleName: role.name), access: .write) } /** @@ -154,6 +232,16 @@ public struct ParseACL: ParseType, Decodable, Equatable, Hashable { set(toRole(roleName: roleName), access: .read, value: value) } + /** + Set whether users belonging to the role are allowed to read this object. + + - parameter value: Whether the given role can read this object. + - parameter role: The `ParseRole` to set access for. + */ + public mutating func setReadAccess(role: T, value: Bool) where T: ParseRole { + set(toRole(roleName: role.name), access: .read, value: value) + } + /** Set whether users belonging to the role with the given name are allowed to write this object. @@ -164,6 +252,16 @@ public struct ParseACL: ParseType, Decodable, Equatable, Hashable { set(toRole(roleName: roleName), access: .write, value: value) } + /** + Set whether users belonging to the role are allowed to write this object. + + - parameter allowed: Whether the given role can write this object. + - parameter role: The `ParseRole` to set access for. + */ + public mutating func setWriteAccess(role: T, value: Bool) where T: ParseRole { + set(toRole(roleName: role.name), access: .write, value: value) + } + private func toRole(roleName: String) -> String { "role:\(roleName)" } @@ -275,12 +373,12 @@ extension ParseACL { } private static func setDefaultAccess(_ acl: ParseACL) -> ParseACL? { - guard let userObjectId = BaseParseUser.current?.objectId else { + guard let currentUser = BaseParseUser.current else { return nil } var modifiedACL = acl - modifiedACL.setReadAccess(userId: userObjectId, value: true) - modifiedACL.setWriteAccess(userId: userObjectId, value: true) + modifiedACL.setReadAccess(user: currentUser, value: true) + modifiedACL.setWriteAccess(user: currentUser, value: true) return modifiedACL } diff --git a/Sources/ParseSwift/Types/ParseFile.swift b/Sources/ParseSwift/Types/ParseFile.swift index d5e6a892f..8d56bf9e1 100644 --- a/Sources/ParseSwift/Types/ParseFile.swift +++ b/Sources/ParseSwift/Types/ParseFile.swift @@ -1,4 +1,7 @@ import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif /** A `ParseFile` object representes a file of binary data stored on the Parse server. diff --git a/Sources/ParseSwift/Types/ParseRelation.swift b/Sources/ParseSwift/Types/ParseRelation.swift new file mode 100644 index 000000000..085a3c7d3 --- /dev/null +++ b/Sources/ParseSwift/Types/ParseRelation.swift @@ -0,0 +1,224 @@ +// +// ParseRelation.swift +// ParseSwift +// +// Created by Corey Baker on 1/18/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +import Foundation + +/** + The `ParseRelation` class that is used to access all of the children of a many-to-many relationship. + Each instance of `ParseRelation` is associated with a particular parent object and key. + */ +public struct ParseRelation: Codable where T: ParseObject { + internal let __type: String = "Relation" // swiftlint:disable:this identifier_name + + /// The parent `ParseObject` + public var parent: T? + + /// The name of the class of the target child objects. + public var className: String? + + var key: String? + + /** + Create a `ParseRelation` with a specific parent and key. + - parameters: + - parent: The parent `ParseObject`. + - key: The key for the relation. + - className: The name of the child class for the relation. + */ + public init(parent: T, key: String? = nil, className: String? = nil) { + self.parent = parent + self.key = key + self.className = className + } + + /** + Create a `ParseRelation` with a specific parent and child. + - parameters: + - parent: The parent `ParseObject`. + - key: The key for the relation. + - child: The child `ParseObject`. + */ + public init(parent: T, key: String? = nil, child: U? = nil) where U: ParseObject { + self.parent = parent + self.key = key + self.className = child?.className + } + + enum CodingKeys: String, CodingKey { + case className + case __type // swiftlint:disable:this identifier_name + } + + /** + Adds a relation to the respective objects. + - parameters: + - key: The key for the relation. + - objects: An array of `ParseObject`'s to add relation to. + - throws: An error of type `ParseError`. + */ + public func add(_ key: String, objects: [U]) throws -> ParseOperation where U: ParseObject { + guard let parent = parent else { + throw ParseError(code: .unknownError, message: "ParseRelation must have the parent set before removing.") + } + if let currentKey = self.key { + if currentKey != key { + throw ParseError(code: .unknownError, message: "All objects have be related to the same key.") + } + } + if !isSameClass(objects) { + throw ParseError(code: .unknownError, message: "All objects have to have the same className.") + } + + return try parent.operation.addRelation(key, objects: objects) + } + + /** + Removes a relation to the respective objects. + - parameters: + - key: The key for the relation. + - objects: An array of `ParseObject`'s to remove relation to. + - throws: An error of type `ParseError`. + */ + public func remove(_ key: String, objects: [U]) throws -> ParseOperation where U: ParseObject { + guard let parent = parent else { + throw ParseError(code: .unknownError, message: "ParseRelation must have the parent set before removing.") + } + if let currentKey = self.key { + if currentKey != key { + throw ParseError(code: .unknownError, message: "All objects have be related to the same key.") + } + } + if !isSameClass(objects) { + throw ParseError(code: .unknownError, message: "All objects have to have the same className.") + } + return try parent.operation.removeRelation(key, objects: objects) + } + + /** + Returns a `Query` that is limited to objects in this relation. + - parameter child: The child class for the relation. + - throws: An error of type `ParseError`. + - returns: A relation query. + */ + public func query(_ child: U) throws -> Query where U: ParseObject { + + guard let parent = self.parent else { + throw ParseError(code: .unknownError, + message: "ParseRelation must have the parent set before querying.") + } + guard let key = self.key else { + throw ParseError(code: .unknownError, + message: "ParseRelation must have the key set before querying.") + } + if !isSameClass([child]) { + throw ParseError(code: .unknownError, + message: "ParseRelation must have the same child class as the original relation.") + } + return Query(related(key: key, object: try parent.toPointer())) + } + + func isSameClass(_ objects: [U]) -> Bool where U: ParseObject { + guard let first = objects.first?.className else { + return true + } + if className != nil { + if className != first { + return false + } + } else { + return false + } + let sameClassObjects = objects.filter({ $0.className == first }) + return sameClassObjects.count == objects.count + } +} + +// MARK: Convenience +public extension ParseRelation { + + /** + Adds a relation to the respective `ParseUser`'s with `key = "users"`. + - parameters: + - users: An array of `ParseUser`'s to add relation to. + - throws: An error of type `ParseError`. + */ + func add(_ users: [U]) throws -> ParseOperation where U: ParseUser { + guard let key = self.key else { + return try add("users", objects: users) + } + return try add(key, objects: users) + } + + /** + Adds a relation to the respective `ParseRole`'s with `key = "roles"`. + - parameters: + - roles: An array of `ParseRole`'s to add relation to. + - throws: An error of type `ParseError`. + */ + func add(_ roles: [U]) throws -> ParseOperation where U: ParseRole { + guard let key = self.key else { + return try add("roles", objects: roles) + } + return try add(key, objects: roles) + } + + /** + Removes a relation to the respective `ParseUser`'s with `key = "users"`. + - parameters: + - users: An array of `ParseUser`'s to add relation to. + - throws: An error of type `ParseError`. + */ + func remove(_ users: [U]) throws -> ParseOperation where U: ParseUser { + guard let key = self.key else { + return try remove("users", objects: users) + } + return try remove(key, objects: users) + } + + /** + Removes a relation to the respective `ParseRole`'s with `key = "roles"`. + - parameters: + - roles: An array of `ParseRole`'s to add relation to. + - throws: An error of type `ParseError`. + */ + func remove(_ roles: [U]) throws -> ParseOperation where U: ParseRole { + guard let key = self.key else { + return try remove("roles", objects: roles) + } + return try remove(key, objects: roles) + } +} + +// MARK: ParseRelation +public extension ParseObject { + + /// Create a new relation. + var relation: ParseRelation { + return ParseRelation(parent: self) + } + + /** + Create a new relation with a specific key. + - parameter key: A key for the relation. + - parameter className: The name of the child class for the relation. + - returns: A new `ParseRelation`. + */ + func relation(_ key: String, className: String? = nil) -> ParseRelation { + ParseRelation(parent: self, key: key, className: className) + } + + /** + Create a new relation to a specific child. + - parameter key: A key for the relation. + - parameter child: The child `ParseObject`. + - returns: A new `ParseRelation`. + */ + func relation(_ key: String, child: U? = nil) -> ParseRelation where U: ParseObject { + ParseRelation(parent: self, key: key, child: child) + } +} diff --git a/Sources/ParseSwift/Types/Pointer.swift b/Sources/ParseSwift/Types/Pointer.swift index 88d9ad5a2..5186bfe2f 100644 --- a/Sources/ParseSwift/Types/Pointer.swift +++ b/Sources/ParseSwift/Types/Pointer.swift @@ -1,15 +1,15 @@ import Foundation -private func getObjectId(target: T) -> String { +private func getObjectId(target: T) throws -> String { guard let objectId = target.objectId else { - fatalError("Cannot set a pointer to an unsaved object") + throw ParseError(code: .missingObjectId, message: "Cannot set a pointer to an unsaved object") } return objectId } -private func getObjectId(target: Objectable) -> String { +private func getObjectId(target: Objectable) throws -> String { guard let objectId = target.objectId else { - fatalError("Cannot set a pointer to an unsaved object") + throw ParseError(code: .missingObjectId, message: "Cannot set a pointer to an unsaved object") } return objectId } @@ -21,8 +21,8 @@ public struct Pointer: Fetchable, Encodable { public var objectId: String public var className: String - public init(_ target: T) { - self.objectId = getObjectId(target: target) + public init(_ target: T) throws { + self.objectId = try getObjectId(target: target) self.className = target.className } @@ -71,8 +71,8 @@ internal struct PointerType: Encodable { public var objectId: String public var className: String - public init(_ target: Objectable) { - self.objectId = getObjectId(target: target) + public init(_ target: Objectable) throws { + self.objectId = try getObjectId(target: target) self.className = target.className } diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 93122eed5..7c8e44547 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -31,11 +31,13 @@ public struct QueryConstraint: Encodable, Equatable { case nearSphere = "$nearSphere" case or = "$or" //swiftlint:disable:this identifier_name case and = "$and" + case nor = "$nor" case relatedTo = "$relatedTo" case within = "$within" case geoWithin = "$geoWithin" case geoIntersects = "$geoIntersects" case maxDistance = "$maxDistance" + case centerSphere = "$centerSphere" case box = "$box" case polygon = "$polygon" case point = "$point" @@ -44,6 +46,7 @@ public struct QueryConstraint: Encodable, Equatable { case term = "$term" case regexOptions = "$options" case object = "object" + case relativeTime = "$relativeTime" } var key: String @@ -70,27 +73,27 @@ public struct QueryConstraint: Encodable, Equatable { } public func > (key: String, value: T) -> QueryConstraint where T: Encodable { - return QueryConstraint(key: key, value: value, comparator: .greaterThan) + QueryConstraint(key: key, value: value, comparator: .greaterThan) } public func >= (key: String, value: T) -> QueryConstraint where T: Encodable { - return QueryConstraint(key: key, value: value, comparator: .greaterThanOrEqualTo) + QueryConstraint(key: key, value: value, comparator: .greaterThanOrEqualTo) } public func < (key: String, value: T) -> QueryConstraint where T: Encodable { - return QueryConstraint(key: key, value: value, comparator: .lessThan) + QueryConstraint(key: key, value: value, comparator: .lessThan) } public func <= (key: String, value: T) -> QueryConstraint where T: Encodable { - return QueryConstraint(key: key, value: value, comparator: .lessThanOrEqualTo) + QueryConstraint(key: key, value: value, comparator: .lessThanOrEqualTo) } public func == (key: String, value: T) -> QueryConstraint where T: Encodable { - return QueryConstraint(key: key, value: value) + QueryConstraint(key: key, value: value) } public func != (key: String, value: T) -> QueryConstraint where T: Encodable { - return QueryConstraint(key: key, value: value, comparator: .notEqualTo) + QueryConstraint(key: key, value: value, comparator: .notEqualTo) } internal struct InQuery: Encodable where T: ParseObject { @@ -138,6 +141,16 @@ public func or (queries: [Query]) -> QueryConstraint where T: Encodable { return QueryConstraint(key: QueryConstraint.Comparator.or.rawValue, value: orQueries) } +/** + Returns a `Query` that is the `nor` of the passed in queries. + - parameter queries: The list of queries to `or` together. + - returns: An instance of `QueryConstraint`'s that are the `nor` of the passed in queries. + */ +public func nor (queries: [Query]) -> QueryConstraint where T: Encodable { + let orQueries = queries.map { OrAndQuery(query: $0) } + return QueryConstraint(key: QueryConstraint.Comparator.nor.rawValue, value: orQueries) +} + /** Constructs a Query that is the AND of the passed in queries. For example: @@ -162,7 +175,7 @@ public func and (queries: [Query]) -> QueryConstraint where T: Encodable { - returns: The same instance of `QueryConstraint` as the receiver. */ public func == (key: String, value: Query) -> QueryConstraint { - return QueryConstraint(key: key, value: InQuery(query: value), comparator: .inQuery) + QueryConstraint(key: key, value: InQuery(query: value), comparator: .inQuery) } /** @@ -173,7 +186,7 @@ public func == (key: String, value: Query) -> QueryConstraint { - returns: The same instance of `QueryConstraint` as the receiver. */ public func != (key: String, query: Query) -> QueryConstraint { - return QueryConstraint(key: key, value: InQuery(query: query), comparator: .notInQuery) + QueryConstraint(key: key, value: InQuery(query: query), comparator: .notInQuery) } /** @@ -210,7 +223,7 @@ public func doesNotMatchKeyInQuery (key: String, queryKey: String, query: Que - returns: The same instance of `QueryConstraint` as the receiver. */ public func containedIn (key: String, array: [T]) -> QueryConstraint where T: Encodable { - return QueryConstraint(key: key, value: array, comparator: .containedIn) + QueryConstraint(key: key, value: array, comparator: .containedIn) } /** @@ -221,7 +234,7 @@ public func containedIn (key: String, array: [T]) -> QueryConstraint where T: - returns: The same instance of `QueryConstraint` as the receiver. */ public func notContainedIn (key: String, array: [T]) -> QueryConstraint where T: Encodable { - return QueryConstraint(key: key, value: array, comparator: .notContainedIn) + QueryConstraint(key: key, value: array, comparator: .notContainedIn) } /** @@ -232,7 +245,31 @@ public func notContainedIn (key: String, array: [T]) -> QueryConstraint where - returns: The same instance of `QueryConstraint` as the receiver. */ public func containsAll (key: String, array: [T]) -> QueryConstraint where T: Encodable { - return QueryConstraint(key: key, value: array, comparator: .all) + QueryConstraint(key: key, value: array, comparator: .all) +} + +/** + Add a constraint to the query that requires a particular key's object + to be contained by the provided array. Get objects where all array elements match. + - parameter key: The key to be constrained. + - parameter array: The possible values for the key's object. + - returns: The same instance of `QueryConstraint` as the receiver. + */ +public func containedBy (key: String, array: [T]) -> QueryConstraint where T: Encodable { + QueryConstraint(key: key, value: array, comparator: .containedBy) +} + +/** + Add a constraint to the query that requires a particular key's time is related to a specified time. E.g. "3 days ago". + + - parameter key: The key to be constrained. Should be a Date field. + - parameter comparator: How the relative time should be compared. Currently only supports the + $lt, $lte, $gt, and $gte operators. + - parameter time: The reference time, e.g. "12 days ago". + - returns: The same instance of `QueryConstraint` as the receiver. + */ +public func relative(key: String, comparator: QueryConstraint.Comparator, time: String) -> QueryConstraint { + QueryConstraint(key: key, value: [QueryConstraint.Comparator.relativeTime.rawValue: time], comparator: comparator) } /** @@ -244,7 +281,7 @@ public func containsAll (key: String, array: [T]) -> QueryConstraint where T: - returns: The same instance of `QueryConstraint` as the receiver. */ public func near(key: String, geoPoint: ParseGeoPoint) -> QueryConstraint { - return QueryConstraint(key: key, value: geoPoint, comparator: .nearSphere) + QueryConstraint(key: key, value: geoPoint, comparator: .nearSphere) } /** @@ -254,12 +291,23 @@ public func near(key: String, geoPoint: ParseGeoPoint) -> QueryConstraint { - parameter key: The key to be constrained. - parameter geoPoint: The reference point as a `ParseGeoPoint`. - parameter distance: Maximum distance in radians. + - parameter sorted: `true` if results should be sorted by distance ascending, `false` is no sorting is required. + Defaults to true. - returns: The same instance of `QueryConstraint` as the receiver. */ -public func withinRadians(key: String, geoPoint: ParseGeoPoint, distance: Double) -> [QueryConstraint] { - var constraints = [QueryConstraint(key: key, value: geoPoint, comparator: .nearSphere)] - constraints.append(.init(key: key, value: distance, comparator: .maxDistance)) - return constraints +public func withinRadians(key: String, + geoPoint: ParseGeoPoint, + distance: Double, + sorted: Bool = true) -> [QueryConstraint] { + if sorted { + var constraints = [QueryConstraint(key: key, value: geoPoint, comparator: .nearSphere)] + constraints.append(.init(key: key, value: distance, comparator: .maxDistance)) + return constraints + } else { + var constraints = [QueryConstraint(key: key, value: geoPoint, comparator: .centerSphere)] + constraints.append(.init(key: key, value: distance, comparator: .geoWithin)) + return constraints + } } /** @@ -269,9 +317,14 @@ public func withinRadians(key: String, geoPoint: ParseGeoPoint, distance: Double - parameter key: The key to be constrained. - parameter geoPoint: The reference point represented as a `ParseGeoPoint`. - parameter distance: Maximum distance in miles. + - parameter sorted: `true` if results should be sorted by distance ascending, `false` is no sorting is required. + Defaults to true. - returns: The same instance of `QueryConstraint` as the receiver. */ -public func withinMiles(key: String, geoPoint: ParseGeoPoint, distance: Double) -> [QueryConstraint] { +public func withinMiles(key: String, + geoPoint: ParseGeoPoint, + distance: Double, + sorted: Bool = true) -> [QueryConstraint] { return withinRadians(key: key, geoPoint: geoPoint, distance: (distance / ParseGeoPoint.earthRadiusMiles)) } @@ -282,9 +335,14 @@ public func withinMiles(key: String, geoPoint: ParseGeoPoint, distance: Double) - parameter key: The key to be constrained. - parameter geoPoint: The reference point represented as a `ParseGeoPoint`. - parameter distance: Maximum distance in kilometers. + - parameter sorted: `true` if results should be sorted by distance ascending, `false` is no sorting is required. + Defaults to true. - returns: The same instance of `QueryConstraint` as the receiver. */ -public func withinKilometers(key: String, geoPoint: ParseGeoPoint, distance: Double) -> [QueryConstraint] { +public func withinKilometers(key: String, + geoPoint: ParseGeoPoint, + distance: Double, + sorted: Bool = true) -> [QueryConstraint] { return withinRadians(key: key, geoPoint: geoPoint, distance: (distance / ParseGeoPoint.earthRadiusKilometers)) } @@ -436,17 +494,18 @@ public func doesNotExist(key: String) -> QueryConstraint { } internal struct RelatedCondition : Encodable where T: ParseObject { - let object: T + let object: Pointer let key: String } /** Add a constraint that requires a key is related. - parameter key: The key that should be related. + - parameter object: The object that should be related. - returns: The same instance of `Query` as the receiver. */ -public func related (key: String, parent: T) -> QueryConstraint where T: ParseObject { - let condition = RelatedCondition(object: parent, key: key) +public func related (key: String, object: Pointer) -> QueryConstraint where T: ParseObject { + let condition = RelatedCondition(object: object, key: key) return .init(key: QueryConstraint.Comparator.relatedTo.rawValue, value: condition) } @@ -459,7 +518,6 @@ internal struct QueryWhere: Encodable, Equatable { constraints[constraint.key] = existing } - // This only encodes the where... func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: RawCodingKey.self) try constraints.forEach { (key, value) in @@ -480,7 +538,7 @@ internal struct QueryWhere: Encodable, Equatable { /** The `Query` class defines a query that is used to query for `ParseObject`s. */ -public class Query: Encodable, Equatable where T: ParseObject { +public struct Query: Encodable, Equatable where T: ParseObject { // interpolate as GET private let method: String = "GET" internal var limit: Int = 100 @@ -496,6 +554,7 @@ public class Query: Encodable, Equatable where T: ParseObject { internal var readPreference: String? internal var includeReadPreference: String? internal var subqueryReadPreference: String? + internal var distinct: String? internal var fields: [String]? /** @@ -522,7 +581,7 @@ public class Query: Encodable, Equatable where T: ParseObject { Create an instance with a variadic amount constraints. - parameter constraints: A variadic amount of zero or more `QueryConstraint`'s. */ - public convenience init(_ constraints: QueryConstraint...) { + public init(_ constraints: QueryConstraint...) { self.init(constraints) } @@ -558,8 +617,9 @@ public class Query: Encodable, Equatable where T: ParseObject { - parameter constraints: A variadic amount of zero or more `QueryConstraint`'s. */ public func `where`(_ constraints: QueryConstraint...) -> Query { - constraints.forEach({ self.where.add($0) }) - return self + var mutableQuery = self + constraints.forEach({ mutableQuery.where.add($0) }) + return mutableQuery } /** @@ -570,8 +630,9 @@ public class Query: Encodable, Equatable where T: ParseObject { - note: If you are calling `find` with `limit = 1`, you may find it easier to use `first` instead. */ public func limit(_ value: Int) -> Query { - self.limit = value - return self + var mutableQuery = self + mutableQuery.limit = value + return mutableQuery } /** @@ -580,8 +641,9 @@ public class Query: Encodable, Equatable where T: ParseObject { - parameter value: `n` number of results to skip. */ public func skip(_ value: Int) -> Query { - self.skip = value - return self + var mutableQuery = self + mutableQuery.skip = value + return mutableQuery } /** @@ -593,10 +655,11 @@ public class Query: Encodable, Equatable where T: ParseObject { public func readPreference(_ readPreference: String?, includeReadPreference: String? = nil, subqueryReadPreference: String? = nil) -> Query { - self.readPreference = readPreference - self.includeReadPreference = includeReadPreference - self.subqueryReadPreference = subqueryReadPreference - return self + var mutableQuery = self + mutableQuery.readPreference = readPreference + mutableQuery.includeReadPreference = includeReadPreference + mutableQuery.subqueryReadPreference = subqueryReadPreference + return mutableQuery } /** @@ -604,8 +667,9 @@ public class Query: Encodable, Equatable where T: ParseObject { - parameter keys: A variadic list of keys to load child `ParseObject`s for. */ public func include(_ keys: String...) -> Query { - self.include = keys - return self + var mutableQuery = self + mutableQuery.include = keys + return mutableQuery } /** @@ -613,8 +677,9 @@ public class Query: Encodable, Equatable where T: ParseObject { - parameter keys: An array of keys to load child `ParseObject`s for. */ public func include(_ keys: [String]) -> Query { - self.include = keys - return self + var mutableQuery = self + mutableQuery.include = keys + return mutableQuery } /** @@ -622,17 +687,19 @@ public class Query: Encodable, Equatable where T: ParseObject { - warning: Requires Parse Server 3.0.0+ */ public func includeAll() -> Query { - self.include = ["*"] - return self + var mutableQuery = self + mutableQuery.include = ["*"] + return mutableQuery } /** - Executes a distinct query and returns unique values. Default is to nil. + Exclude specific keys for a `ParseObject`. Default is to nil. - parameter keys: An arrays of keys to exclude. */ public func exclude(_ keys: [String]?) -> Query { - self.excludeKeys = keys - return self + var mutableQuery = self + mutableQuery.excludeKeys = keys + return mutableQuery } /** @@ -641,8 +708,9 @@ public class Query: Encodable, Equatable where T: ParseObject { - parameter keys: A variadic list of keys include in the result. */ public func select(_ keys: String...) -> Query { - self.keys = keys - return self + var mutableQuery = self + mutableQuery.keys = keys + return mutableQuery } /** @@ -651,8 +719,9 @@ public class Query: Encodable, Equatable where T: ParseObject { - parameter keys: An array of keys to include in the result. */ public func select(_ keys: [String]) -> Query { - self.keys = keys - return self + var mutableQuery = self + mutableQuery.keys = keys + return mutableQuery } /** @@ -660,8 +729,19 @@ public class Query: Encodable, Equatable where T: ParseObject { - parameter keys: An array of keys to order by. */ public func order(_ keys: [Order]?) -> Query { - self.order = keys - return self + var mutableQuery = self + mutableQuery.order = keys + return mutableQuery + } + + /** + Executes a distinct query and returns unique values. Default is to nil. + - parameter keys: A distinct key. + */ + public func distinct(_ key: String?) -> Query { + var mutableQuery = self + mutableQuery.distinct = key + return mutableQuery } /** @@ -676,8 +756,9 @@ public class Query: Encodable, Equatable where T: ParseObject { */ @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func fields(_ keys: String...) -> Query { - self.fields = keys - return self + var mutableQuery = self + mutableQuery.fields = keys + return mutableQuery } /** @@ -692,8 +773,9 @@ public class Query: Encodable, Equatable where T: ParseObject { */ @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func fields(_ keys: [String]) -> Query { - self.fields = keys - return self + var mutableQuery = self + mutableQuery.fields = keys + return mutableQuery } /** @@ -711,7 +793,7 @@ public class Query: Encodable, Equatable where T: ParseObject { } var endpoint: API.Endpoint { - return .objects(className: className) + return .objects(className: T.className) } enum CodingKeys: String, CodingKey { @@ -729,6 +811,7 @@ public class Query: Encodable, Equatable where T: ParseObject { case readPreference case includeReadPreference case subqueryReadPreference + case distinct } } @@ -736,6 +819,7 @@ public class Query: Encodable, Equatable where T: ParseObject { extension Query: Queryable { public typealias ResultType = T + public typealias AggregateType = [[String: String]] /** Finds objects *synchronously* based on the constructed query and sets an error if there was one. @@ -939,25 +1023,95 @@ extension Query: Queryable { } } } + + /** + Executes an aggregate query *asynchronously* and calls the given. + - requires: `.useMasterKey` has to be available and passed as one of the set of `options`. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - throws: An error of type `ParseError`. + - warning: This hasn't been tested thoroughly. + - returns: Returns the `ParseObject`s that match the query. + */ + public func aggregate(_ pipeline: AggregateType, + options: API.Options = []) throws -> [ResultType] { + var options = options + options.insert(.useMasterKey) + + let encoded = try ParseCoding.jsonEncoder() + .encode(self.`where`) + guard let whereString = String(data: encoded, encoding: .utf8) else { + throw ParseError(code: .unknownError, message: "Can't decode where to String.") + } + var updatedPipeline: AggregateType = pipeline + if whereString != "{}" { + updatedPipeline = [["match": whereString]] + updatedPipeline.append(contentsOf: pipeline) + } + + return try aggregateCommand(updatedPipeline) + .execute(options: options) + } + + /** + Executes an aggregate query *asynchronously* and calls the given. + - requires: `.useMasterKey` has to be available and passed as one of the set of `options`. + - parameter pipeline: A pipeline of stages to process 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)` + - warning: This hasn't been tested thoroughly. + */ + public func aggregate(_ pipeline: AggregateType, + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result<[ResultType], ParseError>) -> Void) { + var options = options + options.insert(.useMasterKey) + + guard let encoded = try? ParseCoding.jsonEncoder() + .encode(self.`where`), + let whereString = String(data: encoded, encoding: .utf8) else { + let error = ParseError(code: .unknownError, message: "Can't decode where to String.") + callbackQueue.async { + completion(.failure(error)) + } + return + } + var updatedPipeline: AggregateType = pipeline + if whereString != "{}" { + updatedPipeline = [["match": whereString]] + updatedPipeline.append(contentsOf: pipeline) + } + + aggregateCommand(pipeline) + .executeAsync(options: options) { result in + callbackQueue.async { + completion(result) + } + } + } } -private extension Query { - private func findCommand() -> API.NonParseBodyCommand, [ResultType]> { - return API.NonParseBodyCommand(method: .POST, path: endpoint, body: self) { +extension Query { + + func findCommand() -> API.NonParseBodyCommand, [ResultType]> { + let query = self + return API.NonParseBodyCommand(method: .POST, path: endpoint, body: query) { try ParseCoding.jsonDecoder().decode(QueryResponse.self, from: $0).results } } - private func firstCommand() -> API.NonParseBodyCommand, ResultType?> { - let query = self + func firstCommand() -> API.NonParseBodyCommand, ResultType?> { + var query = self query.limit = 1 return API.NonParseBodyCommand(method: .POST, path: endpoint, body: query) { try ParseCoding.jsonDecoder().decode(QueryResponse.self, from: $0).results.first } } - private func countCommand() -> API.NonParseBodyCommand, Int> { - let query = self + func countCommand() -> API.NonParseBodyCommand, Int> { + var query = self query.limit = 1 query.isCount = true return API.NonParseBodyCommand(method: .POST, path: endpoint, body: query) { @@ -965,8 +1119,8 @@ private extension Query { } } - private func findCommand(explain: Bool, hint: String?) -> API.NonParseBodyCommand, AnyCodable> { - let query = self + func findCommand(explain: Bool, hint: String?) -> API.NonParseBodyCommand, AnyCodable> { + var query = self query.explain = explain query.hint = hint return API.NonParseBodyCommand(method: .POST, path: endpoint, body: query) { @@ -977,8 +1131,8 @@ private extension Query { } } - private func firstCommand(explain: Bool, hint: String?) -> API.NonParseBodyCommand, AnyCodable> { - let query = self + func firstCommand(explain: Bool, hint: String?) -> API.NonParseBodyCommand, AnyCodable> { + var query = self query.limit = 1 query.explain = explain query.hint = hint @@ -990,8 +1144,8 @@ private extension Query { } } - private func countCommand(explain: Bool, hint: String?) -> API.NonParseBodyCommand, AnyCodable> { - let query = self + func countCommand(explain: Bool, hint: String?) -> API.NonParseBodyCommand, AnyCodable> { + var query = self query.limit = 1 query.isCount = true query.explain = explain @@ -1003,6 +1157,13 @@ private extension Query { return AnyCodable() } } + + func aggregateCommand(_ pipeline: AggregateType) -> API.NonParseBodyCommand { + + return API.NonParseBodyCommand(method: .POST, path: .aggregate(className: T.className), body: pipeline) { + try ParseCoding.jsonDecoder().decode(QueryResponse.self, from: $0).results + } + } } // MARK: ParseUser @@ -1019,6 +1180,20 @@ extension Query where T: ParseInstallation { } } +// MARK: ParseSession +extension Query where T: ParseSession { + var endpoint: API.Endpoint { + return .sessions + } +} + +// MARK: ParseRole +extension Query where T: ParseRole { + var endpoint: API.Endpoint { + return .roles + } +} + enum RawCodingKey: CodingKey { case key(String) var stringValue: String { diff --git a/Tests/ParseSwiftTests/ACLTests.swift b/Tests/ParseSwiftTests/ParseACLTests.swift similarity index 68% rename from Tests/ParseSwiftTests/ACLTests.swift rename to Tests/ParseSwiftTests/ParseACLTests.swift index 1d181e166..29606c825 100644 --- a/Tests/ParseSwiftTests/ACLTests.swift +++ b/Tests/ParseSwiftTests/ParseACLTests.swift @@ -1,5 +1,5 @@ // -// ACLTests.swift +// ParseACLTests.swift // ParseSwiftTests // // Created by Corey Baker on 8/22/20. @@ -10,7 +10,7 @@ import Foundation import XCTest @testable import ParseSwift -class ACLTests: XCTestCase { +class ParseACLTests: XCTestCase { override func setUp() { super.setUp() @@ -79,6 +79,22 @@ class ACLTests: XCTestCase { } } + struct Role: ParseRole { + + // required by ParseObject + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + // provided by Role + var name: String + + init() { + self.name = "roleMe" + } + } + func testSetACLOfObjectWithDefaultACL() throws { var user = User() user.ACL = try ParseACL.defaultACL() @@ -99,11 +115,11 @@ class ACLTests: XCTestCase { func testReadAccess() { var acl = ParseACL() - XCTAssertFalse(acl.getReadAccess(userId: "someUserID")) + XCTAssertFalse(acl.getReadAccess(objectId: "someUserID")) XCTAssertFalse(acl.getReadAccess(roleName: "someRoleName")) - acl.setReadAccess(userId: "someUserID", value: true) - XCTAssertTrue(acl.getReadAccess(userId: "someUserID")) + acl.setReadAccess(objectId: "someUserID", value: true) + XCTAssertTrue(acl.getReadAccess(objectId: "someUserID")) acl.setReadAccess(roleName: "someRoleName", value: true) XCTAssertTrue(acl.getReadAccess(roleName: "someRoleName")) @@ -112,13 +128,30 @@ class ACLTests: XCTestCase { XCTAssertTrue(acl.publicWrite) } + func testReadAccessObject() throws { + let user = User(objectId: "someUserID") + let role = try Role(name: "someRoleName") + var acl = ParseACL() + XCTAssertFalse(acl.getReadAccess(user: user)) + XCTAssertFalse(acl.getReadAccess(role: role)) + + acl.setReadAccess(user: user, value: true) + XCTAssertTrue(acl.getReadAccess(user: user)) + + acl.setReadAccess(role: role, value: true) + XCTAssertTrue(acl.getReadAccess(role: role)) + + acl.publicWrite = true + XCTAssertTrue(acl.publicWrite) + } + func testWriteAccess() { var acl = ParseACL() - XCTAssertFalse(acl.getWriteAccess(userId: "someUserID")) + XCTAssertFalse(acl.getWriteAccess(objectId: "someUserID")) XCTAssertFalse(acl.getWriteAccess(roleName: "someRoleName")) - acl.setWriteAccess(userId: "someUserID", value: true) - XCTAssertTrue(acl.getWriteAccess(userId: "someUserID")) + acl.setWriteAccess(objectId: "someUserID", value: true) + XCTAssertTrue(acl.getWriteAccess(objectId: "someUserID")) acl.setWriteAccess(roleName: "someRoleName", value: true) XCTAssertTrue(acl.getWriteAccess(roleName: "someRoleName")) @@ -127,12 +160,29 @@ class ACLTests: XCTestCase { XCTAssertTrue(acl.publicWrite) } + func testWriteAccessObject() throws { + let user = User(objectId: "someUserID") + let role = try Role(name: "someRoleName") + var acl = ParseACL() + XCTAssertFalse(acl.getWriteAccess(user: user)) + XCTAssertFalse(acl.getWriteAccess(role: role)) + + acl.setWriteAccess(user: user, value: true) + XCTAssertTrue(acl.getWriteAccess(user: user)) + + acl.setWriteAccess(role: role, value: true) + XCTAssertTrue(acl.getWriteAccess(role: role)) + + acl.publicWrite = true + XCTAssertTrue(acl.publicWrite) + } + func testCoding() { var acl = ParseACL() - acl.setReadAccess(userId: "a", value: false) - acl.setReadAccess(userId: "b", value: true) - acl.setWriteAccess(userId: "c", value: false) - acl.setWriteAccess(userId: "d", value: true) + acl.setReadAccess(objectId: "a", value: false) + acl.setReadAccess(objectId: "b", value: true) + acl.setWriteAccess(objectId: "c", value: false) + acl.setWriteAccess(objectId: "d", value: true) var encoded: Data? do { @@ -144,10 +194,10 @@ class ACLTests: XCTestCase { if let dataToDecode = encoded { do { let decoded = try ParseCoding.jsonDecoder().decode(ParseACL.self, from: dataToDecode) - XCTAssertEqual(acl.getReadAccess(userId: "a"), decoded.getReadAccess(userId: "a")) - XCTAssertEqual(acl.getReadAccess(userId: "b"), decoded.getReadAccess(userId: "b")) - XCTAssertEqual(acl.getWriteAccess(userId: "c"), decoded.getWriteAccess(userId: "c")) - XCTAssertEqual(acl.getWriteAccess(userId: "d"), decoded.getWriteAccess(userId: "d")) + XCTAssertEqual(acl.getReadAccess(objectId: "a"), decoded.getReadAccess(objectId: "a")) + XCTAssertEqual(acl.getReadAccess(objectId: "b"), decoded.getReadAccess(objectId: "b")) + XCTAssertEqual(acl.getWriteAccess(objectId: "c"), decoded.getWriteAccess(objectId: "c")) + XCTAssertEqual(acl.getWriteAccess(objectId: "d"), decoded.getWriteAccess(objectId: "d")) } catch { XCTFail(error.localizedDescription) } @@ -160,12 +210,12 @@ class ACLTests: XCTestCase { func testDefaultACLNoUser() { var newACL = ParseACL() let userId = "someUserID" - newACL.setReadAccess(userId: userId, value: true) + newACL.setReadAccess(objectId: userId, value: true) do { var defaultACL = try ParseACL.defaultACL() XCTAssertNotEqual(newACL, defaultACL) defaultACL = try ParseACL.setDefaultACL(defaultACL, withAccessForCurrentUser: true) - if defaultACL.getReadAccess(userId: userId) { + if defaultACL.getReadAccess(objectId: userId) { XCTFail("Shouldn't have set read access because there's no current user") } } catch { @@ -175,7 +225,7 @@ class ACLTests: XCTestCase { do { _ = try ParseACL.setDefaultACL(newACL, withAccessForCurrentUser: true) let defaultACL = try ParseACL.defaultACL() - if !defaultACL.getReadAccess(userId: userId) { + if !defaultACL.getReadAccess(objectId: userId) { XCTFail("Should have set defaultACL with read access even though there's no current user") } } catch { @@ -218,8 +268,8 @@ class ACLTests: XCTestCase { defaultACL = try ParseACL.defaultACL() XCTAssertEqual(newACL.publicRead, defaultACL.publicRead) XCTAssertEqual(newACL.publicWrite, defaultACL.publicWrite) - XCTAssertTrue(defaultACL.getReadAccess(userId: userObjectId)) - XCTAssertTrue(defaultACL.getWriteAccess(userId: userObjectId)) + XCTAssertTrue(defaultACL.getReadAccess(objectId: userObjectId)) + XCTAssertTrue(defaultACL.getWriteAccess(objectId: userObjectId)) } catch { XCTFail("Should have set new ACL. Error \(error)") @@ -251,15 +301,21 @@ class ACLTests: XCTestCase { } var newACL = ParseACL() - newACL.setReadAccess(userId: "someUserID", value: true) + newACL.setReadAccess(objectId: "someUserID", value: true) do { _ = try ParseACL.setDefaultACL(newACL, withAccessForCurrentUser: false) let defaultACL = try ParseACL.defaultACL() - XCTAssertTrue(defaultACL.getReadAccess(userId: "someUserID")) - XCTAssertFalse(defaultACL.getReadAccess(userId: userObjectId)) - XCTAssertFalse(defaultACL.getWriteAccess(userId: userObjectId)) + XCTAssertTrue(defaultACL.getReadAccess(objectId: "someUserID")) + XCTAssertFalse(defaultACL.getReadAccess(objectId: userObjectId)) + XCTAssertFalse(defaultACL.getWriteAccess(objectId: userObjectId)) } catch { XCTFail("Should have set new ACL. Error \(error.localizedDescription)") } } } + +extension ParseACLTests.User { + init(objectId: String) { + self.objectId = objectId + } +} diff --git a/Tests/ParseSwiftTests/ParseAuthenticationTests.swift b/Tests/ParseSwiftTests/ParseAuthenticationTests.swift index 48395db93..827eb358b 100644 --- a/Tests/ParseSwiftTests/ParseAuthenticationTests.swift +++ b/Tests/ParseSwiftTests/ParseAuthenticationTests.swift @@ -27,7 +27,7 @@ class ParseAuthenticationTests: XCTestCase { var authData: [String: [String: String]?]? } - struct TestAuth: ParseAuthenticatable { + struct TestAuth: ParseAuthentication { static var __type: String { // swiftlint:disable:this identifier_name "test" } diff --git a/Tests/ParseSwiftTests/ParseObjectBatchTests.swift b/Tests/ParseSwiftTests/ParseObjectBatchTests.swift index 4bcf8771e..17fd0854f 100644 --- a/Tests/ParseSwiftTests/ParseObjectBatchTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectBatchTests.swift @@ -66,7 +66,7 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le var scoreOnServer2 = score2 scoreOnServer2.objectId = "yolo" - scoreOnServer2.createdAt = Date() + scoreOnServer2.createdAt = Calendar.current.date(byAdding: .init(day: -2), to: Date()) scoreOnServer2.updatedAt = scoreOnServer2.createdAt scoreOnServer2.ACL = nil @@ -95,7 +95,7 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le var scoreOnServer2 = score2 scoreOnServer2.objectId = "yolo" - scoreOnServer2.createdAt = Date() + scoreOnServer2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) scoreOnServer2.updatedAt = scoreOnServer2.createdAt scoreOnServer2.ACL = nil @@ -153,8 +153,8 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le XCTFail("Should unwrap dates") return } - guard let originalCreatedAt = scoreOnServer.createdAt, - let originalUpdatedAt = scoreOnServer.updatedAt else { + guard let originalCreatedAt = scoreOnServer2.createdAt, + let originalUpdatedAt = scoreOnServer2.updatedAt else { XCTFail("Should unwrap dates") return } @@ -202,8 +202,8 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le XCTFail("Should unwrap dates") return } - guard let originalCreatedAt = scoreOnServer.createdAt, - let originalUpdatedAt = scoreOnServer.updatedAt else { + guard let originalCreatedAt = scoreOnServer2.createdAt, + let originalUpdatedAt = scoreOnServer2.updatedAt else { XCTFail("Should unwrap dates") return } @@ -225,13 +225,13 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le var scoreOnServer = score scoreOnServer.objectId = "yarr" scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt scoreOnServer.ACL = nil var scoreOnServer2 = score2 scoreOnServer2.objectId = "yolo" - scoreOnServer2.createdAt = Date() - scoreOnServer2.updatedAt = Date() + scoreOnServer2.createdAt = Calendar.current.date(byAdding: .init(day: -2), to: Date()) + scoreOnServer2.updatedAt = scoreOnServer2.createdAt scoreOnServer2.ACL = nil MockURLProtocol.mockRequests { _ in @@ -308,14 +308,14 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le var score2 = GameScore(score: 20) score2.objectId = "yolo" - score2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) - score2.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + score2.createdAt = Calendar.current.date(byAdding: .init(day: -2), to: Date()) + score2.updatedAt = Calendar.current.date(byAdding: .init(day: -2), to: Date()) score2.ACL = nil var scoreOnServer = score scoreOnServer.updatedAt = Date() var scoreOnServer2 = score2 - scoreOnServer2.updatedAt = Date() + scoreOnServer2.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) let response = [BatchResponseItem(success: scoreOnServer, error: nil), BatchResponseItem(success: scoreOnServer2, error: nil)] @@ -435,14 +435,14 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le var score2 = GameScore(score: 20) score2.objectId = "yolo" - score2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) - score2.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + score2.createdAt = Calendar.current.date(byAdding: .init(day: -2), to: Date()) + score2.updatedAt = Calendar.current.date(byAdding: .init(day: -2), to: Date()) score2.ACL = nil var scoreOnServer = score scoreOnServer.updatedAt = Date() var scoreOnServer2 = score2 - scoreOnServer2.updatedAt = Date() + scoreOnServer2.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) MockURLProtocol.mockRequests { _ in do { @@ -478,8 +478,8 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le let score = GameScore(score: 10) var score2 = GameScore(score: 20) score2.objectId = "yolo" - score2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) - score2.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + score2.createdAt = Calendar.current.date(byAdding: .init(day: -2), to: Date()) + score2.updatedAt = Calendar.current.date(byAdding: .init(day: -2), to: Date()) score2.ACL = nil var scoreOnServer = score @@ -489,7 +489,7 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le scoreOnServer.ACL = nil var scoreOnServer2 = score2 - scoreOnServer2.updatedAt = Date() + scoreOnServer2.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) let response = [BatchResponseItem(success: scoreOnServer, error: nil), BatchResponseItem(success: scoreOnServer2, error: nil)] @@ -738,13 +738,13 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le var scoreOnServer = score scoreOnServer.objectId = "yarr" scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt scoreOnServer.ACL = nil var scoreOnServer2 = score2 scoreOnServer2.objectId = "yolo" - scoreOnServer2.createdAt = Date() - scoreOnServer2.updatedAt = Date() + 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), @@ -784,7 +784,7 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le var scoreOnServer2 = score2 scoreOnServer2.objectId = "yolo" - scoreOnServer2.createdAt = Date() + scoreOnServer2.createdAt = Calendar.current.date(byAdding: .init(day: -2), to: Date()) scoreOnServer2.updatedAt = scoreOnServer2.createdAt scoreOnServer2.ACL = nil @@ -948,14 +948,14 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le var score2 = GameScore(score: 20) score2.objectId = "yolo" - score2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) - score2.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + score2.createdAt = Calendar.current.date(byAdding: .init(day: -2), to: Date()) + score2.updatedAt = Calendar.current.date(byAdding: .init(day: -2), to: Date()) score2.ACL = nil var scoreOnServer = score scoreOnServer.updatedAt = Date() var scoreOnServer2 = score2 - scoreOnServer2.updatedAt = Date() + scoreOnServer2.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) let response = [BatchResponseItem(success: scoreOnServer, error: nil), BatchResponseItem(success: scoreOnServer2, error: nil)] @@ -993,14 +993,14 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le var score2 = GameScore(score: 20) score2.objectId = "yolo" - score2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) - score2.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + score2.createdAt = Calendar.current.date(byAdding: .init(day: -2), to: Date()) + score2.updatedAt = Calendar.current.date(byAdding: .init(day: -2), to: Date()) score2.ACL = nil var scoreOnServer = score scoreOnServer.updatedAt = Date() var scoreOnServer2 = score2 - scoreOnServer2.updatedAt = Date() + scoreOnServer2.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) let response = [BatchResponseItem(success: scoreOnServer, error: nil), BatchResponseItem(success: scoreOnServer2, error: nil)] @@ -1039,7 +1039,7 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le var scoreOnServer2 = score2 scoreOnServer2.objectId = "yolo" - scoreOnServer2.createdAt = Date() + scoreOnServer2.createdAt = Calendar.current.date(byAdding: .init(day: -2), to: Date()) scoreOnServer2.updatedAt = scoreOnServer2.createdAt scoreOnServer2.ACL = nil @@ -1102,8 +1102,8 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le XCTFail("Should unwrap dates") return } - guard let originalCreatedAt = scoreOnServer.createdAt, - let originalUpdatedAt = scoreOnServer.updatedAt else { + guard let originalCreatedAt = scoreOnServer2.createdAt, + let originalUpdatedAt = scoreOnServer2.updatedAt else { XCTFail("Should unwrap dates") return } @@ -1209,13 +1209,13 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le var scoreOnServer = score scoreOnServer.objectId = "yarr" scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt scoreOnServer.ACL = nil var scoreOnServer2 = score2 scoreOnServer2.objectId = "yolo" - scoreOnServer2.createdAt = Date() - scoreOnServer2.updatedAt = Date() + 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) @@ -1254,7 +1254,7 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le var scoreOnServer2 = score2 scoreOnServer2.objectId = "yolo" - scoreOnServer2.createdAt = Date() + scoreOnServer2.createdAt = Calendar.current.date(byAdding: .init(day: -2), to: Date()) scoreOnServer2.updatedAt = scoreOnServer2.createdAt scoreOnServer2.ACL = nil diff --git a/Tests/ParseSwiftTests/ParseObjectTests.swift b/Tests/ParseSwiftTests/ParseObjectTests.swift index 5726f655b..d1aac17b4 100644 --- a/Tests/ParseSwiftTests/ParseObjectTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectTests.swift @@ -1194,7 +1194,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length levelOnServer.updatedAt = Date() levelOnServer.ACL = nil levelOnServer.objectId = "yarr" - let pointer = levelOnServer.toPointer() + let pointer = try levelOnServer.toPointer() let encoded: Data! do { encoded = try ParseCoding.jsonEncoder().encode(pointer) diff --git a/Tests/ParseSwiftTests/ParseOperationTests.swift b/Tests/ParseSwiftTests/ParseOperationTests.swift new file mode 100644 index 000000000..503647990 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseOperationTests.swift @@ -0,0 +1,406 @@ +// +// ParseOperation.swift +// ParseSwift +// +// Created by Corey Baker on 1/17/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +import Foundation +import XCTest +@testable import ParseSwift + +class ParseOperationTests: XCTestCase { + struct GameScore: ParseObject { + //: Those are required for Object + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + //: Your own properties + var score: Int + var members = [String]() + var levels: [String]? + var previous: [Level]? + + //custom initializers + init(score: Int) { + self.score = score + } + } + + struct Level: ParseObject { + //: Those are required for Object + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + //: Your own properties + var level: Int + var members = [String]() + + //custom initializers + init(level: Int) { + self.level = level + } + } + + override func setUpWithError() throws { + try super.setUpWithError() + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + testing: true) + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + MockURLProtocol.removeAll() + try KeychainStore.shared.deleteAll() + try ParseStorage.shared.deleteAll() + } + + func testSaveCommand() throws { + var score = GameScore(score: 10) + let objectId = "hello" + score.objectId = objectId + let operations = score.operation + .increment("score", by: 1) + let className = score.className + + let command = try operations.saveCommand() + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/classes/\(className)/\(objectId)") + XCTAssertEqual(command.method, API.Method.PUT) + XCTAssertNil(command.params) + + guard let body = command.body else { + XCTFail("Should be able to unwrap") + return + } + + let expected = "{\"score\":{\"amount\":1,\"__op\":\"Increment\"}}" + let encoded = try ParseCoding.parseEncoder() + .encode(body) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + } + + func testSave() { // swiftlint:disable:this function_body_length + var score = GameScore(score: 10) + score.objectId = "yarr" + let operations = score.operation + .increment("score", by: 1) + + var scoreOnServer = score + scoreOnServer.score = 11 + 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 operations.save() + XCTAssert(saved.hasSameObjectId(as: scoreOnServer)) + guard let savedUpdatedAt = saved.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalUpdatedAt = scoreOnServer.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertEqual(saved.ACL, scoreOnServer.ACL) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testSaveAsyncMainQueue() { + var score = GameScore(score: 10) + score.objectId = "yarr" + let operations = score.operation + .increment("score", by: 1) + + var scoreOnServer = score + scoreOnServer.score = 11 + 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) + } + + let expectation1 = XCTestExpectation(description: "Save object1") + + operations.save(options: [], callbackQueue: .main) { result in + + switch result { + + case .success(let saved): + XCTAssert(saved.hasSameObjectId(as: scoreOnServer)) + guard let savedUpdatedAt = saved.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + guard let originalUpdatedAt = scoreOnServer.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertEqual(saved.ACL, scoreOnServer.ACL) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testIncrement() throws { + let score = GameScore(score: 10) + let operations = score.operation + .increment("score", by: 1) + let expected = "{\"score\":{\"amount\":1,\"__op\":\"Increment\"}}" + let encoded = try ParseCoding.parseEncoder() + .encode(operations) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + } + + func testAdd() throws { + let score = GameScore(score: 10) + let operations = score.operation + .add("test", objects: ["hello"]) + let expected = "{\"test\":{\"objects\":[\"hello\"],\"__op\":\"Add\"}}" + let encoded = try ParseCoding.parseEncoder() + .encode(operations) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + } + + func testAddKeypath() throws { + let score = GameScore(score: 10) + let operations = try score.operation + .add(("test", \.members), objects: ["hello"]) + let expected = "{\"test\":{\"objects\":[\"hello\"],\"__op\":\"Add\"}}" + let encoded = try ParseCoding.parseEncoder() + .encode(operations) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + + } + + func testAddOptionalKeypath() throws { + let score = GameScore(score: 10) + let operations = try score.operation + .add(("test", \.levels), objects: ["hello"]) + let expected = "{\"test\":{\"objects\":[\"hello\"],\"__op\":\"Add\"}}" + let encoded = try ParseCoding.parseEncoder() + .encode(operations) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + } + + func testAddUnique() throws { + let score = GameScore(score: 10) + let operations = score.operation + .addUnique("test", objects: ["hello"]) + let expected = "{\"test\":{\"objects\":[\"hello\"],\"__op\":\"AddUnique\"}}" + let encoded = try ParseCoding.parseEncoder() + .encode(operations) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + } + + func testAddUniqueKeypath() throws { + let score = GameScore(score: 10) + let operations = try score.operation + .addUnique(("test", \.members), objects: ["hello"]) + let expected = "{\"test\":{\"objects\":[\"hello\"],\"__op\":\"AddUnique\"}}" + let encoded = try ParseCoding.parseEncoder() + .encode(operations) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + } + + func testAddUniqueOptionalKeypath() throws { + let score = GameScore(score: 10) + let operations = try score.operation + .addUnique(("test", \.levels), objects: ["hello"]) + let expected = "{\"test\":{\"objects\":[\"hello\"],\"__op\":\"AddUnique\"}}" + let encoded = try ParseCoding.parseEncoder() + .encode(operations) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + } + + func testAddRelation() throws { + let score = GameScore(score: 10) + var score2 = GameScore(score: 20) + score2.objectId = "yolo" + let operations = try score.operation + .addRelation("test", objects: [score2]) + // swiftlint:disable:next line_length + let expected = "{\"test\":{\"objects\":[{\"__type\":\"Pointer\",\"className\":\"GameScore\",\"objectId\":\"yolo\"}],\"__op\":\"AddRelation\"}}" + let encoded = try ParseCoding.parseEncoder() + .encode(operations) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + } + + func testAddRelationKeypath() throws { + let score = GameScore(score: 10) + var level = Level(level: 2) + level.objectId = "yolo" + let operations = try score.operation + .addRelation(("previous", \.previous), objects: [level]) + // swiftlint:disable:next line_length + let expected = "{\"previous\":{\"objects\":[{\"__type\":\"Pointer\",\"className\":\"Level\",\"objectId\":\"yolo\"}],\"__op\":\"AddRelation\"}}" + let encoded = try ParseCoding.parseEncoder() + .encode(operations) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + } + + func testAddRelationOptionalKeypath() throws { + let score = GameScore(score: 10) + var score2 = GameScore(score: 20) + score2.objectId = "yolo" + let operations = try score.operation + .addRelation("test", objects: [score2]) + // swiftlint:disable:next line_length + let expected = "{\"test\":{\"objects\":[{\"__type\":\"Pointer\",\"className\":\"GameScore\",\"objectId\":\"yolo\"}],\"__op\":\"AddRelation\"}}" + let encoded = try ParseCoding.parseEncoder() + .encode(operations) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + } + + func testRemove() throws { + let score = GameScore(score: 10) + let operations = score.operation + .remove("test", objects: ["hello"]) + let expected = "{\"test\":{\"objects\":[\"hello\"],\"__op\":\"Remove\"}}" + let encoded = try ParseCoding.parseEncoder() + .encode(operations) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + } + + func testRemoveKeypath() throws { + let score = GameScore(score: 10) + let operations = try score.operation + .remove(("test", \.members), objects: ["hello"]) + let expected = "{\"test\":{\"objects\":[\"hello\"],\"__op\":\"Remove\"}}" + let encoded = try ParseCoding.parseEncoder() + .encode(operations) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + } + + func testRemoveOptionalKeypath() throws { + let score = GameScore(score: 10) + let operations = try score.operation + .remove(("test", \.levels), objects: ["hello"]) + let expected = "{\"test\":{\"objects\":[\"hello\"],\"__op\":\"Remove\"}}" + let encoded = try ParseCoding.parseEncoder() + .encode(operations) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + } + + func testRemoveRelation() throws { + let score = GameScore(score: 10) + var score2 = GameScore(score: 20) + score2.objectId = "yolo" + let operations = try score.operation + .removeRelation("test", objects: [score2]) + // swiftlint:disable:next line_length + let expected = "{\"test\":{\"objects\":[{\"__type\":\"Pointer\",\"className\":\"GameScore\",\"objectId\":\"yolo\"}],\"__op\":\"RemoveRelation\"}}" + let encoded = try ParseCoding.parseEncoder() + .encode(operations) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + } + + func testRemoveRelationKeypath() throws { + let score = GameScore(score: 10) + var level = Level(level: 2) + level.objectId = "yolo" + let operations = try score.operation + .removeRelation(("previous", \.previous), objects: [level]) + // swiftlint:disable:next line_length + let expected = "{\"previous\":{\"objects\":[{\"__type\":\"Pointer\",\"className\":\"Level\",\"objectId\":\"yolo\"}],\"__op\":\"RemoveRelation\"}}" + let encoded = try ParseCoding.parseEncoder() + .encode(operations) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + } + + func testRemoveRelationOptionalKeypath() throws { + let score = GameScore(score: 10) + var score2 = GameScore(score: 20) + score2.objectId = "yolo" + let operations = try score.operation + .removeRelation("test", objects: [score2]) + // swiftlint:disable:next line_length + let expected = "{\"test\":{\"objects\":[{\"__type\":\"Pointer\",\"className\":\"GameScore\",\"objectId\":\"yolo\"}],\"__op\":\"RemoveRelation\"}}" + let encoded = try ParseCoding.parseEncoder() + .encode(operations) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + } + + func testUnset() throws { + let score = GameScore(score: 10) + let operations = score.operation + .unset("score") + let expected = "{\"score\":{\"__op\":\"Delete\"}}" + let encoded = try ParseCoding.parseEncoder() + .encode(operations) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + } + + func testUnsetKeypath() throws { + let score = GameScore(score: 10) + let operations = score.operation + .unset(("score", \.levels)) + let expected = "{\"score\":{\"__op\":\"Delete\"}}" + let encoded = try ParseCoding.parseEncoder() + .encode(operations) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + } +} diff --git a/Tests/ParseSwiftTests/ParsePointerTests.swift b/Tests/ParseSwiftTests/ParsePointerTests.swift index 379a13938..1e059464c 100644 --- a/Tests/ParseSwiftTests/ParsePointerTests.swift +++ b/Tests/ParseSwiftTests/ParsePointerTests.swift @@ -50,11 +50,11 @@ class ParsePointerTests: XCTestCase { try ParseStorage.shared.deleteAll() } - func testPointer() { + func testPointer() throws { var score = GameScore(score: 10) score.objectId = "yarr" - let pointer = score.toPointer() - let initializedPointer = Pointer(score) + let pointer = try score.toPointer() + let initializedPointer = try Pointer(score) XCTAssertEqual(pointer.className, score.className) XCTAssertEqual(pointer.objectId, score.objectId) XCTAssertEqual(pointer.className, initializedPointer.className) @@ -62,11 +62,11 @@ class ParsePointerTests: XCTestCase { } // swiftlint:disable:next function_body_length - func testFetch() { + func testFetch() throws { var score = GameScore(score: 10) let objectId = "yarr" score.objectId = objectId - let pointer = score.toPointer() + let pointer = try score.toPointer() var scoreOnServer = score scoreOnServer.createdAt = Date() @@ -185,11 +185,11 @@ class ParsePointerTests: XCTestCase { wait(for: [expectation1, expectation2], timeout: 20.0) } - func testThreadSafeFetchAsync() { + func testThreadSafeFetchAsync() throws { var score = GameScore(score: 10) let objectId = "yarr" score.objectId = objectId - let pointer = score.toPointer() + let pointer = try score.toPointer() var scoreOnServer = score scoreOnServer.createdAt = Date() @@ -215,11 +215,11 @@ class ParsePointerTests: XCTestCase { } } - func testFetchAsyncMainQueue() { + func testFetchAsyncMainQueue() throws { var score = GameScore(score: 10) let objectId = "yarr" score.objectId = objectId - let pointer = score.toPointer() + let pointer = try score.toPointer() var scoreOnServer = score scoreOnServer.createdAt = Date() diff --git a/Tests/ParseSwiftTests/ParseQueryTests.swift b/Tests/ParseSwiftTests/ParseQueryTests.swift index 5521fefa9..7b0eecde2 100644 --- a/Tests/ParseSwiftTests/ParseQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryTests.swift @@ -96,22 +96,22 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length func testSkip() { let query = GameScore.query() XCTAssertEqual(query.skip, 0) - _ = query.skip(1) - XCTAssertEqual(query.skip, 1) + let query2 = GameScore.query().skip(1) + XCTAssertEqual(query2.skip, 1) } func testLimit() { - let query = GameScore.query() + var query = GameScore.query() XCTAssertEqual(query.limit, 100) - _ = query.limit(10) + query = query.limit(10) XCTAssertEqual(query.limit, 10) } func testOrder() { let query = GameScore.query() XCTAssertNil(query.order) - _ = query.order([.ascending("yolo")]) - XCTAssertNotNil(query.order) + let query2 = GameScore.query().order([.ascending("yolo")]) + XCTAssertNotNil(query2.order) } func testReadPreferences() { @@ -119,65 +119,76 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertNil(query.readPreference) XCTAssertNil(query.includeReadPreference) XCTAssertNil(query.subqueryReadPreference) - _ = query.readPreference("PRIMARY", + let query2 = GameScore.query().readPreference("PRIMARY", includeReadPreference: "SECONDARY", subqueryReadPreference: "SECONDARY_PREFERRED") - XCTAssertNotNil(query.readPreference) - XCTAssertNotNil(query.includeReadPreference) - XCTAssertNotNil(query.subqueryReadPreference) + XCTAssertNotNil(query2.readPreference) + XCTAssertNotNil(query2.includeReadPreference) + XCTAssertNotNil(query2.subqueryReadPreference) } func testIncludeKeys() { let query = GameScore.query() XCTAssertNil(query.include) - _ = query.include("yolo") - XCTAssertEqual(query.include?.count, 1) - XCTAssertEqual(query.include?.first, "yolo") - _ = query.include("yolo", "wow") - XCTAssertEqual(query.include?.count, 2) - XCTAssertEqual(query.include, ["yolo", "wow"]) - _ = query.include(["yolo"]) - XCTAssertEqual(query.include?.count, 1) - XCTAssertEqual(query.include, ["yolo"]) + var query2 = GameScore.query().include("yolo") + XCTAssertEqual(query2.include?.count, 1) + XCTAssertEqual(query2.include?.first, "yolo") + query2 = query2.include("yolo", "wow") + XCTAssertEqual(query2.include?.count, 2) + XCTAssertEqual(query2.include, ["yolo", "wow"]) + query2 = query2.include(["yolo"]) + XCTAssertEqual(query2.include?.count, 1) + XCTAssertEqual(query2.include, ["yolo"]) + } + + func testDistinct() throws { + let query = GameScore.query() + .distinct("yolo") + + let expected = "{\"limit\":100,\"skip\":0,\"distinct\":\"yolo\",\"_method\":\"GET\",\"where\":{}}" + let encoded = try ParseCoding.parseEncoder() + .encode(query) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) } func testIncludeAllKeys() { let query = GameScore.query() XCTAssertNil(query.include) - _ = query.includeAll() - XCTAssertEqual(query.include?.count, 1) - XCTAssertEqual(query.include, ["*"]) + let query2 = GameScore.query().includeAll() + XCTAssertEqual(query2.include?.count, 1) + XCTAssertEqual(query2.include, ["*"]) } func testExcludeKeys() { let query = GameScore.query() XCTAssertNil(query.excludeKeys) - _ = query.exclude(["yolo"]) - XCTAssertEqual(query.excludeKeys, ["yolo"]) - XCTAssertEqual(query.excludeKeys, ["yolo"]) + let query2 = GameScore.query().exclude(["yolo"]) + XCTAssertEqual(query2.excludeKeys, ["yolo"]) + XCTAssertEqual(query2.excludeKeys, ["yolo"]) } func testSelectKeys() { let query = GameScore.query() XCTAssertNil(query.keys) - _ = query.select("yolo") - XCTAssertEqual(query.keys?.count, 1) - XCTAssertEqual(query.keys?.first, "yolo") - _ = query.select("yolo", "wow") - XCTAssertEqual(query.keys?.count, 2) - XCTAssertEqual(query.keys, ["yolo", "wow"]) - _ = query.select(["yolo"]) - XCTAssertEqual(query.keys?.count, 1) - XCTAssertEqual(query.keys, ["yolo"]) + var query2 = GameScore.query().select("yolo") + XCTAssertEqual(query2.keys?.count, 1) + XCTAssertEqual(query2.keys?.first, "yolo") + query2 = query2.select("yolo", "wow") + XCTAssertEqual(query2.keys?.count, 2) + XCTAssertEqual(query2.keys, ["yolo", "wow"]) + query2 = query2.select(["yolo"]) + XCTAssertEqual(query2.keys?.count, 1) + XCTAssertEqual(query2.keys, ["yolo"]) } func testAddingConstraints() { - let query = GameScore.query() + var query = GameScore.query() XCTAssertEqual(query.className, GameScore.className) XCTAssertEqual(query.className, query.className) XCTAssertEqual(query.`where`.constraints.values.count, 0) - _ = query.`where`("score" > 100, "createdAt" > Date()) + query = query.`where`("score" > 100, "createdAt" > Date()) XCTAssertEqual(query.`where`.constraints.values.count, 2) } @@ -213,6 +224,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } + // MARK: Querying Parse Server func testFindEncoded() throws { let afterDate = Date().addingTimeInterval(-300) @@ -956,6 +968,37 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } } + func testNorQuery() { + let expected: [String: AnyCodable] = [ + "$nor": [ + ["score": ["$lte": 50]], + ["score": ["$lte": 200]] + ] + ] + let query1 = GameScore.query("score" <= 50) + let query2 = GameScore.query("score" <= 200) + let constraint = nor(queries: [query1, query2]) + let query = Query(constraint) + let queryWhere = query.`where` + + do { + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) + let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) + XCTAssertEqual(expected.keys, decodedDictionary.keys) + + guard let expectedValues = expected.values.first?.value as? [[String: [String: Int]]], + let decodedValues = decodedDictionary.values.first?.value as? [[String: [String: Int]]] else { + XCTFail("Should have casted") + return + } + XCTAssertEqual(expectedValues, decodedValues) + + } catch { + XCTFail(error.localizedDescription) + return + } + } + func testAndQuery() { let expected: [String: AnyCodable] = [ "$and": [ @@ -1183,6 +1226,32 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } } + func testWhereContainedBy() { + let expected: [String: AnyCodable] = [ + "yolo": ["$containedBy": ["yarr"]] + ] + let constraint = containedBy(key: "yolo", array: ["yarr"]) + let query = GameScore.query(constraint) + let queryWhere = query.`where` + + do { + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) + let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) + XCTAssertEqual(expected.keys, decodedDictionary.keys) + + guard let expectedValues = expected.values.first?.value as? [String: [String]], + let decodedValues = decodedDictionary.values.first?.value as? [String: [String]] else { + XCTFail("Should have casted") + return + } + XCTAssertEqual(expectedValues, decodedValues) + + } catch { + XCTFail(error.localizedDescription) + return + } + } + func testWhereNotContainedIn() { let expected: [String: AnyCodable] = [ "yolo": ["$nin": ["yarr"]] @@ -1235,15 +1304,18 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } } - func testWhereKeyRelated() { + func testWhereKeyRelated() throws { let expected: [String: AnyCodable] = [ "$relatedTo": [ "key": "yolo", - "object": ["score": 50] + "object": ["__type": "Pointer", + "className": "GameScore", + "objectId": "hello"] ] ] - let object = GameScore(score: 50) - let constraint = related(key: "yolo", parent: object) + var object = GameScore(score: 50) + object.objectId = "hello" + let constraint = related(key: "yolo", object: try object.toPointer()) let query = GameScore.query(constraint) let queryWhere = query.`where` @@ -1254,14 +1326,14 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length guard let expectedValues = expected.values.first?.value as? [String: Any], let expectedKey = expectedValues["key"] as? String, - let expectedObject = expectedValues["object"] as? [String: Int] else { + let expectedObject = expectedValues["object"] as? [String: String] else { XCTFail("Should have casted") return } guard let decodedValues = decodedDictionary.values.first?.value as? [String: Any], let decodedKey = decodedValues["key"] as? String, - let decodedObject = decodedValues["object"] as? [String: Int] else { + let decodedObject = decodedValues["object"] as? [String: String] else { XCTFail("Should have casted") return } @@ -1275,6 +1347,47 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } } + func testWhereKeyRelativeToTime() throws { + let expected: [String: AnyCodable] = [ + "yolo": [ + "$gte": ["$relativeTime": "3 days ago."] + ] + ] + var object = GameScore(score: 50) + object.objectId = "hello" + let constraint = relative(key: "yolo", + comparator: .greaterThanOrEqualTo, + time: "3 days ago.") + let query = GameScore.query(constraint) + let queryWhere = query.`where` + + do { + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) + let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) + XCTAssertEqual(expected.keys, decodedDictionary.keys) + + guard let expectedValues = expected + .values + .first?.value as? [String: [String: String]] else { + XCTFail("Should have casted") + return + } + + guard let decodedValues = decodedDictionary + .values + .first?.value as? [String: [String: String]] else { + XCTFail("Should have casted") + return + } + + XCTAssertEqual(expectedValues, decodedValues) + + } catch { + XCTFail(error.localizedDescription) + return + } + } + // MARK: GeoPoint func testWhereKeyNearGeoPoint() { let expected: [String: AnyCodable] = [ @@ -1453,6 +1566,52 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } } + func testWhereKeyNearGeoPointWithinRadiansNotSorted() { + let expected: [String: AnyCodable] = [ + "yolo": ["$centerSphere": ["latitude": 10, "longitude": 20, "__type": "GeoPoint"], + "$geoWithin": 10 + ] + ] + let geoPoint = ParseGeoPoint(latitude: 10, longitude: 20) + let constraint = withinRadians(key: "yolo", geoPoint: geoPoint, distance: 10.0, sorted: false) + let query = GameScore.query(constraint) + let queryWhere = query.`where` + + do { + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) + let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) + XCTAssertEqual(expected.keys, decodedDictionary.keys) + + guard let expectedValues = expected.values.first?.value as? [String: Any], + let expectedNear = expectedValues["$centerSphere"] as? [String: Any], + let expectedLongitude = expectedNear["longitude"] as? Int, + let expectedLatitude = expectedNear["latitude"] as? Int, + let expectedType = expectedNear["__type"] as? String, + let expectedDistance = expectedValues["$geoWithin"] as? Int else { + XCTFail("Should have casted") + return + } + + guard let decodedValues = decodedDictionary.values.first?.value as? [String: Any], + let decodedNear = decodedValues["$centerSphere"] as? [String: Any], + let decodedLongitude = decodedNear["longitude"] as? Int, + let decodedLatitude = decodedNear["latitude"] as? Int, + let decodedType = decodedNear["__type"] as? String, + let decodedDistance = decodedValues["$geoWithin"] as? Int else { + XCTFail("Should have casted") + return + } + XCTAssertEqual(expectedLongitude, decodedLongitude) + XCTAssertEqual(expectedLatitude, decodedLatitude) + XCTAssertEqual(expectedType, decodedType) + XCTAssertEqual(expectedDistance, decodedDistance) + + } catch { + XCTFail(error.localizedDescription) + return + } + } + // swiftlint:disable:next function_body_length func testWhereKeyNearGeoBox() { let expected: [String: AnyCodable] = [ @@ -2011,5 +2170,153 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } wait(for: [expectation], timeout: 20.0) } + + func testAggregateCommand() throws { + let query = GameScore.query() + let pipeline = [[String: String]]() + let aggregate = query.aggregateCommand(pipeline) + + let expected = "{\"path\":\"\\/aggregate\\/GameScore\",\"method\":\"POST\",\"body\":[]}" + let encoded = try ParseCoding.jsonEncoder() + .encode(aggregate) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + } + + func testAggregate() { + var scoreOnServer = GameScore(score: 10) + scoreOnServer.objectId = "yarr" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = Date() + scoreOnServer.ACL = nil + + let results = QueryResponse(results: [scoreOnServer], count: 1) + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(results) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let query = GameScore.query() + do { + let pipeline = [[String: String]]() + guard let score = try query.aggregate(pipeline).first else { + XCTFail("Should unwrap first object found") + return + } + XCTAssert(score.hasSameObjectId(as: scoreOnServer)) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testAggregateWithWhere() { + var scoreOnServer = GameScore(score: 10) + scoreOnServer.objectId = "yarr" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = Date() + scoreOnServer.ACL = nil + + let results = QueryResponse(results: [scoreOnServer], count: 1) + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(results) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let query = GameScore.query("score" > 9) + do { + let pipeline = [[String: String]]() + guard let score = try query.aggregate(pipeline).first else { + XCTFail("Should unwrap first object found") + return + } + XCTAssert(score.hasSameObjectId(as: scoreOnServer)) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testAggregateAsyncMainQueue() { + var scoreOnServer = GameScore(score: 10) + scoreOnServer.objectId = "yarr" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = Date() + scoreOnServer.ACL = nil + + let results = QueryResponse(results: [scoreOnServer], count: 1) + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(results) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + let query = GameScore.query() + let expectation = XCTestExpectation(description: "Count object1") + let pipeline = [[String: String]]() + query.aggregate(pipeline, options: [], callbackQueue: .main) { result in + + switch result { + + case .success(let found): + guard let score = found.first else { + XCTFail("Should unwrap score count") + expectation.fulfill() + return + } + XCTAssert(score.hasSameObjectId(as: scoreOnServer)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation.fulfill() + } + wait(for: [expectation], timeout: 20.0) + } + + func testAggregateWhereAsyncMainQueue() { + var scoreOnServer = GameScore(score: 10) + scoreOnServer.objectId = "yarr" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = Date() + scoreOnServer.ACL = nil + + let results = QueryResponse(results: [scoreOnServer], count: 1) + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(results) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + let query = GameScore.query("score" > 9) + let expectation = XCTestExpectation(description: "Count object1") + let pipeline = [[String: String]]() + query.aggregate(pipeline, options: [], callbackQueue: .main) { result in + + switch result { + + case .success(let found): + guard let score = found.first else { + XCTFail("Should unwrap score count") + expectation.fulfill() + return + } + XCTAssert(score.hasSameObjectId(as: scoreOnServer)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation.fulfill() + } + wait(for: [expectation], timeout: 20.0) + } } // swiftlint:disable:this file_length diff --git a/Tests/ParseSwiftTests/ParseRelationTests.swift b/Tests/ParseSwiftTests/ParseRelationTests.swift new file mode 100644 index 000000000..f7180101d --- /dev/null +++ b/Tests/ParseSwiftTests/ParseRelationTests.swift @@ -0,0 +1,283 @@ +// +// ParseRelationTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/20/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +import Foundation +import XCTest +@testable import ParseSwift + +class ParseRelationTests: XCTestCase { + struct GameScore: ParseObject { + //: Those are required for Object + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + //: Your own properties + var score: Int + var members = [String]() + var levels: [String]? + + //custom initializers + init(score: Int) { + self.score = score + } + } + + struct Level: ParseObject { + //: Those are required for Object + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + //: Your own properties + var level: Int + var members = [String]() + + //custom initializers + init(level: Int) { + self.level = level + } + } + + override func setUpWithError() throws { + try super.setUpWithError() + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + testing: true) + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + MockURLProtocol.removeAll() + try KeychainStore.shared.deleteAll() + try ParseStorage.shared.deleteAll() + } + + func testEncoding() throws { + var score = GameScore(score: 10) + let objectId = "hello" + score.objectId = objectId + var relation = score.relation + + let expected = "{\"__type\":\"Relation\"}" + let encoded = try ParseCoding.jsonEncoder().encode(relation) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + + relation.className = "hello" + let expected2 = "{\"className\":\"hello\",\"__type\":\"Relation\"}" + let encoded2 = try ParseCoding.jsonEncoder().encode(relation) + let decoded2 = String(data: encoded2, encoding: .utf8) + XCTAssertEqual(decoded2, expected2) + } + + func testParseObjectRelation() throws { + var score = GameScore(score: 10) + let objectId = "hello" + score.objectId = objectId + + var level = Level(level: 1) + level.objectId = "nice" + + var relation = score.relation("yolo", child: level) + + let expected = "{\"className\":\"Level\",\"__type\":\"Relation\"}" + let encoded = try ParseCoding.jsonEncoder().encode(relation) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + + relation.className = "hello" + let expected2 = "{\"className\":\"hello\",\"__type\":\"Relation\"}" + let encoded2 = try ParseCoding.jsonEncoder().encode(relation) + let decoded2 = String(data: encoded2, encoding: .utf8) + XCTAssertEqual(decoded2, expected2) + + var relation2 = score.relation("yolo", className: "Level") + + let expected3 = "{\"className\":\"Level\",\"__type\":\"Relation\"}" + let encoded3 = try ParseCoding.jsonEncoder().encode(relation2) + let decoded3 = String(data: encoded3, encoding: .utf8) + XCTAssertEqual(decoded3, expected3) + + relation2.className = "hello" + let expected4 = "{\"className\":\"hello\",\"__type\":\"Relation\"}" + let encoded4 = try ParseCoding.jsonEncoder().encode(relation2) + let decoded4 = String(data: encoded4, encoding: .utf8) + XCTAssertEqual(decoded4, expected4) + } + + func testInitWithChild() throws { + var score = GameScore(score: 10) + let objectId = "hello" + score.objectId = objectId + + var level = Level(level: 1) + level.objectId = "nice" + var relation = ParseRelation(parent: score, child: level) + + let expected = "{\"className\":\"Level\",\"__type\":\"Relation\"}" + let encoded = try ParseCoding.jsonEncoder().encode(relation) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + + relation.className = "hello" + let expected2 = "{\"className\":\"hello\",\"__type\":\"Relation\"}" + let encoded2 = try ParseCoding.jsonEncoder().encode(relation) + let decoded2 = String(data: encoded2, encoding: .utf8) + XCTAssertEqual(decoded2, expected2) + } + + func testAddIncorrectClassError() throws { + var score = GameScore(score: 10) + let objectId = "hello" + score.objectId = objectId + var relation = score.relation + relation.className = "hello" + var level = Level(level: 1) + level.objectId = "nice" + XCTAssertThrowsError(try relation.add("level", objects: [level])) + } + + func testAddIncorrectKeyError() throws { + var score = GameScore(score: 10) + let objectId = "hello" + score.objectId = objectId + var relation = score.relation + relation.className = "Level" + relation.key = "test" + var level = Level(level: 1) + level.objectId = "nice" + XCTAssertThrowsError(try relation.add("level", objects: [level])) + } + + func testAddOperations() throws { + var score = GameScore(score: 10) + let objectId = "hello" + score.objectId = objectId + var relation = score.relation + var level = Level(level: 1) + level.objectId = "nice" + relation.className = level.className + + let operation = try relation.add("level", objects: [level]) + // swiftlint:disable:next line_length + let expected = "{\"level\":{\"objects\":[{\"__type\":\"Pointer\",\"className\":\"Level\",\"objectId\":\"nice\"}],\"__op\":\"AddRelation\"}}" + let encoded = try ParseCoding.jsonEncoder().encode(operation) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + } + + func testAddOperationsKeyCheck() throws { + var score = GameScore(score: 10) + let objectId = "hello" + score.objectId = objectId + var relation = score.relation + var level = Level(level: 1) + level.objectId = "nice" + relation.className = level.className + relation.key = "level" + + let operation = try relation.add("level", objects: [level]) + // swiftlint:disable:next line_length + let expected = "{\"level\":{\"objects\":[{\"__type\":\"Pointer\",\"className\":\"Level\",\"objectId\":\"nice\"}],\"__op\":\"AddRelation\"}}" + let encoded = try ParseCoding.jsonEncoder().encode(operation) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + } + + func testRemoveIncorrectClassError() throws { + var score = GameScore(score: 10) + let objectId = "hello" + score.objectId = objectId + var relation = score.relation + relation.className = "hello" + var level = Level(level: 1) + level.objectId = "nice" + XCTAssertThrowsError(try relation.remove("level", objects: [level])) + } + + func testRemoveIncorrectKeyError() throws { + var score = GameScore(score: 10) + let objectId = "hello" + score.objectId = objectId + var relation = score.relation + relation.className = "Level" + relation.key = "test" + var level = Level(level: 1) + level.objectId = "nice" + XCTAssertThrowsError(try relation.remove("level", objects: [level])) + } + + func testRemoveOperations() throws { + var score = GameScore(score: 10) + let objectId = "hello" + score.objectId = objectId + var relation = score.relation + var level = Level(level: 1) + level.objectId = "nice" + relation.className = level.className + + let operation = try relation.remove("level", objects: [level]) + // swiftlint:disable:next line_length + let expected = "{\"level\":{\"objects\":[{\"__type\":\"Pointer\",\"className\":\"Level\",\"objectId\":\"nice\"}],\"__op\":\"RemoveRelation\"}}" + let encoded = try ParseCoding.jsonEncoder().encode(operation) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + } + + func testRemoveOperationsKeyCheck() throws { + var score = GameScore(score: 10) + let objectId = "hello" + score.objectId = objectId + var relation = score.relation + var level = Level(level: 1) + level.objectId = "nice" + relation.className = level.className + relation.key = "level" + + let operation = try relation.remove("level", objects: [level]) + // swiftlint:disable:next line_length + let expected = "{\"level\":{\"objects\":[{\"__type\":\"Pointer\",\"className\":\"Level\",\"objectId\":\"nice\"}],\"__op\":\"RemoveRelation\"}}" + let encoded = try ParseCoding.jsonEncoder().encode(operation) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + } + + func testQuery() throws { + var score = GameScore(score: 10) + let objectId = "hello" + score.objectId = objectId + var relation = score.relation + var level = Level(level: 1) + level.objectId = "nice" + relation.className = level.className + + //No Key, this should throw + XCTAssertThrowsError(try relation.query(level)) + + //Wrong child for the relation, should throw + XCTAssertThrowsError(try relation.query(score)) + + relation.key = "level" + let query = try relation.query(level) + + // swiftlint:disable:next line_length + let expected = "{\"limit\":100,\"skip\":0,\"_method\":\"GET\",\"where\":{\"$relatedTo\":{\"key\":\"level\",\"object\":{\"__type\":\"Pointer\",\"className\":\"GameScore\",\"objectId\":\"hello\"}}}}" + let encoded = try ParseCoding.jsonEncoder().encode(query) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + } +} diff --git a/Tests/ParseSwiftTests/ParseRoleTests.swift b/Tests/ParseSwiftTests/ParseRoleTests.swift new file mode 100644 index 000000000..d664b8ae7 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseRoleTests.swift @@ -0,0 +1,357 @@ +// +// ParseRoleTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/18/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +import Foundation +import XCTest +@testable import ParseSwift + +class ParseRoleTests: XCTestCase { + struct GameScore: ParseObject { + //: Those are required for Object + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + //: Your own properties + var score: Int + var members = [String]() + var levels: [String]? + + //custom initializers + init(score: Int) { + self.score = score + } + } + + struct User: ParseUser { + + //: Those are required for Object + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + // provided by User + var username: String? + var email: String? + var password: String? + var authData: [String: [String: String]?]? + + // Your custom keys + var customKey: String? + } + + struct Role: ParseRole { + + // required by ParseObject + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + // provided by Role + var name: String + + init() { + self.name = "roleMe" + } + } + + struct Level: ParseObject { + //: Those are required for Object + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + //: Your own properties + var level: Int + var members = [String]() + + //custom initializers + init(level: Int) { + self.level = level + } + } + + override func setUpWithError() throws { + try super.setUpWithError() + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + testing: true) + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + MockURLProtocol.removeAll() + try KeychainStore.shared.deleteAll() + try ParseStorage.shared.deleteAll() + } + + func testName() throws { + XCTAssertNoThrow(try Role(name: "Hello9_- ")) + XCTAssertThrowsError(try Role(name: "Hello9!")) + XCTAssertNoThrow(try Role(name: "Hello9_- ", acl: ParseACL())) + XCTAssertThrowsError(try Role(name: "Hello9!", acl: ParseACL())) + } + + func testEndPoint() throws { + var role = try Role(name: "Administrator") + role.objectId = "me" + //This endpoint is at the ParseRole level + XCTAssertEqual(role.endpoint.urlComponent, "/roles/me") + } + + func testUserAddIncorrectClassKeyError() throws { + var acl = ParseACL() + acl.publicWrite = false + acl.publicRead = true + + let role = try Role(name: "Administrator", acl: acl) + let userRoles = role.users + + var level = Level(level: 1) + level.objectId = "nice" + XCTAssertThrowsError(try userRoles.add("users", objects: [level])) + } + + func testUserAddIncorrectKeyError() throws { + var acl = ParseACL() + acl.publicWrite = false + acl.publicRead = true + + let role = try Role(name: "Administrator", acl: acl) + let userRoles = role.users + + var user = User() + user.objectId = "heel" + XCTAssertThrowsError(try userRoles.add("level", objects: [user])) + } + + func testUserAddOperation() throws { + var acl = ParseACL() + acl.publicWrite = false + acl.publicRead = true + + let role = try Role(name: "Administrator", acl: acl) + let userRoles = role.users + let expected = "{\"className\":\"_User\",\"__type\":\"Relation\"}" + let encoded = try ParseCoding.jsonEncoder().encode(userRoles) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + XCTAssertEqual(userRoles.key, "users") + + var user = User() + user.objectId = "heel" + let operation = try userRoles.add([user]) + + // swiftlint:disable:next line_length + let expected2 = "{\"users\":{\"objects\":[{\"__type\":\"Pointer\",\"className\":\"_User\",\"objectId\":\"heel\"}],\"__op\":\"AddRelation\"}}" + let encoded2 = try ParseCoding.jsonEncoder().encode(operation) + let decoded2 = String(data: encoded2, encoding: .utf8) + XCTAssertEqual(decoded2, expected2) + } + + func testUserRemoveIncorrectClassKeyError() throws { + var acl = ParseACL() + acl.publicWrite = false + acl.publicRead = true + + let role = try Role(name: "Administrator", acl: acl) + let userRoles = role.users + + var level = Level(level: 1) + level.objectId = "nice" + XCTAssertThrowsError(try userRoles.remove("users", objects: [level])) + } + + func testUserRemoveIncorrectKeyError() throws { + var acl = ParseACL() + acl.publicWrite = false + acl.publicRead = true + + let role = try Role(name: "Administrator", acl: acl) + let userRoles = role.users + + var user = User() + user.objectId = "heel" + XCTAssertThrowsError(try userRoles.remove("level", objects: [user])) + } + + func testUserRemoveOperation() throws { + var acl = ParseACL() + acl.publicWrite = false + acl.publicRead = true + + let role = try Role(name: "Administrator", acl: acl) + let userRoles = role.users + let expected = "{\"className\":\"_User\",\"__type\":\"Relation\"}" + let encoded = try ParseCoding.jsonEncoder().encode(userRoles) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + XCTAssertEqual(userRoles.key, "users") + + var user = User() + user.objectId = "heel" + let operation = try userRoles.remove([user]) + + // swiftlint:disable:next line_length + let expected2 = "{\"users\":{\"objects\":[{\"__type\":\"Pointer\",\"className\":\"_User\",\"objectId\":\"heel\"}],\"__op\":\"RemoveRelation\"}}" + let encoded2 = try ParseCoding.jsonEncoder().encode(operation) + let decoded2 = String(data: encoded2, encoding: .utf8) + XCTAssertEqual(decoded2, expected2) + } + + func testRoleAddIncorrectClassKeyError() throws { + var acl = ParseACL() + acl.publicWrite = false + acl.publicRead = true + + let role = try Role(name: "Administrator", acl: acl) + let roles = role.roles + + var level = Level(level: 1) + level.objectId = "nice" + XCTAssertThrowsError(try roles.add("roles", objects: [level])) + } + + func testRoleAddIncorrectKeyError() throws { + var acl = ParseACL() + acl.publicWrite = false + acl.publicRead = true + + let role = try Role(name: "Administrator", acl: acl) + let roles = role.roles + + var newRole = try Role(name: "Moderator", acl: acl) + newRole.objectId = "heel" + XCTAssertThrowsError(try roles.add("level", objects: [newRole])) + } + + func testRoleAddOperation() throws { + var acl = ParseACL() + acl.publicWrite = false + acl.publicRead = true + + let role = try Role(name: "Administrator", acl: acl) + let roles = role.roles + let expected = "{\"className\":\"_Role\",\"__type\":\"Relation\"}" + let encoded = try ParseCoding.jsonEncoder().encode(roles) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + XCTAssertEqual(roles.key, "roles") + + var newRole = try Role(name: "Moderator", acl: acl) + newRole.objectId = "heel" + let operation = try roles.add([newRole]) + + // swiftlint:disable:next line_length + let expected2 = "{\"roles\":{\"objects\":[{\"__type\":\"Pointer\",\"className\":\"_Role\",\"objectId\":\"heel\"}],\"__op\":\"AddRelation\"}}" + let encoded2 = try ParseCoding.jsonEncoder().encode(operation) + let decoded2 = String(data: encoded2, encoding: .utf8) + XCTAssertEqual(decoded2, expected2) + } + + func testRoleRemoveIncorrectClassKeyError() throws { + var acl = ParseACL() + acl.publicWrite = false + acl.publicRead = true + + let role = try Role(name: "Administrator", acl: acl) + let roles = role.roles + + var level = Level(level: 1) + level.objectId = "nice" + XCTAssertThrowsError(try roles.remove("users", objects: [level])) + } + + func testRoleRemoveIncorrectKeyError() throws { + var acl = ParseACL() + acl.publicWrite = false + acl.publicRead = true + + let role = try Role(name: "Administrator", acl: acl) + let roles = role.roles + + var user = User() + user.objectId = "heel" + XCTAssertThrowsError(try roles.remove("level", objects: [user])) + } + + func testRoleRemoveOperation() throws { + var acl = ParseACL() + acl.publicWrite = false + acl.publicRead = true + + let role = try Role(name: "Administrator", acl: acl) + let roles = role.roles + let expected = "{\"className\":\"_Role\",\"__type\":\"Relation\"}" + let encoded = try ParseCoding.jsonEncoder().encode(roles) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + XCTAssertEqual(roles.key, "roles") + + var newRole = try Role(name: "Moderator", acl: acl) + newRole.objectId = "heel" + let operation = try roles.remove([newRole]) + + // swiftlint:disable:next line_length + let expected2 = "{\"roles\":{\"objects\":[{\"__type\":\"Pointer\",\"className\":\"_Role\",\"objectId\":\"heel\"}],\"__op\":\"RemoveRelation\"}}" + let encoded2 = try ParseCoding.jsonEncoder().encode(operation) + let decoded2 = String(data: encoded2, encoding: .utf8) + XCTAssertEqual(decoded2, expected2) + } + + func testUserQuery() throws { + var acl = ParseACL() + acl.publicWrite = false + acl.publicRead = true + + var user = User() + user.objectId = "heel" + + var userRoles = try Role(name: "Administrator", acl: acl) + userRoles.objectId = "yolo" + let query = try userRoles.queryUsers(user) + + // swiftlint:disable:next line_length + let expected = "{\"limit\":100,\"skip\":0,\"_method\":\"GET\",\"where\":{\"$relatedTo\":{\"key\":\"users\",\"object\":{\"__type\":\"Pointer\",\"className\":\"_Role\",\"objectId\":\"yolo\"}}}}" + let encoded = try ParseCoding.jsonEncoder().encode(query) + let decoded = String(data: encoded, encoding: .utf8) + XCTAssertEqual(decoded, expected) + } + + func testRoleQuery() throws { + var acl = ParseACL() + acl.publicWrite = false + acl.publicRead = true + + var role = try Role(name: "Administrator", acl: acl) + role.objectId = "yolo" + + var newRole = try Role(name: "Moderator", acl: acl) + newRole.objectId = "heel" + guard let query = role.queryRoles else { + XCTFail("Should unwrap, if it doesn't it an error occurred when creating query.") + return + } + + // swiftlint:disable:next line_length + let expected2 = "{\"limit\":100,\"skip\":0,\"_method\":\"GET\",\"where\":{\"$relatedTo\":{\"key\":\"roles\",\"object\":{\"__type\":\"Pointer\",\"className\":\"_Role\",\"objectId\":\"yolo\"}}}}" + let encoded2 = try ParseCoding.jsonEncoder().encode(query) + let decoded2 = String(data: encoded2, encoding: .utf8) + XCTAssertEqual(decoded2, expected2) + } +} diff --git a/Tests/ParseSwiftTests/ParseSessionTests.swift b/Tests/ParseSwiftTests/ParseSessionTests.swift index b3629dc7d..d98913dd0 100644 --- a/Tests/ParseSwiftTests/ParseSessionTests.swift +++ b/Tests/ParseSwiftTests/ParseSessionTests.swift @@ -81,6 +81,7 @@ class ParseSessionTests: XCTestCase { do { let command = try session.fetchCommand() XCTAssertNotNil(command) + //Generates this component because fetchCommand is at the Objective protocol level XCTAssertEqual(command.path.urlComponent, "/classes/_Session/me") XCTAssertEqual(command.method, API.Method.GET) XCTAssertNil(command.params) @@ -90,4 +91,11 @@ class ParseSessionTests: XCTestCase { XCTFail(error.localizedDescription) } } + + func testEndPoint() throws { + var session = Session() + session.objectId = "me" + //This endpoint is at the ParseSession level + XCTAssertEqual(session.endpoint.urlComponent, "/sessions/me") + } }