Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

/users HTTP endpoint + async transactions #9

Merged
merged 1 commit into from
May 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ dependencies {
testImplementation(libs.ktor.server.tests.jvm)
testImplementation(libs.testcontainers.postgres)
testImplementation(libs.testcontainers.jupiter)
testImplementation(libs.kotlinx.coroutines.test)
}

tasks.test {
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ liquibase = "org.liquibase:liquibase-core:4.21.1"

postgres = "org.postgresql:postgresql:42.6.0"

kotlinx-coroutines-test="org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.0"

kotlin-scripting-compiler-embeddable = { group = "org.jetbrains.kotlin", name = "kotlin-scripting-compiler-embeddable", version.ref = "kotlin" }

kotlin-serialization = { group = "org.jetbrains.kotlin", name = "kotlin-serialization", version.ref = "kotlin" }
Expand Down
12 changes: 10 additions & 2 deletions src/main/kotlin/io/ktor/answers/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,32 @@ package io.ktor.answers

import io.ktor.answers.plugins.*
import io.ktor.server.application.*
import liquibase.Liquibase
import liquibase.Scope
import liquibase.command.CommandScope
import liquibase.command.core.UpdateCommandStep.CHANGELOG_FILE_ARG
import liquibase.command.core.UpdateCommandStep.COMMAND_NAME
import liquibase.command.core.helpers.DbUrlConnectionCommandStep.DATABASE_ARG
import liquibase.database.DatabaseFactory
import liquibase.database.jvm.JdbcConnection
import liquibase.resource.ClassLoaderResourceAccessor
import org.jetbrains.exposed.sql.Database
import java.sql.DriverManager


fun Application.module() {
migrateDb()
connectToDb()
configureSerialization()
configureRouting()
}

fun Application.connectToDb() = with(environment.config){
Database.connect(
property("database.url").getString(),
user = property("database.username").getString(),
password = property("database.password").getString()
)
}

fun Application.migrateDb() = with(environment.config) {
migrate(
property("database.url").getString(),
Expand Down
48 changes: 38 additions & 10 deletions src/main/kotlin/io/ktor/answers/db/UserRepository.kt
Original file line number Diff line number Diff line change
@@ -1,21 +1,47 @@
package io.ktor.answers.db

import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import io.ktor.answers.plugins.*
import kotlinx.coroutines.Dispatchers
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SortOrder.ASC
import org.jetbrains.exposed.sql.kotlin.datetime.date
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction

object UserRepository {
fun allUsers(): List<User> {
return transaction { User.find { UserTable.active eq true }.notForUpdate().toList() }
class UserRepository {
private val defaultQueryParams = CommonQueryParams(0, 20, null, null)
suspend fun allUsers(
parsed: CommonQueryParams = defaultQueryParams,
sortBy: String = "name",
order: SortOrder = ASC
): List<User> = asyncTransaction {
User
.find {
val active = UserTable.active eq true
val from = if (parsed.fromDate != null) (UserTable.createdAt.date() greaterEq parsed.fromDate) else null
val to = if (parsed.toDate != null) UserTable.createdAt.date() lessEq parsed.toDate else null
sequenceOf(active, to, from).filterNotNull().reduce { acc, op -> acc and op }
}
.apply {
if (parsed.page != null) limit(parsed.pageSize, parsed.pageSize.toLong() * (parsed.page - 1))
else limit(parsed.pageSize)
}
.orderBy(
when (sortBy) {
"name" -> UserTable.name
"creation" -> UserTable.createdAt
else -> error("Unsupported sort column: $sortBy")
} to order
)
.toList()
}

fun userById(id: Long): User? = transaction {
suspend fun userById(id: Long): User? = asyncTransaction {
User
.find { (UserTable.id eq id) and (UserTable.active eq true) }
.limit(1).firstOrNull()
}

fun userAnswers(userId: Long) = transaction {
suspend fun userAnswers(userId: Long): List<Answer> = asyncTransaction {
Answer
.wrapRows(
UserTable
Expand All @@ -27,7 +53,7 @@ object UserRepository {
.toList()
}

fun userComments(userId: Long) = transaction {
suspend fun userComments(userId: Long): List<Comment> = asyncTransaction {
Comment
.wrapRows(
CommentTable
Expand All @@ -39,7 +65,7 @@ object UserRepository {
.toList()
}

fun userQuestions(userId: Long) = transaction {
suspend fun userQuestions(userId: Long): List<Question> = asyncTransaction {
Question
.wrapRows(
QuestionTable
Expand All @@ -52,3 +78,5 @@ object UserRepository {
}
}

suspend fun <T> asyncTransaction(block: Transaction.() -> T): T =
newSuspendedTransaction(Dispatchers.IO, statement = block)
36 changes: 36 additions & 0 deletions src/main/kotlin/io/ktor/answers/plugins/Routing.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package io.ktor.answers.plugins

import io.ktor.answers.db.*
import io.ktor.server.routing.*
import io.ktor.server.response.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.http.*
import io.ktor.server.application.*
import kotlinx.datetime.LocalDate
import kotlinx.datetime.toLocalDate
import org.jetbrains.exposed.sql.SortOrder

fun Application.configureRouting() {
install(StatusPages) {
Expand All @@ -16,5 +20,37 @@ fun Application.configureRouting() {
get("/") {
call.respondText("Hello World!")
}
route("/users") {
get {
val queryParams = call.request.queryParameters
queryParams.parsed()
val sortBy = queryParams["sortBy"] ?: "name"
val order = (queryParams["order"] ?: "asc").toSortOrder()

call.respond(UserRepository().allUsers(queryParams.parsed(), sortBy, order))
}
}
}
}

private fun String.toSortOrder(): SortOrder = when (this) {
"asc" -> SortOrder.ASC
"desc" -> SortOrder.DESC
else -> error("Unsupported sort order: $this")
}

private fun Parameters.parsed(): CommonQueryParams {
val page = this["page"]?.toInt()
val pageSize = this["pagesize"]?.toInt() ?: 20
val fromDate = this["fromdate"]?.toLocalDate()
val toDate = this["todate"]?.toLocalDate()
return CommonQueryParams(page, pageSize, fromDate, toDate)
}

data class CommonQueryParams(
val page: Int?,
val pageSize: Int,
val fromDate: LocalDate?,
val toDate: LocalDate?,
)

22 changes: 14 additions & 8 deletions src/test/kotlin/io/ktor/answers/DbTest.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package io.ktor.answers

import io.ktor.answers.db.*
import kotlinx.coroutines.test.runTest
import org.jetbrains.exposed.sql.deleteAll
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.jetbrains.exposed.sql.transactions.transaction
import org.testcontainers.junit.jupiter.Testcontainers
import kotlin.random.Random
Expand All @@ -10,11 +13,12 @@ import kotlin.test.assertEquals

@Testcontainers
class DbTest : AbstractDbTest() {
private val userRepository = UserRepository()

@Test
fun `deactivated users should not be in the 'all users' response`() {
val current = UserRepository.allUsers().size
transaction {
fun `deactivated users should not be in the 'all users' response`() = runTest {
val current = userRepository.allUsers().size
newSuspendedTransaction {
User.new {
name = Random.nextString(7)
email = Random.email()
Expand All @@ -23,13 +27,14 @@ class DbTest : AbstractDbTest() {
displayName = Random.nextString(7)
}
}
assertEquals(current, UserRepository.allUsers().size)
assertEquals(current, userRepository.allUsers().size)
newSuspendedTransaction { UserTable.deleteAll() }
}

@Test
fun `insertion of an active user increases number of users in DB by 1`() {
val current = UserRepository.allUsers().size
transaction {
fun `insertion of an active user increases number of users in DB by 1`() = runTest {
val current = userRepository.allUsers().size
newSuspendedTransaction {
User.new {
name = Random.nextString(7)
email = Random.email()
Expand All @@ -38,7 +43,8 @@ class DbTest : AbstractDbTest() {
displayName = Random.nextString(7)
}
}
assertEquals(current + 1, UserRepository.allUsers().size)
assertEquals(current + 1, userRepository.allUsers().size)
newSuspendedTransaction { UserTable.deleteAll() }
}
}

Expand Down