Compare commits

..

No commits in common. "e6a7af840ad283dc5842bd23fe23fb4154770d73" and "11caff1634d0291370ba11d68db5eaa07e53f4a7" have entirely different histories.

19 changed files with 112 additions and 103 deletions

View File

@ -10,6 +10,8 @@ plugins {
}
dependencies {
implementation(project(":simplenotes-persistance"))
implementation(project(":simplenotes-search"))
implementation(project(":simplenotes-domain"))
implementation(project(":simplenotes-types"))
implementation(project(":simplenotes-config"))
@ -23,6 +25,7 @@ dependencies {
implementation(Libs.javaxServlet)
implementation(Libs.kotlinxSerializationJson)
implementation(Libs.logbackClassic)
implementation(Libs.ktormCore)
implementation(Libs.micronaut)
kapt(Libs.micronautProcessor)

View File

@ -1,6 +1,7 @@
package be.simplenotes.app.api
import be.simplenotes.app.extensions.auto
import be.simplenotes.app.utils.parseSearchTerms
import be.simplenotes.domain.usecases.NoteService
import be.simplenotes.types.LoggedInUser
import be.simplenotes.types.PersistedNote
@ -57,7 +58,8 @@ class ApiNoteController(
fun search(request: Request, loggedInUser: LoggedInUser): Response {
val query = searchContentLens(request)
val notes = noteService.search(loggedInUser.userId, query)
val terms = parseSearchTerms(query)
val notes = noteService.search(loggedInUser.userId, terms)
return persistedNotesMetadataLens(notes, Response(OK))
}

View File

@ -1,6 +1,6 @@
package be.simplenotes.app.controllers
import be.simplenotes.domain.usecases.HealthCheckService
import be.simplenotes.persistance.DbHealthCheck
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
@ -8,7 +8,7 @@ import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE
import javax.inject.Singleton
@Singleton
class HealthCheckController(private val healthCheckService: HealthCheckService) {
class HealthCheckController(private val dbHealthCheck: DbHealthCheck) {
fun healthCheck(@Suppress("UNUSED_PARAMETER") request: Request) =
if (healthCheckService.isOk()) Response(OK) else Response(SERVICE_UNAVAILABLE)
if (dbHealthCheck.isOk()) Response(OK) else Response(SERVICE_UNAVAILABLE)
}

View File

@ -2,6 +2,7 @@ package be.simplenotes.app.controllers
import be.simplenotes.app.extensions.html
import be.simplenotes.app.extensions.redirect
import be.simplenotes.app.utils.parseSearchTerms
import be.simplenotes.domain.usecases.NoteService
import be.simplenotes.domain.usecases.markdown.InvalidMeta
import be.simplenotes.domain.usecases.markdown.MissingMeta
@ -68,7 +69,8 @@ class NoteController(
fun search(request: Request, loggedInUser: LoggedInUser): Response {
val query = request.form("search") ?: ""
val notes = noteService.search(loggedInUser.userId, query)
val terms = parseSearchTerms(query)
val notes = noteService.search(loggedInUser.userId, terms)
val deletedCount = noteService.countDeleted(loggedInUser.userId)
return Response(OK).html(view.search(loggedInUser, notes, query, deletedCount))
}

View File

@ -0,0 +1,15 @@
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,8 +2,10 @@ 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
@ -19,11 +21,16 @@ 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<RoutingHttpHandler> {
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)) }
@ -34,10 +41,10 @@ class ApiRoutes(
auth.then(
routes(
"/" bind GET to ::notes,
"/" bind POST to ::createNote,
"/" bind POST to transaction.then(::createNote),
"/search" bind POST to ::search,
"/{uuid}" bind GET to ::note,
"/{uuid}" bind PUT to ::update,
"/{uuid}" bind PUT to transaction.then(::update),
)
).withBasePath("/notes")
}

View File

@ -5,9 +5,11 @@ 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
@ -25,10 +27,15 @@ class BasicRoutes(
private val noteCtrl: NoteController,
@Named("optional") private val authLens: OptionalAuthLens,
private val auth: OptionalAuthFilter,
private val transactionFilter: TransactionFilter,
) : Supplier<RoutingHttpHandler> {
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)) }
@ -45,7 +52,7 @@ class BasicRoutes(
routes(
"/" bind GET to baseCtrl::index,
"/register" bind GET to userCtrl::register,
"/register" bind POST to userCtrl::register,
"/register" bind POST to transactionFilter.then(userCtrl::register),
"/login" bind GET to userCtrl::login,
"/login" bind POST to userCtrl::login,
"/logout" bind POST to userCtrl::logout,

View File

@ -1,8 +1,10 @@
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
@ -18,11 +20,16 @@ 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<RoutingHttpHandler> {
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)) }
@ -32,13 +39,13 @@ class NoteRoutes(
"/" bind GET to ::list,
"/" bind POST to ::search,
"/new" bind GET to ::new,
"/new" bind POST to ::new,
"/new" bind POST to transaction.then(::new),
"/trash" bind GET to ::trash,
"/{uuid}" bind GET to ::note,
"/{uuid}" bind POST to ::note,
"/{uuid}" bind POST to transaction.then(::note),
"/{uuid}/edit" bind GET to ::edit,
"/{uuid}/edit" bind POST to ::edit,
"/deleted/{uuid}" bind POST to ::deleted,
"/{uuid}/edit" bind POST to transaction.then(::edit),
"/deleted/{uuid}" bind POST to transaction.then(::deleted),
).withBasePath("/notes")
}
)

View File

@ -1,8 +1,10 @@
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
@ -18,18 +20,23 @@ 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<RoutingHttpHandler> {
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 settingsController::settings,
"/settings" bind POST to transaction.then(settingsController::settings),
"/export" bind POST to settingsController::export,
)
)

View File

@ -1,4 +1,4 @@
package be.simplenotes.domain.usecases.search
package be.simplenotes.app.utils
import be.simplenotes.search.SearchTerms
@ -16,7 +16,7 @@ private val outerTagRe = outerRegex("tag")
private val contentRe = innerRegex("content")
private val outerContentRe = outerRegex("content")
internal fun parseSearchTerms(input: String): SearchTerms {
fun parseSearchTerms(input: String): SearchTerms {
var c: String = input
fun extract(innerRegex: Regex, outerRegex: Regex): String? {

View File

@ -1,8 +1,7 @@
package be.simplenotes.domain.usecases.search
package be.simplenotes.app.utils
import be.simplenotes.search.SearchTerms
import com.natpryce.hamkrest.assertion.assertThat
import com.natpryce.hamkrest.equalTo
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
import java.util.stream.Stream
@ -14,7 +13,7 @@ internal class SearchTermsParserKtTest {
title: String? = null,
tag: String? = null,
content: String? = null,
all: String? = null,
all: String? = null
): Pair<String, SearchTerms> = input to SearchTerms(title, tag, content, all)
@Suppress("Unused")
@ -40,6 +39,6 @@ internal class SearchTermsParserKtTest {
@ParameterizedTest
@MethodSource("results")
fun `valid search parser`(case: Pair<String, SearchTerms>) {
assertThat(parseSearchTerms(case.first), equalTo(case.second))
assertThat(parseSearchTerms(case.first)).isEqualTo(case.second)
}
}

View File

@ -1,13 +0,0 @@
package be.simplenotes.domain.usecases
import be.simplenotes.persistance.DbHealthCheck
import javax.inject.Singleton
interface HealthCheckService {
fun isOk(): Boolean
}
@Singleton
class HealthCheckServiceImpl(private val dbHealthCheck: DbHealthCheck) : HealthCheckService {
override fun isOk() = dbHealthCheck.isOk()
}

View File

@ -4,11 +4,10 @@ import arrow.core.computations.either
import be.simplenotes.domain.security.HtmlSanitizer
import be.simplenotes.domain.usecases.markdown.MarkdownConverter
import be.simplenotes.domain.usecases.markdown.MarkdownParsingError
import be.simplenotes.domain.usecases.search.parseSearchTerms
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
import be.simplenotes.types.Note
import be.simplenotes.types.PersistedNote
@ -25,35 +24,30 @@ class NoteService(
private val userRepository: UserRepository,
private val searcher: NoteSearcher,
private val htmlSanitizer: HtmlSanitizer,
private val transaction: TransactionService,
) {
fun create(user: LoggedInUser, markdownText: String) = transaction.use {
either.eager<MarkdownParsingError, PersistedNote> {
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) = either.eager<MarkdownParsingError, PersistedNote> {
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,
) = transaction.use {
either.eager<MarkdownParsingError, PersistedNote?> {
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) }
) = either.eager<MarkdownParsingError, PersistedNote?> {
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(
@ -72,22 +66,22 @@ class NoteService(
fun find(userId: Int, uuid: UUID) = noteRepository.find(userId, uuid)
fun trash(userId: Int, uuid: UUID): Boolean = transaction.use {
fun trash(userId: Int, uuid: UUID): Boolean {
val res = noteRepository.delete(userId, uuid, permanent = false)
if (res) searcher.deleteIndex(userId, uuid)
res
return res
}
fun restore(userId: Int, uuid: UUID): Boolean = transaction.use {
fun restore(userId: Int, uuid: UUID): Boolean {
val res = noteRepository.restore(userId, uuid)
if (res) find(userId, uuid)?.let { note -> searcher.indexNote(userId, note) }
res
return res
}
fun delete(userId: Int, uuid: UUID): Boolean = transaction.use {
fun delete(userId: Int, uuid: UUID): Boolean {
val res = noteRepository.delete(userId, uuid, permanent = true)
if (res) searcher.deleteIndex(userId, uuid)
res
return res
}
fun countDeleted(userId: Int) = noteRepository.count(userId, deleted = true)
@ -102,13 +96,13 @@ class NoteService(
}
}
fun search(userId: Int, searchInput: String) = searcher.search(userId, parseSearchTerms(searchInput))
fun search(userId: Int, searchTerms: SearchTerms) = searcher.search(userId, searchTerms)
@PreDestroy
fun dropAllIndexes() = searcher.dropAll()
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 makePublic(userId: Int, uuid: UUID) = noteRepository.makePublic(userId, uuid)
fun makePrivate(userId: Int, uuid: UUID) = noteRepository.makePrivate(userId, uuid)
fun findPublic(uuid: UUID) = noteRepository.findPublic(uuid)
}

View File

@ -6,7 +6,6 @@ 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
@ -17,19 +16,16 @@ 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) = transactionService.use {
either.eager<DeleteError, Unit> {
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) = either.eager<DeleteError, Unit> {
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)
}
}

View File

@ -6,7 +6,6 @@ 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
@ -15,11 +14,10 @@ import javax.inject.Singleton
@Singleton
internal class RegisterUseCaseImpl(
private val userRepository: UserRepository,
private val passwordHash: PasswordHash,
private val transactionService: TransactionService,
private val passwordHash: PasswordHash
) : RegisterUseCase {
override fun register(form: RegisterForm): Either<RegisterError, PersistedUser> = transactionService.use {
UserValidations.validateRegister(form)
override fun register(form: RegisterForm): Either<RegisterError, PersistedUser> {
return UserValidations.validateRegister(form)
.filterOrElse({ !userRepository.exists(it.username) }, { UserExists })
.map { it.copy(password = passwordHash.crypt(it.password)) }
.map { userRepository.create(it) }

View File

@ -4,7 +4,6 @@ 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
@ -17,10 +16,7 @@ internal class RegisterUseCaseImplTest {
// region setup
private val mockUserRepository = mockk<UserRepository>()
private val passwordHash = BcryptPasswordHash(test = true)
private val noopTransactionService = object : TransactionService {
override fun <T> use(block: () -> T) = block()
}
private val registerUseCase = RegisterUseCaseImpl(mockUserRepository, passwordHash, noopTransactionService)
private val registerUseCase = RegisterUseCaseImpl(mockUserRepository, passwordHash)
@BeforeEach
fun resetMocks() {

View File

@ -6,7 +6,8 @@ import be.simplenotes.persistance.utils.type
import me.liuwj.ktorm.database.Database
import me.liuwj.ktorm.database.asIterable
import me.liuwj.ktorm.database.use
import java.sql.SQLException
import java.io.EOFException
import java.sql.SQLTransientException
import javax.inject.Singleton
interface DbHealthCheck {
@ -25,7 +26,9 @@ internal class DbHealthCheckImpl(
it.executeQuery().asIterable().map { it.getString(1) }
}
}.any { it in dataSourceConfig.jdbcUrl }
} catch (e: SQLException) {
} catch (e: SQLTransientException) {
false
} catch (e: EOFException) {
false
}
}

View File

@ -1,9 +0,0 @@
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

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