From 52ad45b1b589c9a1a70981ae6a564f0e1e8290d0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 2 May 2024 14:48:56 +0200 Subject: [PATCH] [Automated] Merge releases into main (#1729) --- .github/workflows/include-deploy-release.yml | 2 +- .../workflows/include-integration-tests.yml | 8 +- .github/workflows/pr.yml | 32 +++---- CHANGELOG.md | 18 +++- buildSrc/src/main/kotlin/Config.kt | 2 +- .../kotlin/internal/interop/ErrorCode.kt | 2 +- .../kotlin/internal/interop/RealmInterop.kt | 22 +++-- .../kotlin/internal/interop/ErrorCode.kt | 2 +- .../kotlin/internal/interop/RealmInterop.kt | 37 +++++--- packages/cinterop/src/native/realm.def | 1 + .../kotlin/internal/interop/ErrorCode.kt | 2 +- .../kotlin/internal/interop/RealmInterop.kt | 66 ++++++++----- packages/external/core | 2 +- packages/jni-swig-stub/realm.i | 4 + .../kotlin/io/realm/kotlin/mongodb/App.kt | 18 ++++ .../annotations/ExperimentalEdgeServerApi.kt | 35 +++++++ .../mongodb/internal/AppConfigurationImpl.kt | 26 ++--- .../realm/kotlin/mongodb/internal/AppImpl.kt | 19 ++++ .../kotlin/mongodb/internal/RealmSyncUtils.kt | 7 +- packages/test-base/build.gradle.kts | 2 +- packages/test-sync/build.gradle.kts | 2 +- .../kotlin/test/mongodb/common/AppTests.kt | 94 +++++++++++++++++++ .../mongodb/common/ProgressListenerTests.kt | 5 + .../kotlin/test/mongodb/common/UserTests.kt | 2 +- 24 files changed, 315 insertions(+), 95 deletions(-) create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/annotations/ExperimentalEdgeServerApi.kt diff --git a/.github/workflows/include-deploy-release.yml b/.github/workflows/include-deploy-release.yml index 7141f919f1..4dc1d2d163 100644 --- a/.github/workflows/include-deploy-release.yml +++ b/.github/workflows/include-deploy-release.yml @@ -13,7 +13,7 @@ on: jobs: deploy: - runs-on: macos-latest + runs-on: macos-12 name: Deploy release steps: diff --git a/.github/workflows/include-integration-tests.yml b/.github/workflows/include-integration-tests.yml index e8b35c5133..e39b2613e0 100644 --- a/.github/workflows/include-integration-tests.yml +++ b/.github/workflows/include-integration-tests.yml @@ -16,7 +16,7 @@ jobs: # TODO: The Monkey seems to crash the app all the time, but with failures that are not coming from the app. Figure out why. # android-sample-app: - # runs-on: macos-latest + # runs-on: macos-12 # steps: # - name: Checkout code # uses: actions/checkout@v3 @@ -87,7 +87,7 @@ jobs: ./gradlew assembleDebug jvmJar realm-java-compatibiliy: - runs-on: macos-latest + runs-on: macos-12 steps: - name: Checkout code uses: actions/checkout@v3 @@ -217,7 +217,7 @@ jobs: - type: gradle75 path: integration-tests/gradle/gradle75-test arguments: integrationTest - runs-on: macos-latest + runs-on: macos-12 steps: - uses: actions/checkout@v3 @@ -295,7 +295,7 @@ jobs: - type: gradle8 path: integration-tests/gradle/gradle8-test arguments: integrationTest - runs-on: macos-latest + runs-on: macos-12 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 9d463ceb66..3f94add458 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -229,7 +229,7 @@ jobs: retention-days: 1 build-jvm-macos-native-lib: - runs-on: macos-latest + runs-on: macos-12 needs: [check-cache, build-jni-swig-stub] if: | always() && @@ -395,7 +395,7 @@ jobs: # This task is also responsible for creating the Gradle and Compiler Plugin as well as # all Kotlin Multiplatform Metadata build-jvm-packages: - runs-on: macos-latest + runs-on: macos-12 needs: [check-cache, build-jvm-linux-native-lib, build-jvm-windows-native-lib, build-jvm-macos-native-lib] if: | always() && @@ -638,7 +638,7 @@ jobs: # TODO: ccache is not being used by this build for some reason build-macos-x64-packages: - runs-on: macos-latest + runs-on: macos-12 needs: check-cache if: always() && !cancelled() && needs.check-cache.outputs.packages-macos-x64-cache-hit != 'true' @@ -706,7 +706,7 @@ jobs: retention-days: 1 build-macos-arm64-packages: - runs-on: macos-latest + runs-on: macos-12 needs: check-cache # needs: static-analysis if: always() && !cancelled() && needs.check-cache.outputs.packages-macos-arm64-cache-hit != 'true' @@ -774,7 +774,7 @@ jobs: retention-days: 1 build-ios-x64-packages: - runs-on: macos-latest + runs-on: macos-12 needs: check-cache # needs: static-analysis if: always() && !cancelled() && needs.check-cache.outputs.packages-ios-x64-cache-hit != 'true' @@ -843,7 +843,7 @@ jobs: retention-days: 1 build-ios-arm64-packages: - runs-on: macos-latest + runs-on: macos-12 needs: check-cache # needs: static-analysis if: always() && !cancelled() && needs.check-cache.outputs.packages-ios-arm64-cache-hit != 'true' @@ -931,7 +931,7 @@ jobs: - type: sync test-title: Unit Test Results - Android Sync (Emulator) - runs-on: macos-latest + runs-on: macos-12 needs: [check-cache, build-android-packages, build-jvm-packages, build-kotlin-metadata-package] if: | always() && @@ -1175,15 +1175,15 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-latest] # , macos-arm] + os: [macos-12] # , macos-arm] type: [base, sync] include: - - os: macos-latest + - os: macos-12 type: base os-id: macos package-prefix: macos-x64 test-title: Unit Test Results - MacOS x64 Base - - os: macos-latest + - os: macos-12 type: sync os-id: macos package-prefix: macos-x64 @@ -1297,15 +1297,15 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-latest] # , macos-arm] + os: [macos-12] # , macos-arm] type: [base, sync] include: - - os: macos-latest + - os: macos-12 type: base package-prefix: x64 test-title: Unit Test Results - iOS x64 Base test-task: iosTest - - os: macos-latest + - os: macos-12 type: sync package-prefix: x64 test-title: Unit Test Results - iOS x64 Sync @@ -1419,10 +1419,10 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-latest, ubuntu-latest, windows-latest] # TODO Should we also test om MacOS arm64? + os: [macos-12, ubuntu-latest, windows-latest] # TODO Should we also test om MacOS arm64? type: [base, sync] include: - - os: macos-latest + - os: macos-12 os-id: mac type: base test-title: Unit Test Results - Base JVM MacOS x64 @@ -1434,7 +1434,7 @@ jobs: os-id: win type: base test-title: Unit Test Results - Base JVM Windows - - os: macos-latest + - os: macos-12 os-id: mac type: sync test-title: Unit Test Results - Sync JVM MacOS x64 diff --git a/CHANGELOG.md b/CHANGELOG.md index c5df543c45..4bd75a5b91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 1.16.0-SNAPSHOT (YYYY-MM-DD) +## 1.17.0-SNAPSHOT (YYYY-MM-DD) [!NOTE] This release will bump the Realm file format from version 23 to 24. Opening a file with an older format will automatically upgrade it from file format v10. If you want to upgrade from an earlier file format version you will have to use Realm Kotlin v1.13.1 or earlier. Downgrading to a previous file format is not possible. @@ -31,7 +31,7 @@ This release will bump the Realm file format from version 23 to 24. Opening a fi ### Internal * None. -## 1.15.1-SNAPSHOT (YYYY-MM-DD) +## 1.16.0 (2024-05-01) [!NOTE] This release will bump the Realm file format from version 23 to 24. Opening a file with an older format will automatically upgrade it from file format v10. If you want to upgrade from an earlier file format version you will have to use Realm Kotlin v1.13.1 or earlier. Downgrading to a previous file format is not possible. @@ -40,10 +40,18 @@ This release will bump the Realm file format from version 23 to 24. Opening a fi * None. ### Enhancements -* None. +* Add support for changing the App Services base URL. It allows to roam between Atlas and Edge Server. Changing the url would trigger a client reset. (Issue [#1659](https://github.com/realm/realm-kotlin/issues/1659)/[RKOTLIN-1013](https://jira.mongodb.org/browse/RKOTLIN-1023)) ### Fixed -* None. +* Fixed a bug when running a IN query (or a query of the pattern `x == 1 OR x == 2 OR x == 3`) when evaluating on a string property with an empty string in the search condition. Matches with an empty string would have been evaluated as if searching for a null string instead. (Core issue [realm/realm-core#7628](https://github.com/realm/realm-core/pull/7628) since Core v10.0.0-beta.9) +* Fixed several issues around encrypted file portability (copying a "bundled" encrypted Realm from one device to another). (Core issues [realm/realm-core#7322](https://github.com/realm/realm-core/issues/7322) and [realm/realm-core#7319](https://github.com/realm/realm-core/issues/7319)) +* Queries using query paths on Mixed values returns inconsistent results (Core issue [realm/realm-core#7587](https://github.com/realm/realm-core/issues/7587), since Core v14.0.0) +* [Sync] `App.allUsers()` included logged out users only if they were logged out while the App instance existed. It now always includes all logged out users. (Core issue [realm/realm-core#7300](https://github.com/realm/realm-core/pull/7300)) +* [Sync] Deleting the active user left the active user unset rather than selecting another logged-in user as the active user like logging out and removing users did. (Core issue [realm/realm-core#7300](https://github.com/realm/realm-core/pull/7300)) +* [Sync] Schema initialization could hit an assertion failure if the sync client applied a downloaded changeset while the Realm file was in the process of being opened (Core issue [realm/realm-core#7041](https://github.com/realm/realm-core/issues/7041), since Core v11.4.0). + +### Known issues +* Missing initial download progress notification when there is no active downloads. (Issue [realm/realm-core#7627](https://github.com/realm/realm-core/issues/7627), since 1.15.1) ### Compatibility * File format: Generates Realms with file format v24 (reads and upgrades file format v10 or later). @@ -61,7 +69,7 @@ This release will bump the Realm file format from version 23 to 24. Opening a fi * Minimum R8: 8.0.34. ### Internal -* None. +* Updated to Realm Core 14.6.1 commit cde3adb7649d3361806dbbae0cf353b8fdc4d54e. ## 1.15.0 (2024-04-17) diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 718ff10f91..819ab881c4 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -62,7 +62,7 @@ val HOST_OS: OperatingSystem = findHostOs() object Realm { val ciBuild = (System.getenv("CI") != null) - const val version = "1.16.0-SNAPSHOT" + const val version = "1.17.0-SNAPSHOT" const val group = "io.realm.kotlin" const val projectUrl = "https://realm.io" const val pluginPortalId = "io.realm.kotlin" diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt index cdc7e59854..d7aaa63eda 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt @@ -119,9 +119,9 @@ expect enum class ErrorCode : CodeDescription { RLM_ERR_CUSTOM_ERROR, RLM_ERR_CLIENT_USER_NOT_FOUND, RLM_ERR_CLIENT_USER_NOT_LOGGED_IN, - RLM_ERR_CLIENT_APP_DEALLOCATED, RLM_ERR_CLIENT_REDIRECT_ERROR, RLM_ERR_CLIENT_TOO_MANY_REDIRECTS, + RLM_ERR_CLIENT_USER_ALREADY_NAMED, RLM_ERR_BAD_TOKEN, RLM_ERR_MALFORMED_JSON, RLM_ERR_MISSING_JSON_KEY, diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 7dce1dcef0..1602477b97 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -575,6 +575,16 @@ expect object RealmInterop { callback: AppCallback>, ) + fun realm_app_get_base_url( + app: RealmAppPointer, + ): String + + fun realm_app_update_base_url( + app: RealmAppPointer, + baseUrl: String?, + callback: AppCallback, + ) + // User fun realm_user_get_all_identities(user: RealmUserPointer): List fun realm_user_get_identity(user: RealmUserPointer): String @@ -596,8 +606,8 @@ expect object RealmInterop { appId: String ) - fun realm_sync_client_config_set_base_file_path( - syncClientConfig: RealmSyncClientConfigurationPointer, + fun realm_app_config_set_base_file_path( + appConfig: RealmAppConfigurationPointer, basePath: String ) @@ -607,13 +617,13 @@ expect object RealmInterop { fun realm_set_log_level(level: CoreLogLevel) - fun realm_sync_client_config_set_metadata_mode( - syncClientConfig: RealmSyncClientConfigurationPointer, + fun realm_app_config_set_metadata_mode( + appConfig: RealmAppConfigurationPointer, metadataMode: MetadataMode ) - fun realm_sync_client_config_set_metadata_encryption_key( - syncClientConfig: RealmSyncClientConfigurationPointer, + fun realm_app_config_set_metadata_encryption_key( + appConfig: RealmAppConfigurationPointer, encryptionKey: ByteArray ) fun realm_sync_client_config_set_user_agent_binding_info( diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt index 50676929df..cc5ae9efe1 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt @@ -116,9 +116,9 @@ actual enum class ErrorCode(actual override val description: String?, actual ove RLM_ERR_CUSTOM_ERROR("CustomError", realm_errno_e.RLM_ERR_CUSTOM_ERROR), RLM_ERR_CLIENT_USER_NOT_FOUND("ClientUserNotFound", realm_errno_e.RLM_ERR_CLIENT_USER_NOT_FOUND), RLM_ERR_CLIENT_USER_NOT_LOGGED_IN("ClientUserNotLoggedIn", realm_errno_e.RLM_ERR_CLIENT_USER_NOT_LOGGED_IN), - RLM_ERR_CLIENT_APP_DEALLOCATED("ClientAppDeallocated", realm_errno_e.RLM_ERR_CLIENT_APP_DEALLOCATED), RLM_ERR_CLIENT_REDIRECT_ERROR("ClientRedirectError", realm_errno_e.RLM_ERR_CLIENT_REDIRECT_ERROR), RLM_ERR_CLIENT_TOO_MANY_REDIRECTS("ClientTooManyRedirects", realm_errno_e.RLM_ERR_CLIENT_TOO_MANY_REDIRECTS), + RLM_ERR_CLIENT_USER_ALREADY_NAMED("ClientUserAlreadyNamed", realm_errno_e.RLM_ERR_CLIENT_USER_ALREADY_NAMED), RLM_ERR_BAD_TOKEN("BadToken", realm_errno_e.RLM_ERR_BAD_TOKEN), RLM_ERR_MALFORMED_JSON("MalformedJson", realm_errno_e.RLM_ERR_MALFORMED_JSON), RLM_ERR_MISSING_JSON_KEY("MissingJsonKey", realm_errno_e.RLM_ERR_MISSING_JSON_KEY), diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 613ac82036..8dcecde28e 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -17,7 +17,6 @@ package io.realm.kotlin.internal.interop import io.realm.kotlin.internal.interop.Constants.ENCRYPTION_KEY_LENGTH -import io.realm.kotlin.internal.interop.RealmInterop.cptr import io.realm.kotlin.internal.interop.sync.ApiKeyWrapper import io.realm.kotlin.internal.interop.sync.AuthProvider import io.realm.kotlin.internal.interop.sync.CoreConnectionState @@ -1158,7 +1157,7 @@ actual object RealmInterop { syncClientConfig: RealmSyncClientConfigurationPointer, basePath: String ): RealmAppPointer { - return LongPointerWrapper(realmc.realm_app_create(appConfig.cptr(), syncClientConfig.cptr()), managed = true) + return LongPointerWrapper(realmc.realm_app_create(appConfig.cptr()), managed = true) } actual fun realm_app_log_in_with_credentials( @@ -1229,6 +1228,18 @@ actual object RealmInterop { return result } + actual fun realm_app_get_base_url( + app: RealmAppPointer, + ): String = realmc.realm_app_get_base_url(app.cptr()) + + actual fun realm_app_update_base_url( + app: RealmAppPointer, + baseUrl: String?, + callback: AppCallback, + ) { + realmc.realm_app_update_base_url(app.cptr(), baseUrl, callback) + } + actual fun realm_user_get_all_identities(user: RealmUserPointer): List { val count = AuthProvider.values().size.toLong() // Optimistically allocate the max size of the array val keys = realmc.new_identityArray(count.toInt()) @@ -1335,11 +1346,11 @@ actual object RealmInterop { ) } - actual fun realm_sync_client_config_set_base_file_path( - syncClientConfig: RealmSyncClientConfigurationPointer, + actual fun realm_app_config_set_base_file_path( + appConfig: RealmAppConfigurationPointer, basePath: String ) { - realmc.realm_sync_client_config_set_base_file_path(syncClientConfig.cptr(), basePath) + realmc.realm_app_config_set_base_file_path(appConfig.cptr(), basePath) } actual fun realm_sync_client_config_set_multiplex_sessions(syncClientConfig: RealmSyncClientConfigurationPointer, enabled: Boolean) { @@ -1354,22 +1365,22 @@ actual object RealmInterop { realmc.realm_set_log_level(level.priority) } - actual fun realm_sync_client_config_set_metadata_mode( - syncClientConfig: RealmSyncClientConfigurationPointer, + actual fun realm_app_config_set_metadata_mode( + appConfig: RealmAppConfigurationPointer, metadataMode: MetadataMode ) { - realmc.realm_sync_client_config_set_metadata_mode( - syncClientConfig.cptr(), + realmc.realm_app_config_set_metadata_mode( + appConfig.cptr(), metadataMode.nativeValue ) } - actual fun realm_sync_client_config_set_metadata_encryption_key( - syncClientConfig: RealmSyncClientConfigurationPointer, + actual fun realm_app_config_set_metadata_encryption_key( + appConfig: RealmAppConfigurationPointer, encryptionKey: ByteArray ) { - realmc.realm_sync_client_config_set_metadata_encryption_key( - syncClientConfig.cptr(), + realmc.realm_app_config_set_metadata_encryption_key( + appConfig.cptr(), encryptionKey ) } diff --git a/packages/cinterop/src/native/realm.def b/packages/cinterop/src/native/realm.def index 01f59930d9..925f075c04 100644 --- a/packages/cinterop/src/native/realm.def +++ b/packages/cinterop/src/native/realm.def @@ -1,5 +1,6 @@ headers = realm.h realm/error_codes.h headerFilter = realm.h realm/error_codes.h +compilerOpts = -DREALM_APP_SERVICES=1 // Relative paths in def file depends are resolved differently dependent on execution // location // https://youtrack.jetbrains.com/issue/KT-43439 diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt index bd7f60e0c6..9c790f1cfd 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt @@ -120,9 +120,9 @@ actual enum class ErrorCode( RLM_ERR_CUSTOM_ERROR("CustomError", realm_errno.RLM_ERR_CUSTOM_ERROR), RLM_ERR_CLIENT_USER_NOT_FOUND("ClientUserNotFound", realm_errno.RLM_ERR_CLIENT_USER_NOT_FOUND), RLM_ERR_CLIENT_USER_NOT_LOGGED_IN("ClientUserNotLoggedIn", realm_errno.RLM_ERR_CLIENT_USER_NOT_LOGGED_IN), - RLM_ERR_CLIENT_APP_DEALLOCATED("ClientAppDeallocated", realm_errno.RLM_ERR_CLIENT_APP_DEALLOCATED), RLM_ERR_CLIENT_REDIRECT_ERROR("ClientRedirectError", realm_errno.RLM_ERR_CLIENT_REDIRECT_ERROR), RLM_ERR_CLIENT_TOO_MANY_REDIRECTS("ClientTooManyRedirects", realm_errno.RLM_ERR_CLIENT_TOO_MANY_REDIRECTS), + RLM_ERR_CLIENT_USER_ALREADY_NAMED("ClientUserAlreadyNamed", realm_errno.RLM_ERR_CLIENT_USER_ALREADY_NAMED), RLM_ERR_BAD_TOKEN("BadToken", realm_errno.RLM_ERR_BAD_TOKEN), RLM_ERR_MALFORMED_JSON("MalformedJson", realm_errno.RLM_ERR_MALFORMED_JSON), RLM_ERR_MISSING_JSON_KEY("MissingJsonKey", realm_errno.RLM_ERR_MISSING_JSON_KEY), diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 69ba346934..805a713265 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -97,7 +97,6 @@ import platform.posix.strerror import platform.posix.uint64_t import platform.posix.uint8_tVar import realm_wrapper.realm_app_error_t -import realm_wrapper.realm_app_user_apikey_t import realm_wrapper.realm_binary_t import realm_wrapper.realm_class_info_t import realm_wrapper.realm_class_key_tVar @@ -123,7 +122,6 @@ import realm_wrapper.realm_results_t import realm_wrapper.realm_scheduler_t import realm_wrapper.realm_set_t import realm_wrapper.realm_string_t -import realm_wrapper.realm_sync_client_metadata_mode import realm_wrapper.realm_sync_session_resync_mode import realm_wrapper.realm_sync_session_state_e import realm_wrapper.realm_sync_session_stop_policy_e @@ -134,7 +132,6 @@ import realm_wrapper.realm_sync_socket_timer_t import realm_wrapper.realm_sync_socket_websocket_t import realm_wrapper.realm_sync_socket_write_callback_t import realm_wrapper.realm_t -import realm_wrapper.realm_user_identity import realm_wrapper.realm_user_t import realm_wrapper.realm_value_t import realm_wrapper.realm_value_type @@ -2066,7 +2063,7 @@ actual object RealmInterop { syncClientConfig: RealmSyncClientConfigurationPointer, basePath: String ): RealmAppPointer { - return CPointerWrapper(realm_wrapper.realm_app_create(appConfig.cptr(), syncClientConfig.cptr()), managed = true) + return CPointerWrapper(realm_wrapper.realm_app_create(appConfig.cptr()), managed = true) } actual fun realm_app_get_current_user(app: RealmAppPointer): RealmUserPointer? { @@ -2133,7 +2130,7 @@ actual object RealmInterop { app.cptr(), user.cptr(), name, - staticCFunction { userData: CPointer?, apiKey: CPointer?, error: CPointer? -> + staticCFunction { userData: CPointer?, apiKey: CPointer?, error: CPointer? -> handleAppCallback(userData, error) { apiKey!!.pointed.let { ApiKeyWrapper( @@ -2224,7 +2221,7 @@ actual object RealmInterop { app.cptr(), user.cptr(), id.realm_object_id_t(), - staticCFunction { userData: CPointer?, apiKey: CPointer?, error: CPointer? -> + staticCFunction { userData: CPointer?, apiKey: CPointer?, error: CPointer? -> handleAppCallback(userData, error) { apiKey!!.pointed.let { ApiKeyWrapper( @@ -2253,7 +2250,7 @@ actual object RealmInterop { realm_wrapper.realm_app_user_apikey_provider_client_fetch_apikeys( app.cptr(), user.cptr(), - staticCFunction { userData: CPointer?, apiKeys: CPointer?, count: size_t, error: CPointer? -> + staticCFunction { userData: CPointer?, apiKeys: CPointer?, count: size_t, error: CPointer? -> handleAppCallback(userData, error) { val result = arrayOfNulls(count.toInt()) for (i in 0 until count.toInt()) { @@ -2374,11 +2371,38 @@ actual object RealmInterop { .also { realm_wrapper.realm_free(cPath) } } + actual fun realm_app_get_base_url( + app: RealmAppPointer, + ): String = realm_wrapper.realm_app_get_base_url(app.cptr())?.toKString()!! + + actual fun realm_app_update_base_url( + app: RealmAppPointer, + baseUrl: String?, + callback: AppCallback, + ) { + checkedBooleanResult( + realm_wrapper.realm_app_update_base_url( + app.cptr(), + baseUrl, + callback = staticCFunction { userData, error -> + handleAppCallback( + userData, + error + ) { /* No-op, returns Unit */ } + }, + StableRef.create(callback).asCPointer(), + staticCFunction { userdata -> + disposeUserData>(userdata) + } + ) + ) + } + actual fun realm_user_get_all_identities(user: RealmUserPointer): List { memScoped { val count = AuthProvider.values().size - val properties = allocArray(count) - val outCount = alloc() + val properties = allocArray(count) + val outCount: ULongVarOf = alloc() realm_wrapper.realm_user_get_all_identities( user.cptr(), properties, @@ -2472,11 +2496,11 @@ actual object RealmInterop { ) } - actual fun realm_sync_client_config_set_base_file_path( - syncClientConfig: RealmSyncClientConfigurationPointer, + actual fun realm_app_config_set_base_file_path( + appConfig: RealmAppConfigurationPointer, basePath: String ) { - realm_wrapper.realm_sync_client_config_set_base_file_path(syncClientConfig.cptr(), basePath) + realm_wrapper.realm_app_config_set_base_file_path(appConfig.cptr(), basePath) } actual fun realm_sync_client_config_set_multiplex_sessions(syncClientConfig: RealmSyncClientConfigurationPointer, enabled: Boolean) { @@ -2498,24 +2522,24 @@ actual object RealmInterop { realm_wrapper.realm_set_log_level(level.priority.toUInt()) } - actual fun realm_sync_client_config_set_metadata_mode( - syncClientConfig: RealmSyncClientConfigurationPointer, + actual fun realm_app_config_set_metadata_mode( + appConfig: RealmAppConfigurationPointer, metadataMode: MetadataMode ) { - realm_wrapper.realm_sync_client_config_set_metadata_mode( - syncClientConfig.cptr(), - realm_sync_client_metadata_mode.byValue(metadataMode.metadataValue.toUInt()) + realm_wrapper.realm_app_config_set_metadata_mode( + appConfig.cptr(), + realm_wrapper.realm_sync_client_metadata_mode.byValue(metadataMode.metadataValue.toUInt()) ) } - actual fun realm_sync_client_config_set_metadata_encryption_key( - syncClientConfig: RealmSyncClientConfigurationPointer, + actual fun realm_app_config_set_metadata_encryption_key( + appConfig: RealmAppConfigurationPointer, encryptionKey: ByteArray ) { memScoped { val encryptionKeyPointer = encryptionKey.refTo(0).getPointer(memScope) - realm_wrapper.realm_sync_client_config_set_metadata_encryption_key( - syncClientConfig.cptr(), + realm_wrapper.realm_app_config_set_metadata_encryption_key( + appConfig.cptr(), encryptionKeyPointer as CPointer ) } diff --git a/packages/external/core b/packages/external/core index 316889b967..cde3adb764 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit 316889b967f845fbc10b4422f96c7eadd47136f2 +Subproject commit cde3adb7649d3361806dbbae0cf353b8fdc4d54e diff --git a/packages/jni-swig-stub/realm.i b/packages/jni-swig-stub/realm.i index 9ea8b25428..8028cd1651 100644 --- a/packages/jni-swig-stub/realm.i +++ b/packages/jni-swig-stub/realm.i @@ -1,5 +1,7 @@ %module(directors="1") realmc +#define REALM_APP_SERVICES 1 + %{ #include "realm.h" #include @@ -523,6 +525,8 @@ $result = SWIG_JavaArrayOutLonglong(jenv, (long long *)result, 2); %ignore "realm_dictionary_add_notification_callback"; %ignore "realm_results_add_notification_callback"; +%ignore "realm_app_config_get_sync_client_config"; + // Swig doesn't understand __attribute__ so eliminate it #define __attribute__(x) diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt index 21a44d3831..d129f3b526 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt @@ -17,6 +17,7 @@ package io.realm.kotlin.mongodb import io.realm.kotlin.internal.util.Validation +import io.realm.kotlin.mongodb.annotations.ExperimentalEdgeServerApi import io.realm.kotlin.mongodb.auth.EmailPasswordAuth import io.realm.kotlin.mongodb.exceptions.AppException import io.realm.kotlin.mongodb.exceptions.AuthException @@ -84,6 +85,23 @@ public interface App { */ public val sync: Sync + /** + * Current base URL to communicate with App Services. + */ + @ExperimentalEdgeServerApi + public val baseUrl: String + + /** + * Sets the App Services base url. + * + * *NOTE* Changing the URL would trigger a client reset. + * + * @param baseUrl The new App Services base url. If `null` it will be using the default value + * ([AppConfiguration.DEFAULT_BASE_URL]). + */ + @ExperimentalEdgeServerApi + public suspend fun updateBaseUrl(baseUrl: String?) + /** * Returns all known users that are either [User.State.LOGGED_IN] or [User.State.LOGGED_OUT]. * Only users that at some point logged into this device will be returned. diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/annotations/ExperimentalEdgeServerApi.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/annotations/ExperimentalEdgeServerApi.kt new file mode 100644 index 0000000000..62d59c2d74 --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/annotations/ExperimentalEdgeServerApi.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024 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. + * + */ +package io.realm.kotlin.mongodb.annotations + +/** + * This annotation mark Realm APIs specific to the Atlas Edge server and are considered + * **experimental**, i.e. there are no guarantees given that these APIs cannot change without + * warning between minor and major versions. They will not change between patch versions. + * + * For all other purposes these APIs are considered stable, i.e. they undergo the same testing + * as other parts of the API and should behave as documented with no bugs. They are primarily + * marked as experimental because we are unsure if these APIs provide value and solve the use + * cases that people have. If not, they will be changed or removed altogether. + */ +@MustBeDocumented +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.PROPERTY, + AnnotationTarget.FUNCTION, + AnnotationTarget.TYPEALIAS +) +@RequiresOptIn(level = RequiresOptIn.Level.ERROR) +public annotation class ExperimentalEdgeServerApi diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt index 08a6b9285d..0943b8c668 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt @@ -132,7 +132,7 @@ public class AppConfigurationImpl @OptIn(ExperimentalKBsonSerializerApi::class) bundleId: String, networkTransport: NetworkTransport ): RealmAppConfigurationPointer { - return RealmInterop.realm_app_config_new( + val appConfigPtr = RealmInterop.realm_app_config_new( appId = appId, baseUrl = baseUrl, networkTransport = RealmInterop.realm_network_transport_new(networkTransport), @@ -146,6 +146,15 @@ public class AppConfigurationImpl @OptIn(ExperimentalKBsonSerializerApi::class) frameworkVersion = RUNTIME_VERSION ) ) + RealmInterop.realm_app_config_set_base_file_path(appConfigPtr, syncRootDirectory) + RealmInterop.realm_app_config_set_metadata_mode(appConfigPtr, metadataMode) + encryptionKey?.let { + RealmInterop.realm_app_config_set_metadata_encryption_key( + appConfigPtr, + it + ) + } + return appConfigPtr } private fun initializeSyncClientConfig( @@ -157,21 +166,6 @@ public class AppConfigurationImpl @OptIn(ExperimentalKBsonSerializerApi::class) .also { syncClientConfig -> // Initialize client configuration first RealmInterop.realm_sync_client_config_set_default_binding_thread_observer(syncClientConfig, appId) - RealmInterop.realm_sync_client_config_set_metadata_mode( - syncClientConfig, - metadataMode - ) - RealmInterop.realm_sync_client_config_set_base_file_path( - syncClientConfig, - syncRootDirectory - ) - - encryptionKey?.let { - RealmInterop.realm_sync_client_config_set_metadata_encryption_key( - syncClientConfig, - it - ) - } sdkInfo?.let { RealmInterop.realm_sync_client_config_set_user_agent_binding_info( diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt index ffd161b575..870e08de95 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt @@ -31,6 +31,7 @@ import io.realm.kotlin.mongodb.AppConfiguration import io.realm.kotlin.mongodb.AuthenticationChange import io.realm.kotlin.mongodb.Credentials import io.realm.kotlin.mongodb.User +import io.realm.kotlin.mongodb.annotations.ExperimentalEdgeServerApi import io.realm.kotlin.mongodb.auth.EmailPasswordAuth import io.realm.kotlin.mongodb.sync.Sync import io.realm.kotlin.types.RealmInstant @@ -63,6 +64,24 @@ public class AppImpl( @Suppress("MagicNumber") private val reconnectThreshold = 5.seconds + @ExperimentalEdgeServerApi + override val baseUrl: String + get() = RealmInterop.realm_app_get_base_url(nativePointer) + + @ExperimentalEdgeServerApi + override suspend fun updateBaseUrl(baseUrl: String?) { + Channel>(1).use { channel -> + RealmInterop.realm_app_update_base_url( + app = nativePointer, + baseUrl = baseUrl?.trimEnd('/'), // trailing slashes are not handled properly in core + callback = channelResultCallback(channel) { + // No-op + } + ) + channel.receive().getOrThrow() + } + } + @Suppress("invisible_member", "invisible_reference", "MagicNumber") private val connectionListener = NetworkStateObserver.ConnectionListener { connectionAvailable -> // In an ideal world, we would be able to reliably detect the network coming and diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncUtils.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncUtils.kt index fb72304c78..b86783b6f0 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncUtils.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncUtils.kt @@ -156,11 +156,8 @@ internal fun convertAppError(appError: AppError): Throwable { ErrorCode.RLM_ERR_CLIENT_USER_NOT_FOUND -> { IllegalStateException(msg) } - ErrorCode.RLM_ERR_CLIENT_USER_NOT_LOGGED_IN -> { - InvalidCredentialsException(msg) - } - ErrorCode.RLM_ERR_CLIENT_APP_DEALLOCATED -> { - AppException(msg) + ErrorCode.RLM_ERR_CLIENT_USER_ALREADY_NAMED -> { + CredentialsCannotBeLinkedException(msg) } else -> { AppException(msg) diff --git a/packages/test-base/build.gradle.kts b/packages/test-base/build.gradle.kts index e56161f7d7..f8eedddd70 100644 --- a/packages/test-base/build.gradle.kts +++ b/packages/test-base/build.gradle.kts @@ -247,7 +247,7 @@ kotlin { } targets.filterIsInstance().forEach { simulatorTargets -> simulatorTargets.testRuns.forEach { testRun -> - testRun.deviceId = project.findProperty("iosDevice")?.toString() ?: "iPhone 12" + testRun.deviceId = project.findProperty("iosDevice")?.toString() ?: "iPhone 14" } } sourceSets { diff --git a/packages/test-sync/build.gradle.kts b/packages/test-sync/build.gradle.kts index 81070f4e04..f24c626ba6 100644 --- a/packages/test-sync/build.gradle.kts +++ b/packages/test-sync/build.gradle.kts @@ -258,7 +258,7 @@ kotlin { } targets.filterIsInstance().forEach { simulatorTargets -> simulatorTargets.testRuns.forEach { testRun -> - testRun.deviceId = project.findProperty("iosDevice")?.toString() ?: "iPhone 12" + testRun.deviceId = project.findProperty("iosDevice")?.toString() ?: "iPhone 14" } } sourceSets { diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt index 10a037e906..576ab58307 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt @@ -21,6 +21,8 @@ import io.realm.kotlin.RealmConfiguration import io.realm.kotlin.internal.platform.appFilesDirectory import io.realm.kotlin.internal.platform.fileExists import io.realm.kotlin.internal.platform.runBlocking +import io.realm.kotlin.log.LogLevel +import io.realm.kotlin.log.RealmLog import io.realm.kotlin.mongodb.App import io.realm.kotlin.mongodb.AppConfiguration import io.realm.kotlin.mongodb.AuthenticationChange @@ -30,8 +32,11 @@ import io.realm.kotlin.mongodb.LoggedIn import io.realm.kotlin.mongodb.LoggedOut import io.realm.kotlin.mongodb.Removed import io.realm.kotlin.mongodb.User +import io.realm.kotlin.mongodb.annotations.ExperimentalEdgeServerApi import io.realm.kotlin.mongodb.exceptions.InvalidCredentialsException +import io.realm.kotlin.mongodb.exceptions.ServiceException import io.realm.kotlin.mongodb.sync.SyncConfiguration +import io.realm.kotlin.test.mongodb.SyncServerConfig import io.realm.kotlin.test.mongodb.TEST_APP_FLEX import io.realm.kotlin.test.mongodb.TestApp import io.realm.kotlin.test.mongodb.asTestApp @@ -478,4 +483,93 @@ class AppTests { } } } + + /** + * The app id must exist on the new base url, it is validated and an exception would be thrown. + * + * This test case circumvents this issue by initializing an app to a url that does not contain the + * app, as it is not validated on initialization. And then updating the base url the the test server. + */ + @Test + @OptIn(ExperimentalEdgeServerApi::class) + fun changeBaseUrl() { + TestApp( + testId = "changeBaseUrl", + builder = { builder -> + // We create a test app that points to the default base url + // this app is not going to be validated yet. + builder.baseUrl(AppConfiguration.DEFAULT_BASE_URL) + } + ).use { testApp -> + assertEquals(AppConfiguration.DEFAULT_BASE_URL, testApp.baseUrl) + + runBlocking { + // Update the base url, this method will validate the app + // if the app id is not available it would fail. + testApp.updateBaseUrl(app.configuration.baseUrl) + } + assertEquals(app.configuration.baseUrl, testApp.baseUrl) + } + } + + @Test + // We don't have a way to test this on CI, so for now just verify manually that the + // request towards the server after setting the URL to null is using the default URL. + @Ignore + @OptIn(ExperimentalEdgeServerApi::class) + fun changeBaseUrl_null() { + TestApp( + testId = "changeBaseUrl", + ).use { testApp -> + assertEquals(SyncServerConfig.url, testApp.baseUrl) + + RealmLog.level = LogLevel.ALL + runBlocking { + testApp.updateBaseUrl(null) + } + } + } + + @Test + @Ignore // See https://github.com/realm/realm-kotlin/issues/1734 + @OptIn(ExperimentalEdgeServerApi::class) + fun changeBaseUrl_trailing_slashes_trimmed() { + assertFailsWithMessage("cannot find app using Client App ID") { + runBlocking { + app.updateBaseUrl(AppConfiguration.DEFAULT_BASE_URL + "///") + } + } + } + + @Test + @Ignore // see https://github.com/realm/realm-kotlin/issues/1734 + @OptIn(ExperimentalEdgeServerApi::class) + fun changeBaseUrl_empty() { + assertFailsWithMessage("cannot find app using Client App ID") { + runBlocking { + app.updateBaseUrl("") + } + } + } + + @Test + @OptIn(ExperimentalEdgeServerApi::class) + fun changeBaseUrl_invalidUrl() { + assertFailsWithMessage("URL missing scheme") { + runBlocking { + app.updateBaseUrl("hello world") + } + } + } + + @Test + @Ignore // see https://github.com/realm/realm-kotlin/issues/1734 + @OptIn(ExperimentalEdgeServerApi::class) + fun changeBaseUrl_nonAppServicesUrl() { + assertFailsWithMessage("http error code considered fatal") { + runBlocking { + app.updateBaseUrl("https://www.google.com/") + } + } + } } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/ProgressListenerTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/ProgressListenerTests.kt index 51437d237c..a57e0bdbf5 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/ProgressListenerTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/ProgressListenerTests.kt @@ -54,6 +54,7 @@ import kotlinx.coroutines.withTimeout import org.mongodb.kbson.ObjectId import kotlin.test.AfterTest import kotlin.test.BeforeTest +import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -84,6 +85,7 @@ class ProgressListenerTests { } @Test + @Ignore // https://github.com/realm/realm-core/issues/7627 fun downloadProgressListener_changesOnly() = runBlocking { Realm.open(createSyncConfig(app.createUserAndLogIn())).use { uploadRealm -> // Verify that we: @@ -173,6 +175,7 @@ class ProgressListenerTests { } @Test + @Ignore // https://github.com/realm/realm-core/issues/7627 fun worksAfterExceptions() = runBlocking { Realm.open(createSyncConfig(app.createUserAndLogIn())).use { realm -> realm.writeSampleData(TEST_SIZE, timeout = TIMEOUT) @@ -195,6 +198,7 @@ class ProgressListenerTests { } @Test + @Ignore // https://github.com/realm/realm-core/issues/7627 fun worksAfterCancel() = runBlocking { Realm.open(createSyncConfig(app.createUserAndLogIn())).use { realm -> realm.writeSampleData(TEST_SIZE, timeout = TIMEOUT) @@ -225,6 +229,7 @@ class ProgressListenerTests { } @Test + @Ignore // https://github.com/realm/realm-core/issues/7627 fun triggerImmediatelyWhenRegistered() = runBlocking { Realm.open(createSyncConfig(app.createUserAndLogIn())).use { realm -> withTimeout(10.seconds) { diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserTests.kt index 417c4c5516..34b69a847d 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserTests.kt @@ -488,7 +488,7 @@ class UserTests { emailUser1.linkCredentials(Credentials.anonymous()) }.let { assertTrue( - it.message!!.contains("linking an anonymous identity is not allowed"), + it.message!!.contains("Cannot add anonymous credentials to an existing user"), it.message ) }