diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml deleted file mode 100644 index 0c78809a..00000000 --- a/.github/workflows/linter.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: Linter (detekt) -on: pull_request -jobs: - detekt: - runs-on: ubuntu-latest - steps: - - name: "checkout" - uses: actions/checkout@v4 - - name: "detekt" - uses: natiginfo/action-detekt-all@1.23.6 diff --git a/build.gradle.kts b/build.gradle.kts index ad0f2011..85d552c4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -62,6 +62,13 @@ dependencies { implementation("com.zaxxer:HikariCP:5.1.0") implementation("org.postgresql:postgresql:42.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.mongodb:mongodb-driver-kotlin-coroutine:4.10.1") + testImplementation("io.mockk:mockk:1.13.10") testImplementation("io.kotest:kotest-runner-junit5:5.8.1") + implementation(kotlin("stdlib-jdk8")) +} +kotlin { + jvmToolchain(15) } diff --git a/settings.gradle.kts b/settings.gradle.kts index 26d50355..f572821b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,9 @@ +pluginManagement { + plugins { + kotlin("jvm") version "2.0.0" + } +} +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} rootProject.name = "mixdrinks" diff --git a/src/main/kotlin/org/mixdrinks/Application.kt b/src/main/kotlin/org/mixdrinks/Application.kt index c303c07f..c4abeb62 100644 --- a/src/main/kotlin/org/mixdrinks/Application.kt +++ b/src/main/kotlin/org/mixdrinks/Application.kt @@ -12,6 +12,7 @@ import io.ktor.server.netty.Netty import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.ktor.server.plugins.cors.routing.CORS import org.jetbrains.exposed.sql.Database +import org.mixdrinks.mongo.Mongo import org.mixdrinks.plugins.configureCache import org.mixdrinks.plugins.configureRedirectMiddleWare import org.mixdrinks.plugins.configureRouting @@ -60,10 +61,12 @@ fun main() { pageSize = config.property("ktor.settings.pageSize").getString().toInt() ) + val mongoString = config.property("ktor.database.mongoString").getString() val appVersion = config.property("ktor.app.version").getString() service(appVersion) - api(appSettings) + + api(appSettings, mongoString) } val port = config.property("ktor.connector.port").getString().toInt() diff --git a/src/main/kotlin/org/mixdrinks/cocktails/visit/VisitRouting.kt b/src/main/kotlin/org/mixdrinks/cocktails/visit/VisitRouting.kt index 94643175..05164ef1 100644 --- a/src/main/kotlin/org/mixdrinks/cocktails/visit/VisitRouting.kt +++ b/src/main/kotlin/org/mixdrinks/cocktails/visit/VisitRouting.kt @@ -10,16 +10,17 @@ import org.jetbrains.exposed.sql.update import org.mixdrinks.cocktails.score.scoreCocktailsChangeResponse import org.mixdrinks.data.Cocktail import org.mixdrinks.data.CocktailsTable +import org.mixdrinks.mongo.Mongo import org.mixdrinks.view.error.QueryRequireException import org.mixdrinks.view.v2.getCocktailId -fun Routing.visitRouting() { +fun Routing.visitRouting(mongo: Mongo) { post("v2/cocktails/visit") { - this.call.incVisitMethod() + this.call.incVisitMethod(mongo) } } -private suspend fun ApplicationCall.incVisitMethod() { +private suspend fun ApplicationCall.incVisitMethod(mongo: Mongo) { val id = this.getCocktailId() transaction { @@ -29,6 +30,8 @@ private suspend fun ApplicationCall.incVisitMethod() { } } + mongo.incVisitCount(id.id) + this.respond( transaction { scoreCocktailsChangeResponse( diff --git a/src/main/kotlin/org/mixdrinks/mongo/Mongo.kt b/src/main/kotlin/org/mixdrinks/mongo/Mongo.kt new file mode 100644 index 00000000..8511bd9b --- /dev/null +++ b/src/main/kotlin/org/mixdrinks/mongo/Mongo.kt @@ -0,0 +1,41 @@ +package org.mixdrinks.mongo + +import com.mongodb.client.model.Filters +import com.mongodb.client.model.Updates +import com.mongodb.kotlin.client.coroutine.MongoClient +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + +class Mongo(connectionString: String) { + + private val client by lazy { + MongoClient.create(connectionString = connectionString) + } + + private val database by lazy { + client.getDatabase("mixdrinks") + } + + init { + GlobalScope.launch { + database.listCollections().collect { + println(it) + } + } + } + + data class MongoCocktail( + val id: Int, + val visitCount: Int, + ) + + suspend fun incVisitCount(id: Int) { + val queryParam = Filters.eq("id", id) + val updateParams = Updates.inc("visitCount", 1) + database.getCollection(collectionName = "cocktails") + .updateOne( + filter = queryParam, + update = updateParams, + ) + } +} diff --git a/src/main/kotlin/org/mixdrinks/view/v2/Api.kt b/src/main/kotlin/org/mixdrinks/view/v2/Api.kt index 27d13da0..1d8743eb 100644 --- a/src/main/kotlin/org/mixdrinks/view/v2/Api.kt +++ b/src/main/kotlin/org/mixdrinks/view/v2/Api.kt @@ -5,6 +5,7 @@ import io.ktor.server.routing.routing import org.mixdrinks.cocktails.CocktailMapper import org.mixdrinks.cocktails.visit.visitRouting import org.mixdrinks.domain.CocktailSelector +import org.mixdrinks.mongo.Mongo import org.mixdrinks.view.cocktail.cocktails import org.mixdrinks.view.controllers.filter.FilterCache import org.mixdrinks.view.controllers.filter.FilterSource @@ -21,23 +22,30 @@ import org.mixdrinks.view.snapshot.SnapshotCreator import org.mixdrinks.view.snapshot.sitemap.SiteMapCreator import org.mixdrinks.view.snapshot.snapshot -fun Application.api(appSettings: AppSettings) { +fun Application.api( + appSettings: AppSettings, + mongoString: String +) { + this.routing { + visitRouting(Mongo(mongoString)) + } + println("Visit routing initialized.") + + this.score(appSettings) + + this.cocktails() + this.items() + this.appSetting(appSettings) + val filterCache = FilterCache() + val cocktailSelector = CocktailSelector(filterCache.filterGroups) val snapshotCreator = SnapshotCreator(filterCache) this.filterMetaInfo(FilterSource(filterCache)) val searchResponseBuilder = SearchResponseBuilder(filterCache, cocktailSelector, DescriptionBuilder(), CocktailMapper()) - this.score(appSettings) - - this.routing { - visitRouting() - } - this.cocktails() - this.items() - this.appSetting(appSettings) this.snapshot(snapshotCreator, SiteMapCreator()) val searchSlugResponseBuilder = SearchSlugResponseBuilder(filterCache, searchResponseBuilder) diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 40dec5e6..30cd8067 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -3,6 +3,7 @@ ktor { url = ${?DB_URL} user = ${?DB_USER} password = ${?DB_PASSWORD} + mongoString = ${?MONGO_STRING} } connector { port = 8080 diff --git a/src/test/kotlin/org/fullness/endtoend/CocktailEndToEndTests.kt b/src/test/kotlin/org/fullness/endtoend/CocktailEndToEndTests.kt deleted file mode 100644 index 5bcd1dae..00000000 --- a/src/test/kotlin/org/fullness/endtoend/CocktailEndToEndTests.kt +++ /dev/null @@ -1,98 +0,0 @@ -package org.fullness.endtoend - -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import io.ktor.client.request.get -import io.ktor.client.request.post -import io.ktor.client.statement.bodyAsText -import io.ktor.http.HttpStatusCode -import io.ktor.serialization.kotlinx.json.json -import io.ktor.server.application.install -import io.ktor.server.plugins.contentnegotiation.ContentNegotiation -import io.ktor.server.routing.routing -import io.ktor.server.testing.ApplicationTestBuilder -import io.ktor.server.testing.testApplication -import kotlinx.serialization.json.Json -import org.createDataBase -import org.jetbrains.exposed.sql.Database -import org.jetbrains.exposed.sql.insert -import org.jetbrains.exposed.sql.transactions.TransactionManager -import org.jetbrains.exposed.sql.transactions.transaction -import org.mixdrinks.cocktails.score.CocktailScoreChangeResponse -import org.mixdrinks.cocktails.visit.visitRouting -import org.mixdrinks.data.CocktailsTable -import org.mixdrinks.view.controllers.score.score -import org.mixdrinks.view.controllers.settings.AppSettings - -class CocktailEndToEndTests : FunSpec({ - - @Suppress("MemberVisibilityCanBePrivate") val database = - Database.connect("jdbc:h2:mem:test_db_22;DB_CLOSE_DELAY=-1;IGNORECASE=true;") - - afterSpec { - TransactionManager.closeAndUnregister(database) - } - - test("Verify ratting return new") { - prepareData( - listOf( - MockCocktailVisit( - id = 0, - visitCount = 10, - ) - ) - ) - - testApplication { - application { - install(ContentNegotiation) { - json() - } - this.score(AppSettings(1, 1, 1)) - this.routing { - visitRouting() - } - } - - verifyVisitCount(10) - - client.post("v2/cocktails/visit?id=0") - - verifyVisitCount(11) - } - } -}) - -private suspend fun ApplicationTestBuilder.verifyVisitCount(count: Int) { - client.get("v2/cocktails/ratting").let { response -> - response.status shouldBe HttpStatusCode.OK - - val result = Json.decodeFromString>(response.bodyAsText()) - - result[0]?.visitCount shouldBe count - } -} - -private data class MockCocktailVisit( - val id: Int, - val visitCount: Int, -) - - -private fun prepareData(cocktails: List) { - transaction { - createDataBase() - - cocktails.forEach { cocktail -> - CocktailsTable.insert { - it[id] = cocktail.id - it[name] = "" - it[steps] = arrayOf() - it[visitCount] = cocktail.visitCount - it[ratingCount] = 10 - it[ratingValue] = 500 - it[slug] = "" - } - } - } -} diff --git a/src/test/kotlin/org/fullness/endtoend/RattingEndToEndTests.kt b/src/test/kotlin/org/fullness/endtoend/RattingEndToEndTests.kt deleted file mode 100644 index 1d79c448..00000000 --- a/src/test/kotlin/org/fullness/endtoend/RattingEndToEndTests.kt +++ /dev/null @@ -1,166 +0,0 @@ -package org.fullness.endtoend - -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import io.ktor.client.request.post -import io.ktor.client.request.setBody -import io.ktor.client.statement.bodyAsText -import io.ktor.http.ContentType -import io.ktor.http.contentType -import io.ktor.serialization.kotlinx.json.json -import io.ktor.server.application.install -import io.ktor.server.auth.Authentication -import io.ktor.server.plugins.contentnegotiation.ContentNegotiation -import io.ktor.server.routing.routing -import io.ktor.server.testing.testApplication -import kotlinx.serialization.json.Json -import org.createDataBase -import org.jetbrains.exposed.sql.Database -import org.jetbrains.exposed.sql.insert -import org.jetbrains.exposed.sql.transactions.TransactionManager -import org.jetbrains.exposed.sql.transactions.transaction -import org.mixdrinks.cocktails.CocktailMapper -import org.mixdrinks.cocktails.score.CocktailScoreChangeResponse -import org.mixdrinks.cocktails.visit.visitRouting -import org.mixdrinks.data.CocktailsTable -import org.mixdrinks.view.controllers.score.score -import org.mixdrinks.view.controllers.settings.AppSettings - -class RattingEndToEndTests : FunSpec({ - - @Suppress("MemberVisibilityCanBePrivate") val database = - Database.connect("jdbc:h2:mem:test_db_22;DB_CLOSE_DELAY=-1;IGNORECASE=true;") - - afterSpec { - TransactionManager.closeAndUnregister(database) - } - - test("Verify visit count change") { - prepareData( - listOf( - MockCocktailRatting( - id = 0, - visitCount = 10, - ratingCount = 10, - ratingValue = 25, - ) - ) - ) - - testApplication { - application { - install(ContentNegotiation) { - json() - } - this.routing { - visitRouting() - } - } - - client.post("v2/cocktails/visit?id=0").let { response -> - val result = Json.decodeFromString(response.bodyAsText()) - result.rating shouldBe 2.5 - result.visitCount shouldBe 11 - } - } - } - - test("Verify ratting change") { - prepareData( - listOf( - MockCocktailRatting( - id = 0, - visitCount = 10, - ratingCount = 10, - ratingValue = 25, - ), MockCocktailRatting( - id = 1, - visitCount = 1, - ratingCount = 0, - ratingValue = null, - ) - ) - ) - - testApplication { - application { - install(ContentNegotiation) { - json() - } - val appSetting = AppSettings(1, 5, 10) - score(appSetting) - } - - client.post("v2/cocktails/score?id=0") { - contentType(ContentType.Application.Json) - setBody("{\"value\":5}") - }.let { response -> - val result = Json.decodeFromString(response.bodyAsText()) - result.rating shouldBe 2.7F - result.visitCount shouldBe 10 - } - } - } - - test("Verify ratting change from null") { - prepareData( - listOf( - MockCocktailRatting( - id = 0, - visitCount = 10, - ratingCount = 10, - ratingValue = 25, - ), MockCocktailRatting( - id = 1, - visitCount = 1, - ratingCount = 0, - ratingValue = null, - ) - ) - ) - - testApplication { - application { - install(ContentNegotiation) { - json() - } - val appSetting = AppSettings(1, 5, 10) - score(appSetting) - } - - client.post("v2/cocktails/score?id=1") { - contentType(ContentType.Application.Json) - setBody("{\"value\":4}") - }.let { response -> - val result = Json.decodeFromString(response.bodyAsText()) - result.rating shouldBe 4F - result.visitCount shouldBe 1 - } - } - } -}) - -private data class MockCocktailRatting( - val id: Int, - val visitCount: Int, - val ratingCount: Int, - val ratingValue: Int?, -) - -private fun prepareData(cocktails: List) { - transaction { - createDataBase() - - cocktails.forEach { cocktail -> - CocktailsTable.insert { - it[id] = cocktail.id - it[name] = "" - it[steps] = arrayOf() - it[visitCount] = cocktail.visitCount - it[ratingCount] = cocktail.ratingCount - it[ratingValue] = cocktail.ratingValue - it[slug] = "cocktail-${cocktail.id}" - } - } - } -}