From 6789f019c860de3d968950453210468634928d78 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 1 Dec 2023 03:14:45 +0100 Subject: [PATCH 01/27] Use baasaas on CI --- .github/workflows/ci.yml | 430 +++++------------- .github/workflows/cleanup-clusters.yml | 17 - .github/workflows/create-cluster.yml | 49 -- .github/workflows/dart-desktop-tests.yml | 37 +- .github/workflows/deploy-baas.yml | 42 ++ .github/workflows/flutter-desktop-tests.yml | 37 +- .github/workflows/shared-apps.yml | 58 --- lib/src/cli/atlas_apps/baas_client.dart | 120 +++-- .../cli/atlas_apps/deleteapps_command.dart | 14 +- .../cli/atlas_apps/deployapps_command.dart | 37 +- lib/src/cli/atlas_apps/options.dart | 9 +- lib/src/cli/atlas_apps/options.g.dart | 8 +- test/app_test.dart | 1 + test/baas_helper.dart | 300 ++++++++++++ test/baas_helper.g.dart | 171 +++++++ test/client_reset_test.dart | 82 +--- test/configuration_test.dart | 6 +- test/indexed_test.dart | 22 +- test/list_test.dart | 1 + test/test.dart | 193 +------- test/user_test.dart | 4 +- 21 files changed, 827 insertions(+), 811 deletions(-) delete mode 100644 .github/workflows/cleanup-clusters.yml delete mode 100644 .github/workflows/create-cluster.yml create mode 100644 .github/workflows/deploy-baas.yml delete mode 100644 .github/workflows/shared-apps.yml create mode 100644 test/baas_helper.dart create mode 100644 test/baas_helper.g.dart diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b31de514b..43cd76f9c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,224 +17,6 @@ concurrency: cancel-in-progress: true jobs: - deploy-dart-cluster: - name: Deploy Dart cluster - secrets: inherit - uses: ./.github/workflows/create-cluster.yml - with: - prefix: d - - deploy-flutter-cluster: - name: Deploy Flutter cluster - secrets: inherit - uses: ./.github/workflows/create-cluster.yml - with: - prefix: f - - create-dart-shared-apps: - name: Create Dart shared Apps - secrets: inherit - needs: - - deploy-dart-cluster - uses: ./.github/workflows/shared-apps.yml - with: - cluster: ${{ needs.deploy-dart-cluster.outputs.clusterName }} - env: Dart - - create-flutter-shared-apps: - name: Create Flutter shared Apps - secrets: inherit - needs: - - deploy-flutter-cluster - uses: ./.github/workflows/shared-apps.yml - with: - cluster: ${{ needs.deploy-flutter-cluster.outputs.clusterName }} - env: Flutter - - delete-dart-cluster: - runs-on: ubuntu-latest - name: Delete Dart Cluster - timeout-minutes: 5 - continue-on-error: true - needs: - - deploy-dart-cluster - - dart-tests-windows - - dart-tests-macos - - dart-tests-macos-arm - - dart-tests-linux - - dart-tests-linux-ubuntu-20 - - steps: - - uses: realm/ci-actions/mdb-realm/cleanup@338bf3e7575015a28faec8b67614385d122aece7 - with: - realmUrl: ${{ env.BAAS_URL }} - atlasUrl: ${{ secrets.ATLAS_QA_URL }} - projectId: ${{ env.BAAS_PROJECT_ID }} - apiKey: ${{ env.BAAS_API_KEY }} - privateApiKey: ${{ env.BAAS_PRIVATE_API_KEY }} - clusterName: ${{ needs.deploy-dart-cluster.outputs.clusterName }} - - delete-flutter-cluster: - runs-on: ubuntu-latest - name: Delete Flutter Cluster - timeout-minutes: 5 - continue-on-error: true - needs: - - deploy-flutter-cluster - - flutter-desktop-tests-windows - - flutter-desktop-tests-macos - - flutter-desktop-tests-linux - - flutter-desktop-tests-linux-ubuntu-20 - - flutter-ios - - flutter-android - steps: - - uses: realm/ci-actions/mdb-realm/cleanup@338bf3e7575015a28faec8b67614385d122aece7 - with: - realmUrl: ${{ env.BAAS_URL }} - atlasUrl: ${{ secrets.ATLAS_QA_URL }} - projectId: ${{ env.BAAS_PROJECT_ID }} - apiKey: ${{ env.BAAS_API_KEY }} - privateApiKey: ${{ env.BAAS_PRIVATE_API_KEY }} - clusterName: ${{ needs.deploy-flutter-cluster.outputs.clusterName }} - - cleanup-dart-shared-apps: - name: Delete Dart shared Apps - secrets: inherit - needs: - - deploy-dart-cluster - - dart-tests-windows - - dart-tests-macos - - dart-tests-macos-arm - - dart-tests-linux - - dart-tests-linux-ubuntu-20 - uses: ./.github/workflows/shared-apps.yml - if: always() - with: - cleanup: true - cluster: ${{ needs.deploy-dart-cluster.outputs.clusterName }} - env: Dart - - cleanup-flutter-shared-apps: - name: Delete Flutter shared Apps - secrets: inherit - needs: - - deploy-flutter-cluster - - flutter-desktop-tests-windows - - flutter-desktop-tests-macos - - flutter-desktop-tests-linux - - flutter-desktop-tests-linux-ubuntu-20 - - flutter-ios - - flutter-android - uses: ./.github/workflows/shared-apps.yml - if: always() - with: - cleanup: true - cluster: ${{ needs.deploy-flutter-cluster.outputs.clusterName }} - env: Flutter - - cleanup-dart-matrix: - needs: - - deploy-dart-cluster - - dart-tests-windows - - dart-tests-macos - - dart-tests-macos-arm - - dart-tests-linux - - dart-tests-linux-ubuntu-20 - strategy: - fail-fast: false - matrix: - include: - - app: dm - description: dart macos - - app: dma - description: dart macos-arm - - app: dl - description: dart linux - - app: dl2 - description: dart linux (ubuntu 20) - - app: dw - description: dart windows - runs-on: ubuntu-latest - name: Cleanup apps for ${{ matrix.description }} - timeout-minutes: 20 - if: always() - env: - BAAS_CLUSTER: ${{ needs.deploy-dart-cluster.outputs.clusterName }} - BAAS_DIFFERENTIATOR: ${{ matrix.app }}${{ github.run_id }}${{ github.run_attempt }} - steps: - - name: Checkout - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 - with: - submodules: false - - - name : Setup Dart SDK - uses: dart-lang/setup-dart@main - with: - sdk: stable - - - name: Cleanup Dart apps - run: | - dart run realm_dart delete-apps \ - --baas-url ${{ env.BAAS_URL }} \ - --atlas-cluster ${{ env.BAAS_CLUSTER }} \ - --api-key ${{ env.BAAS_API_KEY }} \ - --private-api-key ${{ env.BAAS_PRIVATE_API_KEY }} \ - --project-id ${{ env.BAAS_PROJECT_ID }} \ - --differentiator '${{ env.BAAS_DIFFERENTIATOR }}' - - cleanup-flutter-matrix: - needs: - - deploy-flutter-cluster - - flutter-desktop-tests-windows - - flutter-desktop-tests-macos - - flutter-desktop-tests-linux - - flutter-desktop-tests-linux-ubuntu-20 - - flutter-ios - - flutter-android - strategy: - fail-fast: false - matrix: - include: - - app: fm - description: flutter macos - - app: fl - description: flutter linux - - app: fl2 - description: flutter linux (ubuntu 20) - - app: fw - description: flutter windows - - app: fa - description: flutter android - - app: fi - description: flutter iOS - runs-on: ubuntu-latest - name: Cleanup apps for ${{ matrix.description }} - timeout-minutes: 20 - if: always() - env: - BAAS_CLUSTER: ${{ needs.deploy-flutter-cluster.outputs.clusterName }} - BAAS_DIFFERENTIATOR: ${{ matrix.app }}${{ github.run_id }}${{ github.run_attempt }} - steps: - - name: Checkout - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 - with: - submodules: false - - - name : Setup Dart SDK - uses: dart-lang/setup-dart@main - with: - sdk: stable - - - name: Cleanup Flutter apps - run: | - dart run realm_dart delete-apps \ - --baas-url ${{ env.BAAS_URL }} \ - --atlas-cluster ${{ env.BAAS_CLUSTER }} \ - --api-key ${{ env.BAAS_API_KEY }} \ - --private-api-key ${{ env.BAAS_PRIVATE_API_KEY }} \ - --project-id ${{ env.BAAS_PROJECT_ID }} \ - --differentiator '${{ env.BAAS_DIFFERENTIATOR }}' - build-windows: name: Build Windows uses: ./.github/workflows/build-native.yml @@ -287,149 +69,136 @@ jobs: # Dart jobs + deploy-cluster-dart-windows: + name: Deploy Cluster for Dart Windows + uses: ./.github/workflows/deploy-baas.yml + dart-tests-windows: name: Windows Dart Tests uses: ./.github/workflows/dart-desktop-tests.yml needs: - build-windows - - deploy-dart-cluster - - create-dart-shared-apps + - deploy-cluster-dart-windows secrets: inherit with: - os: windows - runner: windows-latest - app: dw - cluster: ${{ needs.deploy-dart-cluster.outputs.clusterName }} + os: windows + runner: windows-latest + baasurl: ${{ needs.deploy-cluster-dart-windows.outputs.url }} + + deploy-cluster-dart-macos: + name: Deploy Cluster for Dart MacOS + uses: ./.github/workflows/deploy-baas.yml dart-tests-macos: name: MacOS Dart Tests uses: ./.github/workflows/dart-desktop-tests.yml needs: - build-macos - - deploy-dart-cluster - - create-dart-shared-apps + - deploy-cluster-dart-macos secrets: inherit with: - os: macos - runner: macos-latest - app: dm - cluster: ${{ needs.deploy-dart-cluster.outputs.clusterName }} + os: macos + runner: macos-latest + baasurl: ${{ needs.deploy-cluster-dart-macos.outputs.url }} + + deploy-cluster-dart-macos-arm: + name: Deploy Cluster for Dart MacOS Arm + uses: ./.github/workflows/deploy-baas.yml dart-tests-macos-arm: name: MacOS Arm Dart Tests uses: ./.github/workflows/dart-desktop-tests.yml needs: - build-macos - - deploy-dart-cluster - - create-dart-shared-apps + - deploy-cluster-dart-macos-arm secrets: inherit with: - os: macos - runner: macos-arm - architecture: arm - app: dma - cluster: ${{ needs.deploy-dart-cluster.outputs.clusterName }} + os: macos + runner: macos-arm + architecture: arm + baasurl: ${{ needs.deploy-cluster-dart-macos-arm.outputs.url }} + + deploy-cluster-dart-linux: + name: Deploy Cluster for Dart Linux + uses: ./.github/workflows/deploy-baas.yml dart-tests-linux: name: Linux Dart Tests uses: ./.github/workflows/dart-desktop-tests.yml needs: - build-linux - - deploy-dart-cluster - - create-dart-shared-apps + - deploy-cluster-dart-linux secrets: inherit with: - os: linux - runner: ubuntu-latest - app: dl - cluster: ${{ needs.deploy-dart-cluster.outputs.clusterName }} + os: linux + runner: ubuntu-latest + baasurl: ${{ needs.deploy-cluster-dart-linux.outputs.url }} - dart-tests-linux-ubuntu-20: - name: Linux Dart Tests (ubuntu 20) - uses: ./.github/workflows/dart-desktop-tests.yml - needs: - - build-linux - - deploy-dart-cluster - - create-dart-shared-apps - secrets: inherit - with: - os: linux - runner: ubuntu-20.04 - app: dl2 - cluster: ${{ needs.deploy-dart-cluster.outputs.clusterName }} # Flutter jobs + deploy-cluster-flutter-windows: + name: Deploy Cluster for Flutter Windows + uses: ./.github/workflows/deploy-baas.yml - flutter-desktop-tests-windows: + flutter-tests-windows: name: Windows Flutter Tests uses: ./.github/workflows/flutter-desktop-tests.yml needs: - build-windows - - deploy-flutter-cluster - - create-flutter-shared-apps + - deploy-cluster-flutter-windows secrets: inherit with: - os: windows - runner: windows-latest - app: fw - cluster: ${{ needs.deploy-flutter-cluster.outputs.clusterName }} + os: windows + runner: windows-latest + baasurl: ${{ needs.deploy-cluster-flutter-windows.outputs.url }} + + deploy-cluster-flutter-macos: + name: Deploy Cluster for Flutter MacOS + uses: ./.github/workflows/deploy-baas.yml - flutter-desktop-tests-macos: + flutter-tests-macos: name: MacOS Flutter Tests uses: ./.github/workflows/flutter-desktop-tests.yml needs: - build-macos - - deploy-flutter-cluster - - create-flutter-shared-apps + - deploy-cluster-flutter-macos secrets: inherit with: - os: macos - runner: macos-13 # workaround to: https://github.com/flutter/flutter/issues/118469 latest is still macos-12 ¯\_(ツ)_/¯ - app: fm - cluster: ${{ needs.deploy-flutter-cluster.outputs.clusterName }} + os: macos + runner: macos-13 # workaround to: https://github.com/flutter/flutter/issues/118469 latest is still macos-12 ¯\_(ツ)_/¯ + baasurl: ${{ needs.deploy-cluster-flutter-macos.outputs.url }} + + deploy-cluster-flutter-linux: + name: Deploy Cluster for Flutter Linux + uses: ./.github/workflows/deploy-baas.yml - flutter-desktop-tests-linux: + flutter-tests-linux: name: Linux Flutter Tests uses: ./.github/workflows/flutter-desktop-tests.yml needs: - build-linux - - deploy-flutter-cluster - - create-flutter-shared-apps + - deploy-cluster-flutter-linux secrets: inherit with: - os: linux - runner: ubuntu-latest - app: fl - cluster: ${{ needs.deploy-flutter-cluster.outputs.clusterName }} + os: linux + runner: ubuntu-latest + baasurl: ${{ needs.deploy-cluster-flutter-linux.outputs.url }} - flutter-desktop-tests-linux-ubuntu-20: - name: Linux Flutter Tests (ubuntu 20) - uses: ./.github/workflows/flutter-desktop-tests.yml - needs: - - build-linux - - deploy-flutter-cluster - - create-flutter-shared-apps - secrets: inherit - with: - os: linux - runner: ubuntu-20.04 - app: fl2 - cluster: ${{ needs.deploy-flutter-cluster.outputs.clusterName }} + deploy-cluster-flutter-ios: + name: Deploy Cluster for Flutter iOS + uses: ./.github/workflows/deploy-baas.yml - flutter-ios: + flutter-tests-ios: runs-on: macos-latest name: IOS Flutter Tests timeout-minutes: 45 needs: - - deploy-flutter-cluster + - deploy-cluster-flutter-ios - build-ios-xcframework - - create-flutter-shared-apps env: - BAAS_CLUSTER: ${{ needs.deploy-flutter-cluster.outputs.clusterName }} - BAAS_DIFFERENTIATOR: fi${{ github.run_id }}${{ github.run_attempt }} + BAAS_URL: ${{ needs.deploy-cluster-flutter-ios.outputs.url }} steps: - - name: Checkout uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 with: @@ -459,36 +228,38 @@ jobs: os: 'iOS' os_version: '>= 14.0' - # This will be a no-op under normal circumstances since the cluster would have been deployed - # in deploy-cluster. It is needed in case we want to re-run the job after the cluster has been reaped. - - name: Create cluster - uses: realm/ci-actions/mdb-realm/deploy@338bf3e7575015a28faec8b67614385d122aece7 - with: - realmUrl: ${{ env.BAAS_URL }} - atlasUrl: ${{ secrets.ATLAS_QA_URL }} - projectId: ${{ env.BAAS_PROJECT_ID }} - apiKey: ${{ env.BAAS_API_KEY }} - privateApiKey: ${{ env.BAAS_PRIVATE_API_KEY }} - clusterName: ${{ env.BAAS_CLUSTER }} + # # This will be a no-op under normal circumstances since the cluster would have been deployed + # # in deploy-cluster. It is needed in case we want to re-run the job after the cluster has been reaped. + # - name: Create cluster + # uses: realm/ci-actions/mdb-realm/deploy@338bf3e7575015a28faec8b67614385d122aece7 + # with: + # realmUrl: ${{ env.BAAS_URL }} + # atlasUrl: ${{ secrets.ATLAS_QA_URL }} + # projectId: ${{ env.BAAS_PROJECT_ID }} + # apiKey: ${{ env.BAAS_API_KEY }} + # privateApiKey: ${{ env.BAAS_PRIVATE_API_KEY }} + # clusterName: ${{ env.BAAS_CLUSTER }} - name: Run tests on iOS Simulator run: | flutter drive --target=test_driver/app.dart --dart-define=testName="" --suppress-analytics --debug working-directory: ./flutter/realm_flutter/tests - flutter-android: + deploy-cluster-flutter-android: + name: Deploy Cluster for Flutter Android + uses: ./.github/workflows/deploy-baas.yml + + flutter-tests-android: runs-on: macos-latest name: Android Flutter Tests timeout-minutes: 45 needs: - - deploy-flutter-cluster + - deploy-cluster-flutter-android - build-android-combined - - create-flutter-shared-apps env: - BAAS_CLUSTER: ${{ needs.deploy-flutter-cluster.outputs.clusterName }} - BAAS_DIFFERENTIATOR: fa${{ github.run_id }}${{ github.run_attempt }} - steps: + BAAS_URL: ${{ needs.deploy-cluster-flutter-android.outputs.url }} + steps: - name: Checkout uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 with: @@ -537,17 +308,17 @@ jobs: cmake: 3.10.2.4988404 script: echo "Generated Emulator snapshot for caching." - # This will be a no-op under normal circumstances since the cluster would have been deployed - # in deploy-cluster. It is needed in case we want to re-run the job after the cluster has been reaped. - - name: Create cluster - uses: realm/ci-actions/mdb-realm/deploy@338bf3e7575015a28faec8b67614385d122aece7 - with: - realmUrl: ${{ env.BAAS_URL }} - atlasUrl: ${{ secrets.ATLAS_QA_URL }} - projectId: ${{ env.BAAS_PROJECT_ID }} - apiKey: ${{ env.BAAS_API_KEY }} - privateApiKey: ${{ env.BAAS_PRIVATE_API_KEY }} - clusterName: ${{ env.BAAS_CLUSTER }} + # # This will be a no-op under normal circumstances since the cluster would have been deployed + # # in deploy-cluster. It is needed in case we want to re-run the job after the cluster has been reaped. + # - name: Create cluster + # uses: realm/ci-actions/mdb-realm/deploy@338bf3e7575015a28faec8b67614385d122aece7 + # with: + # realmUrl: ${{ env.BAAS_URL }} + # atlasUrl: ${{ secrets.ATLAS_QA_URL }} + # projectId: ${{ env.BAAS_PROJECT_ID }} + # apiKey: ${{ env.BAAS_API_KEY }} + # privateApiKey: ${{ env.BAAS_PRIVATE_API_KEY }} + # clusterName: ${{ env.BAAS_CLUSTER }} - name: Run tests on Android Emulator uses: reactivecircus/android-emulator-runner@v2 @@ -674,8 +445,15 @@ jobs: slack-on-failure: name: Report failure in main branch needs: - - cleanup-dart-matrix - - cleanup-flutter-matrix + - dart-tests-linux + - dart-tests-macos + - dart-tests-macos-arm + - dart-tests-windows + - flutter-tests-linux + - flutter-tests-macos + - flutter-tests-windows + - flutter-tests-ios + - flutter-tests-android runs-on: ubuntu-latest if: always() && github.ref == 'refs/heads/main' steps: diff --git a/.github/workflows/cleanup-clusters.yml b/.github/workflows/cleanup-clusters.yml deleted file mode 100644 index 8c0ceb244..000000000 --- a/.github/workflows/cleanup-clusters.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Wipe all clusters and apps - -on: - workflow_dispatch: -jobs: - main: - runs-on: ubuntu-latest - name: Wipe all clusters and apps - steps: - - uses: realm/ci-actions/mdb-realm/deleteAllClusters@338bf3e7575015a28faec8b67614385d122aece7 - with: - realmUrl: ${{ secrets.REALM_QA_URL }} - atlasUrl: ${{ secrets.ATLAS_QA_URL }} - projectId: ${{ secrets.ATLAS_QA_PROJECT_ID }} - apiKey: ${{ secrets.ATLAS_QA_PUBLIC_API_KEY }} - privateApiKey: ${{ secrets.ATLAS_QA_PRIVATE_API_KEY }} - diff --git a/.github/workflows/create-cluster.yml b/.github/workflows/create-cluster.yml deleted file mode 100644 index 8dc89c89d..000000000 --- a/.github/workflows/create-cluster.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Create cluster - -on: - workflow_call: - inputs: - prefix: - description: Cluster name prefix. - required: true - type: string - outputs: - clusterName: - description: "The name of created Cluster" - value: ${{ jobs.deploy-cluster.outputs.clusterName }} - -env: - BAAS_URL: ${{ secrets.REALM_QA_URL }} - BAAS_API_KEY: ${{ secrets.ATLAS_QA_PUBLIC_API_KEY }} - BAAS_PRIVATE_API_KEY: ${{ secrets.ATLAS_QA_PRIVATE_API_KEY }} - BAAS_PROJECT_ID: ${{ secrets.ATLAS_QA_PROJECT_ID}} - REALM_CI: true - -jobs: - deploy-cluster: - runs-on: ubuntu-latest - name: Deploy ${{ inputs.prefix == 'd' && 'Dart' || 'Flutter' }} Cluster - timeout-minutes: 15 - outputs: - clusterName: ${{ steps.cluster-name.outputs.clusterName }} - steps: - - name: Get cluster suffix - id: cluster-name - # Use 'github.ref_name' for generating cluster name. - # 'github.ref_name' is the SHORT ref name of the branch or tag that triggered the workflow run. - # 'github.ref_name' looks like '1234/merge'. We remove '/merge' and get only the number. - # In order to have unique cluster name per run we add a random number to the number extracted from 'ref_name'. - # Maximum 8 symbols could be taken for the name of the cluster, becasuse of the lenght limitation for cluster names. - run: | - triggerName=${{ inputs.prefix }}${{ github.ref_name}} - cluster=${triggerName/'/merge'/''} - echo "clusterName=$(cut -c 1-8 <<< ${cluster}$RANDOM)" >> $GITHUB_OUTPUT - - - uses: realm/ci-actions/mdb-realm/deploy@338bf3e7575015a28faec8b67614385d122aece7 - with: - realmUrl: ${{ env.BAAS_URL }} - atlasUrl: ${{ secrets.ATLAS_QA_URL }} - projectId: ${{ env.BAAS_PROJECT_ID }} - apiKey: ${{ env.BAAS_API_KEY }} - privateApiKey: ${{ env.BAAS_PRIVATE_API_KEY }} - clusterName: ${{ steps.cluster-name.outputs.clusterName }} \ No newline at end of file diff --git a/.github/workflows/dart-desktop-tests.yml b/.github/workflows/dart-desktop-tests.yml index c8b9df310..716db7d5e 100644 --- a/.github/workflows/dart-desktop-tests.yml +++ b/.github/workflows/dart-desktop-tests.yml @@ -15,20 +15,12 @@ on: description: Architecture to execute on. required: false type: string - app: - description: App name prefix. - required: true - type: string - cluster: - description: Cluster name to deploy. + baasurl: + description: BaaS url to test against. required: true type: string env: - BAAS_URL: ${{ secrets.REALM_QA_URL }} - BAAS_API_KEY: ${{ secrets.ATLAS_QA_PUBLIC_API_KEY }} - BAAS_PRIVATE_API_KEY: ${{ secrets.ATLAS_QA_PRIVATE_API_KEY }} - BAAS_PROJECT_ID: ${{ secrets.ATLAS_QA_PROJECT_ID}} REALM_CI: true jobs: @@ -37,8 +29,7 @@ jobs: name: Dart tests on ${{inputs.os }} ${{ inputs.architecture }} timeout-minutes: 45 env: - BAAS_CLUSTER: ${{ inputs.cluster }} - BAAS_DIFFERENTIATOR: ${{ inputs.app }}${{ github.run_id }}${{ github.run_attempt }} + BAAS_URL: ${{ inputs.baasurl }} steps: - name: Checkout @@ -68,17 +59,17 @@ jobs: run: ulimit -n 10240 if: ${{ contains(inputs.os, 'macos') }} - # This will be a no-op under normal circumstances since the cluster would have been deployed - # in deploy-cluster. It is needed in case we want to re-run the job after the cluster has been reaped. - - name: Create cluster - uses: realm/ci-actions/mdb-realm/deploy@338bf3e7575015a28faec8b67614385d122aece7 - with: - realmUrl: ${{ env.BAAS_URL }} - atlasUrl: ${{ secrets.ATLAS_QA_URL }} - projectId: ${{ env.BAAS_PROJECT_ID }} - apiKey: ${{ env.BAAS_API_KEY }} - privateApiKey: ${{ env.BAAS_PRIVATE_API_KEY }} - clusterName: ${{ env.BAAS_CLUSTER }} + # # This will be a no-op under normal circumstances since the cluster would have been deployed + # # in deploy-cluster. It is needed in case we want to re-run the job after the cluster has been reaped. + # - name: Create cluster + # uses: realm/ci-actions/mdb-realm/deploy@338bf3e7575015a28faec8b67614385d122aece7 + # with: + # realmUrl: ${{ env.BAAS_URL }} + # atlasUrl: ${{ secrets.ATLAS_QA_URL }} + # projectId: ${{ env.BAAS_PROJECT_ID }} + # apiKey: ${{ env.BAAS_API_KEY }} + # privateApiKey: ${{ env.BAAS_PRIVATE_API_KEY }} + # clusterName: ${{ env.BAAS_CLUSTER }} - name: Run tests run: ${{ inputs.architecture == 'arm' && 'arch -arm64 ' || '' }}dart test -r expanded --coverage ./coverage/ -j 1 --test-randomize-ordering-seed random diff --git a/.github/workflows/deploy-baas.yml b/.github/workflows/deploy-baas.yml new file mode 100644 index 000000000..596072294 --- /dev/null +++ b/.github/workflows/deploy-baas.yml @@ -0,0 +1,42 @@ +name: Deploy cluster and apps + +on: + workflow_call: + outputs: + url: + description: "The url to connect to" + value: ${{ jobs.deploy-baas.outputs.url }} + +env: + REALM_CI: true + +jobs: + deploy-baas: + runs-on: ubuntu-latest + name: Deploy BaaS + timeout-minutes: 15 + outputs: + url: ${{ steps.baas-url.outputs.content }} + steps: + - name: Checkout + uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + with: + submodules: false + + - name : Setup Dart SDK + uses: dart-lang/setup-dart@main + with: + sdk: stable + architecture: 'x64' + + - name: Install dependencies + run: dart pub get + + - name: Deploy cluster and apps + run: dart run realm_dart deploy-apps --use-baas-aas + + - name: Read baas url + id: baas-url + uses: jaywcjlove/github-action-read-file@3e97449450678b461303d4820406de059f4830ea + with: + path: baasUrl.txt diff --git a/.github/workflows/flutter-desktop-tests.yml b/.github/workflows/flutter-desktop-tests.yml index 8f7568f60..1a2d559b7 100644 --- a/.github/workflows/flutter-desktop-tests.yml +++ b/.github/workflows/flutter-desktop-tests.yml @@ -15,20 +15,12 @@ on: description: Architecture to execute on. required: false type: string - app: - description: App name prefix - required: true - type: string - cluster: - description: Cluster name to deploy. + baasurl: + description: BaaS url to test against. required: true type: string env: - BAAS_URL: ${{ secrets.REALM_QA_URL }} - BAAS_API_KEY: ${{ secrets.ATLAS_QA_PUBLIC_API_KEY }} - BAAS_PRIVATE_API_KEY: ${{ secrets.ATLAS_QA_PRIVATE_API_KEY }} - BAAS_PROJECT_ID: ${{ secrets.ATLAS_QA_PROJECT_ID}} REALM_CI: true jobs: @@ -37,8 +29,7 @@ jobs: name: Flutter tests on ${{inputs.os }}-${{ inputs.architecture }} timeout-minutes: 45 env: - BAAS_CLUSTER: ${{ inputs.cluster }} - BAAS_DIFFERENTIATOR: ${{ inputs.app }}${{ github.run_id }}${{ github.run_attempt }} + BAAS_URL: ${{ inputs.baasurl }} steps: - name: Checkout @@ -78,17 +69,17 @@ jobs: run: ulimit -n 10240 if: ${{ contains(inputs.os, 'macos') }} - # This will be a no-op under normal circumstances since the cluster would have been deployed - # in deploy-cluster. It is needed in case we want to re-run the job after the cluster has been reaped. - - name: Create cluster - uses: realm/ci-actions/mdb-realm/deploy@338bf3e7575015a28faec8b67614385d122aece7 - with: - realmUrl: ${{ env.BAAS_URL }} - atlasUrl: ${{ secrets.ATLAS_QA_URL }} - projectId: ${{ env.BAAS_PROJECT_ID }} - apiKey: ${{ env.BAAS_API_KEY }} - privateApiKey: ${{ env.BAAS_PRIVATE_API_KEY }} - clusterName: ${{ env.BAAS_CLUSTER }} + # # This will be a no-op under normal circumstances since the cluster would have been deployed + # # in deploy-cluster. It is needed in case we want to re-run the job after the cluster has been reaped. + # - name: Create cluster + # uses: realm/ci-actions/mdb-realm/deploy@338bf3e7575015a28faec8b67614385d122aece7 + # with: + # realmUrl: ${{ env.BAAS_URL }} + # atlasUrl: ${{ secrets.ATLAS_QA_URL }} + # projectId: ${{ env.BAAS_PROJECT_ID }} + # apiKey: ${{ env.BAAS_API_KEY }} + # privateApiKey: ${{ env.BAAS_PRIVATE_API_KEY }} + # clusterName: ${{ env.BAAS_CLUSTER }} - name: Run tests run: ${{ inputs.os == 'linux' && 'xvfb-run' || '' }} flutter drive -d ${{ inputs.os }} --target=test_driver/app.dart --suppress-analytics --dart-entrypoint-args="" --debug # -a="Some test name" diff --git a/.github/workflows/shared-apps.yml b/.github/workflows/shared-apps.yml deleted file mode 100644 index 74d213cba..000000000 --- a/.github/workflows/shared-apps.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Shared Apps - -on: - workflow_call: - inputs: - env: - description: Dart or Flutter. - required: true - type: string - cluster: - description: Cluster name to deploy the apps. - required: true - type: string - cleanup: - description: Set to True to delete the shared apps. - required: false - default: false - type: boolean - -env: - BAAS_URL: ${{ secrets.REALM_QA_URL }} - BAAS_API_KEY: ${{ secrets.ATLAS_QA_PUBLIC_API_KEY }} - BAAS_PRIVATE_API_KEY: ${{ secrets.ATLAS_QA_PRIVATE_API_KEY }} - BAAS_PROJECT_ID: ${{ secrets.ATLAS_QA_PROJECT_ID}} - REALM_CI: true - -jobs: - shared-apps: - runs-on: ubuntu-latest - name: ${{ inputs.cleanup && 'Delete' || 'Create'}} ${{ inputs.env }} Shared Apps - timeout-minutes: 45 - env: - BAAS_CLUSTER: ${{ inputs.cluster }} - - steps: - - name: Checkout - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 - with: - submodules: false - - - name : Setup Dart SDK - uses: dart-lang/setup-dart@main - with: - sdk: stable - architecture: 'x64' - - - name: Install dependencies - run: dart pub get - - - name: ${{ inputs.cleanup && 'Delete' || 'Create'}} shared apps - run: | - dart run realm_dart ${{ inputs.cleanup && 'delete-apps' || 'deploy-apps'}} \ - --baas-url ${{ env.BAAS_URL }} \ - --atlas-cluster ${{ env.BAAS_CLUSTER }} \ - --api-key ${{ env.BAAS_API_KEY }} \ - --private-api-key ${{ env.BAAS_PRIVATE_API_KEY }} \ - --project-id ${{ env.BAAS_PROJECT_ID }} \ - --differentiator 'shared' diff --git a/lib/src/cli/atlas_apps/baas_client.dart b/lib/src/cli/atlas_apps/baas_client.dart index f0d78f972..0aa0301d1 100644 --- a/lib/src/cli/atlas_apps/baas_client.dart +++ b/lib/src/cli/atlas_apps/baas_client.dart @@ -16,8 +16,11 @@ // //////////////////////////////////////////////////////////////////////////////// +import 'package:collection/collection.dart'; import 'package:http/http.dart' as http; +import 'package:realm_dart/src/app.dart'; import 'dart:convert'; +import '../../realm_dart.dart'; class BaasClient { static const String _confirmFuncSource = '''exports = async ({ token, tokenId, username }) => { @@ -86,20 +89,20 @@ class BaasClient { static const String defaultAppName = "flexible"; - final String _baseUrl; + final String _adminApiUrl; final String? _clusterName; final Map _headers; final String _appSuffix; - final String _sharedAppSuffix; + + final String baseUrl; late String _groupId; late String publicRSAKey = ''; - BaasClient._(String baseUrl, String? differentiator, [this._clusterName]) - : _baseUrl = '$baseUrl/api/admin/v3.0', + BaasClient._(this.baseUrl, String? differentiator, [this._clusterName]) + : _adminApiUrl = '$baseUrl/api/admin/v3.0', _headers = {'Accept': 'application/json'}, - _appSuffix = '-${shortenDifferentiator(differentiator ?? 'local')}-$_clusterName', - _sharedAppSuffix = '-shared-$_clusterName'; + _appSuffix = '-${shortenDifferentiator(differentiator ?? 'local')}-$_clusterName'; /// A client that imports apps in a MongoDB Atlas docker image. See https://github.com/realm/ci/tree/master/realm/docker/mongodb-realm /// for instructions on how to set it up. @@ -117,6 +120,55 @@ class BaasClient { return result; } + static Future deployContainer() async { + print('Deploying new BaaS container... '); + + final appId = 'baas-container-service-autzb'; + final app = App(AppConfiguration(appId)); + final user = await app.logIn(Credentials.anonymous(reuseCredentials: false)); + final response = await user.functions.call('startContainer') as Map; + final taskId = response['taskId'] as String; + + String? httpUrl; + while (httpUrl == null) { + await Future.delayed(Duration(seconds: 1)); + httpUrl = await _waitForContainer(user, taskId); + } + + print('Deployed BaaS instance at $httpUrl'); + + return httpUrl; + } + + static Future _waitForContainer(User user, String taskId) async { + try { + final containers = await user.functions.call('listContainers') as List; + final targetContainer = containers.firstWhereOrNull((c) => c['id'] == taskId); + if (targetContainer == null) { + print('$taskId is not found in container list. Retrying...'); + return null; + } + + if (targetContainer['lastStatus'] != 'RUNNING') { + print('$taskId status is ${targetContainer['lastStatus']}. Retrying...'); + return null; + } + + final httpUrl = targetContainer['httpUrl'] as String; + + final response = await http.get(Uri.parse('$httpUrl/api/private/v1.0/version')); + if (response.statusCode > 300) { + print('$taskId version response is ${response.statusCode}. Retrying...'); + return null; + } + + return targetContainer['httpUrl'] as String; + } catch (e) { + print('Error waiting for container: $e'); + return null; + } + } + /// A client that imports apps to a MongoDB Atlas environment (typically realm-dev or realm-qa). /// @nodoc static Future atlas(String baseUrl, String cluster, String apiKey, String privateApiKey, String groupId, String? differentiator) async { @@ -133,34 +185,16 @@ class BaasClient { /// for [atlas] one, it will return only apps with suffix equal to the cluster name. If no apps exist, /// then it will create the test applications and return them. /// @nodoc - Future> getOrCreateApps() async { - final result = await _getExistingApps(); - await _createAppIfNotExists(result, defaultAppName, _appSuffix); - await _createAppIfNotExists(result, "autoConfirm", _sharedAppSuffix, confirmationType: "auto"); - await _createAppIfNotExists(result, "emailConfirm", _sharedAppSuffix, confirmationType: "email"); - return result; - } - - Future> getOrCreateSharedApps() async { - final result = await _getExistingApps(); - await _createAppIfNotExists(result, "autoConfirm", _sharedAppSuffix, confirmationType: "auto"); - await _createAppIfNotExists(result, "emailConfirm", _sharedAppSuffix, confirmationType: "email"); - return result; - } - - Future> _getExistingApps() async { - final result = {}; + Future> getOrCreateApps() async { var apps = await _getApps(); - if (apps.isNotEmpty) { - for (final app in apps) { - result[app.name] = app; - } - } - return result; + await _createAppIfNotExists(apps, defaultAppName, _appSuffix); + await _createAppIfNotExists(apps, "autoConfirm", _appSuffix, confirmationType: "auto"); + await _createAppIfNotExists(apps, "emailConfirm", _appSuffix, confirmationType: "email"); + return apps; } Future waitForInitialSync(BaasApp app) async { - while (!await _isSyncComplete(app)) { + while (!await _isSyncComplete(app.appId)) { print('Initial sync for ${app.name} is incomplete. Waiting 5 seconds.'); await Future.delayed(Duration(seconds: 5)); } @@ -168,16 +202,16 @@ class BaasClient { print('Initial sync for ${app.name} is complete.'); } - Future _createAppIfNotExists(Map existingApps, String appName, String appSuffix, {String? confirmationType}) async { - final existingApp = existingApps[appName]; + Future _createAppIfNotExists(List existingApps, String appName, String appSuffix, {String? confirmationType}) async { + final existingApp = existingApps.firstWhereOrNull((a) => a.name == appName); if (existingApp == null) { - existingApps[appName] = await _createApp(appName, appSuffix, confirmationType: confirmationType); + existingApps.add(await _createApp(appName, appSuffix, confirmationType: confirmationType)); } } - Future _isSyncComplete(BaasApp app) async { + Future _isSyncComplete(String appId) async { try { - final response = await _get('groups/$_groupId/apps/$app/sync/progress'); + final response = await _get('groups/$_groupId/apps/$appId/sync/progress'); Map progressInfo = response['progress']; for (final key in progressInfo.keys) { @@ -203,8 +237,6 @@ class BaasClient { final String appName; if (name.endsWith(_appSuffix)) { appName = name.substring(0, name.length - _appSuffix.length); - } else if (name.endsWith(_sharedAppSuffix)) { - appName = name.substring(0, name.length - _sharedAppSuffix.length); } else { return null; } @@ -217,10 +249,9 @@ class BaasClient { Future updateAppConfirmFunction(String name, [String? source]) async { final uniqueName = "$name$_appSuffix"; - final uniqueSharedAppName = "$name$_sharedAppSuffix"; final dynamic docs = await _get('groups/$_groupId/apps'); dynamic doc = docs.firstWhere((dynamic d) { - return d["name"] == uniqueName || d["name"] == uniqueSharedAppName; + return d["name"] == uniqueName; }, orElse: () => throw Exception("BAAS app not found")); final appId = doc['_id'] as String; final appUniqueName = doc['name'] as String; @@ -453,10 +484,10 @@ class BaasClient { } } - Future createApiKey(BaasApp app, String name, bool enabled) async { - final dynamic result = await _post('groups/$_groupId/apps/${app.appId}/api_keys', '{ "name":"$name" }'); + Future createApiKey(String appId, String name, bool enabled) async { + final dynamic result = await _post('groups/$_groupId/apps/$appId/api_keys', '{ "name":"$name" }'); if (!enabled) { - await _put('groups/$_groupId/apps/${app.appId}/api_keys/${result['_id']}/disable', ''); + await _put('groups/$_groupId/apps/$appId/api_keys/${result['_id']}/disable', ''); } return result['key'] as String; @@ -549,7 +580,7 @@ class BaasClient { } Uri _getUri(String relativePath) { - return Uri.parse('$_baseUrl/$relativePath'); + return Uri.parse('$_adminApiUrl/$relativePath'); } Future _post(String relativePath, String payload) async { @@ -599,10 +630,9 @@ class BaasClient { Future setAutomaticRecoveryEnabled(String name, bool enable) async { final uniqueName = "$name$_appSuffix"; - final uniqueSharedAppName = "$name$_sharedAppSuffix"; final dynamic docs = await _get('groups/$_groupId/apps'); dynamic doc = docs.firstWhere((dynamic d) { - return d["name"] == uniqueName || d["name"] == uniqueSharedAppName; + return d["name"] == uniqueName; }, orElse: () => throw Exception("BAAS app not found")); final appId = doc['_id'] as String; final appUniqueName = doc['name'] as String; diff --git a/lib/src/cli/atlas_apps/deleteapps_command.dart b/lib/src/cli/atlas_apps/deleteapps_command.dart index 4cd893658..a7cebc820 100644 --- a/lib/src/cli/atlas_apps/deleteapps_command.dart +++ b/lib/src/cli/atlas_apps/deleteapps_command.dart @@ -56,11 +56,21 @@ class DeleteAppsCommand extends Command { abort('--project-id must be supplied when --atlas-cluster is not set'); } } + + if (!options.useBaaSaaS && options.baasUrl == null) { + abort('--baas-url must be supplied when --use-baas-aas is not set'); + } + + if (options.useBaaSaaS) { + print('Deleting apps from BaaSaaS container is not supported or necessary'); + return; + } + final differentiator = options.differentiator ?? 'local'; final client = await (options.atlasCluster == null - ? BaasClient.docker(options.baasUrl, differentiator) - : BaasClient.atlas(options.baasUrl, options.atlasCluster!, options.apiKey!, options.privateApiKey!, options.projectId!, differentiator)); + ? BaasClient.docker(options.baasUrl!, differentiator) + : BaasClient.atlas(options.baasUrl!, options.atlasCluster!, options.apiKey!, options.privateApiKey!, options.projectId!, differentiator)); await client.deleteApps(); } diff --git a/lib/src/cli/atlas_apps/deployapps_command.dart b/lib/src/cli/atlas_apps/deployapps_command.dart index f155466da..db62a1f53 100644 --- a/lib/src/cli/atlas_apps/deployapps_command.dart +++ b/lib/src/cli/atlas_apps/deployapps_command.dart @@ -56,36 +56,55 @@ RwIDAQAB if (options.atlasCluster != null) { if (options.apiKey == null) { - abort('--api-key must be supplied when --atlas-cluster is not set'); + abort('--api-key must be supplied when --atlas-cluster is set'); } if (options.privateApiKey == null) { - abort('--private-api-key must be supplied when --atlas-cluster is not set'); + abort('--private-api-key must be supplied when --atlas-cluster is set'); } if (options.projectId == null) { - abort('--project-id must be supplied when --atlas-cluster is not set'); + abort('--project-id must be supplied when --atlas-cluster is set'); } + + if (options.useBaaSaaS) { + abort('--use-baas-aas cannot be used when --atlas-cluster is set'); + } + } + + if (!options.useBaaSaaS && options.baasUrl == null) { + abort('--baas-url must be supplied when --use-baas-aas is not set'); } - final differentiator = options.differentiator ?? 'shared'; + late String baasUrl; + if (options.useBaaSaaS) { + baasUrl = await BaasClient.deployContainer(); + final file = File('baasUrl.txt'); + await file.writeAsString(baasUrl); + print('BaasUrl: $baasUrl. Written to ${file.path}'); + } else { + baasUrl = options.baasUrl!; + } + + final differentiator = options.differentiator; try { final client = await (options.atlasCluster == null - ? BaasClient.docker(options.baasUrl, differentiator) - : BaasClient.atlas(options.baasUrl, options.atlasCluster!, options.apiKey!, options.privateApiKey!, options.projectId!, differentiator)); + ? BaasClient.docker(baasUrl, differentiator) + : BaasClient.atlas(baasUrl, options.atlasCluster!, options.apiKey!, options.privateApiKey!, options.projectId!, differentiator)); client.publicRSAKey = publicRSAKeyForJWTValidation; - var apps = await client.getOrCreateSharedApps(); + var apps = await client.getOrCreateApps(); print('App import is complete. There are: ${apps.length} apps on the server:'); List listApps = []; - apps.forEach((_, value) { + for (var value in apps) { print(" App '${value.name}': '${value.clientAppId}'"); if (value.error != null) { print(value.error!); } listApps.add(value.appId); - }); + } print("appIds: "); print(listApps.join(",")); + exit(0); } catch (error) { print(error); } diff --git a/lib/src/cli/atlas_apps/options.dart b/lib/src/cli/atlas_apps/options.dart index 46cbd6cfd..3485dd306 100644 --- a/lib/src/cli/atlas_apps/options.dart +++ b/lib/src/cli/atlas_apps/options.dart @@ -22,8 +22,8 @@ part 'options.g.dart'; @CliOptions() class Options { - @CliOption(help: 'Url for MongoDB Atlas.', defaultsTo: 'http://localhost:9090') - final String baasUrl; + @CliOption(help: 'Url for MongoDB Atlas.') + final String? baasUrl; @CliOption(help: 'The database prefix that will be used for the sync service.') final String? differentiator; @@ -40,7 +40,10 @@ class Options { @CliOption(help: 'The Atlas project id to use for the import. Only used if atlas-cluster is specified.') final String? projectId; - Options(this.baasUrl, {this.atlasCluster, this.apiKey, this.privateApiKey, this.projectId, this.differentiator}); + @CliOption(help: 'Spawn a new container for BaaSaaS and creates app in it.', name: 'use-baas-aas') + final bool useBaaSaaS; + + Options({this.baasUrl, this.atlasCluster, this.apiKey, this.privateApiKey, this.projectId, this.differentiator, this.useBaaSaaS = false}); } String get usage => _$parserForOptions.usage; diff --git a/lib/src/cli/atlas_apps/options.g.dart b/lib/src/cli/atlas_apps/options.g.dart index 10c4720f9..779e7d3a9 100644 --- a/lib/src/cli/atlas_apps/options.g.dart +++ b/lib/src/cli/atlas_apps/options.g.dart @@ -7,19 +7,19 @@ part of 'options.dart'; // ************************************************************************** Options _$parseOptionsResult(ArgResults result) => Options( - result['baas-url'] as String, + baasUrl: result['baas-url'] as String?, atlasCluster: result['atlas-cluster'] as String?, apiKey: result['api-key'] as String?, privateApiKey: result['private-api-key'] as String?, projectId: result['project-id'] as String?, differentiator: result['differentiator'] as String?, + useBaaSaaS: result['use-baas-aas'] as bool, ); ArgParser _$populateOptionsParser(ArgParser parser) => parser ..addOption( 'baas-url', help: 'Url for MongoDB Atlas.', - defaultsTo: 'http://localhost:9090', ) ..addOption( 'differentiator', @@ -43,6 +43,10 @@ ArgParser _$populateOptionsParser(ArgParser parser) => parser 'project-id', help: 'The Atlas project id to use for the import. Only used if atlas-cluster is specified.', + ) + ..addFlag( + 'use-baas-aas', + help: 'Spawn a new container for BaaSaaS and creates app in it.', ); final _$parserForOptions = _$populateOptionsParser(ArgParser()); diff --git a/test/app_test.dart b/test/app_test.dart index 924c7bcaa..39efc98cd 100644 --- a/test/app_test.dart +++ b/test/app_test.dart @@ -28,6 +28,7 @@ import 'test.dart'; Future main([List? args]) async { await setupTests(args); + final a = 5; test('AppConfiguration can be initialized', () { Configuration.defaultRealmPath = path.join(Configuration.defaultStoragePath, Configuration.defaultRealmName); diff --git a/test/baas_helper.dart b/test/baas_helper.dart new file mode 100644 index 000000000..b890db623 --- /dev/null +++ b/test/baas_helper.dart @@ -0,0 +1,300 @@ +import 'dart:io'; +import 'dart:isolate'; + +import 'package:args/args.dart'; +import 'package:path/path.dart' as _path; +import 'package:test/test.dart' as testing; +import 'package:test/test.dart'; + +import '../lib/src/cli/atlas_apps/baas_client.dart'; +import '../lib/realm.dart'; + +import 'test.dart'; + +part 'baas_helper.g.dart'; + +const String argBaasUrl = "BAAS_URL"; +const String argBaasCluster = "BAAS_CLUSTER"; +const String argBaasApiKey = "BAAS_API_KEY"; +const String argBaasPrivateApiKey = "BAAS_PRIVATE_API_KEY"; +const String argBaasProjectId = "BAAS_PROJECT_ID"; +const String argDifferentiator = "BAAS_DIFFERENTIATOR"; +const String argUseBaaSaaS = "BAAS_USE_BAASAAS"; + +const String publicRSAKeyForJWTValidation = '''-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvNHHs8T0AHD7SJ+CKvVR +leeJa4wqYTnaVYV+5bX9FmFXVoN+vHbMLEteMvSw4L3kSRZdcqxY7cTuhlpAvkXP +Yq6qSI+bW8T4jGW963uCc83UhVMx4MH/PzipAlfcPjVO2u4c+dmpgZQpgEmA467u +tauXUhmTsGpgNg2Gvc61B7Ny4LphshsyrfaJ9WjA/NM6LOmEBW3JPNcVG2qyU+gt +O8BM8KOSx9wGyoGs4+OusvRkJizhPaIwa3FInLs4r+xZW9Bp6RndsmVECtvXRv5d +87ztpg6o3DZJRmTp2lAnkNLmxXlFkOSNIwiT3qqyRZOh4DuxPOpfg9K+vtFmRdEJ +RwIDAQAB +-----END PUBLIC KEY-----'''; + +Map parseTestArguments(List? arguments) { + Map testArgs = {}; + final parser = ArgParser() + ..addOption("name") + ..addOption(argBaasUrl) + ..addOption(argBaasCluster) + ..addOption(argBaasApiKey) + ..addOption(argBaasPrivateApiKey) + ..addOption(argBaasProjectId) + ..addOption(argDifferentiator) + ..addOption(argUseBaaSaaS); + + final result = parser.parse(arguments ?? []); + testArgs + ..addArgument(result, "name") + ..addArgument(result, argBaasUrl) + ..addArgument(result, argBaasCluster) + ..addArgument(result, argBaasApiKey) + ..addArgument(result, argBaasPrivateApiKey) + ..addArgument(result, argBaasProjectId) + ..addArgument(result, argDifferentiator) + ..addArgument(result, argUseBaaSaaS); + + return testArgs; +} + +extension on Map { + void addArgument(ArgResults parsedResult, String argName) { + final value = parsedResult.wasParsed(argName) ? parsedResult[argName]?.toString() : Platform.environment[argName]; + if (value != null && value.isNotEmpty) { + this[argName] = value; + } + } +} + +enum AppNames { + flexible, + + // For application with name 'autoConfirm' and with confirmationType = 'auto' + // all the usernames are automatically confirmed. + autoConfirm, + + emailConfirm, +} + +@RealmModel() +class _BaasInfo { + late String baasUrl; + String? cluster; + String? apiKey; + String? privateApiKey; + String? projectId; + String? differentiator; + + late List<_BaasAppDetails> apps; +} + +@RealmModel(ObjectType.embeddedObject) +class _BaasAppDetails { + late String appId; + late String clientAppId; + late String name; + late String uniqueName; + + String? error; +} + +class BaasHelper { + final BaasClient _baasClient; + final _baasApps = {}; + + String get baseUrl => _baasClient.baseUrl; + + static Object? _error; + + static Future setupBaas(Map args) async { + if (_error != null) { + throw _error!; + } + + final realmPath = _path.join(Directory.current.path, 'baasmeta', 'baas_$pid.realm'); + final realm = Realm(Configuration.local([BaasInfo.schema, BaasAppDetails.schema], path: realmPath)); + final (client, baasInfo) = await _setupClient(args, realm); + if (client == null || baasInfo == null) { + return null; + } + + final result = BaasHelper._(client); + + await result._setupApps(baasInfo); + + return result; + } + + BaasHelper._(this._baasClient); + + static Future<(BaasClient?, BaasInfo?)> _setupClient(Map args, Realm realm) async { + try { + var baasInfo = realm.all().firstOrNull; + if (baasInfo == null) { + late String? baasUrl; + final useBaaSaaS = args[argUseBaaSaaS] == 'true'; + if (useBaaSaaS) { + if (args[argBaasCluster] != null) { + throw "$argUseBaaSaaS can't be combined with $argBaasCluster"; + } + + baasUrl = await BaasClient.deployContainer(); + } else { + baasUrl = args[argBaasUrl]; + } + + if (baasUrl == null) { + return (null, null); + } + + baasInfo = realm.write(() => realm.add(BaasInfo(baasUrl!, + cluster: args[argBaasCluster], + apiKey: args[argBaasApiKey], + privateApiKey: args[argBaasPrivateApiKey], + projectId: args[argBaasProjectId], + differentiator: args[argDifferentiator])))!; + } + + final client = await (baasInfo.cluster == null + ? BaasClient.docker(baasInfo.baasUrl, baasInfo.differentiator) + : BaasClient.atlas(baasInfo.baasUrl, baasInfo.cluster!, baasInfo.apiKey!, baasInfo.privateApiKey!, baasInfo.projectId!, baasInfo.differentiator)); + + client.publicRSAKey = publicRSAKeyForJWTValidation; + return (client, baasInfo); + } catch (error) { + print(error); + _error = error; + return (null, null); + } + } + + Future _setupApps(BaasInfo baasInfo) async { + try { + var isNewDeployment = false; + if (baasInfo.apps.isEmpty) { + final apps = await _baasClient.getOrCreateApps(); + baasInfo.realm.write(() { + baasInfo.apps.addAll(apps.map((e) => BaasAppDetails(e.appId, e.clientAppId, e.name, e.uniqueName, error: e.error?.toString()))); + }); + isNewDeployment = true; + } + + for (final app in baasInfo.apps) { + _baasApps[app.name] = BaasApp(app.appId, app.clientAppId, app.name, app.uniqueName)..error = app.error; + } + + if (isNewDeployment) { + await _waitForInitialSync(AppNames.flexible); + } + } catch (error) { + print(error); + _error = error; + } + } + + Future _waitForInitialSync(AppNames app) async { + while (true) { + try { + final baasApp = _baasApps[app.name]!; + print('Validating initial sync is complete...'); + await _baasClient.waitForInitialSync(baasApp); + final appConfig = await _getAppConfig(baasApp.name); + final realm = await getIntegrationRealm(appConfig: appConfig); + await realm.syncSession.waitForUpload(); + await _baasClient.waitForInitialSync(baasApp); + return; + } catch (e) { + print(e); + } + } + } + + Future createServerApiKey(App app, String name, {bool enabled = true}) async { + final baasApp = _baasApps.values.firstWhere((ba) => ba.clientAppId == app.id); + return await _baasClient.createApiKey(baasApp.appId, name, enabled); + } + + void throwIfSetupFailed() { + if (_error != null) { + throw _error!; + } + } + + void printSplunkLogLink(AppNames appName, String? uriVariable) { + if (uriVariable == null) { + return; + } + + final app = _baasApps[appName.name] ?? (throw RealmError("No BAAS apps")); + final baasUri = Uri.parse(uriVariable); + + testing.printOnFailure("App service name: ${app.uniqueName}"); + final host = baasUri.host.endsWith('-qa.mongodb.com') ? "-qa" : ""; + final splunk = Uri.encodeFull( + "https://splunk.corp.mongodb.com/en-US/app/search/search?q=search index=baas$host \"${app.uniqueName}-*\" | reverse | top error msg&earliest=-7d&latest=now&display.general.type=visualizations"); + testing.printOnFailure("Splunk logs: $splunk"); + } + + Future getAppConfig({AppNames appName = AppNames.flexible}) => _getAppConfig(appName.name); + + Future _getAppConfig(String appName) async { + final app = _baasApps[appName] ?? + _baasApps.values.firstWhere((element) => element.name == BaasClient.defaultAppName, orElse: () => throw RealmError("No BAAS apps")); + if (app.error != null) { + throw app.error!; + } + + final temporaryDir = await Directory.systemTemp.createTemp('realm_test_'); + return AppConfiguration( + app.clientAppId, + baseUrl: Uri.parse(baseUrl), + baseFilePath: temporaryDir, + maxConnectionTimeout: Duration(minutes: 10), + defaultRequestTimeout: Duration(minutes: 7), + ); + } + + String getClientAppId({AppNames appName = AppNames.flexible}) => _baasApps[appName.name]!.clientAppId; + + Future disableAutoRecoveryForApp(AppNames appName) async { + await _baasClient.setAutomaticRecoveryEnabled(appName.name, false); + } + + Future enableAutoRecoveryForApp(AppNames appName) async { + await _baasClient.setAutomaticRecoveryEnabled(appName.name, true); + } + + Future triggerClientReset(Realm realm, {bool restartSession = true}) async { + final config = realm.config; + if (config is! FlexibleSyncConfiguration) { + throw RealmError('This should only be invoked for sync realms'); + } + + final session = realm.syncSession; + if (restartSession) { + session.pause(); + } + + final userId = config.user.id; + final appId = _baasApps.values.firstWhere((element) => element.clientAppId == config.user.app.id).appId; + + for (var i = 0; i < 5; i++) { + try { + final result = await config.user.functions.call('triggerClientResetOnSyncServer', [userId, appId]) as Map; + expect(result['status'], 'success'); + break; + } catch (e) { + if (i == 4) { + rethrow; + } + + print('Failed to trigger client reset: $e'); + await Future.delayed(Duration(seconds: i)); + } + } + + if (restartSession) { + session.resume(); + } + } +} diff --git a/test/baas_helper.g.dart b/test/baas_helper.g.dart new file mode 100644 index 000000000..4c20b0e19 --- /dev/null +++ b/test/baas_helper.g.dart @@ -0,0 +1,171 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'baas_helper.dart'; + +// ************************************************************************** +// RealmObjectGenerator +// ************************************************************************** + +// ignore_for_file: type=lint +class BaasInfo extends _BaasInfo + with RealmEntity, RealmObjectBase, RealmObject { + BaasInfo( + String baasUrl, { + String? cluster, + String? apiKey, + String? privateApiKey, + String? projectId, + String? differentiator, + Iterable apps = const [], + }) { + RealmObjectBase.set(this, 'baasUrl', baasUrl); + RealmObjectBase.set(this, 'cluster', cluster); + RealmObjectBase.set(this, 'apiKey', apiKey); + RealmObjectBase.set(this, 'privateApiKey', privateApiKey); + RealmObjectBase.set(this, 'projectId', projectId); + RealmObjectBase.set(this, 'differentiator', differentiator); + RealmObjectBase.set>( + this, 'apps', RealmList(apps)); + } + + BaasInfo._(); + + @override + String get baasUrl => RealmObjectBase.get(this, 'baasUrl') as String; + @override + set baasUrl(String value) => RealmObjectBase.set(this, 'baasUrl', value); + + @override + String? get cluster => + RealmObjectBase.get(this, 'cluster') as String?; + @override + set cluster(String? value) => RealmObjectBase.set(this, 'cluster', value); + + @override + String? get apiKey => RealmObjectBase.get(this, 'apiKey') as String?; + @override + set apiKey(String? value) => RealmObjectBase.set(this, 'apiKey', value); + + @override + String? get privateApiKey => + RealmObjectBase.get(this, 'privateApiKey') as String?; + @override + set privateApiKey(String? value) => + RealmObjectBase.set(this, 'privateApiKey', value); + + @override + String? get projectId => + RealmObjectBase.get(this, 'projectId') as String?; + @override + set projectId(String? value) => RealmObjectBase.set(this, 'projectId', value); + + @override + String? get differentiator => + RealmObjectBase.get(this, 'differentiator') as String?; + @override + set differentiator(String? value) => + RealmObjectBase.set(this, 'differentiator', value); + + @override + RealmList get apps => + RealmObjectBase.get(this, 'apps') + as RealmList; + @override + set apps(covariant RealmList value) => + throw RealmUnsupportedSetError(); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + BaasInfo freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(BaasInfo._); + return const SchemaObject(ObjectType.realmObject, BaasInfo, 'BaasInfo', [ + SchemaProperty('baasUrl', RealmPropertyType.string), + SchemaProperty('cluster', RealmPropertyType.string, optional: true), + SchemaProperty('apiKey', RealmPropertyType.string, optional: true), + SchemaProperty('privateApiKey', RealmPropertyType.string, optional: true), + SchemaProperty('projectId', RealmPropertyType.string, optional: true), + SchemaProperty('differentiator', RealmPropertyType.string, + optional: true), + SchemaProperty('apps', RealmPropertyType.object, + linkTarget: 'BaasAppDetails', + collectionType: RealmCollectionType.list), + ]); + } +} + +// ignore_for_file: type=lint +class BaasAppDetails extends _BaasAppDetails + with RealmEntity, RealmObjectBase, EmbeddedObject { + BaasAppDetails( + String appId, + String clientAppId, + String name, + String uniqueName, { + String? error, + }) { + RealmObjectBase.set(this, 'appId', appId); + RealmObjectBase.set(this, 'clientAppId', clientAppId); + RealmObjectBase.set(this, 'name', name); + RealmObjectBase.set(this, 'uniqueName', uniqueName); + RealmObjectBase.set(this, 'error', error); + } + + BaasAppDetails._(); + + @override + String get appId => RealmObjectBase.get(this, 'appId') as String; + @override + set appId(String value) => RealmObjectBase.set(this, 'appId', value); + + @override + String get clientAppId => + RealmObjectBase.get(this, 'clientAppId') as String; + @override + set clientAppId(String value) => + RealmObjectBase.set(this, 'clientAppId', value); + + @override + String get name => RealmObjectBase.get(this, 'name') as String; + @override + set name(String value) => RealmObjectBase.set(this, 'name', value); + + @override + String get uniqueName => + RealmObjectBase.get(this, 'uniqueName') as String; + @override + set uniqueName(String value) => + RealmObjectBase.set(this, 'uniqueName', value); + + @override + String? get error => RealmObjectBase.get(this, 'error') as String?; + @override + set error(String? value) => RealmObjectBase.set(this, 'error', value); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + BaasAppDetails freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(BaasAppDetails._); + return const SchemaObject( + ObjectType.embeddedObject, BaasAppDetails, 'BaasAppDetails', [ + SchemaProperty('appId', RealmPropertyType.string), + SchemaProperty('clientAppId', RealmPropertyType.string), + SchemaProperty('name', RealmPropertyType.string), + SchemaProperty('uniqueName', RealmPropertyType.string), + SchemaProperty('error', RealmPropertyType.string, optional: true), + ]); + } +} diff --git a/test/client_reset_test.dart b/test/client_reset_test.dart index fb18c624e..5ff938c2e 100644 --- a/test/client_reset_test.dart +++ b/test/client_reset_test.dart @@ -81,7 +81,7 @@ Future main([List? args]) async { final realm = await getRealmAsync(config); await realm.syncSession.waitForUpload(); - await triggerClientReset(realm); + await baasHelper!.triggerClientReset(realm); final clientResetFuture = resetCompleter.future.wait(defaultWaitTimeout, "ManualRecoveryHandler is not reported."); await expectLater(clientResetFuture, throws('Bad client file identifier')); }); @@ -108,7 +108,7 @@ Future main([List? args]) async { clientResetError.resetRealm(); }, test: (error) => error is ClientResetError); - await triggerClientReset(realm); + await baasHelper!.triggerClientReset(realm); await resetRealmFuture.wait(defaultWaitTimeout, "ManualRecoveryHandler is not reported."); @@ -136,7 +136,7 @@ Future main([List? args]) async { return clientResetError.resetRealm(); }, test: (error) => error is ClientResetError); - await triggerClientReset(realm); + await baasHelper!.triggerClientReset(realm); expect(await resetRealmFuture.timeout(defaultWaitTimeout), !Platform.isWindows); expect(File(config.path).existsSync(), Platform.isWindows); // posix and windows semantics are different @@ -162,7 +162,7 @@ Future main([List? args]) async { final realm = await getRealmAsync(config); await realm.syncSession.waitForUpload(); - await triggerClientReset(realm); + await baasHelper!.triggerClientReset(realm); final clientResetFuture = onManualResetFallback.future.wait(defaultWaitTimeout, "onManualResetFallback is not reported."); await expectLater(clientResetFuture, throws()); @@ -188,7 +188,7 @@ Future main([List? args]) async { final realm = await getRealmAsync(config); await realm.syncSession.waitForUpload(); - await triggerClientReset(realm); + await baasHelper!.triggerClientReset(realm); final clientResetFuture = onManualResetFallback.future.wait(defaultWaitTimeout, "onManualResetFallback is not reported."); await expectLater(clientResetFuture, throws()); @@ -215,7 +215,7 @@ Future main([List? args]) async { final realm = await getRealmAsync(config); await realm.syncSession.waitForUpload(); - await triggerClientReset(realm); + await baasHelper!.triggerClientReset(realm); await onBeforeCompleter.future.timeout(defaultWaitTimeout, onTimeout: () => throw TimeoutException("onBeforeReset is not reported")); await onAfterCompleter.future.timeout(defaultWaitTimeout, onTimeout: () => throw TimeoutException("onAfterReset is not reported.")); @@ -273,9 +273,9 @@ Future main([List? args]) async { await waitForCondition(() => notifications.length == 1, timeout: Duration(seconds: 3)); if (shouldDisableAutoRecoveryForApp) { - await disableAutoRecoveryForApp(baasAppName); + await baasHelper!.disableAutoRecoveryForApp(baasAppName); } - await triggerClientReset(realm, restartSession: false); + await baasHelper!.triggerClientReset(realm, restartSession: false); realm.syncSession.resume(); await onAfterCompleter.future.wait(defaultWaitTimeout, "Neither onAfterDiscard nor onManualResetFallback is reported."); @@ -289,7 +289,7 @@ Future main([List? args]) async { expect(notifications.firstWhere((n) => n.deleted.isNotEmpty), isNotNull); } finally { if (shouldDisableAutoRecoveryForApp) { - await enableAutoRecoveryForApp(baasAppName); + await baasHelper!.enableAutoRecoveryForApp(baasAppName); } } }); @@ -339,7 +339,7 @@ Future main([List? args]) async { realm.syncSession.pause(); realm.write(() => realm.add(Product(maybeId, "maybe synced"))); - await triggerClientReset(realm, restartSession: false); + await baasHelper!.triggerClientReset(realm, restartSession: false); realm.syncSession.resume(); await onAfterCompleter.future.wait(defaultWaitTimeout, "Neither onAfterDiscard, onAfterDiscard nor onManualResetFallback is reported."); }); @@ -373,9 +373,9 @@ Future main([List? args]) async { final realm = await getRealmAsync(config); await realm.syncSession.waitForUpload(); - await disableAutoRecoveryForApp(baasAppName); + await baasHelper!.disableAutoRecoveryForApp(baasAppName); try { - await triggerClientReset(realm); + await baasHelper!.triggerClientReset(realm); await onBeforeCompleter.future.wait(defaultWaitTimeout, "onBeforeReset is not reported."); await onAfterCompleter.future.wait(defaultWaitTimeout, "Neither onAfterRecovery nor onAfterDiscard is reported."); @@ -383,7 +383,7 @@ Future main([List? args]) async { expect(recovery, isFalse); expect(discard, isTrue); } finally { - await enableAutoRecoveryForApp(baasAppName); + await baasHelper!.enableAutoRecoveryForApp(baasAppName); } }); } @@ -415,7 +415,7 @@ Future main([List? args]) async { final realm = await getRealmAsync(config); await realm.syncSession.waitForUpload(); - await triggerClientReset(realm); + await baasHelper!.triggerClientReset(realm); await onAfterCompleter.future.wait(defaultWaitTimeout, "onAfterReset is not reported."); @@ -457,7 +457,7 @@ Future main([List? args]) async { final realm = await getRealmAsync(config); await realm.syncSession.waitForUpload(); - await triggerClientReset(realm); + await baasHelper!.triggerClientReset(realm); await manualResetFallbackCompleter.future.wait(defaultWaitTimeout, "onManualResetFallback is not reported."); @@ -525,11 +525,11 @@ Future main([List? args]) async { realmB.write(() => realmB.add(Task(task3Id))); - await triggerClientReset(realmA); + await baasHelper!.triggerClientReset(realmA); await realmA.syncSession.waitForUpload(); await afterRecoverCompleterA.future.wait(defaultWaitTimeout, "onAfterReset for realmA is not reported."); - await triggerClientReset(realmB, restartSession: false); + await baasHelper!.triggerClientReset(realmB, restartSession: false); realmB.syncSession.resume(); await realmB.syncSession.waitForUpload(); await afterRecoverCompleterB.future.wait(defaultWaitTimeout, "onAfterReset for realmB is not reported."); @@ -558,7 +558,7 @@ Future main([List? args]) async { final realm = await getRealmAsync(config); await realm.syncSession.waitForUpload(); - await triggerClientReset(realm); + await baasHelper!.triggerClientReset(realm); await resetCompleter.future.wait(defaultWaitTimeout, "ClientResetError is not reported."); expect(clientResetError.message, isNotEmpty); @@ -639,54 +639,8 @@ class Creator { } } -Future triggerClientReset(Realm realm, {bool restartSession = true}) async { - final config = realm.config; - if (config is! FlexibleSyncConfiguration) { - throw RealmError('This should only be invoked for sync realms'); - } - - final session = realm.syncSession; - if (restartSession) { - session.pause(); - } - - final userId = config.user.id; - final appId = baasApps.values.firstWhere((element) => element.clientAppId == config.user.app.id).appId; - - for (var i = 0; i < 5; i++) { - try { - final result = await config.user.functions.call('triggerClientResetOnSyncServer', [userId, appId]) as Map; - expect(result['status'], 'success'); - break; - } catch (e) { - if (i == 4) { - rethrow; - } - - print('Failed to trigger client reset: $e'); - await Future.delayed(Duration(seconds: i)); - } - } - - if (restartSession) { - session.resume(); - } -} - extension on Future { Future wait(Duration duration, [String message = "Timeout waiting a future to complete."]) { return timeout(duration, onTimeout: () => throw TimeoutException(message)); } } - -Future disableAutoRecoveryForApp(AppNames appName) async { - final client = baasClient ?? (throw StateError("No BAAS client")); - final baasAppName = baasApps[appName.name]!.name; - await client.setAutomaticRecoveryEnabled(baasAppName, false); -} - -Future enableAutoRecoveryForApp(AppNames appName) async { - final client = baasClient ?? (throw StateError("No BAAS client")); - final baasAppName = baasApps[appName.name]!.name; - await client.setAutomaticRecoveryEnabled(baasAppName, true); -} diff --git a/test/configuration_test.dart b/test/configuration_test.dart index 94a3884b3..a507700d0 100644 --- a/test/configuration_test.dart +++ b/test/configuration_test.dart @@ -100,9 +100,9 @@ Future main([List? args]) async { var customDefaultRealmPath = path.join((await Directory.systemTemp.createTemp()).path, Configuration.defaultRealmName); Configuration.defaultRealmPath = customDefaultRealmPath; - final appClientId = baasApps[AppNames.flexible.name]!.clientAppId; - final baasUrl = arguments[argBaasUrl]; - var appConfig = AppConfiguration(appClientId, baseUrl: Uri.parse(baasUrl!)); + final appClientId = baasHelper!.getClientAppId(appName: AppNames.flexible); + final baasUrl = baasHelper!.baseUrl; + var appConfig = AppConfiguration(appClientId, baseUrl: Uri.parse(baasUrl)); expect(appConfig.baseFilePath.path, path.dirname(customDefaultRealmPath)); var app = App(appConfig); diff --git a/test/indexed_test.dart b/test/indexed_test.dart index d92ff818f..9232470ca 100644 --- a/test/indexed_test.dart +++ b/test/indexed_test.dart @@ -87,8 +87,8 @@ const String lordOfTheFlies = 'Lord of the Flies'; const String wheelOfTime = 'The Wheel of Time'; const String silmarillion = 'The Silmarillion'; -void main([List? args]) { - setupTests(args); +void main([List? args]) async { + await setupTests(args); intFactory(int i) => i.hashCode; boolFactory(int i) => i % 2 == 0; @@ -98,13 +98,17 @@ void main([List? args]) { uuidFactory(int i) => Uuid.fromBytes(Uint8List(16).buffer..asByteData().setInt64(0, i.hashCode)); // skip timestamp for now, as timestamps are not indexed properly it seems - final indexedTestData = [('anInt', intFactory), ('string', stringFactory), ('objectId', objectIdFactory), ('uuid', uuidFactory)]; + final indexedTestData = [ + (name: 'anInt', factory: intFactory), + (name: 'string', factory: stringFactory), + (name: 'objectId', factory: objectIdFactory), + (name: 'uuid', factory: uuidFactory) + ]; for (final testCase in indexedTestData) { - test('Indexed faster: ${testCase.$1}', () { + test('Indexed faster: ${testCase.name}', () { final config = Configuration.local([WithIndexes.schema, NoIndexes.schema]); - Realm.deleteRealm(config.path); - final realm = Realm(config); + final realm = getRealm(config); const max = 100000; final allIndexed = realm.all(); final allNotIndexed = realm.all(); @@ -142,11 +146,11 @@ void main([List? args]) { expect(allNotIndexed.length, max); // Inefficient, but fast enough for this test - final searchOrder = (List.generate(max, (i) => i)..shuffle(Random(42))).map((i) => testCase.$2(i)).take(1000).toList(); + final searchOrder = (List.generate(max, (i) => i)..shuffle(Random(42))).map((i) => testCase.factory(i)).take(1000).toList(); @pragma('vm:no-interrupts') Duration measureSpeed(RealmResults results) { - final queries = searchOrder.map((v) => results.query('${testCase.$1} == \$0', [v])).toList(); // pre-calculate queries + final queries = searchOrder.map((v) => results.query('${testCase.name} == \$0', [v])).toList(); // pre-calculate queries final found = []; final sw = Stopwatch()..start(); @@ -166,7 +170,7 @@ void main([List? args]) { final lookupCount = searchOrder.length; display(Type type, Duration duration) { - print('$lookupCount lookups of ${'$type'.padRight(12)} on ${testCase.$1.padRight(10)} : ${duration.inMicroseconds ~/ lookupCount} us/lookup'); + print('$lookupCount lookups of ${'$type'.padRight(12)} on ${testCase.name.padRight(10)} : ${duration.inMicroseconds ~/ lookupCount} us/lookup'); } final indexedTime = measureSpeed(allIndexed); diff --git a/test/list_test.dart b/test/list_test.dart index f93dab8a7..d808c0e41 100644 --- a/test/list_test.dart +++ b/test/list_test.dart @@ -349,6 +349,7 @@ Future main([List? args]) async { op(list, i); } }); + realm.refresh(); } }); } diff --git a/test/test.dart b/test/test.dart index 51d8185ac..6661b67e8 100644 --- a/test/test.dart +++ b/test/test.dart @@ -27,12 +27,14 @@ import 'package:meta/meta.dart'; import 'package:path/path.dart' as _path; import 'package:test/test.dart' hide test; import 'package:test/test.dart' as testing; -import 'package:args/args.dart'; import '../lib/realm.dart'; -import '../lib/src/cli/atlas_apps/baas_client.dart'; import '../lib/src/native/realm_core.dart'; import '../lib/src/configuration.dart'; +import 'baas_helper.dart'; + +export 'baas_helper.dart' show AppNames; + part 'test.g.dart'; @RealmModel() @@ -352,39 +354,12 @@ class _Symmetric { } String? testName; -Map arguments = {}; -final baasApps = {}; final _openRealms = Queue(); -const String argBaasUrl = "BAAS_URL"; -const String argBaasCluster = "BAAS_CLUSTER"; -const String argBaasApiKey = "BAAS_API_KEY"; -const String argBaasPrivateApiKey = "BAAS_PRIVATE_API_KEY"; -const String argBaasProjectId = "BAAS_PROJECT_ID"; -const String argDifferentiator = "BAAS_DIFFERENTIATOR"; String testUsername = "realm-test@realm.io"; String testPassword = "123456"; -const String publicRSAKeyForJWTValidation = '''-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvNHHs8T0AHD7SJ+CKvVR -leeJa4wqYTnaVYV+5bX9FmFXVoN+vHbMLEteMvSw4L3kSRZdcqxY7cTuhlpAvkXP -Yq6qSI+bW8T4jGW963uCc83UhVMx4MH/PzipAlfcPjVO2u4c+dmpgZQpgEmA467u -tauXUhmTsGpgNg2Gvc61B7Ny4LphshsyrfaJ9WjA/NM6LOmEBW3JPNcVG2qyU+gt -O8BM8KOSx9wGyoGs4+OusvRkJizhPaIwa3FInLs4r+xZW9Bp6RndsmVECtvXRv5d -87ztpg6o3DZJRmTp2lAnkNLmxXlFkOSNIwiT3qqyRZOh4DuxPOpfg9K+vtFmRdEJ -RwIDAQAB ------END PUBLIC KEY-----'''; final int encryptionKeySize = 64; -enum AppNames { - flexible, - - // For application with name 'autoConfirm' and with confirmationType = 'auto' - // all the usernames are automatically confirmed. - autoConfirm, - - emailConfirm, -} - const int maxInt = 9223372036854775807; const int minInt = -9223372036854775808; const int jsMaxInt = 9007199254740991; @@ -412,11 +387,12 @@ void xtest(String? name, dynamic Function() testFunction, {dynamic skip, Map setupTests(List? args) async { - arguments = parseTestArguments(args); - testName = arguments["name"]; +BaasHelper? baasHelper; - setUpAll(() async => await (_baasSetupResult ??= setupBaas())); +Future setupTests(List? args) async { + final testArgs = parseTestArguments(args); + testName = testArgs["name"]; + baasHelper = await BaasHelper.setupBaas(testArgs); setUp(() { Realm.logger = Logger.detached('test run') @@ -568,93 +544,6 @@ Future tryDeleteRealm(String path) async { // throw Exception('Failed to delete realm at path $path. Did you forget to close it?'); } -Map parseTestArguments(List? arguments) { - Map testArgs = {}; - final parser = ArgParser() - ..addOption("name") - ..addOption(argBaasUrl) - ..addOption(argBaasCluster) - ..addOption(argBaasApiKey) - ..addOption(argBaasPrivateApiKey) - ..addOption(argBaasProjectId) - ..addOption(argDifferentiator); - - final result = parser.parse(arguments ?? []); - testArgs - ..addArgument(result, "name") - ..addArgument(result, argBaasUrl) - ..addArgument(result, argBaasCluster) - ..addArgument(result, argBaasApiKey) - ..addArgument(result, argBaasPrivateApiKey) - ..addArgument(result, argBaasProjectId) - ..addArgument(result, argDifferentiator); - - return testArgs; -} - -extension on Map { - void addArgument(ArgResults parsedResult, String argName) { - final value = parsedResult.wasParsed(argName) ? parsedResult[argName]?.toString() : Platform.environment[argName]; - if (value != null && value.isNotEmpty) { - this[argName] = value; - } - } -} - -BaasClient? baasClient; -Future? _baasSetupResult; - -Future setupBaas() async { - if (_baasSetupResult != null) { - return _baasSetupResult!; - } - - try { - final baasUrl = arguments[argBaasUrl]; - if (baasUrl == null) { - return true; - } - - final cluster = arguments[argBaasCluster]; - final apiKey = arguments[argBaasApiKey]; - final privateApiKey = arguments[argBaasPrivateApiKey]; - final projectId = arguments[argBaasProjectId]; - final differentiator = arguments[argDifferentiator]; - - final client = await (cluster == null - ? BaasClient.docker(baasUrl, differentiator) - : BaasClient.atlas(baasUrl, cluster, apiKey!, privateApiKey!, projectId!, differentiator)); - - client.publicRSAKey = publicRSAKeyForJWTValidation; - - final apps = await client.getOrCreateApps(); - baasApps.addAll(apps); - baasClient = client; - - await _waitForInitialSync(); - return true; - } catch (error) { - print(error); - return error; - } -} - -Future _waitForInitialSync() async { - while (true) { - try { - print('Validating initial sync is complete...'); - await baasClient!.waitForInitialSync(baasApps[AppNames.flexible.name]!); - final realm = await getIntegrationRealm(); - await realm.syncSession.waitForUpload(); - await baasClient!.waitForInitialSync(baasApps[AppNames.flexible.name]!); - return; - } catch (e) { - print(e); - await _waitForInitialSync(); - } - } -} - @isTest Future baasTest( String name, @@ -662,53 +551,27 @@ Future baasTest( AppNames appName = AppNames.flexible, dynamic skip, }) async { - if (_baasSetupResult is Error) { - throw _baasSetupResult!; - } + baasHelper?.throwIfSetupFailed(); - final baasUri = arguments[argBaasUrl]; - skip = shouldSkip(baasUri, skip); + skip = shouldSkip(skip); test(name, () async { - printSplunkLogLink(appName, baasUri); - final config = await getAppConfig(appName: appName); + baasHelper!.printSplunkLogLink(appName, baasHelper?.baseUrl); + final config = await baasHelper!.getAppConfig(appName: appName); await testFunction(config); }, skip: skip); } -dynamic shouldSkip(String? baasUri, dynamic skip) { - final url = baasUri != null ? Uri.tryParse(baasUri) : null; - +dynamic shouldSkip(dynamic skip) { if (skip == null) { - skip = url == null ? "BAAS URL not present" : false; + skip = baasHelper == null ? "BAAS URL not present" : false; } else if (skip is bool) { - if (url == null) skip = "BAAS URL not present"; + if (baasHelper == null) skip = "BAAS URL not present"; } return skip; } -Future getAppConfig({AppNames appName = AppNames.flexible}) => _getAppConfig(appName.name); - -Future _getAppConfig(String appName) async { - final baasUrl = arguments[argBaasUrl]; - - final app = - baasApps[appName] ?? baasApps.values.firstWhere((element) => element.name == BaasClient.defaultAppName, orElse: () => throw RealmError("No BAAS apps")); - if (app.error != null) { - throw app.error!; - } - - final temporaryDir = await Directory.systemTemp.createTemp('realm_test_'); - return AppConfiguration( - app.clientAppId, - baseUrl: Uri.parse(baasUrl!), - baseFilePath: temporaryDir, - maxConnectionTimeout: Duration(minutes: 10), - defaultRequestTimeout: Duration(minutes: 7), - ); -} - Future getIntegrationUser(App app) async { final email = 'realm_tests_do_autoverify_${generateRandomEmail()}'; final password = 'password'; @@ -721,14 +584,8 @@ Future getAnonymousUser(App app) { return app.logIn(Credentials.anonymous(reuseCredentials: false)); } -Future createServerApiKey(App app, String name, {bool enabled = true}) async { - final baasApp = baasApps.values.firstWhere((ba) => ba.clientAppId == app.id); - final client = baasClient ?? (throw StateError("No BAAS client")); - return await client.createApiKey(baasApp, name, enabled); -} - Future getIntegrationRealm({App? app, ObjectId? differentiator, AppConfiguration? appConfig}) async { - app ??= App(appConfig ?? await getAppConfig()); + app ??= App(appConfig ?? await baasHelper!.getAppConfig()); final user = await getIntegrationUser(app); final config = Configuration.flexibleSync(user, getSyncSchema())..sessionStopPolicy = SessionStopPolicy.immediately; @@ -842,22 +699,6 @@ extension StreamEx on Stream> { } } -void printSplunkLogLink(AppNames appName, String? uriVariable) { - if (uriVariable == null) { - return; - } - - final app = baasApps[appName.name] ?? - baasApps.values.firstWhere((element) => element.name == BaasClient.defaultAppName, orElse: () => throw RealmError("No BAAS apps")); - final baasUri = Uri.parse(uriVariable); - - testing.printOnFailure("App service name: ${app.uniqueName}"); - final host = baasUri.host.endsWith('-qa.mongodb.com') ? "-qa" : ""; - final splunk = Uri.encodeFull( - "https://splunk.corp.mongodb.com/en-US/app/search/search?q=search index=baas$host \"${app.uniqueName}-*\" | reverse | top error msg&earliest=-7d&latest=now&display.general.type=visualizations"); - testing.printOnFailure("Splunk logs: $splunk"); -} - /// Schema list for default app service /// used for all the flexible sync tests. /// The full list of schemas is required when creating diff --git a/test/user_test.dart b/test/user_test.dart index 0980af261..9fd98b375 100644 --- a/test/user_test.dart +++ b/test/user_test.dart @@ -441,7 +441,7 @@ Future main([List? args]) async { baasTest("Credentials.apiKey with server-generated can login user", (configuration) async { final app = App(configuration); - final apiKey = await createServerApiKey(app, ObjectId().toString()); + final apiKey = await baasHelper!.createServerApiKey(app, ObjectId().toString()); final credentials = Credentials.apiKey(apiKey); final apiKeyUser = await app.logIn(credentials); @@ -452,7 +452,7 @@ Future main([List? args]) async { baasTest("Credentials.apiKey with disabled server api key throws an error", (configuration) async { final app = App(configuration); - final apiKey = await createServerApiKey(app, ObjectId().toString(), enabled: false); + final apiKey = await baasHelper!.createServerApiKey(app, ObjectId().toString(), enabled: false); final credentials = Credentials.apiKey(apiKey); await expectLater( From 08b23e406152851eef6a6c1eee3e67379f54a51d Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 1 Dec 2023 03:50:47 +0100 Subject: [PATCH 02/27] Remove dependency on realm in the baas client --- lib/src/cli/atlas_apps/baas_client.dart | 78 ++++++++++++++++++------- 1 file changed, 57 insertions(+), 21 deletions(-) diff --git a/lib/src/cli/atlas_apps/baas_client.dart b/lib/src/cli/atlas_apps/baas_client.dart index 0aa0301d1..eabb7e147 100644 --- a/lib/src/cli/atlas_apps/baas_client.dart +++ b/lib/src/cli/atlas_apps/baas_client.dart @@ -18,9 +18,34 @@ import 'package:collection/collection.dart'; import 'package:http/http.dart' as http; -import 'package:realm_dart/src/app.dart'; import 'dart:convert'; -import '../../realm_dart.dart'; + +class BaasAuthHelper { + static const String _appId = 'baas-container-service-autzb'; + final String _accessToken; + final String _location; + + BaasAuthHelper._(this._accessToken, this._location); + + static Future create() async { + final locationResponse = await http.get(Uri.parse('https://realm.mongodb.com/api/client/v2.0/app/$_appId/location')); + final locationJson = BaasClient._decodeResponse(locationResponse) as Map; + final location = locationJson['hostname'] as String; + + final loginResponse = await http.post(Uri.parse('$location/api/client/v2.0/app/$_appId/auth/providers/anon-user/login')); + final loginJson = BaasClient._decodeResponse(loginResponse) as Map; + final accessToken = loginJson['access_token'] as String; + + return BaasAuthHelper._(accessToken, location); + } + + Future callFunction(String name) async { + final response = await http.post(Uri.parse('$_location/api/client/v2.0/app/$_appId/functions/call'), + headers: {'Authorization': 'Bearer $_accessToken'}, body: jsonEncode({'name': name, 'arguments': []})); + + return BaasClient._decodeResponse(response); + } +} class BaasClient { static const String _confirmFuncSource = '''exports = async ({ token, tokenId, username }) => { @@ -102,7 +127,7 @@ class BaasClient { BaasClient._(this.baseUrl, String? differentiator, [this._clusterName]) : _adminApiUrl = '$baseUrl/api/admin/v3.0', _headers = {'Accept': 'application/json'}, - _appSuffix = '-${shortenDifferentiator(differentiator ?? 'local')}-$_clusterName'; + _appSuffix = '-${shortenDifferentiator(differentiator ?? 'local')}${_clusterName == null ? '' : '-$_clusterName'}'; /// A client that imports apps in a MongoDB Atlas docker image. See https://github.com/realm/ci/tree/master/realm/docker/mongodb-realm /// for instructions on how to set it up. @@ -121,28 +146,39 @@ class BaasClient { } static Future deployContainer() async { - print('Deploying new BaaS container... '); - - final appId = 'baas-container-service-autzb'; - final app = App(AppConfiguration(appId)); - final user = await app.logIn(Credentials.anonymous(reuseCredentials: false)); - final response = await user.functions.call('startContainer') as Map; - final taskId = response['taskId'] as String; - - String? httpUrl; - while (httpUrl == null) { - await Future.delayed(Duration(seconds: 1)); - httpUrl = await _waitForContainer(user, taskId); - } + for (var i = 0; i < 5; i++) { + try { + print('Deploying new BaaS container... '); + + final authHelper = await BaasAuthHelper.create(); + + final response = await authHelper.callFunction('startContainer') as Map; + final taskId = response['taskId'] as String; + + String? httpUrl; + while (httpUrl == null) { + await Future.delayed(Duration(seconds: 1)); + httpUrl = await _waitForContainer(authHelper, taskId); + } + + print('Deployed BaaS instance at $httpUrl'); - print('Deployed BaaS instance at $httpUrl'); + return httpUrl; + } catch (e) { + if (i == 4) { + rethrow; + } + + print('Failed to deploy container: $e'); + } + } - return httpUrl; + throw 'UNREACHABLE'; } - static Future _waitForContainer(User user, String taskId) async { + static Future _waitForContainer(BaasAuthHelper authHelper, String taskId) async { try { - final containers = await user.functions.call('listContainers') as List; + final containers = await authHelper.callFunction('listContainers') as List; final targetContainer = containers.firstWhereOrNull((c) => c['id'] == taskId); if (targetContainer == null) { print('$taskId is not found in container list. Retrying...'); @@ -608,7 +644,7 @@ class BaasClient { return _decodeResponse(response, payload); } - dynamic _decodeResponse(http.Response response, [String? payload]) { + static dynamic _decodeResponse(http.Response response, [String? payload]) { if (response.statusCode > 399 || response.statusCode < 200) { throw Exception('Failed to ${response.request?.method} ${response.request?.url}: ${response.statusCode} ${response.body}. Body: $payload'); } From c4613583a4af59b5400457265a04a80b3ef362ed Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 1 Dec 2023 04:09:14 +0100 Subject: [PATCH 03/27] Regenerate models --- test/baas_helper.g.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/test/baas_helper.g.dart b/test/baas_helper.g.dart index 4c20b0e19..fc9a7d6f4 100644 --- a/test/baas_helper.g.dart +++ b/test/baas_helper.g.dart @@ -100,7 +100,6 @@ class BaasInfo extends _BaasInfo } } -// ignore_for_file: type=lint class BaasAppDetails extends _BaasAppDetails with RealmEntity, RealmObjectBase, EmbeddedObject { BaasAppDetails( From 56d8199a4f3c20bd04871dd5a08931e658fa906a Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 1 Dec 2023 04:32:18 +0100 Subject: [PATCH 04/27] Cleanup containers after test runs --- .github/workflows/ci.yml | 80 +++++++++++++++++++ .github/workflows/deploy-baas.yml | 16 +++- .github/workflows/terminate-baas.yml | 34 ++++++++ lib/src/cli/atlas_apps/baas_client.dart | 29 ++++++- .../cli/atlas_apps/deleteapps_command.dart | 19 +++-- .../cli/atlas_apps/deployapps_command.dart | 9 ++- lib/src/cli/atlas_apps/options.dart | 5 +- lib/src/cli/atlas_apps/options.g.dart | 6 ++ test/baas_helper.dart | 2 +- 9 files changed, 179 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/terminate-baas.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43cd76f9c..96e85329e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,6 +85,15 @@ jobs: runner: windows-latest baasurl: ${{ needs.deploy-cluster-dart-windows.outputs.url }} + cleanup-cluster-dart-windows: + name: Cleanup Cluster for Dart Windows + uses: ./.github/workflows/terminate-baas.yml + needs: + - dart-tests-windows + - deploy-cluster-dart-windows + with: + containerId: ${{ needs.deploy-cluster-dart-windows.outputs.containerId }} + deploy-cluster-dart-macos: name: Deploy Cluster for Dart MacOS uses: ./.github/workflows/deploy-baas.yml @@ -101,6 +110,15 @@ jobs: runner: macos-latest baasurl: ${{ needs.deploy-cluster-dart-macos.outputs.url }} + cleanup-cluster-dart-macos: + name: Cleanup Cluster for Dart macOS + uses: ./.github/workflows/terminate-baas.yml + needs: + - dart-tests-macos + - deploy-cluster-dart-macos + with: + containerId: ${{ needs.deploy-cluster-dart-macos.outputs.containerId }} + deploy-cluster-dart-macos-arm: name: Deploy Cluster for Dart MacOS Arm uses: ./.github/workflows/deploy-baas.yml @@ -118,6 +136,15 @@ jobs: architecture: arm baasurl: ${{ needs.deploy-cluster-dart-macos-arm.outputs.url }} + cleanup-cluster-dart-macos-arm: + name: Cleanup Cluster for Dart macOS Arm + uses: ./.github/workflows/terminate-baas.yml + needs: + - dart-tests-macos-arm + - deploy-cluster-dart-macos-arm + with: + containerId: ${{ needs.deploy-cluster-dart-macos-arm.outputs.containerId }} + deploy-cluster-dart-linux: name: Deploy Cluster for Dart Linux uses: ./.github/workflows/deploy-baas.yml @@ -134,6 +161,14 @@ jobs: runner: ubuntu-latest baasurl: ${{ needs.deploy-cluster-dart-linux.outputs.url }} + cleanup-cluster-dart-linux: + name: Cleanup Cluster for Dart Linux + uses: ./.github/workflows/terminate-baas.yml + needs: + - dart-tests-linux + - deploy-cluster-dart-linux + with: + containerId: ${{ needs.deploy-cluster-dart-linux.outputs.containerId }} # Flutter jobs deploy-cluster-flutter-windows: @@ -152,6 +187,15 @@ jobs: runner: windows-latest baasurl: ${{ needs.deploy-cluster-flutter-windows.outputs.url }} + cleanup-cluster-flutter-windows: + name: Cleanup Cluster for Flutter Windows + uses: ./.github/workflows/terminate-baas.yml + needs: + - flutter-tests-windows + - deploy-cluster-flutter-windows + with: + containerId: ${{ needs.deploy-cluster-flutter-windows.outputs.containerId }} + deploy-cluster-flutter-macos: name: Deploy Cluster for Flutter MacOS uses: ./.github/workflows/deploy-baas.yml @@ -168,6 +212,15 @@ jobs: runner: macos-13 # workaround to: https://github.com/flutter/flutter/issues/118469 latest is still macos-12 ¯\_(ツ)_/¯ baasurl: ${{ needs.deploy-cluster-flutter-macos.outputs.url }} + cleanup-cluster-flutter-macos: + name: Cleanup Cluster for Flutter macOS + uses: ./.github/workflows/terminate-baas.yml + needs: + - flutter-tests-macos + - deploy-cluster-flutter-macos + with: + containerId: ${{ needs.deploy-cluster-flutter-macos.outputs.containerId }} + deploy-cluster-flutter-linux: name: Deploy Cluster for Flutter Linux uses: ./.github/workflows/deploy-baas.yml @@ -184,6 +237,15 @@ jobs: runner: ubuntu-latest baasurl: ${{ needs.deploy-cluster-flutter-linux.outputs.url }} + cleanup-cluster-flutter-linux: + name: Cleanup Cluster for Flutter Linux + uses: ./.github/workflows/terminate-baas.yml + needs: + - flutter-tests-linux + - deploy-cluster-flutter-linux + with: + containerId: ${{ needs.deploy-cluster-flutter-linux.outputs.containerId }} + deploy-cluster-flutter-ios: name: Deploy Cluster for Flutter iOS uses: ./.github/workflows/deploy-baas.yml @@ -245,6 +307,15 @@ jobs: flutter drive --target=test_driver/app.dart --dart-define=testName="" --suppress-analytics --debug working-directory: ./flutter/realm_flutter/tests + cleanup-cluster-flutter-ios: + name: Cleanup Cluster for Flutter iOS + uses: ./.github/workflows/terminate-baas.yml + needs: + - flutter-tests-ios + - deploy-cluster-flutter-ios + with: + containerId: ${{ needs.deploy-cluster-flutter-ios.outputs.containerId }} + deploy-cluster-flutter-android: name: Deploy Cluster for Flutter Android uses: ./.github/workflows/deploy-baas.yml @@ -333,6 +404,15 @@ jobs: script: flutter build apk --debug --target=test_driver/app.dart && flutter install --debug && flutter drive --target=test_driver/app.dart --dart-define=testName="" --suppress-analytics --debug working-directory: ./flutter/realm_flutter/tests + cleanup-cluster-flutter-android: + name: Cleanup Cluster for Flutter Android + uses: ./.github/workflows/terminate-baas.yml + needs: + - flutter-tests-android + - deploy-cluster-flutter-android + with: + containerId: ${{ needs.deploy-cluster-flutter-android.outputs.containerId }} + # Generator jobs generator: diff --git a/.github/workflows/deploy-baas.yml b/.github/workflows/deploy-baas.yml index 596072294..510faffb2 100644 --- a/.github/workflows/deploy-baas.yml +++ b/.github/workflows/deploy-baas.yml @@ -1,11 +1,14 @@ -name: Deploy cluster and apps +name: Deploy BaaS and apps on: workflow_call: outputs: url: - description: "The url to connect to" + description: The url to connect to value: ${{ jobs.deploy-baas.outputs.url }} + containerId: + description: The container id of the BaaS instance + value: ${{ jobs.deploy-baas.outputs.containerId }} env: REALM_CI: true @@ -17,6 +20,7 @@ jobs: timeout-minutes: 15 outputs: url: ${{ steps.baas-url.outputs.content }} + containerId: ${{ steps.container-id.outputs.content }} steps: - name: Checkout uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 @@ -39,4 +43,10 @@ jobs: id: baas-url uses: jaywcjlove/github-action-read-file@3e97449450678b461303d4820406de059f4830ea with: - path: baasUrl.txt + path: baasUrl + + - name: Read container id + id: container-id + uses: jaywcjlove/github-action-read-file@3e97449450678b461303d4820406de059f4830ea + with: + path: containerId diff --git a/.github/workflows/terminate-baas.yml b/.github/workflows/terminate-baas.yml new file mode 100644 index 000000000..ecc47df38 --- /dev/null +++ b/.github/workflows/terminate-baas.yml @@ -0,0 +1,34 @@ +name: Terminate BaaS + +on: + workflow_call: + inputs: + containerId: + description: "The url to connect to" + type: string + +env: + REALM_CI: true + +jobs: + terminate-baas: + runs-on: ubuntu-latest + name: Terminate BaaS + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + with: + submodules: false + + - name : Setup Dart SDK + uses: dart-lang/setup-dart@main + with: + sdk: stable + architecture: 'x64' + + - name: Install dependencies + run: dart pub get + + - name: Terminate baas + run: dart run realm_dart delete-apps --use-baas-aas --container-id ${{ inputs.containerId }} diff --git a/lib/src/cli/atlas_apps/baas_client.dart b/lib/src/cli/atlas_apps/baas_client.dart index eabb7e147..17b64d475 100644 --- a/lib/src/cli/atlas_apps/baas_client.dart +++ b/lib/src/cli/atlas_apps/baas_client.dart @@ -39,9 +39,9 @@ class BaasAuthHelper { return BaasAuthHelper._(accessToken, location); } - Future callFunction(String name) async { + Future callFunction(String name, {List arguments = const []}) async { final response = await http.post(Uri.parse('$_location/api/client/v2.0/app/$_appId/functions/call'), - headers: {'Authorization': 'Bearer $_accessToken'}, body: jsonEncode({'name': name, 'arguments': []})); + headers: {'Authorization': 'Bearer $_accessToken'}, body: jsonEncode({'name': name, 'arguments': arguments})); return BaasClient._decodeResponse(response); } @@ -145,7 +145,28 @@ class BaasClient { return result; } - static Future deployContainer() async { + static Future deleteContainer(String id) async { + for (var i = 0; i < 5; i++) { + try { + print('Deleting BaaS container $id... '); + + final authHelper = await BaasAuthHelper.create(); + + await authHelper.callFunction('stopContainer', arguments: [id]); + return; + } catch (e) { + if (i == 4) { + rethrow; + } + + print('Failed to deploy container: $e'); + } + } + + throw 'UNREACHABLE'; + } + + static Future<(String httpUrl, String containerId)> deployContainer() async { for (var i = 0; i < 5; i++) { try { print('Deploying new BaaS container... '); @@ -163,7 +184,7 @@ class BaasClient { print('Deployed BaaS instance at $httpUrl'); - return httpUrl; + return (httpUrl, taskId); } catch (e) { if (i == 4) { rethrow; diff --git a/lib/src/cli/atlas_apps/deleteapps_command.dart b/lib/src/cli/atlas_apps/deleteapps_command.dart index a7cebc820..d6d23afa4 100644 --- a/lib/src/cli/atlas_apps/deleteapps_command.dart +++ b/lib/src/cli/atlas_apps/deleteapps_command.dart @@ -62,17 +62,20 @@ class DeleteAppsCommand extends Command { } if (options.useBaaSaaS) { - print('Deleting apps from BaaSaaS container is not supported or necessary'); - return; - } + if (options.containerId == null) { + abort('--container-id must be supplied when --use-baas-aas is set'); + } - final differentiator = options.differentiator ?? 'local'; + await BaasClient.deleteContainer(options.containerId!); + } else { + final differentiator = options.differentiator ?? 'local'; - final client = await (options.atlasCluster == null - ? BaasClient.docker(options.baasUrl!, differentiator) - : BaasClient.atlas(options.baasUrl!, options.atlasCluster!, options.apiKey!, options.privateApiKey!, options.projectId!, differentiator)); + final client = await (options.atlasCluster == null + ? BaasClient.docker(options.baasUrl!, differentiator) + : BaasClient.atlas(options.baasUrl!, options.atlasCluster!, options.apiKey!, options.privateApiKey!, options.projectId!, differentiator)); - await client.deleteApps(); + await client.deleteApps(); + } } void abort(String error) { diff --git a/lib/src/cli/atlas_apps/deployapps_command.dart b/lib/src/cli/atlas_apps/deployapps_command.dart index db62a1f53..744963f51 100644 --- a/lib/src/cli/atlas_apps/deployapps_command.dart +++ b/lib/src/cli/atlas_apps/deployapps_command.dart @@ -78,10 +78,11 @@ RwIDAQAB late String baasUrl; if (options.useBaaSaaS) { - baasUrl = await BaasClient.deployContainer(); - final file = File('baasUrl.txt'); - await file.writeAsString(baasUrl); - print('BaasUrl: $baasUrl. Written to ${file.path}'); + late String containerId; + (baasUrl, containerId) = await BaasClient.deployContainer(); + await File('baasUrl').writeAsString(baasUrl); + await File('containerId').writeAsString(containerId); + print('BaasUrl: $baasUrl'); } else { baasUrl = options.baasUrl!; } diff --git a/lib/src/cli/atlas_apps/options.dart b/lib/src/cli/atlas_apps/options.dart index 3485dd306..34fd4522f 100644 --- a/lib/src/cli/atlas_apps/options.dart +++ b/lib/src/cli/atlas_apps/options.dart @@ -43,7 +43,10 @@ class Options { @CliOption(help: 'Spawn a new container for BaaSaaS and creates app in it.', name: 'use-baas-aas') final bool useBaaSaaS; - Options({this.baasUrl, this.atlasCluster, this.apiKey, this.privateApiKey, this.projectId, this.differentiator, this.useBaaSaaS = false}); + @CliOption(help: 'Container id to be cleaned up. Only to be used with use-baas-aas and delete command') + final String? containerId; + + Options({this.baasUrl, this.atlasCluster, this.apiKey, this.privateApiKey, this.projectId, this.differentiator, this.useBaaSaaS = false, this.containerId}); } String get usage => _$parserForOptions.usage; diff --git a/lib/src/cli/atlas_apps/options.g.dart b/lib/src/cli/atlas_apps/options.g.dart index 779e7d3a9..e87e11b05 100644 --- a/lib/src/cli/atlas_apps/options.g.dart +++ b/lib/src/cli/atlas_apps/options.g.dart @@ -14,6 +14,7 @@ Options _$parseOptionsResult(ArgResults result) => Options( projectId: result['project-id'] as String?, differentiator: result['differentiator'] as String?, useBaaSaaS: result['use-baas-aas'] as bool, + containerId: result['container-id'] as String?, ); ArgParser _$populateOptionsParser(ArgParser parser) => parser @@ -47,6 +48,11 @@ ArgParser _$populateOptionsParser(ArgParser parser) => parser ..addFlag( 'use-baas-aas', help: 'Spawn a new container for BaaSaaS and creates app in it.', + ) + ..addOption( + 'container-id', + help: + 'Container id to be cleaned up. Only to be used with use-baas-aas and delete command', ); final _$parserForOptions = _$populateOptionsParser(ArgParser()); diff --git a/test/baas_helper.dart b/test/baas_helper.dart index b890db623..d77e01f26 100644 --- a/test/baas_helper.dart +++ b/test/baas_helper.dart @@ -111,7 +111,7 @@ class BaasHelper { throw _error!; } - final realmPath = _path.join(Directory.current.path, 'baasmeta', 'baas_$pid.realm'); + final realmPath = _path.join(Configuration.defaultStoragePath, 'baasmeta', 'baas_$pid.realm'); final realm = Realm(Configuration.local([BaasInfo.schema, BaasAppDetails.schema], path: realmPath)); final (client, baasInfo) = await _setupClient(args, realm); if (client == null || baasInfo == null) { From dac4274a38965236385dadfe09f5c86b86cbc833 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 1 Dec 2023 04:37:19 +0100 Subject: [PATCH 05/27] Fix CI --- test/baas_helper.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/baas_helper.dart b/test/baas_helper.dart index d77e01f26..68811478d 100644 --- a/test/baas_helper.dart +++ b/test/baas_helper.dart @@ -138,7 +138,7 @@ class BaasHelper { throw "$argUseBaaSaaS can't be combined with $argBaasCluster"; } - baasUrl = await BaasClient.deployContainer(); + (baasUrl, _) = await BaasClient.deployContainer(); } else { baasUrl = args[argBaasUrl]; } From 81f19a07922cc78144e29b7fe3d0bd4408efcbb8 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 1 Dec 2023 05:16:50 +0100 Subject: [PATCH 06/27] Try setting the baas url correctly --- .github/workflows/deploy-baas.yml | 23 ++++++++----------- .../cli/atlas_apps/deployapps_command.dart | 4 ++-- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/.github/workflows/deploy-baas.yml b/.github/workflows/deploy-baas.yml index 510faffb2..c8a3f7f43 100644 --- a/.github/workflows/deploy-baas.yml +++ b/.github/workflows/deploy-baas.yml @@ -19,8 +19,8 @@ jobs: name: Deploy BaaS timeout-minutes: 15 outputs: - url: ${{ steps.baas-url.outputs.content }} - containerId: ${{ steps.container-id.outputs.content }} + url: ${{ steps.set-outputs.outputs.baasUrl }} + containerId: ${{ steps.set-outputs.outputs.containerId }} steps: - name: Checkout uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 @@ -38,15 +38,10 @@ jobs: - name: Deploy cluster and apps run: dart run realm_dart deploy-apps --use-baas-aas - - - name: Read baas url - id: baas-url - uses: jaywcjlove/github-action-read-file@3e97449450678b461303d4820406de059f4830ea - with: - path: baasUrl - - - name: Read container id - id: container-id - uses: jaywcjlove/github-action-read-file@3e97449450678b461303d4820406de059f4830ea - with: - path: containerId + - name: Set outputs + id: set-outputs + run: | + baas_url=`cat baasurl` + container_id=`cat containerid` + echo "baasUrl=$baas_url" >> "$GITHUB_OUTPUT" + echo "containerId=$container_id" >> "$GITHUB_OUTPUT" diff --git a/lib/src/cli/atlas_apps/deployapps_command.dart b/lib/src/cli/atlas_apps/deployapps_command.dart index 744963f51..888ce2306 100644 --- a/lib/src/cli/atlas_apps/deployapps_command.dart +++ b/lib/src/cli/atlas_apps/deployapps_command.dart @@ -80,8 +80,8 @@ RwIDAQAB if (options.useBaaSaaS) { late String containerId; (baasUrl, containerId) = await BaasClient.deployContainer(); - await File('baasUrl').writeAsString(baasUrl); - await File('containerId').writeAsString(containerId); + await File('baasurl').writeAsString(baasUrl); + await File('containerid').writeAsString(containerId); print('BaasUrl: $baasUrl'); } else { baasUrl = options.baasUrl!; From 180f85d42cf35037d7cb822034299767daaddf7c Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 1 Dec 2023 05:29:49 +0100 Subject: [PATCH 07/27] Fix order of test setup --- test/app_test.dart | 1 - test/baas_helper.dart | 6 +++--- test/test.dart | 3 ++- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/app_test.dart b/test/app_test.dart index 39efc98cd..924c7bcaa 100644 --- a/test/app_test.dart +++ b/test/app_test.dart @@ -28,7 +28,6 @@ import 'test.dart'; Future main([List? args]) async { await setupTests(args); - final a = 5; test('AppConfiguration can be initialized', () { Configuration.defaultRealmPath = path.join(Configuration.defaultStoragePath, Configuration.defaultRealmName); diff --git a/test/baas_helper.dart b/test/baas_helper.dart index 68811478d..2a4933e31 100644 --- a/test/baas_helper.dart +++ b/test/baas_helper.dart @@ -1,10 +1,8 @@ import 'dart:io'; -import 'dart:isolate'; import 'package:args/args.dart'; import 'package:path/path.dart' as _path; import 'package:test/test.dart' as testing; -import 'package:test/test.dart'; import '../lib/src/cli/atlas_apps/baas_client.dart'; import '../lib/realm.dart'; @@ -281,7 +279,9 @@ class BaasHelper { for (var i = 0; i < 5; i++) { try { final result = await config.user.functions.call('triggerClientResetOnSyncServer', [userId, appId]) as Map; - expect(result['status'], 'success'); + if (result['status'] != 'success') { + throw 'Unsuccesful status: ${result['status']}'; + } break; } catch (e) { if (i == 4) { diff --git a/test/test.dart b/test/test.dart index 6661b67e8..683c1c90a 100644 --- a/test/test.dart +++ b/test/test.dart @@ -392,7 +392,6 @@ BaasHelper? baasHelper; Future setupTests(List? args) async { final testArgs = parseTestArguments(args); testName = testArgs["name"]; - baasHelper = await BaasHelper.setupBaas(testArgs); setUp(() { Realm.logger = Logger.detached('test run') @@ -426,6 +425,8 @@ Future setupTests(List? args) async { }); }); + baasHelper = await BaasHelper.setupBaas(testArgs); + // Enable this to print platform info, including current PID await _printPlatformInfo(); } From a46bf37cc02089f350ae9e849333c71699f48b34 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 1 Dec 2023 05:56:55 +0100 Subject: [PATCH 08/27] .. --- lib/src/cli/atlas_apps/baas_client.dart | 58 +++++++++---------- .../cli/atlas_apps/deleteapps_command.dart | 2 +- test/baas_helper.dart | 12 +--- 3 files changed, 31 insertions(+), 41 deletions(-) diff --git a/lib/src/cli/atlas_apps/baas_client.dart b/lib/src/cli/atlas_apps/baas_client.dart index 17b64d475..7fdd6abc3 100644 --- a/lib/src/cli/atlas_apps/baas_client.dart +++ b/lib/src/cli/atlas_apps/baas_client.dart @@ -146,51 +146,47 @@ class BaasClient { } static Future deleteContainer(String id) async { - for (var i = 0; i < 5; i++) { - try { - print('Deleting BaaS container $id... '); + try { + print('Deleting BaaS container $id... '); - final authHelper = await BaasAuthHelper.create(); + final authHelper = await BaasAuthHelper.create(); - await authHelper.callFunction('stopContainer', arguments: [id]); - return; - } catch (e) { - if (i == 4) { - rethrow; - } - - print('Failed to deploy container: $e'); - } + await authHelper.callFunction('stopContainer', arguments: [id]); + return; + } catch (e) { + print('Failed to deploy container: $e'); + rethrow; } - - throw 'UNREACHABLE'; } static Future<(String httpUrl, String containerId)> deployContainer() async { - for (var i = 0; i < 5; i++) { - try { - print('Deploying new BaaS container... '); + print('Deploying new BaaS container... '); - final authHelper = await BaasAuthHelper.create(); + final authHelper = await BaasAuthHelper.create(); - final response = await authHelper.callFunction('startContainer') as Map; - final taskId = response['taskId'] as String; + final response = await authHelper.callFunction('startContainer') as Map; + final taskId = response['taskId'] as String; - String? httpUrl; - while (httpUrl == null) { - await Future.delayed(Duration(seconds: 1)); - httpUrl = await _waitForContainer(authHelper, taskId); - } + String? httpUrl; + while (httpUrl == null) { + await Future.delayed(Duration(seconds: 1)); + httpUrl = await _waitForContainer(authHelper, taskId); + } - print('Deployed BaaS instance at $httpUrl'); + print('Deployed BaaS instance at $httpUrl'); - return (httpUrl, taskId); + return (httpUrl, taskId); + } + + static Future retry(Future Function() func, {int attempts = 5}) async { + while (attempts >= 0) { + try { + return await func(); } catch (e) { - if (i == 4) { + print('An error occurred: $e'); + if (--attempts == 0) { rethrow; } - - print('Failed to deploy container: $e'); } } diff --git a/lib/src/cli/atlas_apps/deleteapps_command.dart b/lib/src/cli/atlas_apps/deleteapps_command.dart index d6d23afa4..d108dbc94 100644 --- a/lib/src/cli/atlas_apps/deleteapps_command.dart +++ b/lib/src/cli/atlas_apps/deleteapps_command.dart @@ -66,7 +66,7 @@ class DeleteAppsCommand extends Command { abort('--container-id must be supplied when --use-baas-aas is set'); } - await BaasClient.deleteContainer(options.containerId!); + await BaasClient.retry(() => BaasClient.deleteContainer(options.containerId!)); } else { final differentiator = options.differentiator ?? 'local'; diff --git a/test/baas_helper.dart b/test/baas_helper.dart index 2a4933e31..47ff2b378 100644 --- a/test/baas_helper.dart +++ b/test/baas_helper.dart @@ -7,8 +7,6 @@ import 'package:test/test.dart' as testing; import '../lib/src/cli/atlas_apps/baas_client.dart'; import '../lib/realm.dart'; -import 'test.dart'; - part 'baas_helper.g.dart'; const String argBaasUrl = "BAAS_URL"; @@ -136,7 +134,7 @@ class BaasHelper { throw "$argUseBaaSaaS can't be combined with $argBaasCluster"; } - (baasUrl, _) = await BaasClient.deployContainer(); + (baasUrl, _) = await BaasClient.retry(() => BaasClient.deployContainer()); } else { baasUrl = args[argBaasUrl]; } @@ -153,9 +151,9 @@ class BaasHelper { differentiator: args[argDifferentiator])))!; } - final client = await (baasInfo.cluster == null + final client = await BaasClient.retry(() => (baasInfo!.cluster == null ? BaasClient.docker(baasInfo.baasUrl, baasInfo.differentiator) - : BaasClient.atlas(baasInfo.baasUrl, baasInfo.cluster!, baasInfo.apiKey!, baasInfo.privateApiKey!, baasInfo.projectId!, baasInfo.differentiator)); + : BaasClient.atlas(baasInfo.baasUrl, baasInfo.cluster!, baasInfo.apiKey!, baasInfo.privateApiKey!, baasInfo.projectId!, baasInfo.differentiator))); client.publicRSAKey = publicRSAKeyForJWTValidation; return (client, baasInfo); @@ -196,10 +194,6 @@ class BaasHelper { final baasApp = _baasApps[app.name]!; print('Validating initial sync is complete...'); await _baasClient.waitForInitialSync(baasApp); - final appConfig = await _getAppConfig(baasApp.name); - final realm = await getIntegrationRealm(appConfig: appConfig); - await realm.syncSession.waitForUpload(); - await _baasClient.waitForInitialSync(baasApp); return; } catch (e) { print(e); From a05da4b703b972c051932bdef32b7746e0db246a Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 1 Dec 2023 06:36:50 +0100 Subject: [PATCH 09/27] Add some missing flutter tests --- .../tests/test_driver/app_test.dart | 2 -- .../tests/test_driver/realm_test.dart | 8 ++++++-- test/baas_helper.dart | 4 ++++ test/indexed_test.dart | 2 +- test/test.dart | 17 +++++++++++------ 5 files changed, 22 insertions(+), 11 deletions(-) diff --git a/flutter/realm_flutter/tests/test_driver/app_test.dart b/flutter/realm_flutter/tests/test_driver/app_test.dart index ad9b89170..68636baf3 100644 --- a/flutter/realm_flutter/tests/test_driver/app_test.dart +++ b/flutter/realm_flutter/tests/test_driver/app_test.dart @@ -6,8 +6,6 @@ import 'package:test/test.dart'; import 'const.dart'; void main(List args) { - print("Current PID $pid"); - group('Realm tests', () { FlutterDriver? driver; diff --git a/flutter/realm_flutter/tests/test_driver/realm_test.dart b/flutter/realm_flutter/tests/test_driver/realm_test.dart index 758e40d54..3373d62cc 100644 --- a/flutter/realm_flutter/tests/test_driver/realm_test.dart +++ b/flutter/realm_flutter/tests/test_driver/realm_test.dart @@ -7,6 +7,7 @@ import 'package:test_api/src/backend/invoker.dart'; import 'package:test_api/src/backend/state.dart' as test_api; import '../test/app_test.dart' as app_test; +import '../test/asymmetric_test.dart' as asymmetric_test; import '../test/backlinks_test.dart' as backlinks_test; import '../test/client_reset_test.dart' as client_reset_test; import '../test/configuration_test.dart' as configuration_test; @@ -14,6 +15,7 @@ import '../test/credentials_test.dart' as credentials_test; import '../test/decimal128_test.dart' as decimal128_test; import '../test/dynamic_realm_test.dart' as dynamic_realm_test; import '../test/embedded_test.dart' as embedded_test; +import '../test/geospatial_test.dart' as geospatial_test; import '../test/indexed_test.dart' as indexed_test; import '../test/list_test.dart' as list_test; import '../test/migration_test.dart' as migration_test; @@ -34,6 +36,7 @@ Future main(List args) async { final List failedTests = []; await app_test.main(args); + await asymmetric_test.main(args); await backlinks_test.main(args); await client_reset_test.main(args); await configuration_test.main(args); @@ -41,9 +44,11 @@ Future main(List args) async { await decimal128_test.main(args); await dynamic_realm_test.main(args); await embedded_test.main(args); - indexed_test.main(args); + await geospatial_test.main(args); + await indexed_test.main(args); await list_test.main(args); await migration_test.main(args); + await realm_logger_test.main(args); await realm_object_test.main(args); await realm_set_test.main(args); await realm_test.main(args); @@ -52,7 +57,6 @@ Future main(List args) async { await session_test.main(args); await subscription_test.main(args); await user_test.main(args); - await realm_logger_test.main(args); tearDown(() { if (Invoker.current?.liveTest.state.result == test_api.Result.error || Invoker.current?.liveTest.state.result == test_api.Result.failure) { diff --git a/test/baas_helper.dart b/test/baas_helper.dart index 47ff2b378..2d75c491f 100644 --- a/test/baas_helper.dart +++ b/test/baas_helper.dart @@ -121,6 +121,10 @@ class BaasHelper { return result; } + static bool shouldRunBaasTests(Map args) { + return args[argUseBaaSaaS] == 'true' || args[argBaasUrl] != null; + } + BaasHelper._(this._baasClient); static Future<(BaasClient?, BaasInfo?)> _setupClient(Map args, Realm realm) async { diff --git a/test/indexed_test.dart b/test/indexed_test.dart index 9232470ca..c9b816ed1 100644 --- a/test/indexed_test.dart +++ b/test/indexed_test.dart @@ -87,7 +87,7 @@ const String lordOfTheFlies = 'Lord of the Flies'; const String wheelOfTime = 'The Wheel of Time'; const String silmarillion = 'The Silmarillion'; -void main([List? args]) async { +Future main([List? args]) async { await setupTests(args); intFactory(int i) => i.hashCode; diff --git a/test/test.dart b/test/test.dart index 683c1c90a..d92b611dd 100644 --- a/test/test.dart +++ b/test/test.dart @@ -388,10 +388,15 @@ void xtest(String? name, dynamic Function() testFunction, {dynamic skip, Map _testArgs; Future setupTests(List? args) async { - final testArgs = parseTestArguments(args); - testName = testArgs["name"]; + _testArgs = parseTestArguments(args); + testName = _testArgs["name"]; + + setUpAll(() async { + baasHelper = await BaasHelper.setupBaas(_testArgs); + }); setUp(() { Realm.logger = Logger.detached('test run') @@ -425,8 +430,6 @@ Future setupTests(List? args) async { }); }); - baasHelper = await BaasHelper.setupBaas(testArgs); - // Enable this to print platform info, including current PID await _printPlatformInfo(); } @@ -565,9 +568,11 @@ Future baasTest( dynamic shouldSkip(dynamic skip) { if (skip == null) { - skip = baasHelper == null ? "BAAS URL not present" : false; + skip = BaasHelper.shouldRunBaasTests(_testArgs) ? false : "BAAS URL not present"; } else if (skip is bool) { - if (baasHelper == null) skip = "BAAS URL not present"; + if (!BaasHelper.shouldRunBaasTests(_testArgs)) { + skip = "BAAS URL not present"; + } } return skip; From 2ffa076f612558f6f8a800713434b1fb958a9372 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 1 Dec 2023 16:08:31 +0100 Subject: [PATCH 10/27] Try to fix flutter tests --- test/test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test.dart b/test/test.dart index d92b611dd..a2295abd7 100644 --- a/test/test.dart +++ b/test/test.dart @@ -388,7 +388,7 @@ void xtest(String? name, dynamic Function() testFunction, {dynamic skip, Map _testArgs; +Map _testArgs = {}; Future setupTests(List? args) async { _testArgs = parseTestArguments(args); From 99c58aaed89d801dac3c3c66576bbfe0558b826b Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 1 Dec 2023 18:11:43 +0100 Subject: [PATCH 11/27] Rework indexed test to use values from the second half of the table. --- test/indexed_test.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/indexed_test.dart b/test/indexed_test.dart index c9b816ed1..1339f717f 100644 --- a/test/indexed_test.dart +++ b/test/indexed_test.dart @@ -146,7 +146,8 @@ Future main([List? args]) async { expect(allNotIndexed.length, max); // Inefficient, but fast enough for this test - final searchOrder = (List.generate(max, (i) => i)..shuffle(Random(42))).map((i) => testCase.factory(i)).take(1000).toList(); + final halfMax = max ~/ 2; + final searchOrder = (List.generate(halfMax, (i) => halfMax + i)..shuffle(Random(42))).map((i) => testCase.factory(i)).take(1000).toList(); @pragma('vm:no-interrupts') Duration measureSpeed(RealmResults results) { @@ -155,7 +156,7 @@ Future main([List? args]) async { final sw = Stopwatch()..start(); for (final q in queries) { - found.add(q.singleOrNull); // evaluate query + found.add(q.single); // evaluate query } final timing = sw.elapsed; From 841649bf65ee0e709ef28551447f9fe4d7bb4385 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 8 Dec 2023 15:13:49 +0100 Subject: [PATCH 12/27] Use api key auth --- .github/workflows/ci.yml | 9 +++++++ .github/workflows/deploy-baas.yml | 2 +- lib/src/cli/atlas_apps/baas_client.dart | 19 +++++++------- .../cli/atlas_apps/deleteapps_command.dart | 10 +++---- .../cli/atlas_apps/deployapps_command.dart | 12 ++++----- lib/src/cli/atlas_apps/options.dart | 6 ++--- lib/src/cli/atlas_apps/options.g.dart | 9 ++++--- test/baas_helper.dart | 26 +++++++++---------- 8 files changed, 52 insertions(+), 41 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96e85329e..928e6a0a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,6 +72,7 @@ jobs: deploy-cluster-dart-windows: name: Deploy Cluster for Dart Windows uses: ./.github/workflows/deploy-baas.yml + secrets: inherit dart-tests-windows: name: Windows Dart Tests @@ -97,6 +98,7 @@ jobs: deploy-cluster-dart-macos: name: Deploy Cluster for Dart MacOS uses: ./.github/workflows/deploy-baas.yml + secrets: inherit dart-tests-macos: name: MacOS Dart Tests @@ -122,6 +124,7 @@ jobs: deploy-cluster-dart-macos-arm: name: Deploy Cluster for Dart MacOS Arm uses: ./.github/workflows/deploy-baas.yml + secrets: inherit dart-tests-macos-arm: name: MacOS Arm Dart Tests @@ -148,6 +151,7 @@ jobs: deploy-cluster-dart-linux: name: Deploy Cluster for Dart Linux uses: ./.github/workflows/deploy-baas.yml + secrets: inherit dart-tests-linux: name: Linux Dart Tests @@ -174,6 +178,7 @@ jobs: deploy-cluster-flutter-windows: name: Deploy Cluster for Flutter Windows uses: ./.github/workflows/deploy-baas.yml + secrets: inherit flutter-tests-windows: name: Windows Flutter Tests @@ -199,6 +204,7 @@ jobs: deploy-cluster-flutter-macos: name: Deploy Cluster for Flutter MacOS uses: ./.github/workflows/deploy-baas.yml + secrets: inherit flutter-tests-macos: name: MacOS Flutter Tests @@ -224,6 +230,7 @@ jobs: deploy-cluster-flutter-linux: name: Deploy Cluster for Flutter Linux uses: ./.github/workflows/deploy-baas.yml + secrets: inherit flutter-tests-linux: name: Linux Flutter Tests @@ -249,6 +256,7 @@ jobs: deploy-cluster-flutter-ios: name: Deploy Cluster for Flutter iOS uses: ./.github/workflows/deploy-baas.yml + secrets: inherit flutter-tests-ios: runs-on: macos-latest @@ -319,6 +327,7 @@ jobs: deploy-cluster-flutter-android: name: Deploy Cluster for Flutter Android uses: ./.github/workflows/deploy-baas.yml + secrets: inherit flutter-tests-android: runs-on: macos-latest diff --git a/.github/workflows/deploy-baas.yml b/.github/workflows/deploy-baas.yml index c8a3f7f43..7728f816f 100644 --- a/.github/workflows/deploy-baas.yml +++ b/.github/workflows/deploy-baas.yml @@ -37,7 +37,7 @@ jobs: run: dart pub get - name: Deploy cluster and apps - run: dart run realm_dart deploy-apps --use-baas-aas + run: dart run realm_dart deploy-apps --baasaas-api-key ${{ secrets.BAASAAS_API_KEY }} - name: Set outputs id: set-outputs run: | diff --git a/lib/src/cli/atlas_apps/baas_client.dart b/lib/src/cli/atlas_apps/baas_client.dart index 7fdd6abc3..862f6a19a 100644 --- a/lib/src/cli/atlas_apps/baas_client.dart +++ b/lib/src/cli/atlas_apps/baas_client.dart @@ -27,12 +27,13 @@ class BaasAuthHelper { BaasAuthHelper._(this._accessToken, this._location); - static Future create() async { + static Future create(String apiKey) async { final locationResponse = await http.get(Uri.parse('https://realm.mongodb.com/api/client/v2.0/app/$_appId/location')); final locationJson = BaasClient._decodeResponse(locationResponse) as Map; final location = locationJson['hostname'] as String; - final loginResponse = await http.post(Uri.parse('$location/api/client/v2.0/app/$_appId/auth/providers/anon-user/login')); + final loginResponse = await http.post(Uri.parse('$location/api/client/v2.0/app/$_appId/auth/providers/api-key/login'), + body: jsonEncode({'key': apiKey}), headers: {'Content-Type': 'application/json'}); final loginJson = BaasClient._decodeResponse(loginResponse) as Map; final accessToken = loginJson['access_token'] as String; @@ -145,11 +146,11 @@ class BaasClient { return result; } - static Future deleteContainer(String id) async { + static Future deleteContainer(String id, String apiKey) async { try { print('Deleting BaaS container $id... '); - final authHelper = await BaasAuthHelper.create(); + final authHelper = await BaasAuthHelper.create(apiKey); await authHelper.callFunction('stopContainer', arguments: [id]); return; @@ -159,23 +160,23 @@ class BaasClient { } } - static Future<(String httpUrl, String containerId)> deployContainer() async { + static Future<(String httpUrl, String containerId)> deployContainer(String apiKey) async { print('Deploying new BaaS container... '); - final authHelper = await BaasAuthHelper.create(); + final authHelper = await BaasAuthHelper.create(apiKey); final response = await authHelper.callFunction('startContainer') as Map; - final taskId = response['taskId'] as String; + final id = response['id'] as String; String? httpUrl; while (httpUrl == null) { await Future.delayed(Duration(seconds: 1)); - httpUrl = await _waitForContainer(authHelper, taskId); + httpUrl = await _waitForContainer(authHelper, id); } print('Deployed BaaS instance at $httpUrl'); - return (httpUrl, taskId); + return (httpUrl, id); } static Future retry(Future Function() func, {int attempts = 5}) async { diff --git a/lib/src/cli/atlas_apps/deleteapps_command.dart b/lib/src/cli/atlas_apps/deleteapps_command.dart index d108dbc94..eb84c0b22 100644 --- a/lib/src/cli/atlas_apps/deleteapps_command.dart +++ b/lib/src/cli/atlas_apps/deleteapps_command.dart @@ -57,16 +57,16 @@ class DeleteAppsCommand extends Command { } } - if (!options.useBaaSaaS && options.baasUrl == null) { - abort('--baas-url must be supplied when --use-baas-aas is not set'); + if (options.baasaasApiKey == null && options.baasUrl == null) { + abort('--baas-url must be supplied when --baasaas-api-key is null'); } - if (options.useBaaSaaS) { + if (options.baasaasApiKey != null) { if (options.containerId == null) { - abort('--container-id must be supplied when --use-baas-aas is set'); + abort('--container-id must be supplied when --baasaas-api-key is set'); } - await BaasClient.retry(() => BaasClient.deleteContainer(options.containerId!)); + await BaasClient.retry(() => BaasClient.deleteContainer(options.containerId!, options.baasaasApiKey!)); } else { final differentiator = options.differentiator ?? 'local'; diff --git a/lib/src/cli/atlas_apps/deployapps_command.dart b/lib/src/cli/atlas_apps/deployapps_command.dart index 888ce2306..81b4855b6 100644 --- a/lib/src/cli/atlas_apps/deployapps_command.dart +++ b/lib/src/cli/atlas_apps/deployapps_command.dart @@ -67,19 +67,19 @@ RwIDAQAB abort('--project-id must be supplied when --atlas-cluster is set'); } - if (options.useBaaSaaS) { - abort('--use-baas-aas cannot be used when --atlas-cluster is set'); + if (options.baasaasApiKey != null) { + abort('--baasaas-api-key cannot be used when --atlas-cluster is set'); } } - if (!options.useBaaSaaS && options.baasUrl == null) { - abort('--baas-url must be supplied when --use-baas-aas is not set'); + if (options.baasaasApiKey == null && options.baasUrl == null) { + abort('--baas-url must be supplied when --baasaas-api-key is null'); } late String baasUrl; - if (options.useBaaSaaS) { + if (options.baasaasApiKey != null) { late String containerId; - (baasUrl, containerId) = await BaasClient.deployContainer(); + (baasUrl, containerId) = await BaasClient.deployContainer(options.baasaasApiKey!); await File('baasurl').writeAsString(baasUrl); await File('containerid').writeAsString(containerId); print('BaasUrl: $baasUrl'); diff --git a/lib/src/cli/atlas_apps/options.dart b/lib/src/cli/atlas_apps/options.dart index 34fd4522f..303795b34 100644 --- a/lib/src/cli/atlas_apps/options.dart +++ b/lib/src/cli/atlas_apps/options.dart @@ -40,13 +40,13 @@ class Options { @CliOption(help: 'The Atlas project id to use for the import. Only used if atlas-cluster is specified.') final String? projectId; - @CliOption(help: 'Spawn a new container for BaaSaaS and creates app in it.', name: 'use-baas-aas') - final bool useBaaSaaS; + @CliOption(help: 'API key to use with BaaSaaS to wpawn a new container and create apps in it.', name: 'baasaas-api-key') + final String? baasaasApiKey; @CliOption(help: 'Container id to be cleaned up. Only to be used with use-baas-aas and delete command') final String? containerId; - Options({this.baasUrl, this.atlasCluster, this.apiKey, this.privateApiKey, this.projectId, this.differentiator, this.useBaaSaaS = false, this.containerId}); + Options({this.baasUrl, this.atlasCluster, this.apiKey, this.privateApiKey, this.projectId, this.differentiator, this.baasaasApiKey, this.containerId}); } String get usage => _$parserForOptions.usage; diff --git a/lib/src/cli/atlas_apps/options.g.dart b/lib/src/cli/atlas_apps/options.g.dart index e87e11b05..083b49525 100644 --- a/lib/src/cli/atlas_apps/options.g.dart +++ b/lib/src/cli/atlas_apps/options.g.dart @@ -13,7 +13,7 @@ Options _$parseOptionsResult(ArgResults result) => Options( privateApiKey: result['private-api-key'] as String?, projectId: result['project-id'] as String?, differentiator: result['differentiator'] as String?, - useBaaSaaS: result['use-baas-aas'] as bool, + baasaasApiKey: result['baasaas-api-key'] as String?, containerId: result['container-id'] as String?, ); @@ -45,9 +45,10 @@ ArgParser _$populateOptionsParser(ArgParser parser) => parser help: 'The Atlas project id to use for the import. Only used if atlas-cluster is specified.', ) - ..addFlag( - 'use-baas-aas', - help: 'Spawn a new container for BaaSaaS and creates app in it.', + ..addOption( + 'baasaas-api-key', + help: + 'API key to use with BaaSaaS to wpawn a new container and create apps in it.', ) ..addOption( 'container-id', diff --git a/test/baas_helper.dart b/test/baas_helper.dart index 2d75c491f..0995c7cbc 100644 --- a/test/baas_helper.dart +++ b/test/baas_helper.dart @@ -15,7 +15,7 @@ const String argBaasApiKey = "BAAS_API_KEY"; const String argBaasPrivateApiKey = "BAAS_PRIVATE_API_KEY"; const String argBaasProjectId = "BAAS_PROJECT_ID"; const String argDifferentiator = "BAAS_DIFFERENTIATOR"; -const String argUseBaaSaaS = "BAAS_USE_BAASAAS"; +const String argBaasaasApiKey = "BAAS_BAASAAS_API_KEY"; const String publicRSAKeyForJWTValidation = '''-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvNHHs8T0AHD7SJ+CKvVR @@ -37,7 +37,7 @@ Map parseTestArguments(List? arguments) { ..addOption(argBaasPrivateApiKey) ..addOption(argBaasProjectId) ..addOption(argDifferentiator) - ..addOption(argUseBaaSaaS); + ..addOption(argBaasaasApiKey); final result = parser.parse(arguments ?? []); testArgs @@ -48,7 +48,7 @@ Map parseTestArguments(List? arguments) { ..addArgument(result, argBaasPrivateApiKey) ..addArgument(result, argBaasProjectId) ..addArgument(result, argDifferentiator) - ..addArgument(result, argUseBaaSaaS); + ..addArgument(result, argBaasaasApiKey); return testArgs; } @@ -122,7 +122,7 @@ class BaasHelper { } static bool shouldRunBaasTests(Map args) { - return args[argUseBaaSaaS] == 'true' || args[argBaasUrl] != null; + return args[argBaasaasApiKey] != null || args[argBaasUrl] != null; } BaasHelper._(this._baasClient); @@ -131,16 +131,16 @@ class BaasHelper { try { var baasInfo = realm.all().firstOrNull; if (baasInfo == null) { - late String? baasUrl; - final useBaaSaaS = args[argUseBaaSaaS] == 'true'; - if (useBaaSaaS) { - if (args[argBaasCluster] != null) { - throw "$argUseBaaSaaS can't be combined with $argBaasCluster"; - } + var baasUrl = args[argBaasUrl]; + if (baasUrl == null) { + final baasaasApiKey = args[argBaasaasApiKey]; + if (baasaasApiKey != null) { + if (args[argBaasCluster] != null) { + throw "$argBaasaasApiKey can't be combined with $argBaasCluster"; + } - (baasUrl, _) = await BaasClient.retry(() => BaasClient.deployContainer()); - } else { - baasUrl = args[argBaasUrl]; + (baasUrl, _) = await BaasClient.retry(() => BaasClient.deployContainer(baasaasApiKey)); + } } if (baasUrl == null) { From b13278298b60be1c7205536463631603cac9ca20 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 8 Dec 2023 17:05:30 +0100 Subject: [PATCH 13/27] Post-merge fixes --- test/baas_helper.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/baas_helper.dart b/test/baas_helper.dart index 0995c7cbc..fbfd59c22 100644 --- a/test/baas_helper.dart +++ b/test/baas_helper.dart @@ -4,8 +4,9 @@ import 'package:args/args.dart'; import 'package:path/path.dart' as _path; import 'package:test/test.dart' as testing; -import '../lib/src/cli/atlas_apps/baas_client.dart'; import '../lib/realm.dart'; +import '../lib/src/cli/atlas_apps/baas_client.dart'; +import '../lib/src/native/realm_core.dart'; part 'baas_helper.g.dart'; @@ -201,6 +202,8 @@ class BaasHelper { return; } catch (e) { print(e); + } finally { + realmCore.clearCachedApps(); } } } From 00eb188ae8712d8132298c90203372f657313831 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Tue, 12 Dec 2023 03:14:05 +0100 Subject: [PATCH 14/27] Use endpoints, wire-up differentiator tag --- .github/workflows/ci.yml | 66 +++---- .github/workflows/dart-desktop-tests.yml | 18 +- .github/workflows/deploy-baas.yml | 11 +- .github/workflows/flutter-desktop-tests.yml | 20 +-- lib/src/cli/atlas_apps/baas_client.dart | 124 +++++++------ .../cli/atlas_apps/deployapps_command.dart | 5 +- test/baas_helper.dart | 138 +++++--------- test/baas_helper.g.dart | 170 ------------------ test/test.dart | 2 +- 9 files changed, 166 insertions(+), 388 deletions(-) delete mode 100644 test/baas_helper.g.dart diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 928e6a0a0..aab028672 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,10 +6,6 @@ on: - main pull_request: env: - BAAS_URL: ${{ secrets.REALM_QA_URL }} - BAAS_API_KEY: ${{ secrets.ATLAS_QA_PUBLIC_API_KEY }} - BAAS_PRIVATE_API_KEY: ${{ secrets.ATLAS_QA_PRIVATE_API_KEY }} - BAAS_PROJECT_ID: ${{ secrets.ATLAS_QA_PROJECT_ID}} REALM_CI: true concurrency: @@ -73,6 +69,8 @@ jobs: name: Deploy Cluster for Dart Windows uses: ./.github/workflows/deploy-baas.yml secrets: inherit + with: + differentiator: dw${{ github.run_id }}${{ github.run_attempt }} dart-tests-windows: name: Windows Dart Tests @@ -84,7 +82,7 @@ jobs: with: os: windows runner: windows-latest - baasurl: ${{ needs.deploy-cluster-dart-windows.outputs.url }} + differentiator: dw${{ github.run_id }}${{ github.run_attempt }} cleanup-cluster-dart-windows: name: Cleanup Cluster for Dart Windows @@ -99,6 +97,8 @@ jobs: name: Deploy Cluster for Dart MacOS uses: ./.github/workflows/deploy-baas.yml secrets: inherit + with: + differentiator: dm${{ github.run_id }}${{ github.run_attempt }} dart-tests-macos: name: MacOS Dart Tests @@ -110,7 +110,7 @@ jobs: with: os: macos runner: macos-latest - baasurl: ${{ needs.deploy-cluster-dart-macos.outputs.url }} + differentiator: dm${{ github.run_id }}${{ github.run_attempt }} cleanup-cluster-dart-macos: name: Cleanup Cluster for Dart macOS @@ -125,6 +125,8 @@ jobs: name: Deploy Cluster for Dart MacOS Arm uses: ./.github/workflows/deploy-baas.yml secrets: inherit + with: + differentiator: dma${{ github.run_id }}${{ github.run_attempt }} dart-tests-macos-arm: name: MacOS Arm Dart Tests @@ -137,7 +139,7 @@ jobs: os: macos runner: macos-arm architecture: arm - baasurl: ${{ needs.deploy-cluster-dart-macos-arm.outputs.url }} + differentiator: dma${{ github.run_id }}${{ github.run_attempt }} cleanup-cluster-dart-macos-arm: name: Cleanup Cluster for Dart macOS Arm @@ -152,6 +154,8 @@ jobs: name: Deploy Cluster for Dart Linux uses: ./.github/workflows/deploy-baas.yml secrets: inherit + with: + differentiator: dl${{ github.run_id }}${{ github.run_attempt }} dart-tests-linux: name: Linux Dart Tests @@ -163,7 +167,7 @@ jobs: with: os: linux runner: ubuntu-latest - baasurl: ${{ needs.deploy-cluster-dart-linux.outputs.url }} + differentiator: dl${{ github.run_id }}${{ github.run_attempt }} cleanup-cluster-dart-linux: name: Cleanup Cluster for Dart Linux @@ -179,6 +183,8 @@ jobs: name: Deploy Cluster for Flutter Windows uses: ./.github/workflows/deploy-baas.yml secrets: inherit + with: + differentiator: fw${{ github.run_id }}${{ github.run_attempt }} flutter-tests-windows: name: Windows Flutter Tests @@ -190,7 +196,8 @@ jobs: with: os: windows runner: windows-latest - baasurl: ${{ needs.deploy-cluster-flutter-windows.outputs.url }} + differentiator: fw${{ github.run_id }}${{ github.run_attempt }} + cleanup-cluster-flutter-windows: name: Cleanup Cluster for Flutter Windows @@ -205,6 +212,8 @@ jobs: name: Deploy Cluster for Flutter MacOS uses: ./.github/workflows/deploy-baas.yml secrets: inherit + with: + differentiator: fm${{ github.run_id }}${{ github.run_attempt }} flutter-tests-macos: name: MacOS Flutter Tests @@ -216,7 +225,7 @@ jobs: with: os: macos runner: macos-13 # workaround to: https://github.com/flutter/flutter/issues/118469 latest is still macos-12 ¯\_(ツ)_/¯ - baasurl: ${{ needs.deploy-cluster-flutter-macos.outputs.url }} + differentiator: fm${{ github.run_id }}${{ github.run_attempt }} cleanup-cluster-flutter-macos: name: Cleanup Cluster for Flutter macOS @@ -231,6 +240,8 @@ jobs: name: Deploy Cluster for Flutter Linux uses: ./.github/workflows/deploy-baas.yml secrets: inherit + with: + differentiator: fl${{ github.run_id }}${{ github.run_attempt }} flutter-tests-linux: name: Linux Flutter Tests @@ -242,7 +253,7 @@ jobs: with: os: linux runner: ubuntu-latest - baasurl: ${{ needs.deploy-cluster-flutter-linux.outputs.url }} + differentiator: fl${{ github.run_id }}${{ github.run_attempt }} cleanup-cluster-flutter-linux: name: Cleanup Cluster for Flutter Linux @@ -257,6 +268,8 @@ jobs: name: Deploy Cluster for Flutter iOS uses: ./.github/workflows/deploy-baas.yml secrets: inherit + with: + differentiator: fi${{ github.run_id }}${{ github.run_attempt }} flutter-tests-ios: runs-on: macos-latest @@ -266,7 +279,7 @@ jobs: - deploy-cluster-flutter-ios - build-ios-xcframework env: - BAAS_URL: ${{ needs.deploy-cluster-flutter-ios.outputs.url }} + BAAS_DIFFERENTIATOR: fi${{ github.run_id }}${{ github.run_attempt }} steps: - name: Checkout @@ -298,18 +311,6 @@ jobs: os: 'iOS' os_version: '>= 14.0' - # # This will be a no-op under normal circumstances since the cluster would have been deployed - # # in deploy-cluster. It is needed in case we want to re-run the job after the cluster has been reaped. - # - name: Create cluster - # uses: realm/ci-actions/mdb-realm/deploy@338bf3e7575015a28faec8b67614385d122aece7 - # with: - # realmUrl: ${{ env.BAAS_URL }} - # atlasUrl: ${{ secrets.ATLAS_QA_URL }} - # projectId: ${{ env.BAAS_PROJECT_ID }} - # apiKey: ${{ env.BAAS_API_KEY }} - # privateApiKey: ${{ env.BAAS_PRIVATE_API_KEY }} - # clusterName: ${{ env.BAAS_CLUSTER }} - - name: Run tests on iOS Simulator run: | flutter drive --target=test_driver/app.dart --dart-define=testName="" --suppress-analytics --debug @@ -328,6 +329,9 @@ jobs: name: Deploy Cluster for Flutter Android uses: ./.github/workflows/deploy-baas.yml secrets: inherit + with: + differentiator: fa${{ github.run_id }}${{ github.run_attempt }} + flutter-tests-android: runs-on: macos-latest @@ -337,7 +341,7 @@ jobs: - deploy-cluster-flutter-android - build-android-combined env: - BAAS_URL: ${{ needs.deploy-cluster-flutter-android.outputs.url }} + BAAS_DIFFERENTIATOR: fi${{ github.run_id }}${{ github.run_attempt }} steps: - name: Checkout @@ -388,18 +392,6 @@ jobs: cmake: 3.10.2.4988404 script: echo "Generated Emulator snapshot for caching." - # # This will be a no-op under normal circumstances since the cluster would have been deployed - # # in deploy-cluster. It is needed in case we want to re-run the job after the cluster has been reaped. - # - name: Create cluster - # uses: realm/ci-actions/mdb-realm/deploy@338bf3e7575015a28faec8b67614385d122aece7 - # with: - # realmUrl: ${{ env.BAAS_URL }} - # atlasUrl: ${{ secrets.ATLAS_QA_URL }} - # projectId: ${{ env.BAAS_PROJECT_ID }} - # apiKey: ${{ env.BAAS_API_KEY }} - # privateApiKey: ${{ env.BAAS_PRIVATE_API_KEY }} - # clusterName: ${{ env.BAAS_CLUSTER }} - - name: Run tests on Android Emulator uses: reactivecircus/android-emulator-runner@v2 with: diff --git a/.github/workflows/dart-desktop-tests.yml b/.github/workflows/dart-desktop-tests.yml index 716db7d5e..f5ce068fd 100644 --- a/.github/workflows/dart-desktop-tests.yml +++ b/.github/workflows/dart-desktop-tests.yml @@ -15,8 +15,8 @@ on: description: Architecture to execute on. required: false type: string - baasurl: - description: BaaS url to test against. + differentiator: + description: Differentiator for the BaaS container. required: true type: string @@ -28,8 +28,6 @@ jobs: runs-on: ${{ inputs.runner }} name: Dart tests on ${{inputs.os }} ${{ inputs.architecture }} timeout-minutes: 45 - env: - BAAS_URL: ${{ inputs.baasurl }} steps: - name: Checkout @@ -59,18 +57,6 @@ jobs: run: ulimit -n 10240 if: ${{ contains(inputs.os, 'macos') }} - # # This will be a no-op under normal circumstances since the cluster would have been deployed - # # in deploy-cluster. It is needed in case we want to re-run the job after the cluster has been reaped. - # - name: Create cluster - # uses: realm/ci-actions/mdb-realm/deploy@338bf3e7575015a28faec8b67614385d122aece7 - # with: - # realmUrl: ${{ env.BAAS_URL }} - # atlasUrl: ${{ secrets.ATLAS_QA_URL }} - # projectId: ${{ env.BAAS_PROJECT_ID }} - # apiKey: ${{ env.BAAS_API_KEY }} - # privateApiKey: ${{ env.BAAS_PRIVATE_API_KEY }} - # clusterName: ${{ env.BAAS_CLUSTER }} - - name: Run tests run: ${{ inputs.architecture == 'arm' && 'arch -arm64 ' || '' }}dart test -r expanded --coverage ./coverage/ -j 1 --test-randomize-ordering-seed random diff --git a/.github/workflows/deploy-baas.yml b/.github/workflows/deploy-baas.yml index 7728f816f..f93fc105d 100644 --- a/.github/workflows/deploy-baas.yml +++ b/.github/workflows/deploy-baas.yml @@ -3,15 +3,18 @@ name: Deploy BaaS and apps on: workflow_call: outputs: - url: - description: The url to connect to - value: ${{ jobs.deploy-baas.outputs.url }} containerId: description: The container id of the BaaS instance value: ${{ jobs.deploy-baas.outputs.containerId }} + inputs: + differentiator: + description: Differentiator for the BaaS container. + required: true + type: string env: REALM_CI: true + BAAS_DIFFERENTIATOR: ${{ inputs.differentiator }} jobs: deploy-baas: @@ -41,7 +44,5 @@ jobs: - name: Set outputs id: set-outputs run: | - baas_url=`cat baasurl` container_id=`cat containerid` - echo "baasUrl=$baas_url" >> "$GITHUB_OUTPUT" echo "containerId=$container_id" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/flutter-desktop-tests.yml b/.github/workflows/flutter-desktop-tests.yml index 1a2d559b7..27b874f97 100644 --- a/.github/workflows/flutter-desktop-tests.yml +++ b/.github/workflows/flutter-desktop-tests.yml @@ -15,8 +15,8 @@ on: description: Architecture to execute on. required: false type: string - baasurl: - description: BaaS url to test against. + differentiator: + description: Differentiator for the BaaS container. required: true type: string @@ -29,7 +29,9 @@ jobs: name: Flutter tests on ${{inputs.os }}-${{ inputs.architecture }} timeout-minutes: 45 env: - BAAS_URL: ${{ inputs.baasurl }} + BAASAAS_API_KEY: ${{ secrets.BAASAAS_API_KEY}} + BAAS_DIFFERENTIATOR: ${{ inputs.differentiator }} + steps: - name: Checkout @@ -69,18 +71,6 @@ jobs: run: ulimit -n 10240 if: ${{ contains(inputs.os, 'macos') }} - # # This will be a no-op under normal circumstances since the cluster would have been deployed - # # in deploy-cluster. It is needed in case we want to re-run the job after the cluster has been reaped. - # - name: Create cluster - # uses: realm/ci-actions/mdb-realm/deploy@338bf3e7575015a28faec8b67614385d122aece7 - # with: - # realmUrl: ${{ env.BAAS_URL }} - # atlasUrl: ${{ secrets.ATLAS_QA_URL }} - # projectId: ${{ env.BAAS_PROJECT_ID }} - # apiKey: ${{ env.BAAS_API_KEY }} - # privateApiKey: ${{ env.BAAS_PRIVATE_API_KEY }} - # clusterName: ${{ env.BAAS_CLUSTER }} - - name: Run tests run: ${{ inputs.os == 'linux' && 'xvfb-run' || '' }} flutter drive -d ${{ inputs.os }} --target=test_driver/app.dart --suppress-analytics --dart-entrypoint-args="" --debug # -a="Some test name" working-directory: ./flutter/realm_flutter/tests diff --git a/lib/src/cli/atlas_apps/baas_client.dart b/lib/src/cli/atlas_apps/baas_client.dart index 862f6a19a..860d43498 100644 --- a/lib/src/cli/atlas_apps/baas_client.dart +++ b/lib/src/cli/atlas_apps/baas_client.dart @@ -22,29 +22,25 @@ import 'dart:convert'; class BaasAuthHelper { static const String _appId = 'baas-container-service-autzb'; - final String _accessToken; + final String _apiKey; final String _location; - BaasAuthHelper._(this._accessToken, this._location); + BaasAuthHelper(this._apiKey) : _location = 'https://us-east-1.aws.data.mongodb-api.com'; - static Future create(String apiKey) async { - final locationResponse = await http.get(Uri.parse('https://realm.mongodb.com/api/client/v2.0/app/$_appId/location')); - final locationJson = BaasClient._decodeResponse(locationResponse) as Map; - final location = locationJson['hostname'] as String; - - final loginResponse = await http.post(Uri.parse('$location/api/client/v2.0/app/$_appId/auth/providers/api-key/login'), - body: jsonEncode({'key': apiKey}), headers: {'Content-Type': 'application/json'}); - final loginJson = BaasClient._decodeResponse(loginResponse) as Map; - final accessToken = loginJson['access_token'] as String; + Future callEndpoint(String name, {Object? body, Map? query, bool isPost = true}) async { + var url = '$_location/app/$_appId/endpoint/$name'; + if (query != null) { + url = '$url?${query.entries.map((kvp) => '${kvp.key}=${kvp.value}').join('&')}'; + } + final headers = {'apiKey': _apiKey}; + final response = isPost ? await http.post(Uri.parse(url), headers: headers, body: jsonEncode(body)) : await http.get(Uri.parse(url), headers: headers); - return BaasAuthHelper._(accessToken, location); + return BaasClient._decodeResponse(response); } - Future callFunction(String name, {List arguments = const []}) async { - final response = await http.post(Uri.parse('$_location/api/client/v2.0/app/$_appId/functions/call'), - headers: {'Authorization': 'Bearer $_accessToken'}, body: jsonEncode({'name': name, 'arguments': arguments})); - - return BaasClient._decodeResponse(response); + Future getUserId() async { + final response = await callEndpoint('userinfo', isPost: false) as Map; + return response['id'] as String; } } @@ -125,15 +121,15 @@ class BaasClient { late String _groupId; late String publicRSAKey = ''; - BaasClient._(this.baseUrl, String? differentiator, [this._clusterName]) + BaasClient._(this.baseUrl, String differentiator, [this._clusterName]) : _adminApiUrl = '$baseUrl/api/admin/v3.0', _headers = {'Accept': 'application/json'}, - _appSuffix = '-${shortenDifferentiator(differentiator ?? 'local')}${_clusterName == null ? '' : '-$_clusterName'}'; + _appSuffix = '-${shortenDifferentiator(differentiator)}${_clusterName == null ? '' : '-$_clusterName'}'; /// A client that imports apps in a MongoDB Atlas docker image. See https://github.com/realm/ci/tree/master/realm/docker/mongodb-realm /// for instructions on how to set it up. /// @nodoc - static Future docker(String baseUrl, String? differentiator) async { + static Future docker(String baseUrl, String differentiator) async { final result = BaasClient._(baseUrl, differentiator); await result._authenticate('local-userpass', '{ "username": "unique_user@domain.com", "password": "password" }'); @@ -150,9 +146,9 @@ class BaasClient { try { print('Deleting BaaS container $id... '); - final authHelper = await BaasAuthHelper.create(apiKey); + final authHelper = BaasAuthHelper(apiKey); - await authHelper.callFunction('stopContainer', arguments: [id]); + await authHelper.callEndpoint('stopContainer', query: {'id': id}); return; } catch (e) { print('Failed to deploy container: $e'); @@ -160,12 +156,20 @@ class BaasClient { } } - static Future<(String httpUrl, String containerId)> deployContainer(String apiKey) async { - print('Deploying new BaaS container... '); - - final authHelper = await BaasAuthHelper.create(apiKey); + static Future<(String httpUrl, String containerId)> getOrDeployContainer(String apiKey, String differentiator) async { + final authHelper = BaasAuthHelper(apiKey); + final containers = await _getContainers(authHelper); + final userId = await authHelper.getUserId(); + final existing = containers.firstWhereOrNull((c) => c.creatorId == userId && c.tags['DIFFERENTIATOR'] == differentiator); + if (existing != null) { + print('Using existing BaaS container at ${existing.httpUrl}'); + return (existing.httpUrl, existing.id); + } - final response = await authHelper.callFunction('startContainer') as Map; + print('Deploying new BaaS container... '); + final response = await authHelper.callEndpoint('startContainer', body: [ + {'key': 'DIFFERENTIATOR', 'value': differentiator} + ]) as Map; final id = response['id'] as String; String? httpUrl; @@ -194,21 +198,25 @@ class BaasClient { throw 'UNREACHABLE'; } + static Future> _getContainers(BaasAuthHelper helper) async { + return (await helper.callEndpoint('listContainers', isPost: false) as List).map((e) => _ContainerInfo.fromJson(e)).toList(); + } + static Future _waitForContainer(BaasAuthHelper authHelper, String taskId) async { try { - final containers = await authHelper.callFunction('listContainers') as List; - final targetContainer = containers.firstWhereOrNull((c) => c['id'] == taskId); + final containers = await _getContainers(authHelper); + final targetContainer = containers.firstWhereOrNull((c) => c.id == taskId); if (targetContainer == null) { print('$taskId is not found in container list. Retrying...'); return null; } - if (targetContainer['lastStatus'] != 'RUNNING') { - print('$taskId status is ${targetContainer['lastStatus']}. Retrying...'); + if (!targetContainer.isRunning) { + print('$taskId status is ${targetContainer.lastStatus}. Retrying...'); return null; } - final httpUrl = targetContainer['httpUrl'] as String; + final httpUrl = targetContainer.httpUrl; final response = await http.get(Uri.parse('$httpUrl/api/private/v1.0/version')); if (response.statusCode > 300) { @@ -216,7 +224,7 @@ class BaasClient { return null; } - return targetContainer['httpUrl'] as String; + return httpUrl; } catch (e) { print('Error waiting for container: $e'); return null; @@ -225,7 +233,7 @@ class BaasClient { /// A client that imports apps to a MongoDB Atlas environment (typically realm-dev or realm-qa). /// @nodoc - static Future atlas(String baseUrl, String cluster, String apiKey, String privateApiKey, String groupId, String? differentiator) async { + static Future atlas(String baseUrl, String cluster, String apiKey, String privateApiKey, String groupId, String differentiator) async { final BaasClient result = BaasClient._(baseUrl, differentiator, cluster); await result._authenticate('mongodb-cloud', '{ "username": "$apiKey", "apiKey": "$privateApiKey" }'); @@ -294,7 +302,7 @@ class BaasClient { } else { return null; } - return BaasApp(doc['_id'] as String, doc['client_app_id'] as String, appName, name); + return BaasApp(appId: doc['_id'] as String, clientAppId: doc['client_app_id'] as String, name: appName, uniqueName: name, isNewDeployment: false); }) .where((doc) => doc != null) .map((doc) => doc!) @@ -310,7 +318,7 @@ class BaasClient { final appId = doc['_id'] as String; final appUniqueName = doc['name'] as String; final clientAppId = doc['client_app_id'] as String; - final app = BaasApp(appId, clientAppId, name, appUniqueName); + final app = BaasApp(appId: appId, clientAppId: clientAppId, name: name, uniqueName: appUniqueName, isNewDeployment: false); final dynamic functions = await _get('groups/$_groupId/apps/$appId/functions'); dynamic function = functions.firstWhere((dynamic f) => f["name"] == "confirmFunc", orElse: () => throw Exception("Func 'confirmFunc' not found")); @@ -326,10 +334,8 @@ class BaasClient { BaasApp? app; try { final dynamic doc = await _post('groups/$_groupId/apps', '{ "name": "$uniqueName" }'); - final appId = doc['_id'] as String; - final clientAppId = doc['client_app_id'] as String; - app = BaasApp(appId, clientAppId, name, uniqueName); + app = BaasApp(appId: doc['_id'] as String, clientAppId: doc['client_app_id'] as String, name: name, uniqueName: uniqueName, isNewDeployment: true); final confirmFuncId = await _createFunction(app, 'confirmFunc', _confirmFuncSource); final resetFuncId = await _createFunction(app, 'resetFunc', _resetFuncSource); @@ -358,7 +364,7 @@ class BaasClient { if (publicRSAKey.isNotEmpty) { String publicRSAKeyEncoded = jsonEncode(publicRSAKey); - final dynamic createSecretResult = await _post('groups/$_groupId/apps/$appId/secrets', '{"name":"rsPublicKey","value":$publicRSAKeyEncoded}'); + final dynamic createSecretResult = await _post('groups/$_groupId/apps/$app/secrets', '{"name":"rsPublicKey","value":$publicRSAKeyEncoded}'); String keyName = createSecretResult['name'] as String; await enableProvider(app, 'custom-token', config: '''{ @@ -421,7 +427,7 @@ class BaasClient { }'''); const facebookSecret = "876750ac6d06618b323dee591602897f"; - final dynamic createFacebookSecretResult = await _post('groups/$_groupId/apps/$appId/secrets', '{"name":"facebookSecret","value":"$facebookSecret"}'); + final dynamic createFacebookSecretResult = await _post('groups/$_groupId/apps/$app/secrets', '{"name":"facebookSecret","value":"$facebookSecret"}'); String facebookClientSecretKeyName = createFacebookSecretResult['name'] as String; await enableProvider(app, 'oauth2-facebook', config: '''{ "clientId": "1265617494254819" @@ -494,10 +500,10 @@ class BaasClient { ] }''', ); - await _put('groups/$_groupId/apps/$appId/sync/config', '{ "development_mode_enabled": true }'); + await _put('groups/$_groupId/apps/$app/sync/config', '{ "development_mode_enabled": true }'); //create email/password user for tests - final dynamic createUserResult = await _post('groups/$_groupId/apps/$appId/users', '{"email": "realm-test@realm.io", "password":"123456"}'); + final dynamic createUserResult = await _post('groups/$_groupId/apps/$app/users', '{"email": "realm-test@realm.io", "password":"123456"}'); print("Create user result: $createUserResult"); } catch (error) { print(error); @@ -688,15 +694,13 @@ class BaasClient { dynamic doc = docs.firstWhere((dynamic d) { return d["name"] == uniqueName; }, orElse: () => throw Exception("BAAS app not found")); - final appId = doc['_id'] as String; - final appUniqueName = doc['name'] as String; - final clientAppId = doc['client_app_id'] as String; - final app = BaasApp(appId, clientAppId, name, appUniqueName); + final app = BaasApp( + appId: doc['_id'] as String, clientAppId: doc['client_app_id'] as String, name: name, uniqueName: doc['name'] as String, isNewDeployment: false); - final dynamic services = await _get('groups/$_groupId/apps/$appId/services'); + final dynamic services = await _get('groups/$_groupId/apps/$app/services'); dynamic service = services.firstWhere((dynamic s) => s["name"] == "BackingDB", orElse: () => throw Exception("Func 'confirmFunc' not found")); final mongoServiceId = service['_id'] as String; - final dynamic configDocs = await _get('groups/$_groupId/apps/$appId/services/$mongoServiceId/config'); + final dynamic configDocs = await _get('groups/$_groupId/apps/$app/services/$mongoServiceId/config'); final dynamic flexibleSync = configDocs['flexible_sync']; final dynamic clusterName = configDocs['clusterName']; flexibleSync["is_recovery_mode_disabled"] = !enable; @@ -708,19 +712,37 @@ class BaasClient { } } +class _ContainerInfo { + final String id; + bool get isRunning => lastStatus == 'RUNNING'; + final String httpUrl; + final String lastStatus; + final Map tags; + final String creatorId; + + _ContainerInfo.fromJson(Map json) + : id = json['id'] as String, + lastStatus = json['lastStatus'], + httpUrl = json['httpUrl'] as String, + tags = {for (var v in json['tags'] as List) v['key']: v['value']}, + creatorId = json['creatorId'] as String; +} + class BaasApp { final String appId; final String clientAppId; final String name; final String uniqueName; + final bool isNewDeployment; Object? error; - BaasApp(this.appId, this.clientAppId, this.name, this.uniqueName); + BaasApp({required this.appId, required this.clientAppId, required this.name, required this.uniqueName, required this.isNewDeployment}); BaasApp._empty(this.name) : appId = "", clientAppId = "", - uniqueName = ""; + uniqueName = "", + isNewDeployment = false; @override String toString() { diff --git a/lib/src/cli/atlas_apps/deployapps_command.dart b/lib/src/cli/atlas_apps/deployapps_command.dart index 81b4855b6..8e3b72414 100644 --- a/lib/src/cli/atlas_apps/deployapps_command.dart +++ b/lib/src/cli/atlas_apps/deployapps_command.dart @@ -76,10 +76,12 @@ RwIDAQAB abort('--baas-url must be supplied when --baasaas-api-key is null'); } + final differentiator = options.differentiator ?? 'local'; + late String baasUrl; if (options.baasaasApiKey != null) { late String containerId; - (baasUrl, containerId) = await BaasClient.deployContainer(options.baasaasApiKey!); + (baasUrl, containerId) = await BaasClient.getOrDeployContainer(options.baasaasApiKey!, differentiator); await File('baasurl').writeAsString(baasUrl); await File('containerid').writeAsString(containerId); print('BaasUrl: $baasUrl'); @@ -87,7 +89,6 @@ RwIDAQAB baasUrl = options.baasUrl!; } - final differentiator = options.differentiator; try { final client = await (options.atlasCluster == null ? BaasClient.docker(baasUrl, differentiator) diff --git a/test/baas_helper.dart b/test/baas_helper.dart index fbfd59c22..438398acc 100644 --- a/test/baas_helper.dart +++ b/test/baas_helper.dart @@ -1,15 +1,12 @@ import 'dart:io'; import 'package:args/args.dart'; -import 'package:path/path.dart' as _path; import 'package:test/test.dart' as testing; import '../lib/realm.dart'; import '../lib/src/cli/atlas_apps/baas_client.dart'; import '../lib/src/native/realm_core.dart'; -part 'baas_helper.g.dart'; - const String argBaasUrl = "BAAS_URL"; const String argBaasCluster = "BAAS_CLUSTER"; const String argBaasApiKey = "BAAS_API_KEY"; @@ -73,28 +70,6 @@ enum AppNames { emailConfirm, } -@RealmModel() -class _BaasInfo { - late String baasUrl; - String? cluster; - String? apiKey; - String? privateApiKey; - String? projectId; - String? differentiator; - - late List<_BaasAppDetails> apps; -} - -@RealmModel(ObjectType.embeddedObject) -class _BaasAppDetails { - late String appId; - late String clientAppId; - late String name; - late String uniqueName; - - String? error; -} - class BaasHelper { final BaasClient _baasClient; final _baasApps = {}; @@ -104,22 +79,22 @@ class BaasHelper { static Object? _error; static Future setupBaas(Map args) async { - if (_error != null) { - throw _error!; - } - - final realmPath = _path.join(Configuration.defaultStoragePath, 'baasmeta', 'baas_$pid.realm'); - final realm = Realm(Configuration.local([BaasInfo.schema, BaasAppDetails.schema], path: realmPath)); - final (client, baasInfo) = await _setupClient(args, realm); - if (client == null || baasInfo == null) { - return null; - } + try { + final client = await _setupClient(args); + if (client == null) { + return null; + } - final result = BaasHelper._(client); + final result = BaasHelper._(client); - await result._setupApps(baasInfo); + await result._setupApps(); - return result; + return result; + } catch (e) { + print(e); + _error = e; + rethrow; + } } static bool shouldRunBaasTests(Map args) { @@ -128,64 +103,46 @@ class BaasHelper { BaasHelper._(this._baasClient); - static Future<(BaasClient?, BaasInfo?)> _setupClient(Map args, Realm realm) async { - try { - var baasInfo = realm.all().firstOrNull; - if (baasInfo == null) { - var baasUrl = args[argBaasUrl]; - if (baasUrl == null) { - final baasaasApiKey = args[argBaasaasApiKey]; - if (baasaasApiKey != null) { - if (args[argBaasCluster] != null) { - throw "$argBaasaasApiKey can't be combined with $argBaasCluster"; - } - - (baasUrl, _) = await BaasClient.retry(() => BaasClient.deployContainer(baasaasApiKey)); - } - } - - if (baasUrl == null) { - return (null, null); + static Future _setupClient(Map args) async { + var baasUrl = args[argBaasUrl]; + final differentiator = args[argDifferentiator] ?? 'local'; + if (baasUrl == null) { + final baasaasApiKey = args[argBaasaasApiKey]; + if (baasaasApiKey != null) { + if (args[argBaasCluster] != null) { + throw "$argBaasaasApiKey can't be combined with $argBaasCluster"; } - baasInfo = realm.write(() => realm.add(BaasInfo(baasUrl!, - cluster: args[argBaasCluster], - apiKey: args[argBaasApiKey], - privateApiKey: args[argBaasPrivateApiKey], - projectId: args[argBaasProjectId], - differentiator: args[argDifferentiator])))!; + (baasUrl, _) = await BaasClient.retry(() => BaasClient.getOrDeployContainer(baasaasApiKey, differentiator)); } + } - final client = await BaasClient.retry(() => (baasInfo!.cluster == null - ? BaasClient.docker(baasInfo.baasUrl, baasInfo.differentiator) - : BaasClient.atlas(baasInfo.baasUrl, baasInfo.cluster!, baasInfo.apiKey!, baasInfo.privateApiKey!, baasInfo.projectId!, baasInfo.differentiator))); - - client.publicRSAKey = publicRSAKeyForJWTValidation; - return (client, baasInfo); - } catch (error) { - print(error); - _error = error; - return (null, null); + if (baasUrl == null) { + return null; } + + final cluster = args[argBaasCluster]; + final apiKey = args[argBaasApiKey]; + final privateApiKey = args[argBaasPrivateApiKey]; + final projectId = args[argBaasProjectId]; + + final client = await BaasClient.retry(() => (cluster == null + ? BaasClient.docker(baasUrl!, differentiator) + : BaasClient.atlas(baasUrl!, cluster, apiKey!, privateApiKey!, projectId!, differentiator))); + + client.publicRSAKey = publicRSAKeyForJWTValidation; + return client; } - Future _setupApps(BaasInfo baasInfo) async { + Future _setupApps() async { try { - var isNewDeployment = false; - if (baasInfo.apps.isEmpty) { - final apps = await _baasClient.getOrCreateApps(); - baasInfo.realm.write(() { - baasInfo.apps.addAll(apps.map((e) => BaasAppDetails(e.appId, e.clientAppId, e.name, e.uniqueName, error: e.error?.toString()))); - }); - isNewDeployment = true; - } - - for (final app in baasInfo.apps) { - _baasApps[app.name] = BaasApp(app.appId, app.clientAppId, app.name, app.uniqueName)..error = app.error; - } + final apps = await _baasClient.getOrCreateApps(); - if (isNewDeployment) { - await _waitForInitialSync(AppNames.flexible); + for (final app in apps) { + _baasApps[app.name] = app; + if (app.name == AppNames.flexible.name && app.isNewDeployment) { + await _waitForInitialSync(app); + } } } catch (error) { print(error); @@ -193,12 +150,11 @@ class BaasHelper { } } - Future _waitForInitialSync(AppNames app) async { + Future _waitForInitialSync(BaasApp app) async { while (true) { try { - final baasApp = _baasApps[app.name]!; print('Validating initial sync is complete...'); - await _baasClient.waitForInitialSync(baasApp); + await _baasClient.waitForInitialSync(app); return; } catch (e) { print(e); @@ -213,7 +169,7 @@ class BaasHelper { return await _baasClient.createApiKey(baasApp.appId, name, enabled); } - void throwIfSetupFailed() { + static void throwIfSetupFailed() { if (_error != null) { throw _error!; } diff --git a/test/baas_helper.g.dart b/test/baas_helper.g.dart deleted file mode 100644 index fc9a7d6f4..000000000 --- a/test/baas_helper.g.dart +++ /dev/null @@ -1,170 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'baas_helper.dart'; - -// ************************************************************************** -// RealmObjectGenerator -// ************************************************************************** - -// ignore_for_file: type=lint -class BaasInfo extends _BaasInfo - with RealmEntity, RealmObjectBase, RealmObject { - BaasInfo( - String baasUrl, { - String? cluster, - String? apiKey, - String? privateApiKey, - String? projectId, - String? differentiator, - Iterable apps = const [], - }) { - RealmObjectBase.set(this, 'baasUrl', baasUrl); - RealmObjectBase.set(this, 'cluster', cluster); - RealmObjectBase.set(this, 'apiKey', apiKey); - RealmObjectBase.set(this, 'privateApiKey', privateApiKey); - RealmObjectBase.set(this, 'projectId', projectId); - RealmObjectBase.set(this, 'differentiator', differentiator); - RealmObjectBase.set>( - this, 'apps', RealmList(apps)); - } - - BaasInfo._(); - - @override - String get baasUrl => RealmObjectBase.get(this, 'baasUrl') as String; - @override - set baasUrl(String value) => RealmObjectBase.set(this, 'baasUrl', value); - - @override - String? get cluster => - RealmObjectBase.get(this, 'cluster') as String?; - @override - set cluster(String? value) => RealmObjectBase.set(this, 'cluster', value); - - @override - String? get apiKey => RealmObjectBase.get(this, 'apiKey') as String?; - @override - set apiKey(String? value) => RealmObjectBase.set(this, 'apiKey', value); - - @override - String? get privateApiKey => - RealmObjectBase.get(this, 'privateApiKey') as String?; - @override - set privateApiKey(String? value) => - RealmObjectBase.set(this, 'privateApiKey', value); - - @override - String? get projectId => - RealmObjectBase.get(this, 'projectId') as String?; - @override - set projectId(String? value) => RealmObjectBase.set(this, 'projectId', value); - - @override - String? get differentiator => - RealmObjectBase.get(this, 'differentiator') as String?; - @override - set differentiator(String? value) => - RealmObjectBase.set(this, 'differentiator', value); - - @override - RealmList get apps => - RealmObjectBase.get(this, 'apps') - as RealmList; - @override - set apps(covariant RealmList value) => - throw RealmUnsupportedSetError(); - - @override - Stream> get changes => - RealmObjectBase.getChanges(this); - - @override - BaasInfo freeze() => RealmObjectBase.freezeObject(this); - - static SchemaObject get schema => _schema ??= _initSchema(); - static SchemaObject? _schema; - static SchemaObject _initSchema() { - RealmObjectBase.registerFactory(BaasInfo._); - return const SchemaObject(ObjectType.realmObject, BaasInfo, 'BaasInfo', [ - SchemaProperty('baasUrl', RealmPropertyType.string), - SchemaProperty('cluster', RealmPropertyType.string, optional: true), - SchemaProperty('apiKey', RealmPropertyType.string, optional: true), - SchemaProperty('privateApiKey', RealmPropertyType.string, optional: true), - SchemaProperty('projectId', RealmPropertyType.string, optional: true), - SchemaProperty('differentiator', RealmPropertyType.string, - optional: true), - SchemaProperty('apps', RealmPropertyType.object, - linkTarget: 'BaasAppDetails', - collectionType: RealmCollectionType.list), - ]); - } -} - -class BaasAppDetails extends _BaasAppDetails - with RealmEntity, RealmObjectBase, EmbeddedObject { - BaasAppDetails( - String appId, - String clientAppId, - String name, - String uniqueName, { - String? error, - }) { - RealmObjectBase.set(this, 'appId', appId); - RealmObjectBase.set(this, 'clientAppId', clientAppId); - RealmObjectBase.set(this, 'name', name); - RealmObjectBase.set(this, 'uniqueName', uniqueName); - RealmObjectBase.set(this, 'error', error); - } - - BaasAppDetails._(); - - @override - String get appId => RealmObjectBase.get(this, 'appId') as String; - @override - set appId(String value) => RealmObjectBase.set(this, 'appId', value); - - @override - String get clientAppId => - RealmObjectBase.get(this, 'clientAppId') as String; - @override - set clientAppId(String value) => - RealmObjectBase.set(this, 'clientAppId', value); - - @override - String get name => RealmObjectBase.get(this, 'name') as String; - @override - set name(String value) => RealmObjectBase.set(this, 'name', value); - - @override - String get uniqueName => - RealmObjectBase.get(this, 'uniqueName') as String; - @override - set uniqueName(String value) => - RealmObjectBase.set(this, 'uniqueName', value); - - @override - String? get error => RealmObjectBase.get(this, 'error') as String?; - @override - set error(String? value) => RealmObjectBase.set(this, 'error', value); - - @override - Stream> get changes => - RealmObjectBase.getChanges(this); - - @override - BaasAppDetails freeze() => RealmObjectBase.freezeObject(this); - - static SchemaObject get schema => _schema ??= _initSchema(); - static SchemaObject? _schema; - static SchemaObject _initSchema() { - RealmObjectBase.registerFactory(BaasAppDetails._); - return const SchemaObject( - ObjectType.embeddedObject, BaasAppDetails, 'BaasAppDetails', [ - SchemaProperty('appId', RealmPropertyType.string), - SchemaProperty('clientAppId', RealmPropertyType.string), - SchemaProperty('name', RealmPropertyType.string), - SchemaProperty('uniqueName', RealmPropertyType.string), - SchemaProperty('error', RealmPropertyType.string, optional: true), - ]); - } -} diff --git a/test/test.dart b/test/test.dart index a2295abd7..91e725235 100644 --- a/test/test.dart +++ b/test/test.dart @@ -555,7 +555,7 @@ Future baasTest( AppNames appName = AppNames.flexible, dynamic skip, }) async { - baasHelper?.throwIfSetupFailed(); + BaasHelper.throwIfSetupFailed(); skip = shouldSkip(skip); From f7a2efb35fc5ed8db93f480ffe9d29549d3fa483 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Tue, 12 Dec 2023 03:21:51 +0100 Subject: [PATCH 15/27] Guard against incomplete containers --- .github/workflows/deploy-baas.yml | 3 +-- lib/src/cli/atlas_apps/baas_client.dart | 23 ++++++++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/.github/workflows/deploy-baas.yml b/.github/workflows/deploy-baas.yml index f93fc105d..69ce0b4eb 100644 --- a/.github/workflows/deploy-baas.yml +++ b/.github/workflows/deploy-baas.yml @@ -14,7 +14,6 @@ on: env: REALM_CI: true - BAAS_DIFFERENTIATOR: ${{ inputs.differentiator }} jobs: deploy-baas: @@ -40,7 +39,7 @@ jobs: run: dart pub get - name: Deploy cluster and apps - run: dart run realm_dart deploy-apps --baasaas-api-key ${{ secrets.BAASAAS_API_KEY }} + run: dart run realm_dart deploy-apps --baasaas-api-key ${{ secrets.BAASAAS_API_KEY }} --differentiator ${{ inputs.differentiator }} - name: Set outputs id: set-outputs run: | diff --git a/lib/src/cli/atlas_apps/baas_client.dart b/lib/src/cli/atlas_apps/baas_client.dart index 860d43498..4217baf8c 100644 --- a/lib/src/cli/atlas_apps/baas_client.dart +++ b/lib/src/cli/atlas_apps/baas_client.dart @@ -199,7 +199,7 @@ class BaasClient { } static Future> _getContainers(BaasAuthHelper helper) async { - return (await helper.callEndpoint('listContainers', isPost: false) as List).map((e) => _ContainerInfo.fromJson(e)).toList(); + return (await helper.callEndpoint('listContainers', isPost: false) as List).map((e) => _ContainerInfo.fromJson(e)).whereNotNull().toList(); } static Future _waitForContainer(BaasAuthHelper authHelper, String taskId) async { @@ -720,12 +720,21 @@ class _ContainerInfo { final Map tags; final String creatorId; - _ContainerInfo.fromJson(Map json) - : id = json['id'] as String, - lastStatus = json['lastStatus'], - httpUrl = json['httpUrl'] as String, - tags = {for (var v in json['tags'] as List) v['key']: v['value']}, - creatorId = json['creatorId'] as String; + _ContainerInfo._(this.id, this.httpUrl, this.lastStatus, this.tags, this.creatorId); + + static _ContainerInfo? fromJson(Map json) { + final httpUrl = json['httpUrl'] as String?; + if (httpUrl == null) { + return null; + } + + final id = json['id'] as String; + final lastStatus = json['lastStatus']; + final tags = {for (var v in json['tags'] as List) v['key'] as String: v['value'] as String}; + final creatorId = json['creatorId'] as String; + + return _ContainerInfo._(id, httpUrl, lastStatus, tags, creatorId); + } } class BaasApp { From f848a9f6fc52cb33cbdadb4a7237debff8c230a2 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Tue, 12 Dec 2023 04:00:48 +0100 Subject: [PATCH 16/27] Fix cleanup workflow; fix env variable names --- .github/workflows/ci.yml | 11 +++++++++++ .github/workflows/dart-desktop-tests.yml | 2 ++ .github/workflows/flutter-desktop-tests.yml | 2 +- .github/workflows/terminate-baas.yml | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aab028672..89e5c7605 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,6 +90,7 @@ jobs: needs: - dart-tests-windows - deploy-cluster-dart-windows + secrets: inherit with: containerId: ${{ needs.deploy-cluster-dart-windows.outputs.containerId }} @@ -118,6 +119,7 @@ jobs: needs: - dart-tests-macos - deploy-cluster-dart-macos + secrets: inherit with: containerId: ${{ needs.deploy-cluster-dart-macos.outputs.containerId }} @@ -147,6 +149,7 @@ jobs: needs: - dart-tests-macos-arm - deploy-cluster-dart-macos-arm + secrets: inherit with: containerId: ${{ needs.deploy-cluster-dart-macos-arm.outputs.containerId }} @@ -175,6 +178,7 @@ jobs: needs: - dart-tests-linux - deploy-cluster-dart-linux + secrets: inherit with: containerId: ${{ needs.deploy-cluster-dart-linux.outputs.containerId }} @@ -205,6 +209,7 @@ jobs: needs: - flutter-tests-windows - deploy-cluster-flutter-windows + secrets: inherit with: containerId: ${{ needs.deploy-cluster-flutter-windows.outputs.containerId }} @@ -233,6 +238,7 @@ jobs: needs: - flutter-tests-macos - deploy-cluster-flutter-macos + secrets: inherit with: containerId: ${{ needs.deploy-cluster-flutter-macos.outputs.containerId }} @@ -261,6 +267,7 @@ jobs: needs: - flutter-tests-linux - deploy-cluster-flutter-linux + secrets: inherit with: containerId: ${{ needs.deploy-cluster-flutter-linux.outputs.containerId }} @@ -280,6 +287,7 @@ jobs: - build-ios-xcframework env: BAAS_DIFFERENTIATOR: fi${{ github.run_id }}${{ github.run_attempt }} + BAAS_BAASAAS_API_KEY: ${{ secrets.BAASAAS_API_KEY}} steps: - name: Checkout @@ -322,6 +330,7 @@ jobs: needs: - flutter-tests-ios - deploy-cluster-flutter-ios + secrets: inherit with: containerId: ${{ needs.deploy-cluster-flutter-ios.outputs.containerId }} @@ -342,6 +351,7 @@ jobs: - build-android-combined env: BAAS_DIFFERENTIATOR: fi${{ github.run_id }}${{ github.run_attempt }} + BAAS_BAASAAS_API_KEY: ${{ secrets.BAASAAS_API_KEY}} steps: - name: Checkout @@ -411,6 +421,7 @@ jobs: needs: - flutter-tests-android - deploy-cluster-flutter-android + secrets: inherit with: containerId: ${{ needs.deploy-cluster-flutter-android.outputs.containerId }} diff --git a/.github/workflows/dart-desktop-tests.yml b/.github/workflows/dart-desktop-tests.yml index f5ce068fd..2de9d5744 100644 --- a/.github/workflows/dart-desktop-tests.yml +++ b/.github/workflows/dart-desktop-tests.yml @@ -22,6 +22,8 @@ on: env: REALM_CI: true + BAAS_BAASAAS_API_KEY: ${{ secrets.BAASAAS_API_KEY}} + BAAS_DIFFERENTIATOR: ${{ inputs.differentiator }} jobs: dart-tests: diff --git a/.github/workflows/flutter-desktop-tests.yml b/.github/workflows/flutter-desktop-tests.yml index 27b874f97..c808d1e59 100644 --- a/.github/workflows/flutter-desktop-tests.yml +++ b/.github/workflows/flutter-desktop-tests.yml @@ -29,7 +29,7 @@ jobs: name: Flutter tests on ${{inputs.os }}-${{ inputs.architecture }} timeout-minutes: 45 env: - BAASAAS_API_KEY: ${{ secrets.BAASAAS_API_KEY}} + BAAS_BAASAAS_API_KEY: ${{ secrets.BAASAAS_API_KEY}} BAAS_DIFFERENTIATOR: ${{ inputs.differentiator }} steps: diff --git a/.github/workflows/terminate-baas.yml b/.github/workflows/terminate-baas.yml index ecc47df38..dc750406d 100644 --- a/.github/workflows/terminate-baas.yml +++ b/.github/workflows/terminate-baas.yml @@ -31,4 +31,4 @@ jobs: run: dart pub get - name: Terminate baas - run: dart run realm_dart delete-apps --use-baas-aas --container-id ${{ inputs.containerId }} + run: dart run realm_dart delete-apps --baasaas-api-key ${{ secrets.BAASAAS_API_KEY }} --container-id ${{ inputs.containerId }} From 61c84e46126bbacd86d463aae8852535174932bf Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Tue, 12 Dec 2023 04:22:29 +0100 Subject: [PATCH 17/27] Don't log isolate warnings when the isolate is created by dart test --- .github/workflows/ci.yml | 18 +++++++++--------- .../tests/test_driver/app_test.dart | 1 + lib/src/app.dart | 6 +++++- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89e5c7605..01bf69871 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,7 +73,7 @@ jobs: differentiator: dw${{ github.run_id }}${{ github.run_attempt }} dart-tests-windows: - name: Windows Dart Tests + name: Dart Tests Windows uses: ./.github/workflows/dart-desktop-tests.yml needs: - build-windows @@ -102,7 +102,7 @@ jobs: differentiator: dm${{ github.run_id }}${{ github.run_attempt }} dart-tests-macos: - name: MacOS Dart Tests + name: Dart Tests MacOS uses: ./.github/workflows/dart-desktop-tests.yml needs: - build-macos @@ -131,7 +131,7 @@ jobs: differentiator: dma${{ github.run_id }}${{ github.run_attempt }} dart-tests-macos-arm: - name: MacOS Arm Dart Tests + name: Dart Tests MacOS Arm uses: ./.github/workflows/dart-desktop-tests.yml needs: - build-macos @@ -161,7 +161,7 @@ jobs: differentiator: dl${{ github.run_id }}${{ github.run_attempt }} dart-tests-linux: - name: Linux Dart Tests + name: Dart Tests Linux uses: ./.github/workflows/dart-desktop-tests.yml needs: - build-linux @@ -191,7 +191,7 @@ jobs: differentiator: fw${{ github.run_id }}${{ github.run_attempt }} flutter-tests-windows: - name: Windows Flutter Tests + name: Flutter Tests Windows uses: ./.github/workflows/flutter-desktop-tests.yml needs: - build-windows @@ -221,7 +221,7 @@ jobs: differentiator: fm${{ github.run_id }}${{ github.run_attempt }} flutter-tests-macos: - name: MacOS Flutter Tests + name: Flutter Tests MacOS uses: ./.github/workflows/flutter-desktop-tests.yml needs: - build-macos @@ -250,7 +250,7 @@ jobs: differentiator: fl${{ github.run_id }}${{ github.run_attempt }} flutter-tests-linux: - name: Linux Flutter Tests + name: Flutter Tests Linux uses: ./.github/workflows/flutter-desktop-tests.yml needs: - build-linux @@ -280,7 +280,7 @@ jobs: flutter-tests-ios: runs-on: macos-latest - name: IOS Flutter Tests + name: Flutter Tests iOS timeout-minutes: 45 needs: - deploy-cluster-flutter-ios @@ -344,7 +344,7 @@ jobs: flutter-tests-android: runs-on: macos-latest - name: Android Flutter Tests + name: Flutter Tests Android timeout-minutes: 45 needs: - deploy-cluster-flutter-android diff --git a/flutter/realm_flutter/tests/test_driver/app_test.dart b/flutter/realm_flutter/tests/test_driver/app_test.dart index 68636baf3..1f914a9c8 100644 --- a/flutter/realm_flutter/tests/test_driver/app_test.dart +++ b/flutter/realm_flutter/tests/test_driver/app_test.dart @@ -26,6 +26,7 @@ void main(List args) { testCommandWithArgs += getArgFromEnvVariable("BAAS_PRIVATE_API_KEY"); testCommandWithArgs += getArgFromEnvVariable("BAAS_PROJECT_ID"); testCommandWithArgs += getArgFromEnvVariable("BAAS_DIFFERENTIATOR"); + testCommandWithArgs += getArgFromEnvVariable("BAAS_BAASAAS_API_KEY"); String result = await driver!.requestData(testCommandWithArgs, timeout: const Duration(minutes: 30)); if (result.isNotEmpty) { diff --git a/lib/src/app.dart b/lib/src/app.dart index 8d06c9449..449632874 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -176,7 +176,11 @@ class App implements Finalizable { /// Create an app with a particular [AppConfiguration]. This constructor should only be used on the main isolate and, /// ideally, only once as soon as the app starts. App(AppConfiguration configuration) : _handle = _createApp(configuration) { - if (Isolate.current.debugName != 'main') { + // This is not foolproof, but could point people to errors they may have in their app. Realm apps are cached natively, so calling App(config) + // on a background isolate will not recreate the app. Instead, users should construct the app on the main isolate and then call getById on the + // background isolates. This check will log a warning if the isolate name is != 'main' and doesn't start with 'test/' since dart test will + // construct a new isolate per file and we don't want to log excessively in unit test projects. + if (Isolate.current.debugName != 'main' && Isolate.current.debugName?.startsWith('test/') == false) { Realm.logger.log(RealmLogLevel.warn, "App constructor called on Isolate ${Isolate.current.debugName} which doesn't appear to be the main isolate. If you need an app instance on a background isolate use App.getById after constructing the App on the main isolate."); } From 120e1b5fc6564fe3c6bc8d90780629fa5e8bd0c8 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Tue, 12 Dec 2023 04:47:53 +0100 Subject: [PATCH 18/27] Pass correct differentiator to android tests --- .github/workflows/ci.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01bf69871..c5b9596bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,12 +54,12 @@ jobs: build: '["ios-device", "ios-simulator", "ios-catalyst"]' build-android-combined: - name: Android binaries combine + name: Build combine Android needs: build-android uses: ./.github/workflows/binary-combine-android.yml build-ios-xcframework: - name: IOS binaries combine + name: Build combine iOS needs: build-ios uses: ./.github/workflows/binary-combine-ios.yml @@ -341,7 +341,6 @@ jobs: with: differentiator: fa${{ github.run_id }}${{ github.run_attempt }} - flutter-tests-android: runs-on: macos-latest name: Flutter Tests Android @@ -350,7 +349,7 @@ jobs: - deploy-cluster-flutter-android - build-android-combined env: - BAAS_DIFFERENTIATOR: fi${{ github.run_id }}${{ github.run_attempt }} + BAAS_DIFFERENTIATOR: fa${{ github.run_id }}${{ github.run_attempt }} BAAS_BAASAAS_API_KEY: ${{ secrets.BAASAAS_API_KEY}} steps: From 69feaea217810f49fdb5f552c99d4bd9ea003579 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Tue, 12 Dec 2023 05:25:40 +0100 Subject: [PATCH 19/27] Always run cleanup --- .github/workflows/ci.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5b9596bb..378474d86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,6 +87,7 @@ jobs: cleanup-cluster-dart-windows: name: Cleanup Cluster for Dart Windows uses: ./.github/workflows/terminate-baas.yml + if: always() needs: - dart-tests-windows - deploy-cluster-dart-windows @@ -116,6 +117,7 @@ jobs: cleanup-cluster-dart-macos: name: Cleanup Cluster for Dart macOS uses: ./.github/workflows/terminate-baas.yml + if: always() needs: - dart-tests-macos - deploy-cluster-dart-macos @@ -146,6 +148,7 @@ jobs: cleanup-cluster-dart-macos-arm: name: Cleanup Cluster for Dart macOS Arm uses: ./.github/workflows/terminate-baas.yml + if: always() needs: - dart-tests-macos-arm - deploy-cluster-dart-macos-arm @@ -175,6 +178,7 @@ jobs: cleanup-cluster-dart-linux: name: Cleanup Cluster for Dart Linux uses: ./.github/workflows/terminate-baas.yml + if: always() needs: - dart-tests-linux - deploy-cluster-dart-linux @@ -206,6 +210,7 @@ jobs: cleanup-cluster-flutter-windows: name: Cleanup Cluster for Flutter Windows uses: ./.github/workflows/terminate-baas.yml + if: always() needs: - flutter-tests-windows - deploy-cluster-flutter-windows @@ -235,6 +240,7 @@ jobs: cleanup-cluster-flutter-macos: name: Cleanup Cluster for Flutter macOS uses: ./.github/workflows/terminate-baas.yml + if: always() needs: - flutter-tests-macos - deploy-cluster-flutter-macos @@ -264,6 +270,7 @@ jobs: cleanup-cluster-flutter-linux: name: Cleanup Cluster for Flutter Linux uses: ./.github/workflows/terminate-baas.yml + if: always() needs: - flutter-tests-linux - deploy-cluster-flutter-linux @@ -327,6 +334,7 @@ jobs: cleanup-cluster-flutter-ios: name: Cleanup Cluster for Flutter iOS uses: ./.github/workflows/terminate-baas.yml + if: always() needs: - flutter-tests-ios - deploy-cluster-flutter-ios @@ -417,6 +425,7 @@ jobs: cleanup-cluster-flutter-android: name: Cleanup Cluster for Flutter Android uses: ./.github/workflows/terminate-baas.yml + if: always() needs: - flutter-tests-android - deploy-cluster-flutter-android From 67b29bfb9743104748747723151b8e22a9297bc2 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Tue, 12 Dec 2023 20:31:04 +0100 Subject: [PATCH 20/27] Rework cleanup command --- .github/workflows/ci.yml | 27 ++++++----------- .github/workflows/deploy-baas.yml | 12 -------- .github/workflows/terminate-baas.yml | 7 +++-- lib/src/cli/atlas_apps/baas_client.dart | 29 ++++++++++++------- .../cli/atlas_apps/deleteapps_command.dart | 10 ++----- lib/src/cli/atlas_apps/options.dart | 5 +--- lib/src/cli/atlas_apps/options.g.dart | 6 ---- 7 files changed, 35 insertions(+), 61 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 378474d86..2d3d96e5d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,10 +90,9 @@ jobs: if: always() needs: - dart-tests-windows - - deploy-cluster-dart-windows secrets: inherit with: - containerId: ${{ needs.deploy-cluster-dart-windows.outputs.containerId }} + differentiator: dw${{ github.run_id }}${{ github.run_attempt }} deploy-cluster-dart-macos: name: Deploy Cluster for Dart MacOS @@ -120,10 +119,9 @@ jobs: if: always() needs: - dart-tests-macos - - deploy-cluster-dart-macos secrets: inherit with: - containerId: ${{ needs.deploy-cluster-dart-macos.outputs.containerId }} + differentiator: dm${{ github.run_id }}${{ github.run_attempt }} deploy-cluster-dart-macos-arm: name: Deploy Cluster for Dart MacOS Arm @@ -151,10 +149,9 @@ jobs: if: always() needs: - dart-tests-macos-arm - - deploy-cluster-dart-macos-arm secrets: inherit with: - containerId: ${{ needs.deploy-cluster-dart-macos-arm.outputs.containerId }} + differentiator: dma${{ github.run_id }}${{ github.run_attempt }} deploy-cluster-dart-linux: name: Deploy Cluster for Dart Linux @@ -181,10 +178,9 @@ jobs: if: always() needs: - dart-tests-linux - - deploy-cluster-dart-linux secrets: inherit with: - containerId: ${{ needs.deploy-cluster-dart-linux.outputs.containerId }} + differentiator: dl${{ github.run_id }}${{ github.run_attempt }} # Flutter jobs deploy-cluster-flutter-windows: @@ -213,10 +209,9 @@ jobs: if: always() needs: - flutter-tests-windows - - deploy-cluster-flutter-windows secrets: inherit with: - containerId: ${{ needs.deploy-cluster-flutter-windows.outputs.containerId }} + differentiator: fw${{ github.run_id }}${{ github.run_attempt }} deploy-cluster-flutter-macos: name: Deploy Cluster for Flutter MacOS @@ -243,10 +238,9 @@ jobs: if: always() needs: - flutter-tests-macos - - deploy-cluster-flutter-macos secrets: inherit with: - containerId: ${{ needs.deploy-cluster-flutter-macos.outputs.containerId }} + differentiator: fm${{ github.run_id }}${{ github.run_attempt }} deploy-cluster-flutter-linux: name: Deploy Cluster for Flutter Linux @@ -273,10 +267,9 @@ jobs: if: always() needs: - flutter-tests-linux - - deploy-cluster-flutter-linux secrets: inherit with: - containerId: ${{ needs.deploy-cluster-flutter-linux.outputs.containerId }} + differentiator: fl${{ github.run_id }}${{ github.run_attempt }} deploy-cluster-flutter-ios: name: Deploy Cluster for Flutter iOS @@ -337,10 +330,9 @@ jobs: if: always() needs: - flutter-tests-ios - - deploy-cluster-flutter-ios secrets: inherit with: - containerId: ${{ needs.deploy-cluster-flutter-ios.outputs.containerId }} + differentiator: fi${{ github.run_id }}${{ github.run_attempt }} deploy-cluster-flutter-android: name: Deploy Cluster for Flutter Android @@ -428,10 +420,9 @@ jobs: if: always() needs: - flutter-tests-android - - deploy-cluster-flutter-android secrets: inherit with: - containerId: ${{ needs.deploy-cluster-flutter-android.outputs.containerId }} + differentiator: fa${{ github.run_id }}${{ github.run_attempt }} # Generator jobs diff --git a/.github/workflows/deploy-baas.yml b/.github/workflows/deploy-baas.yml index 69ce0b4eb..7dcc06302 100644 --- a/.github/workflows/deploy-baas.yml +++ b/.github/workflows/deploy-baas.yml @@ -2,10 +2,6 @@ name: Deploy BaaS and apps on: workflow_call: - outputs: - containerId: - description: The container id of the BaaS instance - value: ${{ jobs.deploy-baas.outputs.containerId }} inputs: differentiator: description: Differentiator for the BaaS container. @@ -20,9 +16,6 @@ jobs: runs-on: ubuntu-latest name: Deploy BaaS timeout-minutes: 15 - outputs: - url: ${{ steps.set-outputs.outputs.baasUrl }} - containerId: ${{ steps.set-outputs.outputs.containerId }} steps: - name: Checkout uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 @@ -40,8 +33,3 @@ jobs: - name: Deploy cluster and apps run: dart run realm_dart deploy-apps --baasaas-api-key ${{ secrets.BAASAAS_API_KEY }} --differentiator ${{ inputs.differentiator }} - - name: Set outputs - id: set-outputs - run: | - container_id=`cat containerid` - echo "containerId=$container_id" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/terminate-baas.yml b/.github/workflows/terminate-baas.yml index dc750406d..cf1de2da5 100644 --- a/.github/workflows/terminate-baas.yml +++ b/.github/workflows/terminate-baas.yml @@ -3,8 +3,9 @@ name: Terminate BaaS on: workflow_call: inputs: - containerId: - description: "The url to connect to" + differentiator: + description: Differentiator for the BaaS container. + required: true type: string env: @@ -31,4 +32,4 @@ jobs: run: dart pub get - name: Terminate baas - run: dart run realm_dart delete-apps --baasaas-api-key ${{ secrets.BAASAAS_API_KEY }} --container-id ${{ inputs.containerId }} + run: dart run realm_dart delete-apps --baasaas-api-key ${{ secrets.BAASAAS_API_KEY }} --differentiator ${{ inputs.differentiator }} diff --git a/lib/src/cli/atlas_apps/baas_client.dart b/lib/src/cli/atlas_apps/baas_client.dart index 4217baf8c..b5b4df118 100644 --- a/lib/src/cli/atlas_apps/baas_client.dart +++ b/lib/src/cli/atlas_apps/baas_client.dart @@ -142,25 +142,26 @@ class BaasClient { return result; } - static Future deleteContainer(String id, String apiKey) async { + static Future deleteContainer(String apiKey, String differentiator) async { try { - print('Deleting BaaS container $id... '); - + print('Stopping all containers with differentiator $differentiator'); final authHelper = BaasAuthHelper(apiKey); - - await authHelper.callEndpoint('stopContainer', query: {'id': id}); + final containers = await _getContainers(authHelper, differentiator: differentiator); + for (final container in containers) { + print('Stopping container ${container.id}'); + await authHelper.callEndpoint('stopContainer', query: {'id': container.id}); + print('Stopped container ${container.id}'); + } return; } catch (e) { - print('Failed to deploy container: $e'); + print('Failed to destroy container: $e'); rethrow; } } static Future<(String httpUrl, String containerId)> getOrDeployContainer(String apiKey, String differentiator) async { final authHelper = BaasAuthHelper(apiKey); - final containers = await _getContainers(authHelper); - final userId = await authHelper.getUserId(); - final existing = containers.firstWhereOrNull((c) => c.creatorId == userId && c.tags['DIFFERENTIATOR'] == differentiator); + final existing = (await _getContainers(authHelper, differentiator: differentiator)).firstOrNull; if (existing != null) { print('Using existing BaaS container at ${existing.httpUrl}'); return (existing.httpUrl, existing.id); @@ -198,8 +199,14 @@ class BaasClient { throw 'UNREACHABLE'; } - static Future> _getContainers(BaasAuthHelper helper) async { - return (await helper.callEndpoint('listContainers', isPost: false) as List).map((e) => _ContainerInfo.fromJson(e)).whereNotNull().toList(); + static Future> _getContainers(BaasAuthHelper helper, {String? differentiator}) async { + var result = (await helper.callEndpoint('listContainers', isPost: false) as List).map((e) => _ContainerInfo.fromJson(e)).whereNotNull(); + if (differentiator != null) { + final userId = await helper.getUserId(); + result = result.where((c) => c.creatorId == userId && c.tags['DIFFERENTIATOR'] == differentiator); + } + + return result.toList(); } static Future _waitForContainer(BaasAuthHelper authHelper, String taskId) async { diff --git a/lib/src/cli/atlas_apps/deleteapps_command.dart b/lib/src/cli/atlas_apps/deleteapps_command.dart index eb84c0b22..64a6802a4 100644 --- a/lib/src/cli/atlas_apps/deleteapps_command.dart +++ b/lib/src/cli/atlas_apps/deleteapps_command.dart @@ -61,15 +61,11 @@ class DeleteAppsCommand extends Command { abort('--baas-url must be supplied when --baasaas-api-key is null'); } - if (options.baasaasApiKey != null) { - if (options.containerId == null) { - abort('--container-id must be supplied when --baasaas-api-key is set'); - } + final differentiator = options.differentiator ?? 'local'; - await BaasClient.retry(() => BaasClient.deleteContainer(options.containerId!, options.baasaasApiKey!)); + if (options.baasaasApiKey != null) { + await BaasClient.retry(() => BaasClient.deleteContainer(options.baasaasApiKey!, differentiator)); } else { - final differentiator = options.differentiator ?? 'local'; - final client = await (options.atlasCluster == null ? BaasClient.docker(options.baasUrl!, differentiator) : BaasClient.atlas(options.baasUrl!, options.atlasCluster!, options.apiKey!, options.privateApiKey!, options.projectId!, differentiator)); diff --git a/lib/src/cli/atlas_apps/options.dart b/lib/src/cli/atlas_apps/options.dart index 303795b34..8be1a1019 100644 --- a/lib/src/cli/atlas_apps/options.dart +++ b/lib/src/cli/atlas_apps/options.dart @@ -43,10 +43,7 @@ class Options { @CliOption(help: 'API key to use with BaaSaaS to wpawn a new container and create apps in it.', name: 'baasaas-api-key') final String? baasaasApiKey; - @CliOption(help: 'Container id to be cleaned up. Only to be used with use-baas-aas and delete command') - final String? containerId; - - Options({this.baasUrl, this.atlasCluster, this.apiKey, this.privateApiKey, this.projectId, this.differentiator, this.baasaasApiKey, this.containerId}); + Options({this.baasUrl, this.atlasCluster, this.apiKey, this.privateApiKey, this.projectId, this.differentiator, this.baasaasApiKey}); } String get usage => _$parserForOptions.usage; diff --git a/lib/src/cli/atlas_apps/options.g.dart b/lib/src/cli/atlas_apps/options.g.dart index 083b49525..2b90d9b07 100644 --- a/lib/src/cli/atlas_apps/options.g.dart +++ b/lib/src/cli/atlas_apps/options.g.dart @@ -14,7 +14,6 @@ Options _$parseOptionsResult(ArgResults result) => Options( projectId: result['project-id'] as String?, differentiator: result['differentiator'] as String?, baasaasApiKey: result['baasaas-api-key'] as String?, - containerId: result['container-id'] as String?, ); ArgParser _$populateOptionsParser(ArgParser parser) => parser @@ -49,11 +48,6 @@ ArgParser _$populateOptionsParser(ArgParser parser) => parser 'baasaas-api-key', help: 'API key to use with BaaSaaS to wpawn a new container and create apps in it.', - ) - ..addOption( - 'container-id', - help: - 'Container id to be cleaned up. Only to be used with use-baas-aas and delete command', ); final _$parserForOptions = _$populateOptionsParser(ArgParser()); From 892c40849b1af3f728d988dbfe332d539e3ba417 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Wed, 13 Dec 2023 07:31:39 +0100 Subject: [PATCH 21/27] Use reporter --- .github/workflows/dart-desktop-tests.yml | 10 +++++++++- test/realm_test.dart | 4 ++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dart-desktop-tests.yml b/.github/workflows/dart-desktop-tests.yml index 2de9d5744..790a6fe49 100644 --- a/.github/workflows/dart-desktop-tests.yml +++ b/.github/workflows/dart-desktop-tests.yml @@ -60,7 +60,15 @@ jobs: if: ${{ contains(inputs.os, 'macos') }} - name: Run tests - run: ${{ inputs.architecture == 'arm' && 'arch -arm64 ' || '' }}dart test -r expanded --coverage ./coverage/ -j 1 --test-randomize-ordering-seed random + run: ${{ inputs.architecture == 'arm' && 'arch -arm64 ' || '' }}dart test -r expanded --coverage ./coverage/ -j 1 --test-randomize-ordering-seed random --file-reporter="json:test-results.json" + + - name: Publish Test Report + uses: dorny/test-reporter@v1.7.0 + if: success() || failure() + with: + name: Test Results Dart ${{ inputs.os }} ${{ inputs.architecture }} + path: test-results.json + reporter: dart-json # we're pruning generated files, the cli folder, as well as realm_bindings.dart from our coverage reports - name: Generate realm_dart coverage report diff --git a/test/realm_test.dart b/test/realm_test.dart index 309e3a1f5..489528dba 100644 --- a/test/realm_test.dart +++ b/test/realm_test.dart @@ -33,6 +33,10 @@ import '../lib/src/native/realm_core.dart'; Future main([List? args]) async { await setupTests(args); + test('Expect this test to fail', () { + expect(5, 4); + }); + test('Realm can be created', () { var config = Configuration.local([Car.schema]); var realm = getRealm(config); From 3acebef5f608b2123825d35c18cdc11d30ba067d Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Wed, 13 Dec 2023 07:49:11 +0100 Subject: [PATCH 22/27] Don't fail test run on test errors --- .github/workflows/dart-desktop-tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dart-desktop-tests.yml b/.github/workflows/dart-desktop-tests.yml index 790a6fe49..8afb8d75e 100644 --- a/.github/workflows/dart-desktop-tests.yml +++ b/.github/workflows/dart-desktop-tests.yml @@ -60,7 +60,7 @@ jobs: if: ${{ contains(inputs.os, 'macos') }} - name: Run tests - run: ${{ inputs.architecture == 'arm' && 'arch -arm64 ' || '' }}dart test -r expanded --coverage ./coverage/ -j 1 --test-randomize-ordering-seed random --file-reporter="json:test-results.json" + run: ${{ inputs.architecture == 'arm' && 'arch -arm64 ' || '' }}dart test -r expanded --coverage ./coverage/ -j 1 --test-randomize-ordering-seed random --file-reporter="json:test-results.json" || true - name: Publish Test Report uses: dorny/test-reporter@v1.7.0 @@ -69,6 +69,8 @@ jobs: name: Test Results Dart ${{ inputs.os }} ${{ inputs.architecture }} path: test-results.json reporter: dart-json + only-summary: true + fail-on-error: false # we're pruning generated files, the cli folder, as well as realm_bindings.dart from our coverage reports - name: Generate realm_dart coverage report From 9d8e858b4dcafe861efa4b681ca628856569282e Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Wed, 13 Dec 2023 14:37:03 +0100 Subject: [PATCH 23/27] Fail on error --- .github/workflows/dart-desktop-tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/dart-desktop-tests.yml b/.github/workflows/dart-desktop-tests.yml index 8afb8d75e..c5a157a3f 100644 --- a/.github/workflows/dart-desktop-tests.yml +++ b/.github/workflows/dart-desktop-tests.yml @@ -70,7 +70,6 @@ jobs: path: test-results.json reporter: dart-json only-summary: true - fail-on-error: false # we're pruning generated files, the cli folder, as well as realm_bindings.dart from our coverage reports - name: Generate realm_dart coverage report From 1cd60efc04b59371f945e6682dcb5c661c0daecb Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Tue, 2 Jan 2024 16:45:39 +0100 Subject: [PATCH 24/27] Remove failing test --- test/realm_test.dart | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/realm_test.dart b/test/realm_test.dart index 489528dba..309e3a1f5 100644 --- a/test/realm_test.dart +++ b/test/realm_test.dart @@ -33,10 +33,6 @@ import '../lib/src/native/realm_core.dart'; Future main([List? args]) async { await setupTests(args); - test('Expect this test to fail', () { - expect(5, 4); - }); - test('Realm can be created', () { var config = Configuration.local([Car.schema]); var realm = getRealm(config); From 3e3d0bb65f9bdeea299c5cc8d59d68ead132dfc3 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Wed, 3 Jan 2024 18:01:42 +0100 Subject: [PATCH 25/27] Update lib/src/cli/atlas_apps/options.dart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kasper Overgård Nielsen --- lib/src/cli/atlas_apps/options.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/cli/atlas_apps/options.dart b/lib/src/cli/atlas_apps/options.dart index 8be1a1019..b98658b83 100644 --- a/lib/src/cli/atlas_apps/options.dart +++ b/lib/src/cli/atlas_apps/options.dart @@ -40,7 +40,7 @@ class Options { @CliOption(help: 'The Atlas project id to use for the import. Only used if atlas-cluster is specified.') final String? projectId; - @CliOption(help: 'API key to use with BaaSaaS to wpawn a new container and create apps in it.', name: 'baasaas-api-key') + @CliOption(help: 'API key to use with BaaSaaS to spawn a new container and create apps in it.', name: 'baasaas-api-key') final String? baasaasApiKey; Options({this.baasUrl, this.atlasCluster, this.apiKey, this.privateApiKey, this.projectId, this.differentiator, this.baasaasApiKey}); From 0bc8becdf3db400bf42782e4c86871557ec4f802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Wed, 3 Jan 2024 18:05:34 +0100 Subject: [PATCH 26/27] Support maps (#1406) * Update generator to support maps * Generator updates * Better error message for wrong key type * Remove the default value lists * Add tests for non-empty default collection initializers * Wire up some of the implementation * Wire up most of the test infrastructure * Add notification tests * Fix generator test expects * Fix tests * Add changelog, clean up the generator a little * Revert some unneeded changes * Fix expectations --------- Co-authored-by: Nikola Irinchev --- CHANGELOG.md | 22 +- common/lib/src/realm_types.dart | 20 +- generator/lib/src/dart_type_ex.dart | 14 +- generator/lib/src/field_element_ex.dart | 113 +- generator/lib/src/realm_field_info.dart | 16 +- generator/lib/src/realm_model_info.dart | 19 +- .../dict_non_empty_initializer.dart | 9 + .../dict_non_empty_initializer.expected | 13 + .../list_non_empty_initializer.dart | 9 + .../list_non_empty_initializer.expected | 13 + .../test/error_test_data/map_unsupported.dart | 2 +- .../error_test_data/map_unsupported.expected | 8 +- .../error_test_data/nullable_list.expected | 2 +- .../nullable_list_elements.expected | 2 +- .../nullable_realm_value.expected | 2 +- .../set_non_empty_initializer.dart | 9 + .../set_non_empty_initializer.expected | 13 + ...realm_set_of_nullable_realmobject.expected | 2 +- ..._realm_set_of_nullable_realmvalue.expected | 4 +- ...ted_realm_set_with_default_values.expected | 4 +- generator/test/good_test_data/all_types.dart | 6 +- .../test/good_test_data/all_types.expected | 22 + .../good_test_data/list_initialization.dart | 12 + .../list_initialization.expected | 109 +- generator/test/good_test_data/map.dart | 25 + generator/test/good_test_data/map.expected | 197 ++++ lib/src/collections.dart | 9 + lib/src/list.dart | 12 +- lib/src/map.dart | 304 ++++++ lib/src/native/realm_core.dart | 216 ++++ lib/src/realm_class.dart | 19 +- lib/src/realm_object.dart | 188 ++-- lib/src/results.dart | 29 +- lib/src/set.dart | 12 +- test/backlinks_test.dart | 2 +- test/geospatial_test.dart | 4 +- test/realm_map_test.dart | 966 ++++++++++++++++++ test/realm_map_test.g.dart | 375 +++++++ test/realm_value_test.dart | 6 +- test/results_test.dart | 6 +- test/test.dart | 2 +- 41 files changed, 2624 insertions(+), 193 deletions(-) create mode 100644 generator/test/error_test_data/dict_non_empty_initializer.dart create mode 100644 generator/test/error_test_data/dict_non_empty_initializer.expected create mode 100644 generator/test/error_test_data/list_non_empty_initializer.dart create mode 100644 generator/test/error_test_data/list_non_empty_initializer.expected create mode 100644 generator/test/error_test_data/set_non_empty_initializer.dart create mode 100644 generator/test/error_test_data/set_non_empty_initializer.expected create mode 100644 generator/test/good_test_data/map.dart create mode 100644 generator/test/good_test_data/map.expected create mode 100644 lib/src/map.dart create mode 100644 test/realm_map_test.dart create mode 100644 test/realm_map_test.g.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ad4670c9..f5e5c2e8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,17 +5,37 @@ * The `App(AppConfiguration)` constructor should only be used on the main isolate. Ideally, it should be called once as soon as your app launches. If you attempt to use it on a background isolate (as indicated by `Isolate.debugName` being different from `main`), a warning will be logged. * Added a new method - `App.getById` that allows you to obtain an already constructed app on a background isolate. (Issue [#1433](https://github.com/realm/realm-dart/issues/1433)) +* Added support for fields of type `Map` where `T` is any supported Realm type. You can define a model with a map like: + ```dart + @RealmModel() + class _LotsOfMaps { + late Map persons; + late Map bools; + late Map dateTimes; + late Map decimals; + late Map doubles; + late Map ints; + late Map objectIds; + late Map realmValues; + late Map strings; + late Map datas; + late Map uuids; + } + ``` + + The map keys may not contain `.` or start with `$`. (Issue [#685](https://github.com/realm/realm-dart/issues/685)) ### Fixed * Fixed warnings being emitted by the realm generator requesting that `xyz.g.dart` be included with `part 'xyz.g.dart';` for `xyz.dart` files that import `realm` but don't have realm models defined. Those should not need generated parts and including the part file would have resulted in an empty file with `// ignore_for_file: type=lint` being generated. (PR [#1443](https://github.com/realm/realm-dart/pull/1443)) * Updated the minimum required CMake version for Flutter on Linux to 3.19. (Issue [#1381](https://github.com/realm/realm-dart/issues/1381)) * Errors in user-provided client reset callbacks, such as `RecoverOrDiscardUnsyncedChangesHandler.onBeforeReset/onAfterDiscard` would not be correctly propagated and the client reset exception would contain a message like `A fatal error occurred during client reset: 'User-provided callback failed'` but no details about the actual error. Now `SyncError` has an `innerError` field which contains the original error thrown in the callback. (PR [#1447](https://github.com/realm/realm-dart/pull/1447)) +* Fixed a bug where the generator would not emit errors for invalid default values for collection properties. Default values for collection properties are not supported unless the default value is an empty collection. (PR [#1406](https://github.com/realm/realm-dart/pull/1406)) ### Compatibility * Realm Studio: 13.0.0 or later. ### Internal -* Using Core x.y.z. +* Using Core 13.24.0. ## 1.6.1 (2023-11-30) diff --git a/common/lib/src/realm_types.dart b/common/lib/src/realm_types.dart index 7f48cc2a9..5f57e59cf 100644 --- a/common/lib/src/realm_types.dart +++ b/common/lib/src/realm_types.dart @@ -21,6 +21,7 @@ import 'dart:math'; import 'dart:typed_data'; import 'package:objectid/objectid.dart'; import 'package:sane_uuid/uuid.dart'; +import 'package:collection/collection.dart'; Type _typeOf() => T; @@ -98,7 +99,20 @@ enum RealmCollectionType { list, set, _3, // ignore: unused_field, constant_identifier_names - dictionary, + map; + + String get plural { + switch (this) { + case RealmCollectionType.list: + return "lists"; + case RealmCollectionType.set: + return "sets"; + case RealmCollectionType.map: + return "maps"; + default: + return "none"; + } + } } /// A base class of all Realm errors. @@ -216,6 +230,10 @@ class RealmValue { @override operator ==(Object? other) { if (other is RealmValue) { + if (value is Uint8List && other.value is Uint8List) { + return ListEquality().equals(value as Uint8List, other.value as Uint8List); + } + return value == other.value; } diff --git a/generator/lib/src/dart_type_ex.dart b/generator/lib/src/dart_type_ex.dart index e564797c4..744149334 100644 --- a/generator/lib/src/dart_type_ex.dart +++ b/generator/lib/src/dart_type_ex.dart @@ -55,9 +55,7 @@ extension DartTypeEx on DartType { RealmCollectionType get realmCollectionType { if (isDartCoreSet) return RealmCollectionType.set; if (isDartCoreList) return RealmCollectionType.list; - if (isDartCoreMap && (this as ParameterizedType).typeArguments.first == session.typeProvider.stringType) { - return RealmCollectionType.dictionary; - } + if (isDartCoreMap) return RealmCollectionType.map; return RealmCollectionType.none; } @@ -79,18 +77,14 @@ extension DartTypeEx on DartType { if (self is ParameterizedType) { final mapped = self.typeArguments.last.mappedType; if (self != mapped) { - final provider = session.typeProvider; if (self.isDartCoreList) { - final mappedList = provider.listType(mapped); - return PseudoType('Realm${mappedList.getDisplayString(withNullability: true)}', nullabilitySuffix: mappedList.nullabilitySuffix); + return PseudoType('RealmList<${mapped.getDisplayString(withNullability: true)}>'); } if (self.isDartCoreSet) { - final mappedSet = provider.setType(mapped); - return PseudoType('Realm${mappedSet.getDisplayString(withNullability: true)}', nullabilitySuffix: mappedSet.nullabilitySuffix); + return PseudoType('RealmSet<${mapped.getDisplayString(withNullability: true)}>'); } if (self.isDartCoreMap) { - final mappedMap = provider.mapType(self.typeArguments.first, mapped); - return PseudoType('Realm${mappedMap.getDisplayString(withNullability: true)}', nullabilitySuffix: mappedMap.nullabilitySuffix); + return PseudoType('RealmMap<${mapped.getDisplayString(withNullability: true)}>'); } } } diff --git a/generator/lib/src/field_element_ex.dart b/generator/lib/src/field_element_ex.dart index 64605e071..a95b60a6b 100644 --- a/generator/lib/src/field_element_ex.dart +++ b/generator/lib/src/field_element_ex.dart @@ -17,6 +17,7 @@ //////////////////////////////////////////////////////////////////////////////// import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/src/dart/ast/ast.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:build/build.dart'; @@ -93,17 +94,6 @@ extension FieldElementEx on FieldElement { final indexed = indexedInfo; final backlink = backlinkInfo; - // Check for as-of-yet unsupported type - if (type.isDartCoreMap) { - throw RealmInvalidGenerationSourceError( - 'Field type not supported yet', - element: this, - primarySpan: typeSpan(span!.file), - primaryLabel: 'not yet supported', - todo: 'Avoid using $modelTypeName for now', - ); - } - // Validate primary key if (primaryKey != null) { if (indexed != null) { @@ -220,7 +210,7 @@ extension FieldElementEx on FieldElement { } else { // Validate collections and backlinks if (type.isRealmCollection || backlink != null) { - final typeDescription = type.isRealmCollection ? (type.isRealmSet ? 'sets' : 'collections') : 'backlinks'; + final typeDescription = type.isRealmCollection ? type.realmCollectionType.plural : 'backlinks'; if (type.isNullable) { throw RealmInvalidGenerationSourceError( 'Realm $typeDescription cannot be nullable', @@ -231,40 +221,65 @@ extension FieldElementEx on FieldElement { ); } final itemType = type.basicType; - if (itemType.isRealmModel && itemType.isNullable) { - throw RealmInvalidGenerationSourceError('Nullable realm objects are not allowed in $typeDescription', + final objectsShouldBeNullable = type.realmCollectionType == RealmCollectionType.map; + if (itemType.isRealmModel && itemType.isNullable != objectsShouldBeNullable) { + final requestedObjectType = objectsShouldBeNullable ? 'nullable' : 'non-nullable'; + final invalidObjectType = objectsShouldBeNullable ? 'non-nullable' : 'nullable'; + + throw RealmInvalidGenerationSourceError('Realm objects in $typeDescription must be $requestedObjectType', primarySpan: typeSpan(file), - primaryLabel: 'which has a nullable realm object element type', + primaryLabel: 'which has a $invalidObjectType realm object element type', element: this, - todo: 'Ensure element type is non-nullable'); + todo: 'Ensure element type is $requestedObjectType'); } - if (type.isRealmSet) { - final typeArgument = (type as ParameterizedType).typeArguments.first; - if (realmSetUnsupportedRealmTypes.contains(realmType)) { - throw RealmInvalidGenerationSourceError('$type is not supported', - primarySpan: typeSpan(file), - primaryLabel: 'Set element type is not supported', - element: this, - todo: 'Ensure set element type $typeArgument is a type supported by RealmSet.'); - } - - if (realmType == RealmPropertyType.mixed && typeArgument.isNullable) { - throw RealmInvalidGenerationSourceError('$type is not supported', - primarySpan: typeSpan(file), - primaryLabel: 'Set of nullable RealmValues is not supported', - element: this, - todo: 'Did you mean to use Set instead?'); - } - - final initExpression = initializerExpression; - if (initExpression != null) { - throw RealmInvalidGenerationSourceError('Default values for set are not supported.', - primarySpan: initializerExpressionSpan(file, initExpression), - primaryLabel: 'Remove the default value.', - element: this, - todo: 'Remove the default value for field $displayName.'); - } + if (realmType == RealmPropertyType.mixed && itemType.isNullable) { + throw RealmInvalidGenerationSourceError('$type is not supported', + primarySpan: typeSpan(file), + primaryLabel: 'Nullable RealmValues are not supported', + element: this, + todo: 'Ensure the RealmValue type argument is non-nullable. RealmValue can hold null, but must not be nullable itself.'); + } + + if (itemType.isRealmCollection || itemType.realmType == RealmPropertyType.linkingObjects) { + throw RealmInvalidGenerationSourceError('$type is not supported', + primarySpan: typeSpan(file), + primaryLabel: 'Collections of collections are not supported', + element: this, + todo: 'Ensure the collection element type $itemType is not Iterable.'); + } + + final initExpression = initializerExpression; + if (initExpression != null && !_isValidCollectionInitializer(initExpression)) { + throw RealmInvalidGenerationSourceError('Non-empty default values for $typeDescription are not supported.', + primarySpan: initializerExpressionSpan(file, initExpression), + primaryLabel: 'Remove the default value.', + element: this, + todo: 'Remove the default value for field $displayName or change it to be an empty collection.'); + } + + switch (type.realmCollectionType) { + case RealmCollectionType.map: + final keyType = (type as ParameterizedType).typeArguments.first; + if (!keyType.isDartCoreString || keyType.isNullable) { + throw RealmInvalidGenerationSourceError('$type is not supported', + primarySpan: typeSpan(file), + primaryLabel: 'Non-String keys are not supported in maps', + element: this, + todo: 'Change the map key type to be String'); + } + break; + case RealmCollectionType.set: + if (itemType.realmObjectType == ObjectType.embeddedObject) { + throw RealmInvalidGenerationSourceError('$type is not supported', + primarySpan: typeSpan(file), + primaryLabel: 'Embedded objects in sets are not supported', + element: this, + todo: 'Change the collection element to be a non-embedded object'); + } + break; + default: + break; } } @@ -341,7 +356,7 @@ extension FieldElementEx on FieldElement { 'RealmValue fields cannot be nullable', primarySpan: typeSpan(file), primaryLabel: '$modelTypeName is nullable', - todo: 'Change type to ${modelType.asNonNullable}', + todo: 'Change type to RealmValue. RealmValue can hold null, but must not be nullable itself.', element: this, ); } @@ -368,4 +383,16 @@ extension FieldElementEx on FieldElement { ); } } + + bool _isValidCollectionInitializer(Expression initExpression) { + if (initExpression is AstNodeImpl) { + final astNode = initExpression as AstNodeImpl; + final elementsNode = astNode.namedChildEntities.where((e) => e.name == 'elements').singleOrNull; + final nodeValue = elementsNode?.value; + if (nodeValue is NodeList && nodeValue.isEmpty) { + return true; + } + } + return false; + } } diff --git a/generator/lib/src/realm_field_info.dart b/generator/lib/src/realm_field_info.dart index 0260b0365..1a5fc0616 100644 --- a/generator/lib/src/realm_field_info.dart +++ b/generator/lib/src/realm_field_info.dart @@ -43,8 +43,6 @@ class RealmFieldInfo { DartType get type => fieldElement.type; bool get isFinal => fieldElement.isFinal; - bool get isRealmCollection => type.isRealmCollection; - bool get isDartCoreSet => type.isDartCoreSet; bool get isLate => fieldElement.isLate; bool get hasDefaultValue => fieldElement.hasInitializer; bool get optional => type.basicType.isNullable || realmType == RealmPropertyType.mixed; @@ -53,6 +51,11 @@ class RealmFieldInfo { bool get isMixed => realmType == RealmPropertyType.mixed; bool get isComputed => isRealmBacklink; // only computed, so far + bool get isRealmCollection => type.isRealmCollection; + bool get isDartCoreList => type.isDartCoreList; + bool get isDartCoreSet => type.isDartCoreSet; + bool get isDartCoreMap => type.isDartCoreMap; + String get name => fieldElement.name; String get realmName => mapTo ?? name; @@ -61,17 +64,18 @@ class RealmFieldInfo { String get basicNonNullableMappedTypeName => type.basicType.asNonNullable.mappedName; String get basicRealmTypeName => - fieldElement.modelType.basicType.asNonNullable.element?.remappedRealmName ?? fieldElement.modelType.asNonNullable.basicMappedName; + fieldElement.modelType.basicType.asNonNullable.element?.remappedRealmName ?? fieldElement.modelType.basicType.asNonNullable.basicMappedName; String get modelTypeName => fieldElement.modelTypeName; String get mappedTypeName => fieldElement.mappedTypeName; String get initializer { - if (type.isDartCoreList) return ' = const []'; - if (isMixed && !type.isRealmCollection) return ' = const RealmValue.nullValue()'; + if (type.realmCollectionType == RealmCollectionType.list) return ' = const []'; + if (type.realmCollectionType == RealmCollectionType.set) return ' = const {}'; + if (type.realmCollectionType == RealmCollectionType.map) return ' = const {}'; + if (isMixed) return ' = const RealmValue.nullValue()'; if (hasDefaultValue) return ' = ${fieldElement.initializerExpression}'; - if (type.isDartCoreSet) return ' = const {}'; return ''; // no initializer } diff --git a/generator/lib/src/realm_model_info.dart b/generator/lib/src/realm_model_info.dart index 84843ae4c..21f64e57f 100644 --- a/generator/lib/src/realm_model_info.dart +++ b/generator/lib/src/realm_model_info.dart @@ -43,15 +43,17 @@ class RealmModelInfo { yield ''; } + // Constructor yield '$name('; { final required = allSettable.where((f) => f.isRequired || f.isPrimaryKey); yield* required.map((f) => '${f.mappedTypeName} ${f.name},'); final notRequired = allSettable.where((f) => !f.isRequired && !f.isPrimaryKey); - final collections = fields.where((f) => f.isRealmCollection && !f.isDartCoreSet).toList(); + final lists = fields.where((f) => f.isDartCoreList).toList(); final sets = fields.where((f) => f.isDartCoreSet).toList(); - if (notRequired.isNotEmpty || collections.isNotEmpty || sets.isNotEmpty) { + final maps = fields.where((f) => f.isDartCoreMap).toList(); + if (notRequired.isNotEmpty || lists.isNotEmpty || sets.isNotEmpty || maps.isNotEmpty) { yield '{'; yield* notRequired.map((f) { if (f.type.isUint8List && f.hasDefaultValue) { @@ -59,8 +61,9 @@ class RealmModelInfo { } return '${f.mappedTypeName} ${f.name}${f.initializer},'; }); - yield* collections.map((c) => 'Iterable<${c.type.basicMappedName}> ${c.name}${c.initializer},'); + yield* lists.map((c) => 'Iterable<${c.type.basicMappedName}> ${c.name}${c.initializer},'); yield* sets.map((c) => 'Set<${c.type.basicMappedName}> ${c.name}${c.initializer},'); + yield* maps.map((c) => 'Map ${c.name}${c.initializer},'); yield '}'; } @@ -82,32 +85,40 @@ class RealmModelInfo { return "RealmObjectBase.set(this, '${f.realmName}', ${f.name});"; }); - yield* collections.map((c) { + yield* lists.map((c) { return "RealmObjectBase.set<${c.mappedTypeName}>(this, '${c.realmName}', ${c.mappedTypeName}(${c.name}));"; }); yield* sets.map((c) { return "RealmObjectBase.set<${c.mappedTypeName}>(this, '${c.realmName}', ${c.mappedTypeName}(${c.name}));"; }); + + yield* maps.map((c) { + return "RealmObjectBase.set<${c.mappedTypeName}>(this, '${c.realmName}', ${c.mappedTypeName}(${c.name}));"; + }); } yield '}'; yield ''; yield '$name._();'; yield ''; + // Properties yield* fields.expand((f) => [ ...f.toCode(), '', ]); + // Changes yield '@override'; yield 'Stream> get changes => RealmObjectBase.getChanges<$name>(this);'; yield ''; + // Freeze yield '@override'; yield '$name freeze() => RealmObjectBase.freezeObject<$name>(this);'; yield ''; + // Schema yield 'static SchemaObject get schema => _schema ??= _initSchema();'; yield 'static SchemaObject? _schema;'; yield 'static SchemaObject _initSchema() {'; diff --git a/generator/test/error_test_data/dict_non_empty_initializer.dart b/generator/test/error_test_data/dict_non_empty_initializer.dart new file mode 100644 index 000000000..14f908828 --- /dev/null +++ b/generator/test/error_test_data/dict_non_empty_initializer.dart @@ -0,0 +1,9 @@ +import 'package:realm_common/realm_common.dart'; + +//part 'dict_non_empty_initializer.g.dart'; + +@RealmModel() +class _Bad { + late int x; + final dictWithInitializer = {"a": 5}; +} diff --git a/generator/test/error_test_data/dict_non_empty_initializer.expected b/generator/test/error_test_data/dict_non_empty_initializer.expected new file mode 100644 index 000000000..d37b18643 --- /dev/null +++ b/generator/test/error_test_data/dict_non_empty_initializer.expected @@ -0,0 +1,13 @@ +Non-empty default values for maps are not supported. + +in: asset:pkg/test/error_test_data/dict_non_empty_initializer.dart:8:31 + ╷ +5 │ @RealmModel() +6 │ class _Bad { + │ ━━━━ in realm model for 'Bad' +... │ +8 │ final dictWithInitializer = {"a": 5}; + │ ^^^^^^^^ Remove the default value. + ╵ +Remove the default value for field dictWithInitializer or change it to be an empty collection. + diff --git a/generator/test/error_test_data/list_non_empty_initializer.dart b/generator/test/error_test_data/list_non_empty_initializer.dart new file mode 100644 index 000000000..69dee294a --- /dev/null +++ b/generator/test/error_test_data/list_non_empty_initializer.dart @@ -0,0 +1,9 @@ +import 'package:realm_common/realm_common.dart'; + +//part 'list_non_empty_initializer.g.dart'; + +@RealmModel() +class _Bad { + late int x; + final listWithInitializer = [0]; +} diff --git a/generator/test/error_test_data/list_non_empty_initializer.expected b/generator/test/error_test_data/list_non_empty_initializer.expected new file mode 100644 index 000000000..e3ba0e158 --- /dev/null +++ b/generator/test/error_test_data/list_non_empty_initializer.expected @@ -0,0 +1,13 @@ +Non-empty default values for lists are not supported. + +in: asset:pkg/test/error_test_data/list_non_empty_initializer.dart:8:31 + ╷ +5 │ @RealmModel() +6 │ class _Bad { + │ ━━━━ in realm model for 'Bad' +... │ +8 │ final listWithInitializer = [0]; + │ ^^^ Remove the default value. + ╵ +Remove the default value for field listWithInitializer or change it to be an empty collection. + diff --git a/generator/test/error_test_data/map_unsupported.dart b/generator/test/error_test_data/map_unsupported.dart index d7b8eb47d..203422b10 100644 --- a/generator/test/error_test_data/map_unsupported.dart +++ b/generator/test/error_test_data/map_unsupported.dart @@ -4,5 +4,5 @@ import 'package:realm_common/realm_common.dart'; @RealmModel() class _Person { - late Map relatives; + late Map relatives; } diff --git a/generator/test/error_test_data/map_unsupported.expected b/generator/test/error_test_data/map_unsupported.expected index a570d65dc..15697d247 100644 --- a/generator/test/error_test_data/map_unsupported.expected +++ b/generator/test/error_test_data/map_unsupported.expected @@ -1,12 +1,12 @@ -Field type not supported yet +Map is not supported in: asset:pkg/test/error_test_data/map_unsupported.dart:7:8 ╷ 5 │ @RealmModel() 6 │ class _Person { │ ━━━━━━━ in realm model for 'Person' -7 │ late Map relatives; - │ ^^^^^^^^^^^^^^^^^^^^ not yet supported +7 │ late Map relatives; + │ ^^^^^^^^^^^^^^^^^^ Non-String keys are not supported in maps ╵ -Avoid using Map for now +Change the map key type to be String diff --git a/generator/test/error_test_data/nullable_list.expected b/generator/test/error_test_data/nullable_list.expected index 964176e97..98ec60e20 100644 --- a/generator/test/error_test_data/nullable_list.expected +++ b/generator/test/error_test_data/nullable_list.expected @@ -1,4 +1,4 @@ -Realm collections cannot be nullable +Realm lists cannot be nullable in: asset:pkg/test/error_test_data/nullable_list.dart:10:3 ╷ diff --git a/generator/test/error_test_data/nullable_list_elements.expected b/generator/test/error_test_data/nullable_list_elements.expected index a28c1fa37..a817a5789 100644 --- a/generator/test/error_test_data/nullable_list_elements.expected +++ b/generator/test/error_test_data/nullable_list_elements.expected @@ -1,4 +1,4 @@ -Nullable realm objects are not allowed in collections +Realm objects in lists must be non-nullable in: asset:pkg/test/error_test_data/nullable_list_elements.dart:14:8 ╷ diff --git a/generator/test/error_test_data/nullable_realm_value.expected b/generator/test/error_test_data/nullable_realm_value.expected index 111575679..5ec8abfef 100644 --- a/generator/test/error_test_data/nullable_realm_value.expected +++ b/generator/test/error_test_data/nullable_realm_value.expected @@ -8,5 +8,5 @@ in: asset:pkg/test/error_test_data/nullable_realm_value.dart:7:3 7 │ RealmValue? wrong; │ ^^^^^^^^^^^ RealmValue? is nullable ╵ -Change type to RealmValue +Change type to RealmValue. RealmValue can hold null, but must not be nullable itself. diff --git a/generator/test/error_test_data/set_non_empty_initializer.dart b/generator/test/error_test_data/set_non_empty_initializer.dart new file mode 100644 index 000000000..767bb4225 --- /dev/null +++ b/generator/test/error_test_data/set_non_empty_initializer.dart @@ -0,0 +1,9 @@ +import 'package:realm_common/realm_common.dart'; + +//part 'set_non_empty_initializer.g.dart'; + +@RealmModel() +class _Bad { + late int x; + final setWithInitializer = {0}; +} diff --git a/generator/test/error_test_data/set_non_empty_initializer.expected b/generator/test/error_test_data/set_non_empty_initializer.expected new file mode 100644 index 000000000..648335f5d --- /dev/null +++ b/generator/test/error_test_data/set_non_empty_initializer.expected @@ -0,0 +1,13 @@ +Non-empty default values for sets are not supported. + +in: asset:pkg/test/error_test_data/set_non_empty_initializer.dart:8:30 + ╷ +5 │ @RealmModel() +6 │ class _Bad { + │ ━━━━ in realm model for 'Bad' +... │ +8 │ final setWithInitializer = {0}; + │ ^^^ Remove the default value. + ╵ +Remove the default value for field setWithInitializer or change it to be an empty collection. + diff --git a/generator/test/error_test_data/unsupported_realm_set_of_nullable_realmobject.expected b/generator/test/error_test_data/unsupported_realm_set_of_nullable_realmobject.expected index 1bb634c99..459592781 100644 --- a/generator/test/error_test_data/unsupported_realm_set_of_nullable_realmobject.expected +++ b/generator/test/error_test_data/unsupported_realm_set_of_nullable_realmobject.expected @@ -1,4 +1,4 @@ -Nullable realm objects are not allowed in sets +Realm objects in sets must be non-nullable in: asset:pkg/test/error_test_data/unsupported_realm_set_of_nullable_realmobject.dart:10:8 ╷ diff --git a/generator/test/error_test_data/unsupported_realm_set_of_nullable_realmvalue.expected b/generator/test/error_test_data/unsupported_realm_set_of_nullable_realmvalue.expected index 0f9d18fbc..2e9f837f5 100644 --- a/generator/test/error_test_data/unsupported_realm_set_of_nullable_realmvalue.expected +++ b/generator/test/error_test_data/unsupported_realm_set_of_nullable_realmvalue.expected @@ -7,7 +7,7 @@ in: asset:pkg/test/error_test_data/unsupported_realm_set_of_nullable_realmvalue. │ ━━━━ in realm model for 'Bad' ... │ 10 │ late Set wrong1; - │ ^^^^^^^^^^^^^^^^ Set of nullable RealmValues is not supported + │ ^^^^^^^^^^^^^^^^ Nullable RealmValues are not supported ╵ -Did you mean to use Set instead? +Ensure the RealmValue type argument is non-nullable. RealmValue can hold null, but must not be nullable itself. diff --git a/generator/test/error_test_data/unsupported_realm_set_with_default_values.expected b/generator/test/error_test_data/unsupported_realm_set_with_default_values.expected index bff65897e..0cabfe0e4 100644 --- a/generator/test/error_test_data/unsupported_realm_set_with_default_values.expected +++ b/generator/test/error_test_data/unsupported_realm_set_with_default_values.expected @@ -1,4 +1,4 @@ -Default values for set are not supported. +Non-empty default values for sets are not supported. in: asset:pkg/test/error_test_data/unsupported_realm_set_with_default_values.dart:10:27 ╷ @@ -9,5 +9,5 @@ in: asset:pkg/test/error_test_data/unsupported_realm_set_with_default_values.dar 10 │ late Set wrong1 = {true, false}; │ ^^^^^^^^^^^^^ Remove the default value. ╵ -Remove the default value for field wrong1. +Remove the default value for field wrong1 or change it to be an empty collection. diff --git a/generator/test/good_test_data/all_types.dart b/generator/test/good_test_data/all_types.dart index fde9f6042..da84b8129 100644 --- a/generator/test/good_test_data/all_types.dart +++ b/generator/test/good_test_data/all_types.dart @@ -29,9 +29,9 @@ class _Bar { late Uuid uuid; @Ignored() var theMeaningOfEverything = 42; - var list = [0]; // list of ints with default value - // late Set set; // not supported yet - // late map = {}; // not supported yet + late List list; + late Set set; + late Map map; @Indexed() String? anOptionalString; diff --git a/generator/test/good_test_data/all_types.expected b/generator/test/good_test_data/all_types.expected index f6f114fa8..0ea68f69a 100644 --- a/generator/test/good_test_data/all_types.expected +++ b/generator/test/good_test_data/all_types.expected @@ -68,6 +68,8 @@ class Bar extends _Bar with RealmEntity, RealmObjectBase, RealmObject { RealmValue any = const RealmValue.nullValue(), Iterable list = const [], Iterable manyAny = const [], + Set set = const {}, + Map map = const {}, }) { if (!_defaultsSet) { _defaultsSet = RealmObjectBase.setDefaults({ @@ -89,6 +91,9 @@ class Bar extends _Bar with RealmEntity, RealmObjectBase, RealmObject { RealmObjectBase.set(this, 'decimal', decimal); RealmObjectBase.set>(this, 'list', RealmList(list)); RealmObjectBase.set>(this, 'manyAny', RealmList(manyAny)); + RealmObjectBase.set>(this, 'set', RealmSet(set)); + RealmObjectBase.set>( + this, 'map', RealmMap(map)); } Bar._(); @@ -148,6 +153,19 @@ class Bar extends _Bar with RealmEntity, RealmObjectBase, RealmObject { @override set list(covariant RealmList value) => throw RealmUnsupportedSetError(); + @override + RealmSet get set => + RealmObjectBase.get(this, 'set') as RealmSet; + @override + set set(covariant RealmSet value) => throw RealmUnsupportedSetError(); + + @override + RealmMap get map => + RealmObjectBase.get(this, 'map') as RealmMap; + @override + set map(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + @override String? get anOptionalString => RealmObjectBase.get(this, 'anOptionalString') as String?; @@ -212,6 +230,10 @@ class Bar extends _Bar with RealmEntity, RealmObjectBase, RealmObject { SchemaProperty('uuid', RealmPropertyType.uuid, indexType: RealmIndexType.regular), SchemaProperty('list', RealmPropertyType.int, collectionType: RealmCollectionType.list), + SchemaProperty('set', RealmPropertyType.int, + collectionType: RealmCollectionType.set), + SchemaProperty('map', RealmPropertyType.int, + collectionType: RealmCollectionType.map), SchemaProperty('anOptionalString', RealmPropertyType.string, optional: true, indexType: RealmIndexType.regular), SchemaProperty('any', RealmPropertyType.mixed, optional: true), diff --git a/generator/test/good_test_data/list_initialization.dart b/generator/test/good_test_data/list_initialization.dart index 66e6e8eb0..663825a27 100644 --- a/generator/test/good_test_data/list_initialization.dart +++ b/generator/test/good_test_data/list_initialization.dart @@ -5,4 +5,16 @@ import 'package:realm_common/realm_common.dart'; @RealmModel() class _Person { late List<_Person> children; + + final List initList = []; + final initListWithType = []; + final List initListConst = const []; + + final Set initSet = {}; + final initSetWithType = {}; + final Set initSetConst = const {}; + + final Map initMap = {}; + final initMapWithType = {}; + final Map initMapConst = const {}; } diff --git a/generator/test/good_test_data/list_initialization.expected b/generator/test/good_test_data/list_initialization.expected index 331621f54..d74f0890e 100644 --- a/generator/test/good_test_data/list_initialization.expected +++ b/generator/test/good_test_data/list_initialization.expected @@ -6,9 +6,35 @@ class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { Person({ Iterable children = const [], + Iterable initList = const [], + Iterable initListWithType = const [], + Iterable initListConst = const [], + Set initSet = const {}, + Set initSetWithType = const {}, + Set initSetConst = const {}, + Map initMap = const {}, + Map initMapWithType = const {}, + Map initMapConst = const {}, }) { RealmObjectBase.set>( this, 'children', RealmList(children)); + RealmObjectBase.set>( + this, 'initList', RealmList(initList)); + RealmObjectBase.set>( + this, 'initListWithType', RealmList(initListWithType)); + RealmObjectBase.set>( + this, 'initListConst', RealmList(initListConst)); + RealmObjectBase.set>(this, 'initSet', RealmSet(initSet)); + RealmObjectBase.set>( + this, 'initSetWithType', RealmSet(initSetWithType)); + RealmObjectBase.set>( + this, 'initSetConst', RealmSet(initSetConst)); + RealmObjectBase.set>( + this, 'initMap', RealmMap(initMap)); + RealmObjectBase.set>( + this, 'initMapWithType', RealmMap(initMapWithType)); + RealmObjectBase.set>( + this, 'initMapConst', RealmMap(initMapConst)); } Person._(); @@ -20,6 +46,70 @@ class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { set children(covariant RealmList value) => throw RealmUnsupportedSetError(); + @override + RealmList get initList => + RealmObjectBase.get(this, 'initList') as RealmList; + @override + set initList(covariant RealmList value) => + throw RealmUnsupportedSetError(); + + @override + RealmList get initListWithType => + RealmObjectBase.get(this, 'initListWithType') as RealmList; + @override + set initListWithType(covariant RealmList value) => + throw RealmUnsupportedSetError(); + + @override + RealmList get initListConst => + RealmObjectBase.get(this, 'initListConst') as RealmList; + @override + set initListConst(covariant RealmList value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get initSet => + RealmObjectBase.get(this, 'initSet') as RealmSet; + @override + set initSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get initSetWithType => + RealmObjectBase.get(this, 'initSetWithType') as RealmSet; + @override + set initSetWithType(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get initSetConst => + RealmObjectBase.get(this, 'initSetConst') as RealmSet; + @override + set initSetConst(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get initMap => + RealmObjectBase.get(this, 'initMap') as RealmMap; + @override + set initMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get initMapWithType => + RealmObjectBase.get(this, 'initMapWithType') + as RealmMap; + @override + set initMapWithType(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get initMapConst => + RealmObjectBase.get(this, 'initMapConst') as RealmMap; + @override + set initMapConst(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + @override Stream> get changes => RealmObjectBase.getChanges(this); @@ -34,7 +124,24 @@ class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { return const SchemaObject(ObjectType.realmObject, Person, 'Person', [ SchemaProperty('children', RealmPropertyType.object, linkTarget: 'Person', collectionType: RealmCollectionType.list), + SchemaProperty('initList', RealmPropertyType.int, + collectionType: RealmCollectionType.list), + SchemaProperty('initListWithType', RealmPropertyType.int, + collectionType: RealmCollectionType.list), + SchemaProperty('initListConst', RealmPropertyType.int, + collectionType: RealmCollectionType.list), + SchemaProperty('initSet', RealmPropertyType.int, + collectionType: RealmCollectionType.set), + SchemaProperty('initSetWithType', RealmPropertyType.int, + collectionType: RealmCollectionType.set), + SchemaProperty('initSetConst', RealmPropertyType.int, + collectionType: RealmCollectionType.set), + SchemaProperty('initMap', RealmPropertyType.int, + collectionType: RealmCollectionType.map), + SchemaProperty('initMapWithType', RealmPropertyType.int, + collectionType: RealmCollectionType.map), + SchemaProperty('initMapConst', RealmPropertyType.int, + collectionType: RealmCollectionType.map), ]); } } - diff --git a/generator/test/good_test_data/map.dart b/generator/test/good_test_data/map.dart new file mode 100644 index 000000000..3d05b7c54 --- /dev/null +++ b/generator/test/good_test_data/map.dart @@ -0,0 +1,25 @@ +import 'dart:typed_data'; + +import 'package:realm_common/realm_common.dart'; + +//part 'map.g.dart'; + +@RealmModel() +class _LotsOfMaps { + late Map persons; + late Map bools; + late Map dateTimes; + late Map decimals; + late Map doubles; + late Map ints; + late Map objectIds; + late Map any; + late Map strings; + late Map binary; + late Map uuids; +} + +@RealmModel() +class _Person { + late String name; +} diff --git a/generator/test/good_test_data/map.expected b/generator/test/good_test_data/map.expected new file mode 100644 index 000000000..874fb0596 --- /dev/null +++ b/generator/test/good_test_data/map.expected @@ -0,0 +1,197 @@ +// ************************************************************************** +// RealmObjectGenerator +// ************************************************************************** + +// ignore_for_file: type=lint +class LotsOfMaps extends _LotsOfMaps + with RealmEntity, RealmObjectBase, RealmObject { + LotsOfMaps({ + Map persons = const {}, + Map bools = const {}, + Map dateTimes = const {}, + Map decimals = const {}, + Map doubles = const {}, + Map ints = const {}, + Map objectIds = const {}, + Map any = const {}, + Map strings = const {}, + Map binary = const {}, + Map uuids = const {}, + }) { + RealmObjectBase.set>( + this, 'persons', RealmMap(persons)); + RealmObjectBase.set>( + this, 'bools', RealmMap(bools)); + RealmObjectBase.set>( + this, 'dateTimes', RealmMap(dateTimes)); + RealmObjectBase.set>( + this, 'decimals', RealmMap(decimals)); + RealmObjectBase.set>( + this, 'doubles', RealmMap(doubles)); + RealmObjectBase.set>( + this, 'ints', RealmMap(ints)); + RealmObjectBase.set>( + this, 'objectIds', RealmMap(objectIds)); + RealmObjectBase.set>( + this, 'any', RealmMap(any)); + RealmObjectBase.set>( + this, 'strings', RealmMap(strings)); + RealmObjectBase.set>( + this, 'binary', RealmMap(binary)); + RealmObjectBase.set>( + this, 'uuids', RealmMap(uuids)); + } + + LotsOfMaps._(); + + @override + RealmMap get persons => + RealmObjectBase.get(this, 'persons') as RealmMap; + @override + set persons(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get bools => + RealmObjectBase.get(this, 'bools') as RealmMap; + @override + set bools(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get dateTimes => + RealmObjectBase.get(this, 'dateTimes') + as RealmMap; + @override + set dateTimes(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get decimals => + RealmObjectBase.get(this, 'decimals') + as RealmMap; + @override + set decimals(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get doubles => + RealmObjectBase.get(this, 'doubles') as RealmMap; + @override + set doubles(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get ints => + RealmObjectBase.get(this, 'ints') as RealmMap; + @override + set ints(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get objectIds => + RealmObjectBase.get(this, 'objectIds') + as RealmMap; + @override + set objectIds(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get any => + RealmObjectBase.get(this, 'any') + as RealmMap; + @override + set any(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get strings => + RealmObjectBase.get(this, 'strings') as RealmMap; + @override + set strings(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get binary => + RealmObjectBase.get(this, 'binary') + as RealmMap; + @override + set binary(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get uuids => + RealmObjectBase.get(this, 'uuids') as RealmMap; + @override + set uuids(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + LotsOfMaps freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(LotsOfMaps._); + return const SchemaObject( + ObjectType.realmObject, LotsOfMaps, 'LotsOfMaps', [ + SchemaProperty('persons', RealmPropertyType.object, + optional: true, linkTarget: 'Person', collectionType: RealmCollectionType.map), + SchemaProperty('bools', RealmPropertyType.bool, + collectionType: RealmCollectionType.map), + SchemaProperty('dateTimes', RealmPropertyType.timestamp, + collectionType: RealmCollectionType.map), + SchemaProperty('decimals', RealmPropertyType.decimal128, + collectionType: RealmCollectionType.map), + SchemaProperty('doubles', RealmPropertyType.double, + collectionType: RealmCollectionType.map), + SchemaProperty('ints', RealmPropertyType.int, + collectionType: RealmCollectionType.map), + SchemaProperty('objectIds', RealmPropertyType.objectid, + collectionType: RealmCollectionType.map), + SchemaProperty('any', RealmPropertyType.mixed, + optional: true, collectionType: RealmCollectionType.map), + SchemaProperty('strings', RealmPropertyType.string, + collectionType: RealmCollectionType.map), + SchemaProperty('binary', RealmPropertyType.binary, + collectionType: RealmCollectionType.map), + SchemaProperty('uuids', RealmPropertyType.uuid, + collectionType: RealmCollectionType.map), + ]); + } +} + +class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { + Person( + String name, + ) { + RealmObjectBase.set(this, 'name', name); + } + + Person._(); + + @override + String get name => RealmObjectBase.get(this, 'name') as String; + @override + set name(String value) => RealmObjectBase.set(this, 'name', value); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + Person freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(Person._); + return const SchemaObject(ObjectType.realmObject, Person, 'Person', [ + SchemaProperty('name', RealmPropertyType.string), + ]); + } +} diff --git a/lib/src/collections.dart b/lib/src/collections.dart index 0226605c6..999806734 100644 --- a/lib/src/collections.dart +++ b/lib/src/collections.dart @@ -48,6 +48,15 @@ class CollectionChanges { const CollectionChanges(this.deletions, this.insertions, this.modifications, this.modificationsAfter, this.moves, this.isCleared); } +/// @nodoc +class MapChanges { + final List deletions; + final List insertions; + final List modifications; + + const MapChanges(this.deletions, this.insertions, this.modifications); +} + /// Describes the changes in a Realm collection since the last time the notification callback was invoked. class RealmCollectionChanges implements Finalizable { final RealmCollectionChangesHandle _handle; diff --git a/lib/src/list.dart b/lib/src/list.dart index 7e1e28e88..bf7533d8a 100644 --- a/lib/src/list.dart +++ b/lib/src/list.dart @@ -118,9 +118,7 @@ class ManagedRealmList with RealmEntity, ListMixin impleme late RealmObjectMetadata targetMetadata; late Type type; if (T == RealmValue) { - final tuple = realm.metadata.getByClassKey(realmCore.getClassKey(value)); - type = tuple.item1; - targetMetadata = tuple.item2; + (type, targetMetadata) = realm.metadata.getByClassKey(realmCore.getClassKey(value)); } else { targetMetadata = _metadata!; type = T; @@ -228,16 +226,14 @@ class UnmanagedRealmList extends collection.DelegatingList Stream> get changes => throw RealmStateError("Unmanaged lists don't support changes"); } -// The query operations on lists, as well as the ability to subscribe for notifications, -// only work for list of objects (core restriction), so we add these as an extension methods -// to allow the compiler to prevent misuse. +// The query operations on lists, only work for list of objects (core restriction), +// so we add these as an extension methods to allow the compiler to prevent misuse. extension RealmListOfObject on RealmList { /// Filters the list and returns a new [RealmResults] according to the provided [query] (with optional [arguments]). /// /// Only works for lists of [RealmObject]s or [EmbeddedObject]s. /// - /// The Realm Dart and Realm Flutter SDKs supports querying based on a language inspired by [NSPredicate](https://academy.realm.io/posts/nspredicate-cheatsheet/) - /// and [Predicate Programming Guide.](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Predicates/AdditionalChapters/Introduction.html#//apple_ref/doc/uid/TP40001789) + /// For more details about the syntax of the Realm Query Language, refer to the documentation: https://www.mongodb.com/docs/realm/realm-query-language/. RealmResults query(String query, [List arguments = const []]) { final handle = realmCore.queryList(asManaged(), query, arguments); return RealmResultsInternal.create(handle, realm, _metadata); diff --git a/lib/src/map.dart b/lib/src/map.dart new file mode 100644 index 000000000..6f79591d9 --- /dev/null +++ b/lib/src/map.dart @@ -0,0 +1,304 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright 2023 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +import 'dart:async'; +import 'dart:collection'; + +import 'package:collection/collection.dart' as collection; + +import 'dart:ffi'; + +import 'collections.dart'; +import 'native/realm_core.dart'; +import 'realm_object.dart'; +import 'realm_class.dart'; +import 'results.dart'; + +/// RealmMap is a collection that contains key-value pairs of . +abstract class RealmMap with RealmEntity implements MapBase, Finalizable { + /// Gets a value indicating whether this collection is still valid to use. + /// + /// Indicates whether the [Realm] instance hasn't been closed, + /// if it represents a to-many relationship + /// and it's parent object hasn't been deleted. + bool get isValid; + + /// Creates an unmanaged RealmMap from [items] + factory RealmMap(Map items) => UnmanagedRealmMap(items); + + /// Creates a frozen snapshot of this `RealmMap`. + RealmMap freeze(); + + /// Allows listening for changes when the contents of this collection changes. + Stream> get changes; +} + +class UnmanagedRealmMap extends collection.DelegatingMap with RealmEntity implements RealmMap { + UnmanagedRealmMap([Map? items]) : super(Map.from(items ?? {})); + + @override + bool get isValid => true; + + @override + RealmMap freeze() => throw RealmStateError("Unmanaged maps can't be frozen"); + + @override + Stream> get changes => throw RealmStateError("Unmanaged maps don't support changes"); +} + +class ManagedRealmMap with RealmEntity, MapMixin implements RealmMap { + final RealmMapHandle _handle; + + late final RealmObjectMetadata? _metadata; + + ManagedRealmMap._(this._handle, Realm realm, this._metadata) { + setRealm(realm); + } + + @override + int get length => realmCore.mapGetSize(handle); + + @override + T? remove(Object? key) { + if (key is! String) { + return null; + } + + final value = this[key]; + if (realmCore.mapRemoveKey(handle, key)) { + return value; + } + + return null; + } + + @override + T? operator [](Object? key) { + if (key is! String) { + return null; + } + + try { + var value = realmCore.mapGetElement(this, key); + if (value is RealmObjectHandle) { + late RealmObjectMetadata targetMetadata; + late Type type; + if (T == RealmValue) { + (type, targetMetadata) = realm.metadata.getByClassKey(realmCore.getClassKey(value)); + } else { + targetMetadata = _metadata!; + type = T; + } + value = realm.createObject(type, value, targetMetadata); + } + + if (T == RealmValue) { + // Maps must return `null` if attempting to access a non-existing key. Without this check, + // we'd return RealmValue(null) which is different. + if (value == null && !containsKey(key)) { + return null; + } + + value = RealmValue.from(value); + } + + return value as T?; + } on Exception catch (e) { + throw RealmException("Error getting value at key $key. Error: $e"); + } + } + + @override + void operator []=(String key, Object? value) => RealmMapInternal.setValue(handle, realm, key, value); + + /// Removes all objects from this map; the length of the map becomes zero. + /// The objects are not deleted from the realm, but are no longer referenced from this map. + @override + void clear() => realmCore.mapClear(handle); + + @override + bool get isValid => realmCore.mapIsValid(this); + + @override + RealmMap freeze() { + if (isFrozen) { + return this; + } + + final frozenRealm = realm.freeze(); + return frozenRealm.resolveMap(this)!; + } + + @override + Stream> get changes { + if (isFrozen) { + throw RealmStateError('Map is frozen and cannot emit changes'); + } + final controller = MapNotificationsController(asManaged()); + return controller.createStream(); + } + + @override + Iterable get keys => RealmResultsInternal.create(realmCore.mapGetKeys(this), realm, null); + + @override + Iterable get values => RealmResultsInternal.create(realmCore.mapGetValues(this), realm, metadata); + + @override + bool containsKey(Object? key) => key is String && realmCore.mapContainsKey(this, key); + + @override + bool containsValue(Object? value) { + if (value is! T?) { + return false; + } + + if (value is RealmObjectBase && !value.isManaged) { + return false; + } + + if (value is RealmValue && value.value is RealmObjectBase && !(value.value as RealmObjectBase).isManaged) { + return false; + } + + return realmCore.mapContainsValue(this, value); + } +} + +/// Describes the changes in a Realm map collection since the last time the notification callback was invoked. +class RealmMapChanges { + /// The collection being monitored for changes. + final RealmMap map; + + final RealmMapChangesHandle _handle; + MapChanges? _values; + + RealmMapChanges._(this._handle, this.map); + + MapChanges get _changes => _values ??= realmCore.getMapChanges(_handle); + + /// The keys of the map which have been removed. + List get deleted => _changes.deletions; + + /// The keys of the map which were added. + List get inserted => _changes.insertions; + + /// The keys of the map, whose corresponding values were modified in this version. + List get modified => _changes.modifications; +} + +// The query operations on maps only work for maps of objects (core restriction), +// so we add these as an extension methods to allow the compiler to prevent misuse. +extension RealmMapOfObject on RealmMap { + /// Filters the map values and returns a new [RealmResults] according to the provided [query] (with optional [arguments]). + /// + /// Only works for maps of [RealmObject]s or [EmbeddedObject]s. + /// + /// For more details about the syntax of the Realm Query Language, refer to the documentation: https://www.mongodb.com/docs/realm/realm-query-language/. + RealmResults query(String query, [List arguments = const []]) { + final handle = realmCore.queryMap(asManaged(), query, arguments); + return RealmResultsInternal.create(handle, realm, metadata); + } +} + +/// @nodoc +extension RealmMapInternal on RealmMap { + @pragma('vm:never-inline') + void keepAlive() { + final self = this; + if (self is ManagedRealmMap) { + realm.keepAlive(); + self._handle.keepAlive(); + } + } + + ManagedRealmMap asManaged() => this is ManagedRealmMap ? this as ManagedRealmMap : throw RealmStateError('$this is not managed'); + + RealmMapHandle get handle { + final result = asManaged()._handle; + if (result.released) { + throw RealmClosedError('Cannot access a map that belongs to a closed Realm'); + } + + return result; + } + + RealmObjectMetadata? get metadata => asManaged()._metadata; + + static RealmMap create(RealmMapHandle handle, Realm realm, RealmObjectMetadata? metadata) => + ManagedRealmMap._(handle, realm, metadata); + + static void setValue(RealmMapHandle handle, Realm realm, String key, Object? value, {bool update = false}) { + try { + if (value is EmbeddedObject) { + if (value.isManaged) { + throw RealmError("Can't add to map an embedded object that is already managed"); + } + + final objHandle = realmCore.mapInsertEmbeddedObject(realm, handle, key); + realm.manageEmbedded(objHandle, value); + return; + } + + if (value is RealmValue) { + value = value.value; + } + + if (value is RealmObject && !value.isManaged) { + realm.add(value, update: update); + } + + realmCore.mapInsertValue(handle, key, value); + } on Exception catch (e) { + throw RealmException("Error setting value at key $key. Error: $e"); + } + } +} + +/// @nodoc +class MapNotificationsController extends NotificationsController { + final ManagedRealmMap map; + late final StreamController> streamController; + + MapNotificationsController(this.map); + + @override + RealmNotificationTokenHandle subscribe() { + return realmCore.subscribeMapNotifications(map, this); + } + + Stream> createStream() { + streamController = StreamController>(onListen: start, onCancel: stop); + return streamController.stream; + } + + @override + void onChanges(HandleBase changesHandle) { + if (changesHandle is! RealmMapChangesHandle) { + throw RealmError("Invalid changes handle. RealmMapChangesHandle expected"); + } + + final changes = RealmMapChanges._(changesHandle, map); + streamController.add(changes); + } + + @override + void onError(RealmError error) { + streamController.addError(error); + } +} diff --git a/lib/src/native/realm_core.dart b/lib/src/native/realm_core.dart index 134d1db08..242e11df8 100644 --- a/lib/src/native/realm_core.dart +++ b/lib/src/native/realm_core.dart @@ -39,6 +39,7 @@ import '../configuration.dart'; import '../credentials.dart'; import '../init.dart'; import '../list.dart'; +import '../map.dart'; import '../migration.dart'; import '../realm_class.dart'; import '../realm_object.dart'; @@ -1201,6 +1202,29 @@ class _RealmCore { }); } + RealmResultsHandle queryMap(ManagedRealmMap target, String query, List args) { + return using((arena) { + final length = args.length; + final argsPointer = arena(length); + for (var i = 0; i < length; ++i) { + _intoRealmQueryArg(args[i], argsPointer.elementAt(i), arena); + } + + final results = mapGetValues(target); + final queryHandle = _RealmQueryHandle._( + _realmLib.invokeGetPointer( + () => _realmLib.realm_query_parse_for_results( + results._pointer, + query.toCharPtr(arena), + length, + argsPointer, + ), + ), + target.realm.handle); + return _queryFindAll(queryHandle); + }); + } + RealmResultsHandle resultsFromList(RealmList list) { final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_list_to_results(list.handle._pointer)); return RealmResultsHandle._(pointer, list.realm.handle); @@ -1316,6 +1340,41 @@ class _RealmCore { }); } + MapChanges getMapChanges(RealmMapChangesHandle changes) { + return using((arena) { + final out_num_deletions = arena(); + final out_num_insertions = arena(); + final out_num_modifications = arena(); + _realmLib.realm_dictionary_get_changes( + changes._pointer, + out_num_deletions, + out_num_insertions, + out_num_modifications, + ); + + final deletionsCount = out_num_deletions != nullptr ? out_num_deletions.value : 0; + final insertionCount = out_num_insertions != nullptr ? out_num_insertions.value : 0; + final modificationCount = out_num_modifications != nullptr ? out_num_modifications.value : 0; + + final out_deletion_indexes = arena(deletionsCount); + final out_insertion_indexes = arena(insertionCount); + final out_modification_indexes = arena(modificationCount); + + _realmLib.realm_dictionary_get_changed_keys( + changes._pointer, + out_deletion_indexes, + out_num_deletions, + out_insertion_indexes, + out_num_insertions, + out_modification_indexes, + out_num_modifications, + ); + + return MapChanges(out_deletion_indexes.toStringList(deletionsCount), out_insertion_indexes.toStringList(insertionCount), + out_modification_indexes.toStringList(modificationCount)); + }); + } + _RealmLinkHandle _getObjectAsLink(RealmObjectBase object) { final realmLink = _realmLib.realm_object_as_link(object.handle._pointer); return _RealmLinkHandle._(realmLink); @@ -1488,6 +1547,98 @@ class _RealmCore { return RealmNotificationTokenHandle._(pointer, realmSet.realm.handle); } + int mapGetSize(RealmMapHandle handle) { + return using((Arena arena) { + final size = arena(); + _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_size(handle._pointer, size)); + return size.value; + }); + } + + bool mapRemoveKey(RealmMapHandle handle, String key) { + return using((Arena arena) { + final keyValue = _toRealmValue(key, arena); + final out_erased = arena(); + _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_erase(handle._pointer, keyValue.ref, out_erased)); + return out_erased.value; + }); + } + + Object? mapGetElement(RealmMap map, String key) { + return using((Arena arena) { + final realm_value = arena(); + final key_value = _toRealmValue(key, arena); + final out_found = arena(); + _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_find(map.handle._pointer, key_value.ref, realm_value, out_found)); + if (out_found.value) { + return realm_value.toDartValue(map.realm); + } + + return null; + }); + } + + bool mapIsValid(RealmMap map) { + return _realmLib.realm_dictionary_is_valid(map.handle._pointer); + } + + void mapClear(RealmMapHandle mapHandle) { + _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_clear(mapHandle._pointer)); + } + + RealmResultsHandle mapGetKeys(ManagedRealmMap map) { + return using((Arena arena) { + final out_size = arena(); + final out_keys = arena>(); + _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_get_keys(map.handle._pointer, out_size, out_keys)); + return RealmResultsHandle._(out_keys.value, map.realm.handle); + }); + } + + RealmResultsHandle mapGetValues(ManagedRealmMap map) { + final result = _realmLib.invokeGetPointer(() => _realmLib.realm_dictionary_to_results(map.handle._pointer)); + return RealmResultsHandle._(result, map.realm.handle); + } + + bool mapContainsKey(ManagedRealmMap map, String key) { + return using((Arena arena) { + final key_value = _toRealmValue(key, arena); + final out_found = arena(); + _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_contains_key(map.handle._pointer, key_value.ref, out_found)); + return out_found.value; + }); + } + + bool mapContainsValue(ManagedRealmMap map, Object? value) { + return using((Arena arena) { + final key_value = _toRealmValue(value, arena); + final out_index = arena(); + _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_contains_value(map.handle._pointer, key_value.ref, out_index)); + return out_index.value > -1; + }); + } + + RealmObjectHandle mapInsertEmbeddedObject(Realm realm, RealmMapHandle handle, String key) { + return using((Arena arena) { + final realm_value = _toRealmValue(key, arena); + final ptr = _realmLib.invokeGetPointer(() => _realmLib.realm_dictionary_insert_embedded(handle._pointer, realm_value.ref)); + return RealmObjectHandle._(ptr, realm.handle); + }); + } + + void mapInsertValue(RealmMapHandle handle, String key, Object? value) { + using((Arena arena) { + final key_value = _toRealmValue(key, arena); + final realm_value = _toRealmValue(value, arena); + _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_insert(handle._pointer, key_value.ref, realm_value.ref, nullptr, nullptr)); + }); + } + + RealmMapHandle getMapProperty(RealmObjectBase object, int propertyKey) { + final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_get_dictionary(object.handle._pointer, propertyKey)); + return RealmMapHandle._(pointer, object.realm.handle); + } + bool _equals(HandleBase first, HandleBase second) { return _realmLib.realm_equals(first._pointer.cast(), second._pointer.cast()); } @@ -1569,6 +1720,31 @@ class _RealmCore { } } + static void map_change_callback(Pointer userdata, Pointer data) { + NotificationsController? controller = userdata.toObject(); + if (controller == null) { + return; + } + + if (data == nullptr) { + controller.onError(RealmError("Invalid notifications data received")); + return; + } + + try { + final clonedData = _realmLib.realm_clone(data.cast()); + if (clonedData == nullptr) { + controller.onError(RealmError("Error while cloning notifications data")); + return; + } + + final changesHandle = RealmMapChangesHandle._(clonedData.cast()); + controller.onChanges(changesHandle); + } catch (e) { + controller.onError(RealmError("Error handling change notifications. Error: $e")); + } + } + RealmNotificationTokenHandle subscribeResultsNotifications(RealmResults results, NotificationsController controller) { final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_results_add_notification_callback( results.handle._pointer, @@ -1605,6 +1781,18 @@ class _RealmCore { return RealmNotificationTokenHandle._(pointer, object.realm.handle); } + RealmNotificationTokenHandle subscribeMapNotifications(RealmMap map, NotificationsController controller) { + final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_dictionary_add_notification_callback( + map.handle._pointer, + controller.toWeakHandle(), + nullptr, + nullptr, + Pointer.fromFunction(map_change_callback), + )); + + return RealmNotificationTokenHandle._(pointer, map.realm.handle); + } + bool getObjectChangesIsDeleted(RealmObjectChangesHandle handle) { return _realmLib.realm_object_changes_is_deleted(handle._pointer); } @@ -2512,6 +2700,14 @@ class _RealmCore { }); } + RealmMapHandle? resolveMap(ManagedRealmMap map, Realm frozenRealm) { + return using((Arena arena) { + final resultPtr = arena>(); + _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_resolve_in(map.handle._pointer, frozenRealm.handle._pointer, resultPtr)); + return resultPtr == nullptr ? null : RealmMapHandle._(resultPtr.value, frozenRealm.handle); + }); + } + static void _app_api_key_completion_callback(Pointer userdata, Pointer apiKey, Pointer error) { final Completer? completer = userdata.toObject(isPersistent: true); if (completer == null) { @@ -2880,6 +3076,10 @@ class RealmSetHandle extends RootedHandleBase { RealmSetHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 96); } +class RealmMapHandle extends RootedHandleBase { + RealmMapHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 96); // TODO: check size +} + class _RealmQueryHandle extends RootedHandleBase { _RealmQueryHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 256); } @@ -2896,6 +3096,10 @@ class RealmCollectionChangesHandle extends HandleBase RealmCollectionChangesHandle._(Pointer pointer) : super(pointer, 256); } +class RealmMapChangesHandle extends HandleBase { + RealmMapChangesHandle._(Pointer pointer) : super(pointer, 256); +} + class RealmObjectChangesHandle extends HandleBase { RealmObjectChangesHandle._(Pointer pointer) : super(pointer, 256); } @@ -3175,6 +3379,18 @@ extension on Pointer { } } +extension on Pointer { + List toStringList(int count) { + final result = List.filled(count, ''); + for (var i = 0; i < count; i++) { + final str_value = elementAt(i).ref.values.string; + result[i] = str_value.data.cast().toRealmDartString(length: str_value.size)!; + } + + return result; + } +} + extension on Pointer { T? toObject({bool isPersistent = false}) { assert(this != nullptr, "Pointer is null"); diff --git a/lib/src/realm_class.dart b/lib/src/realm_class.dart index 1845cd5bc..fc7ea30f9 100644 --- a/lib/src/realm_class.dart +++ b/lib/src/realm_class.dart @@ -34,6 +34,7 @@ import 'scheduler.dart'; import 'session.dart'; import 'subscription.dart'; import 'set.dart'; +import 'map.dart'; export 'package:cancellation_token/cancellation_token.dart' show CancellationToken, TimeoutCancellationToken, CancelledException; export 'package:realm_common/realm_common.dart' @@ -108,6 +109,7 @@ export "configuration.dart" export 'credentials.dart' show AuthProviderType, Credentials, EmailPasswordAuthProvider; export 'list.dart' show RealmList, RealmListOfObject, RealmListChanges, ListExtension; export 'set.dart' show RealmSet, RealmSetChanges, RealmSetOfObject; +export 'map.dart' show RealmMap, RealmMapChanges, RealmMapOfObject; export 'migration.dart' show Migration; export 'realm_object.dart' show @@ -737,6 +739,10 @@ extension RealmInternal on Realm { return RealmSetInternal.create(handle, this, metadata); } + RealmMap createMap(RealmMapHandle handle, RealmObjectMetadata? metadata) { + return RealmMapInternal.create(handle, this, metadata); + } + List getPropertyNames(Type type, List propertyKeys) { final metadata = _metadata.getByType(type); final result = []; @@ -807,6 +813,15 @@ extension RealmInternal on Realm { return createSet(handle, set.metadata); } + RealmMap? resolveMap(ManagedRealmMap map) { + final handle = realmCore.resolveMap(map, this); + if (handle == null) { + return null; + } + + return createMap(handle, map.metadata); + } + static MigrationRealm getMigrationRealm(Realm realm) => MigrationRealm._(realm); bool get isInMigration => _isInMigration; @@ -949,11 +964,11 @@ class RealmMetadata { RealmObjectMetadata? getByClassKeyIfExists(int key) => _classKeyMap[key]; - Tuple getByClassKey(int key) { + (Type type, RealmObjectMetadata meta) getByClassKey(int key) { final meta = _classKeyMap[key]; if (meta != null) { final type = _typeMap.entries.firstWhereOrNull((e) => e.value.classKey == key)?.key ?? RealmObjectBase; - return Tuple(type, meta); + return (type, meta); } throw RealmError("Object with classKey $key not found in the current Realm's schema."); } diff --git a/lib/src/realm_object.dart b/lib/src/realm_object.dart index 670ad89ec..6fd91b088 100644 --- a/lib/src/realm_object.dart +++ b/lib/src/realm_object.dart @@ -28,6 +28,7 @@ import 'native/realm_core.dart'; import 'realm_class.dart'; import 'results.dart'; import 'set.dart'; +import 'map.dart'; typedef DartDynamic = dynamic; @@ -145,73 +146,95 @@ class RealmCoreAccessor implements RealmAccessor { Object? get(RealmObjectBase object, String name) { try { final propertyMeta = metadata[name]; - if (propertyMeta.collectionType == RealmCollectionType.list) { - if (propertyMeta.propertyType == RealmPropertyType.linkingObjects) { - final sourceMeta = object.realm.metadata.getByName(propertyMeta.objectType!); - final sourceProperty = sourceMeta[propertyMeta.linkOriginProperty!]; - final handle = realmCore.getBacklinks(object, sourceMeta.classKey, sourceProperty.key); - return RealmResultsInternal.create(handle, object.realm, sourceMeta); - } - - final handle = realmCore.getListProperty(object, propertyMeta.key); - final listMetadata = propertyMeta.objectType == null ? null : object.realm.metadata.getByName(propertyMeta.objectType!); - if (propertyMeta.propertyType == RealmPropertyType.mixed) { - return object.realm.createList(handle, metadata); - } - - // listMetadata is not null when we have list of RealmObjects. If the API was - // called with a generic object arg - get we construct a list of - // RealmObjects since we don't know the type of the object. - if (listMetadata != null && _isTypeGenericObject()) { - switch (listMetadata.schema.baseType) { - case ObjectType.realmObject: - return object.realm.createList(handle, listMetadata); - case ObjectType.embeddedObject: - return object.realm.createList(handle, listMetadata); - case ObjectType.asymmetricObject: - return object.realm.createList(handle, listMetadata); - default: - throw RealmError('List of ${listMetadata.schema.baseType} is not supported yet'); + switch (propertyMeta.collectionType) { + case RealmCollectionType.list: + if (propertyMeta.propertyType == RealmPropertyType.linkingObjects) { + final sourceMeta = object.realm.metadata.getByName(propertyMeta.objectType!); + final sourceProperty = sourceMeta[propertyMeta.linkOriginProperty!]; + final handle = realmCore.getBacklinks(object, sourceMeta.classKey, sourceProperty.key); + return RealmResultsInternal.create(handle, object.realm, sourceMeta); } - } - return object.realm.createList(handle, listMetadata); - } - if (propertyMeta.collectionType == RealmCollectionType.set) { - final handle = realmCore.getSetProperty(object, propertyMeta.key); - final setMetadata = propertyMeta.objectType == null ? null : object.realm.metadata.getByName(propertyMeta.objectType!); - return RealmSetInternal.create(handle, object.realm, setMetadata); - } + final handle = realmCore.getListProperty(object, propertyMeta.key); + final listMetadata = propertyMeta.objectType == null ? null : object.realm.metadata.getByName(propertyMeta.objectType!); - var value = realmCore.getProperty(object, propertyMeta.key); - - if (value is RealmObjectHandle) { - final meta = object.realm.metadata; - final typeName = propertyMeta.objectType; + if (propertyMeta.propertyType == RealmPropertyType.mixed) { + return object.realm.createList(handle, metadata); + } - late Type type; - late RealmObjectMetadata targetMetadata; + // listMetadata is not null when we have list of RealmObjects. If the API was + // called with a generic object arg - get we construct a list of + // RealmObjects since we don't know the type of the object. + if (listMetadata != null && _isTypeGenericObject()) { + switch (listMetadata.schema.baseType) { + case ObjectType.realmObject: + return object.realm.createList(handle, listMetadata); + case ObjectType.embeddedObject: + return object.realm.createList(handle, listMetadata); + case ObjectType.asymmetricObject: + return object.realm.createList(handle, listMetadata); + default: + throw RealmError('List of ${listMetadata.schema.baseType} is not supported yet'); + } + } + return object.realm.createList(handle, listMetadata); + case RealmCollectionType.set: + final handle = realmCore.getSetProperty(object, propertyMeta.key); + final setMetadata = propertyMeta.objectType == null ? null : object.realm.metadata.getByName(propertyMeta.objectType!); + return RealmSetInternal.create(handle, object.realm, setMetadata); + case RealmCollectionType.map: + final handle = realmCore.getMapProperty(object, propertyMeta.key); + final mapMetadata = propertyMeta.objectType == null ? null : object.realm.metadata.getByName(propertyMeta.objectType!); + + if (propertyMeta.propertyType == RealmPropertyType.mixed) { + return object.realm.createMap(handle, metadata); + } - if (propertyMeta.propertyType == RealmPropertyType.mixed) { - final tuple = meta.getByClassKey(realmCore.getClassKey(value)); - type = tuple.item1; - targetMetadata = tuple.item2; - } else { - // If we have an object but the user called the API without providing a generic - // arg, we construct a RealmObject since we don't know the type of the object. - type = _isTypeGenericObject() ? RealmObjectBase : T; - targetMetadata = typeName != null ? meta.getByName(typeName) : meta.getByType(type); - } + // mapMetadata is not null when we have map of RealmObjects. If the API was + // called with a generic object arg - get we construct a map of + // RealmObjects since we don't know the type of the object. + if (mapMetadata != null && _isTypeGenericObject()) { + switch (mapMetadata.schema.baseType) { + case ObjectType.realmObject: + return object.realm.createMap(handle, mapMetadata); + case ObjectType.embeddedObject: + return object.realm.createMap(handle, mapMetadata); + case ObjectType.asymmetricObject: + return object.realm.createMap(handle, mapMetadata); + default: + throw RealmError('Map of ${mapMetadata.schema.baseType} is not supported yet'); + } + } + return object.realm.createMap(handle, mapMetadata); + default: + var value = realmCore.getProperty(object, propertyMeta.key); + + if (value is RealmObjectHandle) { + final meta = object.realm.metadata; + final typeName = propertyMeta.objectType; + + late Type type; + late RealmObjectMetadata targetMetadata; + + if (propertyMeta.propertyType == RealmPropertyType.mixed) { + (type, targetMetadata) = meta.getByClassKey(realmCore.getClassKey(value)); + } else { + // If we have an object but the user called the API without providing a generic + // arg, we construct a RealmObject since we don't know the type of the object. + type = _isTypeGenericObject() ? RealmObjectBase : T; + targetMetadata = typeName != null ? meta.getByName(typeName) : meta.getByType(type); + } + + value = object.realm.createObject(type, value, targetMetadata); + } - value = object.realm.createObject(type, value, targetMetadata); - } + if (T == RealmValue) { + value = RealmValue.from(value); + } - if (T == RealmValue) { - value = RealmValue.from(value); + return value; } - - return value; } on Exception catch (e) { throw RealmException("Error getting property ${metadata._realmObjectTypeName}.$name Error: $e"); } @@ -221,29 +244,20 @@ class RealmCoreAccessor implements RealmAccessor { void set(RealmObjectBase object, String name, Object? value, {bool isDefault = false, bool update = false}) { final propertyMeta = metadata[name]; try { - if (value is RealmList) { + if (value is RealmList) { final handle = realmCore.getListProperty(object, propertyMeta.key); - if (update) realmCore.listClear(handle); - for (var i = 0; i < value.length; i++) { - RealmListInternal.setValue(handle, object.realm, i, value[i], update: update); + if (update) { + realmCore.listClear(handle); } - return; - } - if (value is EmbeddedObject) { - if (value.isManaged) { - throw RealmError("Can't set an embedded object that is already managed"); + for (var i = 0; i < value.length; i++) { + RealmListInternal.setValue(handle, object.realm, i, value[i], update: update); } - - final handle = realmCore.createEmbeddedObject(object, propertyMeta.key); - object.realm.manageEmbedded(handle, value, update: update); return; } - object.realm.addUnmanagedRealmObjectFromValue(value, update); - //TODO: set from ManagedRealmList is not supported yet - if (value is UnmanagedRealmSet) { + if (value is RealmSet) { final handle = realmCore.getSetProperty(object, propertyMeta.key); if (update) { realmCore.realmSetClear(handle); @@ -263,6 +277,30 @@ class RealmCoreAccessor implements RealmAccessor { return; } + if (value is RealmMap) { + final handle = realmCore.getMapProperty(object, propertyMeta.key); + if (update) { + realmCore.mapClear(handle); + } + + for (var kvp in value.entries) { + RealmMapInternal.setValue(handle, object.realm, kvp.key, kvp.value, update: update); + } + return; + } + + if (value is EmbeddedObject) { + if (value.isManaged) { + throw RealmError("Can't set an embedded object that is already managed"); + } + + final handle = realmCore.createEmbeddedObject(object, propertyMeta.key); + object.realm.manageEmbedded(handle, value, update: update); + return; + } + + object.realm.addUnmanagedRealmObjectFromValue(value, update); + if (propertyMeta.isPrimaryKey && !isInMigration) { final currentValue = realmCore.getProperty(object, propertyMeta.key); if (currentValue != value) { @@ -490,8 +528,8 @@ extension EmbeddedObjectExtension on EmbeddedObject { } final parent = realmCore.getEmbeddedParent(this); - final metadata = realm.metadata.getByClassKey(parent.item2); - return realm.createObject(metadata.item1, parent.item1, metadata.item2); + final (type, metadata) = realm.metadata.getByClassKey(parent.item2); + return realm.createObject(type, parent.item1, metadata); } } diff --git a/lib/src/results.dart b/lib/src/results.dart index 0c28f6eed..06db973e0 100644 --- a/lib/src/results.dart +++ b/lib/src/results.dart @@ -50,13 +50,30 @@ class RealmResults extends Iterable with RealmEntity imple /// Returns the element of type `T` at the specified [index]. @override T elementAt(int index) { - if (this is RealmResults) { - final handle = realmCore.resultsGetObjectAt(this, _skipOffset + index); - final accessor = RealmCoreAccessor(metadata, realm.isInMigration); - return RealmObjectInternal.create(T, realm, handle, accessor) as T; - } else { - return realmCore.resultsGetElementAt(this, _skipOffset + index) as T; + // TODO: this is identical to list[] - consider refactoring to combine them. + if (index < 0 || index >= length) { + throw RangeError.range(index, 0, length - 1); } + + var value = realmCore.resultsGetElementAt(this, _skipOffset + index); + + if (value is RealmObjectHandle) { + late RealmObjectMetadata targetMetadata; + late Type type; + if (T == RealmValue) { + (type, targetMetadata) = realm.metadata.getByClassKey(realmCore.getClassKey(value)); + } else { + targetMetadata = _metadata!; + type = T; + } + value = realm.createObject(type, value, targetMetadata); + } + + if (T == RealmValue) { + value = RealmValue.from(value); + } + + return value as T; } @pragma('vm:prefer-inline') diff --git a/lib/src/set.dart b/lib/src/set.dart index 4be7c6d17..3e6edf62f 100644 --- a/lib/src/set.dart +++ b/lib/src/set.dart @@ -166,9 +166,7 @@ class ManagedRealmSet with RealmEntity, SetMixin implement late RealmObjectMetadata targetMetadata; late Type type; if (T == RealmValue) { - final tuple = realm.metadata.getByClassKey(realmCore.getClassKey(value)); - type = tuple.item1; - targetMetadata = tuple.item2; + (type, targetMetadata) = realm.metadata.getByClassKey(realmCore.getClassKey(value)); } else { targetMetadata = _metadata!; // will be null for RealmValue, so defer until here type = T; @@ -367,16 +365,14 @@ class RealmSetNotificationsController extends NotificationsCo } } -// The query operations on sets, as well as the ability to subscribe for notifications, -// only work for sets of objects (core restriction), so we add these as an extension methods -// to allow the compiler to prevent misuse. +// The query operations on sets only work for sets of objects (core restriction), +// so we add these as an extension methods to allow the compiler to prevent misuse. extension RealmSetOfObject on RealmSet { /// Filters the set and returns a new [RealmResults] according to the provided [query] (with optional [arguments]). /// /// Only works for sets of [RealmObject]s or [EmbeddedObject]s. /// - /// The Realm Dart and Realm Flutter SDKs supports querying based on a language inspired by [NSPredicate](https://academy.realm.io/posts/nspredicate-cheatsheet/) - /// and [Predicate Programming Guide.](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Predicates/AdditionalChapters/Introduction.html#//apple_ref/doc/uid/TP40001789) + /// For more details about the syntax of the Realm Query Language, refer to the documentation: https://www.mongodb.com/docs/realm/realm-query-language/. RealmResults query(String query, [List arguments = const []]) { final handle = realmCore.querySet(asManaged(), query, arguments); return RealmResultsInternal.create(handle, realm, _metadata); diff --git a/test/backlinks_test.dart b/test/backlinks_test.dart index 4e77bbdaf..f4b8bf0e2 100644 --- a/test/backlinks_test.dart +++ b/test/backlinks_test.dart @@ -28,7 +28,7 @@ class _Source { String name = 'source'; @MapTo('et mål') // to throw a curve ball.. _Target? oneTarget; - List<_Target> manyTargets = []; + late List<_Target> manyTargets; } @RealmModel() diff --git a/test/geospatial_test.dart b/test/geospatial_test.dart index 9d207e7ec..290048347 100644 --- a/test/geospatial_test.dart +++ b/test/geospatial_test.dart @@ -30,7 +30,7 @@ part 'geospatial_test.g.dart'; @RealmModel(ObjectType.embeddedObject) class _Location { final String type = 'Point'; - final List coordinates = const [0, 0]; + late final List coordinates; double get lon => coordinates[0]; set lon(double value) => coordinates[0] = value; @@ -62,7 +62,7 @@ void createRestaurants(Realm realm) { @RealmModel() class _LocationList { - final locations = <_Location>[]; + late final List<_Location> locations; @override String toString() => '[${locations.join(', ')}]'; diff --git a/test/realm_map_test.dart b/test/realm_map_test.dart new file mode 100644 index 000000000..56ac0b458 --- /dev/null +++ b/test/realm_map_test.dart @@ -0,0 +1,966 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright 2023 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; +import 'package:test/test.dart' hide test, throws; +import '../lib/realm.dart'; + +import 'test.dart'; + +part 'realm_map_test.g.dart'; + +@RealmModel() +class _Car { + @PrimaryKey() + late String make; + late String? color; +} + +@RealmModel(ObjectType.embeddedObject) +class _EmbeddedValue { + late int intValue; +} + +@RealmModel() +class _TestRealmMaps { + @PrimaryKey() + late int key; + + late Map boolMap; + late Map intMap; + late Map stringMap; + late Map doubleMap; + late Map dateTimeMap; + late Map objectIdMap; + late Map uuidMap; + late Map binaryMap; + late Map decimalMap; + + late Map nullableBoolMap; + late Map nullableIntMap; + late Map nullableStringMap; + late Map nullableDoubleMap; + late Map nullableDateTimeMap; + late Map nullableObjectIdMap; + late Map nullableUuidMap; + late Map nullableBinaryMap; + late Map nullableDecimalMap; + + late Map objectsMap; + late Map embeddedMap; + + late Map mixedMap; +} + +class TestCaseData { + final T Function(T) _cloneFunc; + final bool Function(T?, T?) _equalityFunc; + + final T _sampleValue; + + final List<(String key, T value)> _initialValues; + + List<(String key, T value)> get initialValues => _initialValues.map((kvp) => (kvp.$1, _cloneFunc(kvp.$2))).toList(); + + T get sampleValue => _cloneFunc(_sampleValue); + + TestCaseData(this._sampleValue, {bool Function(T?, T?)? equalityFunc, List<(String key, T value)> initialValues = const [], T Function(T)? cloneFunc}) + : _equalityFunc = equalityFunc ?? ((a, b) => a == b), + _cloneFunc = cloneFunc ?? ((v) => v), + _initialValues = initialValues; + + void seed(Map target, {Iterable<(String key, T value)>? values}) { + _writeIfNecessary(target, () { + target.clear(); + for (var (key, value) in values ?? initialValues) { + target[key] = value; + } + }); + } + + void assertEquivalent(Map target) { + final reference = _getReferenceMap(); + _isEquivalent(target, reference); + } + + void assertContainsKey(Map target) { + for (final (key, _) in initialValues) { + expect(target.containsKey(key), true, reason: 'expected to find $key'); + } + + expect(target.containsKey(Uuid.v4().toString()), false); + } + + void assertKeys(Map target) { + expect(target.keys, unorderedEquals(initialValues.map((e) => e.$1))); + } + + void assertValues(Map target) { + expect(target.values.length, initialValues.length); + final actualValues = target.values; + for (final (_, value) in initialValues) { + expect(actualValues.where((element) => _equalityFunc(element, value)).length, greaterThanOrEqualTo(1)); // values may be duplicates + } + + // Test in the other direction in case we have duplicates + for (final value in actualValues) { + expect(initialValues.where((element) => _equalityFunc(element.$2, value)).length, greaterThanOrEqualTo(1)); // values may be duplicates + } + } + + void assertEntries(Map target) { + final reference = _getReferenceMap(); + for (final kvp in target.entries) { + expect(reference.containsKey(kvp.key), true); + expect(_equalityFunc(reference[kvp.key], target[kvp.key]), true); + + reference.remove(kvp.key); + } + + expect(reference, isEmpty); + } + + void assertAccessor(Map target) { + for (final (key, value) in initialValues) { + expect(_equalityFunc(target[key], value), true); + } + + expect(target[Uuid.v4().toString()], null); + } + + void assertSet(Map target) { + var expectedLength = target.length; + + if (target.isNotEmpty) { + final key = target.keys.first; + _writeIfNecessary(target, () { + target[key] = sampleValue; + }); + + expect(target.containsKey(key), true); + expect(_equalityFunc(target[key], sampleValue), true); + expect(target.length, expectedLength); + } + + final newKey = Uuid.v4().toString(); + _writeIfNecessary(target, () { + target[newKey] = sampleValue; + }); + + expectedLength++; + + expect(target.containsKey(newKey), true); + expect(_equalityFunc(target[newKey], sampleValue), true); + expect(target.length, expectedLength); + } + + void assertRemove(Map target) { + seed(target); + + var expectedLength = target.length; + + if (target.isNotEmpty) { + final kvp = target.entries.last; + final removedValue = _writeIfNecessary(target, () => target.remove(kvp.key)); + expectedLength--; + + expect(removedValue, kvp.value); + expect(target.containsKey(kvp.key), false); + expect(target.length, expectedLength); + } + + final newKey = Uuid.v4().toString(); + final removedValue = _writeIfNecessary(target, () => target.remove(newKey)); + + expect(removedValue, null); + expect(target.containsKey(newKey), false); + expect(target.length, expectedLength); + } + + (String key, T value) _getDifferentValue(Map collection, T valueToCompare) { + for (final kvp in collection.entries) { + if (!_areValuesEqual(kvp.value, valueToCompare)) { + return (kvp.key, kvp.value); + } + } + + throw StateError('Could not find a different value'); + } + + void _isEquivalent(Map actual, Map expected) { + expect(actual, hasLength(expected.length)); + for (final kvp in expected.entries) { + final actualEntry = actual.entries.firstWhereOrNull((element) => element.key == kvp.key); + expect(actualEntry, isNotNull, reason: 'expect actual to contain ${kvp.key}'); + final actualValue = actual[kvp.key]; + expect(_equalityFunc(actualValue, kvp.value), true, reason: 'expected $actualValue == ${kvp.value}'); + } + } + + bool _areValuesEqual(T first, T second) { + if (first == second) { + return true; + } + + if (first is Uint8List && second is Uint8List) { + return IterableEquality().equals(first, second); + } + + return false; + } + + U _writeIfNecessary(Map collection, U Function() writeAction) { + Transaction? transaction; + try { + if (collection is RealmMap && collection.isManaged) { + transaction = collection.realm.beginWrite(); + } + + final result = writeAction(); + + transaction?.commit(); + + return result; + } catch (e) { + transaction?.rollback(); + rethrow; + } + } + + Map _getReferenceMap() => {for (var v in initialValues) v.$1: v.$2}; + + @override + String toString() { + return _initialValues.map((kvp) => '${kvp.$1}-${kvp.$2}').join(', '); + } +} + +List> boolTestValues() => [ + TestCaseData(true), + TestCaseData(true, initialValues: [('a', true)]), + TestCaseData(false, initialValues: [('b', false)]), + TestCaseData(true, initialValues: [('a', false), ('b', true)]), + TestCaseData(false, initialValues: [('a', true), ('b', false), ('c', true)]), + ]; + +List> nullableBoolTestValues() => [ + TestCaseData(true), + TestCaseData(true, initialValues: [('a', true)]), + TestCaseData(true, initialValues: [('b', false)]), + TestCaseData(false, initialValues: [('c', null)]), + TestCaseData(true, initialValues: [('a', false), ('b', true)]), + TestCaseData(null, initialValues: [('a', true), ('b', false), ('c', null)]), + ]; + +List> intTestCases() => [ + TestCaseData(123456789), + TestCaseData(123456789, initialValues: [('123', 123)]), + TestCaseData(123456789, initialValues: [('123', -123)]), + TestCaseData(123456789, initialValues: [('a', 1), ('b', 1), ('c', 1)]), + TestCaseData(123456789, initialValues: [('a', 1), ('b', 2), ('c', 3)]), + TestCaseData(123456789, initialValues: [('a', -0x8000000000000000), ('z', 0x7FFFFFFFFFFFFFFF)]), + TestCaseData(123456789, initialValues: [('a', -0x8000000000000000), ('zero', 0), ('one', 1), ('z', 0x7FFFFFFFFFFFFFFF)]), + ]; + +List> nullableIntTestCases() => [ + TestCaseData(1234), + TestCaseData(null, initialValues: [('123', 123)]), + TestCaseData(1234, initialValues: [('123', -123)]), + TestCaseData(1234, initialValues: [('null', null)]), + TestCaseData(1234, initialValues: [('null1', null), ('null2', null), ('null3', null)]), + TestCaseData(null, initialValues: [('a', 1), ('b', null), ('c', 3)]), + TestCaseData(1234, initialValues: [('a', -0x8000000000000000), ('m', null), ('z', 0x7FFFFFFFFFFFFFFF)]), + TestCaseData(1234, initialValues: [('a', -0x8000000000000000), ('zero', 0), ('null', null), ('one', 1), ('z', 0x7FFFFFFFFFFFFFFF)]), + ]; + +List> stringTestValues() => [ + TestCaseData(''), + TestCaseData('', initialValues: [('123', 'abc')]), + TestCaseData('', initialValues: [('a', 'AbCdEfG'), ('b', 'HiJklMn'), ('c', 'OpQrStU')]), + TestCaseData('', initialValues: [('a', 'vwxyz'), ('b', ''), ('c', ' ')]), + TestCaseData('', initialValues: [('a', ''), ('z', 'aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu vv ww xx yy zz')]), + TestCaseData('', initialValues: [('a', ''), ('z', 'lorem ipsum'), ('zero', '-1234567890'), ('one', 'lololo')]), + ]; + +List> nullableStringTestValues() => [ + TestCaseData(null), + TestCaseData(null, initialValues: [('123', 'abc')]), + TestCaseData('', initialValues: [('null', null)]), + TestCaseData('', initialValues: [('null1', null), ('null2', null)]), + TestCaseData('', initialValues: [('a', 'AbCdEfG'), ('b', null), ('c', 'OpQrStU')]), + TestCaseData(null, initialValues: [('a', 'vwxyz'), ('b', null), ('c', ''), ('d', ' ')]), + TestCaseData('', initialValues: [('a', ''), ('m', null), ('z', 'aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu vv ww xx yy zz')]), + TestCaseData('', initialValues: [('a', ''), ('zero', 'lorem ipsum'), ('null', null), ('one', '-1234567890'), ('z', 'lololo')]), + ]; + +List> doubleTestValues() => [ + TestCaseData(789.123), + TestCaseData(789.123, initialValues: [('123', 123.123)]), + TestCaseData(789.123, initialValues: [('123', -123.456)]), + TestCaseData(789.123, initialValues: [('a', 1.1), ('b', 1.1), ('c', 1.1)]), + TestCaseData(789.123, initialValues: [('a', 1), ('b', 2.2), ('c', 3.3)]), + TestCaseData(789.123, + initialValues: [('a', 1), ('b', 2.2), ('c', 3.3), ('d', 4385948963486946854968945789458794538793438693486934869.238593285932859238952398)]), + TestCaseData(789.123, initialValues: [('a', -double.maxFinite), ('z', double.maxFinite)]), + TestCaseData(789.123, initialValues: [('a', -double.maxFinite), ('zero', 0.0), ('one', 1.1), ('z', double.maxFinite)]), + ]; + +List> nullableDoubleTestValues() => [ + TestCaseData(-123.789), + TestCaseData(-123.789, initialValues: [('123', 123.123)]), + TestCaseData(null, initialValues: [('123', -123.456)]), + TestCaseData(-123.789, initialValues: [('null', null)]), + TestCaseData(-123.789, initialValues: [('null1', null), ('null2', null)]), + TestCaseData(-123.789, initialValues: [('a', 1), ('b', null), ('c', 3.3)]), + TestCaseData(null, + initialValues: [('a', 1), ('b', null), ('c', 3.3), ('d', 4385948963486946854968945789458794538793438693486934869.238593285932859238952398)]), + TestCaseData(-123.789, initialValues: [('a', -double.maxFinite), ('m', null), ('z', double.maxFinite)]), + TestCaseData(-123.789, initialValues: [('a', -double.maxFinite), ('zero', 0), ('null', null), ('one', 1.1), ('z', double.maxFinite)]), + ]; + +List> decimal128TestValues() => [ + TestCaseData(Decimal128.parse('1.5')), + TestCaseData(Decimal128.parse('1.5'), initialValues: [('123', Decimal128.parse('123.123'))]), + TestCaseData(Decimal128.parse('1.5'), initialValues: [('123', Decimal128.parse('-123.456'))]), + TestCaseData(Decimal128.parse('1.5'), initialValues: [('a', Decimal128.parse('1.1')), ('b', Decimal128.parse('1.1')), ('c', Decimal128.parse('1.1'))]), + TestCaseData(Decimal128.parse('1.5'), initialValues: [('a', Decimal128.parse('1')), ('b', Decimal128.parse('2.2')), ('c', Decimal128.parse('3.3'))]), + TestCaseData(Decimal128.parse('1.5'), initialValues: [ + ('a', Decimal128.parse('1')), + ('b', Decimal128.parse('2.2')), + ('c', Decimal128.parse('3.3')), + ('d', Decimal128.parse('43859489538793438693486934869.238436346943634634634634634634634634634593285932859238952398')) + ]), + TestCaseData(Decimal128.parse('1.5'), initialValues: [ + ('a', Decimal128.parse('-79228162514264337593543950335')), + ('a1', Decimal128.parse('-79228162514264337593543950335')), + ('z', Decimal128.parse('79228162514264337593543950335')), + ('z1', Decimal128.parse('79228162514264337593543950335')) + ]), + TestCaseData(Decimal128.parse('1.5'), initialValues: [ + ('a', Decimal128.parse('-79228162514264337593543950335')), + ('zero', Decimal128.parse('0')), + ('one', Decimal128.parse('1.1')), + ('z', Decimal128.parse('79228162514264337593543950335')) + ]), + ]; + +List> nullableDecimal128TestValues() => [ + TestCaseData(null), + TestCaseData(Decimal128.parse('-9.7'), initialValues: [('123', Decimal128.parse('123.123'))]), + TestCaseData(Decimal128.parse('-9.7'), initialValues: [('123', Decimal128.parse('-123.456'))]), + TestCaseData(Decimal128.parse('-9.7'), initialValues: [('null', null)]), + TestCaseData(Decimal128.parse('-9.7'), initialValues: [('null1', null), ('null2', null)]), + TestCaseData(Decimal128.parse('-9.7'), initialValues: [('a', Decimal128.parse('1')), ('b', null), ('c', Decimal128.parse('3.3'))]), + TestCaseData(Decimal128.parse('-9.7'), initialValues: [ + ('a', Decimal128.parse('1')), + ('b', null), + ('c', Decimal128.parse('3.3')), + ('d', Decimal128.parse('43859489538793438693486934869.238436346943634634634634634634634634634593285932859238952398')) + ]), + TestCaseData(Decimal128.parse('-9.7'), initialValues: [ + ('a', Decimal128.parse('-79228162514264337593543950335')), + ('a1', Decimal128.parse('-79228162514264337593543950335')), + ('m', null), + ('z', Decimal128.parse('79228162514264337593543950335')) + ]), + TestCaseData(Decimal128.parse('-9.7'), initialValues: [ + ('a', Decimal128.parse('-79228162514264337593543950335')), + ('zero', Decimal128.parse('0')), + ('null', null), + ('one', Decimal128.parse('1.1')), + ('z', Decimal128.parse('79228162514264337593543950335')) + ]), + ]; + +DateTime date0 = DateTime(0).toUtc(); +DateTime date1 = DateTime(1999, 3, 4, 5, 30, 23).toUtc(); +DateTime date2 = DateTime(2030, 1, 3, 9, 25, 34).toUtc(); + +List> dateTimeTestValues() => [ + TestCaseData(DateTime.now().toUtc()), + TestCaseData(DateTime.now().toUtc(), initialValues: [('123', date1)]), + TestCaseData(DateTime.now().toUtc(), initialValues: [('123', date2)]), + TestCaseData(DateTime.now().toUtc(), initialValues: [('a', date1), ('b', date1), ('c', date1)]), + TestCaseData(DateTime.now().toUtc(), initialValues: [('a', date0), ('b', date1), ('c', date2)]), + TestCaseData(DateTime.now().toUtc(), initialValues: [('a', DateTime.fromMillisecondsSinceEpoch(0).toUtc()), ('z', date2)]), + TestCaseData(DateTime.now().toUtc(), + initialValues: [('a', DateTime.fromMillisecondsSinceEpoch(0).toUtc()), ('zero', date1), ('one', date2), ('z', date2)]), + ]; + +List> nullableDateTimeTestValues() => [ + TestCaseData(null), + TestCaseData(DateTime.now().toUtc(), initialValues: [('123', date1)]), + TestCaseData(DateTime.now().toUtc(), initialValues: [('123', date2)]), + TestCaseData(DateTime.now().toUtc(), initialValues: [('null', null)]), + TestCaseData(DateTime.now().toUtc(), initialValues: [('null1', null), ('null2', null)]), + TestCaseData(DateTime.now().toUtc(), initialValues: [('a', date0), ('b', null), ('c', date2)]), + TestCaseData(DateTime.now().toUtc(), initialValues: [('a', date2), ('b', null), ('c', date1), ('d', date0)]), + TestCaseData(DateTime.now().toUtc(), initialValues: [('a', DateTime.fromMillisecondsSinceEpoch(0).toUtc()), ('m', null), ('z', date2)]), + TestCaseData(DateTime.now().toUtc(), + initialValues: [('a', DateTime.fromMillisecondsSinceEpoch(0).toUtc()), ('zero', date1), ('null', null), ('one', date2), ('z', date2)]), + ]; + +ObjectId objectId0 = ObjectId.fromValues(987654321, 5, 1); +ObjectId objectId1 = ObjectId.fromValues(0, 0, 0); +ObjectId objectId2 = ObjectId.fromValues(987654321, 5, 2); +ObjectId objectId3 = ObjectId.fromValues(55555, 123, 1); + +List> objectIdTestValues() => [ + TestCaseData(ObjectId()), + TestCaseData(ObjectId(), initialValues: [('123', objectId1)]), + TestCaseData(ObjectId(), initialValues: [('123', objectId2)]), + TestCaseData(ObjectId(), initialValues: [('a', objectId1), ('b', objectId1), ('c', objectId1)]), + TestCaseData(ObjectId(), initialValues: [('a', objectId0), ('b', objectId1), ('c', objectId2)]), + TestCaseData(ObjectId(), initialValues: [('a', objectId0), ('z', objectId3)]), + TestCaseData(ObjectId(), initialValues: [('a', objectId0), ('zero', objectId1), ('one', objectId2), ('z', objectId3)]), + ]; + +List> nullableObjectIdTestValues() => [ + TestCaseData(ObjectId()), + TestCaseData(ObjectId(), initialValues: [('123', objectId1)]), + TestCaseData(ObjectId(), initialValues: [('123', objectId2)]), + TestCaseData(ObjectId(), initialValues: [('null', null)]), + TestCaseData(ObjectId(), initialValues: [('null1', null), ('null2', null)]), + TestCaseData(null, initialValues: [('a', objectId0), ('b', null), ('c', objectId2)]), + TestCaseData(ObjectId(), initialValues: [('a', objectId2), ('b', null), ('c', objectId1), ('d', objectId0)]), + TestCaseData(ObjectId(), initialValues: [('a', objectId0), ('m', null), ('z', objectId3)]), + TestCaseData(null, initialValues: [('a', objectId0), ('zero', objectId1), ('null', null), ('one', objectId2), ('z', objectId3)]), + ]; + +Uuid uuid0 = Uuid.fromString('48f11f3a-7609-471f-b7ab-81c20c723ed9'); +Uuid uuid1 = Uuid.fromString('957ba4de-3966-46f6-b19f-242996608a8b'); +Uuid uuid2 = Uuid.fromString('081924e2-8e62-4af1-bc9c-e1a7fc365d84'); +Uuid uuid3 = Uuid.fromString('0bef5993-7480-4862-abdc-160bb364d1f3'); + +List> uuidTestValues() => [ + TestCaseData(Uuid.v4()), + TestCaseData(Uuid.v4(), initialValues: [('123', uuid1)]), + TestCaseData(Uuid.v4(), initialValues: [('123', uuid2)]), + TestCaseData(Uuid.v4(), initialValues: [('a', uuid1), ('b', uuid1), ('c', uuid1)]), + TestCaseData(Uuid.v4(), initialValues: [('a', uuid0), ('b', uuid1), ('c', uuid2)]), + TestCaseData(Uuid.v4(), initialValues: [('a', uuid0), ('z', uuid3)]), + TestCaseData(Uuid.v4(), initialValues: [('a', uuid0), ('zero', uuid1), ('one', uuid2), ('z', uuid3)]), + ]; + +List> nullableUuidTestValues() => [ + TestCaseData(Uuid.v4()), + TestCaseData(Uuid.v4(), initialValues: [('123', uuid1)]), + TestCaseData(Uuid.v4(), initialValues: [('123', uuid2)]), + TestCaseData(Uuid.v4(), initialValues: [('null', null)]), + TestCaseData(Uuid.v4(), initialValues: [('null1', null), ('null2', null)]), + TestCaseData(null, initialValues: [('a', uuid0), ('b', null), ('c', uuid2)]), + TestCaseData(Uuid.v4(), initialValues: [('a', uuid2), ('b', null), ('c', uuid1), ('d', uuid0)]), + TestCaseData(Uuid.v4(), initialValues: [('a', uuid0), ('m', null), ('z', uuid3)]), + TestCaseData(null, initialValues: [('a', uuid0), ('zero', uuid1), ('null', null), ('one', uuid2), ('z', uuid3)]), + ]; + +Uint8List byteArray0 = Uint8List.fromList([1, 2, 3]); +Uint8List byteArray1 = Uint8List.fromList([4, 5, 6]); +Uint8List byteArray2 = Uint8List.fromList([7, 8, 9]); + +List> byteArrayTestValues() => [ + TestCaseData(Uint8List.fromList([1, 2, 3]), equalityFunc: IterableEquality().equals), + TestCaseData(Uint8List.fromList([1, 2, 3]), initialValues: [('123', byteArray1)], equalityFunc: IterableEquality().equals), + TestCaseData(Uint8List.fromList([1, 2, 3]), initialValues: [('123', byteArray2)], equalityFunc: IterableEquality().equals), + TestCaseData(Uint8List.fromList([1, 2, 3]), + initialValues: [('a', byteArray1), ('b', byteArray1), ('c', byteArray1)], equalityFunc: IterableEquality().equals), + TestCaseData(Uint8List.fromList([1, 2, 3]), + initialValues: [('a', byteArray0), ('b', byteArray1), ('c', byteArray2)], equalityFunc: IterableEquality().equals), + TestCaseData(Uint8List.fromList([1, 2, 3]), + initialValues: [ + ('a', Uint8List.fromList([0])), + ('z', Uint8List.fromList([255])) + ], + equalityFunc: IterableEquality().equals), + TestCaseData(Uint8List.fromList([1, 2, 3]), + initialValues: [ + ('a', byteArray0), + ('zero', byteArray1), + ('one', byteArray2), + ('z', Uint8List.fromList([255])) + ], + equalityFunc: IterableEquality().equals), + ]; + +List> nullableByteArrayTestValues() => [ + TestCaseData(null), + TestCaseData(Uint8List.fromList([1, 2, 3]), initialValues: [('123', byteArray1)], equalityFunc: IterableEquality().equals), + TestCaseData(Uint8List.fromList([1, 2, 3]), initialValues: [('123', byteArray2)], equalityFunc: IterableEquality().equals), + TestCaseData(Uint8List.fromList([1, 2, 3]), initialValues: [('null', null)], equalityFunc: IterableEquality().equals), + TestCaseData(Uint8List.fromList([1, 2, 3]), initialValues: [('null1', null), ('null2', null)], equalityFunc: IterableEquality().equals), + TestCaseData(null, initialValues: [('a', byteArray0), ('b', null), ('c', byteArray2)], equalityFunc: IterableEquality().equals), + TestCaseData(Uint8List.fromList([1, 2, 3]), + initialValues: [('a', byteArray2), ('b', null), ('c', byteArray1), ('d', byteArray0)], equalityFunc: IterableEquality().equals), + TestCaseData(Uint8List.fromList([1, 2, 3]), + initialValues: [ + ('a', byteArray0), + ('m', null), + ('z', Uint8List.fromList([255])) + ], + equalityFunc: IterableEquality().equals), + TestCaseData(null, + initialValues: [ + ('a', byteArray0), + ('zero', byteArray1), + ('null', null), + ('one', byteArray2), + ('z', Uint8List.fromList([255])) + ], + equalityFunc: IterableEquality().equals), + ]; + +List> realmValueTestValues() => [ + TestCaseData(RealmValue.string('sampleValue'), initialValues: [ + ('nullKey', RealmValue.nullValue()), + ('intKey', RealmValue.int(10)), + ('boolKey', RealmValue.bool(true)), + ('stringKey', RealmValue.string('abc')), + ('dataKey', RealmValue.uint8List(Uint8List.fromList([0, 1, 2]))), + ('dateKey', RealmValue.dateTime(DateTime.fromMillisecondsSinceEpoch(1616137641000).toUtc())), + ('doubleKey', RealmValue.double(2.5)), + ('decimalKey', RealmValue.decimal128(Decimal128.fromDouble(5.0))), + ('objectIdKey', RealmValue.objectId(ObjectId.fromHexString('5f63e882536de46d71877979'))), + ('guidKey', RealmValue.from(Uuid.fromString('F2952191-A847-41C3-8362-497F92CB7D24'))), + ('objectKey', RealmValue.from(Car('Honda'))) + ]) + ]; + +List> _embeddedObjectTestValues() => [ + TestCaseData(null), + TestCaseData(null, + initialValues: [('123', EmbeddedValue(1))], + equalityFunc: (a, b) => a?.intValue == b?.intValue, + cloneFunc: (a) => a == null ? null : EmbeddedValue(a.intValue)), + TestCaseData(EmbeddedValue(999), + initialValues: [('123', EmbeddedValue(1))], + equalityFunc: (a, b) => a?.intValue == b?.intValue, + cloneFunc: (a) => a == null ? null : EmbeddedValue(a.intValue)), + TestCaseData(EmbeddedValue(999), + initialValues: [('null', null)], equalityFunc: (a, b) => a?.intValue == b?.intValue, cloneFunc: (a) => a == null ? null : EmbeddedValue(a.intValue)), + TestCaseData(EmbeddedValue(999), + initialValues: [('null1', null), ('null2', null)], + equalityFunc: (a, b) => a?.intValue == b?.intValue, + cloneFunc: (a) => a == null ? null : EmbeddedValue(a.intValue)), + TestCaseData(EmbeddedValue(999), + initialValues: [('a', EmbeddedValue(1)), ('null', null), ('z', EmbeddedValue(2))], + equalityFunc: (a, b) => a?.intValue == b?.intValue, + cloneFunc: (a) => a == null ? null : EmbeddedValue(a.intValue)), + ]; + +@isTest +void testUnmanaged(RealmMap Function(TestRealmMaps) accessor, TestCaseData testData) { + test('$T unmanaged: $testData', () async { + final testObject = TestRealmMaps(0); + final map = accessor(testObject); + + testData.seed(map); + + await runTestsCore(testData, map, expectManaged: false); + + expect(() => map.freeze(), throwsA(isA())); + }); +} + +@isTest +void testManaged(RealmMap Function(TestRealmMaps) accessor, TestCaseData testData) { + test('$T managed: $testData', () async { + final testObject = TestRealmMaps(0); + final map = accessor(testObject); + + testData.seed(map); + + final config = Configuration.local([TestRealmMaps.schema, Car.schema, EmbeddedValue.schema]); + final realm = getRealm(config); + + realm.write(() { + realm.add(testObject); + }); + + final managedMap = accessor(testObject); + expect(identical(map, managedMap), false); + + await runTestsCore(testData, managedMap, expectManaged: true); + + final frozen = managedMap.freeze(); + expect(frozen.isFrozen, true); + + final newKey = Uuid.v4().toString(); + realm.write(() { + managedMap[newKey] = testData.sampleValue; + }); + + expect(frozen.length, managedMap.length - 1); + expect(frozen[newKey], null); + expect(frozen.containsKey(newKey), false); + expect(() => frozen.changes, throwsA(isA())); + }); +} + +@isTest +testNotifications(RealmMap Function(TestRealmMaps) accessor, TestCaseData testData) { + test('$T notifications', () async { + final testObject = TestRealmMaps(0); + final map = accessor(testObject); + + testData.seed(map); + + final config = Configuration.local([TestRealmMaps.schema, Car.schema, EmbeddedValue.schema]); + final realm = getRealm(config); + + realm.write(() { + realm.add(testObject); + }); + + // final managedMap = accessor(testObject); + // await runManagedNotificationTests(testData, managedMap); + }); + + test('$T key notifications', () async { + // TODO: for some reason, we don't appear to be getting key notifications + final config = Configuration.local([TestRealmMaps.schema, Car.schema, EmbeddedValue.schema]); + final realm = getRealm(config); + + final map = realm.write(() { + final testObject = realm.add(TestRealmMaps(0)); + return accessor(testObject); + }); + + final keysResults = map.keys as RealmResults; + expectLater( + keysResults.changes, + emitsInOrder([ + isA>().having((ch) => ch.inserted, 'inserted', []), // always an empty event on subscription + isA>().having((ch) => ch.inserted, 'inserted', [0]), + isA>().having((ch) => ch.inserted, 'inserted', [1]), + isA>().having((ch) => ch.deleted, 'deleted', [0]), + ])); + + realm.write(() { + map['a'] = testData.sampleValue; + }); + realm.refresh(); + + realm.write(() { + map['b'] = testData.sampleValue; + }); + realm.refresh(); + + realm.write(() { + map.remove('a'); + }); + }, skip: 'Key notifications are not working'); + + test('$T value notifications', () async { + final config = Configuration.local([TestRealmMaps.schema, Car.schema, EmbeddedValue.schema]); + final realm = getRealm(config); + + final map = realm.write(() { + final testObject = realm.add(TestRealmMaps(0)); + return accessor(testObject); + }); + + final valueResults = map.values as RealmResults; + expectLater( + valueResults.changes, + emitsInOrder([ + isA>().having((ch) => ch.inserted, 'inserted', []), // always an empty event on subscription + isA>().having((ch) => ch.inserted, 'inserted', [0]), + isA>().having((ch) => ch.inserted, 'inserted', [1]), + isA>().having((ch) => ch.deleted, 'deleted', [0]), + ])); + + realm.write(() { + map['a'] = testData.sampleValue; + }); + realm.refresh(); + + realm.write(() { + map['b'] = testData.sampleValue; + }); + realm.refresh(); + + realm.write(() { + map.remove('a'); + }); + }); +} + +Future runTestsCore(TestCaseData testData, RealmMap map, {required bool expectManaged}) async { + expect(map.isManaged, expectManaged); + expect(map.isValid, true); + + testData.assertEquivalent(map); + testData.assertContainsKey(map); + testData.assertKeys(map); + testData.assertValues(map); + testData.assertEntries(map); + testData.assertAccessor(map); + testData.assertSet(map); + testData.assertRemove(map); +} + +Future runManagedNotificationTests(TestCaseData testData, RealmMap map) async { + final insertedKey = Uuid.v4().toString(); + final (keyToUpdate, _) = testData._getDifferentValue(map, testData.sampleValue); + + final changes = >[]; + final subscription = map.changes.listen((change) { + changes.add(change); + }); + + var expectedCallbacks = 0; + + Future> waitForChanges(({List inserted, List modified, List deleted}) expected) async { + expectedCallbacks++; + + map.realm.refresh(); + + await waitForCondition(() => changes.length == expectedCallbacks); + final result = changes[expectedCallbacks - 1]; + expect(result.inserted, expected.inserted); + expect(result.modified, expected.modified); + expect(result.deleted, expected.deleted); + + return result; + } + + // Initial callback + await waitForChanges((inserted: [], modified: [], deleted: [])); + + // Insert + map.realm.write(() { + map[insertedKey] = testData.sampleValue; + }); + + await waitForChanges((inserted: [insertedKey], modified: [], deleted: [])); + + // Modify + map.realm.write(() { + map[keyToUpdate] = testData.sampleValue; + }); + + await waitForChanges((inserted: [], modified: [keyToUpdate], deleted: [])); + + // Delete + map.realm.write(() { + map.remove(keyToUpdate); + }); + + await waitForChanges((inserted: [], modified: [], deleted: [keyToUpdate])); + + // Stop listening + subscription.cancel(); + + expect(changes.length, expectedCallbacks); + + map.realm.write(() { + map[Uuid.v4().toString()] = testData.sampleValue; + }); + + map.realm.refresh(); + + // We shouldn't have received a notification + expect(changes.length, expectedCallbacks); +} + +@isTest +void runTests(List> Function() testGetter, RealmMap Function(TestRealmMaps) accessor) { + group('$T test cases', () { + for (var test in testGetter()) { + testUnmanaged(accessor, test); + testManaged(accessor, test); + } + }); + + group('notifications', () { + testNotifications(accessor, testGetter().last); + + test('key notifications', () {}); + }); +} + +final List<({String key, String errorFragment})> invalidKeys = [ + (key: '.', errorFragment: "must not contain '.'"), + (key: '\$', errorFragment: "must not start with '\$'"), + (key: '\$foo', errorFragment: "must not start with '\$'"), + (key: 'foo.bar', errorFragment: "must not contain '.'"), + (key: 'foo.', errorFragment: "must not contain '.'") +]; + +Future main([List? args]) async { + await setupTests(args); + + group('key validation', () { + for (final testData in invalidKeys) { + test('Invalid key: ${testData.key}', () { + final config = Configuration.local([TestRealmMaps.schema, Car.schema, EmbeddedValue.schema]); + final realm = getRealm(config); + + realm.write(() { + final testObject = realm.add(TestRealmMaps(0)); + expect(() => testObject.stringMap[testData.key] = 'value', + throwsA(isA().having((e) => e.message, 'message', contains(testData.errorFragment)))); + + final unmanaged = TestRealmMaps(1); + unmanaged.stringMap[testData.key] = 'value'; + + expect(() => realm.add(unmanaged), throwsA(isA().having((e) => e.message, 'message', contains(testData.errorFragment)))); + }); + }); + } + + for (final key in [r'a$', r'a$$$$$$$$$', r'_$_$_']) { + test('key may contain \$: $key', () { + final config = Configuration.local([TestRealmMaps.schema, Car.schema, EmbeddedValue.schema]); + final realm = getRealm(config); + + realm.write(() { + final testObject = realm.add(TestRealmMaps(0)); + testObject.stringMap[key] = 'value'; + }); + }); + } + }); + + group('queries', () { + test('invalid predicate', () { + final config = Configuration.local([TestRealmMaps.schema, Car.schema, EmbeddedValue.schema]); + final realm = getRealm(config); + final map = realm.write(() => realm.add(TestRealmMaps(0))).objectsMap; + + expect(() => map.query('invalid predicate'), throwsA(isA().having((e) => e.message, 'message', contains('Invalid predicate')))); + }); + + test('invalid number of arguments', () { + final config = Configuration.local([TestRealmMaps.schema, Car.schema, EmbeddedValue.schema]); + final realm = getRealm(config); + final map = realm.write(() => realm.add(TestRealmMaps(0))).objectsMap; + + expect(() => map.query(r'make = $0'), + throwsA(isA().having((e) => e.message, 'message', contains('Request for argument at index 0 but no arguments are provided')))); + }); + + test('unmanaged dictionary throws', () { + final map = TestRealmMaps(0).objectsMap; + expect(() => map.query('query'), throwsA(isA())); + }); + + test('can be filtered', () { + final config = Configuration.local([TestRealmMaps.schema, Car.schema, EmbeddedValue.schema]); + final realm = getRealm(config); + final map = realm.write(() => realm.add(TestRealmMaps(0))).objectsMap; + + final filtered = map.query(r'make BEGINSWITH $0', ['A']); + realm.write(() { + map['a'] = Car('Acura'); + map['b'] = Car('BMW'); + map['c'] = Car('Astra'); + }); + + expect(filtered.length, 2); + expect(filtered.firstWhereOrNull((element) => element.make == 'BMW'), isNull); + }); + + test('can be sorted', () { + final config = Configuration.local([TestRealmMaps.schema, Car.schema, EmbeddedValue.schema]); + final realm = getRealm(config); + final map = realm.write(() => realm.add(TestRealmMaps(0))).objectsMap; + + final filtered = map.query(r'make BEGINSWITH[c] $0 SORT(make desc)', ['A']); + realm.write(() { + map['a'] = Car('aaaa'); + map['b'] = Car('azzz'); + map['c'] = Car('abbb'); + }); + + expect(filtered.length, 3); + expect(filtered.map((e) => e.make), ['azzz', 'abbb', 'aaaa']); + }); + + test('raises notifications', () { + final config = Configuration.local([TestRealmMaps.schema, Car.schema, EmbeddedValue.schema]); + final realm = getRealm(config); + final map = realm.write(() => realm.add(TestRealmMaps(0))).objectsMap; + + expectLater( + map.query('TRUEPREDICATE SORT(make asc)').changes, + emitsInOrder([ + isA>().having((ch) => ch.inserted, 'inserted', []), // always an empty event on subscription + isA>().having((ch) => ch.inserted, 'inserted', [0]), + isA>().having((ch) => ch.modified, 'modified', [0]), + isA>().having((ch) => ch.inserted, 'inserted', [1]), + isA>().having((ch) => ch.deleted, 'deleted', [0]), + ])); + + realm.write(() { + map['a'] = Car('aaaa'); + }); + realm.refresh(); + + realm.write(() { + map['a']!.color = 'some color'; + }); + realm.refresh(); + + realm.write(() { + map['b'] = Car('bbbb'); + }); + realm.refresh(); + + realm.write(() { + map.remove('a'); + }); + realm.refresh(); + }); + }); + + runTests(boolTestValues, (e) => e.boolMap); + runTests(nullableBoolTestValues, (e) => e.nullableBoolMap); + + runTests(intTestCases, (e) => e.intMap); + runTests(nullableIntTestCases, (e) => e.nullableIntMap); + + runTests(stringTestValues, (e) => e.stringMap); + runTests(nullableStringTestValues, (e) => e.nullableStringMap); + + runTests(doubleTestValues, (e) => e.doubleMap); + runTests(nullableDoubleTestValues, (e) => e.nullableDoubleMap); + + runTests(decimal128TestValues, (e) => e.decimalMap); + runTests(nullableDecimal128TestValues, (e) => e.nullableDecimalMap); + + runTests(dateTimeTestValues, (e) => e.dateTimeMap); + runTests(nullableDateTimeTestValues, (e) => e.nullableDateTimeMap); + + runTests(objectIdTestValues, (e) => e.objectIdMap); + runTests(nullableObjectIdTestValues, (e) => e.nullableObjectIdMap); + + runTests(uuidTestValues, (e) => e.uuidMap); + runTests(nullableUuidTestValues, (e) => e.nullableUuidMap); + + runTests(byteArrayTestValues, (e) => e.binaryMap); + runTests(nullableByteArrayTestValues, (e) => e.nullableBinaryMap); + + runTests(realmValueTestValues, (e) => e.mixedMap); + + runTests(_embeddedObjectTestValues, (e) => e.embeddedMap); +} diff --git a/test/realm_map_test.g.dart b/test/realm_map_test.g.dart new file mode 100644 index 000000000..3acadf1f7 --- /dev/null +++ b/test/realm_map_test.g.dart @@ -0,0 +1,375 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'realm_map_test.dart'; + +// ************************************************************************** +// RealmObjectGenerator +// ************************************************************************** + +// ignore_for_file: type=lint +class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { + Car( + String make, { + String? color, + }) { + RealmObjectBase.set(this, 'make', make); + RealmObjectBase.set(this, 'color', color); + } + + Car._(); + + @override + String get make => RealmObjectBase.get(this, 'make') as String; + @override + set make(String value) => RealmObjectBase.set(this, 'make', value); + + @override + String? get color => RealmObjectBase.get(this, 'color') as String?; + @override + set color(String? value) => RealmObjectBase.set(this, 'color', value); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + Car freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(Car._); + return const SchemaObject(ObjectType.realmObject, Car, 'Car', [ + SchemaProperty('make', RealmPropertyType.string, primaryKey: true), + SchemaProperty('color', RealmPropertyType.string, optional: true), + ]); + } +} + +class EmbeddedValue extends _EmbeddedValue + with RealmEntity, RealmObjectBase, EmbeddedObject { + EmbeddedValue( + int intValue, + ) { + RealmObjectBase.set(this, 'intValue', intValue); + } + + EmbeddedValue._(); + + @override + int get intValue => RealmObjectBase.get(this, 'intValue') as int; + @override + set intValue(int value) => RealmObjectBase.set(this, 'intValue', value); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + EmbeddedValue freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(EmbeddedValue._); + return const SchemaObject( + ObjectType.embeddedObject, EmbeddedValue, 'EmbeddedValue', [ + SchemaProperty('intValue', RealmPropertyType.int), + ]); + } +} + +class TestRealmMaps extends _TestRealmMaps + with RealmEntity, RealmObjectBase, RealmObject { + TestRealmMaps( + int key, { + Map boolMap = const {}, + Map intMap = const {}, + Map stringMap = const {}, + Map doubleMap = const {}, + Map dateTimeMap = const {}, + Map objectIdMap = const {}, + Map uuidMap = const {}, + Map binaryMap = const {}, + Map decimalMap = const {}, + Map nullableBoolMap = const {}, + Map nullableIntMap = const {}, + Map nullableStringMap = const {}, + Map nullableDoubleMap = const {}, + Map nullableDateTimeMap = const {}, + Map nullableObjectIdMap = const {}, + Map nullableUuidMap = const {}, + Map nullableBinaryMap = const {}, + Map nullableDecimalMap = const {}, + Map objectsMap = const {}, + Map embeddedMap = const {}, + Map mixedMap = const {}, + }) { + RealmObjectBase.set(this, 'key', key); + RealmObjectBase.set>( + this, 'boolMap', RealmMap(boolMap)); + RealmObjectBase.set>(this, 'intMap', RealmMap(intMap)); + RealmObjectBase.set>( + this, 'stringMap', RealmMap(stringMap)); + RealmObjectBase.set>( + this, 'doubleMap', RealmMap(doubleMap)); + RealmObjectBase.set>( + this, 'dateTimeMap', RealmMap(dateTimeMap)); + RealmObjectBase.set>( + this, 'objectIdMap', RealmMap(objectIdMap)); + RealmObjectBase.set>( + this, 'uuidMap', RealmMap(uuidMap)); + RealmObjectBase.set>( + this, 'binaryMap', RealmMap(binaryMap)); + RealmObjectBase.set>( + this, 'decimalMap', RealmMap(decimalMap)); + RealmObjectBase.set>( + this, 'nullableBoolMap', RealmMap(nullableBoolMap)); + RealmObjectBase.set>( + this, 'nullableIntMap', RealmMap(nullableIntMap)); + RealmObjectBase.set>( + this, 'nullableStringMap', RealmMap(nullableStringMap)); + RealmObjectBase.set>( + this, 'nullableDoubleMap', RealmMap(nullableDoubleMap)); + RealmObjectBase.set>( + this, 'nullableDateTimeMap', RealmMap(nullableDateTimeMap)); + RealmObjectBase.set>( + this, 'nullableObjectIdMap', RealmMap(nullableObjectIdMap)); + RealmObjectBase.set>( + this, 'nullableUuidMap', RealmMap(nullableUuidMap)); + RealmObjectBase.set>( + this, 'nullableBinaryMap', RealmMap(nullableBinaryMap)); + RealmObjectBase.set>( + this, 'nullableDecimalMap', RealmMap(nullableDecimalMap)); + RealmObjectBase.set>( + this, 'objectsMap', RealmMap(objectsMap)); + RealmObjectBase.set>( + this, 'embeddedMap', RealmMap(embeddedMap)); + RealmObjectBase.set>( + this, 'mixedMap', RealmMap(mixedMap)); + } + + TestRealmMaps._(); + + @override + int get key => RealmObjectBase.get(this, 'key') as int; + @override + set key(int value) => RealmObjectBase.set(this, 'key', value); + + @override + RealmMap get boolMap => + RealmObjectBase.get(this, 'boolMap') as RealmMap; + @override + set boolMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get intMap => + RealmObjectBase.get(this, 'intMap') as RealmMap; + @override + set intMap(covariant RealmMap value) => throw RealmUnsupportedSetError(); + + @override + RealmMap get stringMap => + RealmObjectBase.get(this, 'stringMap') as RealmMap; + @override + set stringMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get doubleMap => + RealmObjectBase.get(this, 'doubleMap') as RealmMap; + @override + set doubleMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get dateTimeMap => + RealmObjectBase.get(this, 'dateTimeMap') as RealmMap; + @override + set dateTimeMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get objectIdMap => + RealmObjectBase.get(this, 'objectIdMap') as RealmMap; + @override + set objectIdMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get uuidMap => + RealmObjectBase.get(this, 'uuidMap') as RealmMap; + @override + set uuidMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get binaryMap => + RealmObjectBase.get(this, 'binaryMap') as RealmMap; + @override + set binaryMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get decimalMap => + RealmObjectBase.get(this, 'decimalMap') + as RealmMap; + @override + set decimalMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get nullableBoolMap => + RealmObjectBase.get(this, 'nullableBoolMap') as RealmMap; + @override + set nullableBoolMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get nullableIntMap => + RealmObjectBase.get(this, 'nullableIntMap') as RealmMap; + @override + set nullableIntMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get nullableStringMap => + RealmObjectBase.get(this, 'nullableStringMap') + as RealmMap; + @override + set nullableStringMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get nullableDoubleMap => + RealmObjectBase.get(this, 'nullableDoubleMap') + as RealmMap; + @override + set nullableDoubleMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get nullableDateTimeMap => + RealmObjectBase.get(this, 'nullableDateTimeMap') + as RealmMap; + @override + set nullableDateTimeMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get nullableObjectIdMap => + RealmObjectBase.get(this, 'nullableObjectIdMap') + as RealmMap; + @override + set nullableObjectIdMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get nullableUuidMap => + RealmObjectBase.get(this, 'nullableUuidMap') as RealmMap; + @override + set nullableUuidMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get nullableBinaryMap => + RealmObjectBase.get(this, 'nullableBinaryMap') + as RealmMap; + @override + set nullableBinaryMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get nullableDecimalMap => + RealmObjectBase.get(this, 'nullableDecimalMap') + as RealmMap; + @override + set nullableDecimalMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get objectsMap => + RealmObjectBase.get(this, 'objectsMap') as RealmMap; + @override + set objectsMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get embeddedMap => + RealmObjectBase.get(this, 'embeddedMap') + as RealmMap; + @override + set embeddedMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get mixedMap => + RealmObjectBase.get(this, 'mixedMap') as RealmMap; + @override + set mixedMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + TestRealmMaps freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(TestRealmMaps._); + return const SchemaObject( + ObjectType.realmObject, TestRealmMaps, 'TestRealmMaps', [ + SchemaProperty('key', RealmPropertyType.int, primaryKey: true), + SchemaProperty('boolMap', RealmPropertyType.bool, + collectionType: RealmCollectionType.map), + SchemaProperty('intMap', RealmPropertyType.int, + collectionType: RealmCollectionType.map), + SchemaProperty('stringMap', RealmPropertyType.string, + collectionType: RealmCollectionType.map), + SchemaProperty('doubleMap', RealmPropertyType.double, + collectionType: RealmCollectionType.map), + SchemaProperty('dateTimeMap', RealmPropertyType.timestamp, + collectionType: RealmCollectionType.map), + SchemaProperty('objectIdMap', RealmPropertyType.objectid, + collectionType: RealmCollectionType.map), + SchemaProperty('uuidMap', RealmPropertyType.uuid, + collectionType: RealmCollectionType.map), + SchemaProperty('binaryMap', RealmPropertyType.binary, + collectionType: RealmCollectionType.map), + SchemaProperty('decimalMap', RealmPropertyType.decimal128, + collectionType: RealmCollectionType.map), + SchemaProperty('nullableBoolMap', RealmPropertyType.bool, + optional: true, collectionType: RealmCollectionType.map), + SchemaProperty('nullableIntMap', RealmPropertyType.int, + optional: true, collectionType: RealmCollectionType.map), + SchemaProperty('nullableStringMap', RealmPropertyType.string, + optional: true, collectionType: RealmCollectionType.map), + SchemaProperty('nullableDoubleMap', RealmPropertyType.double, + optional: true, collectionType: RealmCollectionType.map), + SchemaProperty('nullableDateTimeMap', RealmPropertyType.timestamp, + optional: true, collectionType: RealmCollectionType.map), + SchemaProperty('nullableObjectIdMap', RealmPropertyType.objectid, + optional: true, collectionType: RealmCollectionType.map), + SchemaProperty('nullableUuidMap', RealmPropertyType.uuid, + optional: true, collectionType: RealmCollectionType.map), + SchemaProperty('nullableBinaryMap', RealmPropertyType.binary, + optional: true, collectionType: RealmCollectionType.map), + SchemaProperty('nullableDecimalMap', RealmPropertyType.decimal128, + optional: true, collectionType: RealmCollectionType.map), + SchemaProperty('objectsMap', RealmPropertyType.object, + optional: true, + linkTarget: 'Car', + collectionType: RealmCollectionType.map), + SchemaProperty('embeddedMap', RealmPropertyType.object, + optional: true, + linkTarget: 'EmbeddedValue', + collectionType: RealmCollectionType.map), + SchemaProperty('mixedMap', RealmPropertyType.mixed, + optional: true, collectionType: RealmCollectionType.map), + ]); + } +} diff --git a/test/realm_value_test.dart b/test/realm_value_test.dart index 0f34ebdcc..4d7fba048 100644 --- a/test/realm_value_test.dart +++ b/test/realm_value_test.dart @@ -69,11 +69,7 @@ Future main([List? args]) async { final something = realm.write(() => realm.add(AnythingGoes(oneAny: RealmValue.from(x)))); expect(something.oneAny.type, x.runtimeType); expect(something.oneAny.value, x); - if (x is Uint8List) { - expect(something.oneAny, isNot(RealmValue.from(x))); - } else { - expect(something.oneAny, RealmValue.from(x)); - } + expect(something.oneAny, RealmValue.from(x)); }); } diff --git a/test/results_test.dart b/test/results_test.dart index 9a1115b28..fffe93a73 100644 --- a/test/results_test.dart +++ b/test/results_test.dart @@ -132,7 +132,7 @@ Future main([List? args]) async { final cars = realm.all(); - expect(() => cars[0], throws("Requested index 0 calling get() on Results when empty")); + expect(() => cars[0], throws()); }); test('Results iteration test', () { @@ -961,11 +961,11 @@ Future main([List? args]) async { final rit = results.iterator; // you are not supposed to call current before first moveNext - expect(() => rit.current, throwsA(isA())); + expect(() => rit.current, throwsA(isA())); expect(rit.moveNext(), isTrue); expect(rit.moveNext(), isFalse); // you are not supposed to call current, if moveNext return false - expect(() => rit.current, throwsA(isA())); + expect(() => rit.current, throwsA(isA())); }); test('RealmResults.indexOf', () { diff --git a/test/test.dart b/test/test.dart index 91e725235..f48ee9e56 100644 --- a/test/test.dart +++ b/test/test.dart @@ -247,7 +247,7 @@ class _Player { @RealmModel() class _Game { - final winnerByRound = <_Player>[]; // null means no winner yet + final winnerByRound = <_Player>[]; int get rounds => winnerByRound.length; } From 466bc9be1623f439ec6665cadfdd4b934cbc4488 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Wed, 3 Jan 2024 18:16:37 +0100 Subject: [PATCH 27/27] Merge main, regenerate cli --- lib/src/cli/atlas_apps/options.g.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/cli/atlas_apps/options.g.dart b/lib/src/cli/atlas_apps/options.g.dart index 2b90d9b07..a6cd50993 100644 --- a/lib/src/cli/atlas_apps/options.g.dart +++ b/lib/src/cli/atlas_apps/options.g.dart @@ -47,7 +47,7 @@ ArgParser _$populateOptionsParser(ArgParser parser) => parser ..addOption( 'baasaas-api-key', help: - 'API key to use with BaaSaaS to wpawn a new container and create apps in it.', + 'API key to use with BaaSaaS to spawn a new container and create apps in it.', ); final _$parserForOptions = _$populateOptionsParser(ArgParser());