From bf5631447341fa7544bb24182bceb74bc222e99b Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Thu, 5 Nov 2020 14:37:20 +0100 Subject: [PATCH] Move transactions to domain layer --- simplenotes-app/build.gradle.kts | 1 - .../app/filters/TransactionFilter.kt | 15 ------ .../be/simplenotes/app/routes/ApiRoutes.kt | 11 +--- .../be/simplenotes/app/routes/BasicRoutes.kt | 9 +--- .../be/simplenotes/app/routes/NoteRoutes.kt | 15 ++---- .../simplenotes/app/routes/SettingsRoutes.kt | 9 +--- .../domain/usecases/NoteService.kt | 50 +++++++++++-------- .../users/delete/DeleteUseCaseImpl.kt | 24 +++++---- .../users/register/RegisterUseCaseImpl.kt | 8 +-- .../users/register/RegisterUseCaseImplTest.kt | 6 ++- .../transactions/KtormTransactionService.kt | 9 ++++ .../transactions/TransactionService.kt | 5 ++ 12 files changed, 74 insertions(+), 88 deletions(-) delete mode 100644 simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/TransactionFilter.kt create mode 100644 simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/transactions/KtormTransactionService.kt create mode 100644 simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/transactions/TransactionService.kt diff --git a/simplenotes-app/build.gradle.kts b/simplenotes-app/build.gradle.kts index fe693e8..9ad98fd 100644 --- a/simplenotes-app/build.gradle.kts +++ b/simplenotes-app/build.gradle.kts @@ -25,7 +25,6 @@ dependencies { implementation(Libs.javaxServlet) implementation(Libs.kotlinxSerializationJson) implementation(Libs.logbackClassic) - implementation(Libs.ktormCore) implementation(Libs.micronaut) kapt(Libs.micronautProcessor) diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/TransactionFilter.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/TransactionFilter.kt deleted file mode 100644 index d99a7c8..0000000 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/TransactionFilter.kt +++ /dev/null @@ -1,15 +0,0 @@ -package be.simplenotes.app.filters - -import me.liuwj.ktorm.database.Database -import org.http4k.core.Filter -import org.http4k.core.HttpHandler -import javax.inject.Singleton - -@Singleton -class TransactionFilter(private val db: Database) : Filter { - override fun invoke(next: HttpHandler): HttpHandler = { request -> - db.useTransaction { - next(request) - } - } -} diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/ApiRoutes.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/ApiRoutes.kt index e45a88e..60e8ab5 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/ApiRoutes.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/ApiRoutes.kt @@ -2,10 +2,8 @@ package be.simplenotes.app.routes import be.simplenotes.app.api.ApiNoteController import be.simplenotes.app.api.ApiUserController -import be.simplenotes.app.filters.TransactionFilter import be.simplenotes.app.filters.auth.RequiredAuthFilter import be.simplenotes.app.filters.auth.RequiredAuthLens -import org.http4k.core.Filter import org.http4k.core.Method.* import org.http4k.core.Request import org.http4k.core.then @@ -21,16 +19,11 @@ import javax.inject.Singleton class ApiRoutes( private val apiUserController: ApiUserController, private val apiNoteController: ApiNoteController, - private val transaction: TransactionFilter, @Named("api") private val auth: RequiredAuthFilter, @Named("required") private val authLens: RequiredAuthLens, ) : Supplier { override fun get(): RoutingHttpHandler { - fun Filter.then(next: ProtectedHandler) = then { req: Request -> - next(req, authLens(req)) - } - infix fun PathMethod.to(action: ProtectedHandler) = this to { req: Request -> action(req, authLens(req)) } @@ -41,10 +34,10 @@ class ApiRoutes( auth.then( routes( "/" bind GET to ::notes, - "/" bind POST to transaction.then(::createNote), + "/" bind POST to ::createNote, "/search" bind POST to ::search, "/{uuid}" bind GET to ::note, - "/{uuid}" bind PUT to transaction.then(::update), + "/{uuid}" bind PUT to ::update, ) ).withBasePath("/notes") } diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/BasicRoutes.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/BasicRoutes.kt index 228a2ae..08351cd 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/BasicRoutes.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/BasicRoutes.kt @@ -5,11 +5,9 @@ import be.simplenotes.app.controllers.HealthCheckController import be.simplenotes.app.controllers.NoteController import be.simplenotes.app.controllers.UserController import be.simplenotes.app.filters.ImmutableFilter -import be.simplenotes.app.filters.TransactionFilter import be.simplenotes.app.filters.auth.OptionalAuthFilter import be.simplenotes.app.filters.auth.OptionalAuthLens import org.http4k.core.ContentType -import org.http4k.core.Filter import org.http4k.core.Method.GET import org.http4k.core.Method.POST import org.http4k.core.Request @@ -27,15 +25,10 @@ class BasicRoutes( private val noteCtrl: NoteController, @Named("optional") private val authLens: OptionalAuthLens, private val auth: OptionalAuthFilter, - private val transactionFilter: TransactionFilter, ) : Supplier { override fun get(): RoutingHttpHandler { - fun Filter.then(next: PublicHandler) = then { req: Request -> - next(req, authLens(req)) - } - infix fun PathMethod.to(action: PublicHandler) = this to { req: Request -> action(req, authLens(req)) } @@ -52,7 +45,7 @@ class BasicRoutes( routes( "/" bind GET to baseCtrl::index, "/register" bind GET to userCtrl::register, - "/register" bind POST to transactionFilter.then(userCtrl::register), + "/register" bind POST to userCtrl::register, "/login" bind GET to userCtrl::login, "/login" bind POST to userCtrl::login, "/logout" bind POST to userCtrl::logout, diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/NoteRoutes.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/NoteRoutes.kt index 28c3e91..83da7f9 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/NoteRoutes.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/NoteRoutes.kt @@ -1,10 +1,8 @@ package be.simplenotes.app.routes import be.simplenotes.app.controllers.NoteController -import be.simplenotes.app.filters.TransactionFilter import be.simplenotes.app.filters.auth.RequiredAuthFilter import be.simplenotes.app.filters.auth.RequiredAuthLens -import org.http4k.core.Filter import org.http4k.core.Method.GET import org.http4k.core.Method.POST import org.http4k.core.Request @@ -20,16 +18,11 @@ import javax.inject.Singleton @Singleton class NoteRoutes( private val noteCtrl: NoteController, - private val transaction: TransactionFilter, private val auth: RequiredAuthFilter, @Named("required") private val authLens: RequiredAuthLens, ) : Supplier { override fun get(): RoutingHttpHandler { - fun Filter.then(next: ProtectedHandler) = then { req: Request -> - next(req, authLens(req)) - } - infix fun PathMethod.to(action: ProtectedHandler) = this to { req: Request -> action(req, authLens(req)) } @@ -39,13 +32,13 @@ class NoteRoutes( "/" bind GET to ::list, "/" bind POST to ::search, "/new" bind GET to ::new, - "/new" bind POST to transaction.then(::new), + "/new" bind POST to ::new, "/trash" bind GET to ::trash, "/{uuid}" bind GET to ::note, - "/{uuid}" bind POST to transaction.then(::note), + "/{uuid}" bind POST to ::note, "/{uuid}/edit" bind GET to ::edit, - "/{uuid}/edit" bind POST to transaction.then(::edit), - "/deleted/{uuid}" bind POST to transaction.then(::deleted), + "/{uuid}/edit" bind POST to ::edit, + "/deleted/{uuid}" bind POST to ::deleted, ).withBasePath("/notes") } ) diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/SettingsRoutes.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/SettingsRoutes.kt index ddbb9ae..b1a569d 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/SettingsRoutes.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/SettingsRoutes.kt @@ -1,10 +1,8 @@ package be.simplenotes.app.routes import be.simplenotes.app.controllers.SettingsController -import be.simplenotes.app.filters.TransactionFilter import be.simplenotes.app.filters.auth.RequiredAuthFilter import be.simplenotes.app.filters.auth.RequiredAuthLens -import org.http4k.core.Filter import org.http4k.core.Method.GET import org.http4k.core.Method.POST import org.http4k.core.Request @@ -20,23 +18,18 @@ import javax.inject.Singleton @Singleton class SettingsRoutes( private val settingsController: SettingsController, - private val transaction: TransactionFilter, private val auth: RequiredAuthFilter, @Named("required") private val authLens: RequiredAuthLens, ) : Supplier { override fun get(): RoutingHttpHandler { - fun Filter.then(next: ProtectedHandler) = then { req: Request -> - next(req, authLens(req)) - } - infix fun PathMethod.to(action: ProtectedHandler) = this to { req: Request -> action(req, authLens(req)) } return auth.then( routes( "/settings" bind GET to settingsController::settings, - "/settings" bind POST to transaction.then(settingsController::settings), + "/settings" bind POST to settingsController::settings, "/export" bind POST to settingsController::export, ) ) diff --git a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/NoteService.kt b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/NoteService.kt index 212f6d0..9448732 100644 --- a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/NoteService.kt +++ b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/NoteService.kt @@ -6,6 +6,7 @@ import be.simplenotes.domain.usecases.markdown.MarkdownConverter import be.simplenotes.domain.usecases.markdown.MarkdownParsingError import be.simplenotes.persistance.repositories.NoteRepository import be.simplenotes.persistance.repositories.UserRepository +import be.simplenotes.persistance.transactions.TransactionService import be.simplenotes.search.NoteSearcher import be.simplenotes.search.SearchTerms import be.simplenotes.types.LoggedInUser @@ -24,30 +25,35 @@ class NoteService( private val userRepository: UserRepository, private val searcher: NoteSearcher, private val htmlSanitizer: HtmlSanitizer, + private val transaction: TransactionService, ) { - fun create(user: LoggedInUser, markdownText: String) = either.eager { - val persistedNote = !markdownConverter.renderDocument(markdownText) - .map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) } - .map { Note(it.metadata, markdown = markdownText, html = it.html) } - .map { noteRepository.create(user.userId, it) } + fun create(user: LoggedInUser, markdownText: String) = transaction.use { + either.eager { + val persistedNote = !markdownConverter.renderDocument(markdownText) + .map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) } + .map { Note(it.metadata, markdown = markdownText, html = it.html) } + .map { noteRepository.create(user.userId, it) } - searcher.indexNote(user.userId, persistedNote) - persistedNote + searcher.indexNote(user.userId, persistedNote) + persistedNote + } } fun update( user: LoggedInUser, uuid: UUID, markdownText: String, - ) = either.eager { - val persistedNote = !markdownConverter.renderDocument(markdownText) - .map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) } - .map { Note(it.metadata, markdown = markdownText, html = it.html) } - .map { noteRepository.update(user.userId, uuid, it) } + ) = transaction.use { + either.eager { + val persistedNote = !markdownConverter.renderDocument(markdownText) + .map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) } + .map { Note(it.metadata, markdown = markdownText, html = it.html) } + .map { noteRepository.update(user.userId, uuid, it) } - persistedNote?.let { searcher.updateIndex(user.userId, it) } - persistedNote + persistedNote?.let { searcher.updateIndex(user.userId, it) } + persistedNote + } } fun paginatedNotes( @@ -66,22 +72,22 @@ class NoteService( fun find(userId: Int, uuid: UUID) = noteRepository.find(userId, uuid) - fun trash(userId: Int, uuid: UUID): Boolean { + fun trash(userId: Int, uuid: UUID): Boolean = transaction.use { val res = noteRepository.delete(userId, uuid, permanent = false) if (res) searcher.deleteIndex(userId, uuid) - return res + res } - fun restore(userId: Int, uuid: UUID): Boolean { + fun restore(userId: Int, uuid: UUID): Boolean = transaction.use { val res = noteRepository.restore(userId, uuid) if (res) find(userId, uuid)?.let { note -> searcher.indexNote(userId, note) } - return res + res } - fun delete(userId: Int, uuid: UUID): Boolean { + fun delete(userId: Int, uuid: UUID): Boolean = transaction.use { val res = noteRepository.delete(userId, uuid, permanent = true) if (res) searcher.deleteIndex(userId, uuid) - return res + res } fun countDeleted(userId: Int) = noteRepository.count(userId, deleted = true) @@ -101,8 +107,8 @@ class NoteService( @PreDestroy fun dropAllIndexes() = searcher.dropAll() - fun makePublic(userId: Int, uuid: UUID) = noteRepository.makePublic(userId, uuid) - fun makePrivate(userId: Int, uuid: UUID) = noteRepository.makePrivate(userId, uuid) + fun makePublic(userId: Int, uuid: UUID) = transaction.use { noteRepository.makePublic(userId, uuid) } + fun makePrivate(userId: Int, uuid: UUID) = transaction.use { noteRepository.makePrivate(userId, uuid) } fun findPublic(uuid: UUID) = noteRepository.findPublic(uuid) } diff --git a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/users/delete/DeleteUseCaseImpl.kt b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/users/delete/DeleteUseCaseImpl.kt index d662436..ee7d5c8 100644 --- a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/users/delete/DeleteUseCaseImpl.kt +++ b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/users/delete/DeleteUseCaseImpl.kt @@ -6,6 +6,7 @@ import arrow.core.rightIfNotNull import be.simplenotes.domain.security.PasswordHash import be.simplenotes.domain.validation.UserValidations import be.simplenotes.persistance.repositories.UserRepository +import be.simplenotes.persistance.transactions.TransactionService import be.simplenotes.search.NoteSearcher import io.micronaut.context.annotation.Primary import javax.inject.Singleton @@ -16,16 +17,19 @@ internal class DeleteUseCaseImpl( private val userRepository: UserRepository, private val passwordHash: PasswordHash, private val searcher: NoteSearcher, + private val transactionService: TransactionService, ) : DeleteUseCase { - override fun delete(form: DeleteForm) = either.eager { - val user = !UserValidations.validateDelete(form) - val persistedUser = !userRepository.find(user.username).rightIfNotNull { DeleteError.Unregistered } - !Either.conditionally( - passwordHash.verify(user.password, persistedUser.password), - { DeleteError.WrongPassword }, - { Unit } - ) - !Either.conditionally(userRepository.delete(persistedUser.id), { DeleteError.Unregistered }, { Unit }) - searcher.dropIndex(persistedUser.id) + override fun delete(form: DeleteForm) = transactionService.use { + either.eager { + val user = !UserValidations.validateDelete(form) + val persistedUser = !userRepository.find(user.username).rightIfNotNull { DeleteError.Unregistered } + !Either.conditionally( + passwordHash.verify(user.password, persistedUser.password), + { DeleteError.WrongPassword }, + { Unit } + ) + !Either.conditionally(userRepository.delete(persistedUser.id), { DeleteError.Unregistered }, { Unit }) + searcher.dropIndex(persistedUser.id) + } } } diff --git a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/users/register/RegisterUseCaseImpl.kt b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/users/register/RegisterUseCaseImpl.kt index 06551c6..1986c31 100644 --- a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/users/register/RegisterUseCaseImpl.kt +++ b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/users/register/RegisterUseCaseImpl.kt @@ -6,6 +6,7 @@ import arrow.core.leftIfNull import be.simplenotes.domain.security.PasswordHash import be.simplenotes.domain.validation.UserValidations import be.simplenotes.persistance.repositories.UserRepository +import be.simplenotes.persistance.transactions.TransactionService import be.simplenotes.types.PersistedUser import io.micronaut.context.annotation.Primary import javax.inject.Singleton @@ -14,10 +15,11 @@ import javax.inject.Singleton @Singleton internal class RegisterUseCaseImpl( private val userRepository: UserRepository, - private val passwordHash: PasswordHash + private val passwordHash: PasswordHash, + private val transactionService: TransactionService, ) : RegisterUseCase { - override fun register(form: RegisterForm): Either { - return UserValidations.validateRegister(form) + override fun register(form: RegisterForm): Either = transactionService.use { + UserValidations.validateRegister(form) .filterOrElse({ !userRepository.exists(it.username) }, { UserExists }) .map { it.copy(password = passwordHash.crypt(it.password)) } .map { userRepository.create(it) } diff --git a/simplenotes-domain/src/test/kotlin/be/simplenotes/domain/usecases/users/register/RegisterUseCaseImplTest.kt b/simplenotes-domain/src/test/kotlin/be/simplenotes/domain/usecases/users/register/RegisterUseCaseImplTest.kt index f292c67..840d2b1 100644 --- a/simplenotes-domain/src/test/kotlin/be/simplenotes/domain/usecases/users/register/RegisterUseCaseImplTest.kt +++ b/simplenotes-domain/src/test/kotlin/be/simplenotes/domain/usecases/users/register/RegisterUseCaseImplTest.kt @@ -4,6 +4,7 @@ import be.simplenotes.domain.security.BcryptPasswordHash import be.simplenotes.domain.testutils.isLeftOfType import be.simplenotes.domain.testutils.isRight import be.simplenotes.persistance.repositories.UserRepository +import be.simplenotes.persistance.transactions.TransactionService import be.simplenotes.types.PersistedUser import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.equalTo @@ -16,7 +17,10 @@ internal class RegisterUseCaseImplTest { // region setup private val mockUserRepository = mockk() private val passwordHash = BcryptPasswordHash(test = true) - private val registerUseCase = RegisterUseCaseImpl(mockUserRepository, passwordHash) + private val noopTransactionService = object : TransactionService { + override fun use(block: () -> T) = block() + } + private val registerUseCase = RegisterUseCaseImpl(mockUserRepository, passwordHash, noopTransactionService) @BeforeEach fun resetMocks() { diff --git a/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/transactions/KtormTransactionService.kt b/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/transactions/KtormTransactionService.kt new file mode 100644 index 0000000..bbd18a9 --- /dev/null +++ b/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/transactions/KtormTransactionService.kt @@ -0,0 +1,9 @@ +package be.simplenotes.persistance.transactions + +import me.liuwj.ktorm.database.Database +import javax.inject.Singleton + +@Singleton +internal class KtormTransactionService(private val database: Database) : TransactionService { + override fun use(block: () -> T) = database.useTransaction { block() } +} diff --git a/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/transactions/TransactionService.kt b/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/transactions/TransactionService.kt new file mode 100644 index 0000000..30a05ea --- /dev/null +++ b/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/transactions/TransactionService.kt @@ -0,0 +1,5 @@ +package be.simplenotes.persistance.transactions + +interface TransactionService { + fun use(block: () -> T): T +}