From dc60f7f984e4232c6febd88e93b98ffeca9bfcdf Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Wed, 11 Oct 2023 08:59:26 +0200 Subject: [PATCH] Move Java MongoClient APIs to Kotlin and do initial conversion --- .../realm/kotlin/mongodb/mongo/MongoClient.kt | 40 ++ .../kotlin/mongodb/mongo/MongoCollection.kt | 548 ++++++++++++++++++ .../kotlin/mongodb/mongo/MongoDatabase.kt | 54 ++ .../kotlin/mongodb/mongo/MongoNamespace.kt | 170 ++++++ .../mongodb/mongo/events/BaseChangeEvent.kt | 107 ++++ .../mongodb/mongo/events/UpdateDescription.kt | 298 ++++++++++ .../mongo/iterable/AggregateIterable.kt | 56 ++ .../mongodb/mongo/iterable/FindIterable.kt | 140 +++++ .../mongodb/mongo/iterable/MongoCursor.kt | 50 ++ .../mongodb/mongo/iterable/MongoIterable.kt | 131 +++++ .../mongodb/mongo/options/CountOptions.kt | 32 + .../mongo/options/FindOneAndModifyOptions.kt | 57 ++ .../mongodb/mongo/options/FindOptions.kt | 49 ++ .../mongodb/mongo/options/InsertManyResult.kt | 29 + .../mongodb/mongo/options/UpdateOptions.kt | 32 + .../mongodb/mongo/result/DeleteResult.kt | 26 + .../mongodb/mongo/result/InsertOneResult.kt | 28 + .../mongodb/mongo/result/UpdateResult.kt | 37 ++ 18 files changed, 1884 insertions(+) create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/MongoClient.kt create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/MongoCollection.kt create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/MongoDatabase.kt create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/MongoNamespace.kt create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/events/BaseChangeEvent.kt create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/events/UpdateDescription.kt create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/iterable/AggregateIterable.kt create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/iterable/FindIterable.kt create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/iterable/MongoCursor.kt create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/iterable/MongoIterable.kt create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/options/CountOptions.kt create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/options/FindOneAndModifyOptions.kt create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/options/FindOptions.kt create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/options/InsertManyResult.kt create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/options/UpdateOptions.kt create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/result/DeleteResult.kt create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/result/InsertOneResult.kt create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/result/UpdateResult.kt diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/MongoClient.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/MongoClient.kt new file mode 100644 index 0000000000..daa0b6bf96 --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/MongoClient.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.kotlin.mongodb.mongo + +/** + * The remote MongoClient used for working with data in MongoDB remotely via Realm. + */ +public interface MongoClient { + + /** + * Gets a [MongoDatabase] instance for the given database name. + * + * @param databaseName the name of the database to retrieve + * @return a `RemoteMongoDatabase` representing the specified database + */ + public fun getDatabase(databaseName: String): MongoDatabase +// osMongoClient.getDatabase(databaseName, codecRegistry), +// databaseName + + /** + * Returns the service name for this client. + * + * @return the service name. + */ + public val serviceName: String + // return osMongoClient.getServiceName() +} diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/MongoCollection.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/MongoCollection.kt new file mode 100644 index 0000000000..befe649da2 --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/MongoCollection.kt @@ -0,0 +1,548 @@ +/* + * Copyright 2020 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.mongo + +import io.realm.kotlin.mongodb.mongo.options.FindOptions +import io.realm.mongodb.mongo.iterable.AggregateIterable +import io.realm.mongodb.mongo.iterable.FindIterable +import io.realm.kotlin.mongodb.mongo.options.CountOptions +import io.realm.kotlin.mongodb.mongo.options.FindOneAndModifyOptions +import io.realm.kotlin.mongodb.mongo.options.InsertManyResult +import io.realm.kotlin.mongodb.mongo.options.UpdateOptions +import io.realm.kotlin.mongodb.mongo.result.DeleteResult +import io.realm.kotlin.mongodb.mongo.result.InsertOneResult +import io.realm.kotlin.mongodb.mongo.result.UpdateResult +import org.mongodb.kbson.BsonDocument +import kotlin.reflect.KClass + +/** + * The MongoCollection interface provides read and write access to documents. + * + * Use [MongoDatabase.getCollection] to get a collection instance. + * + * Before any access is possible, there must be an active, logged-in user. + * + * @param The type that this collection will encode documents from and decode + * documents to. + * @see MongoDatabase + */ +public interface MongoCollection { + +// private val nameSpace: MongoNamespace +// private val osMongoCollection: OsMongoCollection + + /** + * Gets the namespace of this collection, i.e. the database and collection names together. + * + * @return the namespace + */ + public val namespace: MongoNamespace + + /** + * Gets the name of this collection + * + * @return the name of this collection + */ + public val name: String + // return nameSpace.getCollectionName() + + /** + * Gets the class of documents stored in this collection. + * + * If you used the simple [MongoDatabase.getCollection] to get this collection, + * this is [org.bson.Document]. + * + * @return the class of documents in this collection + */ + public val documentClass: KClass +// return osMongoCollection.getDocumentClass() + + // TODO Do not support CodecRegistry on Kotlin, but use Kotlin Serialization instead? + // fun getCodecRegistry(): CodecRegistry { + + /** + * Creates a new MongoCollection instance with a different default class to cast any + * documents returned from the database into. + * + * @param clazz the default class to which any documents returned from the database + * will be cast. + * @param The type that the new collection will encode documents from and decode + * documents to. + * @return a new MongoCollection instance with the different default class + */ + public fun withDocumentClass(clazz: KClass): MongoCollection + // return MongoCollection(nameSpace, osMongoCollection.withDocumentClass(clazz)) + + // TODO Do not support CodecRegistry on Kotlin, but use Kotlin Serialization instead? + // fun withCodecRegistry(codecRegistry: CodecRegistry?): MongoCollection { + + /** + * Counts the number of documents in the collection. + * + * @param filter an optional query filter + * @param options: optional options describing the count + * @return a task containing the number of documents in the collection + */ + public suspend fun count(filter: BsonDocument = BsonDocument(), options: CountOptions = CountOptions()): Long + + /** + * Finds a document in the collection. + * + * @param filter the query filter + * @param options a [FindOptions] struct + * + * @return a task containing the result of the find one operation + */ + public suspend fun findOne( + filter: BsonDocument = BsonDocument(), + options: FindOptions = FindOptions() + ): DocumentT + + /** + * Finds a document in the collection. + * + * @param filter the query filter + * @param options a [FindOptions] struct + * @param resultClass the class to decode each document into + * @param T the target document type + * @return a task containing the result of the find one operation + */ + public suspend fun findOne( + filter: BsonDocument = BsonDocument(), + options: FindOptions = FindOptions(), + // TODO Is there a good way to combine this method with the one above or otherwise make it easy to switch return result + resultClass: KClass + ): T + + /** + * Finds all documents in the collection. + * + * All documents will be delivered in the form of a [FindIterable] from which individual + * elements can be extracted. + * + * @param filter the query filter + * @param options a [FindOptions] struct + * @return an iterable containing the result of the find operation + */ + public suspend fun find( + filter: BsonDocument = BsonDocument(), + options: FindOptions = FindOptions() + ): FindIterable + + /** + * Finds all documents in the collection specifying an output class. + * + * All documents will be delivered in the form of a [FindIterable] from which individual + * elements can be extracted. + * + * @param filter the query filter + * @param options a [FindOptions] struct + * @param resultClass the class to decode each document into + * @param the target document type of the iterable. + * @return an iterable containing the result of the find operation + */ + public suspend fun find( + filter: BsonDocument = BsonDocument(), + options: FindOptions = FindOptions(), + // TODO Is there a good way to combine this method with the one above or otherwise make it easy to switch + resultClass: KClass + ): FindIterable + + /** + * Aggregates documents according to the specified aggregation pipeline. + * + * All documents will be delivered in the form of an [AggregateIterable] from which + * individual elements can be extracted. + * + * @param pipeline the aggregation pipeline + * @return an [AggregateIterable] from which the results can be extracted + */ + public suspend fun aggregate(pipeline: List): AggregateIterable + + /** + * Aggregates documents according to the specified aggregation pipeline specifying an output + * class. + * + * All documents will be delivered in the form of an [AggregateIterable] from which + * individual elements can be extracted. + * + * @param pipeline the aggregation pipeline + * @param resultClass the class to decode each document into + * @param the target document type of the iterable. + * @return an [AggregateIterable] from which the results can be extracted + */ + public suspend fun aggregate( + pipeline: List, + // TODO Is there a good way to combine this method with the one above or otherwise make it easy to switch + resultClass: KClass + ): AggregateIterable + + /** + * Inserts the provided document. If the document is missing an identifier, the client should + * generate one. + * + * @param document the document to insert + * @return a task containing the result of the insert one operation + */ + public suspend fun insertOne(document: DocumentT): InsertOneResult + + /** + * Inserts one or more documents. + * + * @param documents the documents to insert + * @return a task containing the result of the insert many operation + */ + public suspend fun insertMany(documents: List): InsertManyResult + + /** + * Removes at most one document from the collection that matches the given filter. If no + * documents match, the collection is not + * modified. + * + * @param filter the query filter to apply the the delete operation + * @return a task containing the result of the remove one operation + */ + public suspend fun deleteOne(filter: BsonDocument): DeleteResult + + /** + * Removes all documents from the collection that match the given query filter. If no documents + * match, the collection is not modified. + * + * @param filter the query filter to apply the the delete operation + * @return a task containing the result of the remove many operation + */ + public suspend fun deleteMany(filter: BsonDocument): DeleteResult + + /** + * Update a single document in the collection according to the specified arguments. + * + * @param filter a document describing the query filter, which may not be null. + * @param update a document describing the update, which may not be null. The update to + * apply must include only update operators. + * @param updateOptions the options to apply to the update operation + * @return a task containing the result of the update one operation + */ + public suspend fun updateOne( + filter: BsonDocument, + update: BsonDocument, + updateOptions: UpdateOptions = UpdateOptions() + ): UpdateResult + + /** + * Update all documents in the collection according to the specified arguments. + * + * @param filter a document describing the query filter, which may not be null. + * @param update a document describing the update, which may not be null. The update to + * apply must include only update operators. + * @param updateOptions the options to apply to the update operation + * @return a task containing the result of the update many operation + */ + public suspend fun updateMany( + filter: BsonDocument, + update: BsonDocument, + updateOptions: UpdateOptions = UpdateOptions() + ): UpdateResult + + /** + * Finds a document in the collection and performs the given update. + * + * @param filter the query filter + * @param update the update document + * @param options a [FindOneAndModifyOptions] struct + * @return a task containing the resulting document + */ + public suspend fun findOneAndUpdate( + filter: BsonDocument, + update: BsonDocument, + options: FindOneAndModifyOptions = FindOneAndModifyOptions() + ): DocumentT + + /** + * Finds a document in the collection and performs the given update. + * + * @param filter the query filter + * @param update the update document + * @param options a [FindOneAndModifyOptions] struct + * @param resultClass the class to decode each document into + * @param ResultT the target document type of the iterable. + * @return a task containing the resulting document + */ + public suspend fun findOneAndUpdate( + filter: BsonDocument, + update: BsonDocument, + options: FindOneAndModifyOptions = FindOneAndModifyOptions(), + // TODO Is there a good way to combine this method with the one above or otherwise make it easy to switch + resultClass: KClass + ): ResultT + + /** + * Finds a document in the collection and replaces it with the given document. + * + * @param filter the query filter + * @param replacement the document to replace the matched document with + * @param options a [FindOneAndModifyOptions] struct + * @return a task containing the resulting document + */ + public suspend fun findOneAndReplace( + filter: BsonDocument, + replacement: BsonDocument, + options: FindOneAndModifyOptions = FindOneAndModifyOptions() + ) : DocumentT + + /** + * Finds a document in the collection and replaces it with the given document. + * + * @param filter the query filter + * @param replacement the document to replace the matched document with + * @param resultClass the class to decode each document into + * @param options a [FindOneAndModifyOptions] struct + * @param ResultT the target document type of the iterable. + * @return a task containing the resulting document + */ + public suspend fun findOneAndReplace( + filter: BsonDocument, + replacement: BsonDocument, + options: FindOneAndModifyOptions = FindOneAndModifyOptions(), + // TODO Is there a good way to combine this method with the one above or otherwise make it easy to switch + resultClass: KClass + ): ResultT + + /** + * Finds a document in the collection and delete it. + * + * @param filter the query filter + * @return a task containing the resulting document + */ + public suspend fun findOneAndDelete( + filter: BsonDocument = BsonDocument(), + options: FindOneAndModifyOptions = FindOneAndModifyOptions(), + ): DocumentT + + /** + * Finds a document in the collection and delete it. + * + * @param filter the query filter + * @param options a [FindOneAndModifyOptions] struct + * @param resultClass the class to decode each document into + * @param the target document type of the iterable. + * @return a task containing the resulting document + */ + public suspend fun findOneAndDelete( + filter: BsonDocument = BsonDocument(), + options: FindOneAndModifyOptions = FindOneAndModifyOptions(), + // TODO Is there a good way to combine this method with the one above or otherwise make it easy to switch + resultClass: KClass + ): ResultT + + // TODO Figure out if we should support watch +// /** +// * Watches a collection. The resulting stream will be notified of all events on this collection +// * that the active user is authorized to see based on the configured MongoDB Realm rules. +// * +// * @return a task that provides access to the stream of change events. +// */ +// fun watch(): RealmEventStreamTask { +// return RealmEventStreamTaskImpl(getNamespace().getFullName(), +// object : Executor() { +// @Throws(java.io.IOException::class) +// fun run(): EventStream { +// return osMongoCollection.watch() +// } +// }) +// } +// +// /** +// * Watches specified IDs in a collection. +// * +// * @param ids the ids to watch. +// * @return a task that provides access to the stream of change events. +// */ +// fun watch(vararg ids: BsonValue?): RealmEventStreamTask { +// return RealmEventStreamTaskImpl(getNamespace().getFullName(), +// object : Executor() { +// @Throws(java.io.IOException::class) +// fun run(): EventStream { +// return osMongoCollection.watch(java.util.Arrays.asList(*ids)) +// } +// }) +// } +// +// /** +// * Watches specified IDs in a collection. This convenience overload supports the use case +// * of non-[BsonValue] instances of [ObjectId] by wrapping them in +// * [BsonObjectId] instances for the user. +// * +// * @param ids unique object identifiers of the IDs to watch. +// * @return a task that provides access to the stream of change events. +// */ +// fun watch(vararg ids: ObjectId?): RealmEventStreamTask { +// return RealmEventStreamTaskImpl(getNamespace().getFullName(), +// object : Executor() { +// @Throws(java.io.IOException::class) +// fun run(): EventStream { +// return osMongoCollection.watch(java.util.Arrays.asList(*ids)) +// } +// }) +// } +// +// /** +// * Watches a collection. The provided document will be used as a match expression filter on +// * the change events coming from the stream. This convenience overload supports the use of +// * non-[BsonDocument] instances for the user. +// * +// * +// * See [how to define a match filter](https://docs.mongodb.com/manual/reference/operator/aggregation/match/). +// * +// * +// * Defining the match expression to filter ChangeEvents is similar to +// * [how to define the match expression for triggers](https://docs.mongodb.com/realm/triggers/database-triggers/) +// * +// * @param matchFilter the $match filter to apply to incoming change events +// * @return a task that provides access to the stream of change events. +// */ +// fun watchWithFilter(matchFilter: Document?): RealmEventStreamTask { +// return RealmEventStreamTaskImpl(getNamespace().getFullName(), +// object : Executor() { +// @Throws(java.io.IOException::class) +// fun run(): EventStream { +// return osMongoCollection.watchWithFilter(matchFilter) +// } +// }) +// } +// +// /** +// * Watches a collection. The provided BSON document will be used as a match expression filter on +// * the change events coming from the stream. +// * +// * +// * See [how to define a match filter](https://docs.mongodb.com/manual/reference/operator/aggregation/match/). +// * +// * +// * Defining the match expression to filter ChangeEvents is similar to +// * [how to define the match expression for triggers](https://docs.mongodb.com/realm/triggers/database-triggers/) +// * +// * @param matchFilter the $match filter to apply to incoming change events +// * @return a task that provides access to the stream of change events. +// */ +// fun watchWithFilter(matchFilter: BsonDocument?): RealmEventStreamTask { +// return RealmEventStreamTaskImpl(getNamespace().getFullName(), +// object : Executor() { +// @Throws(java.io.IOException::class) +// fun run(): EventStream { +// return osMongoCollection.watchWithFilter(matchFilter) +// } +// }) +// } +// +// /** +// * Watches a collection asynchronously. The resulting stream will be notified of all events on this collection +// * that the active user is authorized to see based on the configured MongoDB Realm rules. +// * +// * @return a task that provides access to the stream of change events. +// */ +// fun watchAsync(): RealmEventStreamAsyncTask { +// return RealmEventStreamAsyncTaskImpl(getNamespace().getFullName(), +// object : Executor() { +// @Throws(java.io.IOException::class) +// fun run(): EventStream { +// return osMongoCollection.watch() +// } +// }) +// } +// +// /** +// * Watches specified IDs in a collection asynchronously. +// * +// * @param ids the ids to watch. +// * @return a task that provides access to the stream of change events. +// */ +// fun watchAsync(vararg ids: BsonValue?): RealmEventStreamAsyncTask { +// return RealmEventStreamAsyncTaskImpl(getNamespace().getFullName(), +// object : Executor() { +// @Throws(java.io.IOException::class) +// fun run(): EventStream { +// return osMongoCollection.watch(java.util.Arrays.asList(*ids)) +// } +// }) +// } +// +// /** +// * Watches specified IDs in a collection asynchronously. This convenience overload supports the use case +// * of non-[BsonValue] instances of [ObjectId] by wrapping them in +// * [BsonObjectId] instances for the user. +// * +// * @param ids unique object identifiers of the IDs to watch. +// * @return a task that provides access to the stream of change events. +// */ +// fun watchAsync(vararg ids: ObjectId?): RealmEventStreamAsyncTask { +// return RealmEventStreamAsyncTaskImpl(getNamespace().getFullName(), +// object : Executor() { +// @Throws(java.io.IOException::class) +// fun run(): EventStream { +// return osMongoCollection.watch(java.util.Arrays.asList(*ids)) +// } +// }) +// } +// +// /** +// * Watches a collection asynchronously. The provided document will be used as a match expression filter on +// * the change events coming from the stream. This convenience overload supports the use of +// * non-[BsonDocument] instances for the user. +// * +// * +// * See [how to define a match filter](https://docs.mongodb.com/manual/reference/operator/aggregation/match/). +// * +// * +// * Defining the match expression to filter ChangeEvents is similar to +// * [how to define the match expression for triggers](https://docs.mongodb.com/realm/triggers/database-triggers/) +// * +// * @param matchFilter the $match filter to apply to incoming change events +// * @return a task that provides access to the stream of change events. +// */ +// fun watchWithFilterAsync(matchFilter: Document?): RealmEventStreamAsyncTask { +// return RealmEventStreamAsyncTaskImpl(getNamespace().getFullName(), +// object : Executor() { +// @Throws(java.io.IOException::class) +// fun run(): EventStream { +// return osMongoCollection.watchWithFilter(matchFilter) +// } +// }) +// } +// +// /** +// * Watches a collection asynchronously. The provided BSON document will be used as a match expression filter on +// * the change events coming from the stream. +// * +// * +// * See [how to define a match filter](https://docs.mongodb.com/manual/reference/operator/aggregation/match/). +// * +// * +// * Defining the match expression to filter ChangeEvents is similar to +// * [how to define the match expression for triggers](https://docs.mongodb.com/realm/triggers/database-triggers/) +// * +// * @param matchFilter the $match filter to apply to incoming change events +// * @return a task that provides access to the stream of change events. +// */ +// fun watchWithFilterAsync(matchFilter: BsonDocument?): RealmEventStreamAsyncTask { +// return RealmEventStreamAsyncTaskImpl(getNamespace().getFullName(), +// object : Executor() { +// @Throws(java.io.IOException::class) +// fun run(): EventStream { +// return osMongoCollection.watchWithFilter(matchFilter) +// } +// }) +// } +} diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/MongoDatabase.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/MongoDatabase.kt new file mode 100644 index 0000000000..d3a30a75ca --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/MongoDatabase.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2020 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.mongo + +import org.mongodb.kbson.BsonDocument +import kotlin.reflect.KClass + +/** + * The RemoteMongoDatabase provides access to its [BsonDocument] [MongoCollection]s. + */ +public interface MongoDatabase { + + /** + * Gets the name of the database. + * + * @return the database name + */ + public val name: String + + /** + * Gets a collection. + * + * @param collectionName the name of the collection to return + * @return the collection + */ + public fun getCollection(collectionName: String): MongoCollection + + /** + * Gets a collection, with a specific default document class. + * + * @param collectionName the name of the collection to return + * @param documentClass the default class to cast any documents returned from the database into. + * @param DocumentT the type of the class to use instead of `Document`. + * @return the collection + */ + public fun getCollection( + collectionName: String, + // TODO Is there a good way to combine this method with the one above or otherwise make it easy to switch + documentClass: KClass + ): MongoCollection +} diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/MongoNamespace.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/MongoNamespace.kt new file mode 100644 index 0000000000..3fa652e140 --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/MongoNamespace.kt @@ -0,0 +1,170 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.kotlin.mongodb.mongo + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient + +/** + * TODO This class needs to be supported by Kotlin Serialization + * A MongoDB namespace, which includes a database name and collection name. + */ +@Serializable +public class MongoNamespace { + + /** + * Construct an instance for the given full name. The database name is the string preceding the first `"."` character. + * + * @param fullName the non-null full namespace + * @see .checkDatabaseNameValidity + * @see .checkCollectionNameValidity + */ + public constructor(fullName: String) { + this.fullName = fullName + databaseName = getDatatabaseNameFromFullName(fullName) + collectionName = getCollectionNameFullName(fullName) + checkDatabaseNameValidity(databaseName) + checkCollectionNameValidity(collectionName) + } + + /** + * Construct an instance from the given database name and collection name. + * + * @param databaseName the valid database name + * @param collectionName the valid collection name + * @see .checkDatabaseNameValidity + * @see .checkCollectionNameValidity + */ + public constructor( + databaseName: String, + collectionName: String + ) { + checkDatabaseNameValidity(databaseName) + checkCollectionNameValidity(collectionName) + this.databaseName = databaseName + this.collectionName = collectionName + fullName = "$databaseName.$collectionName" + } + + /** + * Gets the database name. + * + * @return the database name + */ + @SerialName("db") + public val databaseName: String + + /** + * Gets the collection name. + * + * @return the collection name + */ + @SerialName("coll") + public val collectionName: String + + /** + * Gets the full name, which is the database name and the collection name, separated by a period. + * + * @return the full name + */ + @Transient + public val fullName: String + + + + /** + * Returns the standard MongoDB representation of a namespace, which is `<database>.<collection>`. + * + * @return string representation of the namespace. + */ + override fun toString(): String { + return fullName + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as MongoNamespace + + if (databaseName != other.databaseName) return false + return collectionName == other.collectionName + } + + override fun hashCode(): Int { + var result = databaseName.hashCode() + result = 31 * result + collectionName.hashCode() + return result + } + + private companion object { + private const val COMMAND_COLLECTION_NAME = "\$cmd" + private val PROHIBITED_CHARACTERS_IN_DATABASE_NAME: Set = setOf( + '\u0000', '/', '\\', ' ', '"', '.' + ) + + /** + * Check the validity of the given database name. A valid database name is non-null, non-empty, and does not contain any of the + * following characters: `'\0', '/', '\\', ' ', '"', '.'`. The server may impose additional restrictions on database names. + * + * @param databaseName the database name + * @throws IllegalArgumentException if the database name is invalid + */ + private fun checkDatabaseNameValidity(databaseName: String) { + isTrueArgument("databaseName is not empty", databaseName.isNotEmpty()) + for (i in databaseName.indices) { + isTrueArgument( + "databaseName does not contain '" + databaseName[i] + "'", + !PROHIBITED_CHARACTERS_IN_DATABASE_NAME.contains( + databaseName[i] + ) + ) + } + } + + /** + * Check the validity of the given collection name. A valid collection name is non-null and non-empty. The server may impose + * additional restrictions on collection names. + * + * @param collectionName the collection name + * @throws IllegalArgumentException if the collection name is invalid + */ + private fun checkCollectionNameValidity(collectionName: String) { + isTrueArgument("collectionName is not empty", !collectionName.isEmpty()) + } + + private fun getCollectionNameFullName(namespace: String): String { + val firstDot = namespace.indexOf('.') + return if (firstDot == -1) { + namespace + } else namespace.substring(firstDot + 1) + } + + private fun getDatatabaseNameFromFullName(namespace: String): String { + val firstDot = namespace.indexOf('.') + return if (firstDot == -1) { + "" + } else namespace.substring(0, firstDot) + } + + private fun isTrueArgument(name: String, condition: Boolean) { + if (!condition) { + throw IllegalArgumentException("state should be: $name") + } + } + } +} diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/events/BaseChangeEvent.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/events/BaseChangeEvent.kt new file mode 100644 index 0000000000..37c7d71c67 --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/events/BaseChangeEvent.kt @@ -0,0 +1,107 @@ +// TODO Figure out if we should support watch +///* +// * Copyright 2020 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.mongodb.mongo.events +// +//import org.bson.BsonDocument +// +///** +// * Represents the set of properties that exist on all MongoDB realm change events produced +// * by watch streams in this SDK. Other change event types inherit from this type. +// * +// * @param The type of the full document in the change event. +// */ +//abstract class BaseChangeEvent protected constructor( +// /** +// * Returns the operation type of the change that triggered the change event. +// * +// * @return the operation type of this change event. +// */ +// val operationType: OperationType, +// @field:Nullable +// /** +// * The full document at some point after the change has been applied. +// * +// * @return the full document. +// */ +// @get:Nullable +// @param:Nullable val fullDocument: DocumentT, +// documentKey: BsonDocument, +// @Nullable updateDescription: UpdateDescription?, +// hasUncommittedWrites: Boolean +//) { +// +// private val documentKey: BsonDocument +// +// @Nullable +// private val updateDescription: UpdateDescription +// private val hasUncommittedWrites: Boolean +// +// /** +// * The unique identifier for the document that was actually changed. +// * +// * @return the document key. +// */ +// fun getDocumentKey(): BsonDocument { +// return documentKey +// } +// +// /** +// * In the case of an update, the description of which fields have been added, removed or updated. +// * +// * @return the update description. +// */ +// @Nullable +// fun getUpdateDescription(): UpdateDescription { +// return updateDescription +// } +// +// /** +// * Indicates a local change event that has not yet been synchronized with a remote data store. +// * Used only for the sync use case. +// * +// * @return whether or not this change event represents uncommitted writes. +// */ +// fun hasUncommittedWrites(): Boolean { +// return hasUncommittedWrites +// } +// +// init { +// this.documentKey = documentKey +// this.updateDescription = +// if (updateDescription == null) UpdateDescription(null, null) else updateDescription +// this.hasUncommittedWrites = hasUncommittedWrites +// } +// +// /** +// * Converts the change event to a BSON representation, as it would look on a MongoDB realm change +// * stream, or a Realm compact watch stream. +// * +// * @return The BSON document representation of the change event. +// */ +// abstract fun toBsonDocument(): BsonDocument? +// +// /** +// * Represents the different MongoDB operations that can occur. +// */ +// enum class OperationType { +// INSERT, +// DELETE, +// REPLACE, +// UPDATE, +// UNKNOWN +// } +//} diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/events/UpdateDescription.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/events/UpdateDescription.kt new file mode 100644 index 0000000000..452eb54ca9 --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/events/UpdateDescription.kt @@ -0,0 +1,298 @@ +// TODO Figure out if we should support watch +///* +// * Copyright 2020 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.mongodb.mongo.events +// +//import io.realm.internal.Util.checkContainsKey +//import io.realm.mongodb.AppException +//import io.realm.mongodb.ErrorCode +//import org.bson.BsonArray +//import org.bson.BsonBoolean +//import org.bson.BsonDocument +//import org.bson.BsonElement +//import org.bson.BsonString +//import org.bson.BsonValue +// +///** +// * Indicates which fields have been modified in a given update operation. +// */ +//class UpdateDescription internal constructor( +// updatedFields: BsonDocument?, +// removedFields: Collection? +//) { +// private val updatedFields: BsonDocument +// private val removedFields: MutableSet +// +// /** +// * Creates an update description with the specified updated fields and removed field names. +// * +// * @param updatedFields Nested key-value pair representation of updated fields. +// * @param removedFields Collection of removed field names. +// */ +// init { +// this.updatedFields = if (updatedFields == null) BsonDocument() else updatedFields +// this.removedFields = +// if (removedFields == null) java.util.HashSet() else java.util.HashSet( +// removedFields +// ) +// } +// +// /** +// * Returns a [BsonDocument] containing keys and values representing (respectively) the +// * fields that have changed in the corresponding update and their new values. +// * +// * @return the updated field names and their new values. +// */ +// fun getUpdatedFields(): BsonDocument { +// return updatedFields +// } +// +// /** +// * Returns a [List] containing the field names that have been removed in the corresponding +// * update. +// * +// * @return the removed fields names. +// */ +// fun getRemovedFields(): Collection { +// return removedFields +// } +// +// /** +// * Convert this update description to an update document. +// * +// * @return an update document with the appropriate $set and $unset documents. +// */ +// fun toUpdateDocument(): BsonDocument { +// val unsets: MutableList = java.util.ArrayList() +// for (removedField in removedFields) { +// unsets.add(BsonElement(removedField, BsonBoolean(true))) +// } +// val updateDocument = BsonDocument() +// if (updatedFields.size() > 0) { +// updateDocument.append("\$set", updatedFields) +// } +// if (unsets.size > 0) { +// updateDocument.append("\$unset", BsonDocument(unsets)) +// } +// return updateDocument +// } +// +// /** +// * Converts this update description to its document representation as it would appear in a +// * MongoDB Change Event. +// * +// * @return the update description document as it would appear in a change event +// */ +// fun toBsonDocument(): BsonDocument { +// val updateDescDoc = BsonDocument() +// updateDescDoc.put( +// Fields.UPDATED_FIELDS_FIELD, +// getUpdatedFields() +// ) +// val removedFields = BsonArray() +// for (field in getRemovedFields()) { +// removedFields.add(BsonString(field)) +// } +// updateDescDoc.put( +// Fields.REMOVED_FIELDS_FIELD, +// removedFields +// ) +// return updateDescDoc +// } +// +// /** +// * Unilaterally merge an update description into this update description. +// * +// * @param otherDescription the update description to merge into this +// * @return this merged update description +// */ +// fun merge(@Nullable otherDescription: UpdateDescription?): UpdateDescription { +// if (otherDescription != null) { +// for ((key) in updatedFields.entrySet()) { +// if (otherDescription.removedFields.contains(key)) { +// updatedFields.remove(key) +// } +// } +// for (removedField in removedFields) { +// if (otherDescription.updatedFields.containsKey(removedField)) { +// removedFields.remove(removedField) +// } +// } +// removedFields.addAll(otherDescription.removedFields) +// updatedFields.putAll(otherDescription.updatedFields) +// } +// return this +// } +// +// val isEmpty: Boolean +// /** +// * Determines whether this update description is empty. +// * +// * @return true if the update description is empty, false otherwise +// */ +// get() = updatedFields.isEmpty() && removedFields.isEmpty() +// +// override fun equals(obj: Any?): Boolean { +// if (obj == null || obj.javaClass != UpdateDescription::class.java) { +// return false +// } +// val other = obj as UpdateDescription +// return other.getRemovedFields() == removedFields && other.getUpdatedFields().equals( +// updatedFields +// ) +// } +// +// override fun hashCode(): Int { +// return removedFields.hashCode() + 31 * updatedFields.hashCode() +// } +// +// private object Fields { +// const val UPDATED_FIELDS_FIELD = "updatedFields" +// const val REMOVED_FIELDS_FIELD = "removedFields" +// } +// +// companion object { +// private const val DOCUMENT_VERSION_FIELD = "__stitch_sync_version" +// +// /** +// * Converts an update description BSON document from a MongoDB Change Event into an +// * UpdateDescription object. +// * +// * @param document the +// * @return the converted UpdateDescription +// */ +// fun fromBsonDocument(document: BsonDocument): UpdateDescription { +// try { +// checkContainsKey(Fields.UPDATED_FIELDS_FIELD, document, "document") +// checkContainsKey(Fields.REMOVED_FIELDS_FIELD, document, "document") +// } catch (exception: java.lang.IllegalArgumentException) { +// throw AppException(ErrorCode.EVENT_DESERIALIZING, exception) +// } +// val removedFieldsArr: BsonArray = document.getArray(Fields.REMOVED_FIELDS_FIELD) +// val removedFields: MutableSet = java.util.HashSet(removedFieldsArr.size()) +// for (field in removedFieldsArr) { +// removedFields.add(field.asString().getValue()) +// } +// return UpdateDescription( +// document.getDocument(Fields.UPDATED_FIELDS_FIELD), +// removedFields +// ) +// } +// +// /** +// * Find the diff between two documents. +// * +// * +// * NOTE: This does not do a full diff on [BsonArray]. If there is +// * an inequality between the old and new array, the old array will +// * simply be replaced by the new one. +// * +// * @param beforeDocument original document +// * @param afterDocument document to diff on +// * @param onKey the key for our depth level +// * @param updatedFields contiguous document of updated fields, +// * nested or otherwise +// * @param removedFields contiguous list of removedFields, +// * nested or otherwise +// * @return a description of the updated fields and removed keys between the documents +// */ +// private fun diff( +// beforeDocument: BsonDocument, +// afterDocument: BsonDocument, +// @Nullable onKey: String?, +// updatedFields: BsonDocument, +// removedFields: MutableSet +// ): UpdateDescription { +// // for each key in this document... +// for ((key, oldValue) in beforeDocument.entrySet()) { +// // don't worry about the _id or version field for now +// if (key == "_id" || key == DOCUMENT_VERSION_FIELD) { +// continue +// } +// val actualKey = if (onKey == null) key else String.format("%s.%s", onKey, key) +// // if the key exists in the other document AND both are BsonDocuments +// // diff the documents recursively, carrying over the keys to keep +// // updatedFields and removedFields flat. +// // this will allow us to reference whole objects as well as nested +// // properties. +// // else if the key does not exist, the key has been removed. +// if (afterDocument.containsKey(key)) { +// val newValue: BsonValue = afterDocument.get(key) +// if (oldValue is BsonDocument && newValue is BsonDocument) { +// diff( +// oldValue as BsonDocument, +// newValue as BsonDocument, +// actualKey, +// updatedFields, +// removedFields +// ) +// } else if (!oldValue.equals(newValue)) { +// updatedFields.put(actualKey, newValue) +// } +// } else { +// removedFields.add(actualKey) +// } +// } +// +// // for each key in the other document... +// for ((key, newValue) in afterDocument.entrySet()) { +// // don't worry about the _id or version field for now +// if (key == "_id" || key == DOCUMENT_VERSION_FIELD) { +// continue +// } +// // if the key is not in the this document, +// // it is a new key with a new value. +// // updatedFields will included keys that must +// // be newly created. +// val actualKey = if (onKey == null) key else String.format("%s.%s", onKey, key) +// if (!beforeDocument.containsKey(key)) { +// updatedFields.put(actualKey, newValue) +// } +// } +// return UpdateDescription(updatedFields, removedFields) +// } +// +// /** +// * Find the diff between two documents. +// * +// * +// * NOTE: This does not do a full diff on [BsonArray]. If there is +// * an inequality between the old and new array, the old array will +// * simply be replaced by the new one. +// * +// * @param beforeDocument original document +// * @param afterDocument document to diff on +// * @return a description of the updated fields and removed keys between the documents. +// */ +// fun diff( +// @Nullable beforeDocument: BsonDocument?, +// @Nullable afterDocument: BsonDocument? +// ): UpdateDescription { +// return if (beforeDocument == null || afterDocument == null) { +// UpdateDescription( +// BsonDocument(), +// java.util.HashSet() +// ) +// } else diff( +// beforeDocument, +// afterDocument, +// null, +// BsonDocument(), +// java.util.HashSet() +// ) +// } +// } +//} diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/iterable/AggregateIterable.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/iterable/AggregateIterable.kt new file mode 100644 index 0000000000..84128ee5ce --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/iterable/AggregateIterable.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2020 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.mongodb.mongo.iterable + +import io.realm.internal.jni.JniBsonProtocol +import io.realm.internal.network.NetworkRequest +import io.realm.internal.objectstore.OsJavaNetworkTransport +import io.realm.internal.objectstore.OsMongoCollection +import org.bson.codecs.configuration.CodecRegistry +import org.bson.conversions.Bson +import org.mongodb.kbson.serialization.Bson + +/** + * Specific iterable for [io.realm.mongodb.mongo.MongoCollection.aggregate] operations. + * + * @param The type to which this iterable will decode documents. + */ +class AggregateIterable( + threadPoolExecutor: java.util.concurrent.ThreadPoolExecutor?, + osMongoCollection: OsMongoCollection<*>, + codecRegistry: CodecRegistry?, + resultClass: java.lang.Class, + pipeline: List +) : MongoIterable(threadPoolExecutor, osMongoCollection, codecRegistry, resultClass) { + private val pipeline: List + + init { + this.pipeline = pipeline + } + + override fun callNative(callback: NetworkRequest<*>) { + val pipelineString: String = JniBsonProtocol.encode(pipeline, codecRegistry) + nativeAggregate(osMongoCollection.getNativePtr(), pipelineString, callback) + } + + companion object { + private external fun nativeAggregate( + remoteMongoCollectionPtr: Long, + pipeline: String, + callback: OsJavaNetworkTransport.NetworkTransportJNIResultCallback + ) + } +} diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/iterable/FindIterable.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/iterable/FindIterable.kt new file mode 100644 index 0000000000..889788e73f --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/iterable/FindIterable.kt @@ -0,0 +1,140 @@ +/* + * Copyright 2020 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.mongodb.mongo.iterable + +import io.realm.internal.jni.JniBsonProtocol +import io.realm.internal.network.NetworkRequest +import io.realm.internal.objectstore.OsJavaNetworkTransport +import io.realm.internal.objectstore.OsMongoCollection +import io.realm.kotlin.mongodb.mongo.options.FindOptions +import org.bson.Document +import org.bson.codecs.configuration.CodecRegistry +import org.bson.conversions.Bson +import org.mongodb.kbson.serialization.Bson + +// TODO Unclear exactly how much this needs to change? Should we also expose this as an `FindFlow()` +// similar to the coroutine kotlin driver: https://github.com/mongodb/mongo-java-driver/blob/master/driver-kotlin-coroutine/src/main/kotlin/com/mongodb/kotlin/client/coroutine/MongoCollection.kt#L285C73-L285C81 +// or should we expose both? + +/** + * Specific iterable for [io.realm.mongodb.mongo.MongoCollection.find] operations. + * + * @param The type to which this iterable will decode documents. + */ +class FindIterable( + threadPoolExecutor: java.util.concurrent.ThreadPoolExecutor?, + osMongoCollection: OsMongoCollection<*>, + codecRegistry: CodecRegistry?, + resultClass: java.lang.Class +) : MongoIterable(threadPoolExecutor, osMongoCollection, codecRegistry, resultClass) { + private val options: FindOptions? + private val encodedEmptyDocument: String + private var filter: Bson + + init { + options = FindOptions() + filter = Document() + encodedEmptyDocument = JniBsonProtocol.encode(Document(), codecRegistry) + } + + override fun callNative(callback: NetworkRequest<*>) { + val filterString: String = JniBsonProtocol.encode(filter, codecRegistry) + var projectionString = encodedEmptyDocument + var sortString = encodedEmptyDocument + if (options == null) { + nativeFind( + FIND, + osMongoCollection.getNativePtr(), + filterString, + projectionString, + sortString, + 0, + callback + ) + } else { + projectionString = JniBsonProtocol.encode(options.getProjection(), codecRegistry) + sortString = JniBsonProtocol.encode(options.getSort(), codecRegistry) + nativeFind( + FIND_WITH_OPTIONS, + osMongoCollection.getNativePtr(), + filterString, + projectionString, + sortString, + options.getLimit() + .toLong(), + callback + ) + } + } + + /** + * Sets the query filter to apply to the query. + * + * @param filter the filter, which may be null. + * @return this + */ + fun filter(@Nullable filter: Bson): FindIterable { + this.filter = filter + return this + } + + /** + * Sets the limit to apply. + * + * @param limit the limit, which may be 0 + * @return this + */ + fun limit(limit: Int): FindIterable { + options!!.limit(limit) + return this + } + + /** + * Sets a document describing the fields to return for all matching documents. + * + * @param projection the project document, which may be null. + * @return this + */ + fun projection(@Nullable projection: Bson): FindIterable { + options!!.projection(projection) + return this + } + + /** + * Sets the sort criteria to apply to the query. + * + * @param sort the sort criteria, which may be null. + * @return this + */ + fun sort(@Nullable sort: Bson): FindIterable { + options!!.sort(sort) + return this + } + + companion object { + private const val FIND = 1 + private const val FIND_WITH_OPTIONS = 2 + private external fun nativeFind( + findType: Int, + remoteMongoCollectionPtr: Long, + filter: String, + projection: String, + sort: String, + limit: Long, + callback: OsJavaNetworkTransport.NetworkTransportJNIResultCallback + ) + } +} diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/iterable/MongoCursor.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/iterable/MongoCursor.kt new file mode 100644 index 0000000000..7abc0d4359 --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/iterable/MongoCursor.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2020 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.mongodb.mongo.iterable + +/** + * The Mongo Cursor class is fundamentally an [Iterator] containing an additional + * `tryNext()` method for convenience. + * + * + * An application should ensure that a cursor is closed in all circumstances, e.g. using a + * try-with-resources statement. + * + * @param The type of documents the cursor contains + */ +class MongoCursor internal constructor(private val iterator: Iterator) : + MutableIterator, java.io.Closeable { + override fun hasNext(): Boolean { + return iterator.hasNext() + } + + override fun next(): ResultT { + return iterator.next() + } + + /** + * A special `next()` case that returns the next document if available or null. + * + * @return A `Task` containing the next document if available or null. + */ + fun tryNext(): ResultT? { + return if (!iterator.hasNext()) { + null + } else iterator.next() + } + + public override fun close() {} +} diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/iterable/MongoIterable.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/iterable/MongoIterable.kt new file mode 100644 index 0000000000..fbbe8e1ada --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/iterable/MongoIterable.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2020 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.mongodb.mongo.iterable + +import io.realm.internal.async.RealmResultTaskImpl +import io.realm.internal.jni.JniBsonProtocol +import io.realm.internal.network.NetworkRequest +import io.realm.internal.objectstore.OsMongoCollection +import io.realm.mongodb.RealmResultTask +import org.bson.codecs.configuration.CodecRegistry + +// TODO + +/** + * The MongoIterable is the results from an operation, such as a `find()` or an + * `aggregate()` query. + * + * + * This class somewhat mimics the behavior of an [Iterable] but given its results are + * obtained asynchronously, its values are wrapped inside a `Task`. + * + * @param The type to which this iterable will decode documents. + */ +abstract class MongoIterable internal constructor( + threadPoolExecutor: java.util.concurrent.ThreadPoolExecutor?, + osMongoCollection: OsMongoCollection<*>, + codecRegistry: CodecRegistry?, + resultClass: java.lang.Class +) { + protected val osMongoCollection: OsMongoCollection<*> + protected val codecRegistry: CodecRegistry? + private val resultClass: java.lang.Class + private val threadPoolExecutor: java.util.concurrent.ThreadPoolExecutor? + + init { + this.threadPoolExecutor = threadPoolExecutor + this.osMongoCollection = osMongoCollection + this.codecRegistry = codecRegistry + this.resultClass = resultClass + } + + abstract fun callNative(callback: NetworkRequest<*>?) + + /** + * Returns a cursor of the operation represented by this iterable. + * + * + * The result is wrapped in a `Task` since the iterator should be capable of + * asynchronously retrieve documents from the server. + * + * @return an asynchronous task with cursor of the operation represented by this iterable. + */ + operator fun iterator(): RealmResultTask> { + return RealmResultTaskImpl(threadPoolExecutor, object : Executor?>() { + @Nullable + fun run(): MongoCursor { + return MongoCursor(collection.iterator()) + } + }) + } + + /** + * Helper to return the first item in the iterator or null. + * + * + * The result is wrapped in a `Task` since the iterator should be capable of + * asynchronously retrieve documents from the server. + * + * @return a task containing the first item or null. + */ + fun first(): RealmResultTask { + val task: NetworkRequest = object : NetworkRequest() { + protected fun mapSuccess(result: Any): ResultT? { + val decodedCollection = mapCollection(result) + val iter = decodedCollection.iterator() + return if (iter.hasNext()) iter.next() else null + } + + protected fun execute(callback: NetworkRequest?) { + callNative(callback) + } + } + return RealmResultTaskImpl(threadPoolExecutor, object : Executor() { + @Nullable + fun run(): ResultT { + return task.resultOrThrow() + } + }) + } + + private val collection: Collection + private get() = object : NetworkRequest?>() { + protected fun mapSuccess(result: Any): Collection { + return mapCollection(result) + } + + protected fun execute(callback: NetworkRequest?>?) { + callNative(callback) + } + }.resultOrThrow() + + private fun mapCollection(result: Any): Collection { + val collection: Collection<*> = + JniBsonProtocol.decode(result as String, MutableCollection::class.java, codecRegistry) + val decodedCollection: MutableCollection = java.util.ArrayList() + for (collectionElement in collection) { + val encodedElement: String = JniBsonProtocol.encode(collectionElement, codecRegistry) + decodedCollection.add( + JniBsonProtocol.decode( + encodedElement, + resultClass, + codecRegistry + ) + ) + } + return decodedCollection + } +} diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/options/CountOptions.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/options/CountOptions.kt new file mode 100644 index 0000000000..05c84891e7 --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/options/CountOptions.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.kotlin.mongodb.mongo.options + +/** + * The options for a count operation. + */ +public data class CountOptions( + /** + * The limit to apply. The default is 0, which means there is no limit. + */ + val limit: Int = 0 +) { + override fun toString(): String { + return """ + RemoteCountOptions{limit=$limit} + """ + } +} diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/options/FindOneAndModifyOptions.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/options/FindOneAndModifyOptions.kt new file mode 100644 index 0000000000..a0542b573a --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/options/FindOneAndModifyOptions.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.kotlin.mongodb.mongo.options + +import org.mongodb.kbson.BsonDocument + +/** + * The options to apply to a findOneAndUpdate, findOneAndReplace, or findOneAndDelete operation + * (also commonly referred to as findOneAndModify operations). + */ +public data class FindOneAndModifyOptions( + /** + * A document describing the fields to return for all matching documents. + * // TODO Test if there is a difference between `null` and `BsonDocument()`, if not, maybe just disallow `null`? + */ + val projection: BsonDocument? = null, + /** + * The sort criteria to apply to the query, or `null` if no sorting is done. + * // TODO Test if there is a difference between `null` and `BsonDocument()`, if not, maybe just disallow `null`? + */ + val sort: BsonDocument? = null, + /** + * Set to `true` if a new document should be inserted if there are no matches to the query filter. + */ + val upsert: Boolean = false, + /** + * Set to true if findOneAndModify operations should return the new updated document. + * Set to false / leave blank to have these operation return the document before the update. + * Note: Only findOneAndUpdate and findOneAndReplace take this options + * findOneAndDelete will always return the old document + */ + val returnNewDocument: Boolean = false +) { + override fun toString(): String { + return """ + RemoteFindOneAndModifyOptions{ + projection=$projection, + sort=$sort, + upsert=$upsert, + returnNewDocument=$returnNewDocument + } + """ + } +} diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/options/FindOptions.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/options/FindOptions.kt new file mode 100644 index 0000000000..7f1b4e9ddd --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/options/FindOptions.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.kotlin.mongodb.mongo.options + +import org.mongodb.kbson.BsonDocument + +/** + * The options to apply to a find operation (also commonly referred to as a query). + */ +public data class FindOptions( + /** + * The limit to apply. The default is 0, which means there is no limit. + */ + val limit: Int = 0, + /** + * A document describing the fields to return for all matching documents. + * // TODO Test if there is a difference between `null` and `BsonDocument()`, if not, maybe just disallow `null`? + */ + val projection: BsonDocument? = null, + /** + * The sort criteria to apply to the query, or `null` if no sorting is done. + * // TODO Test if there is a difference between `null` and `BsonDocument()`, if not, maybe just disallow `null`? + */ + val sort: BsonDocument? = null +) { + override fun toString(): String { + return """ + RemoteFindOptions{ + limit=$limit, + projection=$projection, + sort=$sort + } + """.trimIndent() + } +} + diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/options/InsertManyResult.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/options/InsertManyResult.kt new file mode 100644 index 0000000000..b92d8d38d8 --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/options/InsertManyResult.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.kotlin.mongodb.mongo.options + +import org.mongodb.kbson.BsonValue + +/** + * The result of an insert many operation. + */ +public data class InsertManyResult( + /** + * the _ids of the inserted documents arranged by the index of the document + * from the operation and its corresponding id. + */ + val insertedIds: Map +) diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/options/UpdateOptions.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/options/UpdateOptions.kt new file mode 100644 index 0000000000..1055852df7 --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/options/UpdateOptions.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.kotlin.mongodb.mongo.options + +/** + * The options to apply when updating documents. + */ +public data class UpdateOptions( + /** + * Set to `true` if a new document should be inserted if there are no matches to the query filter. + */ + val upsert: Boolean +) { + override fun toString(): String { + return """ + RemoteUpdateOptions{upsert=$upsert} + """ + } +} diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/result/DeleteResult.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/result/DeleteResult.kt new file mode 100644 index 0000000000..ef482bbc90 --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/result/DeleteResult.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.kotlin.mongodb.mongo.result + +/** + * The result of a delete operation. + */ +public data class DeleteResult( + /** + * The number of documents deleted. + */ + val deletedCount: Long +) diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/result/InsertOneResult.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/result/InsertOneResult.kt new file mode 100644 index 0000000000..81ca1e3919 --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/result/InsertOneResult.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.kotlin.mongodb.mongo.result + +import org.mongodb.kbson.BsonValue + +/** + * The result of an insert one operation. + */ +public data class InsertOneResult( + /** + * the _id of the inserted document. + */ + val insertedId: BsonValue +) \ No newline at end of file diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/result/UpdateResult.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/result/UpdateResult.kt new file mode 100644 index 0000000000..5e18072049 --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/mongo/result/UpdateResult.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.kotlin.mongodb.mongo.result + +import org.mongodb.kbson.BsonValue + +/** + * The result of an update operation. + */ +public data class UpdateResult( + /** + * The number of documents matched by the query. + */ + val matchedCount: Long, + /** + * The number of documents modified. + */ + val modifiedCount: Long, + /** + * The _id of the inserted document if the replace resulted in an inserted document, + * otherwise null. + */ + val upsertedId: BsonValue? // TODO Should we use BsonNull here instead? +)