diff --git a/driver-api/src/commonMain/kotlin/MongoCollection.kt b/driver-api/src/commonMain/kotlin/MongoCollection.kt index 1bced91b..84712ba0 100644 --- a/driver-api/src/commonMain/kotlin/MongoCollection.kt +++ b/driver-api/src/commonMain/kotlin/MongoCollection.kt @@ -24,6 +24,7 @@ import opensavvy.ktmongo.dsl.LowLevelApi import opensavvy.ktmongo.dsl.path.PropertyNameStrategy import opensavvy.ktmongo.dsl.query.FilterQuery import kotlin.reflect.KType +import kotlin.reflect.typeOf /** * A collection stores related documents together. @@ -151,6 +152,34 @@ interface MongoCollection : ObjectIdGenerator, @LowLevelApi val type: KType + // region Type manipulation + + /** + * Overwrites the represented type of this value. + * + * This method can be useful to bypass type checks. + * + * ### Example + * + * If we know there are some documents that have the wrong format, we can use `unsafeCast` to find them + * without needing to add their fields to the production DTO. + * + * ```kotlin + * class User( + * val _id: ObjectId, + * val name: String, + * ) + * + * users.unsafeCast() // Treat all data as arbitrary BSON documents + * .find { BsonDocument.get("oldField") ne null } + * .forEach { + * println(it["oldField"]?.decodeString()) + * } + * ``` + */ + fun unsafeCast(type: KType): MongoCollection + + // endregion // region Specializations override fun filter(filter: FilterQuery.() -> Unit): MongoCollection @@ -158,3 +187,29 @@ interface MongoCollection : ObjectIdGenerator, // endregion } + +/** + * Overwrites the represented type of this value. + * + * This method can be useful to bypass type checks. + * + * ### Example + * + * If we know there are some documents that have the wrong format, we can use `unsafeCast` to find them + * without needing to add their fields to the production DTO. + * + * ```kotlin + * class User( + * val _id: ObjectId, + * val name: String, + * ) + * + * users.unsafeCast() // Treat all data as arbitrary BSON documents + * .find { BsonDocument.get("oldField") ne null } + * .forEach { + * println(it["oldField"]?.decodeString()) + * } + * ``` + */ +inline fun MongoCollection<*>.unsafeCast(): MongoCollection = + unsafeCast(typeOf()) diff --git a/driver-sync-api/src/commonMain/kotlin/MongoCollection.kt b/driver-sync-api/src/commonMain/kotlin/MongoCollection.kt index 2278bccb..3bdf1d8e 100644 --- a/driver-sync-api/src/commonMain/kotlin/MongoCollection.kt +++ b/driver-sync-api/src/commonMain/kotlin/MongoCollection.kt @@ -24,6 +24,7 @@ import opensavvy.ktmongo.dsl.path.PropertyNameStrategy import opensavvy.ktmongo.dsl.query.FilterQuery import opensavvy.ktmongo.sync.api.operations.* import kotlin.reflect.KType +import kotlin.reflect.typeOf /** * A collection stores related documents together. @@ -151,6 +152,34 @@ interface MongoCollection : ObjectIdGenerator, @LowLevelApi val type: KType + // region Type manipulation + + /** + * Overwrites the represented type of this value. + * + * This method can be useful to bypass type checks. + * + * ### Example + * + * If we know there are some documents that have the wrong format, we can use `unsafeCast` to find them + * without needing to add their fields to the production DTO. + * + * ```kotlin + * class User( + * val _id: ObjectId, + * val name: String, + * ) + * + * users.unsafeCast() // Treat all data as arbitrary BSON documents + * .find { BsonDocument.get("oldField") ne null } + * .forEach { + * println(it["oldField"]?.decodeString()) + * } + * ``` + */ + fun unsafeCast(type: KType): MongoCollection + + // endregion // region Specializations override fun filter(filter: FilterQuery.() -> Unit): MongoCollection @@ -158,3 +187,29 @@ interface MongoCollection : ObjectIdGenerator, // endregion } + +/** + * Overwrites the represented type of this value. + * + * This method can be useful to bypass type checks. + * + * ### Example + * + * If we know there are some documents that have the wrong format, we can use `unsafeCast` to find them + * without needing to add their fields to the production DTO. + * + * ```kotlin + * class User( + * val _id: ObjectId, + * val name: String, + * ) + * + * users.unsafeCast() // Treat all data as arbitrary BSON documents + * .find { BsonDocument.get("oldField") ne null } + * .forEach { + * println(it["oldField"]?.decodeString()) + * } + * ``` + */ +inline fun MongoCollection<*>.unsafeCast(): MongoCollection = + unsafeCast(typeOf()) diff --git a/test/src/commonMain/kotlin/operations/InsertOperations.test.kt b/test/src/commonMain/kotlin/operations/InsertOperations.test.kt index 4d0451fa..931494f2 100644 --- a/test/src/commonMain/kotlin/operations/InsertOperations.test.kt +++ b/test/src/commonMain/kotlin/operations/InsertOperations.test.kt @@ -14,14 +14,21 @@ * limitations under the License. */ +@file:OptIn(LowLevelApi::class) + package opensavvy.ktmongo.tests.api.operations import kotlinx.serialization.Serializable import opensavvy.ktmongo.api.MongoClient +import opensavvy.ktmongo.api.unsafeCast +import opensavvy.ktmongo.bson.BsonDocument +import opensavvy.ktmongo.bson.decode import opensavvy.ktmongo.bson.types.ObjectId +import opensavvy.ktmongo.dsl.LowLevelApi import opensavvy.ktmongo.dsl.command.InsertOne import opensavvy.ktmongo.dsl.command.errors.MongoWriteException import opensavvy.ktmongo.dsl.options.WriteConcern +import opensavvy.ktmongo.dsl.path.Field import opensavvy.ktmongo.tests.api.collection import opensavvy.prepared.suite.Prepared import opensavvy.prepared.suite.SuiteDsl @@ -96,6 +103,32 @@ fun SuiteDsl.verifyInsertOperations( } } + test("Decreased type safety") { + val docs = collection().unsafeCast() + val id = docs.newId() + + docs.insertOne( + docs.factory.buildDocument { + writeString("name", "Bob") + writeString("other", "value") + writeObjectId("_id", id) + } + ) + + check(docs.count() == 1L) + check(collection().find().toList() == listOf(InsertOperationsUser(id, "Bob"))) // The additional field is not visible after deserialization + + val withUnsafeField = collection().find { + Field.unsafe("other") eq "value" + } + + val withUnsafeDocument = docs.find { + BsonDocument.get("other") eq "value" + } + + check(withUnsafeField.toList() == withUnsafeDocument.toList().map { it.decode() }) + } + suite("Options") { test("insertOne • Write concern") { collection().insertOne(