Skip to content

Commit

Permalink
[RKOTLIN-612] MongoClient API (#1593)
Browse files Browse the repository at this point in the history
  • Loading branch information
rorbech authored May 22, 2024
1 parent dccafa2 commit cd6cc63
Show file tree
Hide file tree
Showing 50 changed files with 4,958 additions and 543 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ This release will bump the Realm file format 24. Opening a file with an older fo
* Support for RealmLists and RealmDictionaries in `RealmAny`. (Issue [#1434](https://github.com/realm/realm-kotlin/issues/1434))
* Optimized `RealmList.indexOf()` and `RealmList.contains()` using Core implementation of operations instead of iterating elements and comparing them in Kotlin. (Issue [#1625](https://github.com/realm/realm-kotlin/pull/1666) [RKOTLIN-995](https://jira.mongodb.org/browse/RKOTLIN-995)).
* Add support for filtering logs by category. (Issue [#1691](https://github.com/realm/realm-kotlin/issues/1691) [JIRA](https://jira.mongodb.org/browse/RKOTLIN-1038))
* [Sync] Add Mongo Client API to access Atlas App Service collections. It can be accessed through `User.mongoClient`. (Issue [#972](https://github.com/realm/realm-kotlin/issues/972)/[RKOTLIN-612](https://jira.mongodb.org/browse/RKOTLIN-612))

### Fixed
* Inserting the same typed link to the same key in a dictionary more than once would incorrectly create multiple backlinks to the object. This did not appear to cause any crashes later, but would have affecting explicit backlink count queries (eg: `...@links.@count`) and possibly notifications (Core Issue [realm/realm-core#7676](https://github.com/realm/realm-core/issues/7676) since v1.16.0).
Expand Down Expand Up @@ -177,9 +178,9 @@ This release will bump the Realm file format from version 23 to 24. Opening a fi
* None.

### Enhancements
* [Sync] Added option to use managed WebSockets via OkHttp instead of Realm's built-in WebSocket client for Sync traffic (Only Android and JVM targets for now). Managed WebSockets offer improved support for proxies and firewalls that require authentication. This feature is currently opt-in and can be enabled by using `AppConfiguration.usePlatformNetworking()`. Managed WebSockets will become the default in a future version. (PR [#1528](https://github.com/realm/realm-kotlin/pull/1528)).
* `AutoClientResetFailed` exception now reports as the throwable cause any user exceptions that might occur during a client reset. (Issue [#1580](https://github.com/realm/realm-kotlin/issues/1580))
* The Unpacking of JVM native library will use the current library version instead of a calculated hash for the path. (Issue [#1617](https://github.com/realm/realm-kotlin/issues/1617)).
* [Sync] Added option to use managed WebSockets via OkHttp instead of Realm's built-in WebSocket client for Sync traffic (Only Android and JVM targets for now). Managed WebSockets offer improved support for proxies and firewalls that require authentication. This feature is currently opt-in and can be enabled by using `AppConfiguration.usePlatformNetworking()`. Managed WebSockets will become the default in a future version. (PR [#1528](https://github.com/realm/realm-kotlin/pull/1528)).
* [Sync] `AutoClientResetFailed` exception now reports as the throwable cause any user exceptions that might occur during a client reset. (Issue [#1580](https://github.com/realm/realm-kotlin/issues/1580))

### Fixed
* Cache notification callback JNI references at startup to ensure that symbols can be resolved in core callbacks. (Issue [#1577](https://github.com/realm/realm-kotlin/issues/1577))
Expand Down
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ object Versions {
const val jmh = "1.34" // https://github.com/openjdk/jmh
const val jmhPlugin = "0.6.6" // https://github.com/melix/jmh-gradle-plugin
const val junit = "4.13.2" // https://mvnrepository.com/artifact/junit/junit
const val kbson = "0.3.0" // https://github.com/mongodb/kbson
const val kbson = "0.4.0" // https://github.com/mongodb/kbson
// When updating the Kotlin version, also remember to update /examples/min-android-sample/build.gradle.kts
const val kotlin = "1.9.0" // https://github.com/JetBrains/kotlin and https://kotlinlang.org/docs/releases.html#release-details
const val kotlinJvmTarget = "1.8" // Which JVM bytecode version is kotlin compiled to.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,7 @@ expect object RealmInterop {
app: RealmAppPointer,
user: RealmUserPointer,
name: String,
serviceName: String? = null,
serializedEjsonArgs: String, // as ejson
callback: AppCallback<String>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1710,14 +1710,16 @@ actual object RealmInterop {
)
}

@Suppress("LongParameterList")
actual fun realm_app_call_function(
app: RealmAppPointer,
user: RealmUserPointer,
name: String,
serviceName: String?,
serializedEjsonArgs: String,
callback: AppCallback<String>
) {
realmc.realm_app_call_function(app.cptr(), user.cptr(), name, serializedEjsonArgs, null, callback)
realmc.realm_app_call_function(app.cptr(), user.cptr(), name, serializedEjsonArgs, serviceName, callback)
}

actual fun realm_app_call_reset_password_function(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3262,6 +3262,7 @@ actual object RealmInterop {
app: RealmAppPointer,
user: RealmUserPointer,
name: String,
serviceName: String?,
serializedEjsonArgs: String,
callback: AppCallback<String>
) {
Expand All @@ -3270,7 +3271,7 @@ actual object RealmInterop {
user.cptr(),
name,
serializedEjsonArgs,
null,
serviceName,
staticCFunction { userData: CPointer<out CPointed>?, data: CPointer<ByteVarOf<Byte>>?, error: CPointer<realm_app_error_t>? ->
handleAppCallback(userData, error) {
data.safeKString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ public fun RealmInstant.toDuration(): Duration {
return epochSeconds.seconds + nanosecondsOfSecond.nanoseconds
}

internal fun Duration.toRealmInstant(): RealmInstant {
public fun Duration.toRealmInstant(): RealmInstant {
val seconds: Long = this.inWholeSeconds
val nanos: Duration = (this - seconds.seconds)
return RealmInstant.from(seconds, nanos.inWholeNanoseconds.toInt())
// We cannot do duration arithmetic as some operations on INFINITE and NEG_INFINITE will overflow
val nanos: Int = (this.inWholeNanoseconds - (seconds * RealmInstant.SEC_AS_NANOSECOND)).toInt()
return RealmInstant.from(seconds, nanos)
}

internal fun RealmInstant.restrictToMillisPrecision() =
toDuration().inWholeMilliseconds.milliseconds.toRealmInstant()

internal fun RealmInstant.asBsonDateTime() = BsonDateTime(toDuration().inWholeMilliseconds)
public inline fun RealmInstant.asBsonDateTime(): BsonDateTime = BsonDateTime(toDuration().inWholeMilliseconds)
public inline fun BsonDateTime.asRealmInstant(): RealmInstant = value.milliseconds.toRealmInstant()
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import kotlin.reflect.KProperty1
public interface RealmObjectCompanion {
public val `io_realm_kotlin_class`: KClass<out TypedRealmObject>
public val `io_realm_kotlin_className`: String
public val `io_realm_kotlin_fields`: Map<String, KProperty1<BaseRealmObject, Any?>>
public val `io_realm_kotlin_fields`: Map<String, Pair<KClass<*>, KProperty1<BaseRealmObject, Any?>>>
public val `io_realm_kotlin_primaryKey`: KMutableProperty1<*, *>?
public val `io_realm_kotlin_classKind`: RealmClassKind
public fun `io_realm_kotlin_schema`(): RealmClassImpl
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,15 @@ internal fun <T : BaseRealmObject> RealmObjectReference<T>.toRealmObject(): T =
* Returns the [RealmObjectCompanion] associated with a given [BaseRealmObject]'s [KClass].
*/
internal inline fun KClass<*>.realmObjectCompanionOrNull(): RealmObjectCompanion? {
@Suppress("invisible_reference", "invisible_member")
return realmObjectCompanionOrNull(this)
}

/**
* Returns the [RealmObjectCompanion] associated with a given [BaseRealmObject]'s [KClass].
*/
internal inline fun <reified T : BaseRealmObject> KClass<T>.realmObjectCompanionOrThrow(): RealmObjectCompanion {
@Suppress("invisible_reference", "invisible_member")
return realmObjectCompanionOrThrow(this)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import io.realm.kotlin.internal.util.HEX_PATTERN
import io.realm.kotlin.internal.util.parseHex
import io.realm.kotlin.internal.util.toHexString
import io.realm.kotlin.types.RealmUUID
import org.mongodb.kbson.BsonBinary
import org.mongodb.kbson.BsonBinarySubType
import kotlin.experimental.and
import kotlin.experimental.or

Expand Down Expand Up @@ -101,3 +103,6 @@ public class RealmUUIDImpl : RealmUUID {
}
}
}

public inline fun RealmUUID.asBsonBinary(): BsonBinary = BsonBinary(BsonBinarySubType.UUID_STANDARD, bytes)
public inline fun BsonBinary.asRealmUUID(): RealmUUID = RealmUUID.from(data)
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ import kotlin.reflect.KClass
* associated [RealmObjectCompanion], in which case the `clazz` wasn't a user defined class
* implementing [BaseRealmObject] augmented by our compiler plugin.
*/
@PublishedApi
internal expect fun <T : Any> realmObjectCompanionOrNull(clazz: KClass<T>): RealmObjectCompanion?

/**
* Returns the [RealmObjectCompanion] associated with a given [BaseRealmObject]'s [KClass].
*/

@PublishedApi
internal expect fun <T : BaseRealmObject> realmObjectCompanionOrThrow(clazz: KClass<T>): RealmObjectCompanion
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ public class CachedClassMetadata(
properties = interopProperties.map { propertyInfo: PropertyInfo ->
CachedPropertyMetadata(
propertyInfo,
companion?.io_realm_kotlin_fields?.get(propertyInfo.name)
companion?.io_realm_kotlin_fields?.get(propertyInfo.name)?.second
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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.internal.schema

import io.realm.kotlin.internal.interop.CollectionType
import io.realm.kotlin.schema.ListPropertyType
import io.realm.kotlin.schema.MapPropertyType
import io.realm.kotlin.schema.RealmPropertyType
import io.realm.kotlin.schema.SetPropertyType
import io.realm.kotlin.schema.ValuePropertyType

public val RealmPropertyType.collectionType: CollectionType
get() {
return when (this) {
is ListPropertyType -> CollectionType.RLM_COLLECTION_TYPE_LIST
is MapPropertyType -> CollectionType.RLM_COLLECTION_TYPE_DICTIONARY
is SetPropertyType -> CollectionType.RLM_COLLECTION_TYPE_SET
is ValuePropertyType -> CollectionType.RLM_COLLECTION_TYPE_NONE
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public object Validation {
* to that type, otherwise an IllegalArgumentException is thrown with the provided error message.
*/
@OptIn(ExperimentalContracts::class)
public inline fun <reified T : Any?> isType(arg: Any?, errorMessage: String) {
public inline fun <reified T : Any?> isType(arg: Any?, errorMessage: String = "Object '$arg' is not of type ${T::class.qualifiedName}") {
contract {
returns() implies (arg is T)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import io.realm.kotlin.ext.asRealmObject
import io.realm.kotlin.ext.toRealmDictionary
import io.realm.kotlin.ext.toRealmList
import io.realm.kotlin.ext.toRealmSet
import io.realm.kotlin.internal.toDuration
import io.realm.kotlin.internal.toRealmInstant
import io.realm.kotlin.internal.asBsonDateTime
import io.realm.kotlin.internal.asRealmInstant
import io.realm.kotlin.types.MutableRealmInt
import io.realm.kotlin.types.RealmAny
import io.realm.kotlin.types.RealmAny.Type
Expand All @@ -46,7 +46,6 @@ import org.mongodb.kbson.BsonBinarySubType
import org.mongodb.kbson.BsonDateTime
import org.mongodb.kbson.BsonObjectId
import org.mongodb.kbson.Decimal128
import kotlin.time.Duration.Companion.milliseconds

/**
* KSerializer implementation for [RealmList]. Serialization is done as a generic list structure,
Expand Down Expand Up @@ -250,12 +249,12 @@ public object RealmInstantKSerializer : KSerializer<RealmInstant> {
override val descriptor: SerialDescriptor = serializer.descriptor

override fun deserialize(decoder: Decoder): RealmInstant =
decoder.decodeSerializableValue(serializer).value.milliseconds.toRealmInstant()
decoder.decodeSerializableValue(serializer).asRealmInstant()

override fun serialize(encoder: Encoder, value: RealmInstant) {
encoder.encodeSerializableValue(
serializer = serializer,
value = BsonDateTime(value.toDuration().inWholeMilliseconds)
value = value.asBsonDateTime()
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import io.realm.kotlin.internal.platform.currentTime
public interface RealmInstant : Comparable<RealmInstant> {

public companion object {
private const val SEC_AS_NANOSECOND: Int = 1_000_000_000
internal const val SEC_AS_NANOSECOND: Int = 1_000_000_000

/**
* Minimum timestamp that can be stored in Realm.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ import kotlin.reflect.full.companionObjectInstance

// TODO OPTIMIZE Can we eliminate the reflective approach? Maybe by embedding the information
// through the compiler plugin or something similar to the Native findAssociatedObject
@PublishedApi
internal actual fun <T : Any> realmObjectCompanionOrNull(clazz: KClass<T>): RealmObjectCompanion? =
if (clazz.companionObjectInstance is RealmObjectCompanion) {
clazz.companionObjectInstance as RealmObjectCompanion
} else null

@PublishedApi
internal actual fun <T : BaseRealmObject> realmObjectCompanionOrThrow(clazz: KClass<T>): RealmObjectCompanion =
realmObjectCompanionOrNull(clazz)
?: error("Couldn't find companion object of class '${clazz.simpleName}'.\nA common cause for this is when the `io.realm.kotlin` is not applied to the Gradle module that contains the '${clazz.simpleName}' class.")
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ import kotlin.reflect.ExperimentalAssociatedObjects
import kotlin.reflect.KClass
import kotlin.reflect.findAssociatedObject

@PublishedApi
internal actual fun <T : Any> realmObjectCompanionOrNull(clazz: KClass<T>): RealmObjectCompanion? =
@OptIn(ExperimentalAssociatedObjects::class)
when (val associatedObject = clazz.findAssociatedObject<ModelObject>()) {
is RealmObjectCompanion -> associatedObject
else -> null
}

@PublishedApi
internal actual fun <T : BaseRealmObject> realmObjectCompanionOrThrow(clazz: KClass<T>): RealmObjectCompanion =
realmObjectCompanionOrNull(clazz)
?: error("Couldn't find companion object of class '${clazz.simpleName}'.\nA common cause for this is when the `io.realm.kotlin` is not applied to the Gradle module that contains the '${clazz.simpleName}' class.")
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import io.realm.kotlin.mongodb.auth.ApiKeyAuth
import io.realm.kotlin.mongodb.exceptions.AppException
import io.realm.kotlin.mongodb.ext.customDataAsBsonDocument
import io.realm.kotlin.mongodb.ext.profileAsBsonDocument
import io.realm.kotlin.mongodb.mongo.MongoClient
import io.realm.kotlin.mongodb.sync.SyncConfiguration
import org.mongodb.kbson.ExperimentalKBsonSerializerApi
import org.mongodb.kbson.serialization.EJson

/**
* A **user** holds the user's metadata and tokens for accessing App Services and Device Sync
Expand Down Expand Up @@ -186,6 +189,25 @@ public interface User {
*/
public suspend fun linkCredentials(credentials: Credentials): User

/**
* Get a [MongoClient] for accessing documents from App Service's _Data Source_.
*
* Serialization to and from EJSON is performed with [KBSON](https://github.com/mongodb/kbson)
* that supports the [Kotlin Serialization framework](https://github.com/Kotlin/kotlinx.serialization)
* and handles serialization to and from classes marked with [Serializable]. Serialization of
* realm objects and links have some caveats and requires special configuration. For full
* details see [MongoClient].
*
* @param serviceName the name of the data service.
* @param eJson the EJson serializer that the [MongoClient] should use to convert objects and
* primary keys with. Will default to the app's [EJson] instance configured with
* [AppConfiguration.Builder.ejson]. For details on configuration of serialization see
* [MongoClient].
* throws IllegalStateException if trying to obtain a [MongoClient] from a logged out [User].
*/
@ExperimentalKBsonSerializerApi
public fun mongoClient(serviceName: String, eJson: EJson? = null): MongoClient

/**
* Two Users are considered equal if they have the same user identity and are associated
* with the same app.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import io.realm.kotlin.internal.interop.CodeDescription

/**
* This exception is considered the top-level or "catch-all" for problems related to HTTP requests
* made towards App Services. This covers both HTTP transport problems, problems passing JSON
* or the server considering the request invalid, for whatever reason.
* made towards App Services. This covers both HTTP transport problems, or the server considering
* the request invalid, for whatever reason.
*
* Generally, reacting to this exception will be hard, except to log the error for further
* analysis. But in many cases a more specific subtype will be thrown, which will be easier to
Expand All @@ -31,7 +31,7 @@ import io.realm.kotlin.internal.interop.CodeDescription
* @see BadRequestException
* @see AuthException
*/
public open class ServiceException internal constructor(
public open class ServiceException @PublishedApi internal constructor(
message: String,
internal val errorCode: CodeDescription? = null
) : AppException(message)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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.ext

import io.realm.kotlin.mongodb.internal.MongoClientCollection
import io.realm.kotlin.mongodb.internal.MongoClientImpl
import io.realm.kotlin.mongodb.mongo.MongoClient
import io.realm.kotlin.mongodb.mongo.MongoCollection
import io.realm.kotlin.types.BaseRealmObject
import org.mongodb.kbson.ExperimentalKBsonSerializerApi
import org.mongodb.kbson.serialization.EJson

/**
* Get a [MongoCollection] that exposes methods to retrieve and update data from the remote
* collection of objects of schema type [T].
*
* Serialization to and from EJSON is performed with [KBSON](https://github.com/mongodb/kbson)
* and requires to opt-in to the experimental [ExperimentalKBsonSerializerApi]-feature.
*
* @param eJson the EJson serializer that the [MongoCollection] should use to convert objects and
* primary keys with. Will default to the databases [EJson] instance.
* @param T the schema type indicating which for which remote entities of the collection will be
* serialized from and to.
* @return a [MongoCollection] that will accept and return entities from the remote collection
* as [T] values.
*/
@ExperimentalKBsonSerializerApi
public inline fun <reified T : BaseRealmObject> MongoClient.collection(eJson: EJson? = null): MongoCollection<T> {
@Suppress("invisible_reference", "invisible_member")
return MongoClientCollection(this as MongoClientImpl, io.realm.kotlin.internal.platform.realmObjectCompanionOrThrow(T::class).io_realm_kotlin_className, eJson ?: this.eJson)
}
Loading

0 comments on commit cd6cc63

Please sign in to comment.