├── .codecov.yml ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── release.yml │ ├── slider.yml │ └── stale.yml ├── .gitignore ├── .spi.yml ├── .swiftlint.yml ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── CONTRIBUTING.md ├── LICENSE ├── Package.resolved ├── Package.swift ├── ParseCareKit.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── ParseCareKit.xcscheme │ └── TestHostAll.xcscheme ├── ParseCareKit.xctestplan ├── ParseCareKit └── Info.plist ├── ParseCareKitTests ├── Info.plist └── ParseCareKitTests.swift ├── README.md ├── Scripts ├── generate-documentation └── update-gh-pages-documentation-site ├── Sources └── ParseCareKit │ ├── Documentation.docc │ ├── Connect ParseCareKit to Your App.tutorial │ ├── ParseCareKit.md │ ├── ParseCareKit.tutorial │ └── Resources │ │ └── parsecarekit.png │ ├── Extensions │ ├── CareKitEssentialVersionable+Parse.swift │ ├── OCKBiologicalSex+Sendable.swift │ ├── OCKContactCategory+Sendable.swift │ ├── OCKLabeledValue+Sendable.swift │ ├── OCKNote+Sendable.swift │ ├── OCKOutcomeValue+Sendable.swift │ ├── OCKPostalAddress+Sendable.swift │ ├── OCKSchedule+Sendable.swift │ ├── OCKScheduleElement+Sendable.swift │ └── OCKSemanticVersion+Sendable.swift │ ├── Models │ ├── PCKCarePlan.swift │ ├── PCKClock.swift │ ├── PCKCodingKeys.swift │ ├── PCKContact.swift │ ├── PCKEntity.swift │ ├── PCKHealthKitTask.swift │ ├── PCKOutcome.swift │ ├── PCKPatient.swift │ ├── PCKRevisionRecord.swift │ ├── PCKStoreClass.swift │ ├── PCKTask.swift │ ├── Parse │ │ ├── PCKReadRole.swift │ │ ├── PCKUser.swift │ │ └── PCKWriteRole.swift │ ├── ParseCareKitError.swift │ └── RemoteSynchronizing.swift │ ├── PCKUtility.swift │ ├── ParseCareKit.h │ ├── ParseCareKitConstants.swift │ ├── ParseCareKitLog.swift │ ├── ParseRemote.swift │ └── Protocols │ ├── PCKObjectable+async.swift │ ├── PCKObjectable+combine.swift │ ├── PCKObjectable.swift │ ├── PCKRoleable.swift │ ├── PCKVersionable+async.swift │ ├── PCKVersionable+combine.swift │ ├── PCKVersionable.swift │ └── ParseRemoteDelegate.swift ├── TestHostAll ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── ContentView.swift ├── ParseCareKit.plist ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── TestHostAll.entitlements └── TestHostAllApp.swift └── Tests └── ParseCareKitTests ├── EncodingCareKitTests.swift ├── LoggerTests.swift ├── NetworkMocking ├── MockURLProtocol.swift └── MockURLResponse.swift └── UtilityTests.swift /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | ignore: 3 | - Tests/.* 4 | status: 5 | patch: 6 | default: 7 | target: 0 8 | changes: false 9 | project: 10 | default: 11 | target: 17 12 | comment: 13 | require_changes: true 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: [ main ] 5 | pull_request: 6 | branches: [ main ] 7 | 8 | env: 9 | CI_XCODE: '/Applications/Xcode_15.4.app/Contents/Developer' 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | xcode-test-ios: 17 | runs-on: macos-14 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Install SwiftLint 21 | run: brew install swiftlint 22 | - name: Build 23 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -testPlan ParseCareKit -scheme ParseCareKit -destination platform\=iOS\ Simulator,name\=iPhone\ 14\ Pro\ Max -derivedDataPath DerivedData clean test | xcpretty 24 | env: 25 | DEVELOPER_DIR: ${{ env.CI_XCODE }} 26 | - name: Prepare codecov 27 | uses: sersoft-gmbh/swift-coverage-action@v4 28 | id: coverage-files 29 | with: 30 | target-name-filter: '^ParseCareKit$' 31 | format: lcov 32 | search-paths: ./DerivedData 33 | env: 34 | DEVELOPER_DIR: ${{ env.CI_XCODE }} 35 | - name: Upload coverage to Codecov 36 | uses: codecov/codecov-action@v5 37 | with: 38 | files: ${{join(fromJSON(steps.coverage-files.outputs.files), ',')}} 39 | env_vars: IOS 40 | token: ${{ secrets.CODECOV_TOKEN }} 41 | env: 42 | DEVELOPER_DIR: ${{ env.CI_XCODE }} 43 | 44 | xcode-test-macos: 45 | runs-on: macos-14 46 | steps: 47 | - uses: actions/checkout@v4 48 | - name: Install SwiftLint 49 | run: brew install swiftlint 50 | - name: Build 51 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -testPlan ParseCareKit -scheme ParseCareKit -destination platform\=macOS -derivedDataPath DerivedData clean test | xcpretty 52 | env: 53 | DEVELOPER_DIR: ${{ env.CI_XCODE }} 54 | - name: Prepare codecov 55 | uses: sersoft-gmbh/swift-coverage-action@v4 56 | id: coverage-files 57 | with: 58 | target-name-filter: '^ParseCareKit$' 59 | format: lcov 60 | search-paths: ./DerivedData 61 | env: 62 | DEVELOPER_DIR: ${{ env.CI_XCODE }} 63 | - name: Upload coverage to Codecov 64 | uses: codecov/codecov-action@v5 65 | with: 66 | files: ${{join(fromJSON(steps.coverage-files.outputs.files), ',')}} 67 | env_vars: MACOS 68 | token: ${{ secrets.CODECOV_TOKEN }} 69 | env: 70 | DEVELOPER_DIR: ${{ env.CI_XCODE }} 71 | 72 | xcode-build-watchos: 73 | runs-on: macos-14 74 | steps: 75 | - uses: actions/checkout@v4 76 | - name: Upload codecov yml 77 | run: | 78 | cat .codecov.yml | curl --data-binary @- https://codecov.io/validate 79 | - name: Build 80 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -scheme ParseCareKit -destination platform\=watchOS\ Simulator,name\=Apple\ Watch\ Series\ 6\ \(44mm\) | xcpretty 81 | env: 82 | DEVELOPER_DIR: ${{ env.CI_XCODE }} 83 | 84 | spm-test: 85 | timeout-minutes: 15 86 | runs-on: macos-14 87 | steps: 88 | - uses: actions/checkout@v4 89 | - name: Build-Test 90 | run: swift build -v 91 | env: 92 | DEVELOPER_DIR: ${{ env.CI_XCODE }} 93 | 94 | docs: 95 | runs-on: macos-14 96 | steps: 97 | - uses: actions/checkout@v4 98 | - name: Generate Docs 99 | run: set -o pipefail && env NSUnbufferedIO=YES Scripts/generate-documentation 100 | env: 101 | DEVELOPER_DIR: ${{ env.CI_XCODE }} 102 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | release: 4 | types: [published] 5 | env: 6 | CI_XCODE: '/Applications/Xcode_15.4.app/Contents/Developer' 7 | 8 | jobs: 9 | docs: 10 | runs-on: macos-14 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Build and Deploy Docs 14 | run: set -o pipefail && env NSUnbufferedIO=YES Scripts/update-gh-pages-documentation-site 15 | env: 16 | CURRENT_BRANCH_NAME: release 17 | DEVELOPER_DIR: ${{ env.CI_XCODE }} 18 | -------------------------------------------------------------------------------- /.github/workflows/slider.yml: -------------------------------------------------------------------------------- 1 | name: slider 2 | on: 3 | push: 4 | branches: [ slider ] 5 | pull_request: 6 | branches: [ slider ] 7 | env: 8 | CI_XCODE_13: '/Applications/Xcode_14.2.app/Contents/Developer' 9 | 10 | jobs: 11 | xcode-test-ios: 12 | runs-on: macos-11 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Build 16 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -testPlan ParseCareKit -scheme ParseCareKit -destination platform\=iOS\ Simulator,name\=iPhone\ 12\ Pro\ Max -derivedDataPath DerivedData test | xcpretty 17 | env: 18 | DEVELOPER_DIR: ${{ env.CI_XCODE_13 }} 19 | - name: Prepare codecov 20 | run: | 21 | XCTEST=$(find DerivedData -type f -name 'ParseCareKitTests') 22 | PROFDATA=$(find DerivedData -type f -name '*.profdata') 23 | xcrun llvm-cov export "${XCTEST}" -format="lcov" -instr-profile "${PROFDATA}" > info.lcov 24 | env: 25 | DEVELOPER_DIR: ${{ env.CI_XCODE_13 }} 26 | - name: Upload coverage to Codecov 27 | uses: codecov/codecov-action@v5 28 | with: 29 | fail_ci_if_error: false 30 | env: 31 | DEVELOPER_DIR: ${{ env.CI_XCODE_13 }} 32 | 33 | xcode-build-watchos: 34 | runs-on: macos-11 35 | steps: 36 | - uses: actions/checkout@v4 37 | - name: Build 38 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -scheme ParseCareKit-watchOS -destination platform\=watchOS\ Simulator,name\=Apple\ Watch\ Series\ 6\ -\ 44mm | xcpretty 39 | env: 40 | DEVELOPER_DIR: ${{ env.CI_XCODE_13 }} 41 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "30 1 * * *" 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/stale@v9 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | stale-issue-message: 'Stale issue message' 17 | stale-pr-message: 'Stale pull request message' 18 | stale-issue-label: 'no-issue-activity' 19 | stale-pr-label: 'no-pr-activity' 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | .DS_Store 12 | ParseCareKit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved 13 | 14 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 15 | build/ 16 | DerivedData/ 17 | *.moved-aside 18 | *.pbxuser 19 | !default.pbxuser 20 | *.mode1v3 21 | !default.mode1v3 22 | *.mode2v3 23 | !default.mode2v3 24 | *.perspectivev3 25 | !default.perspectivev3 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | 30 | ## App packaging 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | # Swift Package Manager 40 | # 41 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 42 | # Packages/ 43 | # Package.pins 44 | # Package.resolved 45 | # *.xcodeproj 46 | # 47 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 48 | # hence it is not needed unless you have added a package configuration file to your project 49 | # .swiftpm 50 | 51 | .build/ 52 | 53 | # CocoaPods 54 | # 55 | # We recommend against adding the Pods directory to your .gitignore. However 56 | # you should judge for yourself, the pros and cons are mentioned at: 57 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 58 | # 59 | Pods/ 60 | Podfile 61 | Podfile.lock 62 | # 63 | # Add this line if you want to avoid checking in source code from the Xcode workspace 64 | # *.xcworkspace 65 | 66 | # Carthage 67 | # 68 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 69 | # Carthage/Checkouts 70 | 71 | Carthage/Build/ 72 | 73 | # Accio dependency management 74 | Dependencies/ 75 | .accio/ 76 | 77 | # fastlane 78 | # 79 | # It is recommended to not store the screenshots in the git repo. 80 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 81 | # For more information about the recommended setup visit: 82 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 83 | 84 | fastlane/report.xml 85 | fastlane/Preview.html 86 | fastlane/screenshots/**/*.png 87 | fastlane/test_output 88 | 89 | # Code Injection 90 | # 91 | # After new code Injection tools there's a generated folder /iOSInjectionProject 92 | # https://github.com/johnno1962/injectionforxcode 93 | 94 | iOSInjectionProject/ 95 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [ParseCareKit] 5 | swift_version: 5.9 6 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - file_length 3 | - cyclomatic_complexity 4 | - function_body_length 5 | - type_body_length 6 | - blanket_disable_command 7 | excluded: 8 | - .build 9 | - .dependencies 10 | - DerivedData 11 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the ParseCareKit Framework 2 | 3 | If you are not familiar with Pull Requests and want to know more about them, you can visit the [Creating a pull request](https://help.github.com/articles/creating-a-pull-request/) article. It contains detailed information about the process. 4 | 5 | ## Setting up your local machine 6 | 7 | * [Fork](https://github.com/netreconlab/ParseCareKit.git) this project and clone the fork on to your local machine: 8 | 9 | ```sh 10 | $ git clone https://github.com/netreconlab/ParseCareKit.git 11 | $ cd ParseCareKit # go into the clone directory 12 | ``` 13 | 14 | * Please install [SwiftLint](https://github.com/realm/SwiftLint) to ensure that your PR conforms to our coding standards: 15 | 16 | ```sh 17 | $ brew install swiftlint 18 | ``` 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Network Reconnaissance Lab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "carekit", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/cbaker6/CareKit.git", 7 | "state" : { 8 | "revision" : "61be991889f6108a3e7ddd89ed953b996b52fe07", 9 | "version" : "3.1.3" 10 | } 11 | }, 12 | { 13 | "identity" : "carekitessentials", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/netreconlab/CareKitEssentials", 16 | "state" : { 17 | "revision" : "e3997726340928e5c21e26b5ae9fdd1d8cbbad77", 18 | "version" : "1.1.4" 19 | } 20 | }, 21 | { 22 | "identity" : "fhirmodels", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/apple/FHIRModels.git", 25 | "state" : { 26 | "revision" : "b7fcf26b818704f489719f8cdd9e270aaab37823", 27 | "version" : "0.7.0" 28 | } 29 | }, 30 | { 31 | "identity" : "parse-swift", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/netreconlab/Parse-Swift.git", 34 | "state" : { 35 | "revision" : "12b8e752a21d53769b39441d02ff03261edecec8", 36 | "version" : "5.12.3" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-async-algorithms", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/apple/swift-async-algorithms", 43 | "state" : { 44 | "revision" : "042e1c4d9d19748c9c228f8d4ebc97bb1e339b0b", 45 | "version" : "1.0.4" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-collections", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/apple/swift-collections.git", 52 | "state" : { 53 | "revision" : "3d2dc41a01f9e49d84f0a3925fb858bed64f702d", 54 | "version" : "1.1.2" 55 | } 56 | } 57 | ], 58 | "version" : 2 59 | } 60 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "ParseCareKit", 7 | platforms: [.iOS(.v16), .macOS(.v14), .watchOS(.v10)], 8 | products: [ 9 | .library( 10 | name: "ParseCareKit", 11 | targets: ["ParseCareKit"] 12 | ) 13 | ], 14 | dependencies: [ 15 | .package( 16 | url: "https://github.com/cbaker6/CareKit.git", 17 | .upToNextMajor(from: "3.1.3") 18 | ), 19 | .package( 20 | url: "https://github.com/netreconlab/Parse-Swift.git", 21 | .upToNextMajor(from: "5.12.3") 22 | ), 23 | .package( 24 | url: "https://github.com/netreconlab/CareKitEssentials.git", 25 | .upToNextMajor(from: "1.1.4") 26 | ) 27 | ], 28 | targets: [ 29 | .target( 30 | name: "ParseCareKit", 31 | dependencies: [ 32 | .product(name: "ParseSwift", package: "Parse-Swift"), 33 | .product(name: "CareKitStore", package: "CareKit"), 34 | .product(name: "CareKitEssentials", package: "CareKitEssentials") 35 | ] 36 | ), 37 | .testTarget( 38 | name: "ParseCareKitTests", 39 | dependencies: ["ParseCareKit"] 40 | ) 41 | ] 42 | ) 43 | -------------------------------------------------------------------------------- /ParseCareKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ParseCareKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ParseCareKit.xcodeproj/xcshareddata/xcschemes/ParseCareKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 49 | 50 | 51 | 52 | 54 | 60 | 61 | 62 | 63 | 64 | 74 | 75 | 81 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /ParseCareKit.xcodeproj/xcshareddata/xcschemes/TestHostAll.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /ParseCareKit.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "E66CB5BA-36B7-4F17-9864-FE4AF04F004B", 5 | "name" : "Configuration 1", 6 | "options" : { 7 | 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | "testExecutionOrdering" : "random", 13 | "testRepetitionMode" : "retryOnFailure" 14 | }, 15 | "testTargets" : [ 16 | { 17 | "skippedTests" : [ 18 | "ParseCareKitTests\/testExample()", 19 | "ParseCareKitTests\/testPerformanceExample()" 20 | ], 21 | "target" : { 22 | "containerPath" : "container:ParseCareKit.xcodeproj", 23 | "identifier" : "705DC91C2526A4B80035BBE3", 24 | "name" : "ParseCareKitTests" 25 | } 26 | } 27 | ], 28 | "version" : 1 29 | } 30 | -------------------------------------------------------------------------------- /ParseCareKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /ParseCareKitTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /ParseCareKitTests/ParseCareKitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParseCareKitTests.swift 3 | // ParseCareKitTests 4 | // 5 | // Created by Corey Baker on 10/1/20. 6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class ParseCareKitTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | } 25 | 26 | func testPerformanceExample() throws { 27 | // This is an example of a performance test case. 28 | measure { 29 | // Put the code you want to measure the time of here. 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Scripts/generate-documentation: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (c) 2022, Apple Inc. All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without modification, 6 | # are permitted provided that the following conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright notice, this 9 | # list of conditions and the following disclaimer. 10 | # 11 | # 2. Redistributions in binary form must reproduce the above copyright notice, 12 | # this list of conditions and the following disclaimer in the documentation and/or 13 | # other materials provided with the distribution. 14 | # 15 | # 3. Neither the name of the copyright holder(s) nor the names of any contributors 16 | # may be used to endorse or promote products derived from this software without 17 | # specific prior written permission. No license is granted to the trademarks of 18 | # the copyright holders even if such marks are included in this software. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 24 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | # A `realpath` alternative using the default C implementation. 32 | filepath() { 33 | [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" 34 | } 35 | 36 | # First get the absolute path to this file so we can get the absolute file path to the Swift-DocC root source dir. 37 | PROJECT_ROOT="$(dirname $(dirname $(filepath $0)))" 38 | DOCS_DIR="$PROJECT_ROOT/.build/swift-docc" 39 | SGFS_DIR="$DOCS_DIR/symbol-graph-files" 40 | TEMP_WORKSPACE_DIR="$DOCS_DIR/temporary-workspace-holding-directory" 41 | 42 | DOCC_CMD=convert 43 | OUTPUT_PATH="$DOCS_DIR/ParseCareKit.doccarchive" 44 | HOSTING_BASE_PATH="" 45 | PUBLISH="NO" 46 | 47 | # Process command line arguments 48 | OUTPUT_PATH_PROCESSED=0 49 | HOSTING_BASE_PATH_PROCESSED=0 50 | while test $# -gt 0; do 51 | case "$1" in 52 | --help) 53 | echo "Usage: $(basename $0) [] [] [--preview] [--publish] [--help]" 54 | echo 55 | echo "Builds ParseCareKit and generates or previews the Swift-DocC documentation." 56 | echo 57 | echo " --preview: Starts a preview server after generating documentation." 58 | echo " --publish: Configures the documentation build for publishing on GitHub pages." 59 | echo 60 | exit 0 61 | ;; 62 | --preview) 63 | DOCC_CMD=preview 64 | shift 65 | ;; 66 | --publish) 67 | PUBLISH="YES" 68 | shift 69 | ;; 70 | *) 71 | if [ ${OUTPUT_PATH_PROCESSED} -eq 0 ]; then 72 | OUTPUT_PATH="$1" 73 | OUTPUT_PATH_PROCESSED=1 74 | elif [ ${HOSTING_BASE_PATH_PROCESSED} -eq 0 ]; then 75 | HOSTING_BASE_PATH="$1" 76 | HOSTING_BASE_PATH_PROCESSED=1 77 | else 78 | echo "Unrecognised argument \"$1\"" 79 | exit 1 80 | fi 81 | ;; 82 | esac 83 | shift 84 | done 85 | 86 | if [ "$PUBLISH" = "YES" ]; then 87 | if [ ${HOSTING_BASE_PATH_PROCESSED} -eq 0 ]; then 88 | echo "A hosting base path must be provided if the '--publish' flag is passed." 89 | echo "See '--help' for details." 90 | exit 1 91 | fi 92 | fi 93 | 94 | # Create the output directory for the symbol graphs if needed. 95 | mkdir -p "$DOCS_DIR" 96 | mkdir -p "$SGFS_DIR" 97 | rm -f $SGFS_DIR/*.* 98 | 99 | cd "$PROJECT_ROOT" 100 | 101 | # Temporarily move the Xcode workspace aside so that xcodebuild uses the Swift package directly 102 | mkdir "$TEMP_WORKSPACE_DIR" 103 | mv ParseCareKit.xcodeproj "$TEMP_WORKSPACE_DIR/ParseCareKit.xcodeproj" 104 | 105 | xcodebuild clean build -scheme ParseCareKit \ 106 | -destination generic/platform=iOS \ 107 | OTHER_SWIFT_FLAGS="-emit-symbol-graph -emit-symbol-graph-dir '$SGFS_DIR'" | xcpretty 108 | 109 | mv "$TEMP_WORKSPACE_DIR/ParseCareKit.xcodeproj" ./ParseCareKit.xcodeproj 110 | rm -r "$TEMP_WORKSPACE_DIR" 111 | 112 | # Pretty print DocC JSON output so that it can be consistently diffed between commits 113 | export DOCC_JSON_PRETTYPRINT="YES" 114 | 115 | # By default pass the --index flag so we produce a full DocC archive. 116 | EXTRA_DOCC_FLAGS="--index" 117 | 118 | # If building for publishing, don't pass the --index flag but pass additional flags for 119 | # static hosting configuration. 120 | if [ "$PUBLISH" = "YES" ]; then 121 | EXTRA_DOCC_FLAGS="--transform-for-static-hosting --hosting-base-path ParseCareKit/$HOSTING_BASE_PATH" 122 | fi 123 | 124 | # Handle the case where a DocC catalog does not exist in the ParseCareKit repo 125 | if [ -d Sources/ParseCareKit/Documentation.docc ]; then 126 | # The DocC catalog exists, so pass it to the docc invocation. 127 | DOCC_CMD="$DOCC_CMD Sources/ParseCareKit/Documentation.docc" 128 | fi 129 | 130 | xcrun docc $DOCC_CMD \ 131 | --additional-symbol-graph-dir "$SGFS_DIR" \ 132 | --output-path "$OUTPUT_PATH" $EXTRA_DOCC_FLAGS \ 133 | --fallback-display-name ParseCareKit \ 134 | --fallback-bundle-identifier edu.uky.cs.netreconlab.ParseCareKit \ 135 | --fallback-bundle-version 1.0.0 136 | 137 | if [[ "$DOCC_CMD" == "convert"* ]]; then 138 | echo 139 | echo "Generated DocC archive at: $OUTPUT_PATH" 140 | fi 141 | 142 | -------------------------------------------------------------------------------- /Scripts/update-gh-pages-documentation-site: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (c) 2022, Apple Inc. All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without modification, 6 | # are permitted provided that the following conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright notice, this 9 | # list of conditions and the following disclaimer. 10 | # 11 | # 2. Redistributions in binary form must reproduce the above copyright notice, 12 | # this list of conditions and the following disclaimer in the documentation and/or 13 | # other materials provided with the distribution. 14 | # 15 | # 3. Neither the name of the copyright holder(s) nor the names of any contributors 16 | # may be used to endorse or promote products derived from this software without 17 | # specific prior written permission. No license is granted to the trademarks of 18 | # the copyright holders even if such marks are included in this software. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 24 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | set -eu 32 | 33 | # A `realpath` alternative using the default C implementation. 34 | filepath() { 35 | [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" 36 | } 37 | 38 | PROJECT_ROOT="$(dirname $(dirname $(filepath $0)))" 39 | 40 | # Set current directory to the repository root 41 | cd "$PROJECT_ROOT" 42 | 43 | # Use git worktree to checkout the gh-pages branch of this repository in a gh-pages sub-directory 44 | git fetch 45 | git worktree add --checkout gh-pages origin/gh-pages 46 | 47 | # Get the name of the current branch to use as the subdirectory for the deployment 48 | if [ -z ${CURRENT_BRANCH_NAME+x} ]; then 49 | CURRENT_BRANCH_NAME=`git rev-parse --abbrev-ref HEAD` 50 | fi 51 | 52 | # Replace any forward slashes in the current branch name with dashes 53 | DEPLOYMENT_SUBDIRECTORY=${CURRENT_BRANCH_NAME//\//-} 54 | 55 | # Create a subdirectory for the current branch name if it doesn't exist 56 | mkdir -p "./gh-pages/$DEPLOYMENT_SUBDIRECTORY" 57 | 58 | # Generate documentation output it 59 | # to the /docs subdirectory in the gh-pages worktree directory. 60 | ./Scripts/generate-documentation "$PROJECT_ROOT/gh-pages/$DEPLOYMENT_SUBDIRECTORY" "$DEPLOYMENT_SUBDIRECTORY" --publish 61 | 62 | # Save the current commit we've just built documentation from in a variable 63 | CURRENT_COMMIT_HASH=`git rev-parse --short HEAD` 64 | 65 | # Commit and push our changes to the gh-pages branch 66 | cd gh-pages 67 | git add "$DEPLOYMENT_SUBDIRECTORY" 68 | 69 | if [ -n "$(git status --porcelain)" ]; then 70 | echo "Documentation changes found. Commiting the changes to the 'gh-pages' branch and pushing to origin." 71 | git commit -m "Update documentation to $CURRENT_COMMIT_HASH on '$CURRENT_BRANCH_NAME'" 72 | git push origin HEAD:gh-pages 73 | else 74 | # No changes found, nothing to commit. 75 | echo "No documentation changes found." 76 | fi 77 | 78 | # Delete the git worktree we created 79 | cd .. 80 | git worktree remove gh-pages -------------------------------------------------------------------------------- /Sources/ParseCareKit/Documentation.docc/Connect ParseCareKit to Your App.tutorial: -------------------------------------------------------------------------------- 1 | @Tutorial(time: 1) { 2 | @Intro(title: "Connect ParseCareKit to Your App") { 3 | 4 | } 5 | 6 | @Section(title: "Connect ParseCareKit to your app") { 7 | @Steps { 8 | 9 | @Step { 10 | Initialize the framework. 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Documentation.docc/ParseCareKit.md: -------------------------------------------------------------------------------- 1 | # ``ParseCareKit`` 2 | 3 | Seamlessly Synchronize CareKit data with a remote Parse Server. 4 | 5 | ## Overview 6 | ![ParseCareKit Logo](parsecarekit.png) 7 | This framework provides an API to synchronize [CareKit](https://github.com/carekit-apple/CareKit) data with [parse-server](https://github.com/parse-community/parse-server) using [Parse-Swift](https://github.com/netreconlab/Parse-Swift). The learn more about how to use ParseCareKit check out the [API documentation](https://netreconlab.github.io/ParseCareKit/api/) along with the rest of the README. 8 | 9 | **Use at your own risk. There is no promise that this is HIPAA compliant and we are not responsible for any mishandling of your data** 10 | 11 | For the backend, it is suggested to use [parse-hipaa](https://github.com/netreconlab/parse-hipaa) which is an out-of-the-box HIPAA compliant Parse/[Postgres](https://www.postgresql.org) or Parse/[Mongo](https://www.mongodb.com) server that comes with [Parse Dashboard](https://github.com/parse-community/parse-dashboard). Since [parse-hipaa](https://github.com/netreconlab/parse-hipaa) is a pare-server, it can be used for [iOS](https://docs.parseplatform.org/ios/guide/), [Android](https://docs.parseplatform.org/android/guide/), and web based apps. API's such as [GraphQL](https://docs.parseplatform.org/graphql/guide/), [REST](https://docs.parseplatform.org/rest/guide/), and [JS](https://docs.parseplatform.org/js/guide/) are also enabled in parse-hipaa and can be accessed directly or tested via the "API Console" in parse-dashboard. See the [Parse SDK documentation](https://parseplatform.org/#sdks) for details. These docker images include the necessary database auditing and logging for HIPAA compliance. 12 | 13 | You can also use ParseCareKit with any parse-server setup. If you devide to use your own parse-server, it's strongly recommended to add the following [CloudCode](https://github.com/netreconlab/parse-hipaa/tree/main/parse/cloud) to your server's "cloud" folder to ensure the necessary classes and fields are created as well as ensuring uniqueness of pushed entities. In addition, you should follow the [directions](https://github.com/netreconlab/parse-hipaa#running-in-production-for-parsecarekit) to setup additional indexes for optimized queries. ***Note that CareKit data is extremely sensitive and you are responsible for ensuring your parse-server meets HIPAA compliance.*** 14 | 15 | The following CareKit Entities are synchronized with Parse tables/classes: 16 | - [x] OCKPatient <-> Patient 17 | - [x] OCKCarePlan <-> CarePlan 18 | - [x] OCKTask <-> Task 19 | - [x] OCKHealthKitTask <-> HealthKitTask 20 | - [x] OCKContact <-> Contact 21 | - [x] OCKOutcome <-> Outcome 22 | - [x] OCKRevisionRecord.Clock <-> Clock 23 | 24 | ParseCareKit enables iOS and watchOS devices belonging to the same user to be reactively sychronized using [ParseLiveQuery](https://docs.parseplatform.org/parse-server/guide/#live-queries) without the need of push notifications assuming the [LiveQuery server has been configured](https://docs.parseplatform.org/parse-server/guide/#livequery-server). 25 | 26 | ## Topics 27 | 28 | ### Initialize the SDK 29 | 30 | - ``ParseCareKit/ParseRemote/init(uuid:auto:subscribeToRemoteUpdates:defaultACL:)`` 31 | - ``ParseCareKit/ParseRemote/init(uuid:auto:replacePCKStoreClasses:subscribeToRemoteUpdates:defaultACL:)`` 32 | - ``ParseCareKit/ParseRemote/init(uuid:auto:replacePCKStoreClasses:customClasses:subscribeToRemoteUpdates:defaultACL:)`` 33 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Documentation.docc/ParseCareKit.tutorial: -------------------------------------------------------------------------------- 1 | @Tutorials(name: "ParseCareKit") { 2 | @Intro(title: "Welcome to ParseCareKit") { 3 | 4 | } 5 | 6 | @Chapter(name: "Start Syncing CareKit Data To Your Server") { 7 | 8 | @Image(source: parsecarekit.png, alt: "ParseCareKit Logo") 9 | @TutorialReference(tutorial: "doc:Connect-ParseCareKit-to-Your-App") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Documentation.docc/Resources/parsecarekit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netreconlab/ParseCareKit/8446699179418a70aa69216ec632ec97b522a9ce/Sources/ParseCareKit/Documentation.docc/Resources/parsecarekit.png -------------------------------------------------------------------------------- /Sources/ParseCareKit/Extensions/CareKitEssentialVersionable+Parse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CareKitEssentialVersionable+Parse.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 11/21/21. 6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CareKitEssentials 11 | import CareKitStore 12 | import ParseSwift 13 | import os.log 14 | 15 | public extension CareKitEssentialVersionable { 16 | /** 17 | The Parse ACL for this object. 18 | */ 19 | var acl: ParseACL? { 20 | get { 21 | guard let aclString = userInfo?[ParseCareKitConstants.acl], 22 | let aclData = aclString.data(using: .utf8), 23 | let acl = try? PCKUtility.decoder().decode(ParseACL.self, from: aclData) else { 24 | return nil 25 | } 26 | return acl 27 | } 28 | set { 29 | do { 30 | let encodedACL = try PCKUtility.jsonEncoder().encode(newValue) 31 | guard let aclString = String(data: encodedACL, encoding: .utf8) else { 32 | throw ParseCareKitError.cantEncodeACL 33 | } 34 | if userInfo != nil { 35 | userInfo?[ParseCareKitConstants.acl] = aclString 36 | } else { 37 | userInfo = [ParseCareKitConstants.acl: aclString] 38 | } 39 | } catch { 40 | Logger.ockCarePlan.error("Cannot set ACL: \(error)") 41 | } 42 | } 43 | } 44 | 45 | /** 46 | The Parse `className` for this object. 47 | */ 48 | var className: String? { 49 | get { 50 | return userInfo?[CustomKey.className] 51 | } 52 | set { 53 | if userInfo != nil { 54 | userInfo?[CustomKey.className] = newValue 55 | } else if let newValue = newValue { 56 | userInfo = [CustomKey.className: newValue] 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Extensions/OCKBiologicalSex+Sendable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OCKBiologicalSex+Sendable.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 7/13/24. 6 | // Copyright © 2024 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import CareKitStore 10 | import Foundation 11 | 12 | extension OCKBiologicalSex: @unchecked Sendable {} 13 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Extensions/OCKContactCategory+Sendable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OCKContactCategory+Sendable.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 7/13/24. 6 | // Copyright © 2024 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import CareKitStore 10 | import Foundation 11 | 12 | extension OCKContactCategory: @unchecked Sendable {} 13 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Extensions/OCKLabeledValue+Sendable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OCKLabeledValue+Sendable.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 7/13/24. 6 | // Copyright © 2024 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import CareKitStore 10 | import Foundation 11 | 12 | extension OCKLabeledValue: @unchecked Sendable {} 13 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Extensions/OCKNote+Sendable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OCKNote+Sendable.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 7/13/24. 6 | // Copyright © 2024 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import CareKitStore 10 | import Foundation 11 | 12 | extension OCKNote: @unchecked Sendable {} 13 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Extensions/OCKOutcomeValue+Sendable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OCKOutcomeValue.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 7/13/24. 6 | // Copyright © 2024 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import CareKitStore 10 | import Foundation 11 | 12 | extension OCKOutcomeValue: @unchecked Sendable {} 13 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Extensions/OCKPostalAddress+Sendable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OCKPostalAddress+Sendable.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 7/13/24. 6 | // Copyright © 2024 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import CareKitStore 10 | import Foundation 11 | 12 | extension OCKPostalAddress: @unchecked Sendable {} 13 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Extensions/OCKSchedule+Sendable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OCKSchedule+Sendable.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 7/13/24. 6 | // Copyright © 2024 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import CareKitStore 10 | import Foundation 11 | 12 | extension OCKSchedule: @unchecked Sendable {} 13 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Extensions/OCKScheduleElement+Sendable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OCKScheduleElement+Sendable.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 7/13/24. 6 | // Copyright © 2024 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import CareKitStore 10 | import Foundation 11 | 12 | extension OCKScheduleElement: @unchecked Sendable {} 13 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Extensions/OCKSemanticVersion+Sendable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OCKSemanticVersion+Sendable.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 7/13/24. 6 | // Copyright © 2024 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import CareKitStore 10 | import Foundation 11 | 12 | extension OCKSemanticVersion: @unchecked Sendable {} 13 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Models/PCKCarePlan.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PCKCarePlan.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 1/17/20. 6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ParseSwift 11 | import CareKitStore 12 | import os.log 13 | 14 | // swiftlint:disable line_length 15 | 16 | /// An `PCKCarePlan` is the ParseCareKit equivalent of `OCKCarePlan`. An `OCKCarePlan` represents 17 | /// a set of tasks, including both interventions and assesments, that a patient is supposed to 18 | /// complete as part of his or her treatment for a specific condition. For example, a care plan for obesity 19 | /// may include tasks requiring the patient to exercise, record their weight, and log meals. As the care 20 | /// plan evolves with the patient's progress, the care provider may modify the exercises and include notes each 21 | /// time about why the changes were made. 22 | public struct PCKCarePlan: PCKVersionable { 23 | 24 | public var previousVersionUUIDs: [UUID]? { 25 | willSet { 26 | guard let newValue = newValue else { 27 | previousVersions = nil 28 | return 29 | } 30 | var newPreviousVersions = [Pointer]() 31 | newValue.forEach { newPreviousVersions.append(Pointer(objectId: $0.uuidString)) } 32 | previousVersions = newPreviousVersions 33 | } 34 | } 35 | 36 | public var nextVersionUUIDs: [UUID]? { 37 | willSet { 38 | guard let newValue = newValue else { 39 | nextVersions = nil 40 | return 41 | } 42 | var newNextVersions = [Pointer]() 43 | newValue.forEach { newNextVersions.append(Pointer(objectId: $0.uuidString)) } 44 | nextVersions = newNextVersions 45 | } 46 | } 47 | 48 | public var previousVersions: [Pointer]? 49 | 50 | public var nextVersions: [Pointer]? 51 | 52 | public var effectiveDate: Date? 53 | 54 | public var entityId: String? 55 | 56 | public var logicalClock: Int? 57 | 58 | public var clock: PCKClock? 59 | 60 | public var schemaVersion: OCKSemanticVersion? 61 | 62 | public var createdDate: Date? 63 | 64 | public var updatedDate: Date? 65 | 66 | public var deletedDate: Date? 67 | 68 | public var timezone: TimeZone? 69 | 70 | public var userInfo: [String: String]? 71 | 72 | public var groupIdentifier: String? 73 | 74 | public var tags: [String]? 75 | 76 | public var source: String? 77 | 78 | public var asset: String? 79 | 80 | public var notes: [OCKNote]? 81 | 82 | public var remoteID: String? 83 | 84 | public var encodingForParse: Bool = true { 85 | willSet { 86 | prepareEncodingRelational(newValue) 87 | } 88 | } 89 | 90 | public static var className: String { 91 | "CarePlan" 92 | } 93 | 94 | public var objectId: String? 95 | 96 | public var createdAt: Date? 97 | 98 | public var updatedAt: Date? 99 | 100 | public var ACL: ParseACL? 101 | 102 | public var originalData: Data? 103 | 104 | /// The patient to whom this care plan belongs. 105 | public var patient: PCKPatient? { 106 | didSet { 107 | patientUUID = patient?.uuid 108 | } 109 | } 110 | 111 | /// The UUID of the patient to whom this care plan belongs. 112 | public var patientUUID: UUID? { 113 | didSet { 114 | if patientUUID != patient?.uuid { 115 | patient = nil 116 | } 117 | } 118 | } 119 | 120 | /// A title describing this care plan. 121 | public var title: String? 122 | 123 | enum CodingKeys: String, CodingKey { 124 | case objectId, createdAt, updatedAt 125 | case entityId, schemaVersion, createdDate, updatedDate, deletedDate, 126 | timezone, userInfo, groupIdentifier, tags, source, asset, remoteID, 127 | notes, logicalClock 128 | case previousVersionUUIDs, nextVersionUUIDs, effectiveDate 129 | case title, patient, patientUUID 130 | } 131 | 132 | public init() { 133 | ACL = PCKUtility.getDefaultACL() 134 | } 135 | 136 | public static func new(from careKitEntity: OCKEntity) throws -> PCKCarePlan { 137 | switch careKitEntity { 138 | case .carePlan(let entity): 139 | return try new(from: entity) 140 | default: 141 | Logger.carePlan.error("new(with:) The wrong type (\(careKitEntity.entityType, privacy: .private)) of entity was passed as an argument.") 142 | throw ParseCareKitError.classTypeNotAnEligibleType 143 | } 144 | } 145 | 146 | public static func copyValues(from other: PCKCarePlan, to here: PCKCarePlan) throws -> Self { 147 | var here = here 148 | here.copyVersionedValues(from: other) 149 | here.previousVersionUUIDs = other.previousVersionUUIDs 150 | here.nextVersionUUIDs = other.nextVersionUUIDs 151 | here.patient = other.patient 152 | here.title = other.title 153 | return here 154 | } 155 | 156 | /** 157 | Creates a new ParseCareKit object from a specified CareKit CarePlan. 158 | 159 | - parameter from: The CareKit Plan used to create the new ParseCareKit object. 160 | - returns: Returns a new version of `Self` 161 | - throws: `Error`. 162 | */ 163 | public static func new(from carePlanAny: OCKAnyCarePlan) throws -> PCKCarePlan { 164 | 165 | guard let carePlan = carePlanAny as? OCKCarePlan else { 166 | throw ParseCareKitError.cantCastToNeededClassType 167 | } 168 | let encoded = try PCKUtility.jsonEncoder().encode(carePlan) 169 | var decoded = try PCKUtility.decoder().decode(Self.self, from: encoded) 170 | decoded.objectId = carePlan.uuid.uuidString 171 | decoded.entityId = carePlan.id 172 | decoded.patient = PCKPatient(uuid: carePlan.patientUUID) 173 | decoded.previousVersions = carePlan.previousVersionUUIDs.map { Pointer(objectId: $0.uuidString) } 174 | decoded.nextVersions = carePlan.nextVersionUUIDs.map { Pointer(objectId: $0.uuidString) } 175 | if let acl = carePlan.acl { 176 | decoded.ACL = acl 177 | } else { 178 | decoded.ACL = PCKUtility.getDefaultACL() 179 | } 180 | return decoded 181 | } 182 | 183 | mutating func prepareEncodingRelational(_ encodingForParse: Bool) { 184 | if patient != nil { 185 | patient?.encodingForParse = encodingForParse 186 | } 187 | } 188 | 189 | // Note that CarePlans have to be saved to CareKit first in order to properly convert to CareKit 190 | public func convertToCareKit() throws -> OCKCarePlan { 191 | var mutableCarePlan = self 192 | mutableCarePlan.encodingForParse = false 193 | let encoded = try PCKUtility.jsonEncoder().encode(mutableCarePlan) 194 | return try PCKUtility.decoder().decode(OCKCarePlan.self, from: encoded) 195 | } 196 | } 197 | 198 | extension PCKCarePlan { 199 | public func encode(to encoder: Encoder) throws { 200 | var container = encoder.container(keyedBy: CodingKeys.self) 201 | if encodingForParse { 202 | try container.encodeIfPresent(patient?.toPointer(), forKey: .patient) 203 | } 204 | try container.encodeIfPresent(title, forKey: .title) 205 | try container.encodeIfPresent(patientUUID, forKey: .patientUUID) 206 | try encodeVersionable(to: encoder) 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Models/PCKClock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Clock.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 5/9/20. 6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ParseSwift 11 | import CareKitStore 12 | import os.log 13 | 14 | public struct PCKClock: ParseObject { 15 | 16 | public static var className: String { 17 | "Clock" 18 | } 19 | 20 | public var objectId: String? 21 | 22 | public var createdAt: Date? 23 | 24 | public var updatedAt: Date? 25 | 26 | public var ACL: ParseACL? 27 | 28 | public var originalData: Data? 29 | 30 | public var uuid: UUID? 31 | 32 | var knowledgeVectorString: String? 33 | 34 | /// A knowledge vector indicating the last known state of each other device 35 | /// by the device that authored this revision record. 36 | public var knowledgeVector: OCKRevisionRecord.KnowledgeVector? { 37 | get { 38 | try? PCKClock.decodeVector(knowledgeVectorString) 39 | } 40 | set { 41 | guard let newValue = newValue else { 42 | knowledgeVectorString = nil 43 | return 44 | } 45 | knowledgeVectorString = PCKClock.encodeVector(newValue) 46 | } 47 | } 48 | 49 | public init() { } 50 | 51 | public func merge(with object: PCKClock) throws -> PCKClock { 52 | var updated = try mergeParse(with: object) 53 | if updated.shouldRestoreKey(\.uuid, 54 | original: object) { 55 | updated.uuid = object.uuid 56 | } 57 | if updated.shouldRestoreKey(\.knowledgeVectorString, 58 | original: object) { 59 | updated.knowledgeVectorString = object.knowledgeVectorString 60 | } 61 | return updated 62 | } 63 | 64 | static func decodeVector(_ clock: Self) throws -> OCKRevisionRecord.KnowledgeVector { 65 | try decodeVector(clock.knowledgeVectorString) 66 | } 67 | 68 | static func decodeVector(_ vector: String?) throws -> OCKRevisionRecord.KnowledgeVector { 69 | guard let data = vector?.data(using: .utf8) else { 70 | let errorString = "Could not get data as utf8" 71 | Logger.clock.error("\(errorString)") 72 | throw ParseCareKitError.errorString(errorString) 73 | } 74 | 75 | do { 76 | return try JSONDecoder().decode(OCKRevisionRecord.KnowledgeVector.self, 77 | from: data) 78 | } catch { 79 | Logger.clock.error("Clock.decodeVector(): \(error, privacy: .private). Vector \(data, privacy: .private).") 80 | throw ParseCareKitError.errorString("Clock.decodeVector(): \(error)") 81 | } 82 | } 83 | 84 | static func encodeVector(_ vector: OCKRevisionRecord.KnowledgeVector, for clock: Self) -> Self? { 85 | guard let remoteVectorString = encodeVector(vector) else { 86 | return nil 87 | } 88 | var mutableClock = clock 89 | mutableClock.knowledgeVectorString = remoteVectorString 90 | return mutableClock 91 | } 92 | 93 | static func encodeVector(_ vector: OCKRevisionRecord.KnowledgeVector) -> String? { 94 | do { 95 | let json = try JSONEncoder().encode(vector) 96 | guard let remoteVectorString = String(data: json, encoding: .utf8) else { 97 | return nil 98 | } 99 | return remoteVectorString 100 | } catch { 101 | Logger.clock.error("Clock.encodeVector(): \(error, privacy: .private).") 102 | return nil 103 | } 104 | } 105 | 106 | func setupWriteRole(_ owner: PCKUser) async throws -> PCKWriteRole { 107 | var role: PCKWriteRole 108 | let roleName = try PCKWriteRole.roleName(owner: owner) 109 | do { 110 | role = try await PCKWriteRole.query(ParseKey.name == roleName).first() 111 | } catch { 112 | role = try PCKWriteRole.create(with: owner) 113 | role = try await role.create() 114 | } 115 | return role 116 | } 117 | 118 | func setupReadRole(_ owner: PCKUser) async throws -> PCKReadRole { 119 | var role: PCKReadRole 120 | let roleName = try PCKReadRole.roleName(owner: owner) 121 | do { 122 | role = try await PCKReadRole.query(ParseKey.name == roleName).first() 123 | } catch { 124 | role = try PCKReadRole.create(with: owner) 125 | role = try await role.create() 126 | } 127 | return role 128 | } 129 | 130 | func setupACLWithRoles() async throws -> Self { 131 | let currentUser = try await PCKUser.current() 132 | let writeRole = try await setupWriteRole(currentUser) 133 | let readRole = try await setupReadRole(currentUser) 134 | let writeRoleName = try PCKWriteRole.roleName(owner: currentUser) 135 | let readRoleName = try PCKReadRole.roleName(owner: currentUser) 136 | do { 137 | _ = try await readRole 138 | .queryRoles() 139 | .where(ParseKey.name == writeRoleName) 140 | .first() 141 | } catch { 142 | // Need to give write role read access. 143 | guard let roles = try readRole.roles?.add([writeRole]) else { 144 | throw ParseCareKitError.errorString("Should have roles for readRole") 145 | } 146 | _ = try await roles.save() 147 | } 148 | var mutatingClock = self 149 | mutatingClock.ACL = ACL ?? ParseACL() 150 | mutatingClock.ACL?.setReadAccess(user: currentUser, value: true) 151 | mutatingClock.ACL?.setWriteAccess(user: currentUser, value: true) 152 | mutatingClock.ACL?.setWriteAccess(roleName: writeRoleName, value: true) 153 | mutatingClock.ACL?.setReadAccess(roleName: readRoleName, value: true) 154 | mutatingClock.ACL?.setWriteAccess(roleName: ParseCareKitConstants.administratorRole, value: true) 155 | mutatingClock.ACL?.setReadAccess(roleName: ParseCareKitConstants.administratorRole, value: true) 156 | return mutatingClock 157 | } 158 | 159 | static func new(uuid: UUID) async throws -> Self { 160 | var newClock = PCKClock(uuid: uuid) 161 | newClock = try await newClock.setupACLWithRoles() 162 | return try await newClock.create() 163 | } 164 | 165 | static func fetchFromRemote( 166 | _ uuid: UUID, 167 | createNewIfNeeded: Bool = false 168 | ) async throws -> Self { 169 | do { 170 | let fetchedClock = try await Self(uuid: uuid).fetch() 171 | return fetchedClock 172 | } catch let originalError { 173 | do { 174 | // Check if using an old version of ParseCareKit 175 | // which didn't have the uuid as the objectID. 176 | let query = Self.query(ClockKey.uuid == uuid) 177 | let queriedClock = try await query.first() 178 | return queriedClock 179 | } catch { 180 | guard createNewIfNeeded else { 181 | throw originalError 182 | } 183 | do { 184 | let newClock = try await new(uuid: uuid) 185 | return newClock 186 | } catch { 187 | guard let parseError = error as? ParseError else { 188 | let errorString = "Could not cast error to ParseError" 189 | Logger.clock.error("\(errorString): \(error)") 190 | let parseError = ParseError( 191 | message: errorString, 192 | swift: error 193 | ) 194 | throw parseError 195 | } 196 | Logger.clock.error("\(parseError)") 197 | throw parseError 198 | } 199 | } 200 | } 201 | } 202 | } 203 | 204 | extension PCKClock { 205 | init(uuid: UUID) { 206 | self.uuid = uuid 207 | self.objectId = uuid.uuidString 208 | knowledgeVectorString = "{\"processes\":[{\"id\":\"\(uuid)\",\"clock\":0}]}" 209 | ACL = PCKUtility.getDefaultACL() 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Models/PCKCodingKeys.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PCKCodingKeys.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 4/17/23. 6 | // Copyright © 2023 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: Coding 12 | enum PCKCodingKeys: String, CodingKey { 13 | case entityId, id 14 | case uuid, schemaVersion, createdDate, updatedDate, deletedDate, timezone, 15 | userInfo, groupIdentifier, tags, source, asset, remoteID, notes, 16 | logicalClock, clock, className, ACL, objectId, updatedAt, createdAt 17 | case effectiveDate, previousVersionUUIDs, nextVersionUUIDs, 18 | previousVersions, nextVersions 19 | 20 | } 21 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Models/PCKContact.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PCKContact.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 1/17/20. 6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ParseSwift 11 | import CareKitStore 12 | import os.log 13 | 14 | // swiftlint:disable cyclomatic_complexity 15 | // swiftlint:disable line_length 16 | // swiftlint:disable function_body_length 17 | // swiftlint:disable type_body_length 18 | 19 | /// An `PCKContact` is the ParseCareKit equivalent of `OCKContact`. An `OCKContact`represents a contact that a user 20 | /// may want to get in touch with. A contact may be a care provider, a friend, or a family member. Contacts must have at 21 | /// least a name, and may optionally have numerous other addresses at which to be contacted. 22 | public struct PCKContact: PCKVersionable { 23 | 24 | public var previousVersionUUIDs: [UUID]? { 25 | willSet { 26 | guard let newValue = newValue else { 27 | previousVersions = nil 28 | return 29 | } 30 | var newPreviousVersions = [Pointer]() 31 | newValue.forEach { newPreviousVersions.append(Pointer(objectId: $0.uuidString)) } 32 | previousVersions = newPreviousVersions 33 | } 34 | } 35 | 36 | public var nextVersionUUIDs: [UUID]? { 37 | willSet { 38 | guard let newValue = newValue else { 39 | nextVersions = nil 40 | return 41 | } 42 | var newNextVersions = [Pointer]() 43 | newValue.forEach { newNextVersions.append(Pointer(objectId: $0.uuidString)) } 44 | nextVersions = newNextVersions 45 | } 46 | } 47 | 48 | public var previousVersions: [Pointer]? 49 | 50 | public var nextVersions: [Pointer]? 51 | 52 | public var effectiveDate: Date? 53 | 54 | public var entityId: String? 55 | 56 | public var logicalClock: Int? 57 | 58 | public var clock: PCKClock? 59 | 60 | public var schemaVersion: OCKSemanticVersion? 61 | 62 | public var createdDate: Date? 63 | 64 | public var updatedDate: Date? 65 | 66 | public var deletedDate: Date? 67 | 68 | public var timezone: TimeZone? 69 | 70 | public var userInfo: [String: String]? 71 | 72 | public var groupIdentifier: String? 73 | 74 | public var tags: [String]? 75 | 76 | public var source: String? 77 | 78 | public var asset: String? 79 | 80 | public var notes: [OCKNote]? 81 | 82 | public var remoteID: String? 83 | 84 | public var encodingForParse: Bool = true { 85 | willSet { 86 | prepareEncodingRelational(newValue) 87 | } 88 | } 89 | 90 | public static var className: String { 91 | "Contact" 92 | } 93 | 94 | public var objectId: String? 95 | 96 | public var createdAt: Date? 97 | 98 | public var updatedAt: Date? 99 | 100 | public var ACL: ParseACL? 101 | 102 | public var originalData: Data? 103 | 104 | /// The contact's postal address. 105 | public var address: OCKPostalAddress? 106 | 107 | /// Indicates if this contact is care provider or if they are a friend or family member. 108 | public var category: OCKContactCategory? 109 | 110 | /// The contact's name. 111 | public var name: PersonNameComponents? 112 | 113 | /// The organization this contact belongs to. 114 | public var organization: String? 115 | 116 | /// A description of what this contact's role is. 117 | public var role: String? 118 | 119 | /// A title for this contact. 120 | public var title: String? 121 | 122 | /// The version in the local database for the care plan associated with this contact. 123 | public var carePlan: PCKCarePlan? { 124 | didSet { 125 | carePlanUUID = carePlan?.uuid 126 | } 127 | } 128 | 129 | /// The version id in the local database for the care plan associated with this contact. 130 | public var carePlanUUID: UUID? { 131 | didSet { 132 | if carePlanUUID != carePlan?.uuid { 133 | carePlan = nil 134 | } 135 | } 136 | } 137 | 138 | /// An array of numbers that the contact can be messaged at. 139 | /// The number strings may contains non-numeric characters. 140 | public var messagingNumbers: [OCKLabeledValue]? 141 | 142 | /// An array of the contact's email addresses. 143 | public var emailAddresses: [OCKLabeledValue]? 144 | 145 | /// An array of the contact's phone numbers. 146 | /// The number strings may contains non-numeric characters. 147 | public var phoneNumbers: [OCKLabeledValue]? 148 | 149 | /// An array of other information that could be used reach this contact. 150 | public var otherContactInfo: [OCKLabeledValue]? 151 | 152 | enum CodingKeys: String, CodingKey { 153 | case objectId, createdAt, updatedAt 154 | case entityId, schemaVersion, createdDate, updatedDate, deletedDate, 155 | timezone, userInfo, groupIdentifier, tags, source, asset, remoteID, 156 | notes, logicalClock 157 | case previousVersionUUIDs, nextVersionUUIDs, effectiveDate 158 | case carePlan, title, carePlanUUID, address, category, name, organization, role 159 | case emailAddresses, messagingNumbers, phoneNumbers, otherContactInfo 160 | } 161 | 162 | public init() { 163 | ACL = PCKUtility.getDefaultACL() 164 | } 165 | 166 | public static func new(from careKitEntity: OCKEntity) throws -> PCKContact { 167 | 168 | switch careKitEntity { 169 | case .contact(let entity): 170 | return try Self.new(from: entity) 171 | default: 172 | Logger.contact.error("new(with:) The wrong type (\(careKitEntity.entityType, privacy: .private)) of entity was passed as an argument.") 173 | throw ParseCareKitError.classTypeNotAnEligibleType 174 | } 175 | } 176 | 177 | public static func copyValues(from other: PCKContact, to here: PCKContact) throws -> Self { 178 | var here = here 179 | here.copyVersionedValues(from: other) 180 | here.previousVersionUUIDs = other.previousVersionUUIDs 181 | here.nextVersionUUIDs = other.nextVersionUUIDs 182 | here.address = other.address 183 | here.category = other.category 184 | here.title = other.title 185 | here.name = other.name 186 | here.organization = other.organization 187 | here.role = other.role 188 | here.carePlan = other.carePlan 189 | return here 190 | } 191 | 192 | /** 193 | Creates a new ParseCareKit object from a specified CareKit Contact. 194 | 195 | - parameter from: The CareKit Contact used to create the new ParseCareKit object. 196 | - returns: Returns a new version of `Self` 197 | - throws: `Error`. 198 | */ 199 | public static func new(from contactAny: OCKAnyContact) throws -> PCKContact { 200 | 201 | guard let contact = contactAny as? OCKContact else { 202 | throw ParseCareKitError.cantCastToNeededClassType 203 | } 204 | let encoded = try PCKUtility.jsonEncoder().encode(contact) 205 | var decoded = try PCKUtility.decoder().decode(Self.self, from: encoded) 206 | decoded.objectId = contact.uuid.uuidString 207 | decoded.entityId = contact.id 208 | decoded.carePlan = PCKCarePlan(uuid: contact.carePlanUUID) 209 | decoded.previousVersions = contact.previousVersionUUIDs.map { Pointer(objectId: $0.uuidString) } 210 | decoded.nextVersions = contact.nextVersionUUIDs.map { Pointer(objectId: $0.uuidString) } 211 | if let acl = contact.acl { 212 | decoded.ACL = acl 213 | } else { 214 | decoded.ACL = PCKUtility.getDefaultACL() 215 | } 216 | return decoded 217 | } 218 | 219 | mutating func prepareEncodingRelational(_ encodingForParse: Bool) { 220 | if carePlan != nil { 221 | carePlan?.encodingForParse = encodingForParse 222 | } 223 | } 224 | 225 | public func convertToCareKit() throws -> OCKContact { 226 | var mutableContact = self 227 | mutableContact.encodingForParse = false 228 | let encoded = try PCKUtility.jsonEncoder().encode(mutableContact) 229 | return try PCKUtility.decoder().decode(OCKContact.self, from: encoded) 230 | } 231 | } 232 | 233 | extension PCKContact { 234 | public func encode(to encoder: Encoder) throws { 235 | var container = encoder.container(keyedBy: CodingKeys.self) 236 | 237 | if encodingForParse { 238 | try container.encodeIfPresent(carePlan?.toPointer(), forKey: .carePlan) 239 | } 240 | 241 | try container.encodeIfPresent(title, forKey: .title) 242 | try container.encodeIfPresent(carePlanUUID, forKey: .carePlanUUID) 243 | try container.encodeIfPresent(address, forKey: .address) 244 | try container.encodeIfPresent(category, forKey: .category) 245 | try container.encodeIfPresent(name, forKey: .name) 246 | try container.encodeIfPresent(organization, forKey: .organization) 247 | try container.encodeIfPresent(role, forKey: .role) 248 | try container.encodeIfPresent(emailAddresses, forKey: .emailAddresses) 249 | try container.encodeIfPresent(messagingNumbers, forKey: .messagingNumbers) 250 | try container.encodeIfPresent(phoneNumbers, forKey: .phoneNumbers) 251 | try container.encodeIfPresent(otherContactInfo, forKey: .otherContactInfo) 252 | try encodeVersionable(to: encoder) 253 | } 254 | } // swiftlint:disable:this file_length 255 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Models/PCKEntity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PCKEntity.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 4/16/23. 6 | // Copyright © 2023 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import CareKitStore 10 | import Foundation 11 | 12 | /// Holds one of several possible modified entities. 13 | public enum PCKEntity: Hashable, Codable, Sendable { 14 | 15 | /// A patient entity. 16 | case patient(PCKPatient) 17 | 18 | /// A care plan entity. 19 | case carePlan(PCKCarePlan) 20 | 21 | /// A contact entity. 22 | case contact(PCKContact) 23 | 24 | /// A task entity. 25 | case task(PCKTask) 26 | 27 | /// A HealthKit linked task. 28 | case healthKitTask(PCKHealthKitTask) 29 | 30 | /// An outcome entity. 31 | case outcome(PCKOutcome) 32 | 33 | private enum Keys: CodingKey { 34 | case type 35 | case object 36 | } 37 | 38 | /// The type of the contained entity. 39 | public var entityType: EntityType { 40 | switch self { 41 | case .patient: return .patient 42 | case .carePlan: return .carePlan 43 | case .contact: return .contact 44 | case .task: return .task 45 | case .healthKitTask: return .healthKitTask 46 | case .outcome: return .outcome 47 | } 48 | } 49 | 50 | /// The underlying ParseCareKit type for the respective entity. 51 | public var value: any PCKVersionable { 52 | switch self { 53 | case let .patient(patient): return patient 54 | case let .carePlan(plan): return plan 55 | case let .contact(contact): return contact 56 | case let .task(task): return task 57 | case let .healthKitTask(task): return task 58 | case let .outcome(outcome): return outcome 59 | } 60 | } 61 | 62 | /// The `OCKEntity` of the `PCKEntity`. 63 | public func careKit() throws -> OCKEntity { 64 | switch self { 65 | case let .patient(patient): 66 | return OCKEntity.patient(try patient.convertToCareKit()) 67 | case let .carePlan(plan): 68 | return OCKEntity.carePlan(try plan.convertToCareKit()) 69 | case let .contact(contact): 70 | return OCKEntity.contact(try contact.convertToCareKit()) 71 | case let .task(task): 72 | return OCKEntity.task(try task.convertToCareKit()) 73 | case let .healthKitTask(task): 74 | return OCKEntity.healthKitTask(try task.convertToCareKit()) 75 | case let .outcome(outcome): 76 | return OCKEntity.outcome(try outcome.convertToCareKit()) 77 | } 78 | } 79 | 80 | /// Describes the types of entities that may be included in a revision record. 81 | public enum EntityType: String, Equatable, Codable, CodingKey, CaseIterable { 82 | 83 | /// The patient entity type 84 | case patient 85 | 86 | /// The care plan entity type 87 | case carePlan 88 | 89 | /// The contact entity type 90 | case contact 91 | 92 | /// The task entity type. 93 | case task 94 | 95 | /// The HealthKit task type. 96 | case healthKitTask 97 | 98 | /// The outcome entity type. 99 | case outcome 100 | } 101 | } 102 | 103 | // MARK: Encoding 104 | public extension PCKEntity { 105 | 106 | func encode(to encoder: Encoder) throws { 107 | var container = encoder.container(keyedBy: Keys.self) 108 | try container.encode(entityType, forKey: .type) 109 | switch self { 110 | case let .patient(patient): try container.encode(try patient.toPointer(), forKey: .object) 111 | case let .carePlan(plan): try container.encode(try plan.toPointer(), forKey: .object) 112 | case let .contact(contact): try container.encode(try contact.toPointer(), forKey: .object) 113 | case let .task(task): try container.encode(try task.toPointer(), forKey: .object) 114 | case let .healthKitTask(task): try container.encode(try task.toPointer(), forKey: .object) 115 | case let .outcome(outcome): try container.encode(try outcome.toPointer(), forKey: .object) 116 | } 117 | } 118 | } 119 | 120 | // MARK: Decoding 121 | public extension PCKEntity { 122 | init(from decoder: Decoder) throws { 123 | let container = try decoder.container(keyedBy: Keys.self) 124 | switch try container.decode(EntityType.self, forKey: .type) { 125 | case .patient: self = .patient(try container.decode(PCKPatient.self, forKey: .object)) 126 | case .carePlan: self = .carePlan(try container.decode(PCKCarePlan.self, forKey: .object)) 127 | case .contact: self = .contact(try container.decode(PCKContact.self, forKey: .object)) 128 | case .task: self = .task(try container.decode(PCKTask.self, forKey: .object)) 129 | case .healthKitTask: self = .healthKitTask(try container.decode(PCKHealthKitTask.self, forKey: .object)) 130 | case .outcome: self = .outcome(try container.decode(PCKOutcome.self, forKey: .object)) 131 | } 132 | } 133 | } 134 | 135 | // MARK: Compatability with OCKEntity 136 | public extension OCKEntity { 137 | func parseEntity() throws -> PCKEntity { 138 | switch self { 139 | case let .patient(patient): 140 | return PCKEntity.patient(try PCKPatient.new(from: patient)) 141 | case let .carePlan(plan): 142 | return PCKEntity.carePlan(try PCKCarePlan.new(from: plan)) 143 | case let .contact(contact): 144 | return PCKEntity.contact(try PCKContact.new(from: contact)) 145 | case let .task(task): 146 | return PCKEntity.task(try PCKTask.new(from: task)) 147 | case let .healthKitTask(task): 148 | return PCKEntity.healthKitTask(try PCKHealthKitTask.new(from: task)) 149 | case let .outcome(outcome): 150 | return PCKEntity.outcome(try PCKOutcome.new(from: outcome)) 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Models/PCKHealthKitTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PCKHealthKitTask.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 2/20/21. 6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ParseSwift 11 | import CareKitStore 12 | import os.log 13 | 14 | // swiftlint:disable line_length 15 | // swiftlint:disable cyclomatic_complexity 16 | // swiftlint:disable function_body_length 17 | // swiftlint:disable type_body_length 18 | 19 | /// An `PCKHealthKitTask` is the ParseCareKit equivalent of `OCKHealthKitTask`. An `OCKHealthKitTask` represents some task or action that a 20 | /// patient is supposed to perform. Tasks are optionally associable with an `OCKCarePlan` and must have a unique 21 | /// id and schedule. The schedule determines when and how often the task should be performed, and the 22 | /// `impactsAdherence` flag may be used to specify whether or not the patients adherence to this task will affect 23 | /// their daily completion rings. 24 | public struct PCKHealthKitTask: PCKVersionable { 25 | 26 | public var previousVersionUUIDs: [UUID]? { 27 | willSet { 28 | guard let newValue = newValue else { 29 | previousVersions = nil 30 | return 31 | } 32 | var newPreviousVersions = [Pointer]() 33 | newValue.forEach { newPreviousVersions.append(Pointer(objectId: $0.uuidString)) } 34 | previousVersions = newPreviousVersions 35 | } 36 | } 37 | 38 | public var nextVersionUUIDs: [UUID]? { 39 | willSet { 40 | guard let newValue = newValue else { 41 | nextVersions = nil 42 | return 43 | } 44 | var newNextVersions = [Pointer]() 45 | newValue.forEach { newNextVersions.append(Pointer(objectId: $0.uuidString)) } 46 | nextVersions = newNextVersions 47 | } 48 | } 49 | 50 | public var previousVersions: [Pointer]? 51 | 52 | public var nextVersions: [Pointer]? 53 | 54 | public var effectiveDate: Date? 55 | 56 | public var entityId: String? 57 | 58 | public var logicalClock: Int? 59 | 60 | public var clock: PCKClock? 61 | 62 | public var schemaVersion: OCKSemanticVersion? 63 | 64 | public var createdDate: Date? 65 | 66 | public var updatedDate: Date? 67 | 68 | public var deletedDate: Date? 69 | 70 | public var timezone: TimeZone? 71 | 72 | public var userInfo: [String: String]? 73 | 74 | public var groupIdentifier: String? 75 | 76 | public var tags: [String]? 77 | 78 | public var source: String? 79 | 80 | public var asset: String? 81 | 82 | public var notes: [OCKNote]? 83 | 84 | public var remoteID: String? 85 | 86 | public var encodingForParse: Bool = true { 87 | willSet { 88 | prepareEncodingRelational(newValue) 89 | } 90 | } 91 | 92 | public static var className: String { 93 | "HealthKitTask" 94 | } 95 | 96 | public var objectId: String? 97 | 98 | public var createdAt: Date? 99 | 100 | public var updatedAt: Date? 101 | 102 | public var ACL: ParseACL? 103 | 104 | public var originalData: Data? 105 | 106 | #if canImport(HealthKit) 107 | /// A structure specifying how this task is linked with HealthKit. 108 | public var healthKitLinkage: OCKHealthKitLinkage? { 109 | get { 110 | guard let data = healthKitLinkageString?.data(using: .utf8) else { 111 | return nil 112 | } 113 | return try? JSONDecoder().decode(OCKHealthKitLinkage.self, 114 | from: data) 115 | } set { 116 | guard let json = try? JSONEncoder().encode(newValue), 117 | let encodedString = String(data: json, encoding: .utf8) else { 118 | healthKitLinkageString = nil 119 | return 120 | } 121 | healthKitLinkageString = encodedString 122 | } 123 | } 124 | #endif 125 | 126 | /// A string specifying how this task is linked with HealthKit. 127 | public var healthKitLinkageString: String? 128 | 129 | /// If true, completion of this task will be factored into the patient's overall adherence. True by default. 130 | public var impactsAdherence: Bool? 131 | 132 | /// Instructions about how this task should be performed. 133 | public var instructions: String? 134 | 135 | /// A title that will be used to represent this task to the patient. 136 | public var title: String? 137 | 138 | /// A schedule that specifies how often this task occurs. 139 | public var schedule: OCKSchedule? 140 | 141 | /// The care plan to which this task belongs. 142 | public var carePlan: PCKCarePlan? { 143 | didSet { 144 | carePlanUUID = carePlan?.uuid 145 | } 146 | } 147 | 148 | /// The UUID of the care plan to which this task belongs. 149 | public var carePlanUUID: UUID? { 150 | didSet { 151 | if carePlanUUID != carePlan?.uuid { 152 | carePlan = nil 153 | } 154 | } 155 | } 156 | 157 | enum CodingKeys: String, CodingKey { 158 | case objectId, createdAt, updatedAt, 159 | className, ACL, uuid 160 | case entityId, schemaVersion, createdDate, updatedDate, 161 | deletedDate, timezone, userInfo, groupIdentifier, 162 | tags, source, asset, remoteID, notes, logicalClock 163 | case previousVersionUUIDs, nextVersionUUIDs, effectiveDate 164 | case title, carePlan, carePlanUUID, impactsAdherence, 165 | instructions, schedule, healthKitLinkageString 166 | case previousVersions, nextVersions 167 | #if canImport(HealthKit) 168 | case healthKitLinkage 169 | #endif 170 | } 171 | 172 | public init() { 173 | ACL = PCKUtility.getDefaultACL() 174 | } 175 | 176 | public static func new(from careKitEntity: OCKEntity) throws -> PCKHealthKitTask { 177 | 178 | switch careKitEntity { 179 | case .healthKitTask(let entity): 180 | return try new(from: entity) 181 | default: 182 | Logger.healthKitTask.error("new(with:) The wrong type (\(careKitEntity.entityType, privacy: .private)) of entity was passed as an argument.") 183 | throw ParseCareKitError.classTypeNotAnEligibleType 184 | } 185 | } 186 | 187 | public static func copyValues(from other: PCKHealthKitTask, to here: PCKHealthKitTask) throws -> PCKHealthKitTask { 188 | var here = here 189 | here.copyVersionedValues(from: other) 190 | here.previousVersionUUIDs = other.previousVersionUUIDs 191 | here.nextVersionUUIDs = other.nextVersionUUIDs 192 | here.impactsAdherence = other.impactsAdherence 193 | here.instructions = other.instructions 194 | here.title = other.title 195 | here.schedule = other.schedule 196 | here.carePlan = other.carePlan 197 | here.carePlanUUID = other.carePlanUUID 198 | return here 199 | } 200 | 201 | /** 202 | Creates a new ParseCareKit object from a specified CareKit Task. 203 | 204 | - parameter from: The CareKit Task used to create the new ParseCareKit object. 205 | - returns: Returns a new version of `Self` 206 | - throws: `Error`. 207 | */ 208 | public static func new(from taskAny: OCKAnyTask) throws -> PCKHealthKitTask { 209 | 210 | guard let task = taskAny as? OCKHealthKitTask else { 211 | throw ParseCareKitError.cantCastToNeededClassType 212 | } 213 | 214 | let encoded = try PCKUtility.jsonEncoder().encode(task) 215 | var decoded = try PCKUtility.decoder().decode(Self.self, from: encoded) 216 | decoded.objectId = task.uuid.uuidString 217 | decoded.entityId = task.id 218 | decoded.carePlan = PCKCarePlan(uuid: task.carePlanUUID) 219 | decoded.previousVersions = task.previousVersionUUIDs.map { Pointer(objectId: $0.uuidString) } 220 | decoded.nextVersions = task.nextVersionUUIDs.map { Pointer(objectId: $0.uuidString) } 221 | if let acl = task.acl { 222 | decoded.ACL = acl 223 | } else { 224 | decoded.ACL = PCKUtility.getDefaultACL() 225 | } 226 | return decoded 227 | } 228 | 229 | mutating func prepareEncodingRelational(_ encodingForParse: Bool) { 230 | if carePlan != nil { 231 | carePlan?.encodingForParse = encodingForParse 232 | } 233 | } 234 | 235 | // Note that Tasks have to be saved to CareKit first in order to properly convert Outcome to CareKit 236 | public func convertToCareKit() throws -> OCKHealthKitTask { 237 | var mutableTask = self 238 | mutableTask.encodingForParse = false 239 | let encoded = try PCKUtility.jsonEncoder().encode(mutableTask) 240 | return try PCKUtility.decoder().decode(OCKHealthKitTask.self, from: encoded) 241 | } 242 | } 243 | 244 | public extension PCKHealthKitTask { 245 | init(from decoder: Decoder) throws { 246 | let container = try decoder.container(keyedBy: CodingKeys.self) 247 | self.objectId = try container.decodeIfPresent(String.self, forKey: .objectId) 248 | self.createdAt = try container.decodeIfPresent(Date.self, forKey: .createdAt) 249 | self.updatedAt = try container.decodeIfPresent(Date.self, forKey: .updatedAt) 250 | self.ACL = try container.decodeIfPresent(ParseACL.self, forKey: .ACL) 251 | self.healthKitLinkageString = try container.decodeIfPresent(String.self, forKey: .healthKitLinkageString) 252 | #if canImport(HealthKit) 253 | if healthKitLinkageString == nil { 254 | self.healthKitLinkage = try container.decodeIfPresent(OCKHealthKitLinkage.self, forKey: .healthKitLinkage) 255 | } 256 | #endif 257 | self.carePlan = try container.decodeIfPresent(PCKCarePlan.self, forKey: .carePlan) 258 | self.carePlanUUID = try container.decodeIfPresent(UUID.self, forKey: .carePlanUUID) 259 | self.title = try container.decodeIfPresent(String.self, forKey: .title) 260 | self.logicalClock = try container.decodeIfPresent(Int.self, forKey: .logicalClock) 261 | self.impactsAdherence = try container.decodeIfPresent(Bool.self, forKey: .impactsAdherence) 262 | self.instructions = try container.decodeIfPresent(String.self, forKey: .instructions) 263 | self.schedule = try container.decodeIfPresent(OCKSchedule.self, forKey: .schedule) 264 | self.entityId = try container.decodeIfPresent(String.self, forKey: .entityId) 265 | self.createdDate = try container.decodeIfPresent(Date.self, forKey: .createdDate) 266 | self.updatedDate = try container.decodeIfPresent(Date.self, forKey: .updatedDate) 267 | self.deletedDate = try container.decodeIfPresent(Date.self, forKey: .deletedDate) 268 | self.effectiveDate = try container.decodeIfPresent(Date.self, forKey: .effectiveDate) 269 | self.timezone = try container.decodeIfPresent(TimeZone.self, forKey: .timezone) 270 | self.previousVersions = try container.decodeIfPresent([Pointer].self, forKey: .previousVersions) 271 | self.nextVersions = try container.decodeIfPresent([Pointer].self, forKey: .nextVersions) 272 | self.previousVersionUUIDs = try container.decodeIfPresent([UUID].self, forKey: .previousVersionUUIDs) 273 | self.nextVersionUUIDs = try container.decodeIfPresent([UUID].self, forKey: .nextVersionUUIDs) 274 | self.userInfo = try container.decodeIfPresent([String: String].self, forKey: .userInfo) 275 | self.remoteID = try container.decodeIfPresent(String.self, forKey: .remoteID) 276 | self.source = try container.decodeIfPresent(String.self, forKey: .source) 277 | self.asset = try container.decodeIfPresent(String.self, forKey: .asset) 278 | self.schemaVersion = try container.decodeIfPresent(OCKSemanticVersion.self, forKey: .schemaVersion) 279 | self.groupIdentifier = try container.decodeIfPresent(String.self, forKey: .groupIdentifier) 280 | self.tags = try container.decodeIfPresent([String].self, forKey: .tags) 281 | self.notes = try container.decodeIfPresent([OCKNote].self, forKey: .notes) 282 | } 283 | 284 | func encode(to encoder: Encoder) throws { 285 | var container = encoder.container(keyedBy: CodingKeys.self) 286 | if encodingForParse { 287 | try container.encodeIfPresent(carePlan?.toPointer(), forKey: .carePlan) 288 | try container.encodeIfPresent(healthKitLinkageString, forKey: .healthKitLinkageString) 289 | } else { 290 | #if canImport(HealthKit) 291 | try container.encodeIfPresent(healthKitLinkage, forKey: .healthKitLinkage) 292 | #endif 293 | } 294 | try container.encodeIfPresent(title, forKey: .title) 295 | try container.encodeIfPresent(carePlanUUID, forKey: .carePlanUUID) 296 | try container.encodeIfPresent(impactsAdherence, forKey: .impactsAdherence) 297 | try container.encodeIfPresent(instructions, forKey: .instructions) 298 | try container.encodeIfPresent(schedule, forKey: .schedule) 299 | try encodeVersionable(to: encoder) 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Models/PCKOutcome.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PCKOutcomes.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 1/14/20. 6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ParseSwift 11 | import CareKitStore 12 | import os.log 13 | 14 | // swiftlint:disable cyclomatic_complexity 15 | // swiftlint:disable line_length 16 | // swiftlint:disable type_body_length 17 | 18 | /// An `PCKOutcome` is the ParseCareKit equivalent of `OCKOutcome`. An `OCKOutcome` represents the 19 | /// outcome of an event corresponding to a task. An outcome may have 0 or more values associated with it. 20 | /// For example, a task that asks a patient to measure their temperature will have events whose outcome 21 | /// will contain a single value representing the patient's temperature. 22 | public struct PCKOutcome: PCKVersionable { 23 | 24 | public var previousVersionUUIDs: [UUID]? { 25 | willSet { 26 | guard let newValue = newValue else { 27 | previousVersions = nil 28 | return 29 | } 30 | var newPreviousVersions = [Pointer]() 31 | newValue.forEach { newPreviousVersions.append(Pointer(objectId: $0.uuidString)) } 32 | previousVersions = newPreviousVersions 33 | } 34 | } 35 | 36 | public var nextVersionUUIDs: [UUID]? { 37 | willSet { 38 | guard let newValue = newValue else { 39 | nextVersions = nil 40 | return 41 | } 42 | var newNextVersions = [Pointer]() 43 | newValue.forEach { newNextVersions.append(Pointer(objectId: $0.uuidString)) } 44 | nextVersions = newNextVersions 45 | } 46 | } 47 | 48 | public var previousVersions: [Pointer]? 49 | 50 | public var nextVersions: [Pointer]? 51 | 52 | public var effectiveDate: Date? 53 | 54 | public var entityId: String? 55 | 56 | public var logicalClock: Int? 57 | 58 | public var clock: PCKClock? 59 | 60 | public var schemaVersion: OCKSemanticVersion? 61 | 62 | public var createdDate: Date? 63 | 64 | public var updatedDate: Date? 65 | 66 | public var timezone: TimeZone? 67 | 68 | public var userInfo: [String: String]? 69 | 70 | public var groupIdentifier: String? 71 | 72 | public var tags: [String]? 73 | 74 | public var source: String? 75 | 76 | public var asset: String? 77 | 78 | public var notes: [OCKNote]? 79 | 80 | public var remoteID: String? 81 | 82 | public var encodingForParse: Bool = true { 83 | willSet { 84 | prepareEncodingRelational(newValue) 85 | } 86 | } 87 | 88 | public static var className: String { 89 | "Outcome" 90 | } 91 | 92 | public var objectId: String? 93 | 94 | public var createdAt: Date? 95 | 96 | public var updatedAt: Date? 97 | 98 | public var ACL: ParseACL? 99 | 100 | public var originalData: Data? 101 | 102 | var startDate: Date? // Custom added, check if needed 103 | 104 | var endDate: Date? // Custom added, check if needed 105 | 106 | /// The date on which this object was tombstoned. Note that objects are never actually deleted, 107 | /// but rather they are tombstoned and will no longer be returned from queries. 108 | public var deletedDate: Date? 109 | 110 | /// Specifies how many events occured before this outcome was created. For example, if a task is schedule to happen twice per day, then 111 | /// the 2nd outcome on the 2nd day will have a `taskOccurrenceIndex` of 3. 112 | /// 113 | /// - Note: The task occurrence references a specific version of a task, so if a new version the task is created, the task occurrence index 114 | /// will start again from 0. 115 | public var taskOccurrenceIndex: Int? 116 | 117 | /// An array of values associated with this outcome. Most outcomes will have 0 or 1 values, but some may have more. 118 | /// - Examples: 119 | /// - A task to call a physician might have 0 values, or 1 value containing the time stamp of when the call was placed. 120 | /// - A task to walk 2,000 steps might have 1 value, with that value being the number of steps that were actually taken. 121 | /// - A task to complete a survey might have multiple values corresponding to the answers to the questions in the survey. 122 | public var values: [OCKOutcomeValue]? 123 | 124 | /// The version of the task to which this outcomes belongs. 125 | public var task: PCKTask? { 126 | didSet { 127 | taskUUID = task?.uuid 128 | } 129 | } 130 | 131 | /// The version ID of the task to which this outcomes belongs. 132 | public var taskUUID: UUID? { 133 | didSet { 134 | if taskUUID != task?.uuid { 135 | task = nil 136 | } 137 | } 138 | } 139 | 140 | public init() { 141 | ACL = PCKUtility.getDefaultACL() 142 | } 143 | 144 | enum CodingKeys: String, CodingKey { 145 | case objectId, createdAt, updatedAt 146 | case entityId, schemaVersion, createdDate, updatedDate, timezone, 147 | userInfo, groupIdentifier, tags, source, asset, remoteID, notes 148 | case previousVersionUUIDs, nextVersionUUIDs, effectiveDate 149 | case task, taskUUID, taskOccurrenceIndex, values, deletedDate, startDate, endDate 150 | } 151 | 152 | public static func new(from careKitEntity: OCKEntity) throws -> Self { 153 | switch careKitEntity { 154 | case .outcome(let entity): 155 | return try new(from: entity) 156 | default: 157 | Logger.outcome.error("new(from:) The wrong type (\(careKitEntity.entityType, privacy: .private)) of entity was passed as an argument.") 158 | throw ParseCareKitError.classTypeNotAnEligibleType 159 | } 160 | } 161 | 162 | public static func copyValues(from other: PCKOutcome, to here: PCKOutcome) throws -> Self { 163 | var here = here 164 | here.copyVersionedValues(from: other) 165 | here.previousVersionUUIDs = other.previousVersionUUIDs 166 | here.nextVersionUUIDs = other.nextVersionUUIDs 167 | here.taskOccurrenceIndex = other.taskOccurrenceIndex 168 | here.values = other.values 169 | here.task = other.task 170 | return here 171 | } 172 | 173 | /** 174 | Creates a new ParseCareKit object from a specified CareKit Outcome. 175 | 176 | - parameter from: The CareKit Outcome used to create the new ParseCareKit object. 177 | - returns: Returns a new version of `Self` 178 | - throws: `Error`. 179 | */ 180 | public static func new(from outcomeAny: OCKAnyOutcome) throws -> Self { 181 | 182 | guard let outcome = outcomeAny as? OCKOutcome else { 183 | throw ParseCareKitError.cantCastToNeededClassType 184 | } 185 | let encoded = try PCKUtility.jsonEncoder().encode(outcome) 186 | var decoded = try PCKUtility.decoder().decode(Self.self, from: encoded) 187 | decoded.objectId = outcome.uuid.uuidString 188 | decoded.entityId = outcome.id 189 | decoded.task = PCKTask(uuid: outcome.taskUUID) 190 | decoded.previousVersions = outcome.previousVersionUUIDs.map { Pointer(objectId: $0.uuidString) } 191 | decoded.nextVersions = outcome.nextVersionUUIDs.map { Pointer(objectId: $0.uuidString) } 192 | if let acl = outcome.acl { 193 | decoded.ACL = acl 194 | } else { 195 | decoded.ACL = PCKUtility.getDefaultACL() 196 | } 197 | return decoded 198 | } 199 | 200 | public func copyRelational(_ parse: PCKOutcome) -> PCKOutcome { 201 | var copy = self 202 | if copy.values == nil { 203 | copy.values = .init() 204 | } 205 | if let valuesToCopy = parse.values { 206 | copy.values = valuesToCopy 207 | } 208 | return copy 209 | } 210 | 211 | mutating public func prepareEncodingRelational(_ encodingForParse: Bool) { 212 | if task != nil { 213 | task!.encodingForParse = encodingForParse 214 | } 215 | } 216 | 217 | // Note that Tasks have to be saved to CareKit first in order to properly convert Outcome to CareKit 218 | public func convertToCareKit() throws -> OCKOutcome { 219 | var mutableOutcome = self 220 | mutableOutcome.encodingForParse = false 221 | let encoded = try PCKUtility.jsonEncoder().encode(mutableOutcome) 222 | return try PCKUtility.decoder().decode(OCKOutcome.self, from: encoded) 223 | } 224 | 225 | public func fetchLocalDataAndSave(_ delegate: ParseRemoteDelegate?, 226 | completion: @escaping(Result) -> Void) { 227 | guard let taskUUID = taskUUID, 228 | let taskOccurrenceIndex = taskOccurrenceIndex else { 229 | completion(.failure(ParseCareKitError.errorString(""" 230 | Missing taskUUID or taskOccurrenceIndex for PCKOutcome: \(self) 231 | """))) 232 | return 233 | } 234 | guard let store = delegate?.provideStore() else { 235 | completion(.failure(ParseCareKitError.errorString(""" 236 | Missing ParseRemoteDelegate.provideStore() method which is required to sync OCKOutcome's 237 | """))) 238 | return 239 | } 240 | var task = OCKTaskQuery() 241 | task.uuids = [taskUUID] 242 | store.fetchAnyTasks(query: task, callbackQueue: .main) { taskResults in 243 | 244 | switch taskResults { 245 | case .success(let tasks): 246 | guard let task = tasks.first else { 247 | let error = ParseCareKitError.errorString("Could not find taskUUID \(taskUUID) in delegate store") 248 | completion(.failure(error)) 249 | return 250 | } 251 | var mutableOutcome = self 252 | 253 | guard let event = task.schedule.event(forOccurrenceIndex: taskOccurrenceIndex) else { 254 | mutableOutcome.startDate = nil 255 | mutableOutcome.endDate = nil 256 | mutableOutcome.save { result in 257 | switch result { 258 | case .success(let outcome): 259 | completion(.success(outcome)) 260 | case .failure(let error): 261 | let parseCareKitError = ParseCareKitError.errorString(error.localizedDescription) 262 | completion(.failure(parseCareKitError)) 263 | } 264 | } 265 | return 266 | } 267 | 268 | mutableOutcome.startDate = event.start 269 | mutableOutcome.endDate = event.end 270 | mutableOutcome.save { result in 271 | switch result { 272 | case .success(let outcome): 273 | completion(.success(outcome)) 274 | case .failure(let error): 275 | let parseCareKitError = ParseCareKitError.errorString(error.localizedDescription) 276 | completion(.failure(parseCareKitError)) 277 | } 278 | } 279 | case .failure(let error): 280 | completion(.failure(error)) 281 | } 282 | } 283 | } 284 | 285 | public static func tagWithId(_ outcome: OCKOutcome) -> OCKOutcome? { 286 | 287 | var mutableOutcome = outcome 288 | 289 | if mutableOutcome.tags != nil { 290 | if !mutableOutcome.tags!.contains(mutableOutcome.id) { 291 | mutableOutcome.tags!.append(mutableOutcome.id) 292 | return mutableOutcome 293 | } 294 | } else { 295 | mutableOutcome.tags = [mutableOutcome.id] 296 | return mutableOutcome 297 | } 298 | 299 | return nil 300 | } 301 | 302 | public static func queryNotDeleted() -> Query { 303 | let taskQuery = PCKTask.query( 304 | doesNotExist(key: OutcomeKey.deletedDate) 305 | ) 306 | // **** BAKER need to fix matchesKeyInQuery and find equivalent "queryKey" in matchesQuery 307 | let query = Self.query( 308 | doesNotExist( 309 | key: OutcomeKey.deletedDate 310 | ), 311 | matchesKeyInQuery( 312 | key: OutcomeKey.task, 313 | queryKey: OutcomeKey.task, 314 | query: taskQuery 315 | ) 316 | ) 317 | .limit(queryLimit) 318 | .includeAll() 319 | 320 | return query 321 | } 322 | 323 | func findOutcomes() async throws -> [PCKOutcome] { 324 | let query = Self.queryNotDeleted() 325 | .limit(queryLimit) 326 | return try await query.find() 327 | } 328 | 329 | public func findOutcomesInBackground(completion: @escaping([PCKOutcome]?, Error?) -> Void) { 330 | let query = Self.queryNotDeleted() 331 | query.find { results in 332 | 333 | switch results { 334 | 335 | case .success(let entities): 336 | completion(entities, nil) 337 | case .failure(let error): 338 | completion(nil, error) 339 | } 340 | } 341 | } 342 | } 343 | 344 | extension PCKOutcome { 345 | public func encode(to encoder: Encoder) throws { 346 | var container = encoder.container(keyedBy: CodingKeys.self) 347 | if encodingForParse { 348 | try container.encodeIfPresent(task?.toPointer(), forKey: .task) 349 | try container.encodeIfPresent(startDate, forKey: .startDate) 350 | try container.encodeIfPresent(endDate, forKey: .endDate) 351 | } 352 | try container.encodeIfPresent(taskUUID, forKey: .taskUUID) 353 | try container.encodeIfPresent(taskOccurrenceIndex, forKey: .taskOccurrenceIndex) 354 | try container.encodeIfPresent(values, forKey: .values) 355 | try encodeVersionable(to: encoder) 356 | } 357 | }// swiftlint:disable:this file_length 358 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Models/PCKPatient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PCKPatients.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 10/5/19. 6 | // Copyright © 2019 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import CareKitEssentials 10 | import CareKitStore 11 | import Foundation 12 | import os.log 13 | import ParseSwift 14 | 15 | // swiftlint:disable cyclomatic_complexity 16 | // swiftlint:disable type_body_length 17 | // swiftlint:disable line_length 18 | 19 | /// An `PCKPatient` is the ParseCareKit equivalent of `OCKPatient`. An `OCKPatient` represents a patient. 20 | public struct PCKPatient: PCKVersionable { 21 | 22 | public var previousVersionUUIDs: [UUID]? { 23 | willSet { 24 | guard let newValue = newValue else { 25 | previousVersions = nil 26 | return 27 | } 28 | var newPreviousVersions = [Pointer]() 29 | newValue.forEach { newPreviousVersions.append(Pointer(objectId: $0.uuidString)) } 30 | previousVersions = newPreviousVersions 31 | } 32 | } 33 | 34 | public var nextVersionUUIDs: [UUID]? { 35 | willSet { 36 | guard let newValue = newValue else { 37 | nextVersions = nil 38 | return 39 | } 40 | var newNextVersions = [Pointer]() 41 | newValue.forEach { newNextVersions.append(Pointer(objectId: $0.uuidString)) } 42 | nextVersions = newNextVersions 43 | } 44 | } 45 | 46 | public var previousVersions: [Pointer]? 47 | 48 | public var nextVersions: [Pointer]? 49 | 50 | public var effectiveDate: Date? 51 | 52 | public var entityId: String? 53 | 54 | public var logicalClock: Int? 55 | 56 | public var clock: PCKClock? 57 | 58 | public var schemaVersion: OCKSemanticVersion? 59 | 60 | public var createdDate: Date? 61 | 62 | public var updatedDate: Date? 63 | 64 | public var deletedDate: Date? 65 | 66 | public var timezone: TimeZone? 67 | 68 | public var userInfo: [String: String]? 69 | 70 | public var groupIdentifier: String? 71 | 72 | public var tags: [String]? 73 | 74 | public var source: String? 75 | 76 | public var asset: String? 77 | 78 | public var notes: [OCKNote]? 79 | 80 | public var remoteID: String? 81 | 82 | public var encodingForParse: Bool = true 83 | 84 | public static var className: String { 85 | "Patient" 86 | } 87 | 88 | public var objectId: String? 89 | 90 | public var createdAt: Date? 91 | 92 | public var updatedAt: Date? 93 | 94 | public var ACL: ParseACL? 95 | 96 | public var originalData: Data? 97 | 98 | /// A list of substances this patient is allergic to. 99 | public var allergies: [String]? 100 | 101 | /// The patient's birthday, used to compute their age. 102 | public var birthday: Date? 103 | 104 | /// The patient's name. 105 | public var name: PersonNameComponents? 106 | 107 | /// The patient's biological sex. 108 | public var sex: OCKBiologicalSex? 109 | 110 | enum CodingKeys: String, CodingKey { 111 | case objectId, createdAt, updatedAt 112 | case entityId, schemaVersion, createdDate, updatedDate, 113 | deletedDate, timezone, userInfo, groupIdentifier, tags, 114 | source, asset, remoteID, notes, logicalClock 115 | case previousVersionUUIDs, nextVersionUUIDs, effectiveDate 116 | case allergies, birthday, name, sex 117 | } 118 | 119 | public init() { 120 | ACL = PCKUtility.getDefaultACL() 121 | } 122 | 123 | public func encode(to encoder: Encoder) throws { 124 | var container = encoder.container(keyedBy: CodingKeys.self) 125 | try container.encodeIfPresent(allergies, forKey: .allergies) 126 | try container.encodeIfPresent(birthday, forKey: .birthday) 127 | try container.encodeIfPresent(name, forKey: .name) 128 | try container.encodeIfPresent(sex, forKey: .sex) 129 | try encodeVersionable(to: encoder) 130 | } 131 | 132 | public static func new(from careKitEntity: OCKEntity) throws -> PCKPatient { 133 | 134 | switch careKitEntity { 135 | case .patient(let entity): 136 | return try new(from: entity) 137 | default: 138 | Logger.patient.error("new(with:) The wrong type (\(careKitEntity.entityType, privacy: .private)) of entity was passed as an argument.") 139 | throw ParseCareKitError.classTypeNotAnEligibleType 140 | } 141 | } 142 | 143 | public static func copyValues(from other: PCKPatient, to here: PCKPatient) throws -> Self { 144 | var here = here 145 | here.copyVersionedValues(from: other) 146 | here.previousVersionUUIDs = other.previousVersionUUIDs 147 | here.nextVersionUUIDs = other.nextVersionUUIDs 148 | here.name = other.name 149 | here.birthday = other.birthday 150 | here.sex = other.sex 151 | here.allergies = other.allergies 152 | return here 153 | } 154 | 155 | /** 156 | Creates a new ParseCareKit object from a specified CareKit Patient. 157 | 158 | - parameter from: The CareKit Patient used to create the new ParseCareKit object. 159 | - returns: Returns a new version of `Self` 160 | - throws: `Error`. 161 | */ 162 | public static func new(from patientAny: OCKAnyPatient) throws -> PCKPatient { 163 | 164 | guard let patient = patientAny as? OCKPatient else { 165 | throw ParseCareKitError.cantCastToNeededClassType 166 | } 167 | 168 | let encoded = try PCKUtility.jsonEncoder().encode(patient) 169 | var decoded = try PCKUtility.decoder().decode(PCKPatient.self, from: encoded) 170 | decoded.objectId = patient.uuid.uuidString 171 | decoded.entityId = patient.id 172 | decoded.previousVersions = patient.previousVersionUUIDs.map { Pointer(objectId: $0.uuidString) } 173 | decoded.nextVersions = patient.nextVersionUUIDs.map { Pointer(objectId: $0.uuidString) } 174 | if let acl = patient.acl { 175 | decoded.ACL = acl 176 | } else { 177 | decoded.ACL = PCKUtility.getDefaultACL() 178 | } 179 | return decoded 180 | } 181 | 182 | public func convertToCareKit() throws -> OCKPatient { 183 | var mutablePatient = self 184 | mutablePatient.encodingForParse = false 185 | let encoded = try PCKUtility.jsonEncoder().encode(mutablePatient) 186 | return try PCKUtility.decoder().decode(OCKPatient.self, from: encoded) 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Models/PCKRevisionRecord.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PCKRevisionRecord.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 4/16/23. 6 | // Copyright © 2023 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import CareKitStore 10 | import Foundation 11 | import os.log 12 | import ParseSwift 13 | 14 | /// Revision records are exchanged by the CareKit and a ParseCareKit remote during synchronization. 15 | /// Each revision record contains an array of entities as well as a knowledge vector. 16 | struct PCKRevisionRecord: ParseObject { 17 | 18 | public static var className: String { 19 | "RevisionRecord" 20 | } 21 | 22 | var originalData: Data? 23 | 24 | var objectId: String? 25 | 26 | var createdAt: Date? 27 | 28 | var updatedAt: Date? 29 | 30 | var ACL: ParseACL? 31 | 32 | var clockUUID: UUID? 33 | 34 | /// The clock value when this record was added to the Parse remote. 35 | var logicalClock: Int? 36 | 37 | /// The clock associated with this record when it was added to the Parse remote. 38 | var clock: PCKClock? 39 | 40 | /// The entities that were modified, in the order the were inserted into the database. 41 | /// The first entity is the oldest and the last entity is the newest. 42 | var entities: [PCKEntity]? 43 | 44 | /// A knowledge vector indicating the last known state of each other device 45 | /// by the device that authored this revision record. 46 | var knowledgeVector: OCKRevisionRecord.KnowledgeVector? { 47 | get { 48 | try? PCKClock.decodeVector(knowledgeVectorString) 49 | } 50 | set { 51 | guard let newValue = newValue else { 52 | knowledgeVectorString = nil 53 | return 54 | } 55 | knowledgeVectorString = PCKClock.encodeVector(newValue) 56 | } 57 | } 58 | 59 | var knowledgeVectorString: String? 60 | 61 | var storeClassesToSynchronize: [PCKStoreClass: any PCKVersionable.Type]? = try? PCKStoreClass.getConcrete() 62 | 63 | var customClassesToSynchronize: [String: any PCKVersionable.Type]? 64 | 65 | var objects: [any PCKVersionable] { 66 | guard let entities = entities else { 67 | return [] 68 | } 69 | return entities.map { $0.value } 70 | } 71 | 72 | var patients: [PCKPatient] { 73 | guard let entities = entities else { 74 | return [] 75 | } 76 | return entities.compactMap { $0.value as? PCKPatient } 77 | } 78 | 79 | var carePlans: [PCKCarePlan] { 80 | guard let entities = entities else { 81 | return [] 82 | } 83 | return entities.compactMap { $0.value as? PCKCarePlan } 84 | } 85 | 86 | var contacts: [PCKContact] { 87 | guard let entities = entities else { 88 | return [] 89 | } 90 | return entities.compactMap { $0.value as? PCKContact } 91 | } 92 | 93 | var tasks: [PCKTask] { 94 | guard let entities = entities else { 95 | return [] 96 | } 97 | return entities.compactMap { $0.value as? PCKTask } 98 | } 99 | 100 | var healthKitTasks: [PCKHealthKitTask] { 101 | guard let entities = entities else { 102 | return [] 103 | } 104 | return entities.compactMap { $0.value as? PCKHealthKitTask } 105 | } 106 | 107 | var outcomes: [PCKOutcome] { 108 | guard let entities = entities else { 109 | return [] 110 | } 111 | return entities.compactMap { $0.value as? PCKOutcome } 112 | } 113 | 114 | enum CodingKeys: String, CodingKey { 115 | case objectId, createdAt, updatedAt, className, 116 | ACL, knowledgeVectorString, entities, 117 | logicalClock, clock, clockUUID 118 | } 119 | 120 | func hash(into hasher: inout Hasher) { 121 | hasher.combine(id) 122 | hasher.combine(createdAt) 123 | hasher.combine(updatedAt) 124 | hasher.combine(ACL) 125 | hasher.combine(originalData) 126 | hasher.combine(clockUUID) 127 | hasher.combine(knowledgeVectorString) 128 | hasher.combine(logicalClock) 129 | hasher.combine(clock) 130 | hasher.combine(entities) 131 | } 132 | 133 | static func == (lhs: PCKRevisionRecord, rhs: PCKRevisionRecord) -> Bool { 134 | lhs.id == rhs.id && 135 | lhs.createdAt == rhs.createdAt && 136 | lhs.updatedAt == rhs.updatedAt && 137 | lhs.ACL == rhs.ACL && 138 | lhs.originalData == rhs.originalData && 139 | lhs.clockUUID == rhs.clockUUID && 140 | lhs.clock == rhs.clock && 141 | lhs.knowledgeVectorString == rhs.knowledgeVectorString && 142 | lhs.logicalClock == rhs.logicalClock && 143 | lhs.entities == rhs.entities 144 | } 145 | 146 | func convertToCareKit() throws -> OCKRevisionRecord { 147 | guard let entities = entities, 148 | let knowledgeVector = knowledgeVector else { 149 | throw ParseCareKitError.couldntUnwrapSelf 150 | } 151 | let careKitEntities = try entities.compactMap { try $0.careKit() } 152 | return OCKRevisionRecord(entities: careKitEntities, 153 | knowledgeVector: knowledgeVector) 154 | } 155 | 156 | func save( 157 | options: API.Options = [], 158 | batchLimit: Int 159 | ) async throws { 160 | let duplicateErrorString = "Attempted to add an object that is already on the server, skipping the save" 161 | let patientObjectIDs: [String] = patients.compactMap(\.objectId) 162 | do { 163 | let numberOfPatients = patients.count 164 | if numberOfPatients > batchLimit { 165 | Logger.revisionRecord.warning( 166 | "Attempting to save a large amount of \(numberOfPatients) Patients to the server, please ensure your server supports transactions of this size" 167 | ) 168 | } 169 | let results = try await patients.createAll( 170 | batchLimit: numberOfPatients, 171 | options: options 172 | ) 173 | results.forEach { result in 174 | switch result { 175 | case .success: 176 | return 177 | case .failure(let error): 178 | if error.equalsTo(.duplicateValue) { 179 | Logger.revisionRecord.warning( 180 | "\(duplicateErrorString)" 181 | ) 182 | } else { 183 | Logger.revisionRecord.error("Failed to save RevisionRecord due to Patient: \(error)") 184 | } 185 | } 186 | } 187 | } catch let parseError as ParseError { 188 | if parseError.equalsTo(.duplicateValue) { 189 | Logger.revisionRecord.warning( 190 | "\(duplicateErrorString). Verify the following Patients are already on the server: \(patientObjectIDs)" 191 | ) 192 | } else { 193 | Logger.revisionRecord.error("Failed to save RevisionRecord due to Patients: \(parseError)") 194 | } 195 | } 196 | 197 | let carePlanObjectIDs: [String] = carePlans.compactMap(\.objectId) 198 | do { 199 | let numberOfCarePlans = carePlans.count 200 | if numberOfCarePlans > batchLimit { 201 | Logger.revisionRecord.warning( 202 | "Attempting to save a large amount of \(numberOfCarePlans) CarePlans to the server, please ensure your server supports transactions of this size" 203 | ) 204 | } 205 | let results = try await carePlans.createAll( 206 | batchLimit: numberOfCarePlans, 207 | options: options 208 | ) 209 | results.forEach { result in 210 | switch result { 211 | case .success: 212 | return 213 | case .failure(let error): 214 | if error.equalsTo(.duplicateValue) { 215 | Logger.revisionRecord.warning( 216 | "\(duplicateErrorString)" 217 | ) 218 | } else { 219 | Logger.revisionRecord.error("Failed to save RevisionRecord due to CarePlan: \(error)") 220 | } 221 | } 222 | } 223 | } catch let parseError as ParseError { 224 | if parseError.equalsTo(.duplicateValue) { 225 | Logger.revisionRecord.warning( 226 | "\(duplicateErrorString). Verify the following CarePlans are already on the server: \(carePlanObjectIDs)" 227 | ) 228 | } else { 229 | Logger.revisionRecord.error("Failed to save RevisionRecord due to CarePlans: \(parseError)") 230 | } 231 | } 232 | 233 | let contactObjectIDs: [String] = contacts.compactMap(\.objectId) 234 | do { 235 | let numberOfContacts = contacts.count 236 | if numberOfContacts > batchLimit { 237 | Logger.revisionRecord.warning( 238 | "Attempting to save a large amount of \(numberOfContacts) Contacts to the server, please ensure your server supports transactions of this size" 239 | ) 240 | } 241 | let results = try await contacts.createAll( 242 | batchLimit: numberOfContacts, 243 | options: options 244 | ) 245 | results.forEach { result in 246 | switch result { 247 | case .success: 248 | return 249 | case .failure(let error): 250 | if error.equalsTo(.duplicateValue) { 251 | Logger.revisionRecord.warning( 252 | "\(duplicateErrorString)" 253 | ) 254 | } else { 255 | Logger.revisionRecord.error("Failed to save RevisionRecord due to Contact: \(error)") 256 | } 257 | } 258 | } 259 | } catch let parseError as ParseError { 260 | if parseError.equalsTo(.duplicateValue) { 261 | Logger.revisionRecord.warning( 262 | "\(duplicateErrorString). Verify the following Contacts are already on the server: \(contactObjectIDs)" 263 | ) 264 | } else { 265 | Logger.revisionRecord.error("Failed to save RevisionRecord due to Contacts: \(parseError)") 266 | } 267 | } 268 | 269 | let taskObjectIDs: [String] = tasks.compactMap(\.objectId) 270 | do { 271 | let numberOfTasks = tasks.count 272 | if numberOfTasks > batchLimit { 273 | Logger.revisionRecord.warning( 274 | "Attempting to save a large amount of \(numberOfTasks) Tasks to the server, please ensure your server supports transactions of this size" 275 | ) 276 | } 277 | let results = try await tasks.createAll( 278 | batchLimit: numberOfTasks, 279 | options: options 280 | ) 281 | results.forEach { result in 282 | switch result { 283 | case .success: 284 | return 285 | case .failure(let error): 286 | if error.equalsTo(.duplicateValue) { 287 | Logger.revisionRecord.warning( 288 | "\(duplicateErrorString)" 289 | ) 290 | } else { 291 | Logger.revisionRecord.error("Failed to save RevisionRecord due to Task: \(error)") 292 | } 293 | } 294 | } 295 | } catch let parseError as ParseError { 296 | if parseError.equalsTo(.duplicateValue) { 297 | Logger.revisionRecord.warning( 298 | "\(duplicateErrorString). Verify the following Tasks are already on the server: \(taskObjectIDs)" 299 | ) 300 | } else { 301 | Logger.revisionRecord.error("Failed to save RevisionRecord due to Tasks: \(parseError)") 302 | } 303 | } 304 | 305 | let healthKitTaskObjectIDs: [String] = healthKitTasks.compactMap(\.objectId) 306 | do { 307 | let numberOfHealthKitTasks = healthKitTasks.count 308 | if numberOfHealthKitTasks > batchLimit { 309 | Logger.revisionRecord.warning( 310 | "Attempting to save a large amount of \(numberOfHealthKitTasks) HealthKitTasks to the server, please ensure your server supports transactions of this size" 311 | ) 312 | } 313 | let results = try await healthKitTasks.createAll( 314 | batchLimit: numberOfHealthKitTasks, 315 | options: options 316 | ) 317 | results.forEach { result in 318 | switch result { 319 | case .success: 320 | return 321 | case .failure(let error): 322 | if error.equalsTo(.duplicateValue) { 323 | Logger.revisionRecord.warning( 324 | "\(duplicateErrorString)" 325 | ) 326 | } else { 327 | Logger.revisionRecord.error("Failed to save RevisionRecord due to HealthKitTask: \(error)") 328 | } 329 | } 330 | } 331 | } catch let parseError as ParseError { 332 | if parseError.equalsTo(.duplicateValue) { 333 | Logger.revisionRecord.warning( 334 | "\(duplicateErrorString). Verify the following HealthKitTasks are already on the server: \(healthKitTaskObjectIDs)" 335 | ) 336 | } else { 337 | Logger.revisionRecord.error("Failed to save RevisionRecord due to HealthKitTasks: \(parseError)") 338 | } 339 | } 340 | 341 | let outcomeObjectIDs: [String] = outcomes.compactMap(\.objectId) 342 | do { 343 | let numberOfOutcomes = outcomes.count 344 | if numberOfOutcomes > batchLimit { 345 | Logger.revisionRecord.warning( 346 | "Attempting to save a large amount of \(numberOfOutcomes) Outcomes to the server, please ensure your server supports transactions of this size" 347 | ) 348 | } 349 | let results = try await outcomes.createAll( 350 | batchLimit: numberOfOutcomes, 351 | options: options 352 | ) 353 | results.forEach { result in 354 | switch result { 355 | case .success: 356 | return 357 | case .failure(let error): 358 | if error.equalsTo(.duplicateValue) { 359 | Logger.revisionRecord.warning( 360 | "\(duplicateErrorString)" 361 | ) 362 | } else { 363 | Logger.revisionRecord.error("Failed to save RevisionRecord due to Outcome: \(error)") 364 | } 365 | } 366 | } 367 | } catch let parseError as ParseError { 368 | if parseError.equalsTo(.duplicateValue) { 369 | Logger.revisionRecord.warning( 370 | "\(duplicateErrorString). Verify the following Outcomes are already on the server: \(outcomeObjectIDs)" 371 | ) 372 | } else { 373 | Logger.revisionRecord.error("Failed to save RevisionRecord due to Outcomes: \(parseError)") 374 | } 375 | } 376 | do { 377 | try await self.create( 378 | options: options 379 | ) 380 | } catch let parseError as ParseError { 381 | if parseError.equalsTo(.duplicateValue) { 382 | Logger.revisionRecord.warning( 383 | "\(duplicateErrorString). Verify the following RevisionRecord is already on the server: \(self.id)" 384 | ) 385 | } else { 386 | Logger.revisionRecord.error("Failed to save RevisionRecord due to creating RevisionRecord: \(parseError)") 387 | } 388 | } 389 | } 390 | 391 | func fetchEntities(options: API.Options = []) async throws -> Self { 392 | guard let entities = entities else { 393 | throw ParseCareKitError.couldntUnwrapSelf 394 | } 395 | var mutableRecord = self 396 | let patients = try await PCKPatient.query( 397 | containedIn( 398 | key: ParseKey.objectId, 399 | array: self.patients.compactMap { $0.objectId } 400 | ) 401 | ) 402 | .limit(queryLimit) 403 | .find(options: options) 404 | 405 | let carePlans = try await PCKCarePlan.query( 406 | containedIn( 407 | key: ParseKey.objectId, 408 | array: self.carePlans.compactMap { $0.objectId } 409 | ) 410 | ) 411 | .limit(queryLimit) 412 | .find(options: options) 413 | 414 | let contacts = try await PCKContact.query( 415 | containedIn( 416 | key: ParseKey.objectId, 417 | array: self.contacts.compactMap { $0.objectId } 418 | ) 419 | ) 420 | .limit(queryLimit) 421 | .find(options: options) 422 | 423 | let tasks = try await PCKTask.query( 424 | containedIn( 425 | key: ParseKey.objectId, 426 | array: self.tasks.compactMap { $0.objectId } 427 | ) 428 | ) 429 | .limit(queryLimit) 430 | .find(options: options) 431 | 432 | let healthKitTasks = try await PCKHealthKitTask.query( 433 | containedIn( 434 | key: ParseKey.objectId, 435 | array: self.healthKitTasks.compactMap { $0.objectId } 436 | ) 437 | ) 438 | .limit(queryLimit) 439 | .find(options: options) 440 | 441 | let outcomes = try await PCKOutcome.query( 442 | containedIn( 443 | key: ParseKey.objectId, 444 | array: self.outcomes.compactMap { $0.objectId } 445 | ) 446 | ) 447 | .limit(queryLimit) 448 | .find(options: options) 449 | 450 | mutableRecord.entities?.removeAll() 451 | try entities.forEach { entity in 452 | switch entity { 453 | case .patient(let patient): 454 | guard let fetched = patients.first(where: { $0.objectId == patient.objectId }) else { 455 | throw ParseCareKitError.errorString(""" 456 | Patient with objectId, \"\(String(describing: patient.objectId))\" is not on remote 457 | """) 458 | } 459 | mutableRecord.entities?.append(PCKEntity.patient(fetched)) 460 | case .carePlan(let plan): 461 | guard let fetched = carePlans.first(where: { $0.objectId == plan.objectId }) else { 462 | throw ParseCareKitError.errorString(""" 463 | CarePlan with objectId, \"\(String(describing: plan.objectId))\" is not on remote 464 | """) 465 | } 466 | mutableRecord.entities?.append(PCKEntity.carePlan(fetched)) 467 | case .contact(let contact): 468 | guard let fetched = contacts.first(where: { $0.objectId == contact.objectId }) else { 469 | throw ParseCareKitError.errorString(""" 470 | Contact with objectId, \"\(String(describing: contact.objectId))\" is not on remote 471 | """) 472 | } 473 | mutableRecord.entities?.append(PCKEntity.contact(fetched)) 474 | case .task(let task): 475 | guard let fetched = tasks.first(where: { $0.objectId == task.objectId }) else { 476 | throw ParseCareKitError.errorString(""" 477 | Task with objectId, \"\(String(describing: task.objectId))\" is not on remote 478 | """) 479 | } 480 | mutableRecord.entities?.append(PCKEntity.task(fetched)) 481 | case .healthKitTask(let healthKitTask): 482 | guard let fetched = healthKitTasks.first(where: { $0.objectId == healthKitTask.objectId }) else { 483 | throw ParseCareKitError.errorString(""" 484 | HealthKitTask with objectId, \"\(String(describing: healthKitTask.objectId))\" is not on remote 485 | """) 486 | } 487 | mutableRecord.entities?.append(PCKEntity.healthKitTask(fetched)) 488 | case .outcome(let outcome): 489 | guard let fetched = outcomes.first(where: { $0.objectId == outcome.objectId }) else { 490 | throw ParseCareKitError.errorString(""" 491 | Outcome with objectId, \"\(String(describing: outcome.objectId))\" is not on remote 492 | """) 493 | } 494 | mutableRecord.entities?.append(PCKEntity.outcome(fetched)) 495 | } 496 | } 497 | return mutableRecord 498 | } 499 | } 500 | 501 | extension PCKRevisionRecord { 502 | 503 | init(from decoder: Decoder) throws { 504 | let container = try decoder.container(keyedBy: CodingKeys.self) 505 | self.objectId = try container.decodeIfPresent(String.self, forKey: .objectId) 506 | self.createdAt = try container.decodeIfPresent(Date.self, forKey: .createdAt) 507 | self.updatedAt = try container.decodeIfPresent(Date.self, forKey: .updatedAt) 508 | self.ACL = try container.decodeIfPresent(ParseACL.self, forKey: .ACL) 509 | self.knowledgeVectorString = try container.decodeIfPresent(String.self, forKey: .knowledgeVectorString) 510 | self.entities = try container.decodeIfPresent([PCKEntity].self, forKey: .entities) 511 | self.clock = try container.decodeIfPresent(PCKClock.self, forKey: .clock) 512 | self.logicalClock = try container.decodeIfPresent(Int.self, forKey: .logicalClock) 513 | self.clockUUID = try container.decodeIfPresent(UUID.self, forKey: .clockUUID) 514 | } 515 | 516 | public func encode(to encoder: Encoder) throws { 517 | var container = encoder.container(keyedBy: CodingKeys.self) 518 | try container.encodeIfPresent(objectId, forKey: .objectId) 519 | try container.encodeIfPresent(createdAt, forKey: .createdAt) 520 | try container.encodeIfPresent(updatedAt, forKey: .updatedAt) 521 | try container.encodeIfPresent(ACL, forKey: .ACL) 522 | try container.encodeIfPresent(knowledgeVectorString, forKey: .knowledgeVectorString) 523 | try container.encodeIfPresent(entities, forKey: .entities) 524 | try container.encodeIfPresent(clock, forKey: .clock) 525 | try container.encodeIfPresent(logicalClock, forKey: .logicalClock) 526 | try container.encodeIfPresent(clockUUID, forKey: .clockUUID) 527 | } 528 | 529 | /// Create a new instance of `PCKRevisionRecord`. 530 | /// 531 | /// - Parameters: 532 | /// - record: The CareKit revision record. 533 | /// - remoteClockUUID: The remote clock uuid this record is designed for. 534 | /// - remoteClock: The remote clock uuid this record is designed for. 535 | /// - remoteClockValue: The remote clock uuid this record is designed for. 536 | init(record: OCKRevisionRecord, 537 | remoteClockUUID: UUID, 538 | remoteClock: PCKClock, 539 | remoteClockValue: Int, 540 | storeClassesToSynchronize: [PCKStoreClass: any PCKVersionable.Type]? = nil, 541 | customClassesToSynchronize: [String: any PCKVersionable.Type]? = nil) throws { 542 | self.objectId = UUID().uuidString 543 | self.ACL = PCKUtility.getDefaultACL() 544 | self.clockUUID = remoteClockUUID 545 | self.logicalClock = remoteClockValue 546 | self.clock = remoteClock 547 | self.knowledgeVector = record.knowledgeVector 548 | self.storeClassesToSynchronize = storeClassesToSynchronize 549 | self.customClassesToSynchronize = customClassesToSynchronize 550 | self.entities = try record.entities.compactMap { entity in 551 | var parseEntity = try entity.parseEntity().value 552 | parseEntity.logicalClock = remoteClockValue // Stamp Entity 553 | parseEntity.clock = remoteClock 554 | parseEntity.remoteID = remoteClockUUID.uuidString 555 | switch entity { 556 | case .patient: 557 | guard let parseEntity = parseEntity as? PCKPatient else { 558 | return nil 559 | } 560 | return PCKEntity.patient(parseEntity) 561 | case .carePlan: 562 | guard let parseEntity = parseEntity as? PCKCarePlan else { 563 | return nil 564 | } 565 | return PCKEntity.carePlan(parseEntity) 566 | case .contact: 567 | guard let parseEntity = parseEntity as? PCKContact else { 568 | return nil 569 | } 570 | return PCKEntity.contact(parseEntity) 571 | case .task: 572 | guard let parseEntity = parseEntity as? PCKTask else { 573 | return nil 574 | } 575 | return PCKEntity.task(parseEntity) 576 | case .healthKitTask: 577 | guard let parseEntity = parseEntity as? PCKHealthKitTask else { 578 | return nil 579 | } 580 | return PCKEntity.healthKitTask(parseEntity) 581 | case .outcome: 582 | guard let parseEntity = parseEntity as? PCKOutcome else { 583 | return nil 584 | } 585 | return PCKEntity.outcome(parseEntity) 586 | } 587 | } 588 | } 589 | } 590 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Models/PCKStoreClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PCKStoreClass.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 4/17/23. 6 | // Copyright © 2023 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import CareKitStore 10 | import Foundation 11 | import os.log 12 | 13 | // swiftlint:disable line_length 14 | 15 | /// Types of ParseCareKit classes. 16 | public enum PCKStoreClass: String, Hashable, CaseIterable, Sendable { 17 | /// The ParseCareKit equivalent of `OCKPatient`. 18 | case patient 19 | /// The ParseCareKit equivalent of `OCKCarePlan`. 20 | case carePlan 21 | /// The ParseCareKit equivalent of `OCKContact`. 22 | case contact 23 | /// The ParseCareKit equivalent of `OCKTask`. 24 | case task 25 | /// The ParseCareKit equivalent of `OCKHealthKitTask`. 26 | case healthKitTask 27 | /// The ParseCareKit equivalent of `OCKOutcome`. 28 | case outcome 29 | 30 | func getDefault() -> any PCKVersionable.Type { 31 | switch self { 32 | case .patient: 33 | return PCKPatient.self 34 | case .carePlan: 35 | return PCKCarePlan.self 36 | case .contact: 37 | return PCKContact.self 38 | case .task: 39 | return PCKTask.self 40 | case .healthKitTask: 41 | return PCKHealthKitTask.self 42 | case .outcome: 43 | return PCKOutcome.self 44 | } 45 | } 46 | 47 | static func replaceRemoteConcreteClasses(_ newClasses: [PCKStoreClass: any PCKVersionable.Type]) throws -> [PCKStoreClass: any PCKVersionable.Type] { 48 | var updatedClasses = try getConcrete() 49 | 50 | for (key, value) in newClasses { 51 | if isCorrectType(key, check: value) { 52 | updatedClasses[key] = value 53 | } else { 54 | Logger.pullRevisions.debug("PCKStoreClass.replaceRemoteConcreteClasses(). Discarding class for `\(key.rawValue, privacy: .private)` because it's of the wrong type. All classes need to subclass a PCK concrete type. If you are trying to map a class to a OCKStore concreate type, pass it to `customClasses` instead. This class is not compatibile") 55 | } 56 | } 57 | return updatedClasses 58 | } 59 | 60 | static func getConcrete() throws -> [PCKStoreClass: any PCKVersionable.Type] { 61 | 62 | var concreteClasses: [PCKStoreClass: any PCKVersionable.Type] = [ 63 | .carePlan: PCKStoreClass.carePlan.getDefault(), 64 | .contact: PCKStoreClass.contact.getDefault(), 65 | .outcome: PCKStoreClass.outcome.getDefault(), 66 | .patient: PCKStoreClass.patient.getDefault(), 67 | .task: PCKStoreClass.task.getDefault(), 68 | .healthKitTask: PCKStoreClass.healthKitTask.getDefault() 69 | ] 70 | 71 | for (key, value) in concreteClasses { 72 | // swiftlint:disable for_where 73 | if !isCorrectType(key, check: value) { 74 | concreteClasses.removeValue(forKey: key) 75 | } 76 | } 77 | 78 | // Ensure all default classes are created 79 | guard concreteClasses.count == Self.allCases.count else { 80 | throw ParseCareKitError.couldntCreateConcreteClasses 81 | } 82 | 83 | return concreteClasses 84 | } 85 | 86 | static func replaceConcreteClasses(_ newClasses: [PCKStoreClass: any PCKVersionable.Type]) throws -> [PCKStoreClass: any PCKVersionable.Type] { 87 | var updatedClasses = try getConcrete() 88 | 89 | for (key, value) in newClasses { 90 | if isCorrectType(key, check: value) { 91 | updatedClasses[key] = value 92 | } else { 93 | Logger.pullRevisions.debug("PCKStoreClass.replaceConcreteClasses(). Discarding class for `\(key.rawValue, privacy: .private)` because it's of the wrong type. All classes need to subclass a PCK concrete type. If you are trying to map a class to a OCKStore concreate type, pass it to `customClasses` instead. This class is not compatibile") 94 | } 95 | } 96 | return updatedClasses 97 | } 98 | 99 | static func isCorrectType(_ type: PCKStoreClass, check: any PCKVersionable.Type) -> Bool { 100 | switch type { 101 | case .carePlan: 102 | return check is PCKCarePlan.Type 103 | case .contact: 104 | return check is PCKContact.Type 105 | case .outcome: 106 | return check is PCKOutcome.Type 107 | case .patient: 108 | return check is PCKPatient.Type 109 | case .task: 110 | return check is PCKTask.Type 111 | case .healthKitTask: 112 | return check is PCKHealthKitTask.Type 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Models/PCKTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Task.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 1/17/20. 6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ParseSwift 11 | import CareKitStore 12 | import os.log 13 | 14 | // swiftlint:disable line_length 15 | // swiftlint:disable cyclomatic_complexity 16 | // swiftlint:disable function_body_length 17 | // swiftlint:disable type_body_length 18 | 19 | /// An `PCKTask` is the ParseCareKit equivalent of `OCKTask`. An `OCKTask` represents some task or action that a 20 | /// patient is supposed to perform. Tasks are optionally associable with an `OCKCarePlan` and must have a unique 21 | /// id and schedule. The schedule determines when and how often the task should be performed, and the 22 | /// `impactsAdherence` flag may be used to specify whether or not the patients adherence to this task will affect 23 | /// their daily completion rings. 24 | public struct PCKTask: PCKVersionable { 25 | 26 | public var previousVersionUUIDs: [UUID]? { 27 | willSet { 28 | guard let newValue = newValue else { 29 | previousVersions = nil 30 | return 31 | } 32 | var newPreviousVersions = [Pointer]() 33 | newValue.forEach { newPreviousVersions.append(Pointer(objectId: $0.uuidString)) } 34 | previousVersions = newPreviousVersions 35 | } 36 | } 37 | 38 | public var nextVersionUUIDs: [UUID]? { 39 | willSet { 40 | guard let newValue = newValue else { 41 | nextVersions = nil 42 | return 43 | } 44 | var newNextVersions = [Pointer]() 45 | newValue.forEach { newNextVersions.append(Pointer(objectId: $0.uuidString)) } 46 | nextVersions = newNextVersions 47 | } 48 | } 49 | 50 | public var previousVersions: [Pointer]? 51 | 52 | public var nextVersions: [Pointer]? 53 | 54 | public var effectiveDate: Date? 55 | 56 | public var entityId: String? 57 | 58 | public var logicalClock: Int? 59 | 60 | public var clock: PCKClock? 61 | 62 | public var schemaVersion: OCKSemanticVersion? 63 | 64 | public var createdDate: Date? 65 | 66 | public var updatedDate: Date? 67 | 68 | public var deletedDate: Date? 69 | 70 | public var timezone: TimeZone? 71 | 72 | public var userInfo: [String: String]? 73 | 74 | public var groupIdentifier: String? 75 | 76 | public var tags: [String]? 77 | 78 | public var source: String? 79 | 80 | public var asset: String? 81 | 82 | public var notes: [OCKNote]? 83 | 84 | public var remoteID: String? 85 | 86 | public var encodingForParse: Bool = true { 87 | willSet { 88 | prepareEncodingRelational(newValue) 89 | } 90 | } 91 | 92 | public static var className: String { 93 | "Task" 94 | } 95 | 96 | public var objectId: String? 97 | 98 | public var createdAt: Date? 99 | 100 | public var updatedAt: Date? 101 | 102 | public var ACL: ParseACL? 103 | 104 | public var originalData: Data? 105 | 106 | /// If true, completion of this task will be factored into the patient's overall adherence. True by default. 107 | public var impactsAdherence: Bool? 108 | 109 | /// Instructions about how this task should be performed. 110 | public var instructions: String? 111 | 112 | /// A title that will be used to represent this task to the patient. 113 | public var title: String? 114 | 115 | /// A schedule that specifies how often this task occurs. 116 | public var schedule: OCKSchedule? 117 | 118 | /// The care plan to which this task belongs. 119 | public var carePlan: PCKCarePlan? { 120 | didSet { 121 | carePlanUUID = carePlan?.uuid 122 | } 123 | } 124 | 125 | /// The UUID of the care plan to which this task belongs. 126 | public var carePlanUUID: UUID? { 127 | didSet { 128 | if carePlanUUID != carePlan?.uuid { 129 | carePlan = nil 130 | } 131 | } 132 | } 133 | 134 | enum CodingKeys: String, CodingKey { 135 | case objectId, createdAt, updatedAt 136 | case entityId, schemaVersion, createdDate, updatedDate, 137 | deletedDate, timezone, userInfo, groupIdentifier, 138 | tags, source, asset, remoteID, notes, logicalClock 139 | case previousVersionUUIDs, nextVersionUUIDs, effectiveDate 140 | case title, carePlan, carePlanUUID, impactsAdherence, instructions, schedule 141 | } 142 | 143 | public init() { 144 | ACL = PCKUtility.getDefaultACL() 145 | } 146 | 147 | public static func new(from careKitEntity: OCKEntity) throws -> PCKTask { 148 | 149 | switch careKitEntity { 150 | case .task(let entity): 151 | return try new(from: entity) 152 | default: 153 | Logger.task.error("new(with:) The wrong type (\(careKitEntity.entityType, privacy: .private)) of entity was passed as an argument.") 154 | throw ParseCareKitError.classTypeNotAnEligibleType 155 | } 156 | } 157 | 158 | public static func copyValues(from other: PCKTask, to here: PCKTask) throws -> PCKTask { 159 | var here = here 160 | here.copyVersionedValues(from: other) 161 | here.previousVersionUUIDs = other.previousVersionUUIDs 162 | here.nextVersionUUIDs = other.nextVersionUUIDs 163 | here.impactsAdherence = other.impactsAdherence 164 | here.instructions = other.instructions 165 | here.title = other.title 166 | here.schedule = other.schedule 167 | here.carePlan = other.carePlan 168 | here.carePlanUUID = other.carePlanUUID 169 | return here 170 | } 171 | 172 | /** 173 | Creates a new ParseCareKit object from a specified CareKit Task. 174 | 175 | - parameter from: The CareKit Task used to create the new ParseCareKit object. 176 | - returns: Returns a new version of `Self` 177 | - throws: `Error`. 178 | */ 179 | public static func new(from taskAny: OCKAnyTask) throws -> PCKTask { 180 | 181 | guard let task = taskAny as? OCKTask else { 182 | throw ParseCareKitError.cantCastToNeededClassType 183 | } 184 | 185 | let encoded = try PCKUtility.jsonEncoder().encode(task) 186 | var decoded = try PCKUtility.decoder().decode(Self.self, from: encoded) 187 | decoded.objectId = task.uuid.uuidString 188 | decoded.entityId = task.id 189 | decoded.carePlan = PCKCarePlan(uuid: task.carePlanUUID) 190 | decoded.previousVersions = task.previousVersionUUIDs.map { Pointer(objectId: $0.uuidString) } 191 | decoded.nextVersions = task.nextVersionUUIDs.map { Pointer(objectId: $0.uuidString) } 192 | if let acl = task.acl { 193 | decoded.ACL = acl 194 | } else { 195 | decoded.ACL = PCKUtility.getDefaultACL() 196 | } 197 | return decoded 198 | } 199 | 200 | mutating func prepareEncodingRelational(_ encodingForParse: Bool) { 201 | if carePlan != nil { 202 | carePlan?.encodingForParse = encodingForParse 203 | } 204 | } 205 | 206 | // Note that Tasks have to be saved to CareKit first in order to properly convert Outcome to CareKit 207 | public func convertToCareKit() throws -> OCKTask { 208 | var mutableTask = self 209 | mutableTask.encodingForParse = false 210 | let encoded = try PCKUtility.jsonEncoder().encode(mutableTask) 211 | return try PCKUtility.decoder().decode(OCKTask.self, from: encoded) 212 | } 213 | } 214 | 215 | extension PCKTask { 216 | public func encode(to encoder: Encoder) throws { 217 | var container = encoder.container(keyedBy: CodingKeys.self) 218 | if encodingForParse { 219 | try container.encodeIfPresent(carePlan?.toPointer(), forKey: .carePlan) 220 | } 221 | try container.encodeIfPresent(title, forKey: .title) 222 | try container.encodeIfPresent(carePlanUUID, forKey: .carePlanUUID) 223 | try container.encodeIfPresent(impactsAdherence, forKey: .impactsAdherence) 224 | try container.encodeIfPresent(instructions, forKey: .instructions) 225 | try container.encodeIfPresent(schedule, forKey: .schedule) 226 | try encodeVersionable(to: encoder) 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Models/Parse/PCKReadRole.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PCKReadRole.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 1/30/22. 6 | // Copyright © 2022 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ParseSwift 11 | 12 | struct PCKReadRole: PCKRoleable { 13 | typealias RoleUser = PCKUser 14 | 15 | var originalData: Data? 16 | 17 | var objectId: String? 18 | 19 | var createdAt: Date? 20 | 21 | var updatedAt: Date? 22 | 23 | var ACL: ParseACL? 24 | 25 | var name: String? 26 | 27 | var owner: PCKUser? 28 | 29 | static var appendString: String { 30 | "_read" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Models/Parse/PCKUser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PCKUser.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 9/25/20. 6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ParseSwift 11 | 12 | struct PCKUser: ParseUser { 13 | 14 | var authData: [String: [String: String]?]? 15 | 16 | var username: String? 17 | 18 | var email: String? 19 | 20 | var emailVerified: Bool? 21 | 22 | var password: String? 23 | 24 | var objectId: String? 25 | 26 | var createdAt: Date? 27 | 28 | var updatedAt: Date? 29 | 30 | var ACL: ParseACL? 31 | 32 | var originalData: Data? 33 | } 34 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Models/Parse/PCKWriteRole.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PCKWriteRole.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 1/30/22. 6 | // Copyright © 2022 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ParseSwift 11 | 12 | struct PCKWriteRole: PCKRoleable { 13 | typealias RoleUser = PCKUser 14 | 15 | var originalData: Data? 16 | 17 | var objectId: String? 18 | 19 | var createdAt: Date? 20 | 21 | var updatedAt: Date? 22 | 23 | var ACL: ParseACL? 24 | 25 | var name: String? 26 | 27 | var owner: PCKUser? 28 | 29 | static var appendString: String { 30 | "_write" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Models/ParseCareKitError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParseCareKitError.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 12/12/20. 6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum ParseCareKitError: Error { 12 | case userNotLoggedIn 13 | case relatedEntityNotOnRemote 14 | case requiredValueCantBeUnwrapped 15 | case objectIdDoesntMatchRemoteId 16 | case objectNotFoundOnParseServer 17 | case remoteClockLargerThanLocal 18 | case couldntUnwrapClock 19 | case couldntUnwrapRequiredField 20 | case couldntUnwrapSelf 21 | case remoteVersionNewerThanLocal 22 | case uuidAlreadyExists 23 | case cantCastToNeededClassType 24 | case cantEncodeACL 25 | case classTypeNotAnEligibleType 26 | case couldntCreateConcreteClasses 27 | case syncAlreadyInProgress 28 | case parseHealthError 29 | case errorString(_ string: String) 30 | } 31 | 32 | extension ParseCareKitError: LocalizedError { 33 | public var errorDescription: String? { 34 | switch self { 35 | case .userNotLoggedIn: 36 | return NSLocalizedString("ParseCareKit: Parse User is not logged in.", comment: "Login error") 37 | case .relatedEntityNotOnRemote: 38 | return NSLocalizedString("ParseCareKit: Related entity is not on remote.", comment: "Related entity error") 39 | case .requiredValueCantBeUnwrapped: 40 | return NSLocalizedString("ParseCareKit: Required value can't be unwrapped.", comment: "Unwrapping error") 41 | case .couldntUnwrapClock: 42 | return NSLocalizedString("ParseCareKit: Clock can't be unwrapped.", comment: "Clock Unwrapping error") 43 | case .couldntUnwrapRequiredField: 44 | return NSLocalizedString("ParseCareKit: Could not unwrap required field.", 45 | comment: "Could not unwrap required field") 46 | case .objectIdDoesntMatchRemoteId: 47 | return NSLocalizedString("ParseCareKit: remoteId and objectId don't match.", 48 | comment: "Remote/Local mismatch error") 49 | case .remoteClockLargerThanLocal: 50 | return NSLocalizedString("Remote clock larger than local during pushRevisions, not pushing", 51 | comment: "Knowledge vector larger on Remote") 52 | case .couldntUnwrapSelf: 53 | return NSLocalizedString("Cannot unwrap self. This class has already been deallocated", 54 | comment: "Cannot unwrap self, class deallocated") 55 | case .remoteVersionNewerThanLocal: 56 | return NSLocalizedString("Cannot sync, the Remote version newer than local version", 57 | comment: "Remote version newer than local version") 58 | case .uuidAlreadyExists: 59 | return NSLocalizedString("Cannot sync, the uuid already exists on the Remote", 60 | comment: "UUID is not unique") 61 | case .cantCastToNeededClassType: 62 | return NSLocalizedString("Cannot cast to needed class type", 63 | comment: "Cannot cast to needed class type") 64 | case .cantEncodeACL: 65 | return NSLocalizedString("Cannot encode ACL", 66 | comment: "Cannot encode ACL") 67 | case .classTypeNotAnEligibleType: 68 | return NSLocalizedString("PCKClass type is not an eligible type", 69 | comment: "PCKClass type is not an eligible type") 70 | case .couldntCreateConcreteClasses: 71 | return NSLocalizedString("Could not create concrete classes", 72 | comment: "Could not create concrete classes") 73 | case .objectNotFoundOnParseServer: 74 | return NSLocalizedString("Object couldn't be found on the Parse Server", 75 | comment: "Object couldn't be found on the Parse Server") 76 | case .syncAlreadyInProgress: 77 | return NSLocalizedString("Sync already in progress!", comment: "Sync already in progress!") 78 | case .parseHealthError: 79 | return NSLocalizedString("There was a problem with the health of the remote!", 80 | comment: "There was a problem with the health of the remote!") 81 | case .errorString(let string): return string 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Models/RemoteSynchronizing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RemoteSynchronizing.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 4/10/23. 6 | // Copyright © 2023 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import CareKitStore 10 | import Foundation 11 | 12 | actor RemoteSynchronizing { 13 | var isSynchronizing = false { 14 | willSet { 15 | if newValue { 16 | resetLiveQueryRetry() 17 | } 18 | } 19 | } 20 | var liveQueryRetry = 0 21 | var clock: PCKClock? 22 | var knowledgeVector: OCKRevisionRecord.KnowledgeVector? { 23 | clock?.knowledgeVector 24 | } 25 | 26 | func synchronizing() { 27 | isSynchronizing = true 28 | } 29 | 30 | func notSynchronzing() { 31 | isSynchronizing = false 32 | } 33 | 34 | func resetLiveQueryRetry() { 35 | liveQueryRetry = 0 36 | } 37 | 38 | func retryLiveQueryAfter() throws -> Int { 39 | liveQueryRetry += 1 40 | guard liveQueryRetry <= 10 else { 41 | throw ParseCareKitError.errorString("Max retries reached") 42 | } 43 | return Int.random(in: 0...liveQueryRetry) 44 | } 45 | 46 | func updateClock(_ clock: PCKClock?) { 47 | self.clock = clock 48 | } 49 | 50 | func updateClockIfNeeded(_ clock: PCKClock) { 51 | guard self.clock == nil else { 52 | return 53 | } 54 | self.clock = clock 55 | } 56 | 57 | func hasNewerClock(_ vector: OCKRevisionRecord.KnowledgeVector, for uuid: UUID) -> Bool { 58 | guard let currentVector = knowledgeVector else { 59 | return true 60 | } 61 | var testVector = currentVector 62 | testVector.merge(with: vector) 63 | let currentClock = currentVector.clock(for: uuid) 64 | return vector.clock(for: uuid) > currentClock || testVector.uuids.count > currentVector.uuids.count 65 | } 66 | 67 | func hasNewerVector(_ vector: OCKRevisionRecord.KnowledgeVector) -> Bool { 68 | guard let currentVector = knowledgeVector else { 69 | return true 70 | } 71 | return vector > currentVector 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/PCKUtility.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PCKUtility.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 4/26/20. 6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ParseSwift 11 | import os.log 12 | 13 | // swiftlint:disable line_length 14 | 15 | /// Utility functions designed to make things easier. 16 | public class PCKUtility { 17 | 18 | class func getPlistConfiguration(fileName: String) throws -> [String: AnyObject] { 19 | var propertyListFormat = PropertyListSerialization.PropertyListFormat.xml 20 | guard let path = Bundle.main.path(forResource: fileName, ofType: "plist"), 21 | let xml = FileManager.default.contents(atPath: path) else { 22 | fatalError("Error in ParseCareKit.configureParse(). Cannot find ParseCareKit.plist in this project") 23 | } 24 | 25 | return try PropertyListSerialization.propertyList(from: xml, 26 | options: .mutableContainersAndLeaves, 27 | // swiftlint:disable:next force_cast 28 | format: &propertyListFormat) as! [String: AnyObject] 29 | } 30 | 31 | /** 32 | Configure the client to connect to a Parse Server based on a ParseCareKit.plist file. 33 | 34 | The key/values supported in the file are a dictionary named `ParseClientConfiguration`: 35 | - Server - (String) The server URL to connect to Parse Server. 36 | - ApplicationID - (String) The application id of your Parse application. 37 | - ClientKey - (String) The client key of your Parse application. 38 | - LiveQueryServer - (String) The live query server URL to connect to Parse Server. 39 | - UseTransactionsInternally - (Boolean) Use transactions inside the Client SDK. 40 | - DeleteKeychainIfNeeded - (Boolean) Deletes the Parse Keychain when the app is running for the first time. 41 | - parameter fileName: Name of **.plist** file that contains config. Defaults to "ParseCareKit". 42 | - parameter authentication: A callback block that will be used to receive/accept/decline network challenges. 43 | Defaults to `nil` in which the SDK will use the default OS authentication methods for challenges. 44 | It should have the following argument signature: `(challenge: URLAuthenticationChallenge, 45 | completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void`. 46 | See Apple's [documentation](https://developer.apple.com/documentation/foundation/urlsessiontaskdelegate/1411595-urlsession) for more for details. 47 | */ 48 | public class func configureParse(fileName: String = "ParseCareKit", 49 | authentication: ((URLAuthenticationChallenge, 50 | (URLSession.AuthChallengeDisposition, 51 | URLCredential?) -> Void) -> Void)? = nil) async throws { 52 | var plistConfiguration: [String: AnyObject] 53 | var clientKey: String? 54 | var liveQueryURL: URL? 55 | var useTransactions = false 56 | var deleteKeychainIfNeeded = false 57 | do { 58 | plistConfiguration = try Self.getPlistConfiguration(fileName: fileName) 59 | } catch { 60 | fatalError("Error in ParseCareKit.configureParse(). Could not serialize plist. \(error)") 61 | } 62 | 63 | guard let appID = plistConfiguration["ApplicationID"] as? String, 64 | let server = plistConfiguration["Server"] as? String, 65 | let serverURL = URL(string: server) else { 66 | fatalError("Error in ParseCareKit.configureParse(). Missing keys in \(plistConfiguration)") 67 | } 68 | 69 | if let client = plistConfiguration["ClientKey"] as? String { 70 | clientKey = client 71 | } 72 | 73 | if let liveQuery = plistConfiguration["LiveQueryServer"] as? String { 74 | liveQueryURL = URL(string: liveQuery) 75 | } 76 | 77 | if let transactions = plistConfiguration["UseTransactions"] as? Bool { 78 | useTransactions = transactions 79 | } 80 | 81 | if let deleteKeychain = plistConfiguration["DeleteKeychainIfNeeded"] as? Bool { 82 | deleteKeychainIfNeeded = deleteKeychain 83 | } 84 | 85 | try await ParseSwift.initialize( 86 | applicationId: appID, 87 | clientKey: clientKey, 88 | serverURL: serverURL, 89 | liveQueryServerURL: liveQueryURL, 90 | requiringCustomObjectIds: true, 91 | usingTransactions: useTransactions, 92 | usingPostForQuery: true, 93 | deletingKeychainIfNeeded: deleteKeychainIfNeeded, 94 | authentication: authentication 95 | ) 96 | } 97 | 98 | /** 99 | Setup the Keychain access group and synchronization across devices. 100 | 101 | The key/values supported in the file are a dictionary named `ParseClientConfiguration`: 102 | - AccessGroup - (String) The Keychain access group. 103 | - SynchronizeKeychain - (Boolean) Whether or not to synchronize the Keychain across devices. 104 | - parameter fileName: Name of **.plist** file that contains config. Defaults to "ParseCareKit". 105 | */ 106 | public class func setAccessGroup(fileName: String = "ParseCareKit") async throws { 107 | var plistConfiguration: [String: AnyObject] 108 | var accessGroup: String? 109 | var synchronizeKeychain = false 110 | 111 | do { 112 | plistConfiguration = try Self.getPlistConfiguration(fileName: fileName) 113 | } catch { 114 | fatalError("Error in ParseCareKit.configureParse(). Could not serialize plist. \(error)") 115 | } 116 | 117 | if let keychainAccessGroup = plistConfiguration["AccessGroup"] as? String { 118 | accessGroup = keychainAccessGroup 119 | } 120 | 121 | if let synchronizeKeychainAcrossDevices = plistConfiguration["SynchronizeKeychain"] as? Bool { 122 | synchronizeKeychain = synchronizeKeychainAcrossDevices 123 | } 124 | 125 | try await ParseSwift.setAccessGroup(accessGroup, 126 | synchronizeAcrossDevices: synchronizeKeychain) 127 | } 128 | 129 | /// Get the current Parse Encoder with custom date strategy. 130 | public class func encoder() -> ParseEncoder { 131 | PCKOutcome.getEncoder() 132 | } 133 | 134 | /// Get the current JSON Encoder with custom date strategy. 135 | public class func jsonEncoder() -> JSONEncoder { 136 | PCKOutcome.getJSONEncoder() 137 | } 138 | 139 | /// Get the current JSON Decoder with custom date strategy. 140 | public class func decoder() -> JSONDecoder { 141 | PCKOutcome.getDecoder() 142 | } 143 | 144 | /// Remove ParseCareKit cache from device. 145 | public class func removeCache() { 146 | UserDefaults.standard.removeObject(forKey: ParseCareKitConstants.defaultACL) 147 | UserDefaults.standard.synchronize() 148 | } 149 | 150 | /// Get the default ACL for `ParseCareKit` objects. 151 | public class func getDefaultACL() -> ParseACL? { 152 | guard let aclString = UserDefaults.standard.value(forKey: ParseCareKitConstants.defaultACL) as? String, 153 | let aclData = aclString.data(using: .utf8), 154 | let acl = try? decoder().decode(ParseACL.self, from: aclData) else { 155 | return nil 156 | } 157 | return acl 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/ParseCareKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // ParseCareKit.h 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 4/26/20. 6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for ParseCareKit. 12 | FOUNDATION_EXPORT double ParseCareKitVersionNumber; 13 | 14 | //! Project version string for ParseCareKit. 15 | FOUNDATION_EXPORT const unsigned char ParseCareKitVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/ParseCareKitConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParseCareKitConstants.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 4/26/20. 6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ParseSwift 11 | import CareKitStore 12 | import os.log 13 | 14 | public enum ParseCareKitConstants { 15 | static let defaultACL = "edu.netreconlab.ParseCareKit_defaultACL" 16 | static let acl = "_acl" 17 | static let administratorRole = "Administrators" 18 | } 19 | 20 | // MARK: Custom Enums 21 | let queryLimit = 1000 22 | enum CustomKey { 23 | static let className = "className" 24 | } 25 | 26 | // MARK: Parse Database Keys 27 | 28 | /// Parse business logic keys. These keys can be used for querying Parse objects. 29 | public enum ParseKey { 30 | /// objectId key. 31 | public static let objectId = "objectId" 32 | /// createdAt key. 33 | public static let createdAt = "createdAt" 34 | /// updatedAt key. 35 | public static let updatedAt = "updatedAt" 36 | /// objectId key. 37 | public static let ACL = "ACL" 38 | /// name key for ParseRole. 39 | public static let name = "name" 40 | } 41 | 42 | /// Keys for all `PCKObjectable` objects. These keys can be used for querying Parse objects. 43 | public enum ObjectableKey { 44 | /// entityId key. 45 | public static let entityId = "entityId" 46 | /// asset key. 47 | public static let asset = "asset" 48 | /// groupIdentifier key. 49 | public static let groupIdentifier = "groupIdentifier" 50 | /// notes key. 51 | public static let notes = "notes" 52 | /// timezone key. 53 | public static let timezone = "timezone" 54 | /// logicalClock key. 55 | public static let logicalClock = "logicalClock" 56 | /// clock key. 57 | public static let clock = "clock" 58 | /// clockUUID key. 59 | public static let clockUUID = "clockUUID" 60 | /// createdDate key. 61 | public static let createdDate = "createdDate" 62 | /// updatedDate key. 63 | public static let updatedDate = "updatedDate" 64 | /// tags key. 65 | public static let tags = "tags" 66 | /// userInfo key. 67 | public static let userInfo = "userInfo" 68 | /// source key. 69 | public static let source = "source" 70 | /// remoteID key. 71 | public static let remoteID = "remoteID" 72 | } 73 | 74 | /// Keys for all `PCKVersionable` objects. These keys can be used for querying Parse objects. 75 | public enum VersionableKey { 76 | /// deletedDate key. 77 | public static let deletedDate = "deletedDate" 78 | /// effectiveDate key. 79 | public static let effectiveDate = "effectiveDate" 80 | /// nextVersionUUIDs key. 81 | public static let nextVersionUUIDs = "nextVersionUUIDs" 82 | /// previousVersionUUIDs key. 83 | public static let previousVersionUUIDs = "previousVersionUUIDs" 84 | } 85 | 86 | // MARK: Patient Class 87 | /// Keys for `PCKPatient` objects. These keys can be used for querying Parse objects. 88 | public enum PatientKey { 89 | /// className key. 90 | public static let className = "Patient" 91 | /// allergies key. 92 | public static let allergies = "alergies" 93 | /// birthday key. 94 | public static let birthday = "birthday" 95 | /// sex key. 96 | public static let sex = "sex" 97 | /// name key. 98 | public static let name = "name" 99 | } 100 | 101 | // MARK: CarePlan Class 102 | /// Keys for `PCKCarePlan` objects. These keys can be used for querying Parse objects. 103 | public enum CarePlanKey { 104 | /// className key. 105 | public static let className = "CarePlan" 106 | /// patient key. 107 | public static let patient = "patient" 108 | /// title key. 109 | public static let title = "title" 110 | } 111 | 112 | // MARK: Contact Class 113 | /// Keys for `PCKContact` objects. These keys can be used for querying Parse objects. 114 | public enum ContactKey { 115 | /// className key. 116 | public static let className = "Contact" 117 | /// carePlan key. 118 | public static let carePlan = "carePlan" 119 | /// title key. 120 | public static let title = "title" 121 | /// role key. 122 | public static let role = "role" 123 | /// organization key. 124 | public static let organization = "organization" 125 | /// category key. 126 | public static let category = "category" 127 | /// name key. 128 | public static let name = "name" 129 | /// address key. 130 | public static let address = "address" 131 | /// emailAddresses key. 132 | public static let emailAddresses = "emailAddresses" 133 | /// phoneNumbers key. 134 | public static let phoneNumbers = "phoneNumbers" 135 | /// messagingNumbers key. 136 | public static let messagingNumbers = "messagingNumbers" 137 | /// otherContactInfo key. 138 | public static let otherContactInfo = "otherContactInfo" 139 | } 140 | 141 | // MARK: Task Class 142 | /// Keys for `PCKTask` objects. These keys can be used for querying Parse objects. 143 | public enum TaskKey { 144 | /// className key. 145 | public static let className = "Task" 146 | /// title key. 147 | public static let title = "title" 148 | /// carePlan key. 149 | public static let carePlan = "carePlan" 150 | /// impactsAdherence key. 151 | public static let impactsAdherence = "impactsAdherence" 152 | /// instructions key. 153 | public static let instructions = "instructions" 154 | /// elements key. 155 | public static let elements = "elements" 156 | } 157 | 158 | // MARK: Outcome Class 159 | /// Keys for `PCKOutcome` objects. These keys can be used for querying Parse objects. 160 | public enum OutcomeKey { 161 | /// className key. 162 | public static let className = "Outcome" 163 | /// deletedDate key. 164 | public static let deletedDate = "deletedDate" 165 | /// task key. 166 | public static let task = "task" 167 | /// taskOccurrenceIndex key. 168 | public static let taskOccurrenceIndex = "taskOccurrenceIndex" 169 | /// values key. 170 | public static let values = "values" 171 | } 172 | 173 | // MARK: Clock Class 174 | /// Keys for `Clock` objects. These keys can be used for querying Parse objects. 175 | public enum ClockKey { 176 | /// className key. 177 | public static let className = "Clock" 178 | /// uuid key. 179 | public static let uuid = "uuid" 180 | /// vector key. 181 | public static let vector = "vector" 182 | } 183 | 184 | // MARK: RevisionRecord Class 185 | /// Keys for `RevisionRecord` objects. These keys can be used for querying Parse objects. 186 | enum RevisionRecordKey { 187 | /// className key. 188 | static let className = "RevisionRecord" 189 | /// clockUUID key. 190 | static let clockUUID = "clockUUID" 191 | /// logicalClock key. 192 | static let logicalClock = "logicalClock" 193 | } 194 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/ParseCareKitLog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParseCareKitLog.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 12/12/20. 6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import os.log 11 | 12 | extension OSLog { 13 | private static var subsystem = Bundle.main.bundleIdentifier! 14 | static let category = "ParseCareKit" 15 | static let carePlan = OSLog(subsystem: subsystem, category: "\(category).carePlan") 16 | static let contact = OSLog(subsystem: subsystem, category: "\(category).carePlan") 17 | static let patient = OSLog(subsystem: subsystem, category: "\(category).patient") 18 | static let task = OSLog(subsystem: subsystem, category: "\(category).task") 19 | static let healthKitTask = OSLog(subsystem: subsystem, category: "\(category).healthKitTask") 20 | static let outcome = OSLog(subsystem: subsystem, category: "\(category).outcome") 21 | static let versionable = OSLog(subsystem: subsystem, category: "\(category).versionable") 22 | static let objectable = OSLog(subsystem: subsystem, category: "\(category).objectable") 23 | static let pullRevisions = OSLog(subsystem: subsystem, category: "\(category).pullRevisions") 24 | static let pushRevisions = OSLog(subsystem: subsystem, category: "\(category).pushRevisions") 25 | static let syncProgress = OSLog(subsystem: subsystem, category: "\(category).syncProgress") 26 | static let clock = OSLog(subsystem: subsystem, category: "\(category).clock") 27 | static let clockSubscription = OSLog(subsystem: subsystem, category: "\(category).clockSubscription") 28 | static let defaultACL = OSLog(subsystem: subsystem, category: "\(category).defaultACL") 29 | static let initializer = OSLog(subsystem: subsystem, category: "\(category).initializer") 30 | static let deinitializer = OSLog(subsystem: subsystem, category: "\(category).deinitializer") 31 | static let ockCarePlan = OSLog(subsystem: subsystem, category: "\(category).OCKCarePlan") 32 | static let ockContact = OSLog(subsystem: subsystem, category: "\(category).OCKContact") 33 | static let ockHealthKitTask = OSLog(subsystem: subsystem, category: "\(category).OCKHealthKitTask") 34 | static let ockOutcome = OSLog(subsystem: subsystem, category: "\(category).OCKOutcome") 35 | static let ockPatient = OSLog(subsystem: subsystem, category: "\(category).OCKPatient") 36 | static let ockTask = OSLog(subsystem: subsystem, category: "\(category).OCKTask") 37 | } 38 | 39 | @available(iOS 14.0, watchOS 7.0, *) 40 | extension Logger { 41 | private static var subsystem = Bundle.main.bundleIdentifier! 42 | static let category = "ParseCareKit" 43 | static let carePlan = Logger(subsystem: subsystem, category: "\(category).carePlan") 44 | static let contact = Logger(subsystem: subsystem, category: "\(category).carePlan") 45 | static let patient = Logger(subsystem: subsystem, category: "\(category).patient") 46 | static let task = Logger(subsystem: subsystem, category: "\(category).task") 47 | static let healthKitTask = Logger(subsystem: subsystem, category: "\(category).healthKitTask") 48 | static let outcome = Logger(subsystem: subsystem, category: "\(category).outcome") 49 | static let versionable = Logger(subsystem: subsystem, category: "\(category).versionable") 50 | static let objectable = Logger(subsystem: subsystem, category: "\(category).objectable") 51 | static let pullRevisions = Logger(subsystem: subsystem, category: "\(category).pullRevisions") 52 | static let pushRevisions = Logger(subsystem: subsystem, category: "\(category).pushRevisions") 53 | static let syncProgress = Logger(subsystem: subsystem, category: "\(category).syncProgress") 54 | static let clock = Logger(subsystem: subsystem, category: "\(category).clock") 55 | static let clockSubscription = Logger(subsystem: subsystem, category: "\(category).clockSubscription") 56 | static let revisionRecord = Logger(subsystem: subsystem, category: "\(category).revisionRecord") 57 | static let defaultACL = Logger(subsystem: subsystem, category: "\(category).defaultACL") 58 | static let initializer = Logger(subsystem: subsystem, category: "\(category).initializer") 59 | static let deinitializer = Logger(subsystem: subsystem, category: "\(category).deinitializer") 60 | static let ockCarePlan = Logger(subsystem: subsystem, category: "\(category).OCKCarePlan") 61 | static let ockContact = Logger(subsystem: subsystem, category: "\(category).OCKContact") 62 | static let ockHealthKitTask = Logger(subsystem: subsystem, category: "\(category).OCKHealthKitTask") 63 | static let ockOutcome = Logger(subsystem: subsystem, category: "\(category).OCKOutcome") 64 | static let ockHealthKitOutcome = Logger(subsystem: subsystem, category: "\(category).OCKHealthKitOutcome") 65 | static let ockPatient = Logger(subsystem: subsystem, category: "\(category).OCKPatient") 66 | static let ockTask = Logger(subsystem: subsystem, category: "\(category).OCKTask") 67 | } 68 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Protocols/PCKObjectable+async.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PCKObjectable+async.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 10/6/21. 6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ParseSwift 11 | 12 | public extension PCKObjectable { 13 | 14 | /** 15 | Finds the first object on the remote that has the same `uuid`. 16 | - Parameters: 17 | - uuid: The UUID to search for. 18 | - options: A set of header options sent to the remote. Defaults to an empty set. 19 | - relatedObject: An object that has the same `uuid` as the one being searched for. 20 | - returns: The first object found with the matching `uuid`. 21 | - throws: `ParseError`. 22 | */ 23 | static func first(_ uuid: UUID?, 24 | options: API.Options = []) async throws -> Self { 25 | try await withCheckedThrowingContinuation { continuation in 26 | Self.first( 27 | uuid, 28 | options: options, 29 | completion: { continuation.resume(with: $0) } 30 | ) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Protocols/PCKObjectable+combine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PCKObjectable+combine.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 10/6/21. 6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Foundation 11 | import ParseSwift 12 | import Combine 13 | 14 | @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) 15 | public extension PCKObjectable { 16 | 17 | /** 18 | Finds the first object on the remote that has the same `uuid`. 19 | - Parameters: 20 | - uuid: The UUID to search for. 21 | - options: A set of header options sent to the remote. Defaults to an empty set. 22 | - relatedObject: An object that has the same `uuid` as the one being searched for. 23 | - returns: The first object found with the matching `uuid`. 24 | - throws: `Error`. 25 | */ 26 | static func firstPublisher(_ uuid: UUID?, 27 | options: API.Options = []) -> Future { 28 | Future { promise in 29 | Self.first(uuid, 30 | options: options, 31 | completion: promise) 32 | } 33 | } 34 | } 35 | 36 | #endif 37 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Protocols/PCKObjectable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PCKObjectable.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 5/26/20. 6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ParseSwift 11 | import CareKitStore 12 | import os.log 13 | 14 | /** 15 | Objects that conform to the `PCKObjectable` protocol are Parse interpretations of `OCKObjectCompatible` objects. 16 | */ 17 | public protocol PCKObjectable: ParseObject { 18 | /// A universally unique identifier for this object. 19 | var uuid: UUID? { get } 20 | 21 | /// A human readable unique identifier. It is used strictly by the developer and will never be shown to a user 22 | var id: String { get } 23 | 24 | /// A human readable unique identifier (same as `id`, but this is what's on the Parse remote, `id` is 25 | /// already taken in Parse). It is used strictly by the developer and will never be shown to a user 26 | var entityId: String? { get set } 27 | 28 | /// The clock value of when this object was added to the Parse remote. 29 | var logicalClock: Int? { get set } 30 | 31 | /// The clock of when this object was added to the Parse remote. 32 | var clock: PCKClock? { get set } 33 | 34 | /// The semantic version of the database schema when this object was created. 35 | /// The value will be nil for objects that have not yet been persisted. 36 | var schemaVersion: OCKSemanticVersion? { get set } 37 | 38 | /// The date at which the object was first persisted to the database. 39 | /// It will be nil for unpersisted values and objects. 40 | var createdDate: Date? { get set } 41 | 42 | /// The last date at which the object was updated. 43 | /// It will be nil for unpersisted values and objects. 44 | var updatedDate: Date? { get set } 45 | 46 | /// The timezone this record was created in. 47 | var timezone: TimeZone? { get set } 48 | 49 | /// A dictionary of information that can be provided by developers to support their own unique 50 | /// use cases. 51 | var userInfo: [String: String]? { get set } 52 | 53 | /// A user-defined group identifier that can be used both for querying and sorting results. 54 | /// Examples may include: "medications", "exercises", "family", "males", "diabetics", etc. 55 | var groupIdentifier: String? { get set } 56 | 57 | /// An array of user-defined tags that can be used to sort or classify objects or values. 58 | var tags: [String]? { get set } 59 | 60 | /// Specifies where this object originated from. It could contain information about the device 61 | /// used to record the data, its software version, or the person who recorded the data. 62 | var source: String? { get set } 63 | 64 | /// Specifies the location of some asset associated with this object. It could be the URL for 65 | /// an image or video, the bundle name of a audio asset, or any other representation the 66 | /// developer chooses. 67 | var asset: String? { get set } 68 | 69 | /// Any array of notes associated with this object. 70 | var notes: [OCKNote]? { get set } 71 | 72 | /// A unique id optionally used by a remote database. Its precise format will be 73 | /// determined by the remote database, but it is generally not expected to be human readable. 74 | var remoteID: String? { get set } 75 | 76 | /// A boolean that is `true` when encoding the object for Parse. If `false` the object is encoding for CareKit. 77 | var encodingForParse: Bool { get set } 78 | 79 | /// Copy the values of a ParseCareKit object. 80 | static func copyValues(from other: Self, to here: Self) throws -> Self 81 | 82 | /// Initialize with UUID. 83 | init?(uuid: UUID?) 84 | 85 | /** 86 | Creates a new ParseCareKit object from a specified CareKit entity. 87 | 88 | - parameter from: The CareKit entity used to create the new ParseCareKit object. 89 | - returns: Returns a new version of `Self` 90 | - throws: `Error`. 91 | */ 92 | static func new(from careKitEntity: OCKEntity) throws -> Self 93 | } 94 | 95 | // MARK: Defaults 96 | extension PCKObjectable { 97 | 98 | public init?(uuid: UUID?) { 99 | guard let uuid = uuid else { 100 | return nil 101 | } 102 | self.init(objectId: uuid.uuidString) 103 | } 104 | 105 | public var uuid: UUID? { 106 | guard let objectId = objectId, 107 | let uuid = UUID(uuidString: objectId) else { 108 | return nil 109 | } 110 | return uuid 111 | } 112 | 113 | public var id: String { 114 | guard let returnId = entityId else { 115 | return "" 116 | } 117 | return returnId 118 | } 119 | } 120 | 121 | extension PCKObjectable { 122 | 123 | func copyRelationalEntities(_ parse: Self) -> Self { 124 | var current = self 125 | current.notes = parse.notes 126 | return current 127 | } 128 | 129 | /// Copies the common values of another PCKObjectable object. 130 | /// - parameter from: The PCKObjectable object to copy from. 131 | mutating public func copyCommonValues(from other: Self) { 132 | entityId = other.entityId 133 | updatedDate = other.updatedDate 134 | timezone = other.timezone 135 | userInfo = other.userInfo 136 | remoteID = other.remoteID 137 | createdDate = other.createdDate 138 | notes = other.notes 139 | logicalClock = other.logicalClock 140 | clock = other.clock 141 | source = other.source 142 | asset = other.asset 143 | schemaVersion = other.schemaVersion 144 | groupIdentifier = other.groupIdentifier 145 | tags = other.tags 146 | } 147 | 148 | // Stamps all related entities with the current `logicalClock` value 149 | /*mutating public func stampRelationalEntities() throws -> Self { 150 | guard let logicalClock = self.logicalClock else { 151 | throw ParseCareKitError.couldntUnwrapSelf 152 | } 153 | var updatedNotes = [OCKNote]() 154 | notes?.forEach { 155 | var update = $0 156 | update.stamp(logicalClock) 157 | updatedNotes.append(update) 158 | } 159 | self.notes = updatedNotes 160 | return self 161 | }*/ 162 | 163 | /// Determines if this PCKObjectable object can be converted to CareKit 164 | public func canConvertToCareKit() -> Bool { 165 | guard self.entityId != nil else { 166 | return false 167 | } 168 | return true 169 | } 170 | 171 | /** 172 | Finds the first object on the remote that has the same `uuid`. 173 | - Parameters: 174 | - uuid: The UUID to search for. 175 | - options: A set of header options sent to the remote. Defaults to an empty set. 176 | - relatedObject: An object that has the same `uuid` as the one being searched for. 177 | - completion: The block to execute. 178 | It should have the following argument signature: `(Result)`. 179 | */ 180 | static public func first(_ uuid: UUID?, 181 | options: API.Options = [], 182 | completion: @escaping(Result) -> Void) { 183 | 184 | guard let uuidString = uuid?.uuidString else { 185 | completion(.failure(ParseCareKitError.requiredValueCantBeUnwrapped)) 186 | return 187 | } 188 | 189 | let query = Self.query(ParseKey.objectId == uuidString) 190 | .includeAll() 191 | query.first(options: options) { result in 192 | 193 | switch result { 194 | 195 | case .success(let object): 196 | completion(.success(object)) 197 | case .failure(let error): 198 | completion(.failure(error)) 199 | } 200 | 201 | } 202 | } 203 | 204 | /** 205 | Create a `DateInterval` like how CareKit generates one. 206 | - Parameters: 207 | - for: the date to start the interval. 208 | 209 | - returns: a interval from `for` to the next day. 210 | */ 211 | public static func createCurrentDateInterval(for date: Date) -> DateInterval { 212 | let startOfDay = Calendar.current.startOfDay(for: date) 213 | let endOfDay = Calendar.current.date(byAdding: DateComponents(day: 1, second: -1), to: startOfDay)! 214 | return DateInterval(start: startOfDay, end: endOfDay) 215 | } 216 | } 217 | 218 | // MARK: Encodable 219 | extension PCKObjectable { 220 | 221 | /** 222 | Encodes the PCKObjectable properties of the object 223 | - Parameters: 224 | - to: the encoder the properties should be encoded to. 225 | */ 226 | public func encodeObjectable(to encoder: Encoder) throws { 227 | var container = encoder.container(keyedBy: PCKCodingKeys.self) 228 | 229 | if encodingForParse { 230 | try container.encodeIfPresent(entityId, forKey: .entityId) 231 | try container.encodeIfPresent(objectId, forKey: .objectId) 232 | try container.encodeIfPresent(ACL, forKey: .ACL) 233 | try container.encodeIfPresent(logicalClock, forKey: .logicalClock) 234 | try container.encodeIfPresent(clock, forKey: .clock) 235 | } else { 236 | try container.encodeIfPresent(entityId, forKey: .id) 237 | try container.encodeIfPresent(uuid, forKey: .uuid) 238 | } 239 | try container.encodeIfPresent(schemaVersion, forKey: .schemaVersion) 240 | try container.encodeIfPresent(createdDate, forKey: .createdDate) 241 | try container.encodeIfPresent(updatedDate, forKey: .updatedDate) 242 | try container.encodeIfPresent(timezone, forKey: .timezone) 243 | try container.encodeIfPresent(userInfo, forKey: .userInfo) 244 | try container.encodeIfPresent(groupIdentifier, forKey: .groupIdentifier) 245 | try container.encodeIfPresent(tags, forKey: .tags) 246 | try container.encodeIfPresent(source, forKey: .source) 247 | try container.encodeIfPresent(asset, forKey: .asset) 248 | try container.encodeIfPresent(remoteID, forKey: .remoteID) 249 | try container.encodeIfPresent(notes, forKey: .notes) 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Protocols/PCKRoleable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PCKRoleable.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 1/30/22. 6 | // Copyright © 2022 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ParseSwift 11 | 12 | /** 13 | Objects that conform to the `PCKRoleable` protocol are `ParseRole`'s . 14 | */ 15 | public protocol PCKRoleable: ParseRole { 16 | 17 | /// The default string to be appended to the `name`. 18 | /// It is expected for each `ParseRole` to implement it's own `appendString`. 19 | static var appendString: String { get } 20 | 21 | /// The owner of this `ParseRole`. 22 | var owner: RoleUser? { get set } 23 | } 24 | 25 | public extension PCKRoleable { 26 | 27 | static var appendString: String { 28 | "_user" 29 | } 30 | 31 | /** 32 | Creates a name for the role by using appending `appendString` to the `objectId`. 33 | - parameter owner: The owner of the `ParseRole`. 34 | - returns: The concatenated `objectId` and `appendString`. 35 | - throws: An `Error` if the `owner` is missing the `objectId`. 36 | */ 37 | static func roleName(owner: RoleUser?) throws -> String { 38 | guard var ownerObjectId = owner?.objectId else { 39 | throw ParseCareKitError.errorString("Owner doesn't have an objectId") 40 | } 41 | ownerObjectId.append(Self.appendString) 42 | return ownerObjectId 43 | } 44 | 45 | /** 46 | Creates a new private `ParseRole` with the owner having read/write permission. 47 | - parameter with: The owner of the `ParseRole`. 48 | - returns: The new `ParseRole`. 49 | - throws: An `Error` if the `ParseRole` cannot be created. 50 | */ 51 | static func create(with owner: RoleUser) throws -> Self { 52 | var ownerACL = ParseACL() 53 | ownerACL.publicRead = false 54 | ownerACL.publicWrite = false 55 | ownerACL.setWriteAccess(user: owner, value: true) 56 | ownerACL.setReadAccess(user: owner, value: true) 57 | let roleName = try Self.roleName(owner: owner) 58 | var newRole = try Self(name: roleName, acl: ownerACL) 59 | newRole.owner = owner 60 | return newRole 61 | } 62 | 63 | func merge(with object: Self) throws -> Self { 64 | var updated = try mergeParse(with: object) 65 | if updated.shouldRestoreKey(\.owner, 66 | original: object) { 67 | updated.owner = object.owner 68 | } 69 | return updated 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Protocols/PCKVersionable+async.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PCKVersionable+async.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 10/6/21. 6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ParseSwift 11 | 12 | public extension PCKVersionable { 13 | 14 | /** 15 | Find versioned objects *asynchronously* like `fetch` in CareKit. Finds the newest version 16 | that has not been deleted. 17 | - Parameters: 18 | - for: The date the objects are active. 19 | - options: A set of header options sent to the remote. Defaults to an empty set. 20 | - callbackQueue: The queue to return to after completion. Default value of `.main`. 21 | - returns: An array of objects matching the query. 22 | - throws: `ParseError`. 23 | */ 24 | func find( 25 | for date: Date, 26 | options: API.Options = [] 27 | ) async throws -> [Self] { 28 | try await withCheckedThrowingContinuation { continuation in 29 | self.find( 30 | for: date, 31 | options: options, 32 | completion: { continuation.resume(with: $0) } 33 | ) 34 | } 35 | } 36 | 37 | /** 38 | Saves a `PCKVersionable` object. 39 | - Parameters: 40 | - uuid: The UUID to search for. 41 | - options: A set of header options sent to the remote. Defaults to an empty set. 42 | - relatedObject: An object that has the same `uuid` as the one being searched for. 43 | - returns: The saved version. 44 | - throws: `ParseError`. 45 | */ 46 | func save(options: API.Options = []) async throws -> Self { 47 | try await withCheckedThrowingContinuation { continuation in 48 | self.save( 49 | options: options, 50 | completion: { continuation.resume(with: $0) } 51 | ) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Protocols/PCKVersionable+combine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PCKVersionable+combine.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 10/6/21. 6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Foundation 11 | import ParseSwift 12 | import Combine 13 | 14 | @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) 15 | public extension PCKVersionable { 16 | 17 | /** 18 | Find versioned objects *asynchronously* like `fetch` in CareKit. Finds the newest version 19 | that has not been deleted. Publishes when complete. 20 | - Parameters: 21 | - for: The date the objects are active. 22 | - options: A set of header options sent to the remote. Defaults to an empty set. 23 | - returns: `Future<[Self],ParseError>`. 24 | */ 25 | func findPublisher( 26 | for date: Date, 27 | options: API.Options = [] 28 | ) -> Future<[Self], ParseError> { 29 | Future { promise in 30 | self.find( 31 | for: date, 32 | options: options, 33 | completion: promise 34 | ) 35 | } 36 | } 37 | 38 | /** 39 | Saves a `PCKVersionable` object. *asynchronously*. Publishes when complete. 40 | - Parameters: 41 | - options: A set of header options sent to the remote. Defaults to an empty set. 42 | - returns: `Future<[Self],ParseError>`. 43 | */ 44 | func savePublisher(for date: Date, 45 | options: API.Options = []) -> Future { 46 | Future { promise in 47 | self.save(options: options, 48 | completion: promise) 49 | } 50 | } 51 | } 52 | 53 | #endif 54 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Protocols/PCKVersionable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PCKVersionable.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 9/28/20. 6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ParseSwift 11 | import os.log 12 | 13 | // swiftlint:disable line_length 14 | // swiftlint:disable cyclomatic_complexity 15 | // swiftlint:disable function_body_length 16 | 17 | /** 18 | Objects that conform to the `PCKVersionable` protocol are Parse interpretations of `OCKVersionedObjectCompatible` objects. 19 | */ 20 | public protocol PCKVersionable: PCKObjectable { 21 | /// The UUIDs of the previous version of this object, or nil if there is no previous version. 22 | /// The UUIDs are in no particular order. 23 | var previousVersionUUIDs: [UUID]? { get set } 24 | 25 | /// The UUIDs of the next version of this object, or nil if there is no next version. 26 | /// The UUIDs are in no particular order. 27 | var nextVersionUUIDs: [UUID]? { get set } 28 | 29 | /// Parse Pointers to previous versions of this object, or nil if there is no previous version. 30 | /// The versions are in no particular order. 31 | var previousVersions: [Pointer]? { get set } 32 | 33 | /// Parse Pointers to next versions of this object, or nil if there is no next version. 34 | /// The versions are in no particular order. 35 | var nextVersions: [Pointer]? { get set } 36 | 37 | /// The date that this version of the object begins to take precedence over the previous version. 38 | /// Often this will be the same as the `createdDate`, but is not required to be. 39 | var effectiveDate: Date? { get set } 40 | 41 | /// The date on which this object was marked deleted. Note that objects are never actually deleted, 42 | /// but rather they are marked deleted and will no longer be returned from queries. 43 | var deletedDate: Date? { get set } 44 | } 45 | 46 | extension PCKVersionable { 47 | 48 | /// Copies the common values of another PCKVersionable object. 49 | /// - parameter from: The PCKVersionable object to copy from. 50 | mutating public func copyVersionedValues(from other: Self) { 51 | self.effectiveDate = other.effectiveDate 52 | self.deletedDate = other.deletedDate 53 | self.copyCommonValues(from: other) 54 | } 55 | } 56 | 57 | // MARK: Fetching 58 | extension PCKVersionable { 59 | private static func queryNotDeleted() -> Query { 60 | Self.query(doesNotExist(key: VersionableKey.deletedDate)) 61 | } 62 | 63 | private static func queryNewestVersion(for date: Date) -> Query { 64 | let interval = createCurrentDateInterval(for: date) 65 | 66 | let startsBeforeEndOfQuery = Self.query(VersionableKey.effectiveDate < interval.end) 67 | let noNextVersion = queryNoNextVersion(for: date) 68 | return .init(and(queries: [startsBeforeEndOfQuery, noNextVersion])) 69 | } 70 | 71 | private static func queryNoNextVersion(for date: Date) -> Query { 72 | // Where empty array 73 | let query = Self.query(VersionableKey.nextVersionUUIDs == [String]()) 74 | 75 | let interval = createCurrentDateInterval(for: date) 76 | let greaterEqualEffectiveDate = self.query(VersionableKey.effectiveDate >= interval.end) 77 | return Self.query(or(queries: [query, greaterEqualEffectiveDate])) 78 | } 79 | 80 | /** 81 | Querying versioned objects just like CareKit. Creates a query that finds 82 | the newest version that has not been deleted. This is the query used by `find(for date: Date)`. 83 | Use this query to build from if you desire a more intricate query. 84 | - Parameters: 85 | - for: The date the object is active. 86 | - returns: `Query`. 87 | */ 88 | public static func query(for date: Date) -> Query { 89 | .init(and(queries: [queryNotDeleted(), 90 | queryNewestVersion(for: date)])) 91 | } 92 | 93 | /** 94 | Find versioned objects *asynchronously* like `fetch` in CareKit. Finds the newest version 95 | that has not been deleted. 96 | - Parameters: 97 | - for: The date the objects are active. 98 | - options: A set of header options sent to the remote. Defaults to an empty set. 99 | - callbackQueue: The queue to return to after completion. Default value of `.main`. 100 | - completion: The block to execute. 101 | It should have the following argument signature: `(Result<[Self],ParseError>)`. 102 | */ 103 | public func find( 104 | for date: Date, 105 | options: API.Options = [], 106 | callbackQueue: DispatchQueue = .main, 107 | completion: @escaping(Result<[Self], ParseError>) -> Void 108 | ) { 109 | let query = Self.query(for: date) 110 | .limit(queryLimit) 111 | .includeAll() 112 | query.find( 113 | options: options, 114 | callbackQueue: callbackQueue, 115 | completion: completion 116 | ) 117 | } 118 | } 119 | 120 | // MARK: Encodable 121 | extension PCKVersionable { 122 | 123 | /** 124 | Encodes the PCKVersionable properties of the object 125 | - Parameters: 126 | - to: the encoder the properties should be encoded to. 127 | */ 128 | public func encodeVersionable(to encoder: Encoder) throws { 129 | var container = encoder.container(keyedBy: PCKCodingKeys.self) 130 | try container.encodeIfPresent(deletedDate, forKey: .deletedDate) 131 | try container.encodeIfPresent(effectiveDate, forKey: .effectiveDate) 132 | try container.encodeIfPresent(previousVersionUUIDs, forKey: .previousVersionUUIDs) 133 | try container.encodeIfPresent(nextVersionUUIDs, forKey: .nextVersionUUIDs) 134 | try container.encodeIfPresent(previousVersions, forKey: .previousVersions) 135 | try container.encodeIfPresent(nextVersions, forKey: .nextVersions) 136 | try encodeObjectable(to: encoder) 137 | } 138 | } // swiftlint:disable:this file_length 139 | -------------------------------------------------------------------------------- /Sources/ParseCareKit/Protocols/ParseRemoteDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParseRemoteDelegate.swift 3 | // ParseCareKit 4 | // 5 | // Created by Corey Baker on 12/13/20. 6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CareKitStore 11 | 12 | /** 13 | Objects that conform to the `ParseRemoteDelegate` protocol are 14 | able to respond to updates and resolve conflicts when needed. 15 | */ 16 | public protocol ParseRemoteDelegate: OCKRemoteSynchronizationDelegate { 17 | /// When a conflict occurs, decide if the local or remote record should be kept. 18 | func chooseConflictResolution(conflicts: [OCKEntity], completion: @escaping OCKResultClosure) 19 | 20 | /// Receive a notification when data has been successfully pushed to the remote. 21 | func successfullyPushedToRemote() 22 | 23 | /// Sometimes the remote will need the local data store to fetch additional information 24 | /// required for proper synchronization. 25 | /// - note: The remote will never use this method to modify the store. 26 | func provideStore() -> OCKAnyStoreProtocol 27 | } 28 | 29 | extension ParseRemoteDelegate { 30 | func successfullyPushedToRemote() {} 31 | } 32 | -------------------------------------------------------------------------------- /TestHostAll/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /TestHostAll/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "1x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "2x", 16 | "size" : "16x16" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "1x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "2x", 26 | "size" : "32x32" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "2x", 36 | "size" : "128x128" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "1x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "2x", 46 | "size" : "256x256" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "1x", 51 | "size" : "512x512" 52 | }, 53 | { 54 | "idiom" : "mac", 55 | "scale" : "2x", 56 | "size" : "512x512" 57 | } 58 | ], 59 | "info" : { 60 | "author" : "xcode", 61 | "version" : 1 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /TestHostAll/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TestHostAll/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // TestHostAll 4 | // 5 | // Created by Corey Baker on 5/8/23. 6 | // Copyright © 2023 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ContentView: View { 12 | var body: some View { 13 | VStack { 14 | Image(systemName: "globe") 15 | .imageScale(.large) 16 | .foregroundColor(.accentColor) 17 | Text("Hello, world!") 18 | } 19 | .padding() 20 | } 21 | } 22 | 23 | struct ContentView_Previews: PreviewProvider { 24 | static var previews: some View { 25 | ContentView() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /TestHostAll/ParseCareKit.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ApplicationID 6 | 3B5FD9DA-C278-4582-90DC-101C08E7FC98 7 | ClientKey 8 | hello 9 | Server 10 | http://localhost:1337/parse 11 | LiveQueryServer 12 | ws://localhost:1337/parse 13 | UseTransactions 14 | 15 | DeleteKeychainIfNeeded 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /TestHostAll/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TestHostAll/TestHostAll.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /TestHostAll/TestHostAllApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestHostAllApp.swift 3 | // TestHostAll 4 | // 5 | // Created by Corey Baker on 5/8/23. 6 | // Copyright © 2023 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import ParseCareKit 10 | import SwiftUI 11 | 12 | @main 13 | struct TestHostAllApp: App { 14 | var body: some Scene { 15 | WindowGroup { 16 | ContentView() 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/ParseCareKitTests/LoggerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoggerTests.swift 3 | // ParseCareKitTests 4 | // 5 | // Created by Corey Baker on 12/13/20. 6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | @testable import ParseCareKit 12 | import os.log 13 | 14 | class LoggerTests: XCTestCase { 15 | 16 | override func setUpWithError() throws {} 17 | 18 | override func tearDownWithError() throws {} 19 | 20 | func testCarePlan() throws { 21 | Logger.carePlan.error("Testing") 22 | } 23 | 24 | func testContact() throws { 25 | Logger.contact.error("Testing") 26 | } 27 | 28 | func testPatient() throws { 29 | Logger.patient.error("Testing") 30 | } 31 | 32 | func testTask() throws { 33 | Logger.task.error("Testing") 34 | } 35 | 36 | func testOutcome() throws { 37 | Logger.outcome.error("Testing") 38 | } 39 | 40 | func testVersionable() throws { 41 | Logger.versionable.error("Testing") 42 | } 43 | 44 | func testObjectable() throws { 45 | Logger.objectable.error("Testing") 46 | } 47 | 48 | func testPullRevisions() throws { 49 | Logger.pullRevisions.error("Testing") 50 | } 51 | 52 | func testPushRevisions() throws { 53 | Logger.pushRevisions.error("Testing") 54 | } 55 | 56 | func testClock() throws { 57 | Logger.clock.error("Testing") 58 | } 59 | 60 | func testInitializer() throws { 61 | Logger.initializer.error("Testing") 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Tests/ParseCareKitTests/NetworkMocking/MockURLProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockURLProtocol.swift 3 | // ParseSwiftTests 4 | // 5 | // Created by Corey E. Baker on 7/19/20. 6 | // Copyright © 2020 Parse Community. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | typealias MockURLProtocolRequestTestClosure = (URLRequest) -> Bool 12 | typealias MockURLResponseContructingClosure = (URLRequest) -> MockURLResponse? 13 | 14 | struct MockURLProtocolMock { 15 | var attempts: Int 16 | var test: MockURLProtocolRequestTestClosure 17 | var response: MockURLResponseContructingClosure 18 | } 19 | 20 | class MockURLProtocol: URLProtocol { 21 | var mock: MockURLProtocolMock? 22 | static var mocks: [MockURLProtocolMock] = [] 23 | private var loading: Bool = false 24 | var isLoading: Bool { 25 | return loading 26 | } 27 | 28 | class func mockRequests(response: @escaping MockURLResponseContructingClosure) { 29 | mockRequestsPassing(NSIntegerMax, test: { _ in return true }, with: response) 30 | } 31 | 32 | class func mockRequestsPassing(_ test: @escaping MockURLProtocolRequestTestClosure, 33 | with response: @escaping MockURLResponseContructingClosure) { 34 | mockRequestsPassing(NSIntegerMax, test: test, with: response) 35 | } 36 | 37 | class func mockRequestsPassing(_ attempts: Int, test: @escaping MockURLProtocolRequestTestClosure, 38 | with response: @escaping MockURLResponseContructingClosure) { 39 | let mock = MockURLProtocolMock(attempts: attempts, test: test, response: response) 40 | mocks.append(mock) 41 | if mocks.count == 1 { 42 | URLProtocol.registerClass(MockURLProtocol.self) 43 | } 44 | } 45 | 46 | class func removeAll() { 47 | if !mocks.isEmpty { 48 | URLProtocol.unregisterClass(MockURLProtocol.self) 49 | } 50 | mocks.removeAll() 51 | } 52 | 53 | class func firstMockForRequest(_ request: URLRequest) -> MockURLProtocolMock? { 54 | for mock in mocks { 55 | if (mock.attempts > 0) && mock.test(request) { 56 | return mock 57 | } 58 | } 59 | return nil 60 | } 61 | 62 | override class func canInit(with request: URLRequest) -> Bool { 63 | return MockURLProtocol.firstMockForRequest(request) != nil 64 | } 65 | 66 | override class func canInit(with task: URLSessionTask) -> Bool { 67 | guard let originalRequest = task.originalRequest else { 68 | return false 69 | } 70 | return MockURLProtocol.firstMockForRequest(originalRequest) != nil 71 | } 72 | 73 | override class func canonicalRequest(for request: URLRequest) -> URLRequest { 74 | return request 75 | } 76 | 77 | override init(request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) { 78 | self.mock = nil 79 | super.init(request: request, cachedResponse: cachedResponse, client: client) 80 | guard let mock = MockURLProtocol.firstMockForRequest(request) else { 81 | self.mock = nil 82 | return 83 | } 84 | self.mock = mock 85 | } 86 | 87 | override func startLoading() { 88 | self.loading = true 89 | self.mock?.attempts -= 1 90 | guard let response = self.mock?.response(request) else { 91 | return 92 | } 93 | 94 | if let error = response.error { 95 | DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + response.delay * Double(NSEC_PER_SEC)) { 96 | 97 | if self.loading { 98 | self.client?.urlProtocol(self, didFailWithError: error) 99 | } 100 | 101 | } 102 | return 103 | } 104 | 105 | guard let url = request.url, 106 | let urlResponse = HTTPURLResponse(url: url, statusCode: response.statusCode, 107 | httpVersion: "HTTP/2", headerFields: response.headerFields) else { 108 | return 109 | } 110 | 111 | DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + response.delay * Double(NSEC_PER_SEC)) { 112 | 113 | if !self.loading { 114 | return 115 | } 116 | 117 | self.client?.urlProtocol(self, didReceive: urlResponse, cacheStoragePolicy: .notAllowed) 118 | if let data = response.responseData { 119 | self.client?.urlProtocol(self, didLoad: data) 120 | } 121 | self.client?.urlProtocolDidFinishLoading(self) 122 | } 123 | 124 | } 125 | 126 | override func stopLoading() { 127 | self.loading = false 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Tests/ParseCareKitTests/NetworkMocking/MockURLResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockURLResponse.swift 3 | // ParseSwiftTests 4 | // 5 | // Created by Corey Baker on 7/18/20. 6 | // Copyright © 2020 Parse Community. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import ParseSwift 11 | 12 | struct MockURLResponse { 13 | var statusCode: Int = 200 14 | var headerFields = [String: String]() 15 | var responseData: Data? 16 | var delay: TimeInterval! 17 | var error: Error? 18 | 19 | init(error: Error) { 20 | self.delay = .init(0.0) 21 | self.error = error 22 | self.responseData = nil 23 | self.statusCode = 400 24 | } 25 | 26 | init(string: String) throws { 27 | try self.init(string: string, statusCode: 200, delay: .init(0.0)) 28 | } 29 | 30 | init(string: String, statusCode: Int, delay: TimeInterval, 31 | headerFields: [String: String] = ["Content-Type": "application/json"]) throws { 32 | 33 | do { 34 | let encoded = try JSONEncoder().encode(string) 35 | self.init(data: encoded, statusCode: statusCode, delay: delay, headerFields: headerFields) 36 | } catch { 37 | throw ParseError(code: .otherCause, message: "unable to convert string to data") 38 | } 39 | } 40 | 41 | init(data: Data, statusCode: Int, delay: TimeInterval, 42 | headerFields: [String: String] = ["Content-Type": "application/json"]) { 43 | self.statusCode = statusCode 44 | self.headerFields = headerFields 45 | self.responseData = data 46 | self.delay = delay 47 | self.error = nil 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/ParseCareKitTests/UtilityTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UtilityTests.swift 3 | // ParseCareKitTests 4 | // 5 | // Created by Corey Baker on 11/21/21. 6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ParseCareKit 11 | @testable import ParseSwift 12 | 13 | class UtilityTests: XCTestCase { 14 | 15 | override func setUp() async throws {} 16 | 17 | override func tearDown() async throws { 18 | MockURLProtocol.removeAll() 19 | try await KeychainStore.shared.deleteAll() 20 | try await ParseStorage.shared.deleteAll() 21 | PCKUtility.removeCache() 22 | } 23 | 24 | func testConfigureParse() async throws { 25 | try await PCKUtility.configureParse { (_, completionHandler) in 26 | completionHandler(.performDefaultHandling, nil) 27 | } 28 | XCTAssertEqual(ParseSwift.configuration.applicationId, "3B5FD9DA-C278-4582-90DC-101C08E7FC98") 29 | XCTAssertEqual(ParseSwift.configuration.clientKey, "hello") 30 | XCTAssertEqual(ParseSwift.configuration.serverURL, URL(string: "http://localhost:1337/parse")) 31 | XCTAssertEqual(ParseSwift.configuration.liveQuerysServerURL, URL(string: "ws://localhost:1337/parse")) 32 | XCTAssertTrue(ParseSwift.configuration.isUsingTransactions) 33 | XCTAssertTrue(ParseSwift.configuration.isDeletingKeychainIfNeeded) 34 | } 35 | } 36 | --------------------------------------------------------------------------------