From e03e12110b4fe05d9f4d433c9149607a884dfcb7 Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Wed, 1 Jul 2020 00:12:56 +0200 Subject: [PATCH] Cleaner code --- api/resources/application.yaml | 1 - api/src/Configuration.kt | 21 +++++++ api/src/Dependencies.kt | 48 +++++++++----- api/src/NotesApplication.kt | 26 ++++---- api/src/auth/AuthenticationModule.kt | 12 ++-- api/src/auth/UserDbIdPrincipal.kt | 5 -- api/src/auth/UserIdPrincipal.kt | 9 +++ .../extensions/ApplicationCallExtensions.kt | 4 +- api/src/extensions/Coroutines.kt | 8 +-- api/src/extensions/KtorExtensions.kt | 8 +++ api/src/factories/ConfigurationFactory.kt | 7 +++ api/src/factories/DataSourceFactory.kt | 20 ++++++ api/src/factories/DatabaseFactory.kt | 6 ++ api/src/factories/SimpleJwtFactory.kt | 6 ++ api/src/features/ConfigurationFeature.kt | 62 ------------------- api/src/features/ContentNegotiationFeature.kt | 5 +- api/src/features/CorsFeature.kt | 17 ++--- api/src/features/ErrorFeature.kt | 6 +- api/src/features/Features.kt | 16 ----- api/src/features/Migration.kt | 28 +++++++++ .../features/ShutdownDatabaseConnection.kt | 13 ++++ api/src/migrations/Migration.kt | 20 ------ api/src/routing/UserRoutes.kt | 6 +- api/src/services/NoteService.kt | 43 +++++-------- api/src/services/Services.kt | 14 ----- api/src/services/UserService.kt | 8 +-- api/src/validation/NoteValidation.kt | 1 - .../routing/AuthControllerKtTest.kt | 2 +- .../integration/services/NoteServiceTest.kt | 6 +- .../integration/services/UserServiceTest.kt | 16 ++--- api/test/utils/TestContainers.kt | 2 +- 31 files changed, 223 insertions(+), 223 deletions(-) create mode 100644 api/src/Configuration.kt delete mode 100644 api/src/auth/UserDbIdPrincipal.kt create mode 100644 api/src/auth/UserIdPrincipal.kt create mode 100644 api/src/extensions/KtorExtensions.kt create mode 100644 api/src/factories/ConfigurationFactory.kt create mode 100644 api/src/factories/DataSourceFactory.kt create mode 100644 api/src/factories/DatabaseFactory.kt create mode 100644 api/src/factories/SimpleJwtFactory.kt delete mode 100644 api/src/features/ConfigurationFeature.kt delete mode 100644 api/src/features/Features.kt create mode 100644 api/src/features/Migration.kt create mode 100644 api/src/features/ShutdownDatabaseConnection.kt delete mode 100644 api/src/migrations/Migration.kt delete mode 100644 api/src/services/Services.kt diff --git a/api/resources/application.yaml b/api/resources/application.yaml index 2fd75d4..25209d8 100644 --- a/api/resources/application.yaml +++ b/api/resources/application.yaml @@ -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 diff --git a/api/src/Configuration.kt b/api/src/Configuration.kt new file mode 100644 index 0000000..68344e7 --- /dev/null +++ b/api/src/Configuration.kt @@ -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) diff --git a/api/src/Dependencies.kt b/api/src/Dependencies.kt index 3c86e37..210d4b6 100644 --- a/api/src/Dependencies.kt +++ b/api/src/Dependencies.kt @@ -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() with singleton { LoggerFactory.getLogger("Application") } - bind() with singleton { Migration(this.kodein) } - bind() with singleton { Database.connect(this.instance()) } + bind() from singleton { NoteService(instance()) } + bind() from singleton { UserService(instance(), instance()) } + + bind() from singleton { configurationFactory() } + + bind() from setBinding() + bind().inSet() with singleton { ErrorHandler() } + bind().inSet() with singleton { ContentNegotiationFeature() } + bind().inSet() with singleton { CorsFeature(instance().server.cors) } + bind().inSet() with singleton { AuthenticationModule(instance(tag = "auth")) } + bind().inSet() with singleton { MigrationHook(instance()) } + bind().inSet() with singleton { ShutdownDatabaseConnection(instance()) } + + bind(tag = "auth") with singleton { simpleJwtFactory(instance().jwt.auth) } + bind(tag = "refresh") with singleton { simpleJwtFactory(instance().jwt.refresh) } + + bind() from singleton { LoggerFactory.getLogger("Application") } + bind() from singleton { dataSourceFactory(instance().database) } + bind() from singleton { databaseFactory(instance()) } + bind() from singleton { Migration(instance()) } + bind() with singleton { BcryptPasswordHash() } } diff --git a/api/src/NotesApplication.kt b/api/src/NotesApplication.kt index e9b109d..a6d66e6 100644 --- a/api/src/NotesApplication.kt +++ b/api/src/NotesApplication.kt @@ -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) { - - val kodein = Kodein{ +fun main() { + val kodein = Kodein { import(mainModule) - constant("config file") with "/application.prod.yaml" // FIXME } val config by kodein.instance() val logger by kodein.instance() logger.info("Running application with configuration $config") logger.debug("Kodein bindings\n${kodein.container.tree.bindings.description()}") - val migration by kodein.instance() - 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 by kodein.instance() + + builders.forEach { + it.builder(this) + } routing { route("/user") { diff --git a/api/src/auth/AuthenticationModule.kt b/api/src/auth/AuthenticationModule.kt index 33e3441..1c909c1 100644 --- a/api/src/auth/AuthenticationModule.kt +++ b/api/src/auth/AuthenticationModule.kt @@ -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(tag = "auth") - verifier(simpleJwt.verifier) + verifier(authJwt.verifier) validate { - UserDbIdPrincipal(it.payload.getClaim("id").asInt()) + UserIdPrincipal(it.payload.getClaim("id").asInt()) } } } -} +}) diff --git a/api/src/auth/UserDbIdPrincipal.kt b/api/src/auth/UserDbIdPrincipal.kt deleted file mode 100644 index 375c88b..0000000 --- a/api/src/auth/UserDbIdPrincipal.kt +++ /dev/null @@ -1,5 +0,0 @@ -package be.vandewalleh.auth - -import io.ktor.auth.Principal - -data class UserDbIdPrincipal(val id: Int) : Principal diff --git a/api/src/auth/UserIdPrincipal.kt b/api/src/auth/UserIdPrincipal.kt new file mode 100644 index 0000000..95cf5ee --- /dev/null +++ b/api/src/auth/UserIdPrincipal.kt @@ -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 diff --git a/api/src/extensions/ApplicationCallExtensions.kt b/api/src/extensions/ApplicationCallExtensions.kt index cfdcca6..90a9c88 100644 --- a/api/src/extensions/ApplicationCallExtensions.kt +++ b/api/src/extensions/ApplicationCallExtensions.kt @@ -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()!!.id +fun ApplicationCall.authenticatedUserId() = principal()!!.id diff --git a/api/src/extensions/Coroutines.kt b/api/src/extensions/Coroutines.kt index 6218cd4..a665342 100644 --- a/api/src/extensions/Coroutines.kt +++ b/api/src/extensions/Coroutines.kt @@ -1,11 +1,7 @@ package be.vandewalleh.extensions -import kotlinx.coroutines.* - - -fun ioAsync(block: suspend CoroutineScope.() -> T): Deferred { - return CoroutineScope(Dispatchers.IO).async(block = block) -} +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext suspend inline fun launchIo(crossinline block: () -> T): T = withContext(Dispatchers.IO) { diff --git a/api/src/extensions/KtorExtensions.kt b/api/src/extensions/KtorExtensions.kt new file mode 100644 index 0000000..ca15c75 --- /dev/null +++ b/api/src/extensions/KtorExtensions.kt @@ -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) diff --git a/api/src/factories/ConfigurationFactory.kt b/api/src/factories/ConfigurationFactory.kt new file mode 100644 index 0000000..3552063 --- /dev/null +++ b/api/src/factories/ConfigurationFactory.kt @@ -0,0 +1,7 @@ +package be.vandewalleh.factories + +import be.vandewalleh.Config +import com.sksamuel.hoplite.ConfigLoader + +fun configurationFactory() = + ConfigLoader().loadConfigOrThrow("/application.yaml") diff --git a/api/src/factories/DataSourceFactory.kt b/api/src/factories/DataSourceFactory.kt new file mode 100644 index 0000000..25f38d9 --- /dev/null +++ b/api/src/factories/DataSourceFactory.kt @@ -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) +} diff --git a/api/src/factories/DatabaseFactory.kt b/api/src/factories/DatabaseFactory.kt new file mode 100644 index 0000000..d3837ab --- /dev/null +++ b/api/src/factories/DatabaseFactory.kt @@ -0,0 +1,6 @@ +package be.vandewalleh.factories + +import me.liuwj.ktorm.database.* +import javax.sql.DataSource + +fun databaseFactory(dataSource: DataSource) = Database.connect(dataSource) diff --git a/api/src/factories/SimpleJwtFactory.kt b/api/src/factories/SimpleJwtFactory.kt new file mode 100644 index 0000000..7f39546 --- /dev/null +++ b/api/src/factories/SimpleJwtFactory.kt @@ -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) diff --git a/api/src/features/ConfigurationFeature.kt b/api/src/features/ConfigurationFeature.kt deleted file mode 100644 index e9de45d..0000000 --- a/api/src/features/ConfigurationFeature.kt +++ /dev/null @@ -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("/application.yaml") } - bind(tag = "auth") with singleton { configureAuthJwt(this.kodein) } - bind(tag = "refresh") with singleton { configureRefreshJwt(this.kodein) } - bind() 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() - 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() - 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() - 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) -} diff --git a/api/src/features/ContentNegotiationFeature.kt b/api/src/features/ContentNegotiationFeature.kt index 85fcbcb..95b867d 100644 --- a/api/src/features/ContentNegotiationFeature.kt +++ b/api/src/features/ContentNegotiationFeature.kt @@ -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() } } -} +}) diff --git a/api/src/features/CorsFeature.kt b/api/src/features/CorsFeature.kt index 019c172..57bee73 100644 --- a/api/src/features/CorsFeature.kt +++ b/api/src/features/CorsFeature.kt @@ -1,14 +1,17 @@ package be.vandewalleh.features +import be.vandewalleh.extensions.ApplicationBuilder import io.ktor.application.* import io.ktor.features.* import io.ktor.http.* -fun Application.corsFeature() { - install(CORS) { - anyHost() - header(HttpHeaders.ContentType) - header(HttpHeaders.Authorization) - methods.add(HttpMethod.Delete) +class CorsFeature(enabled: Boolean) : ApplicationBuilder({ + if (enabled) { + install(CORS) { + anyHost() + header(HttpHeaders.ContentType) + header(HttpHeaders.Authorization) + methods.add(HttpMethod.Delete) + } } -} \ No newline at end of file +}) diff --git a/api/src/features/ErrorFeature.kt b/api/src/features/ErrorFeature.kt index 0462e9d..a174d6a 100644 --- a/api/src/features/ErrorFeature.kt +++ b/api/src/features/ErrorFeature.kt @@ -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) diff --git a/api/src/features/Features.kt b/api/src/features/Features.kt deleted file mode 100644 index cea8bd3..0000000 --- a/api/src/features/Features.kt +++ /dev/null @@ -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() - if (config.server.cors) { - corsFeature() - } - contentNegotiationFeature() - authenticationModule(kodein) - handleErrors() -} diff --git a/api/src/features/Migration.kt b/api/src/features/Migration.kt new file mode 100644 index 0000000..6d335fe --- /dev/null +++ b/api/src/features/Migration.kt @@ -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() + } +} diff --git a/api/src/features/ShutdownDatabaseConnection.kt b/api/src/features/ShutdownDatabaseConnection.kt new file mode 100644 index 0000000..db91b1f --- /dev/null +++ b/api/src/features/ShutdownDatabaseConnection.kt @@ -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() + } + } +}) diff --git a/api/src/migrations/Migration.kt b/api/src/migrations/Migration.kt deleted file mode 100644 index 357465d..0000000 --- a/api/src/migrations/Migration.kt +++ /dev/null @@ -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() - val flyway = Flyway.configure() - .dataSource(dataSource) - .baselineOnMigrate(true) - .load() - flyway.migrate() - } - -} \ No newline at end of file diff --git a/api/src/routing/UserRoutes.kt b/api/src/routing/UserRoutes.kt index bb2e1cf..6db3b69 100644 --- a/api/src/routing/UserRoutes.kt +++ b/api/src/routing/UserRoutes.kt @@ -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()!!.id + val id = call.principal()!!.id val info = userService.find(id) if (info != null) call.respond(mapOf("user" to info)) else call.respondStatus(HttpStatusCode.Unauthorized) diff --git a/api/src/services/NoteService.kt b/api/src/services/NoteService.kt index 41e86d8..cd07b2c 100644 --- a/api/src/services/NoteService.kt +++ b/api/src/services/NoteService.kt @@ -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() +class NoteService(private val db: Database) { /** * returns a list of [Note] associated with the userId */ - suspend fun findAll(userId: Int): List { - val notes = launchIo { - db.sequenceOf(Notes, withReferences = false) - .filterColumns { listOf(it.uuid, it.title, it.updatedAt) } - .filter { it.userId eq userId } - .sortedByDescending { it.updatedAt } - .toList() - } + suspend fun findAll(userId: Int, limit: Int = 20, offset: Int = 0): List = 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 - } - 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 { diff --git a/api/src/services/Services.kt b/api/src/services/Services.kt deleted file mode 100644 index f5fdbe8..0000000 --- a/api/src/services/Services.kt +++ /dev/null @@ -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() with singleton { NoteService(this.kodein) } - bind() with singleton { UserService(this.kodein) } -} diff --git a/api/src/services/UserService.kt b/api/src/services/UserService.kt index 18821a9..3e0137f 100644 --- a/api/src/services/UserService.kt +++ b/api/src/services/UserService.kt @@ -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() - private val passwordHash by instance() - +class UserService(private val db: Database, private val passwordHash: PasswordHash) { /** * returns a user from it's username if found or null diff --git a/api/src/validation/NoteValidation.kt b/api/src/validation/NoteValidation.kt index 0c04dc6..f9262dc 100644 --- a/api/src/validation/NoteValidation.kt +++ b/api/src/validation/NoteValidation.kt @@ -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 = ValidatorBuilder.of() .konstraint(Note::title) { diff --git a/api/test/integration/routing/AuthControllerKtTest.kt b/api/test/integration/routing/AuthControllerKtTest.kt index 0eeacbc..dec46af 100644 --- a/api/test/integration/routing/AuthControllerKtTest.kt +++ b/api/test/integration/routing/AuthControllerKtTest.kt @@ -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 diff --git a/api/test/integration/services/NoteServiceTest.kt b/api/test/integration/services/NoteServiceTest.kt index d22758f..7dbc0df 100644 --- a/api/test/integration/services/NoteServiceTest.kt +++ b/api/test/integration/services/NoteServiceTest.kt @@ -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(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) diff --git a/api/test/integration/services/UserServiceTest.kt b/api/test/integration/services/UserServiceTest.kt index 8a6cb1c..86543e4 100644 --- a/api/test/integration/services/UserServiceTest.kt +++ b/api/test/integration/services/UserServiceTest.kt @@ -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(overrides = true) with singleton { mariadb.datasource() } - } - - private val migration by kodein.instance() - - init { - migration.migrate() + bind(overrides = true) from singleton { mariadb.datasource() } } private val userService by kodein.instance() + init { + val migration by kodein.instance() + migration.migrate() + } + @Test @Order(1) fun `test create user`() { diff --git a/api/test/utils/TestContainers.kt b/api/test/utils/TestContainers.kt index db245fe..5ca87ad 100644 --- a/api/test/utils/TestContainers.kt +++ b/api/test/utils/TestContainers.kt @@ -6,7 +6,7 @@ import org.testcontainers.containers.MariaDBContainer import javax.sql.DataSource class KMariadbContainer : MariaDBContainer() { - fun datasource() : DataSource { + fun datasource() : HikariDataSource { val hikariConfig = HikariConfig().apply { jdbcUrl = this@KMariadbContainer.jdbcUrl username = this@KMariadbContainer.username