diff --git a/CHANGELOG.md b/CHANGELOG.md index 48c21527e1..233153e294 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Enhancements * Added support for `UUID` through a new property type: `RealmUUID`. +* [Sync] Add support for `User.delete()`, making it possible to delete user data on the server side (Issue [#491](https://github.com/realm/realm-kotlin/issues/491)). ### Fixed * Missing proguard configuration for `CoreErrorUtils`. (Issue [#942](https://github.com/realm/realm-kotlin/issues/942)) 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 66e3fcbb19..170f08de2b 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 @@ -263,6 +263,7 @@ expect object RealmInterop { fun realm_app_log_in_with_credentials(app: RealmAppPointer, credentials: RealmCredentialsPointer, callback: AppCallback) fun realm_app_log_out(app: RealmAppPointer, user: RealmUserPointer, callback: AppCallback) fun realm_app_remove_user(app: RealmAppPointer, user: RealmUserPointer, callback: AppCallback) + fun realm_app_delete_user(app: RealmAppPointer, user: RealmUserPointer, callback: AppCallback) fun realm_clear_cached_apps() fun realm_app_sync_client_get_default_file_path_for_realm( app: RealmAppPointer, diff --git a/packages/cinterop/src/darwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/darwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 34e2d35e6d..16786e9639 100644 --- a/packages/cinterop/src/darwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/darwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -1575,6 +1575,26 @@ actual object RealmInterop { ) } + actual fun realm_app_delete_user( + app: RealmAppPointer, + user: RealmUserPointer, + callback: AppCallback + ) { + checkedBooleanResult( + realm_wrapper.realm_app_delete_user( + app.cptr(), + user.cptr(), + staticCFunction { userData, error -> + handleAppCallback(userData, error) { /* No-op, returns Unit */ } + }, + StableRef.create(callback).asCPointer(), + staticCFunction { userdata -> + disposeUserData>(userdata) + } + ) + ) + } + actual fun realm_clear_cached_apps() { realm_wrapper.realm_clear_cached_apps() } 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 5767b11b15..7de346df81 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 @@ -688,6 +688,14 @@ actual object RealmInterop { realmc.realm_app_remove_user(app.cptr(), user.cptr(), callback) } + actual fun realm_app_delete_user( + app: RealmAppPointer, + user: RealmUserPointer, + callback: AppCallback + ) { + realmc.realm_app_delete_user(app.cptr(), user.cptr(), callback) + } + actual fun realm_app_get_current_user(app: RealmAppPointer): RealmUserPointer? { val ptr = realmc.realm_app_get_current_user(app.cptr()) return nativePointerOrNull(ptr) diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/User.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/User.kt index 32423fb437..24bc77fdb7 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/User.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/User.kt @@ -88,6 +88,21 @@ public interface User { // TODO Document how this method behave if offline public suspend fun remove(): User + /** + * Permanently deletes this user from your Atlas App Services app. + * + * If the user was deleted successfully on Atlas, the user state will be set to + * [State.REMOVED] and any local Realm files owned by the user will be deleted. If + * the server request fails, the local state will not be modified. + * + * All user realms should be closed before calling this method. + * + * @throws IllegalStateException if the user was already removed or not logged in. + * @throws io.realm.kotlin.mongodb.exceptions.ServiceException if a failure occurred when + * communicating with App Services. See [AppException] for details. + */ + public suspend fun delete() + /** * Two Users are considered equal if they have the same user identity and are associated * with the same app. diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/UserImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/UserImpl.kt index 748d00faa7..2660720e6c 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/UserImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/UserImpl.kt @@ -69,6 +69,23 @@ public class UserImpl( return this } + override suspend fun delete() { + if (state != User.State.LOGGED_IN) { + throw IllegalStateException("User must be logged in, in order to be deleted.") + } + Channel>(1).use { channel -> + RealmInterop.realm_app_delete_user( + app.nativePointer, + nativePointer, + channelResultCallback(channel) { + // No-op + }.freeze() + ) + return@use channel.receive() + .getOrThrow() + } + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || this::class != other::class) return false diff --git a/test/sync/src/androidTest/kotlin/io/realm/kotlin/test/mongodb/shared/UserTests.kt b/test/sync/src/androidTest/kotlin/io/realm/kotlin/test/mongodb/shared/UserTests.kt index a5e4ff8087..901b937ecd 100644 --- a/test/sync/src/androidTest/kotlin/io/realm/kotlin/test/mongodb/shared/UserTests.kt +++ b/test/sync/src/androidTest/kotlin/io/realm/kotlin/test/mongodb/shared/UserTests.kt @@ -16,12 +16,17 @@ package io.realm.kotlin.test.mongodb.shared +import io.realm.kotlin.Realm +import io.realm.kotlin.entities.sync.SyncObjectWithAllTypes +import io.realm.kotlin.internal.platform.fileExists import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.mongodb.App import io.realm.kotlin.mongodb.Credentials import io.realm.kotlin.mongodb.User +import io.realm.kotlin.mongodb.sync.SyncConfiguration import io.realm.kotlin.test.mongodb.TestApp import io.realm.kotlin.test.mongodb.asTestApp +import io.realm.kotlin.test.util.TestHelper import io.realm.kotlin.test.util.TestHelper.randomEmail import kotlin.test.AfterTest import kotlin.test.BeforeTest @@ -297,6 +302,57 @@ class UserTests { } } + @Test + fun deleteUser() { + runBlocking { + val user1 = createUserAndLogin() + val config = SyncConfiguration.create( + user1, + TestHelper.randomPartitionValue(), + setOf(SyncObjectWithAllTypes::class) + ) + Realm.open(config).close() + assertTrue(fileExists(config.path)) + assertEquals(user1, app.currentUser) + assertEquals(1, app.allUsers().size) + user1.delete() + assertEquals(User.State.REMOVED, user1.state) + assertNull(app.currentUser) + assertEquals(0, app.allUsers().size) + assertFalse(fileExists(config.path)) + } + } + + @Test + fun deleteUser_loggedOutThrows() { + runBlocking { + val user1 = createUserAndLogin() + val config = SyncConfiguration.create( + user1, + TestHelper.randomPartitionValue(), + setOf(SyncObjectWithAllTypes::class) + ) + Realm.open(config).close() + user1.logOut() + assertTrue(fileExists(config.path)) + assertFailsWith { + user1.delete() + } + assertTrue(fileExists(config.path)) + } + } + + @Test + fun deleteUser_throwsIfUserAlreadyDeleted() { + runBlocking { + val user1 = createUserAndLogin() + user1.delete() + assertFailsWith { + user1.delete() + } + } + } + // @Test // fun getApiKeyAuthProvider() { // val user: User = app.registerUserAndLogin(TestHelper.getRandomEmail(), "123456")