diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 2e5864113d..749866c7c2 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -128,12 +128,12 @@ object Versions { const val latestKotlin = "2.0.0" // https://kotlinlang.org/docs/eap.html#build-details const val kotlinCompileTesting = "0.5.0-alpha07" // https://github.com/zacsweers/kotlin-compile-testing const val ktlint = "0.45.2" // https://github.com/pinterest/ktlint - const val ktor = "2.3.7" // https://github.com/ktorio/ktor + const val ktor = "2.3.12" // https://github.com/ktorio/ktor const val multidex = "2.0.1" // https://developer.android.com/jetpack/androidx/releases/multidex const val nexusPublishPlugin = "1.1.0" // https://github.com/gradle-nexus/publish-plugin const val okio = "3.2.0" // https://square.github.io/okio/#releases const val relinker = "1.4.5" // https://github.com/KeepSafe/ReLinker - const val serialization = "1.6.0" // https://kotlinlang.org/docs/releases.html#release-details + const val serialization = "1.7.1" // https://kotlinlang.org/docs/releases.html#release-details const val shadowJar = "6.1.0" // https://mvnrepository.com/artifact/com.github.johnrengelman.shadow/com.github.johnrengelman.shadow.gradle.plugin?repo=gradle-plugins const val snakeYaml = "1.33" // https://github.com/snakeyaml/snakeyaml val sourceCompatibilityVersion = JavaVersion.VERSION_1_8 // Language level of any Java source code. diff --git a/packages/test-sync/build.gradle.kts b/packages/test-sync/build.gradle.kts index 8bf974864c..a374182607 100644 --- a/packages/test-sync/build.gradle.kts +++ b/packages/test-sync/build.gradle.kts @@ -101,6 +101,7 @@ kotlin { implementation("io.ktor:ktor-client-logging:${Versions.ktor}") implementation("io.ktor:ktor-serialization-kotlinx-json:${Versions.ktor}") implementation("io.ktor:ktor-client-content-negotiation:${Versions.ktor}") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:${Versions.serialization}") implementation("com.squareup.okio:okio:${Versions.okio}") } diff --git a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt index 60ae1c5295..079ab3b377 100644 --- a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt +++ b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt @@ -19,7 +19,6 @@ package io.realm.kotlin.test.mongodb -import io.realm.kotlin.Realm import io.realm.kotlin.annotations.ExperimentalRealmSerializerApi import io.realm.kotlin.internal.interop.RealmInterop import io.realm.kotlin.internal.interop.SynchronizableObject @@ -31,15 +30,12 @@ import io.realm.kotlin.mongodb.AppConfiguration import io.realm.kotlin.mongodb.Credentials import io.realm.kotlin.mongodb.User import io.realm.kotlin.mongodb.internal.AppConfigurationImpl -import io.realm.kotlin.mongodb.sync.SyncConfiguration -import io.realm.kotlin.test.mongodb.common.FLEXIBLE_SYNC_SCHEMA import io.realm.kotlin.test.mongodb.util.AppAdmin import io.realm.kotlin.test.mongodb.util.AppAdminImpl import io.realm.kotlin.test.mongodb.util.AppInitializer import io.realm.kotlin.test.mongodb.util.AppServicesClient import io.realm.kotlin.test.platform.PlatformUtils import io.realm.kotlin.test.util.TestHelper -import io.realm.kotlin.test.util.use import kotlinx.coroutines.CloseableCoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -113,28 +109,6 @@ open class TestApp private constructor( ) ) - init { - // For apps with Flexible Sync, we need to bootstrap all the schemas to work around - // https://github.com/realm/realm-core/issues/7297. - // So we create a dummy Realm, upload all the schemas and close the Realm again. - if (app.configuration.appId.startsWith(TEST_APP_FLEX, ignoreCase = false)) { - runBlocking { - val user = app.login(Credentials.anonymous()) - val config = SyncConfiguration.create(user, FLEXIBLE_SYNC_SCHEMA) - try { - Realm.open(config).use { - // Using syncSession.uploadAllLocalChanges() seems to just hang forever. - // This is tracked by the above Core issue. Instead use the Sync Progress - // endpoint to signal when the schemas are ready. - pairAdminApp.second.waitForSyncBootstrap() - } - } finally { - user.delete() - } - } - } - } - fun createUserAndLogin(): User = runBlocking { val (email, password) = TestHelper.randomEmail() to "password1234" emailPasswordAuth.registerUser(email, password).run { diff --git a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/AppServicesClient.kt b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/AppServicesClient.kt index 366cb6dfaf..a754eb792b 100644 --- a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/AppServicesClient.kt +++ b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/AppServicesClient.kt @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:Suppress("invisible_member", "invisible_reference") package io.realm.kotlin.test.mongodb.util @@ -35,18 +36,23 @@ import io.ktor.http.HttpMethod.Companion.Post import io.ktor.http.contentType import io.ktor.http.isSuccess import io.ktor.serialization.kotlinx.json.json +import io.realm.kotlin.internal.interop.PropertyType import io.realm.kotlin.internal.platform.runBlocking +import io.realm.kotlin.internal.schema.RealmClassImpl import io.realm.kotlin.mongodb.sync.SyncMode +import io.realm.kotlin.schema.RealmClassKind import io.realm.kotlin.test.mongodb.SyncServerConfig import io.realm.kotlin.test.mongodb.TEST_APP_CLUSTER_NAME -import io.realm.kotlin.test.mongodb.common.FLEXIBLE_SYNC_SCHEMA_COUNT +import io.realm.kotlin.types.BaseRealmObject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.json.ClassDiscriminatorMode import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement @@ -57,16 +63,24 @@ import kotlinx.serialization.json.add import kotlinx.serialization.json.boolean import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.encodeToJsonElement import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put import kotlinx.serialization.serializer +import kotlin.reflect.KClass private const val ADMIN_PATH = "/api/admin/v3.0" private const val PRIVATE_PATH = "/api/private/v1.0" +@OptIn(ExperimentalSerializationApi::class) +private val json = Json { + classDiscriminatorMode = ClassDiscriminatorMode.NONE + encodeDefaults = true +} + data class SyncPermissions( val read: Boolean, val write: Boolean @@ -125,6 +139,203 @@ data class BaasApp( get() = client.baseUrl + PRIVATE_PATH + "/groups/${this.groupId}/apps/${this._id}" } +@Serializable +data class Schema( + val metadata: SchemaMetadata = SchemaMetadata( + database = "database", + collection = "title" + ), + val schema: SchemaData, + val relationships: Map = emptyMap(), +) { + constructor( + database: String, + schema: SchemaData, + relationships: Map, + ) : this( + metadata = SchemaMetadata( + database = database, + collection = schema.title + ), + schema = schema, + relationships = relationships + ) +} + +@Serializable +data class SchemaMetadata( + var database: String = "", + @SerialName("data_source") + var dataSource: String = "BackingDB", + var collection: String = "SyncDog", +) + +@Serializable +data class SchemaRelationship( + @SerialName("source_key") + val sourceKey: String, + @SerialName("foreign_key") + val foreignKey: String, + @SerialName("is_list") + val isList: Boolean, + val ref: String = "", +) { + constructor( + target: String, + database: String, + sourceKey: String, + foreignKey: String, + isList: Boolean, + ) : this( + sourceKey = sourceKey, + foreignKey = foreignKey, + isList = isList, + ref = "#/relationship/BackingDB/$database/$target" + ) +} + +@Serializable +sealed interface SchemaPropertyType { + @Transient val isRequired: Boolean +} + +@Serializable +class ObjectReferenceType( + @Transient val sourceKey: String = "", + @Transient val targetKey: String = "", + @Transient val target: String = "", + @Transient val isList: Boolean = false, + val bsonType: PrimitivePropertyType.Type, +) : SchemaPropertyType { + constructor(sourceKey: String, targetSchema: RealmClassImpl, isCollection: Boolean) : this( + sourceKey = sourceKey, + targetKey = targetSchema.cinteropClass.primaryKey, + target = targetSchema.name, + bsonType = targetSchema.cinteropProperties + .first { it.name == targetSchema.cinteropClass.primaryKey } + .type + .toSchemaType(), + isList = isCollection + ) + + @Transient + override val isRequired: Boolean = false +} + +@Serializable +data class SchemaData( + var title: String = "", + var properties: Map = mutableMapOf(), + val required: List = mutableListOf(), + @Transient val kind: RealmClassKind = RealmClassKind.STANDARD, + val type: PrimitivePropertyType.Type = PrimitivePropertyType.Type.OBJECT, +) : SchemaPropertyType { + @Transient + override val isRequired: Boolean = false +} + +@Serializable +data class CollectionPropertyType( + val items: SchemaPropertyType, + val uniqueItems: Boolean = false, +) : SchemaPropertyType { + val bsonType = PrimitivePropertyType.Type.ARRAY + @Transient + override val isRequired: Boolean = false +} + +@Serializable +data class MapPropertyType( + val additionalProperties: SchemaPropertyType, +) : SchemaPropertyType { + val bsonType = PrimitivePropertyType.Type.OBJECT + @Transient + override val isRequired: Boolean = false +} + +@Serializable +open class PrimitivePropertyType( + val bsonType: Type, + @Transient override val isRequired: Boolean = false, +) : SchemaPropertyType { + + enum class Type { + @SerialName("string") + STRING, + + @SerialName("object") + OBJECT, + + @SerialName("array") + ARRAY, + + @SerialName("objectId") + OBJECT_ID, + + @SerialName("boolean") + BOOLEAN, + + @SerialName("bool") + BOOL, + + @SerialName("null") + NULL, + + @SerialName("regex") + REGEX, + + @SerialName("date") + DATE, + + @SerialName("timestamp") + TIMESTAMP, + + @SerialName("int") + INT, + + @SerialName("long") + LONG, + + @SerialName("decimal") + DECIMAL, + + @SerialName("double") + DOUBLE, + + @SerialName("number") + NUMBER, + + @SerialName("binData") + BIN_DATA, + + @SerialName("uuid") + UUID, + + @SerialName("mixed") + MIXED, + + @SerialName("float") + FLOAT; + } +} + +fun PropertyType.toSchemaType() = + when (this) { + PropertyType.RLM_PROPERTY_TYPE_BOOL -> PrimitivePropertyType.Type.BOOL + PropertyType.RLM_PROPERTY_TYPE_INT -> PrimitivePropertyType.Type.INT + PropertyType.RLM_PROPERTY_TYPE_STRING -> PrimitivePropertyType.Type.STRING + PropertyType.RLM_PROPERTY_TYPE_BINARY -> PrimitivePropertyType.Type.BIN_DATA + PropertyType.RLM_PROPERTY_TYPE_OBJECT -> PrimitivePropertyType.Type.OBJECT + PropertyType.RLM_PROPERTY_TYPE_FLOAT -> PrimitivePropertyType.Type.FLOAT + PropertyType.RLM_PROPERTY_TYPE_DOUBLE -> PrimitivePropertyType.Type.DOUBLE + PropertyType.RLM_PROPERTY_TYPE_DECIMAL128 -> PrimitivePropertyType.Type.DECIMAL + PropertyType.RLM_PROPERTY_TYPE_TIMESTAMP -> PrimitivePropertyType.Type.DATE + PropertyType.RLM_PROPERTY_TYPE_OBJECT_ID -> PrimitivePropertyType.Type.OBJECT_ID + PropertyType.RLM_PROPERTY_TYPE_UUID -> PrimitivePropertyType.Type.UUID + PropertyType.RLM_PROPERTY_TYPE_MIXED -> PrimitivePropertyType.Type.MIXED + else -> throw IllegalArgumentException("Unsupported type") + } + /** * Client to interact with App Services Server. It allows to create Applications and tweak their * configurations. @@ -189,6 +400,31 @@ class AppServicesClient( } } + suspend fun BaasApp.setSchema( + schema: Set>, + extraProperties: Map = emptyMap() + ) { + val schemas = SchemaProcessor.process( + databaseName = clientAppId, + classes = schema, + extraProperties = extraProperties + ) + + // First we create the schemas without the relationships + val ids: Map = schemas.entries + .associate { (name, schema: Schema) -> + name to addSchema(schema = schema.copy(relationships = emptyMap())) + } + + // then we update the schema to add the relationships + schemas.forEach { (name, schema) -> + updateSchema( + id = ids[name]!!, + schema = schema + ) + } + } + suspend fun BaasApp.addFunction(function: Function): Function = withContext(dispatcher) { httpClient.typedRequest( @@ -200,15 +436,31 @@ class AppServicesClient( } } - suspend fun BaasApp.addSchema(schema: String): JsonObject = + suspend fun BaasApp.updateSchema( + id: String, + schema: Schema, + ): HttpResponse = + withContext(dispatcher) { + httpClient.request( + "$url/schemas/$id" + ) { + this.method = HttpMethod.Put + setBody(json.encodeToJsonElement(schema)) + contentType(ContentType.Application.Json) + } + } + + suspend fun BaasApp.addSchema(schema: Schema): String = withContext(dispatcher) { httpClient.typedRequest( Post, "$url/schemas" ) { - setBody(Json.parseToJsonElement(schema)) + setBody(json.encodeToJsonElement(schema)) contentType(ContentType.Application.Json) } + }.let { jsonObject: JsonObject -> + jsonObject["_id"]!!.jsonPrimitive.content } suspend fun BaasApp.addService(service: String): Service = @@ -555,22 +807,8 @@ class AppServicesClient( Get, "$url/sync/progress" ).let { obj: JsonObject -> - val statuses: JsonElement = obj["progress"]!! - when (statuses) { - is JsonObject -> { - if (statuses.keys.isEmpty()) { - // It might take a few seconds to register the Schemas, so treat - // "empty" progress as initial sync not being complete (as we always - // have at least one pre-defined schema). - false - } - val bootstrapComplete: List = statuses.keys.map { schemaClass -> - statuses[schemaClass]!!.jsonObject["complete"]?.jsonPrimitive?.boolean == true - } - bootstrapComplete.all { it } && statuses.size == FLEXIBLE_SYNC_SCHEMA_COUNT - } - else -> false - } + println(obj) + obj["accepting_clients"]?.jsonPrimitive?.boolean ?: false } } catch (ex: IllegalStateException) { if (ex.message!!.contains("there are no mongodb/atlas services with provided sync state")) { @@ -653,6 +891,7 @@ class AppServicesClient( unauthorizedClient.close() val httpClient = defaultClient("realm-baas-authorized", debug) { + expectSuccess = true defaultRequest { headers { append("Authorization", "Bearer $accessToken") diff --git a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/SchemaProcessor.kt b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/SchemaProcessor.kt new file mode 100644 index 0000000000..1e8b9fc8ae --- /dev/null +++ b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/SchemaProcessor.kt @@ -0,0 +1,189 @@ +@file:Suppress("invisible_member", "invisible_reference") + +package io.realm.kotlin.test.mongodb.util + +import io.realm.kotlin.internal.interop.CollectionType +import io.realm.kotlin.internal.interop.PropertyInfo +import io.realm.kotlin.internal.interop.PropertyType +import io.realm.kotlin.internal.realmObjectCompanionOrNull +import io.realm.kotlin.internal.schema.RealmClassImpl +import io.realm.kotlin.schema.RealmClassKind +import io.realm.kotlin.types.BaseRealmObject +import kotlin.reflect.KClass + +// TODO REname methods and classes +class SchemaProcessor private constructor( + classes: Set>, + private val databaseName: String, + private val extraProperties: Map, +) { + companion object { + + fun process( + databaseName: String, + classes: Set>, + extraProperties: Map = emptyMap(), + ): Map { + val processor = SchemaProcessor(classes, databaseName, extraProperties) + + return processor.processedSchemas + .entries + .filterNot { (_, schema) -> schema.kind == RealmClassKind.EMBEDDED } + .associate { (name, schema) -> + // add metadata + name to Schema( + databaseName, + schema, + processor.processedRelationships[name]!! + ) + } + } + } + + private val realmSchemas: Map = classes.associate { clazz -> + val companion = clazz.realmObjectCompanionOrNull()!! + val realmSchema = companion.io_realm_kotlin_schema() + realmSchema.cinteropClass.name to realmSchema + } + + val processedSchemas: MutableMap = mutableMapOf() + val processedRelationships: MutableMap> = mutableMapOf() + + init { + // TODO CHECK embedded CYCLES + generateSchemas() + generateRelationships() + } + + private fun generateRelationships() { + processedSchemas.values.forEach { schema -> + processedRelationships[schema.title] = + findRelationships(schema.properties).associateBy { it.sourceKey } + } + } + + private fun findRelationships( + properties: Map, + path: String = "", + ): List = + properties.entries + .filterNot { (_, value) -> + value is PrimitivePropertyType + } + .flatMap { (key, value: SchemaPropertyType) -> + value.toSchemaRelationships(key, path) + } + + private fun SchemaPropertyType.toSchemaRelationships( + key: String, + path: String = "", + ): List { + return when (this) { + is ObjectReferenceType -> listOf(toSchemaRelationship(path)) + is CollectionPropertyType -> items.toSchemaRelationships("$path$key.[]") + is MapPropertyType -> additionalProperties.toSchemaRelationships("$path$key.[]") + is SchemaData -> findRelationships(properties, "$path$key.") + else -> emptyList() + } + } + + private fun ObjectReferenceType.toSchemaRelationship(path: String = "") = + SchemaRelationship( + database = databaseName, + target = target, + sourceKey = "$path$sourceKey", + foreignKey = targetKey, + isList = isList + ) + + private fun generateSchemas() { + realmSchemas.forEach { entry -> + if (entry.key !in processedSchemas) + entry.value.toSchema() + } + } + + private fun RealmClassImpl.toSchema() { + val name = cinteropClass.name + + val properties: Map = cinteropProperties + .filterNot { + it.isComputed + } + .associate { property: PropertyInfo -> + property.name to property.toSchemaProperty() + } + when (kind) { + RealmClassKind.STANDARD -> + extraProperties.entries.associate { + it.key to PrimitivePropertyType( + bsonType = it.value, + isRequired = false, + ) + } + + RealmClassKind.EMBEDDED -> emptyMap() + RealmClassKind.ASYMMETRIC -> emptyMap() + } + + val required: List = properties.entries + .filter { (_, value) -> + value.isRequired + } + .map { (name, _) -> name } + + processedSchemas[name] = SchemaData( + title = name, + properties = properties, + required = required, + kind = kind + ) + } + + private fun PropertyInfo.toSchemaProperty(): SchemaPropertyType = + when (collectionType) { + CollectionType.RLM_COLLECTION_TYPE_NONE -> propertyValueType() + CollectionType.RLM_COLLECTION_TYPE_LIST -> CollectionPropertyType( + items = propertyValueType(isCollection = true), + uniqueItems = false + ) + + CollectionType.RLM_COLLECTION_TYPE_SET -> CollectionPropertyType( + items = propertyValueType(isCollection = true), + uniqueItems = true + ) + + CollectionType.RLM_COLLECTION_TYPE_DICTIONARY -> MapPropertyType( + additionalProperties = propertyValueType(isCollection = true) + ) + + else -> throw IllegalStateException("Unsupported $collectionType") + } + + private fun PropertyInfo.propertyValueType(isCollection: Boolean = false): SchemaPropertyType = + if (type == PropertyType.RLM_PROPERTY_TYPE_OBJECT) + realmSchemas[linkTarget]!! + .let { targetSchema: RealmClassImpl -> + when (targetSchema.kind) { + RealmClassKind.STANDARD -> ObjectReferenceType( + name, + targetSchema, + isCollection + ) + + RealmClassKind.EMBEDDED -> getSchema(targetSchema.name) + RealmClassKind.ASYMMETRIC -> TODO() + } + } + else + PrimitivePropertyType( + bsonType = type.toSchemaType(), + isRequired = !isNullable + ) + + private fun getSchema(name: String): SchemaData { + if (name !in processedSchemas) + realmSchemas[name]!!.toSchema() + + return processedSchemas[name]!! + } +} diff --git a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/TestAppInitializer.kt b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/TestAppInitializer.kt index be5a48f0d5..3064bf410c 100644 --- a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/TestAppInitializer.kt +++ b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/TestAppInitializer.kt @@ -18,6 +18,9 @@ package io.realm.kotlin.test.mongodb.util import io.realm.kotlin.test.mongodb.TEST_APP_CLUSTER_NAME import io.realm.kotlin.test.mongodb.TEST_APP_FLEX import io.realm.kotlin.test.mongodb.TEST_APP_PARTITION +import io.realm.kotlin.test.mongodb.common.FLEXIBLE_SYNC_SCHEMA +import io.realm.kotlin.test.mongodb.common.PARTITION_BASED_SCHEMA +import kotlinx.coroutines.delay import kotlinx.serialization.json.Json interface AppInitializer { @@ -107,7 +110,10 @@ open class BaseAppInitializer( with(client) { block?.invoke(this, app) } - app.setDevelopmentMode(true) + + while (!app.initialSyncComplete()) { + delay(500) + } } } } @@ -134,9 +140,12 @@ object DefaultFlexibleSyncAppInitializer : @Suppress("LongMethod") suspend fun AppServicesClient.initializeFlexibleSync( app: BaasApp, - recoveryDisabled: Boolean = false, // TODO + recoveryDisabled: Boolean = false, ) { val databaseName = app.clientAppId + + app.setSchema(FLEXIBLE_SYNC_SCHEMA) + app.mongodbService.setSyncConfig( """ { @@ -146,7 +155,14 @@ suspend fun AppServicesClient.initializeFlexibleSync( "is_recovery_mode_disabled": $recoveryDisabled, "queryable_fields_names": [ "name", - "section" + "section", + "stringField", + "location", + "selector" + ], + "asymmetric_tables": [ + "AsymmetricA", + "Measurement" ] } } @@ -157,13 +173,18 @@ suspend fun AppServicesClient.initializeFlexibleSync( @Suppress("LongMethod") suspend fun AppServicesClient.initializePartitionSync( app: BaasApp, - recoveryDisabled: Boolean = false, // TODO + recoveryDisabled: Boolean = false, ) { val databaseName = app.clientAppId app.addFunction(canReadPartition) app.addFunction(canWritePartition) + app.setSchema( + schema = PARTITION_BASED_SCHEMA, + extraProperties = mapOf("realm_id" to PrimitivePropertyType.Type.STRING) + ) + app.mongodbService.setSyncConfig( """ { @@ -201,89 +222,6 @@ suspend fun AppServicesClient.initializePartitionSync( } """.trimIndent() ) - - app.addSchema( - """ - { - "metadata": { - "data_source": "BackingDB", - "database": "$databaseName", - "collection": "SyncDog" - }, - "schema": { - "properties": { - "_id": { - "bsonType": "objectId" - }, - "breed": { - "bsonType": "string" - }, - "name": { - "bsonType": "string" - }, - "realm_id": { - "bsonType": "string" - } - }, - "required": [ - "name" - ], - "title": "SyncDog" - } - } - """.trimIndent() - ) - - app.addSchema( - """ - { - "metadata": { - "data_source": "BackingDB", - "database": "$databaseName", - "collection": "SyncPerson" - }, - "relationships": { - "dogs": { - "ref": "#/relationship/BackingDB/$databaseName/SyncDog", - "source_key": "dogs", - "foreign_key": "_id", - "is_list": true - } - }, - "schema": { - "properties": { - "_id": { - "bsonType": "objectId" - }, - "age": { - "bsonType": "int" - }, - "dogs": { - "bsonType": "array", - "items": { - "bsonType": "objectId" - } - }, - "firstName": { - "bsonType": "string" - }, - "lastName": { - "bsonType": "string" - }, - "realm_id": { - "bsonType": "string" - } - }, - "required": [ - "firstName", - "lastName", - "age" - ], - "title": "SyncPerson" - } - } - """.trimIndent() - ) } suspend fun AppServicesClient.addEmailProvider( diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt index 239d1254b5..0cc452e6cf 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt @@ -1585,10 +1585,18 @@ class SyncedRealmTests { // key of the objects from asset-pbs.realm will not be unique on secondary runs. @Test fun initialRealm_partitionBasedSync() { + // Delete any document from previous runs + with(app.asTestApp) { + runBlocking { + deleteDocuments(clientAppId, ParentPk::class.simpleName!!, "{}") + } + } + val (email, password) = randomEmail() to "password1234" val user = runBlocking { app.createUserAndLogIn(email, password) } + val config1 = createPartitionSyncConfig( user = user, partitionValue = partitionValue, name = "db1", errorHandler = object : SyncSession.ErrorHandler {