Skip to content

Commit

Permalink
Merge pull request #43 from flytegg/dev/mongo
Browse files Browse the repository at this point in the history
merge: dev/mongo into master - add MongoDB framework
  • Loading branch information
joshbker authored Feb 11, 2024
2 parents 99c20aa + da8b9cf commit 404b1fe
Show file tree
Hide file tree
Showing 6 changed files with 404 additions and 27 deletions.
48 changes: 45 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Maven
<dependency>
<groupId>gg.flyte</groupId>
<artifactId>twilight</artifactId>
<version>1.0.39</version>
<version>1.1.0-SNAPSHOT</version>
</dependency>
```

Expand All @@ -29,14 +29,14 @@ maven {
url "https://repo.flyte.gg/releases"
}
implementation "gg.flyte:twilight:1.0.39"
implementation "gg.flyte:twilight:1.1.0-SNAPSHOT"
```

Gradle (Kotlin DSL)
```kotlin
maven("https://repo.flyte.gg/releases")

implementation("gg.flyte:twilight:1.0.39")
implementation("gg.flyte:twilight:1.1.0-SNAPSHOT")
```

Certain features of Twilight require configuration, which can be done via the Twilight class. To setup a Twilight class instance, you can use the `twilight` function as shown below:
Expand Down Expand Up @@ -289,6 +289,48 @@ MongoDB.collection("my-collection")
```
And use the standard features of the Mongo Java Driver with your `MongoCollection`.

**OR** you can use some of our custom features, making communicating with a Mongo database infinitely easier. Here's how you do it:

```kt
class Profile(
@field:Id val id: UUID,
val name: String
) : MongoSerializable
```

What's happening here? We're declaring what should be used as the key identifier for our class in the database, we can do so by annotating a field with `@field:Id`.

We also implement an interface `MongoSerializable`. This gives us access to a bunch of methods which make our lives really easy when it comes to moving between our class instance and our database.

For example, I could do the following:
```kt
val profile = Profile(UUID.randomUUID(), "Name")

profile.save()
// this returns a CompletableFuture of the UpdateResult
// or we could do profile.saveSync() if we need it to be sync, does not return wrapped by a CompletableFuture

profile.delete()
// this returns a CompletableFuture of the DeleteResult
// same as save, can do profile.deleteSync() for sync, does not return wrapped by a CompletableFuture
```

If we ever want to find and load and instance of our class from the database, we can use some functions from the TwilightMongoCollection:

```kt
val collection = MongoDB.collection<Profile>() // by default this assumes the name of the collection is the plural camel case of the type, f.x. Profile -> profiles, SomeExampleThing -> someExampleThings
// you can specify the name of the collection if you wish it to be different like so
val collection = MongoDB.collection<Profile>("myCollection")
collection.find() // returns a CompletableFuture<MongoIterable<Profile>>
collection.find(BsonFilter) // returns a CompletableFuture<MongoIterable<Profile>>
collection.findById(id) // id must be the same type as the field marked as the id on the class, returns a CompletableFuture<MongoIterable<Profile>>
collection.delete(BsonFilter) // returns a CompletableFuture<DeleteResult>
collection.deleteById(id) // id must be the same type as the field marked as the id on the class, returns a CompletableFuture<DeleteResult>
// all of these have sync versions which follow the same pattern, f.x. collection.findSync(), where the return value is the same as the async version, just not wrapped by a CompletableFuture
```

If we need something that isn't already wrapped by the TwilightMongoCollection, it exposes us the MongoCollection of Documents, which we can get with `collection.documents`.

### Ternary Operator
There is a basic ternary operator implementation added which can be used like so:
```kotlin
Expand Down
5 changes: 3 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ plugins {
}

group = "gg.flyte"
version = "1.0.39"
version = "1.1.0-SNAPSHOT"

repositories {
mavenLocal()
Expand All @@ -26,8 +26,9 @@ dependencies {
compileOnly("io.papermc.paper:paper-api:1.20.4-R0.1-SNAPSHOT")

implementation("io.github.cdimascio:dotenv-kotlin:6.4.1")
implementation("org.mongodb:mongodb-driver-sync:4.9.0")
implementation("org.mongodb:mongodb-driver-kotlin-sync:4.11.0")
implementation("com.google.code.gson:gson:2.10.1")
implementation(kotlin("reflect"))

// api("com.github.okkero:Skedule:v1.2.6")
// implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0-RC2")
Expand Down
188 changes: 167 additions & 21 deletions src/main/kotlin/gg/flyte/twilight/data/MongoDB.kt
Original file line number Diff line number Diff line change
@@ -1,44 +1,190 @@
package gg.flyte.twilight.data

import com.mongodb.ConnectionString
import com.mongodb.MongoClientSettings
import com.mongodb.MongoClientSettings.getDefaultCodecRegistry
import com.mongodb.client.MongoClient
import com.mongodb.client.MongoClients
import com.mongodb.client.MongoCollection
import com.mongodb.client.MongoDatabase
import com.mongodb.client.model.Filters.eq
import com.mongodb.client.model.ReplaceOptions
import com.mongodb.client.result.DeleteResult
import com.mongodb.client.result.UpdateResult
import com.mongodb.kotlin.client.MongoClient
import com.mongodb.kotlin.client.MongoCollection
import com.mongodb.kotlin.client.MongoDatabase
import com.mongodb.kotlin.client.MongoIterable
import gg.flyte.twilight.Twilight
import gg.flyte.twilight.data.MongoDB.executor
import gg.flyte.twilight.environment.Environment
import gg.flyte.twilight.gson.GSON
import gg.flyte.twilight.gson.toJson
import gg.flyte.twilight.string.Case
import gg.flyte.twilight.string.formatCase
import gg.flyte.twilight.string.pluralize
import org.bson.Document
import org.bson.UuidRepresentation
import org.bson.codecs.configuration.CodecRegistries.fromProviders
import org.bson.codecs.configuration.CodecRegistries.fromRegistries
import org.bson.codecs.pojo.PojoCodecProvider
import org.bson.conversions.Bson
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1
import kotlin.reflect.KType
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaField
import kotlin.reflect.jvm.javaType

object MongoDB {

private val codecRegistry = fromRegistries(
getDefaultCodecRegistry(),
fromProviders(PojoCodecProvider.builder().automatic(true).build())
)

private lateinit var client: MongoClient
private lateinit var database: MongoDatabase
internal lateinit var database: MongoDatabase
internal val executor: Executor = Executors.newCachedThreadPool()

fun mongo(mongo: Settings) {
client = MongoClients.create(mongo.uri)
client = MongoClient.create(
MongoClientSettings.builder()
.uuidRepresentation(UuidRepresentation.STANDARD)
.codecRegistry(fromProviders(getDefaultCodecRegistry()))
.applyConnectionString(ConnectionString(mongo.uri))
.build()
)
database = client.getDatabase(mongo.database + if (Twilight.usingEnv && Environment.isDev()) "-dev" else "")
.withCodecRegistry(codecRegistry)
}

fun collection(name: String): MongoCollection<Document> {
return database.getCollection(name)
}
fun collection(name: String): MongoCollection<Document> = database.getCollection(name)

fun <T> collection(name: String, `class`: Class<T>): MongoCollection<T> {
return database.getCollection(name, `class`)
}
@Deprecated("Use collection(clazz: KClass<out MongoSerializable>)", ReplaceWith("collection(`class`)"))
fun <T : Any> collection(name: String, `class`: Class<T>): MongoCollection<T> =
database.getCollection(name, `class`)

class Settings {
var uri: String = if (Twilight.usingEnv) Environment.get("MONGO_URI") else ""
var database: String = if (Twilight.usingEnv) Environment.get("MONGO_DATABASE") else ""
}

}
val collections = mutableMapOf<KClass<out MongoSerializable>, TwilightMongoCollection<out MongoSerializable>>()

inline fun <reified T : MongoSerializable> collection(
name: String = T::class.simpleName!!.pluralize().formatCase(Case.CAMEL)
): TwilightMongoCollection<T> {
@Suppress("unchecked_cast")
return collections.getOrPut(T::class) { TwilightMongoCollection(T::class, name) } as TwilightMongoCollection<T>
}

fun collection(clazz: KClass<out MongoSerializable>): TwilightMongoCollection<out MongoSerializable> {
return collections.getOrPut(clazz) {
TwilightMongoCollection(
clazz,
clazz.simpleName!!.pluralize().formatCase(Case.CAMEL)
)
}
}

}

class TwilightMongoCollection<T : MongoSerializable>(
private val clazz: KClass<out MongoSerializable>,
val name: String
) {

val idField = IdField(clazz)
val documents: MongoCollection<Document> = MongoDB.database.getCollection(name, Document::class.java)

fun saveSync(serializable: MongoSerializable): UpdateResult = with(serializable.toDocument()) {
documents.replaceOne(
eq(idField.name, this[idField.name]),
this,
ReplaceOptions().upsert(true)
)
}

fun save(serializable: MongoSerializable): CompletableFuture<UpdateResult> =
CompletableFuture.supplyAsync({ saveSync(serializable) }, executor)

fun findSync(filter: Bson? = null): MongoIterable<T> =
(if (filter == null) documents.find() else documents.find(filter)).map {
@Suppress("unchecked_cast")
GSON.fromJson(it.toJson(), clazz.java) as T
}

fun find(filter: Bson? = null): CompletableFuture<MongoIterable<T>> =
CompletableFuture.supplyAsync({ findSync(filter) }, executor)

fun findByIdSync(id: Any): MongoIterable<T> {
require(id::class.javaObjectType == idField.type.javaType) {
"id must be of type ${idField.type} (Java: ${idField.type.javaType})"
}
return findSync(eq(idField.name, id))
}

fun findById(id: Any): CompletableFuture<MongoIterable<T>> =
CompletableFuture.supplyAsync({ findByIdSync(id) }, executor)

fun deleteSync(filter: Bson): DeleteResult = documents.deleteMany(filter)

fun delete(filter: Bson): CompletableFuture<DeleteResult> =
CompletableFuture.supplyAsync({ deleteSync(filter) }, executor)

fun deleteByIdSync(id: Any): DeleteResult {
require(id::class.javaObjectType == idField.type.javaType) {
"id must be of type ${idField.type} (Java: ${idField.type.javaType})"
}
return deleteSync(eq(idField.name, id))
}

fun deleteById(id: Any): CompletableFuture<DeleteResult> =
CompletableFuture.supplyAsync({ deleteByIdSync(id) }, executor)

}

interface MongoSerializable {
fun saveSync(): UpdateResult = MongoDB.collection(this::class).saveSync(this)

fun save(): CompletableFuture<UpdateResult> = MongoDB.collection(this::class).save(this)

fun deleteSync(): DeleteResult = with(MongoDB.collection(this::class)) {
deleteSync(eq(idField.name, idField.value(this@MongoSerializable)))
}

fun delete(): CompletableFuture<DeleteResult> = with(MongoDB.collection(this::class)) {
delete(eq(idField.name, idField.value(this@MongoSerializable)))
}

fun toDocument(): Document = Document.parse(toJson())
}

@Target(AnnotationTarget.FIELD)
annotation class Id

data class IdField(val clazz: KClass<out MongoSerializable>) {

private val idField: KProperty1<out MongoSerializable, *>
val name: String
val type: KType

init {
val idFields = clazz.memberProperties.filter { it.javaField?.isAnnotationPresent(Id::class.java) == true }
println(idFields)

require(idFields.size == 1) {
when (idFields.size) {
0 -> "Class does not have a field annotated with @Id"
else -> "Class must not have more than one field annotated with @Id"
}
}

idField = idFields.first()

name = idField.name
type = idField.returnType
}

@Suppress("unchecked_cast")
fun value(instance: MongoSerializable): Any = (idField as KProperty1<Any, *>).get(instance)
?: throw IllegalStateException("Field annotated with @Id must not be null")

}

fun Any.toDocument(): Document = Document.parse(toJson())

infix fun <V> KProperty<V>.eq(other: Any): Bson = eq(this.name, other)
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package gg.flyte.twilight.data.service

import com.google.gson.JsonParser
import com.mongodb.MongoException
import com.mongodb.client.MongoCollection
import com.mongodb.client.model.Filters
import com.mongodb.kotlin.client.MongoCollection
import gg.flyte.twilight.Twilight
import gg.flyte.twilight.data.MongoDB
import gg.flyte.twilight.environment.Environment
Expand Down
Loading

0 comments on commit 404b1fe

Please sign in to comment.