Cleaner code
This commit is contained in:
parent
0993d93ccc
commit
e03e12110b
@ -16,7 +16,6 @@ jwt:
|
||||
validity: 1
|
||||
unit: HOURS
|
||||
refresh:
|
||||
|
||||
secret: ${JWT_REFRESH_SECRET=-wWchkx44YGig4Q5Z7b7+E/3ymGEGd6PS7UGedMul3bg=} # Can be generated with `openssl rand -base64 32`
|
||||
validity: 15
|
||||
unit: DAYS
|
||||
|
||||
21
api/src/Configuration.kt
Normal file
21
api/src/Configuration.kt
Normal file
@ -0,0 +1,21 @@
|
||||
package be.vandewalleh
|
||||
|
||||
import com.sksamuel.hoplite.Masked
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
data class Config(val database: DatabaseConfig, val server: ServerConfig, val jwt: JwtConfig) {
|
||||
override fun toString(): String {
|
||||
return """
|
||||
Config(
|
||||
database=$database,
|
||||
server=$server,
|
||||
jwt=$jwt
|
||||
)
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
data class DatabaseConfig(val host: String, val port: Int, val name: String, val username: String, val password: Masked)
|
||||
data class ServerConfig(val host: String, val port: Int, val cors: Boolean)
|
||||
data class JwtConfig(val auth: Jwt, val refresh: Jwt)
|
||||
data class Jwt(val validity: Long, val unit: TimeUnit, val secret: Masked)
|
||||
@ -1,24 +1,40 @@
|
||||
package be.vandewalleh
|
||||
|
||||
import be.vandewalleh.features.BcryptPasswordHash
|
||||
import be.vandewalleh.features.PasswordHash
|
||||
import be.vandewalleh.features.configurationModule
|
||||
import be.vandewalleh.migrations.Migration
|
||||
import be.vandewalleh.services.serviceModule
|
||||
import me.liuwj.ktorm.database.*
|
||||
import be.vandewalleh.auth.AuthenticationModule
|
||||
import be.vandewalleh.auth.SimpleJWT
|
||||
import be.vandewalleh.extensions.ApplicationBuilder
|
||||
import be.vandewalleh.factories.configurationFactory
|
||||
import be.vandewalleh.factories.dataSourceFactory
|
||||
import be.vandewalleh.factories.databaseFactory
|
||||
import be.vandewalleh.factories.simpleJwtFactory
|
||||
import be.vandewalleh.features.*
|
||||
import be.vandewalleh.services.NoteService
|
||||
import be.vandewalleh.services.UserService
|
||||
import org.kodein.di.Kodein
|
||||
import org.kodein.di.generic.bind
|
||||
import org.kodein.di.generic.instance
|
||||
import org.kodein.di.generic.singleton
|
||||
import org.slf4j.Logger
|
||||
import org.kodein.di.generic.*
|
||||
import org.slf4j.LoggerFactory
|
||||
import javax.sql.DataSource
|
||||
|
||||
val mainModule = Kodein.Module("main") {
|
||||
import(serviceModule)
|
||||
import(configurationModule)
|
||||
bind<Logger>() with singleton { LoggerFactory.getLogger("Application") }
|
||||
bind<Migration>() with singleton { Migration(this.kodein) }
|
||||
bind<Database>() with singleton { Database.connect(this.instance<DataSource>()) }
|
||||
bind() from singleton { NoteService(instance()) }
|
||||
bind() from singleton { UserService(instance(), instance()) }
|
||||
|
||||
bind() from singleton { configurationFactory() }
|
||||
|
||||
bind() from setBinding<ApplicationBuilder>()
|
||||
bind<ApplicationBuilder>().inSet() with singleton { ErrorHandler() }
|
||||
bind<ApplicationBuilder>().inSet() with singleton { ContentNegotiationFeature() }
|
||||
bind<ApplicationBuilder>().inSet() with singleton { CorsFeature(instance<Config>().server.cors) }
|
||||
bind<ApplicationBuilder>().inSet() with singleton { AuthenticationModule(instance(tag = "auth")) }
|
||||
bind<ApplicationBuilder>().inSet() with singleton { MigrationHook(instance()) }
|
||||
bind<ApplicationBuilder>().inSet() with singleton { ShutdownDatabaseConnection(instance()) }
|
||||
|
||||
bind<SimpleJWT>(tag = "auth") with singleton { simpleJwtFactory(instance<Config>().jwt.auth) }
|
||||
bind<SimpleJWT>(tag = "refresh") with singleton { simpleJwtFactory(instance<Config>().jwt.refresh) }
|
||||
|
||||
bind() from singleton { LoggerFactory.getLogger("Application") }
|
||||
bind() from singleton { dataSourceFactory(instance<Config>().database) }
|
||||
bind() from singleton { databaseFactory(instance()) }
|
||||
bind() from singleton { Migration(instance()) }
|
||||
|
||||
bind<PasswordHash>() with singleton { BcryptPasswordHash() }
|
||||
}
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
package be.vandewalleh
|
||||
|
||||
import be.vandewalleh.features.Config
|
||||
import be.vandewalleh.features.loadFeatures
|
||||
import be.vandewalleh.migrations.Migration
|
||||
import be.vandewalleh.extensions.ApplicationBuilder
|
||||
import be.vandewalleh.routing.noteRoutes
|
||||
import be.vandewalleh.routing.tagsRoute
|
||||
import be.vandewalleh.routing.userRoutes
|
||||
@ -14,23 +12,20 @@ import io.ktor.server.netty.*
|
||||
import org.kodein.di.Kodein
|
||||
import org.kodein.di.description
|
||||
import org.kodein.di.generic.instance
|
||||
import org.kodein.di.generic.with
|
||||
import org.slf4j.Logger
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
|
||||
fun main() {
|
||||
val kodein = Kodein {
|
||||
import(mainModule)
|
||||
constant("config file") with "/application.prod.yaml" // FIXME
|
||||
}
|
||||
|
||||
val config by kodein.instance<Config>()
|
||||
val logger by kodein.instance<Logger>()
|
||||
logger.info("Running application with configuration $config")
|
||||
logger.debug("Kodein bindings\n${kodein.container.tree.bindings.description()}")
|
||||
val migration by kodein.instance<Migration>()
|
||||
migration.migrate()
|
||||
|
||||
serve(kodein)
|
||||
}
|
||||
|
||||
@ -47,12 +42,19 @@ fun serve(kodein: Kodein) {
|
||||
port = config.server.port
|
||||
}
|
||||
}
|
||||
embeddedServer(Netty, env).start(wait = true)
|
||||
with(embeddedServer(Netty, env)) {
|
||||
addShutdownHook { stop(3, 5, TimeUnit.SECONDS) }
|
||||
start(wait = true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun Application.module(kodein: Kodein) {
|
||||
loadFeatures(kodein)
|
||||
val builders: Set<ApplicationBuilder> by kodein.instance()
|
||||
|
||||
builders.forEach {
|
||||
it.builder(this)
|
||||
}
|
||||
|
||||
routing {
|
||||
route("/user") {
|
||||
|
||||
@ -1,19 +1,17 @@
|
||||
package be.vandewalleh.auth
|
||||
|
||||
import be.vandewalleh.extensions.ApplicationBuilder
|
||||
import io.ktor.application.*
|
||||
import io.ktor.auth.*
|
||||
import io.ktor.auth.jwt.*
|
||||
import org.kodein.di.Kodein
|
||||
import org.kodein.di.generic.instance
|
||||
|
||||
fun Application.authenticationModule(kodein: Kodein) {
|
||||
class AuthenticationModule(authJwt: SimpleJWT) : ApplicationBuilder({
|
||||
install(Authentication) {
|
||||
jwt {
|
||||
val simpleJwt by kodein.instance<SimpleJWT>(tag = "auth")
|
||||
verifier(simpleJwt.verifier)
|
||||
verifier(authJwt.verifier)
|
||||
validate {
|
||||
UserDbIdPrincipal(it.payload.getClaim("id").asInt())
|
||||
}
|
||||
UserIdPrincipal(it.payload.getClaim("id").asInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
package be.vandewalleh.auth
|
||||
|
||||
import io.ktor.auth.Principal
|
||||
|
||||
data class UserDbIdPrincipal(val id: Int) : Principal
|
||||
9
api/src/auth/UserIdPrincipal.kt
Normal file
9
api/src/auth/UserIdPrincipal.kt
Normal file
@ -0,0 +1,9 @@
|
||||
package be.vandewalleh.auth
|
||||
|
||||
import io.ktor.auth.*
|
||||
|
||||
/**
|
||||
* Represents a simple user's principal identified by [id]
|
||||
* @property id of the user
|
||||
*/
|
||||
data class UserIdPrincipal(val id: Int) : Principal
|
||||
@ -1,6 +1,6 @@
|
||||
package be.vandewalleh.extensions
|
||||
|
||||
import be.vandewalleh.auth.UserDbIdPrincipal
|
||||
import be.vandewalleh.auth.UserIdPrincipal
|
||||
import io.ktor.application.*
|
||||
import io.ktor.auth.*
|
||||
import io.ktor.http.*
|
||||
@ -17,4 +17,4 @@ suspend fun ApplicationCall.respondStatus(status: HttpStatusCode) {
|
||||
/**
|
||||
* @return the userId for the currently authenticated user
|
||||
*/
|
||||
fun ApplicationCall.authenticatedUserId() = principal<UserDbIdPrincipal>()!!.id
|
||||
fun ApplicationCall.authenticatedUserId() = principal<UserIdPrincipal>()!!.id
|
||||
|
||||
@ -1,11 +1,7 @@
|
||||
package be.vandewalleh.extensions
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
|
||||
fun <T> ioAsync(block: suspend CoroutineScope.() -> T): Deferred<T> {
|
||||
return CoroutineScope(Dispatchers.IO).async(block = block)
|
||||
}
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
suspend inline fun <T> launchIo(crossinline block: () -> T): T =
|
||||
withContext(Dispatchers.IO) {
|
||||
|
||||
8
api/src/extensions/KtorExtensions.kt
Normal file
8
api/src/extensions/KtorExtensions.kt
Normal file
@ -0,0 +1,8 @@
|
||||
package be.vandewalleh.extensions
|
||||
|
||||
import io.ktor.application.*
|
||||
import io.ktor.routing.*
|
||||
|
||||
abstract class RoutingBuilder(val builder: Routing.() -> Unit)
|
||||
|
||||
abstract class ApplicationBuilder(val builder: Application.() -> Unit)
|
||||
7
api/src/factories/ConfigurationFactory.kt
Normal file
7
api/src/factories/ConfigurationFactory.kt
Normal file
@ -0,0 +1,7 @@
|
||||
package be.vandewalleh.factories
|
||||
|
||||
import be.vandewalleh.Config
|
||||
import com.sksamuel.hoplite.ConfigLoader
|
||||
|
||||
fun configurationFactory() =
|
||||
ConfigLoader().loadConfigOrThrow<Config>("/application.yaml")
|
||||
20
api/src/factories/DataSourceFactory.kt
Normal file
20
api/src/factories/DataSourceFactory.kt
Normal file
@ -0,0 +1,20 @@
|
||||
package be.vandewalleh.factories
|
||||
|
||||
import be.vandewalleh.DatabaseConfig
|
||||
import com.zaxxer.hikari.HikariConfig
|
||||
import com.zaxxer.hikari.HikariDataSource
|
||||
|
||||
fun dataSourceFactory(config: DatabaseConfig): HikariDataSource {
|
||||
val host = config.host
|
||||
val port = config.port
|
||||
val name = config.name
|
||||
|
||||
val hikariConfig = HikariConfig().apply {
|
||||
jdbcUrl = "jdbc:mariadb://$host:$port/$name"
|
||||
username = config.username
|
||||
password = config.password.value
|
||||
connectionTimeout = 3000 // 3 seconds
|
||||
}
|
||||
|
||||
return HikariDataSource(hikariConfig)
|
||||
}
|
||||
6
api/src/factories/DatabaseFactory.kt
Normal file
6
api/src/factories/DatabaseFactory.kt
Normal file
@ -0,0 +1,6 @@
|
||||
package be.vandewalleh.factories
|
||||
|
||||
import me.liuwj.ktorm.database.*
|
||||
import javax.sql.DataSource
|
||||
|
||||
fun databaseFactory(dataSource: DataSource) = Database.connect(dataSource)
|
||||
6
api/src/factories/SimpleJwtFactory.kt
Normal file
6
api/src/factories/SimpleJwtFactory.kt
Normal file
@ -0,0 +1,6 @@
|
||||
package be.vandewalleh.factories
|
||||
|
||||
import be.vandewalleh.Jwt
|
||||
import be.vandewalleh.auth.SimpleJWT
|
||||
|
||||
fun simpleJwtFactory(jwt: Jwt) = SimpleJWT(jwt.secret.value, jwt.validity, jwt.unit)
|
||||
@ -1,62 +0,0 @@
|
||||
package be.vandewalleh.features
|
||||
|
||||
import be.vandewalleh.auth.SimpleJWT
|
||||
import com.sksamuel.hoplite.ConfigLoader
|
||||
import com.sksamuel.hoplite.Masked
|
||||
import com.zaxxer.hikari.HikariConfig
|
||||
import com.zaxxer.hikari.HikariDataSource
|
||||
import org.kodein.di.Kodein
|
||||
import org.kodein.di.generic.bind
|
||||
import org.kodein.di.generic.instance
|
||||
import org.kodein.di.generic.singleton
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.sql.DataSource
|
||||
|
||||
/**
|
||||
* [Kodein] controller module containing the app configuration
|
||||
*/
|
||||
val configurationModule = Kodein.Module(name = "Configuration") {
|
||||
bind() from singleton { ConfigLoader().loadConfigOrThrow<Config>("/application.yaml") }
|
||||
bind<SimpleJWT>(tag = "auth") with singleton { configureAuthJwt(this.kodein) }
|
||||
bind<SimpleJWT>(tag = "refresh") with singleton { configureRefreshJwt(this.kodein) }
|
||||
bind<DataSource>() with singleton { configureDatasource(this.kodein) }
|
||||
}
|
||||
|
||||
data class DatabaseConfig(val host: String, val port: Int, val name: String, val username: String, val password: Masked)
|
||||
data class ServerConfig(val host: String, val port: Int, val cors: Boolean)
|
||||
data class JwtConfig(val auth: Jwt, val refresh: Jwt)
|
||||
data class Jwt(val validity: Long, val unit: TimeUnit, val secret: Masked)
|
||||
data class Config(val database: DatabaseConfig, val server: ServerConfig, val jwt: JwtConfig)
|
||||
|
||||
|
||||
private fun configureAuthJwt(kodein: Kodein): SimpleJWT {
|
||||
val config by kodein.instance<Config>()
|
||||
val jwtSecret = config.jwt.auth.secret
|
||||
val authConfig = config.jwt.auth
|
||||
return SimpleJWT(jwtSecret.value, authConfig.validity, authConfig.unit)
|
||||
}
|
||||
|
||||
private fun configureRefreshJwt(kodein: Kodein): SimpleJWT {
|
||||
val config by kodein.instance<Config>()
|
||||
val jwtSecret = config.jwt.refresh.secret
|
||||
val refreshConfig = config.jwt.auth
|
||||
return SimpleJWT(jwtSecret.value, refreshConfig.validity, refreshConfig.unit)
|
||||
}
|
||||
|
||||
private fun configureDatasource(kodein: Kodein): DataSource {
|
||||
val config by kodein.instance<Config>()
|
||||
val dbConfig = config.database
|
||||
|
||||
val host = dbConfig.host
|
||||
val port = dbConfig.port
|
||||
val name = dbConfig.name
|
||||
|
||||
val hikariConfig = HikariConfig().apply {
|
||||
jdbcUrl = "jdbc:mariadb://$host:$port/$name"
|
||||
username = dbConfig.username
|
||||
password = dbConfig.password.value
|
||||
connectionTimeout = 3000 // 3 seconds
|
||||
}
|
||||
|
||||
return HikariDataSource(hikariConfig)
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
package be.vandewalleh.features
|
||||
|
||||
import be.vandewalleh.extensions.ApplicationBuilder
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.databind.util.StdDateFormat
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||
@ -8,7 +9,7 @@ import io.ktor.features.*
|
||||
import io.ktor.jackson.*
|
||||
import me.liuwj.ktorm.jackson.*
|
||||
|
||||
fun Application.contentNegotiationFeature() {
|
||||
class ContentNegotiationFeature : ApplicationBuilder({
|
||||
install(ContentNegotiation) {
|
||||
jackson {
|
||||
registerModule(KtormModule())
|
||||
@ -17,4 +18,4 @@ fun Application.contentNegotiationFeature() {
|
||||
dateFormat = StdDateFormat()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
package be.vandewalleh.features
|
||||
|
||||
import be.vandewalleh.extensions.ApplicationBuilder
|
||||
import io.ktor.application.*
|
||||
import io.ktor.features.*
|
||||
import io.ktor.http.*
|
||||
|
||||
fun Application.corsFeature() {
|
||||
class CorsFeature(enabled: Boolean) : ApplicationBuilder({
|
||||
if (enabled) {
|
||||
install(CORS) {
|
||||
anyHost()
|
||||
header(HttpHeaders.ContentType)
|
||||
@ -12,3 +14,4 @@ fun Application.corsFeature() {
|
||||
methods.add(HttpMethod.Delete)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package be.vandewalleh.features
|
||||
|
||||
import be.vandewalleh.extensions.ApplicationBuilder
|
||||
import io.ktor.application.*
|
||||
import io.ktor.features.*
|
||||
import io.ktor.http.*
|
||||
@ -7,7 +8,8 @@ import io.ktor.response.*
|
||||
import io.ktor.utils.io.errors.*
|
||||
import java.sql.SQLTransientConnectionException
|
||||
|
||||
fun Application.handleErrors() {
|
||||
|
||||
class ErrorHandler : ApplicationBuilder({
|
||||
install(StatusPages) {
|
||||
|
||||
jacksonErrors()
|
||||
@ -24,7 +26,7 @@ fun Application.handleErrors() {
|
||||
call.respond(HttpStatusCode.InternalServerError, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
class ValidationException(val error: String) : RuntimeException()
|
||||
class ErrorResponse(val error: String)
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
package be.vandewalleh.features
|
||||
|
||||
import be.vandewalleh.auth.authenticationModule
|
||||
import io.ktor.application.*
|
||||
import org.kodein.di.Kodein
|
||||
import org.kodein.di.generic.instance
|
||||
|
||||
fun Application.loadFeatures(kodein: Kodein) {
|
||||
val config by kodein.instance<Config>()
|
||||
if (config.server.cors) {
|
||||
corsFeature()
|
||||
}
|
||||
contentNegotiationFeature()
|
||||
authenticationModule(kodein)
|
||||
handleErrors()
|
||||
}
|
||||
28
api/src/features/Migration.kt
Normal file
28
api/src/features/Migration.kt
Normal file
@ -0,0 +1,28 @@
|
||||
package be.vandewalleh.features
|
||||
|
||||
import be.vandewalleh.extensions.ApplicationBuilder
|
||||
import io.ktor.application.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.flywaydb.core.Flyway
|
||||
import javax.sql.DataSource
|
||||
|
||||
|
||||
class MigrationHook(migration: Migration) : ApplicationBuilder({
|
||||
environment.monitor.subscribe(ApplicationStarted) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
migration.migrate()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
class Migration(private val dataSource: DataSource) {
|
||||
fun migrate() {
|
||||
Flyway.configure()
|
||||
.dataSource(dataSource)
|
||||
.baselineOnMigrate(true)
|
||||
.load()
|
||||
.migrate()
|
||||
}
|
||||
}
|
||||
13
api/src/features/ShutdownDatabaseConnection.kt
Normal file
13
api/src/features/ShutdownDatabaseConnection.kt
Normal file
@ -0,0 +1,13 @@
|
||||
package be.vandewalleh.features
|
||||
|
||||
import be.vandewalleh.extensions.ApplicationBuilder
|
||||
import com.zaxxer.hikari.HikariDataSource
|
||||
import io.ktor.application.*
|
||||
|
||||
class ShutdownDatabaseConnection(hikariDataSource: HikariDataSource) : ApplicationBuilder({
|
||||
environment.monitor.subscribe(ApplicationStopPreparing) {
|
||||
if (!hikariDataSource.isClosed) {
|
||||
hikariDataSource.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -1,20 +0,0 @@
|
||||
package be.vandewalleh.migrations
|
||||
|
||||
import org.flywaydb.core.Flyway
|
||||
import org.kodein.di.Kodein
|
||||
import org.kodein.di.KodeinAware
|
||||
import org.kodein.di.generic.instance
|
||||
import javax.sql.DataSource
|
||||
|
||||
class Migration(override val kodein: Kodein) : KodeinAware {
|
||||
|
||||
fun migrate() {
|
||||
val dataSource by instance<DataSource>()
|
||||
val flyway = Flyway.configure()
|
||||
.dataSource(dataSource)
|
||||
.baselineOnMigrate(true)
|
||||
.load()
|
||||
flyway.migrate()
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,10 +1,10 @@
|
||||
package be.vandewalleh.routing
|
||||
|
||||
import be.vandewalleh.auth.SimpleJWT
|
||||
import be.vandewalleh.auth.UserDbIdPrincipal
|
||||
import be.vandewalleh.auth.UserIdPrincipal
|
||||
import be.vandewalleh.auth.UsernamePasswordCredential
|
||||
import be.vandewalleh.extensions.respondStatus
|
||||
import be.vandewalleh.extensions.authenticatedUserId
|
||||
import be.vandewalleh.extensions.respondStatus
|
||||
import be.vandewalleh.features.PasswordHash
|
||||
import be.vandewalleh.services.UserService
|
||||
import be.vandewalleh.validation.receiveValidated
|
||||
@ -87,7 +87,7 @@ fun Route.userRoutes(kodein: Kodein) {
|
||||
}
|
||||
|
||||
get("/me") {
|
||||
val id = call.principal<UserDbIdPrincipal>()!!.id
|
||||
val id = call.principal<UserIdPrincipal>()!!.id
|
||||
val info = userService.find(id)
|
||||
if (info != null) call.respond(mapOf("user" to info))
|
||||
else call.respondStatus(HttpStatusCode.Unauthorized)
|
||||
|
||||
@ -1,45 +1,38 @@
|
||||
package be.vandewalleh.services
|
||||
|
||||
import be.vandewalleh.entities.Note
|
||||
import be.vandewalleh.extensions.ioAsync
|
||||
import be.vandewalleh.extensions.launchIo
|
||||
import be.vandewalleh.tables.Notes
|
||||
import be.vandewalleh.tables.Tags
|
||||
import me.liuwj.ktorm.database.*
|
||||
import me.liuwj.ktorm.dsl.*
|
||||
import me.liuwj.ktorm.entity.*
|
||||
import org.kodein.di.Kodein
|
||||
import org.kodein.di.KodeinAware
|
||||
import org.kodein.di.generic.instance
|
||||
import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* service to handle database queries at the Notes level.
|
||||
*/
|
||||
class NoteService(override val kodein: Kodein) : KodeinAware {
|
||||
private val db by instance<Database>()
|
||||
class NoteService(private val db: Database) {
|
||||
|
||||
/**
|
||||
* returns a list of [Note] associated with the userId
|
||||
*/
|
||||
suspend fun findAll(userId: Int): List<Note> {
|
||||
val notes = launchIo {
|
||||
db.sequenceOf(Notes, withReferences = false)
|
||||
.filterColumns { listOf(it.uuid, it.title, it.updatedAt) }
|
||||
suspend fun findAll(userId: Int, limit: Int = 20, offset: Int = 0): List<Note> = launchIo {
|
||||
val notes = db.sequenceOf(Notes, withReferences = false)
|
||||
.filterColumns { it.columns - it.userId }
|
||||
.filter { it.userId eq userId }
|
||||
.sortedByDescending { it.updatedAt }
|
||||
.take(limit).drop(offset)
|
||||
.toList()
|
||||
}
|
||||
|
||||
if (notes.isEmpty()) return emptyList()
|
||||
if (notes.isEmpty()) return@launchIo emptyList()
|
||||
|
||||
val allTags = launchIo {
|
||||
val allTags =
|
||||
db.sequenceOf(Tags, withReferences = false)
|
||||
.filterColumns { listOf(it.noteUuid, it.name) }
|
||||
.filter { it.noteUuid inList notes.map { note -> note.uuid } }
|
||||
.toList()
|
||||
}
|
||||
|
||||
val tagsByUuid = allTags.groupByTo(HashMap(), { it.note.uuid }, { it.name })
|
||||
|
||||
@ -48,7 +41,7 @@ class NoteService(override val kodein: Kodein) : KodeinAware {
|
||||
if (tags != null) it.tags = tags
|
||||
}
|
||||
|
||||
return notes
|
||||
notes
|
||||
}
|
||||
|
||||
suspend fun exists(userId: Int, uuid: UUID) = launchIo {
|
||||
@ -79,24 +72,20 @@ class NoteService(override val kodein: Kodein) : KodeinAware {
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
suspend fun find(userId: Int, noteUuid: UUID): Note? {
|
||||
val deferredNote = ioAsync {
|
||||
suspend fun find(userId: Int, noteUuid: UUID): Note? = launchIo {
|
||||
val note =
|
||||
db.sequenceOf(Notes, withReferences = false)
|
||||
.filterColumns { it.columns - it.userId }
|
||||
.filter { it.uuid eq noteUuid }
|
||||
.find { it.userId eq userId }
|
||||
}
|
||||
?: return@launchIo null
|
||||
|
||||
val deferredTags = ioAsync {
|
||||
val tags =
|
||||
db.sequenceOf(Tags, withReferences = false)
|
||||
.filter { it.noteUuid eq noteUuid }
|
||||
.mapColumns { it.name } as List<String>
|
||||
}
|
||||
|
||||
val note = deferredNote.await() ?: return null
|
||||
val tags = deferredTags.await()
|
||||
|
||||
return note.also { it.tags = tags }
|
||||
note.also { it.tags = tags }
|
||||
}
|
||||
|
||||
suspend fun updateNote(userId: Int, note: Note): Boolean = launchIo {
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
package be.vandewalleh.services
|
||||
|
||||
import org.kodein.di.Kodein
|
||||
import org.kodein.di.generic.bind
|
||||
import org.kodein.di.generic.singleton
|
||||
|
||||
|
||||
/**
|
||||
* [Kodein] controller module containing the app services
|
||||
*/
|
||||
val serviceModule = Kodein.Module(name = "Services") {
|
||||
bind<NoteService>() with singleton { NoteService(this.kodein) }
|
||||
bind<UserService>() with singleton { UserService(this.kodein) }
|
||||
}
|
||||
@ -7,18 +7,12 @@ import be.vandewalleh.tables.Users
|
||||
import me.liuwj.ktorm.database.*
|
||||
import me.liuwj.ktorm.dsl.*
|
||||
import me.liuwj.ktorm.entity.*
|
||||
import org.kodein.di.Kodein
|
||||
import org.kodein.di.KodeinAware
|
||||
import org.kodein.di.generic.instance
|
||||
import java.sql.SQLIntegrityConstraintViolationException
|
||||
|
||||
/**
|
||||
* service to handle database queries for users.
|
||||
*/
|
||||
class UserService(override val kodein: Kodein) : KodeinAware {
|
||||
private val db by instance<Database>()
|
||||
private val passwordHash by instance<PasswordHash>()
|
||||
|
||||
class UserService(private val db: Database, private val passwordHash: PasswordHash) {
|
||||
|
||||
/**
|
||||
* returns a user from it's username if found or null
|
||||
|
||||
@ -4,7 +4,6 @@ import am.ik.yavi.builder.ValidatorBuilder
|
||||
import am.ik.yavi.builder.konstraint
|
||||
import am.ik.yavi.core.Validator
|
||||
import be.vandewalleh.entities.Note
|
||||
import be.vandewalleh.entities.User
|
||||
|
||||
val noteValidator: Validator<Note> = ValidatorBuilder.of<Note>()
|
||||
.konstraint(Note::title) {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package integration.routing
|
||||
|
||||
import be.vandewalleh.Config
|
||||
import be.vandewalleh.auth.SimpleJWT
|
||||
import be.vandewalleh.entities.User
|
||||
import be.vandewalleh.features.Config
|
||||
import be.vandewalleh.features.PasswordHash
|
||||
import be.vandewalleh.mainModule
|
||||
import be.vandewalleh.module
|
||||
|
||||
@ -4,8 +4,8 @@ import am.ik.yavi.builder.ValidatorBuilder
|
||||
import am.ik.yavi.core.CustomConstraint
|
||||
import am.ik.yavi.core.Validator
|
||||
import be.vandewalleh.entities.Note
|
||||
import be.vandewalleh.features.Migration
|
||||
import be.vandewalleh.mainModule
|
||||
import be.vandewalleh.migrations.Migration
|
||||
import be.vandewalleh.services.NoteService
|
||||
import be.vandewalleh.services.UserService
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
@ -34,7 +34,7 @@ class NoteServiceTest {
|
||||
|
||||
private val kodein = Kodein {
|
||||
import(mainModule, allowOverride = true)
|
||||
bind<DataSource>(overrides = true) with singleton { mariadb.datasource() }
|
||||
bind(overrides = true) from singleton { mariadb.datasource() }
|
||||
}
|
||||
|
||||
init {
|
||||
@ -216,7 +216,7 @@ class NoteServiceTest {
|
||||
disable(DeserializationFeature.ACCEPT_FLOAT_AS_INT)
|
||||
dateFormat = StdDateFormat()
|
||||
}
|
||||
val note: Note = objectMapper.readValue("""{"uuid": "c6d80-5fe6-4a30-b034-da63f6663c2c"}""")
|
||||
val note: Note = objectMapper.readValue("""{"uuid": "2007e4d7-2986-4188-bde1-b99916d94bad"}""")
|
||||
println(note.uuid)
|
||||
println(note.uuid::class.qualifiedName)
|
||||
println(note.uuid.leastSignificantBits)
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
package integration.services
|
||||
|
||||
import be.vandewalleh.features.Migration
|
||||
import be.vandewalleh.mainModule
|
||||
import be.vandewalleh.migrations.Migration
|
||||
import be.vandewalleh.services.UserService
|
||||
import com.zaxxer.hikari.HikariDataSource
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.amshove.kluent.*
|
||||
import org.junit.jupiter.api.*
|
||||
@ -22,17 +23,16 @@ class UserServiceTest {
|
||||
|
||||
private val kodein = Kodein {
|
||||
import(mainModule, allowOverride = true)
|
||||
bind<DataSource>(overrides = true) with singleton { mariadb.datasource() }
|
||||
}
|
||||
|
||||
private val migration by kodein.instance<Migration>()
|
||||
|
||||
init {
|
||||
migration.migrate()
|
||||
bind(overrides = true) from singleton { mariadb.datasource() }
|
||||
}
|
||||
|
||||
private val userService by kodein.instance<UserService>()
|
||||
|
||||
init {
|
||||
val migration by kodein.instance<Migration>()
|
||||
migration.migrate()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
fun `test create user`() {
|
||||
|
||||
@ -6,7 +6,7 @@ import org.testcontainers.containers.MariaDBContainer
|
||||
import javax.sql.DataSource
|
||||
|
||||
class KMariadbContainer : MariaDBContainer<KMariadbContainer>() {
|
||||
fun datasource() : DataSource {
|
||||
fun datasource() : HikariDataSource {
|
||||
val hikariConfig = HikariConfig().apply {
|
||||
jdbcUrl = this@KMariadbContainer.jdbcUrl
|
||||
username = this@KMariadbContainer.username
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user