From 236c03b2dc41b04fb710c79701162f693abf9622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Mon, 10 Aug 2026 23:30:48 +0200 Subject: [PATCH] feat(driver-sync): Implement the unified API --- driver-sync/build.gradle.kts | 1 + .../src/jvmMain/kotlin/JvmMongoCollection.kt | 4 +- .../src/jvmMain/kotlin/JvmMongoIterable.kt | 6 +- .../kotlin/SyncFilteredMongoCollectionImpl.kt | 248 ++++++++ .../kotlin/SyncMongoAggregationPipeline.kt | 92 +++ .../SyncMongoAggregationPipelineImpl.kt | 174 ++++++ .../src/jvmMain/kotlin/SyncMongoClient.kt | 71 +++ .../src/jvmMain/kotlin/SyncMongoClientImpl.kt | 87 +++ .../src/jvmMain/kotlin/SyncMongoCollection.kt | 102 ++++ .../jvmMain/kotlin/SyncMongoCollectionImpl.kt | 556 ++++++++++++++++++ .../src/jvmMain/kotlin/SyncMongoDatabase.kt | 77 +++ .../jvmMain/kotlin/SyncMongoDatabaseImpl.kt | 69 +++ .../src/jvmMain/kotlin/SyncMongoIterable.kt | 67 +++ .../kotlin/SyncMongoIterableImpl.aggregate.kt | 45 ++ .../kotlin/SyncMongoIterableImpl.find.kt | 55 ++ 15 files changed, 1649 insertions(+), 5 deletions(-) create mode 100644 driver-sync/src/jvmMain/kotlin/SyncFilteredMongoCollectionImpl.kt create mode 100644 driver-sync/src/jvmMain/kotlin/SyncMongoAggregationPipeline.kt create mode 100644 driver-sync/src/jvmMain/kotlin/SyncMongoAggregationPipelineImpl.kt create mode 100644 driver-sync/src/jvmMain/kotlin/SyncMongoClient.kt create mode 100644 driver-sync/src/jvmMain/kotlin/SyncMongoClientImpl.kt create mode 100644 driver-sync/src/jvmMain/kotlin/SyncMongoCollection.kt create mode 100644 driver-sync/src/jvmMain/kotlin/SyncMongoCollectionImpl.kt create mode 100644 driver-sync/src/jvmMain/kotlin/SyncMongoDatabase.kt create mode 100644 driver-sync/src/jvmMain/kotlin/SyncMongoDatabaseImpl.kt create mode 100644 driver-sync/src/jvmMain/kotlin/SyncMongoIterable.kt create mode 100644 driver-sync/src/jvmMain/kotlin/SyncMongoIterableImpl.aggregate.kt create mode 100644 driver-sync/src/jvmMain/kotlin/SyncMongoIterableImpl.find.kt diff --git a/driver-sync/build.gradle.kts b/driver-sync/build.gradle.kts index 9b75e6e1..a450d80d 100644 --- a/driver-sync/build.gradle.kts +++ b/driver-sync/build.gradle.kts @@ -24,6 +24,7 @@ kotlin { sourceSets.commonMain.dependencies { api(projects.dsl) + api(projects.driverSyncApi) api(projects.driverSharedOfficial) } diff --git a/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt b/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt index 50b8ca74..88fb6393 100644 --- a/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt +++ b/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt @@ -56,7 +56,7 @@ import kotlin.reflect.typeOf * * To access the inner iterable, see [asKotlinClient]. * - * To convert an existing MongoDB iterable into an instance of this class, see [asKtMongo]. + * To convert an existing MongoDB iterable into an instance of this class, see [asKtMongoLegacy]. */ class JvmMongoCollection internal constructor( inner: com.mongodb.kotlin.client.MongoCollection, @@ -404,7 +404,7 @@ class JvmMongoCollection internal constructor( inner.aggregate( pipeline = pipeline.chain.toBsonList().map { it.toJava() }, resultClass = documentType, - ).asKtMongo() + ).asKtMongoLegacy() } ) diff --git a/driver-sync/src/jvmMain/kotlin/JvmMongoIterable.kt b/driver-sync/src/jvmMain/kotlin/JvmMongoIterable.kt index 6aee5243..4468244a 100644 --- a/driver-sync/src/jvmMain/kotlin/JvmMongoIterable.kt +++ b/driver-sync/src/jvmMain/kotlin/JvmMongoIterable.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024-2025, OpenSavvy and contributors. + * Copyright (c) 2024-2026, OpenSavvy and contributors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ import java.util.stream.StreamSupport * * To access the inner iterable, see [asKotlinMongoIterable]. * - * To convert an existing MongoDB iterable into an instance of this class, see [asKtMongo]. + * To convert an existing MongoDB iterable into an instance of this class, see [asKtMongoLegacy]. */ class JvmMongoIterable internal constructor( private val inner: com.mongodb.kotlin.client.MongoIterable, @@ -99,5 +99,5 @@ class JvmMongoIterable internal constructor( * Converts a [MongoDB MongoIterable][com.mongodb.kotlin.client.MongoIterable] into a * [KtMongo MongoIterable][JvmMongoIterable]. */ -fun com.mongodb.kotlin.client.MongoIterable.asKtMongo(): JvmMongoIterable = +fun com.mongodb.kotlin.client.MongoIterable.asKtMongoLegacy(): JvmMongoIterable = JvmMongoIterable(this) diff --git a/driver-sync/src/jvmMain/kotlin/SyncFilteredMongoCollectionImpl.kt b/driver-sync/src/jvmMain/kotlin/SyncFilteredMongoCollectionImpl.kt new file mode 100644 index 00000000..70f8e586 --- /dev/null +++ b/driver-sync/src/jvmMain/kotlin/SyncFilteredMongoCollectionImpl.kt @@ -0,0 +1,248 @@ +/* + * Copyright (c) 2026, OpenSavvy and contributors. + * + * 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 opensavvy.ktmongo.sync + +import opensavvy.ktmongo.bson.official.BsonFactory +import opensavvy.ktmongo.bson.types.ObjectIdGenerator +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.command.* +import opensavvy.ktmongo.dsl.path.PropertyNameStrategy +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.dsl.query.UpdateQuery +import opensavvy.ktmongo.dsl.query.UpdateWithPipelineQuery +import opensavvy.ktmongo.dsl.query.UpsertQuery +import opensavvy.ktmongo.sync.api.operations.UpdateOperations +import kotlin.reflect.KType + +private class SyncFilteredMongoCollectionImpl( + private val upstream: SyncMongoCollection, + private val globalFilter: FilterQuery.() -> Unit, +) : SyncMongoCollection { + + override val name: String + get() = upstream.name + + override val fullyQualifiedName: String + get() = upstream.fullyQualifiedName + + override val propertyNameStrategy: PropertyNameStrategy + get() = upstream.propertyNameStrategy + + override val objectIdGenerator: ObjectIdGenerator + get() = upstream.objectIdGenerator + + @LowLevelApi + override val type: KType + get() = upstream.type + + override fun filter(filter: FilterQuery.() -> Unit): SyncMongoCollection = + upstream.filter { + globalFilter() + filter() + } + + override fun asOfficial(): com.mongodb.kotlin.client.MongoCollection = + upstream.asOfficial() + + override val factory: BsonFactory + get() = upstream.factory + + override fun upsertOne(options: UpdateOptions.() -> Unit, filter: FilterQuery.() -> Unit, update: UpsertQuery.() -> Unit): SyncMongoCollection.UpsertResult = + upstream.upsertOne( + options = options, + filter = { + globalFilter() + filter() + }, + update = update + ) + + override fun upsertOneWithPipeline(options: UpdateOptions.() -> Unit, filter: FilterQuery.() -> Unit, update: UpdateWithPipelineQuery.() -> Unit): SyncMongoCollection.UpsertResult = + upstream.upsertOneWithPipeline( + options = options, + filter = { + globalFilter() + filter() + }, + update = update + ) + + override fun aggregate(): SyncMongoAggregationPipeline = + upstream.aggregate() + .match { globalFilter() } + + @LowLevelApi + override val context: BsonContext + get() = upstream.context + + override fun drop(options: DropOptions.() -> Unit) = + upstream.drop(options) + + override fun count(): Long = + upstream.count { + globalFilter() + } + + override fun count(options: CountOptions.() -> Unit, predicate: FilterQuery.() -> Unit): Long = + upstream.count( + options = options, + predicate = { + globalFilter() + predicate() + } + ) + + override fun countEstimated(): Long = + count() + + override fun deleteOne(options: DeleteOneOptions.() -> Unit, filter: FilterQuery.() -> Unit) = + upstream.deleteOne( + options = options, + filter = { + globalFilter() + filter() + } + ) + + override fun deleteMany(options: DeleteManyOptions.() -> Unit, filter: FilterQuery.() -> Unit) = + upstream.deleteMany( + options = options, + filter = { + globalFilter() + filter() + } + ) + + override fun find(): SyncMongoFindIterable = + upstream.find { globalFilter() } + + override fun find(options: FindOptions.() -> Unit, filter: FilterQuery.() -> Unit): SyncMongoFindIterable = + upstream.find( + options = options, + filter = { + globalFilter() + filter() + } + ) + + override fun insertOne(document: Document, options: InsertOneOptions.() -> Unit) = + upstream.insertOne( + document = document, + options = options, + ) + + override fun insertMany(documents: Iterable, options: InsertManyOptions.() -> Unit) = + upstream.insertMany( + documents = documents, + options = options, + ) + + override fun updateMany(options: UpdateOptions.() -> Unit, filter: FilterQuery.() -> Unit, update: UpdateQuery.() -> Unit): UpdateOperations.UpdateResult = + upstream.updateMany( + options = options, + filter = { + globalFilter() + filter() + }, + update = update, + ) + + override fun updateOne(options: UpdateOptions.() -> Unit, filter: FilterQuery.() -> Unit, update: UpdateQuery.() -> Unit): UpdateOperations.UpdateResult = + upstream.updateOne( + options = options, + filter = { + globalFilter() + filter() + }, + update = update, + ) + + override fun replaceOne(options: ReplaceOptions.() -> Unit, filter: FilterQuery.() -> Unit, document: Document) = + upstream.replaceOne( + options = options, + filter = { + globalFilter() + filter() + }, + document = document, + ) + + override fun repsertOne(options: ReplaceOptions.() -> Unit, filter: FilterQuery.() -> Unit, document: Document) = + upstream.repsertOne( + options = options, + filter = { + globalFilter() + filter() + }, + document = document, + ) + + override fun findOneAndUpdate(options: UpdateOptions.() -> Unit, filter: FilterQuery.() -> Unit, update: UpdateQuery.() -> Unit): Document? = + upstream.findOneAndUpdate( + options = options, + filter = { + globalFilter() + filter() + }, + update = update, + ) + + override fun bulkWrite(options: BulkWriteOptions.() -> Unit, filter: FilterQuery.() -> Unit, operations: BulkWrite.() -> Unit) = + upstream.bulkWrite( + options = options, + filter = { + globalFilter() + filter() + }, + operations = operations, + ) + + override fun updateManyWithPipeline(options: UpdateOptions.() -> Unit, filter: FilterQuery.() -> Unit, update: UpdateWithPipelineQuery.() -> Unit): UpdateOperations.UpdateResult = + upstream.updateManyWithPipeline( + options = options, + filter = { + globalFilter() + filter() + }, + update = update, + ) + + override fun updateOneWithPipeline(options: UpdateOptions.() -> Unit, filter: FilterQuery.() -> Unit, update: UpdateWithPipelineQuery.() -> Unit): UpdateOperations.UpdateResult = + upstream.updateOneWithPipeline( + options = options, + filter = { + globalFilter() + filter() + }, + update = update, + ) + + @OptIn(LowLevelApi::class) + override fun toString(): String { + val filter = FilterQuery(context) + globalFilter(filter) + + return "$upstream.filter($filter)" + } +} + +internal fun createFilteredCollection( + upstream: SyncMongoCollection, + globalFilter: FilterQuery.() -> Unit, +): SyncMongoCollection = + SyncFilteredMongoCollectionImpl(upstream, globalFilter) diff --git a/driver-sync/src/jvmMain/kotlin/SyncMongoAggregationPipeline.kt b/driver-sync/src/jvmMain/kotlin/SyncMongoAggregationPipeline.kt new file mode 100644 index 00000000..b4b80feb --- /dev/null +++ b/driver-sync/src/jvmMain/kotlin/SyncMongoAggregationPipeline.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2026, OpenSavvy and contributors. + * + * 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. + */ + +@file:JvmMultifileClass +@file:JvmName("KtMongo") + +package opensavvy.ktmongo.sync + +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.AccumulationOperators +import opensavvy.ktmongo.dsl.aggregation.AggregationOperators +import opensavvy.ktmongo.dsl.aggregation.Value +import opensavvy.ktmongo.dsl.aggregation.stages.* +import opensavvy.ktmongo.dsl.options.SortOptionDsl +import opensavvy.ktmongo.dsl.path.Field +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.sync.api.MongoAggregationPipeline +import kotlin.reflect.KProperty1 +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +/** + * An aggregation pipeline built on top of the + * [official Kotlin driver](https://www.mongodb.com/docs/drivers/kotlin/coroutine/current/). + * + * To start a pipeline, call [MongoCollection.aggregate][SyncMongoCollection.aggregate]. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/core/aggregation-pipeline/) + */ +interface SyncMongoAggregationPipeline : MongoAggregationPipeline { + + @LowLevelApi + override fun asIterable(type: KType): SyncMongoAggregateIterable + + override fun limit(amount: Long): SyncMongoAggregationPipeline + + override fun limit(amount: Int): SyncMongoAggregationPipeline + + override fun match(filter: FilterQuery.() -> Unit): SyncMongoAggregationPipeline + + override fun matchExpr(filter: AggregationOperators.() -> Value): SyncMongoAggregationPipeline + + override fun sample(size: Int): SyncMongoAggregationPipeline + + override fun set(block: SetStageOperators.() -> Unit): SyncMongoAggregationPipeline + + override fun skip(amount: Long): SyncMongoAggregationPipeline + + override fun skip(amount: Int): SyncMongoAggregationPipeline + + override fun sort(block: SortOptionDsl.() -> Unit): SyncMongoAggregationPipeline + + override fun unset(block: UnsetStageOperators.() -> Unit): SyncMongoAggregationPipeline + + override fun project(block: ProjectStageOperators.() -> Unit): SyncMongoAggregationPipeline + + override fun unionWith(other: HasUnionWithCompatibility): SyncMongoAggregationPipeline + + override fun lookup(block: LookupStageOperators.() -> Unit): SyncMongoAggregationPipeline + + override fun group(block: AccumulationOperators.() -> Unit): SyncMongoAggregationPipeline + + override fun countTo(field: Field): SyncMongoAggregationPipeline + + override fun countTo(field: KProperty1): SyncMongoAggregationPipeline + +} + +/** + * Access the data of this pipeline as a [MongoIterable]. + * + * The methods of [MongoIterable] are available directly on this type + * as extension methods, there is no need to convert to a [MongoIterable] yourself. + */ +@OptIn(LowLevelApi::class) +inline fun SyncMongoAggregationPipeline.asIterable(): SyncMongoAggregateIterable = + this.asIterable(typeOf()) diff --git a/driver-sync/src/jvmMain/kotlin/SyncMongoAggregationPipelineImpl.kt b/driver-sync/src/jvmMain/kotlin/SyncMongoAggregationPipelineImpl.kt new file mode 100644 index 00000000..344548a9 --- /dev/null +++ b/driver-sync/src/jvmMain/kotlin/SyncMongoAggregationPipelineImpl.kt @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2026, OpenSavvy and contributors. + * + * 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. + */ + +@file:JvmMultifileClass +@file:JvmName("KtMongo") + +package opensavvy.ktmongo.sync + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.DangerousMongoApi +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.* +import opensavvy.ktmongo.dsl.aggregation.stages.* +import opensavvy.ktmongo.dsl.options.SortOptionDsl +import opensavvy.ktmongo.dsl.path.Field +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.dsl.tree.BsonNode +import opensavvy.ktmongo.official.toJava +import org.bson.conversions.Bson +import kotlin.reflect.KClass +import kotlin.reflect.KProperty1 +import kotlin.reflect.KType + +private class SyncMongoAggregationPipelineImpl @OptIn(LowLevelApi::class) constructor( + private val collection: SyncMongoCollection<*>, + context: BsonContext, + chain: PipelineChainLink, + private val executeAggregate: (List, Class) -> SyncMongoAggregateIterable, +) : AbstractPipeline(context, chain), + AggregationPipeline, + SyncMongoAggregationPipeline { + + // region Execution + + @LowLevelApi + @Suppress("UNCHECKED_CAST") + override fun asIterable(type: KType): SyncMongoAggregateIterable = + executeAggregate(chain.toBsonList().map { it.toJava() }, (type.classifier as KClass).java) + + // endregion + // region Pipeline + + @LowLevelApi + @DangerousMongoApi + override fun withStage(stage: BsonNode): SyncMongoAggregationPipelineImpl = + SyncMongoAggregationPipelineImpl(collection, context, chain.withStage(stage), executeAggregate) + + @Suppress("UNCHECKED_CAST") + @LowLevelApi + @DangerousMongoApi + override fun reinterpret(): SyncMongoAggregationPipelineImpl = + this as SyncMongoAggregationPipelineImpl + + // endregion + // region Stages + + @KtMongoDsl + override fun limit(amount: Long): SyncMongoAggregationPipelineImpl = + super.limit(amount) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun limit(amount: Int): SyncMongoAggregationPipelineImpl = + super.limit(amount) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun match(filter: FilterQuery.() -> Unit): SyncMongoAggregationPipelineImpl = + super.match(filter) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun matchExpr(filter: AggregationOperators.() -> Value): SyncMongoAggregationPipelineImpl = + super.matchExpr(filter) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun sample(size: Int): SyncMongoAggregationPipelineImpl = + super.sample(size) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun set(block: SetStageOperators.() -> Unit): SyncMongoAggregationPipelineImpl = + super.set(block) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun skip(amount: Long): SyncMongoAggregationPipelineImpl = + super.skip(amount) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun skip(amount: Int): SyncMongoAggregationPipelineImpl = + super.skip(amount) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun sort(block: SortOptionDsl.() -> Unit): SyncMongoAggregationPipelineImpl = + super.sort(block) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun unset(block: UnsetStageOperators.() -> Unit): SyncMongoAggregationPipelineImpl = + super.unset(block) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun project(block: ProjectStageOperators.() -> Unit): SyncMongoAggregationPipelineImpl = + super.project(block) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun unionWith(other: HasUnionWithCompatibility): SyncMongoAggregationPipelineImpl = + super.unionWith(other) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun lookup(block: LookupStageOperators.() -> Unit): SyncMongoAggregationPipelineImpl = + super.lookup(block) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun group(block: AccumulationOperators.() -> Unit): SyncMongoAggregationPipelineImpl = + super.group(block) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun countTo(field: Field): SyncMongoAggregationPipelineImpl = + super.countTo(field) as SyncMongoAggregationPipelineImpl + + @KtMongoDsl + override fun countTo(field: KProperty1): SyncMongoAggregationPipelineImpl = + super.countTo(field) as SyncMongoAggregationPipelineImpl + + // endregion + // region $unionWith support + + @OptIn(LowLevelApi::class) + override fun embedInUnionWith(writer: BsonFieldWriter) = with(writer) { + writeString("coll", collection.name) + writeArray("pipeline") { + this@SyncMongoAggregationPipelineImpl.writeTo(this) + } + } + + // endregion + // region $lookup support + + @LowLevelApi + override fun embedInLookup(writer: BsonFieldWriter) = with(writer) { + writeString("from", collection.name) + + if (chain.isNotEmpty()) { + writeArray("pipeline") { + this@SyncMongoAggregationPipelineImpl.writeTo(this) + } + } + } + + // endregion + + override fun toString(): String = + "$collection.aggregate(${super.toString()})" +} + +@LowLevelApi +internal fun SyncMongoAggregationPipeline( + collection: SyncMongoCollection<*>, + context: BsonContext, + chain: PipelineChainLink, + executeAggregate: (List, Class) -> SyncMongoAggregateIterable, +): SyncMongoAggregationPipeline = + SyncMongoAggregationPipelineImpl(collection, context, chain, executeAggregate) diff --git a/driver-sync/src/jvmMain/kotlin/SyncMongoClient.kt b/driver-sync/src/jvmMain/kotlin/SyncMongoClient.kt new file mode 100644 index 00000000..89c966a2 --- /dev/null +++ b/driver-sync/src/jvmMain/kotlin/SyncMongoClient.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2026, OpenSavvy and contributors. + * + * 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. + */ + +@file:JvmMultifileClass +@file:JvmName("KtMongo") + +package opensavvy.ktmongo.sync + +import opensavvy.ktmongo.sync.api.MongoClient + +/** + * Entry-point to the KtMongo Coroutines driver. + * + * The Coroutine client provides a coroutine-aware API which internally uses the + * [official Kotlin driver](https://www.mongodb.com/docs/drivers/kotlin/coroutine/current/). + * + * ### Organizing data + * + * Accessing MongoDB data happens in three steps: + * - [SyncMongoClient]: represents the connection to the MongoDB application, handles + * the lifecycle and the configuration. + * - [SyncMongoDatabase] (accessed with [SyncMongoClient.database]): each database groups data together. + * This allows deploying multiple applications (or the same application multiple times) + * without name collisions. + * - [SyncMongoCollection] (accessed with [SyncMongoDatabase.collection]): each collection stores data together. + * Documents in a collection may have a different structure. + * + * ### Example + * + * ```kotlin + * @Serializable + * class User( + * val _id: ObjectId, + * val name: String, + * val age: Int, + * ) + * + * fun main() = runBlocking { + * val client = SyncMongoClient("mongodb://localhost:27017") + * + * val database = client.database("my-app") + * val users = database.collection("users") + * + * println("The database contains ${users.count()} users.") + * } + * ``` + * + * @see asKtMongoLegacy Convert an existing instance from the official Kotlin driver. + */ +interface SyncMongoClient : MongoClient { + + /** + * Obtains the underlying MongoDB client from the official Kotlin driver. + */ + fun asOfficial(): com.mongodb.kotlin.client.MongoClient + + override fun database(name: String): SyncMongoDatabase +} diff --git a/driver-sync/src/jvmMain/kotlin/SyncMongoClientImpl.kt b/driver-sync/src/jvmMain/kotlin/SyncMongoClientImpl.kt new file mode 100644 index 00000000..fece084d --- /dev/null +++ b/driver-sync/src/jvmMain/kotlin/SyncMongoClientImpl.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2026, OpenSavvy and contributors. + * + * 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. + */ + +@file:JvmMultifileClass +@file:JvmName("KtMongo") + +package opensavvy.ktmongo.sync + +import com.mongodb.kotlin.client.MongoClient + +private class SyncMongoClientImpl( + private val inner: MongoClient, +) : SyncMongoClient { + + override fun asOfficial(): MongoClient = + inner + + override fun database(name: String): SyncMongoDatabase = + inner.getDatabase(name).asKtMongo() + + override fun close() { + inner.close() + } + + override fun toString(): String = + "SyncMongoClient()" +} + +/** + * Instantiates a KtMongo [SyncMongoClient] using an existing client from the official Kotlin driver. + * + * This method allows taking advantage of the full configuration power of the official client. + * + * ### Example + * + * ```kotlin + * fun main() = runBlocking { + * val client = SyncMongoClient() + * + * val database = client.database("my-app") + * val users = database.collection("users") + * + * println("Users: ${users.count()}") + * } + * ``` + */ +fun SyncMongoClient( + connectionString: String = "mongodb://localhost:27017", +): SyncMongoClient = + SyncMongoClientImpl(MongoClient.create(connectionString)) + +/** + * Instantiates a KtMongo [SyncMongoClient] using an existing client from the official Kotlin driver. + * + * This method allows taking advantage of the full configuration power of the official client. + * + * ### Example + * + * ```kotlin + * import com.mongodb.kotlin.client.coroutine.MongoClient + * + * fun main() = runBlocking { + * val client = MongoClient.create(/* … */) + * .asKtMongo() + * + * val database = client.database("my-app") + * val users = database.collection("users") + * + * println("Users: ${users.count()}") + * } + * ``` + */ +fun MongoClient.asKtMongo(): SyncMongoClient = + SyncMongoClientImpl(this) diff --git a/driver-sync/src/jvmMain/kotlin/SyncMongoCollection.kt b/driver-sync/src/jvmMain/kotlin/SyncMongoCollection.kt new file mode 100644 index 00000000..34e11762 --- /dev/null +++ b/driver-sync/src/jvmMain/kotlin/SyncMongoCollection.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2026, OpenSavvy and contributors. + * + * 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. + */ + +@file:JvmMultifileClass +@file:JvmName("KtMongo") + +package opensavvy.ktmongo.sync + +import opensavvy.ktmongo.bson.official.BsonFactory +import opensavvy.ktmongo.bson.official.BsonValue +import opensavvy.ktmongo.dsl.command.FindOptions +import opensavvy.ktmongo.dsl.command.UpdateOptions +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.dsl.query.UpdateWithPipelineQuery +import opensavvy.ktmongo.dsl.query.UpsertQuery +import opensavvy.ktmongo.sync.api.MongoCollection +import opensavvy.ktmongo.sync.api.operations.UpdateOperations + +/** + * A collection stores related documents together. + * + * The Coroutine client provides a coroutine-aware API which internally uses the + * [official Kotlin driver](https://www.mongodb.com/docs/drivers/kotlin/coroutine/current/). + * + * Usually, all documents in a collection have the same shape (the same fields). + * However, heterogeneous structure can be achieved by using: + * - Kotlin collections, like [List] and [Set], the embed an arbitrary number of items. + * - Polymorphism, for example with `sealed class`, to have different fields based on a discriminator. + * + * To avoid name collisions, collections are grouped into [databases][MongoDatabase]. + * + * To obtain a collection, see [MongoDatabase.collection]. + * + * ### Size limit + * + * A MongoDB document cannot exceed 16 MiB. + * + * You can measure the size of a document with [opensavvy.ktmongo.bson.BsonDocument.toByteArray] + * followed by [ByteArray.size]. + * + * The maximum nesting is 100 levels. + * Each document or array adds a level. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/core/databases-and-collections/) + * - [Size limits](https://www.mongodb.com/docs/manual/reference/limits/#bson-documents) + * + * @see asKtMongoLegacy Convert an existing instance from the official Kotlin driver. + */ +interface SyncMongoCollection : MongoCollection { + + /** + * Obtains the underlying MongoDB collection from the official Kotlin driver. + */ + fun asOfficial(): com.mongodb.kotlin.client.MongoCollection + + override val factory: BsonFactory + + /** + * The return value of [upsertOne] and [upsertOneWithPipeline]. + */ + interface UpsertResult : UpdateOperations.UpsertResult { + + override val upsertedId: BsonValue? + } + + @IgnorableReturnValue + override fun upsertOne( + options: UpdateOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + update: UpsertQuery.() -> Unit, + ): UpsertResult + + @IgnorableReturnValue + override fun upsertOneWithPipeline( + options: UpdateOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + update: UpdateWithPipelineQuery.() -> Unit, + ): UpsertResult + + override fun aggregate(): SyncMongoAggregationPipeline + + override fun filter(filter: FilterQuery.() -> Unit): SyncMongoCollection + + override fun find(): SyncMongoFindIterable + + override fun find(options: FindOptions.() -> Unit, filter: FilterQuery.() -> Unit): SyncMongoFindIterable +} diff --git a/driver-sync/src/jvmMain/kotlin/SyncMongoCollectionImpl.kt b/driver-sync/src/jvmMain/kotlin/SyncMongoCollectionImpl.kt new file mode 100644 index 00000000..342281b0 --- /dev/null +++ b/driver-sync/src/jvmMain/kotlin/SyncMongoCollectionImpl.kt @@ -0,0 +1,556 @@ +/* + * Copyright (c) 2026, OpenSavvy and contributors. + * + * 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. + */ + +@file:JvmMultifileClass +@file:JvmName("KtMongo") + +package opensavvy.ktmongo.sync + +import com.mongodb.client.model.DeleteOptions +import com.mongodb.client.model.FindOneAndUpdateOptions +import com.mongodb.kotlin.client.MongoCollection +import opensavvy.ktmongo.bson.official.BsonFactory +import opensavvy.ktmongo.bson.official.BsonValue +import opensavvy.ktmongo.bson.official.types.Jvm +import opensavvy.ktmongo.bson.types.ObjectIdGenerator +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.PipelineChainLink +import opensavvy.ktmongo.dsl.command.* +import opensavvy.ktmongo.dsl.options.ArrayFiltersOption +import opensavvy.ktmongo.dsl.options.WithWriteConcern +import opensavvy.ktmongo.dsl.options.WriteConcernOption +import opensavvy.ktmongo.dsl.options.option +import opensavvy.ktmongo.dsl.path.PropertyNameStrategy +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.dsl.query.UpdateQuery +import opensavvy.ktmongo.dsl.query.UpdateWithPipelineQuery +import opensavvy.ktmongo.dsl.query.UpsertQuery +import opensavvy.ktmongo.official.command.toJava +import opensavvy.ktmongo.official.options.* +import opensavvy.ktmongo.official.options.toJava +import opensavvy.ktmongo.official.toJava +import opensavvy.ktmongo.sync.api.operations.UpdateOperations +import java.util.concurrent.TimeUnit +import kotlin.reflect.KType +import kotlin.reflect.typeOf +import com.mongodb.client.model.ReplaceOptions as MongoReplaceOptions +import com.mongodb.client.model.UpdateOptions as MongoUpdateOptions + +private class SyncMongoCollectionImpl( + inner: MongoCollection, + override val factory: BsonFactory, + override val propertyNameStrategy: PropertyNameStrategy, + override val objectIdGenerator: ObjectIdGenerator, + @property:LowLevelApi + override val type: KType, +) : SyncMongoCollection { + + private val inner = inner + .withCodecRegistry(factory.codecRegistry) + + override fun asOfficial(): MongoCollection = + inner + + override val name: String + get() = inner.namespace.collectionName + + override val fullyQualifiedName: String + get() = inner.namespace.fullName + + private inner class CoroutineBsonContext : BsonContext, + opensavvy.ktmongo.bson.BsonFactory by factory, + ObjectIdGenerator by objectIdGenerator, + PropertyNameStrategy by propertyNameStrategy + + @LowLevelApi + override val context: BsonContext = CoroutineBsonContext() + + // region Count + + override fun count(): Long = + inner.countDocuments() + + @OptIn(LowLevelApi::class) + override fun count( + options: CountOptions.() -> Unit, + predicate: FilterQuery.() -> Unit, + ): Long { + val model = Count(context) + + model.options.options() + model.filter.predicate() + + return inner.countDocuments( + factory.buildDocument(model.filter).raw, + model.options.toJava() + ) + } + + override fun countEstimated(): Long = + inner.estimatedDocumentCount() + + // endregion + // region Find + + override fun find(): SyncMongoFindIterable = + inner.find().asKtMongo(lazyStringRepresentation = { "$this.find({})" }) + + @OptIn(LowLevelApi::class) + override fun find( + options: FindOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + ): SyncMongoFindIterable { + val model = Find(context) + + model.options.options() + model.filter.filter() + + return inner + .withReadConcern(model.options.readReadConcern()) + .withReadPreference(model.options.readReadPreference()) + .find(factory.buildDocument(model.filter).raw) + .limit(model.options.readLimit()) + .skip(model.options.readSkip()) + .maxTime(model.options.readMaxTimeMS().toLong(), TimeUnit.MILLISECONDS) + .sort(model.options.readSortDocument()) + .asKtMongo(lazyStringRepresentation = { "$this.find($model)" }) + } + + // endregion + // region Insert + + @OptIn(LowLevelApi::class) + override fun insertOne(document: Document, options: InsertOneOptions.() -> Unit) { + val model = InsertOne(context, document, type) + + model.options.options() + + inner.withWriteConcern(model.options).insertOne( + model.document, + com.mongodb.client.model.InsertOneOptions() + ) + } + + @OptIn(LowLevelApi::class) + override fun insertMany(documents: Iterable, options: InsertManyOptions.() -> Unit) { + val model = InsertMany(context, documents.toList(), type) + + model.options.options() + + inner.withWriteConcern(model.options).insertMany( + model.documents, + com.mongodb.client.model.InsertManyOptions() + ) + } + + // endregion + // region Delete + + @OptIn(LowLevelApi::class) + override fun deleteOne( + options: DeleteOneOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + ) { + val model = DeleteOne(context) + + model.filter.filter() + model.options.options() + + inner.withWriteConcern(model.options).deleteOne( + filter = factory.buildDocument(model.filter).raw, + options = DeleteOptions() + ) + } + + @OptIn(LowLevelApi::class) + override fun deleteMany( + options: DeleteManyOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + ) { + val model = DeleteMany(context) + + model.filter.filter() + model.options.options() + + inner.withWriteConcern(model.options).deleteMany( + filter = factory.buildDocument(model.filter).raw, + options = DeleteOptions() + ) + } + + // endregion + // region Collection + + @OptIn(LowLevelApi::class) + override fun drop(options: DropOptions.() -> Unit) { + val model = Drop(context) + + model.options.options() + + inner.withWriteConcern(model.options).drop() + } + + // endregion + // region Update + + @OptIn(LowLevelApi::class) + override fun updateMany( + options: UpdateOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + update: UpdateQuery.() -> Unit, + ): UpdateOperations.UpdateResult { + val model = UpdateMany(context) + + model.options.options() + model.filter.filter() + model.update.update() + + val result = inner + .withWriteConcern(model.options) + .updateMany( + factory.buildDocument(model.filter).raw, + factory.buildDocument(model.update).raw, + MongoUpdateOptions() + .arrayFilters(model.options.option()?.filters.orEmpty().map { factory.readDocument(it).raw }), + ) + return CoroutineUpdateResult(result, factory) + } + + @OptIn(LowLevelApi::class) + override fun updateOne( + options: UpdateOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + update: UpdateQuery.() -> Unit, + ): UpdateOperations.UpdateResult { + val model = UpdateOne(context) + + model.options.options() + model.filter.filter() + model.update.update() + + val result = inner + .withWriteConcern(model.options) + .updateOne( + factory.buildDocument(model.filter).raw, + factory.buildDocument(model.update).raw, + MongoUpdateOptions() + .arrayFilters(model.options.option()?.filters.orEmpty().map { factory.readDocument(it).raw }), + ) + return CoroutineUpdateResult(result, factory) + } + + @OptIn(LowLevelApi::class) + override fun upsertOne( + options: UpdateOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + update: UpsertQuery.() -> Unit, + ): SyncMongoCollection.UpsertResult { + val model = UpsertOne(context) + + model.options.options() + model.filter.filter() + model.update.update() + + val result = inner + .withWriteConcern(model.options) + .updateOne( + factory.buildDocument(model.filter).raw, + factory.buildDocument(model.update).raw, + MongoUpdateOptions() + .upsert(true) + .arrayFilters(model.options.option()?.filters.orEmpty().map { factory.readDocument(it).raw }), + ) + return CoroutineUpdateResult(result, factory) + } + + @OptIn(LowLevelApi::class) + override fun replaceOne( + options: ReplaceOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + document: Document, + ) { + val model = ReplaceOne(context, document, type) + + model.options.options() + model.filter.filter() + + inner.withWriteConcern(model.options).replaceOne( + factory.buildDocument(model.filter).raw, + document, + MongoReplaceOptions(), + ) + } + + @OptIn(LowLevelApi::class) + override fun repsertOne( + options: ReplaceOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + document: Document, + ) { + val model = RepsertOne(context, document, type) + + model.options.options() + model.filter.filter() + + inner.withWriteConcern(model.options).replaceOne( + factory.buildDocument(model.filter).raw, + document, + MongoReplaceOptions().upsert(true), + ) + } + + @OptIn(LowLevelApi::class) + override fun findOneAndUpdate( + options: UpdateOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + update: UpdateQuery.() -> Unit, + ): Document? { + val model = UpdateOne(context) + + model.options.options() + model.filter.filter() + model.update.update() + + return inner.withWriteConcern(model.options).findOneAndUpdate( + factory.buildDocument(model.filter).raw, + factory.buildDocument(model.update).raw, + FindOneAndUpdateOptions(), + ) + } + + @OptIn(LowLevelApi::class) + override fun bulkWrite( + options: BulkWriteOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + operations: BulkWrite.() -> Unit, + ) { + val model = BulkWrite(context, type, filter) + + model.options.options() + model.operations() + + inner.withWriteConcern(model.options).bulkWrite( + model.operations.map { it.toJava() }.toList(), + options = com.mongodb.client.model.BulkWriteOptions(), + ) + } + + // endregion + // region UpdatePipeline + + @OptIn(LowLevelApi::class) + override fun updateManyWithPipeline( + options: UpdateOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + update: UpdateWithPipelineQuery.() -> Unit, + ): UpdateOperations.UpdateResult { + val model = UpdateManyWithPipeline(context) + + model.options.options() + model.filter.filter() + model.update.update() + + val result = inner + .withWriteConcern(model.options) + .updateMany( + factory.buildDocument(model.filter).raw, + model.updates.map { it.toJava() }, + MongoUpdateOptions(), + ) + return CoroutineUpdateResult(result, factory) + } + + @OptIn(LowLevelApi::class) + override fun updateOneWithPipeline( + options: UpdateOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + update: UpdateWithPipelineQuery.() -> Unit, + ): UpdateOperations.UpdateResult { + val model = UpdateOneWithPipeline(context) + + model.options.options() + model.filter.filter() + model.update.update() + + val result = inner + .withWriteConcern(model.options) + .updateOne( + factory.buildDocument(model.filter).raw, + model.updates.map { it.toJava() }, + MongoUpdateOptions(), + ) + return CoroutineUpdateResult(result, factory) + } + + @OptIn(LowLevelApi::class) + override fun upsertOneWithPipeline( + options: UpdateOptions.() -> Unit, + filter: FilterQuery.() -> Unit, + update: UpdateWithPipelineQuery.() -> Unit, + ): SyncMongoCollection.UpsertResult { + val model = UpsertOneWithPipeline(context) + + model.options.options() + model.filter.filter() + model.update.update() + + val result = inner + .withWriteConcern(model.options) + .updateOne( + factory.buildDocument(model.filter).raw, + model.updates.map { it.toJava() }, + MongoUpdateOptions().upsert(true), + ) + return CoroutineUpdateResult(result, factory) + } + + // endregion + // region Aggregation + + @OptIn(LowLevelApi::class) + override fun aggregate(): SyncMongoAggregationPipeline = + SyncMongoAggregationPipeline( + collection = this, + context = context, + chain = PipelineChainLink(context), + executeAggregate = { pipeline, documentClass -> + inner.aggregate(pipeline, documentClass) + .asKtMongo() + }, + ) + + // endregion + // region Filter + + override fun filter(filter: FilterQuery.() -> Unit): SyncMongoCollection = + createFilteredCollection(this, filter) + + // endregion + + override fun toString(): String = + "SyncMongoCollection($fullyQualifiedName)" +} + +private class CoroutineUpdateResult( + private val inner: com.mongodb.client.result.UpdateResult, + private val factory: BsonFactory, +) : SyncMongoCollection.UpsertResult { + override val acknowledged: Boolean + get() = inner.wasAcknowledged() + + override val matchedCount: Long + get() = inner.matchedCount + + override val modifiedCount: Long + get() = inner.modifiedCount + + override val upsertedId: BsonValue? + get() = inner.upsertedId?.let { factory.readValue(it) } + + override val upsertedCount: Int + get() = if (inner.upsertedId == null) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CoroutineUpdateResult) return false + + if (inner != other.inner) return false + if (factory != other.factory) return false + + return true + } + + override fun hashCode(): Int { + var result = inner.hashCode() + result = 31 * result + factory.hashCode() + return result + } + + override fun toString(): String = + if (acknowledged) "UpdateResult(acknowledged=true, matchedCount=$matchedCount, modifiedCount=$modifiedCount, upsertedCount=$upsertedCount, upsertedId=$upsertedId)" + else "UpdateResult(acknowledged=false)" +} + +/** + * Instantiates a KtMongo [SyncMongoCollection] using an existing collection from the official Kotlin driver. + * + * ### Example + * + * ```kotlin + * import com.mongodb.kotlin.client.coroutine.MongoClient + * + * fun main() = runBlocking { + * val client = MongoClient.create(/* … */) + * val database = client.database("my-app") + * val users = database.collection("users") + * .asKtMongo() + * + * println("Users: ${users.count()}") + * } + * ``` + */ +fun MongoCollection.asKtMongo( + factory: BsonFactory = BsonFactory(this.codecRegistry), + propertyNameStrategy: PropertyNameStrategy = PropertyNameStrategy.Default, + objectIdGenerator: ObjectIdGenerator = ObjectIdGenerator.Jvm(), + type: KType, +): SyncMongoCollection = + SyncMongoCollectionImpl( + inner = this, + factory = factory, + propertyNameStrategy = propertyNameStrategy, + objectIdGenerator = objectIdGenerator, + type = type, + ) + +/** + * Instantiates a KtMongo [SyncMongoCollection] using an existing collection from the official Kotlin driver. + * + * ### Example + * + * ```kotlin + * import com.mongodb.kotlin.client.coroutine.MongoClient + * + * fun main() = runBlocking { + * val client = MongoClient.create(/* … */) + * val database = client.database("my-app") + * val users = database.collection("users") + * .asKtMongo() + * + * println("Users: ${users.count()}") + * } + * ``` + */ +inline fun MongoCollection.asKtMongo( + factory: BsonFactory = BsonFactory(this.codecRegistry), + propertyNameStrategy: PropertyNameStrategy = PropertyNameStrategy.Default, + objectIdGenerator: ObjectIdGenerator = ObjectIdGenerator.Jvm(), +): SyncMongoCollection = + asKtMongo( + factory = factory, + propertyNameStrategy = propertyNameStrategy, + objectIdGenerator = objectIdGenerator, + type = typeOf(), + ) + +@LowLevelApi +private fun MongoCollection.withWriteConcern(option: WithWriteConcern): MongoCollection { + val concern = option.option()?.concern + ?: return this + + return this.withWriteConcern(concern.toJava()) +} diff --git a/driver-sync/src/jvmMain/kotlin/SyncMongoDatabase.kt b/driver-sync/src/jvmMain/kotlin/SyncMongoDatabase.kt new file mode 100644 index 00000000..b8daeff2 --- /dev/null +++ b/driver-sync/src/jvmMain/kotlin/SyncMongoDatabase.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2026, OpenSavvy and contributors. + * + * 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. + */ + +@file:JvmMultifileClass +@file:JvmName("KtMongo") + +package opensavvy.ktmongo.sync + +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.sync.api.MongoDatabase +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +/** + * A grouping of collections with the same theme. + * + * The Coroutine client provides a coroutine-aware API which internally uses the + * [official Kotlin driver](https://www.mongodb.com/docs/drivers/kotlin/coroutine/current/). + * + * ### What is a database? + * + * [Collections][MongoCollection] are grouped into databases to avoid name collisions. + * Databases are similar to Kotlin packages. + * If multiple applications are deployed in the same MongoDB instance in their own database, + * they can use the same collection names (e.g. `users`) without conflicts. + * + * Each database has a [name] that must be unique within a MongoDB deployment. + * + * ### Access + * + * To obtain a database, see [MongoClient.database]. + * + * To obtain a collection, see [collection]. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/core/databases-and-collections/) + * + * @see asKtMongoLegacy Convert an existing instance from the official Kotlin driver. + */ +interface SyncMongoDatabase : MongoDatabase { + + /** + * Obtains the underlying MongoDB database from the official Kotlin driver. + */ + fun asOfficial(): com.mongodb.kotlin.client.MongoDatabase + + @LowLevelApi + override fun collection(name: String, type: KType): SyncMongoCollection + + /** + * Creates a [MongoCollection] object. + * + * This method is purely a client-side operation, it does nothing in the MongoDB server. + * In MongoDB, databases and collections are created implicitly on the first insert. + * + * For an example, see [MongoClient]. + */ + @OptIn(LowLevelApi::class) + @Suppress("WRONG_MODIFIER_CONTAINING_DECLARATION") + final inline fun collection(name: String): SyncMongoCollection = + collection(name, type = typeOf()) + +} diff --git a/driver-sync/src/jvmMain/kotlin/SyncMongoDatabaseImpl.kt b/driver-sync/src/jvmMain/kotlin/SyncMongoDatabaseImpl.kt new file mode 100644 index 00000000..f851b9e6 --- /dev/null +++ b/driver-sync/src/jvmMain/kotlin/SyncMongoDatabaseImpl.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026, OpenSavvy and contributors. + * + * 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. + */ + +@file:JvmMultifileClass +@file:JvmName("KtMongo") + +package opensavvy.ktmongo.sync + +import com.mongodb.kotlin.client.MongoDatabase +import opensavvy.ktmongo.dsl.LowLevelApi +import kotlin.reflect.KClass +import kotlin.reflect.KType + +private class SyncMongoDatabaseImpl( + private val inner: MongoDatabase, +) : SyncMongoDatabase { + + override fun asOfficial(): MongoDatabase = + inner + + @LowLevelApi + @Suppress("UNCHECKED_CAST") + override fun collection(name: String, type: KType): SyncMongoCollection = + inner.getCollection(name, (type.classifier as KClass).java) + .asKtMongo(type = type) + + override val name: String + get() = inner.name + + override fun toString(): String = + "SyncMongoDatabase($name)" +} + +/** + * Instantiates a KtMongo [SyncMongoDatabase] using an existing client from the official Kotlin driver. + * + * ### Example + * + * ```kotlin + * import com.mongodb.kotlin.client.coroutine.MongoClient + * + * fun main() = runBlocking { + * val client = MongoClient.create(/* … */) + * val database = client.database("my-app") + * .asKtMongo() + * + * val users = database.collection("users") + * + * println("Users: ${users.count()}") + * } + * ``` + */ +fun MongoDatabase.asKtMongo(): SyncMongoDatabase = + SyncMongoDatabaseImpl( + inner = this, + ) diff --git a/driver-sync/src/jvmMain/kotlin/SyncMongoIterable.kt b/driver-sync/src/jvmMain/kotlin/SyncMongoIterable.kt new file mode 100644 index 00000000..c9b0a875 --- /dev/null +++ b/driver-sync/src/jvmMain/kotlin/SyncMongoIterable.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2026, OpenSavvy and contributors. + * + * 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. + */ + +@file:JvmMultifileClass +@file:JvmName("KtMongo") + +package opensavvy.ktmongo.sync + +import com.mongodb.kotlin.client.AggregateIterable +import com.mongodb.kotlin.client.FindIterable + +/** + * Streaming-capable iterable cursor to read data from the database. + * + * The Coroutine client provides a coroutine-aware API which internally uses the + * [official Kotlin driver](https://www.mongodb.com/docs/drivers/kotlin/coroutine/current/). + * + * This type wraps a [FindFlow] from the official driver. + * See also [SyncMongoAggregateIterable]. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/core/cursors/) + */ +interface SyncMongoFindIterable : opensavvy.ktmongo.sync.api.MongoIterable { + + /** + * Obtains the underlying MongoDB flow from the official Kotlin driver. + */ + fun asOfficial(): FindIterable + +} + +/** + * Streaming-capable iterable cursor to read data from the database. + * + * The Coroutine client provides a coroutine-aware API which internally uses the + * [official Kotlin driver](https://www.mongodb.com/docs/drivers/kotlin/coroutine/current/). + * + * This type wraps a [AggregateFlow] from the official driver. + * See also [SyncMongoFindIterable]. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/core/cursors/) + */ +interface SyncMongoAggregateIterable : opensavvy.ktmongo.sync.api.MongoIterable { + + /** + * Obtains the underlying MongoDB flow from the official Kotlin driver. + */ + fun asOfficial(): AggregateIterable + +} diff --git a/driver-sync/src/jvmMain/kotlin/SyncMongoIterableImpl.aggregate.kt b/driver-sync/src/jvmMain/kotlin/SyncMongoIterableImpl.aggregate.kt new file mode 100644 index 00000000..de7763cd --- /dev/null +++ b/driver-sync/src/jvmMain/kotlin/SyncMongoIterableImpl.aggregate.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2026, OpenSavvy and contributors. + * + * 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. + */ + +@file:JvmMultifileClass +@file:JvmName("KtMongo") + +package opensavvy.ktmongo.sync + +import com.mongodb.kotlin.client.AggregateIterable + +private class SyncMongoAggregateIterableImpl( + private val inner: AggregateIterable, +) : SyncMongoAggregateIterable { + + override fun asOfficial(): AggregateIterable = + inner + + override fun first(): Document = + inner.first() + + override fun firstOrNull(): Document? = + inner.firstOrNull() + + override fun forEach(action: (Document) -> Unit): Unit = + inner.forEach(action) +} + +/** + * Instantiates a KtMongo [SyncMongoAggregateIterable] using an existing flow from the official Kotlin driver. + */ +fun AggregateIterable.asKtMongo(): SyncMongoAggregateIterable = + SyncMongoAggregateIterableImpl(this) diff --git a/driver-sync/src/jvmMain/kotlin/SyncMongoIterableImpl.find.kt b/driver-sync/src/jvmMain/kotlin/SyncMongoIterableImpl.find.kt new file mode 100644 index 00000000..14e5454f --- /dev/null +++ b/driver-sync/src/jvmMain/kotlin/SyncMongoIterableImpl.find.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2026, OpenSavvy and contributors. + * + * 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. + */ + +@file:JvmMultifileClass +@file:JvmName("KtMongo") + +package opensavvy.ktmongo.sync + +import com.mongodb.kotlin.client.FindIterable + +private class SyncMongoFindIterableImpl( + private val inner: FindIterable, + private val lazyStringRepresentation: (() -> String)?, +) : SyncMongoFindIterable { + + override fun asOfficial(): FindIterable = + inner + + override fun first(): Document = + inner.first() + + override fun firstOrNull(): Document? = + inner.firstOrNull() + + override fun forEach(action: (Document) -> Unit): Unit = + inner.forEach(action) + + override fun toString(): String = lazyStringRepresentation?.invoke() + ?: super.toString() +} + +/** + * Instantiates a KtMongo [SyncMongoFindIterable] using an existing flow from the official Kotlin driver. + */ +fun FindIterable.asKtMongo(): SyncMongoFindIterable = + SyncMongoFindIterableImpl(this, lazyStringRepresentation = { "$this.asKtMongo()" }) + +// Same but allows customizing the toString() +internal fun FindIterable.asKtMongo( + lazyStringRepresentation: (() -> String)?, +): SyncMongoFindIterable = + SyncMongoFindIterableImpl(this, lazyStringRepresentation) -- 2.51.2