Move transactions to domain layer

This commit is contained in:
Hubert Van De Walle 2020-11-05 14:37:20 +01:00
parent 11caff1634
commit bf56314473
12 changed files with 74 additions and 88 deletions

View File

@ -25,7 +25,6 @@ dependencies {
implementation(Libs.javaxServlet) implementation(Libs.javaxServlet)
implementation(Libs.kotlinxSerializationJson) implementation(Libs.kotlinxSerializationJson)
implementation(Libs.logbackClassic) implementation(Libs.logbackClassic)
implementation(Libs.ktormCore)
implementation(Libs.micronaut) implementation(Libs.micronaut)
kapt(Libs.micronautProcessor) kapt(Libs.micronautProcessor)

View File

@ -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)
}
}
}

View File

@ -2,10 +2,8 @@ package be.simplenotes.app.routes
import be.simplenotes.app.api.ApiNoteController import be.simplenotes.app.api.ApiNoteController
import be.simplenotes.app.api.ApiUserController 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.RequiredAuthFilter
import be.simplenotes.app.filters.auth.RequiredAuthLens import be.simplenotes.app.filters.auth.RequiredAuthLens
import org.http4k.core.Filter
import org.http4k.core.Method.* import org.http4k.core.Method.*
import org.http4k.core.Request import org.http4k.core.Request
import org.http4k.core.then import org.http4k.core.then
@ -21,16 +19,11 @@ import javax.inject.Singleton
class ApiRoutes( class ApiRoutes(
private val apiUserController: ApiUserController, private val apiUserController: ApiUserController,
private val apiNoteController: ApiNoteController, private val apiNoteController: ApiNoteController,
private val transaction: TransactionFilter,
@Named("api") private val auth: RequiredAuthFilter, @Named("api") private val auth: RequiredAuthFilter,
@Named("required") private val authLens: RequiredAuthLens, @Named("required") private val authLens: RequiredAuthLens,
) : Supplier<RoutingHttpHandler> { ) : Supplier<RoutingHttpHandler> {
override fun get(): RoutingHttpHandler { override fun get(): RoutingHttpHandler {
fun Filter.then(next: ProtectedHandler) = then { req: Request ->
next(req, authLens(req))
}
infix fun PathMethod.to(action: ProtectedHandler) = infix fun PathMethod.to(action: ProtectedHandler) =
this to { req: Request -> action(req, authLens(req)) } this to { req: Request -> action(req, authLens(req)) }
@ -41,10 +34,10 @@ class ApiRoutes(
auth.then( auth.then(
routes( routes(
"/" bind GET to ::notes, "/" bind GET to ::notes,
"/" bind POST to transaction.then(::createNote), "/" bind POST to ::createNote,
"/search" bind POST to ::search, "/search" bind POST to ::search,
"/{uuid}" bind GET to ::note, "/{uuid}" bind GET to ::note,
"/{uuid}" bind PUT to transaction.then(::update), "/{uuid}" bind PUT to ::update,
) )
).withBasePath("/notes") ).withBasePath("/notes")
} }

View File

@ -5,11 +5,9 @@ import be.simplenotes.app.controllers.HealthCheckController
import be.simplenotes.app.controllers.NoteController import be.simplenotes.app.controllers.NoteController
import be.simplenotes.app.controllers.UserController import be.simplenotes.app.controllers.UserController
import be.simplenotes.app.filters.ImmutableFilter 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.OptionalAuthFilter
import be.simplenotes.app.filters.auth.OptionalAuthLens import be.simplenotes.app.filters.auth.OptionalAuthLens
import org.http4k.core.ContentType import org.http4k.core.ContentType
import org.http4k.core.Filter
import org.http4k.core.Method.GET import org.http4k.core.Method.GET
import org.http4k.core.Method.POST import org.http4k.core.Method.POST
import org.http4k.core.Request import org.http4k.core.Request
@ -27,15 +25,10 @@ class BasicRoutes(
private val noteCtrl: NoteController, private val noteCtrl: NoteController,
@Named("optional") private val authLens: OptionalAuthLens, @Named("optional") private val authLens: OptionalAuthLens,
private val auth: OptionalAuthFilter, private val auth: OptionalAuthFilter,
private val transactionFilter: TransactionFilter,
) : Supplier<RoutingHttpHandler> { ) : Supplier<RoutingHttpHandler> {
override fun get(): RoutingHttpHandler { override fun get(): RoutingHttpHandler {
fun Filter.then(next: PublicHandler) = then { req: Request ->
next(req, authLens(req))
}
infix fun PathMethod.to(action: PublicHandler) = infix fun PathMethod.to(action: PublicHandler) =
this to { req: Request -> action(req, authLens(req)) } this to { req: Request -> action(req, authLens(req)) }
@ -52,7 +45,7 @@ class BasicRoutes(
routes( routes(
"/" bind GET to baseCtrl::index, "/" bind GET to baseCtrl::index,
"/register" bind GET to userCtrl::register, "/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 GET to userCtrl::login,
"/login" bind POST to userCtrl::login, "/login" bind POST to userCtrl::login,
"/logout" bind POST to userCtrl::logout, "/logout" bind POST to userCtrl::logout,

View File

@ -1,10 +1,8 @@
package be.simplenotes.app.routes package be.simplenotes.app.routes
import be.simplenotes.app.controllers.NoteController 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.RequiredAuthFilter
import be.simplenotes.app.filters.auth.RequiredAuthLens import be.simplenotes.app.filters.auth.RequiredAuthLens
import org.http4k.core.Filter
import org.http4k.core.Method.GET import org.http4k.core.Method.GET
import org.http4k.core.Method.POST import org.http4k.core.Method.POST
import org.http4k.core.Request import org.http4k.core.Request
@ -20,16 +18,11 @@ import javax.inject.Singleton
@Singleton @Singleton
class NoteRoutes( class NoteRoutes(
private val noteCtrl: NoteController, private val noteCtrl: NoteController,
private val transaction: TransactionFilter,
private val auth: RequiredAuthFilter, private val auth: RequiredAuthFilter,
@Named("required") private val authLens: RequiredAuthLens, @Named("required") private val authLens: RequiredAuthLens,
) : Supplier<RoutingHttpHandler> { ) : Supplier<RoutingHttpHandler> {
override fun get(): RoutingHttpHandler { override fun get(): RoutingHttpHandler {
fun Filter.then(next: ProtectedHandler) = then { req: Request ->
next(req, authLens(req))
}
infix fun PathMethod.to(action: ProtectedHandler) = infix fun PathMethod.to(action: ProtectedHandler) =
this to { req: Request -> action(req, authLens(req)) } this to { req: Request -> action(req, authLens(req)) }
@ -39,13 +32,13 @@ class NoteRoutes(
"/" bind GET to ::list, "/" bind GET to ::list,
"/" bind POST to ::search, "/" bind POST to ::search,
"/new" bind GET to ::new, "/new" bind GET to ::new,
"/new" bind POST to transaction.then(::new), "/new" bind POST to ::new,
"/trash" bind GET to ::trash, "/trash" bind GET to ::trash,
"/{uuid}" bind GET to ::note, "/{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 GET to ::edit,
"/{uuid}/edit" bind POST to transaction.then(::edit), "/{uuid}/edit" bind POST to ::edit,
"/deleted/{uuid}" bind POST to transaction.then(::deleted), "/deleted/{uuid}" bind POST to ::deleted,
).withBasePath("/notes") ).withBasePath("/notes")
} }
) )

View File

@ -1,10 +1,8 @@
package be.simplenotes.app.routes package be.simplenotes.app.routes
import be.simplenotes.app.controllers.SettingsController 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.RequiredAuthFilter
import be.simplenotes.app.filters.auth.RequiredAuthLens import be.simplenotes.app.filters.auth.RequiredAuthLens
import org.http4k.core.Filter
import org.http4k.core.Method.GET import org.http4k.core.Method.GET
import org.http4k.core.Method.POST import org.http4k.core.Method.POST
import org.http4k.core.Request import org.http4k.core.Request
@ -20,23 +18,18 @@ import javax.inject.Singleton
@Singleton @Singleton
class SettingsRoutes( class SettingsRoutes(
private val settingsController: SettingsController, private val settingsController: SettingsController,
private val transaction: TransactionFilter,
private val auth: RequiredAuthFilter, private val auth: RequiredAuthFilter,
@Named("required") private val authLens: RequiredAuthLens, @Named("required") private val authLens: RequiredAuthLens,
) : Supplier<RoutingHttpHandler> { ) : Supplier<RoutingHttpHandler> {
override fun get(): RoutingHttpHandler { override fun get(): RoutingHttpHandler {
fun Filter.then(next: ProtectedHandler) = then { req: Request ->
next(req, authLens(req))
}
infix fun PathMethod.to(action: ProtectedHandler) = infix fun PathMethod.to(action: ProtectedHandler) =
this to { req: Request -> action(req, authLens(req)) } this to { req: Request -> action(req, authLens(req)) }
return auth.then( return auth.then(
routes( routes(
"/settings" bind GET to settingsController::settings, "/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, "/export" bind POST to settingsController::export,
) )
) )

View File

@ -6,6 +6,7 @@ import be.simplenotes.domain.usecases.markdown.MarkdownConverter
import be.simplenotes.domain.usecases.markdown.MarkdownParsingError import be.simplenotes.domain.usecases.markdown.MarkdownParsingError
import be.simplenotes.persistance.repositories.NoteRepository import be.simplenotes.persistance.repositories.NoteRepository
import be.simplenotes.persistance.repositories.UserRepository import be.simplenotes.persistance.repositories.UserRepository
import be.simplenotes.persistance.transactions.TransactionService
import be.simplenotes.search.NoteSearcher import be.simplenotes.search.NoteSearcher
import be.simplenotes.search.SearchTerms import be.simplenotes.search.SearchTerms
import be.simplenotes.types.LoggedInUser import be.simplenotes.types.LoggedInUser
@ -24,30 +25,35 @@ class NoteService(
private val userRepository: UserRepository, private val userRepository: UserRepository,
private val searcher: NoteSearcher, private val searcher: NoteSearcher,
private val htmlSanitizer: HtmlSanitizer, private val htmlSanitizer: HtmlSanitizer,
private val transaction: TransactionService,
) { ) {
fun create(user: LoggedInUser, markdownText: String) = either.eager<MarkdownParsingError, PersistedNote> { fun create(user: LoggedInUser, markdownText: String) = transaction.use {
val persistedNote = !markdownConverter.renderDocument(markdownText) either.eager<MarkdownParsingError, PersistedNote> {
.map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) } val persistedNote = !markdownConverter.renderDocument(markdownText)
.map { Note(it.metadata, markdown = markdownText, html = it.html) } .map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) }
.map { noteRepository.create(user.userId, it) } .map { Note(it.metadata, markdown = markdownText, html = it.html) }
.map { noteRepository.create(user.userId, it) }
searcher.indexNote(user.userId, persistedNote) searcher.indexNote(user.userId, persistedNote)
persistedNote persistedNote
}
} }
fun update( fun update(
user: LoggedInUser, user: LoggedInUser,
uuid: UUID, uuid: UUID,
markdownText: String, markdownText: String,
) = either.eager<MarkdownParsingError, PersistedNote?> { ) = transaction.use {
val persistedNote = !markdownConverter.renderDocument(markdownText) either.eager<MarkdownParsingError, PersistedNote?> {
.map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) } val persistedNote = !markdownConverter.renderDocument(markdownText)
.map { Note(it.metadata, markdown = markdownText, html = it.html) } .map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) }
.map { noteRepository.update(user.userId, uuid, it) } .map { Note(it.metadata, markdown = markdownText, html = it.html) }
.map { noteRepository.update(user.userId, uuid, it) }
persistedNote?.let { searcher.updateIndex(user.userId, it) } persistedNote?.let { searcher.updateIndex(user.userId, it) }
persistedNote persistedNote
}
} }
fun paginatedNotes( fun paginatedNotes(
@ -66,22 +72,22 @@ class NoteService(
fun find(userId: Int, uuid: UUID) = noteRepository.find(userId, uuid) 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) val res = noteRepository.delete(userId, uuid, permanent = false)
if (res) searcher.deleteIndex(userId, uuid) 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) val res = noteRepository.restore(userId, uuid)
if (res) find(userId, uuid)?.let { note -> searcher.indexNote(userId, note) } 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) val res = noteRepository.delete(userId, uuid, permanent = true)
if (res) searcher.deleteIndex(userId, uuid) if (res) searcher.deleteIndex(userId, uuid)
return res res
} }
fun countDeleted(userId: Int) = noteRepository.count(userId, deleted = true) fun countDeleted(userId: Int) = noteRepository.count(userId, deleted = true)
@ -101,8 +107,8 @@ class NoteService(
@PreDestroy @PreDestroy
fun dropAllIndexes() = searcher.dropAll() fun dropAllIndexes() = searcher.dropAll()
fun makePublic(userId: Int, uuid: UUID) = noteRepository.makePublic(userId, uuid) fun makePublic(userId: Int, uuid: UUID) = transaction.use { noteRepository.makePublic(userId, uuid) }
fun makePrivate(userId: Int, uuid: UUID) = noteRepository.makePrivate(userId, uuid) fun makePrivate(userId: Int, uuid: UUID) = transaction.use { noteRepository.makePrivate(userId, uuid) }
fun findPublic(uuid: UUID) = noteRepository.findPublic(uuid) fun findPublic(uuid: UUID) = noteRepository.findPublic(uuid)
} }

View File

@ -6,6 +6,7 @@ import arrow.core.rightIfNotNull
import be.simplenotes.domain.security.PasswordHash import be.simplenotes.domain.security.PasswordHash
import be.simplenotes.domain.validation.UserValidations import be.simplenotes.domain.validation.UserValidations
import be.simplenotes.persistance.repositories.UserRepository import be.simplenotes.persistance.repositories.UserRepository
import be.simplenotes.persistance.transactions.TransactionService
import be.simplenotes.search.NoteSearcher import be.simplenotes.search.NoteSearcher
import io.micronaut.context.annotation.Primary import io.micronaut.context.annotation.Primary
import javax.inject.Singleton import javax.inject.Singleton
@ -16,16 +17,19 @@ internal class DeleteUseCaseImpl(
private val userRepository: UserRepository, private val userRepository: UserRepository,
private val passwordHash: PasswordHash, private val passwordHash: PasswordHash,
private val searcher: NoteSearcher, private val searcher: NoteSearcher,
private val transactionService: TransactionService,
) : DeleteUseCase { ) : DeleteUseCase {
override fun delete(form: DeleteForm) = either.eager<DeleteError, Unit> { override fun delete(form: DeleteForm) = transactionService.use {
val user = !UserValidations.validateDelete(form) either.eager<DeleteError, Unit> {
val persistedUser = !userRepository.find(user.username).rightIfNotNull { DeleteError.Unregistered } val user = !UserValidations.validateDelete(form)
!Either.conditionally( val persistedUser = !userRepository.find(user.username).rightIfNotNull { DeleteError.Unregistered }
passwordHash.verify(user.password, persistedUser.password), !Either.conditionally(
{ DeleteError.WrongPassword }, passwordHash.verify(user.password, persistedUser.password),
{ Unit } { DeleteError.WrongPassword },
) { Unit }
!Either.conditionally(userRepository.delete(persistedUser.id), { DeleteError.Unregistered }, { Unit }) )
searcher.dropIndex(persistedUser.id) !Either.conditionally(userRepository.delete(persistedUser.id), { DeleteError.Unregistered }, { Unit })
searcher.dropIndex(persistedUser.id)
}
} }
} }

View File

@ -6,6 +6,7 @@ import arrow.core.leftIfNull
import be.simplenotes.domain.security.PasswordHash import be.simplenotes.domain.security.PasswordHash
import be.simplenotes.domain.validation.UserValidations import be.simplenotes.domain.validation.UserValidations
import be.simplenotes.persistance.repositories.UserRepository import be.simplenotes.persistance.repositories.UserRepository
import be.simplenotes.persistance.transactions.TransactionService
import be.simplenotes.types.PersistedUser import be.simplenotes.types.PersistedUser
import io.micronaut.context.annotation.Primary import io.micronaut.context.annotation.Primary
import javax.inject.Singleton import javax.inject.Singleton
@ -14,10 +15,11 @@ import javax.inject.Singleton
@Singleton @Singleton
internal class RegisterUseCaseImpl( internal class RegisterUseCaseImpl(
private val userRepository: UserRepository, private val userRepository: UserRepository,
private val passwordHash: PasswordHash private val passwordHash: PasswordHash,
private val transactionService: TransactionService,
) : RegisterUseCase { ) : RegisterUseCase {
override fun register(form: RegisterForm): Either<RegisterError, PersistedUser> { override fun register(form: RegisterForm): Either<RegisterError, PersistedUser> = transactionService.use {
return UserValidations.validateRegister(form) UserValidations.validateRegister(form)
.filterOrElse({ !userRepository.exists(it.username) }, { UserExists }) .filterOrElse({ !userRepository.exists(it.username) }, { UserExists })
.map { it.copy(password = passwordHash.crypt(it.password)) } .map { it.copy(password = passwordHash.crypt(it.password)) }
.map { userRepository.create(it) } .map { userRepository.create(it) }

View File

@ -4,6 +4,7 @@ import be.simplenotes.domain.security.BcryptPasswordHash
import be.simplenotes.domain.testutils.isLeftOfType import be.simplenotes.domain.testutils.isLeftOfType
import be.simplenotes.domain.testutils.isRight import be.simplenotes.domain.testutils.isRight
import be.simplenotes.persistance.repositories.UserRepository import be.simplenotes.persistance.repositories.UserRepository
import be.simplenotes.persistance.transactions.TransactionService
import be.simplenotes.types.PersistedUser import be.simplenotes.types.PersistedUser
import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.assertion.assertThat
import com.natpryce.hamkrest.equalTo import com.natpryce.hamkrest.equalTo
@ -16,7 +17,10 @@ internal class RegisterUseCaseImplTest {
// region setup // region setup
private val mockUserRepository = mockk<UserRepository>() private val mockUserRepository = mockk<UserRepository>()
private val passwordHash = BcryptPasswordHash(test = true) private val passwordHash = BcryptPasswordHash(test = true)
private val registerUseCase = RegisterUseCaseImpl(mockUserRepository, passwordHash) private val noopTransactionService = object : TransactionService {
override fun <T> use(block: () -> T) = block()
}
private val registerUseCase = RegisterUseCaseImpl(mockUserRepository, passwordHash, noopTransactionService)
@BeforeEach @BeforeEach
fun resetMocks() { fun resetMocks() {

View File

@ -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 <T> use(block: () -> T) = database.useTransaction { block() }
}

View File

@ -0,0 +1,5 @@
package be.simplenotes.persistance.transactions
interface TransactionService {
fun <T> use(block: () -> T): T
}