Remove Boilerplate Use-case thingy
This commit is contained in:
parent
3e1683dfe5
commit
a4bf998c5b
@ -9,9 +9,8 @@ charset = utf-8
|
|||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
|
||||||
[*.{kt, kts}]
|
[*.{kt,kts}]
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
max_line_length = 120
|
max_line_length = 120
|
||||||
disabled_rules = no-wildcard-imports
|
disabled_rules = no-wildcard-imports,import-ordering
|
||||||
kotlin_imports_layout = idea
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
package be.simplenotes.app.api
|
package be.simplenotes.app.api
|
||||||
|
|
||||||
import be.simplenotes.app.extensions.auto
|
import be.simplenotes.app.extensions.auto
|
||||||
import be.simplenotes.domain.usecases.NoteService
|
import be.simplenotes.domain.NoteService
|
||||||
import be.simplenotes.types.LoggedInUser
|
import be.simplenotes.types.LoggedInUser
|
||||||
import be.simplenotes.types.PersistedNote
|
import be.simplenotes.types.PersistedNote
|
||||||
import be.simplenotes.types.PersistedNoteMetadata
|
import be.simplenotes.types.PersistedNoteMetadata
|
||||||
@ -76,4 +76,4 @@ data class NoteContent(val content: String)
|
|||||||
data class UuidContent(@Contextual val uuid: UUID)
|
data class UuidContent(@Contextual val uuid: UUID)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SearchContent(@Contextual val query: String)
|
data class SearchContent(val query: String)
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
package be.simplenotes.app.api
|
package be.simplenotes.app.api
|
||||||
|
|
||||||
import be.simplenotes.app.extensions.auto
|
import be.simplenotes.app.extensions.auto
|
||||||
import be.simplenotes.domain.usecases.UserService
|
import be.simplenotes.domain.LoginForm
|
||||||
import be.simplenotes.domain.usecases.users.login.LoginForm
|
import be.simplenotes.domain.UserService
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import org.http4k.core.Request
|
import org.http4k.core.Request
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package be.simplenotes.app.controllers
|
package be.simplenotes.app.controllers
|
||||||
|
|
||||||
import be.simplenotes.domain.usecases.HealthCheckService
|
import be.simplenotes.domain.HealthCheckService
|
||||||
import org.http4k.core.Request
|
import org.http4k.core.Request
|
||||||
import org.http4k.core.Response
|
import org.http4k.core.Response
|
||||||
import org.http4k.core.Status.Companion.OK
|
import org.http4k.core.Status.Companion.OK
|
||||||
|
|||||||
@ -2,10 +2,8 @@ package be.simplenotes.app.controllers
|
|||||||
|
|
||||||
import be.simplenotes.app.extensions.html
|
import be.simplenotes.app.extensions.html
|
||||||
import be.simplenotes.app.extensions.redirect
|
import be.simplenotes.app.extensions.redirect
|
||||||
import be.simplenotes.domain.usecases.NoteService
|
import be.simplenotes.domain.MarkdownParsingError
|
||||||
import be.simplenotes.domain.usecases.markdown.InvalidMeta
|
import be.simplenotes.domain.NoteService
|
||||||
import be.simplenotes.domain.usecases.markdown.MissingMeta
|
|
||||||
import be.simplenotes.domain.usecases.markdown.ValidationError
|
|
||||||
import be.simplenotes.types.LoggedInUser
|
import be.simplenotes.types.LoggedInUser
|
||||||
import be.simplenotes.views.NoteView
|
import be.simplenotes.views.NoteView
|
||||||
import org.http4k.core.Method
|
import org.http4k.core.Method
|
||||||
@ -34,17 +32,17 @@ class NoteController(
|
|||||||
return noteService.create(loggedInUser, markdownForm).fold(
|
return noteService.create(loggedInUser, markdownForm).fold(
|
||||||
{
|
{
|
||||||
val html = when (it) {
|
val html = when (it) {
|
||||||
MissingMeta -> view.noteEditor(
|
MarkdownParsingError.MissingMeta -> view.noteEditor(
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
error = "Missing note metadata",
|
error = "Missing note metadata",
|
||||||
textarea = markdownForm
|
textarea = markdownForm
|
||||||
)
|
)
|
||||||
InvalidMeta -> view.noteEditor(
|
MarkdownParsingError.InvalidMeta -> view.noteEditor(
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
error = "Invalid note metadata",
|
error = "Invalid note metadata",
|
||||||
textarea = markdownForm
|
textarea = markdownForm
|
||||||
)
|
)
|
||||||
is ValidationError -> view.noteEditor(
|
is MarkdownParsingError.ValidationError -> view.noteEditor(
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
validationErrors = it.validationErrors,
|
validationErrors = it.validationErrors,
|
||||||
textarea = markdownForm
|
textarea = markdownForm
|
||||||
@ -113,17 +111,17 @@ class NoteController(
|
|||||||
return noteService.update(loggedInUser, note.uuid, markdownForm).fold(
|
return noteService.update(loggedInUser, note.uuid, markdownForm).fold(
|
||||||
{
|
{
|
||||||
val html = when (it) {
|
val html = when (it) {
|
||||||
MissingMeta -> view.noteEditor(
|
MarkdownParsingError.MissingMeta -> view.noteEditor(
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
error = "Missing note metadata",
|
error = "Missing note metadata",
|
||||||
textarea = markdownForm
|
textarea = markdownForm
|
||||||
)
|
)
|
||||||
InvalidMeta -> view.noteEditor(
|
MarkdownParsingError.InvalidMeta -> view.noteEditor(
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
error = "Invalid note metadata",
|
error = "Invalid note metadata",
|
||||||
textarea = markdownForm
|
textarea = markdownForm
|
||||||
)
|
)
|
||||||
is ValidationError -> view.noteEditor(
|
is MarkdownParsingError.ValidationError -> view.noteEditor(
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
validationErrors = it.validationErrors,
|
validationErrors = it.validationErrors,
|
||||||
textarea = markdownForm
|
textarea = markdownForm
|
||||||
|
|||||||
@ -2,9 +2,10 @@ package be.simplenotes.app.controllers
|
|||||||
|
|
||||||
import be.simplenotes.app.extensions.html
|
import be.simplenotes.app.extensions.html
|
||||||
import be.simplenotes.app.extensions.redirect
|
import be.simplenotes.app.extensions.redirect
|
||||||
import be.simplenotes.domain.usecases.UserService
|
import be.simplenotes.domain.DeleteError
|
||||||
import be.simplenotes.domain.usecases.users.delete.DeleteError
|
import be.simplenotes.domain.DeleteForm
|
||||||
import be.simplenotes.domain.usecases.users.delete.DeleteForm
|
import be.simplenotes.domain.ExportService
|
||||||
|
import be.simplenotes.domain.UserService
|
||||||
import be.simplenotes.types.LoggedInUser
|
import be.simplenotes.types.LoggedInUser
|
||||||
import be.simplenotes.views.SettingView
|
import be.simplenotes.views.SettingView
|
||||||
import org.http4k.core.*
|
import org.http4k.core.*
|
||||||
@ -15,6 +16,7 @@ import javax.inject.Singleton
|
|||||||
@Singleton
|
@Singleton
|
||||||
class SettingsController(
|
class SettingsController(
|
||||||
private val userService: UserService,
|
private val userService: UserService,
|
||||||
|
private val exportService: ExportService,
|
||||||
private val settingView: SettingView,
|
private val settingView: SettingView,
|
||||||
) {
|
) {
|
||||||
fun settings(request: Request, loggedInUser: LoggedInUser): Response {
|
fun settings(request: Request, loggedInUser: LoggedInUser): Response {
|
||||||
@ -61,15 +63,15 @@ class SettingsController(
|
|||||||
return if (isDownload) {
|
return if (isDownload) {
|
||||||
val filename = "simplenotes-export-${loggedInUser.username}"
|
val filename = "simplenotes-export-${loggedInUser.username}"
|
||||||
if (request.form("format") == "zip") {
|
if (request.form("format") == "zip") {
|
||||||
val zip = userService.exportAsZip(loggedInUser.userId)
|
val zip = exportService.exportAsZip(loggedInUser.userId)
|
||||||
Response(Status.OK)
|
Response(Status.OK)
|
||||||
.with(attachment("$filename.zip", "application/zip"))
|
.with(attachment("$filename.zip", "application/zip"))
|
||||||
.body(zip)
|
.body(zip)
|
||||||
} else
|
} else
|
||||||
Response(Status.OK)
|
Response(Status.OK)
|
||||||
.with(attachment("$filename.json", "application/json"))
|
.with(attachment("$filename.json", "application/json"))
|
||||||
.body(userService.exportAsJson(loggedInUser.userId))
|
.body(exportService.exportAsJson(loggedInUser.userId))
|
||||||
} else Response(Status.OK).body(userService.exportAsJson(loggedInUser.userId)).header(
|
} else Response(Status.OK).body(exportService.exportAsJson(loggedInUser.userId)).header(
|
||||||
"Content-Type",
|
"Content-Type",
|
||||||
"application/json"
|
"application/json"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -4,11 +4,7 @@ import be.simplenotes.app.extensions.html
|
|||||||
import be.simplenotes.app.extensions.isSecure
|
import be.simplenotes.app.extensions.isSecure
|
||||||
import be.simplenotes.app.extensions.redirect
|
import be.simplenotes.app.extensions.redirect
|
||||||
import be.simplenotes.config.JwtConfig
|
import be.simplenotes.config.JwtConfig
|
||||||
import be.simplenotes.domain.usecases.UserService
|
import be.simplenotes.domain.*
|
||||||
import be.simplenotes.domain.usecases.users.login.*
|
|
||||||
import be.simplenotes.domain.usecases.users.register.InvalidRegisterForm
|
|
||||||
import be.simplenotes.domain.usecases.users.register.RegisterForm
|
|
||||||
import be.simplenotes.domain.usecases.users.register.UserExists
|
|
||||||
import be.simplenotes.types.LoggedInUser
|
import be.simplenotes.types.LoggedInUser
|
||||||
import be.simplenotes.views.UserView
|
import be.simplenotes.views.UserView
|
||||||
import org.http4k.core.Method.GET
|
import org.http4k.core.Method.GET
|
||||||
@ -39,11 +35,11 @@ class UserController(
|
|||||||
return result.fold(
|
return result.fold(
|
||||||
{
|
{
|
||||||
val html = when (it) {
|
val html = when (it) {
|
||||||
UserExists -> userView.register(
|
RegisterError.UserExists -> userView.register(
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
error = "User already exists"
|
error = "User already exists"
|
||||||
)
|
)
|
||||||
is InvalidRegisterForm ->
|
is RegisterError.InvalidRegisterForm ->
|
||||||
userView.register(
|
userView.register(
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
validationErrors = it.validationErrors
|
validationErrors = it.validationErrors
|
||||||
@ -70,17 +66,17 @@ class UserController(
|
|||||||
return result.fold(
|
return result.fold(
|
||||||
{
|
{
|
||||||
val html = when (it) {
|
val html = when (it) {
|
||||||
Unregistered ->
|
LoginError.Unregistered ->
|
||||||
userView.login(
|
userView.login(
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
error = "User does not exist"
|
error = "User does not exist"
|
||||||
)
|
)
|
||||||
WrongPassword ->
|
LoginError.WrongPassword ->
|
||||||
userView.login(
|
userView.login(
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
error = "Wrong password"
|
error = "Wrong password"
|
||||||
)
|
)
|
||||||
is InvalidLoginForm ->
|
is LoginError.InvalidLoginForm ->
|
||||||
userView.login(
|
userView.login(
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
validationErrors = it.validationErrors
|
validationErrors = it.validationErrors
|
||||||
|
|||||||
@ -25,5 +25,7 @@ tasks.withType<JavaCompile> {
|
|||||||
options.encoding = "UTF-8"
|
options.encoding = "UTF-8"
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets["main"].resources.srcDirs("resources")
|
sourceSets["main"].resources.setSrcDirs(listOf("resources"))
|
||||||
sourceSets["test"].resources.srcDirs("testresources")
|
sourceSets["main"].java.setSrcDirs(emptyList<String>())
|
||||||
|
sourceSets["test"].resources.setSrcDirs(listOf("testresources"))
|
||||||
|
sourceSets["test"].java.setSrcDirs(emptyList<String>())
|
||||||
|
|||||||
@ -25,5 +25,5 @@ tasks.withType<KotlinCompile> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin.sourceSets["main"].kotlin.srcDirs("src")
|
kotlin.sourceSets["main"].kotlin.setSrcDirs(listOf("src"))
|
||||||
kotlin.sourceSets["test"].kotlin.srcDirs("test")
|
kotlin.sourceSets["test"].kotlin.setSrcDirs(listOf("test"))
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
package be.simplenotes.domain.usecases.export
|
package be.simplenotes.domain
|
||||||
|
|
||||||
import be.simplenotes.persistence.repositories.NoteRepository
|
import be.simplenotes.persistence.repositories.NoteRepository
|
||||||
import be.simplenotes.types.ExportedNote
|
import be.simplenotes.types.ExportedNote
|
||||||
import io.micronaut.context.annotation.Primary
|
|
||||||
import kotlinx.serialization.builtins.ListSerializer
|
import kotlinx.serialization.builtins.ListSerializer
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
|
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
|
||||||
@ -12,12 +11,17 @@ import java.io.ByteArrayOutputStream
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Primary
|
interface ExportService {
|
||||||
|
fun exportAsJson(userId: Int): String
|
||||||
|
fun exportAsZip(userId: Int): InputStream
|
||||||
|
}
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
internal class ExportUseCaseImpl(
|
internal class ExportServiceImpl(
|
||||||
private val noteRepository: NoteRepository,
|
private val noteRepository: NoteRepository,
|
||||||
private val json: Json,
|
private val json: Json,
|
||||||
) : ExportUseCase {
|
) : ExportService {
|
||||||
|
|
||||||
override fun exportAsJson(userId: Int): String {
|
override fun exportAsJson(userId: Int): String {
|
||||||
val notes = noteRepository.export(userId)
|
val notes = noteRepository.export(userId)
|
||||||
return json.encodeToString(ListSerializer(ExportedNote.serializer()), notes)
|
return json.encodeToString(ListSerializer(ExportedNote.serializer()), notes)
|
||||||
@ -38,7 +42,7 @@ internal class ExportUseCaseImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ZipOutput : AutoCloseable {
|
private class ZipOutput : AutoCloseable {
|
||||||
val outputStream = ByteArrayOutputStream()
|
val outputStream = ByteArrayOutputStream()
|
||||||
private val zipOutputStream = ZipArchiveOutputStream(outputStream)
|
private val zipOutputStream = ZipArchiveOutputStream(outputStream)
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package be.simplenotes.domain.usecases
|
package be.simplenotes.domain
|
||||||
|
|
||||||
import be.simplenotes.persistence.DbHealthCheck
|
import be.simplenotes.persistence.DbHealthCheck
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
@ -1 +0,0 @@
|
|||||||
package be.simplenotes.domain
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package be.simplenotes.domain.usecases.markdown
|
package be.simplenotes.domain
|
||||||
|
|
||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
import arrow.core.computations.either
|
import arrow.core.computations.either
|
||||||
@ -8,38 +8,43 @@ import be.simplenotes.domain.validation.NoteValidations
|
|||||||
import be.simplenotes.types.NoteMetadata
|
import be.simplenotes.types.NoteMetadata
|
||||||
import com.vladsch.flexmark.html.HtmlRenderer
|
import com.vladsch.flexmark.html.HtmlRenderer
|
||||||
import com.vladsch.flexmark.parser.Parser
|
import com.vladsch.flexmark.parser.Parser
|
||||||
|
import io.konform.validation.ValidationErrors
|
||||||
import org.yaml.snakeyaml.Yaml
|
import org.yaml.snakeyaml.Yaml
|
||||||
import org.yaml.snakeyaml.parser.ParserException
|
import org.yaml.snakeyaml.parser.ParserException
|
||||||
import org.yaml.snakeyaml.scanner.ScannerException
|
import org.yaml.snakeyaml.scanner.ScannerException
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
interface MarkdownService {
|
||||||
|
fun renderDocument(input: String): Either<MarkdownParsingError, Document>
|
||||||
|
}
|
||||||
|
|
||||||
private typealias MetaMdPair = Pair<String, String>
|
private typealias MetaMdPair = Pair<String, String>
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
internal class MarkdownConverterImpl(
|
internal class MarkdownServiceImpl(
|
||||||
private val parser: Parser,
|
private val parser: Parser,
|
||||||
private val renderer: HtmlRenderer,
|
private val renderer: HtmlRenderer,
|
||||||
) : MarkdownConverter {
|
) : MarkdownService {
|
||||||
private val yamlBoundPattern = "-{3}".toRegex()
|
private val yamlBoundPattern = "-{3}".toRegex()
|
||||||
private fun splitMetaFromDocument(input: String): Either<MissingMeta, MetaMdPair> {
|
private fun splitMetaFromDocument(input: String): Either<MarkdownParsingError.MissingMeta, MetaMdPair> {
|
||||||
val split = input.split(yamlBoundPattern, 3)
|
val split = input.split(yamlBoundPattern, 3)
|
||||||
if (split.size < 3) return MissingMeta.left()
|
if (split.size < 3) return MarkdownParsingError.MissingMeta.left()
|
||||||
return (split[1].trim() to split[2].trim()).right()
|
return (split[1].trim() to split[2].trim()).right()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val yaml = Yaml()
|
private val yaml = Yaml()
|
||||||
private fun parseMeta(input: String): Either<InvalidMeta, NoteMetadata> {
|
private fun parseMeta(input: String): Either<MarkdownParsingError.InvalidMeta, NoteMetadata> {
|
||||||
val load: Map<String, Any> = try {
|
val load: Map<String, Any> = try {
|
||||||
yaml.load(input)
|
yaml.load(input)
|
||||||
} catch (e: ParserException) {
|
} catch (e: ParserException) {
|
||||||
return InvalidMeta.left()
|
return MarkdownParsingError.InvalidMeta.left()
|
||||||
} catch (e: ScannerException) {
|
} catch (e: ScannerException) {
|
||||||
return InvalidMeta.left()
|
return MarkdownParsingError.InvalidMeta.left()
|
||||||
}
|
}
|
||||||
|
|
||||||
val title = when (val titleNode = load["title"]) {
|
val title = when (val titleNode = load["title"]) {
|
||||||
is String, is Number -> titleNode.toString()
|
is String, is Number -> titleNode.toString()
|
||||||
else -> return InvalidMeta.left()
|
else -> return MarkdownParsingError.InvalidMeta.left()
|
||||||
}
|
}
|
||||||
|
|
||||||
val tagsNode = load["tags"]
|
val tagsNode = load["tags"]
|
||||||
@ -61,3 +66,11 @@ internal class MarkdownConverterImpl(
|
|||||||
Document(parsedMeta, html)
|
Document(parsedMeta, html)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed class MarkdownParsingError {
|
||||||
|
object MissingMeta : MarkdownParsingError()
|
||||||
|
object InvalidMeta : MarkdownParsingError()
|
||||||
|
class ValidationError(val validationErrors: ValidationErrors) : MarkdownParsingError()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Document(val metadata: NoteMetadata, val html: String)
|
||||||
@ -1,10 +1,8 @@
|
|||||||
package be.simplenotes.domain.usecases
|
package be.simplenotes.domain
|
||||||
|
|
||||||
import arrow.core.computations.either
|
import arrow.core.computations.either
|
||||||
import be.simplenotes.domain.security.HtmlSanitizer
|
import be.simplenotes.domain.security.HtmlSanitizer
|
||||||
import be.simplenotes.domain.usecases.markdown.MarkdownConverter
|
import be.simplenotes.domain.utils.parseSearchTerms
|
||||||
import be.simplenotes.domain.usecases.markdown.MarkdownParsingError
|
|
||||||
import be.simplenotes.domain.usecases.search.parseSearchTerms
|
|
||||||
import be.simplenotes.persistence.repositories.NoteRepository
|
import be.simplenotes.persistence.repositories.NoteRepository
|
||||||
import be.simplenotes.persistence.repositories.UserRepository
|
import be.simplenotes.persistence.repositories.UserRepository
|
||||||
import be.simplenotes.persistence.transactions.TransactionService
|
import be.simplenotes.persistence.transactions.TransactionService
|
||||||
@ -20,7 +18,7 @@ import javax.inject.Singleton
|
|||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class NoteService(
|
class NoteService(
|
||||||
private val markdownConverter: MarkdownConverter,
|
private val markdownService: MarkdownService,
|
||||||
private val noteRepository: NoteRepository,
|
private val noteRepository: NoteRepository,
|
||||||
private val userRepository: UserRepository,
|
private val userRepository: UserRepository,
|
||||||
private val searcher: NoteSearcher,
|
private val searcher: NoteSearcher,
|
||||||
@ -30,29 +28,23 @@ class NoteService(
|
|||||||
|
|
||||||
fun create(user: LoggedInUser, markdownText: String) = transaction.use {
|
fun create(user: LoggedInUser, markdownText: String) = transaction.use {
|
||||||
either.eager<MarkdownParsingError, PersistedNote> {
|
either.eager<MarkdownParsingError, PersistedNote> {
|
||||||
val persistedNote = !markdownConverter.renderDocument(markdownText)
|
markdownService.renderDocument(markdownText)
|
||||||
.map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) }
|
.map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) }
|
||||||
.map { Note(it.metadata, markdown = markdownText, html = it.html) }
|
.map { Note(it.metadata, markdown = markdownText, html = it.html) }
|
||||||
.map { noteRepository.create(user.userId, it) }
|
.map { noteRepository.create(user.userId, it) }
|
||||||
|
.bind()
|
||||||
searcher.indexNote(user.userId, persistedNote)
|
.also { searcher.indexNote(user.userId, it) }
|
||||||
persistedNote
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(
|
fun update(user: LoggedInUser, uuid: UUID, markdownText: String) = transaction.use {
|
||||||
user: LoggedInUser,
|
|
||||||
uuid: UUID,
|
|
||||||
markdownText: String,
|
|
||||||
) = transaction.use {
|
|
||||||
either.eager<MarkdownParsingError, PersistedNote?> {
|
either.eager<MarkdownParsingError, PersistedNote?> {
|
||||||
val persistedNote = !markdownConverter.renderDocument(markdownText)
|
markdownService.renderDocument(markdownText)
|
||||||
.map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) }
|
.map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) }
|
||||||
.map { Note(it.metadata, markdown = markdownText, html = it.html) }
|
.map { Note(it.metadata, markdown = markdownText, html = it.html) }
|
||||||
.map { noteRepository.update(user.userId, uuid, it) }
|
.map { noteRepository.update(user.userId, uuid, it) }
|
||||||
|
.bind()
|
||||||
persistedNote?.let { searcher.updateIndex(user.userId, it) }
|
?.also { searcher.updateIndex(user.userId, it) }
|
||||||
persistedNote
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
92
domain/src/UserService.kt
Normal file
92
domain/src/UserService.kt
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package be.simplenotes.domain
|
||||||
|
|
||||||
|
import arrow.core.Either
|
||||||
|
import arrow.core.computations.either
|
||||||
|
import arrow.core.filterOrElse
|
||||||
|
import arrow.core.leftIfNull
|
||||||
|
import arrow.core.rightIfNotNull
|
||||||
|
import be.simplenotes.domain.security.PasswordHash
|
||||||
|
import be.simplenotes.domain.security.SimpleJwt
|
||||||
|
import be.simplenotes.domain.validation.UserValidations
|
||||||
|
import be.simplenotes.persistence.repositories.UserRepository
|
||||||
|
import be.simplenotes.persistence.transactions.TransactionService
|
||||||
|
import be.simplenotes.search.NoteSearcher
|
||||||
|
import be.simplenotes.types.LoggedInUser
|
||||||
|
import be.simplenotes.types.PersistedUser
|
||||||
|
import io.konform.validation.ValidationErrors
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
interface UserService {
|
||||||
|
fun register(form: RegisterForm): Either<RegisterError, PersistedUser>
|
||||||
|
fun login(form: LoginForm): Either<LoginError, Token>
|
||||||
|
fun delete(form: DeleteForm): Either<DeleteError, Unit>
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
internal class UserServiceImpl(
|
||||||
|
private val userRepository: UserRepository,
|
||||||
|
private val passwordHash: PasswordHash,
|
||||||
|
private val jwt: SimpleJwt<LoggedInUser>,
|
||||||
|
private val searcher: NoteSearcher,
|
||||||
|
private val transactionService: TransactionService,
|
||||||
|
) : UserService {
|
||||||
|
|
||||||
|
override fun register(form: RegisterForm) = transactionService.use {
|
||||||
|
UserValidations.validateRegister(form)
|
||||||
|
.filterOrElse({ !userRepository.exists(it.username) }, { RegisterError.UserExists })
|
||||||
|
.map { it.copy(password = passwordHash.crypt(it.password)) }
|
||||||
|
.map { userRepository.create(it) }
|
||||||
|
.leftIfNull { RegisterError.UserExists }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun login(form: LoginForm) = either.eager<LoginError, Token> {
|
||||||
|
UserValidations.validateLogin(form)
|
||||||
|
.bind()
|
||||||
|
.let { userRepository.find(it.username) }
|
||||||
|
.rightIfNotNull { LoginError.Unregistered }
|
||||||
|
.filterOrElse({ passwordHash.verify(form.password!!, it.password) }, { LoginError.WrongPassword })
|
||||||
|
.map { jwt.sign(LoggedInUser(it)) }
|
||||||
|
.bind()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 },
|
||||||
|
{ }
|
||||||
|
)
|
||||||
|
!Either.conditionally(userRepository.delete(persistedUser.id), { DeleteError.Unregistered }, { })
|
||||||
|
searcher.dropIndex(persistedUser.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class DeleteError {
|
||||||
|
object Unregistered : DeleteError()
|
||||||
|
object WrongPassword : DeleteError()
|
||||||
|
class InvalidForm(val validationErrors: ValidationErrors) : DeleteError()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DeleteForm(val username: String?, val password: String?, val checked: Boolean)
|
||||||
|
|
||||||
|
sealed class LoginError {
|
||||||
|
object Unregistered : LoginError()
|
||||||
|
object WrongPassword : LoginError()
|
||||||
|
class InvalidLoginForm(val validationErrors: ValidationErrors) : LoginError()
|
||||||
|
}
|
||||||
|
|
||||||
|
typealias Token = String
|
||||||
|
|
||||||
|
sealed class RegisterError {
|
||||||
|
object UserExists : RegisterError()
|
||||||
|
class InvalidRegisterForm(val validationErrors: ValidationErrors) : RegisterError()
|
||||||
|
}
|
||||||
|
|
||||||
|
typealias RegisterForm = LoginForm
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LoginForm(val username: String?, val password: String?)
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package be.simplenotes.domain.usecases.markdown
|
package be.simplenotes.domain.modules
|
||||||
|
|
||||||
import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension
|
import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension
|
||||||
import com.vladsch.flexmark.html.HtmlRenderer
|
import com.vladsch.flexmark.html.HtmlRenderer
|
||||||
@ -1,18 +0,0 @@
|
|||||||
package be.simplenotes.domain.usecases
|
|
||||||
|
|
||||||
import be.simplenotes.domain.usecases.export.ExportUseCase
|
|
||||||
import be.simplenotes.domain.usecases.users.delete.DeleteUseCase
|
|
||||||
import be.simplenotes.domain.usecases.users.login.LoginUseCase
|
|
||||||
import be.simplenotes.domain.usecases.users.register.RegisterUseCase
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class UserService(
|
|
||||||
loginUseCase: LoginUseCase,
|
|
||||||
registerUseCase: RegisterUseCase,
|
|
||||||
deleteUseCase: DeleteUseCase,
|
|
||||||
exportUseCase: ExportUseCase,
|
|
||||||
) : LoginUseCase by loginUseCase,
|
|
||||||
RegisterUseCase by registerUseCase,
|
|
||||||
DeleteUseCase by deleteUseCase,
|
|
||||||
ExportUseCase by exportUseCase
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
package be.simplenotes.domain.usecases.export
|
|
||||||
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
interface ExportUseCase {
|
|
||||||
fun exportAsJson(userId: Int): String
|
|
||||||
fun exportAsZip(userId: Int): InputStream
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
package be.simplenotes.domain.usecases.markdown
|
|
||||||
|
|
||||||
import arrow.core.Either
|
|
||||||
import be.simplenotes.types.NoteMetadata
|
|
||||||
import io.konform.validation.ValidationErrors
|
|
||||||
|
|
||||||
sealed class MarkdownParsingError
|
|
||||||
object MissingMeta : MarkdownParsingError()
|
|
||||||
object InvalidMeta : MarkdownParsingError()
|
|
||||||
class ValidationError(val validationErrors: ValidationErrors) : MarkdownParsingError()
|
|
||||||
|
|
||||||
data class Document(val metadata: NoteMetadata, val html: String)
|
|
||||||
|
|
||||||
interface MarkdownConverter {
|
|
||||||
fun renderDocument(input: String): Either<MarkdownParsingError, Document>
|
|
||||||
}
|
|
||||||
@ -1,96 +0,0 @@
|
|||||||
package be.simplenotes.domain.usecases.search
|
|
||||||
|
|
||||||
import be.simplenotes.search.SearchTerms
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
private enum class Quote { SingleQuote, DoubleQuote, }
|
|
||||||
|
|
||||||
data class ParsedSearchInput(val global: List<String>, val entries: Map<String, String>)
|
|
||||||
|
|
||||||
object SearchInputParser {
|
|
||||||
fun parseInput(input: String): ParsedSearchInput {
|
|
||||||
val tokenizer = StringTokenizer(input, ":\"' ", true)
|
|
||||||
|
|
||||||
val tokens = ArrayList<String>()
|
|
||||||
val current = StringBuilder()
|
|
||||||
var quoteOpen: Quote? = null
|
|
||||||
|
|
||||||
fun push() {
|
|
||||||
if (current.isNotEmpty()) {
|
|
||||||
tokens.add(current.toString())
|
|
||||||
}
|
|
||||||
current.setLength(0)
|
|
||||||
quoteOpen = null
|
|
||||||
}
|
|
||||||
|
|
||||||
while (tokenizer.hasMoreTokens()) {
|
|
||||||
when (val token = tokenizer.nextToken()) {
|
|
||||||
"\"" -> when {
|
|
||||||
Quote.DoubleQuote == quoteOpen -> push()
|
|
||||||
quoteOpen == null -> quoteOpen = Quote.DoubleQuote
|
|
||||||
else -> current.append(token)
|
|
||||||
}
|
|
||||||
"'" -> when {
|
|
||||||
Quote.SingleQuote == quoteOpen -> push()
|
|
||||||
quoteOpen == null -> quoteOpen = Quote.SingleQuote
|
|
||||||
else -> current.append(token)
|
|
||||||
}
|
|
||||||
" " -> {
|
|
||||||
if (quoteOpen != null) current.append(" ")
|
|
||||||
else push()
|
|
||||||
}
|
|
||||||
":" -> {
|
|
||||||
push()
|
|
||||||
tokens.add(token)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
current.append(token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
push()
|
|
||||||
|
|
||||||
val entries = HashMap<String, String>()
|
|
||||||
|
|
||||||
val colonIndexes = ArrayList<Int>()
|
|
||||||
tokens.forEachIndexed { index, token ->
|
|
||||||
if (token == ":") colonIndexes += index
|
|
||||||
}
|
|
||||||
|
|
||||||
var changes = 0
|
|
||||||
for (colonIndex in colonIndexes) {
|
|
||||||
val offset = changes * 3
|
|
||||||
|
|
||||||
val key = tokens.getOrNull(colonIndex - 1 - offset)
|
|
||||||
val value = tokens.getOrNull(colonIndex + 1 - offset)
|
|
||||||
|
|
||||||
if (key != null && value != null) {
|
|
||||||
entries[key] = value
|
|
||||||
tokens.removeAt(colonIndex - 1 - offset) // remove key
|
|
||||||
tokens.removeAt(colonIndex - 1 - offset) // remove :
|
|
||||||
tokens.removeAt(colonIndex - 1 - offset) // remove value
|
|
||||||
changes++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ParsedSearchInput(global = tokens, entries = entries)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun parseSearchTerms(input: String): SearchTerms {
|
|
||||||
val parsedInput = SearchInputParser.parseInput(input)
|
|
||||||
|
|
||||||
val title: String? = parsedInput.entries["title"]
|
|
||||||
val tag: String? = parsedInput.entries["tag"]
|
|
||||||
val content: String? = parsedInput.entries["content"]
|
|
||||||
|
|
||||||
val all = parsedInput.global.takeIf { it.isNotEmpty() }?.joinToString(" ")
|
|
||||||
|
|
||||||
return SearchTerms(
|
|
||||||
title = title,
|
|
||||||
tag = tag,
|
|
||||||
content = content,
|
|
||||||
all = all
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
package be.simplenotes.domain.usecases.users.delete
|
|
||||||
|
|
||||||
import arrow.core.Either
|
|
||||||
import arrow.core.computations.either
|
|
||||||
import arrow.core.rightIfNotNull
|
|
||||||
import be.simplenotes.domain.security.PasswordHash
|
|
||||||
import be.simplenotes.domain.validation.UserValidations
|
|
||||||
import be.simplenotes.persistence.repositories.UserRepository
|
|
||||||
import be.simplenotes.persistence.transactions.TransactionService
|
|
||||||
import be.simplenotes.search.NoteSearcher
|
|
||||||
import io.micronaut.context.annotation.Primary
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Primary
|
|
||||||
@Singleton
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
package be.simplenotes.domain.usecases.users.delete
|
|
||||||
|
|
||||||
import arrow.core.Either
|
|
||||||
import io.konform.validation.ValidationErrors
|
|
||||||
|
|
||||||
sealed class DeleteError {
|
|
||||||
object Unregistered : DeleteError()
|
|
||||||
object WrongPassword : DeleteError()
|
|
||||||
class InvalidForm(val validationErrors: ValidationErrors) : DeleteError()
|
|
||||||
}
|
|
||||||
|
|
||||||
data class DeleteForm(val username: String?, val password: String?, val checked: Boolean)
|
|
||||||
|
|
||||||
interface DeleteUseCase {
|
|
||||||
fun delete(form: DeleteForm): Either<DeleteError, Unit>
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
package be.simplenotes.domain.usecases.users.login
|
|
||||||
|
|
||||||
import arrow.core.computations.either
|
|
||||||
import arrow.core.filterOrElse
|
|
||||||
import arrow.core.rightIfNotNull
|
|
||||||
import be.simplenotes.domain.security.PasswordHash
|
|
||||||
import be.simplenotes.domain.security.SimpleJwt
|
|
||||||
import be.simplenotes.domain.validation.UserValidations
|
|
||||||
import be.simplenotes.persistence.repositories.UserRepository
|
|
||||||
import be.simplenotes.types.LoggedInUser
|
|
||||||
import io.micronaut.context.annotation.Primary
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@Primary
|
|
||||||
internal class LoginUseCaseImpl(
|
|
||||||
private val userRepository: UserRepository,
|
|
||||||
private val passwordHash: PasswordHash,
|
|
||||||
private val jwt: SimpleJwt<LoggedInUser>
|
|
||||||
) : LoginUseCase {
|
|
||||||
override fun login(form: LoginForm) = either.eager<LoginError, Token> {
|
|
||||||
val user = !UserValidations.validateLogin(form)
|
|
||||||
!userRepository.find(user.username)
|
|
||||||
.rightIfNotNull { Unregistered }
|
|
||||||
.filterOrElse({ passwordHash.verify(form.password!!, it.password) }, { WrongPassword })
|
|
||||||
.map { jwt.sign(LoggedInUser(it)) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
package be.simplenotes.domain.usecases.users.login
|
|
||||||
|
|
||||||
import arrow.core.Either
|
|
||||||
import io.konform.validation.ValidationErrors
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
sealed class LoginError
|
|
||||||
object Unregistered : LoginError()
|
|
||||||
object WrongPassword : LoginError()
|
|
||||||
class InvalidLoginForm(val validationErrors: ValidationErrors) : LoginError()
|
|
||||||
|
|
||||||
typealias Token = String
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class LoginForm(val username: String?, val password: String?)
|
|
||||||
|
|
||||||
interface LoginUseCase {
|
|
||||||
fun login(form: LoginForm): Either<LoginError, Token>
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
package be.simplenotes.domain.usecases.users.register
|
|
||||||
|
|
||||||
import arrow.core.Either
|
|
||||||
import arrow.core.filterOrElse
|
|
||||||
import arrow.core.leftIfNull
|
|
||||||
import be.simplenotes.domain.security.PasswordHash
|
|
||||||
import be.simplenotes.domain.validation.UserValidations
|
|
||||||
import be.simplenotes.persistence.repositories.UserRepository
|
|
||||||
import be.simplenotes.persistence.transactions.TransactionService
|
|
||||||
import be.simplenotes.types.PersistedUser
|
|
||||||
import io.micronaut.context.annotation.Primary
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Primary
|
|
||||||
@Singleton
|
|
||||||
internal class RegisterUseCaseImpl(
|
|
||||||
private val userRepository: UserRepository,
|
|
||||||
private val passwordHash: PasswordHash,
|
|
||||||
private val transactionService: TransactionService,
|
|
||||||
) : RegisterUseCase {
|
|
||||||
override fun register(form: RegisterForm): Either<RegisterError, PersistedUser> = transactionService.use {
|
|
||||||
UserValidations.validateRegister(form)
|
|
||||||
.filterOrElse({ !userRepository.exists(it.username) }, { UserExists })
|
|
||||||
.map { it.copy(password = passwordHash.crypt(it.password)) }
|
|
||||||
.map { userRepository.create(it) }
|
|
||||||
.leftIfNull { UserExists }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
package be.simplenotes.domain.usecases.users.register
|
|
||||||
|
|
||||||
import arrow.core.Either
|
|
||||||
import be.simplenotes.domain.usecases.users.login.LoginForm
|
|
||||||
import be.simplenotes.types.PersistedUser
|
|
||||||
import io.konform.validation.ValidationErrors
|
|
||||||
|
|
||||||
sealed class RegisterError
|
|
||||||
object UserExists : RegisterError()
|
|
||||||
class InvalidRegisterForm(val validationErrors: ValidationErrors) : RegisterError()
|
|
||||||
|
|
||||||
typealias RegisterForm = LoginForm
|
|
||||||
|
|
||||||
interface RegisterUseCase {
|
|
||||||
fun register(form: RegisterForm): Either<RegisterError, PersistedUser>
|
|
||||||
}
|
|
||||||
94
domain/src/utils/SearchTermsParser.kt
Normal file
94
domain/src/utils/SearchTermsParser.kt
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package be.simplenotes.domain.utils
|
||||||
|
|
||||||
|
import be.simplenotes.search.SearchTerms
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
private enum class Quote { SingleQuote, DoubleQuote, }
|
||||||
|
|
||||||
|
private data class ParsedSearchInput(val global: List<String>, val entries: Map<String, String>)
|
||||||
|
|
||||||
|
private fun parseInput(input: String): ParsedSearchInput {
|
||||||
|
val tokenizer = StringTokenizer(input, ":\"' ", true)
|
||||||
|
|
||||||
|
val tokens = ArrayList<String>()
|
||||||
|
val current = StringBuilder()
|
||||||
|
var quoteOpen: Quote? = null
|
||||||
|
|
||||||
|
fun push() {
|
||||||
|
if (current.isNotEmpty()) {
|
||||||
|
tokens.add(current.toString())
|
||||||
|
}
|
||||||
|
current.setLength(0)
|
||||||
|
quoteOpen = null
|
||||||
|
}
|
||||||
|
|
||||||
|
while (tokenizer.hasMoreTokens()) {
|
||||||
|
when (val token = tokenizer.nextToken()) {
|
||||||
|
"\"" -> when {
|
||||||
|
Quote.DoubleQuote == quoteOpen -> push()
|
||||||
|
quoteOpen == null -> quoteOpen = Quote.DoubleQuote
|
||||||
|
else -> current.append(token)
|
||||||
|
}
|
||||||
|
"'" -> when {
|
||||||
|
Quote.SingleQuote == quoteOpen -> push()
|
||||||
|
quoteOpen == null -> quoteOpen = Quote.SingleQuote
|
||||||
|
else -> current.append(token)
|
||||||
|
}
|
||||||
|
" " -> {
|
||||||
|
if (quoteOpen != null) current.append(" ")
|
||||||
|
else push()
|
||||||
|
}
|
||||||
|
":" -> {
|
||||||
|
push()
|
||||||
|
tokens.add(token)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
current.append(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
push()
|
||||||
|
|
||||||
|
val entries = HashMap<String, String>()
|
||||||
|
|
||||||
|
val colonIndexes = ArrayList<Int>()
|
||||||
|
tokens.forEachIndexed { index, token ->
|
||||||
|
if (token == ":") colonIndexes += index
|
||||||
|
}
|
||||||
|
|
||||||
|
var changes = 0
|
||||||
|
for (colonIndex in colonIndexes) {
|
||||||
|
val offset = changes * 3
|
||||||
|
|
||||||
|
val key = tokens.getOrNull(colonIndex - 1 - offset)
|
||||||
|
val value = tokens.getOrNull(colonIndex + 1 - offset)
|
||||||
|
|
||||||
|
if (key != null && value != null) {
|
||||||
|
entries[key] = value
|
||||||
|
tokens.removeAt(colonIndex - 1 - offset) // remove key
|
||||||
|
tokens.removeAt(colonIndex - 1 - offset) // remove :
|
||||||
|
tokens.removeAt(colonIndex - 1 - offset) // remove value
|
||||||
|
changes++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParsedSearchInput(global = tokens, entries = entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun parseSearchTerms(input: String): SearchTerms {
|
||||||
|
val parsedInput = parseInput(input)
|
||||||
|
|
||||||
|
val title: String? = parsedInput.entries["title"]
|
||||||
|
val tag: String? = parsedInput.entries["tag"]
|
||||||
|
val content: String? = parsedInput.entries["content"]
|
||||||
|
|
||||||
|
val all = parsedInput.global.takeIf { it.isNotEmpty() }?.joinToString(" ")
|
||||||
|
|
||||||
|
return SearchTerms(
|
||||||
|
title = title,
|
||||||
|
tag = tag,
|
||||||
|
content = content,
|
||||||
|
all = all
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
package be.simplenotes.domain.validation
|
package be.simplenotes.domain.validation
|
||||||
|
|
||||||
import be.simplenotes.domain.usecases.markdown.ValidationError
|
import be.simplenotes.domain.MarkdownParsingError
|
||||||
import be.simplenotes.types.NoteMetadata
|
import be.simplenotes.types.NoteMetadata
|
||||||
import io.konform.validation.Validation
|
import io.konform.validation.Validation
|
||||||
import io.konform.validation.jsonschema.maxItems
|
import io.konform.validation.jsonschema.maxItems
|
||||||
@ -27,9 +27,9 @@ internal object NoteValidations {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun validateMetadata(meta: NoteMetadata): ValidationError? {
|
fun validateMetadata(meta: NoteMetadata): MarkdownParsingError.ValidationError? {
|
||||||
val errors = metaValidator.validate(meta).errors
|
val errors = metaValidator.validate(meta).errors
|
||||||
return if (errors.isEmpty()) null
|
return if (errors.isEmpty()) null
|
||||||
else return ValidationError(errors)
|
else return MarkdownParsingError.ValidationError(errors)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,12 +3,7 @@ package be.simplenotes.domain.validation
|
|||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
import arrow.core.left
|
import arrow.core.left
|
||||||
import arrow.core.right
|
import arrow.core.right
|
||||||
import be.simplenotes.domain.usecases.users.delete.DeleteError
|
import be.simplenotes.domain.*
|
||||||
import be.simplenotes.domain.usecases.users.delete.DeleteForm
|
|
||||||
import be.simplenotes.domain.usecases.users.login.InvalidLoginForm
|
|
||||||
import be.simplenotes.domain.usecases.users.login.LoginForm
|
|
||||||
import be.simplenotes.domain.usecases.users.register.InvalidRegisterForm
|
|
||||||
import be.simplenotes.domain.usecases.users.register.RegisterForm
|
|
||||||
import be.simplenotes.types.User
|
import be.simplenotes.types.User
|
||||||
import io.konform.validation.Validation
|
import io.konform.validation.Validation
|
||||||
import io.konform.validation.jsonschema.maxLength
|
import io.konform.validation.jsonschema.maxLength
|
||||||
@ -26,16 +21,16 @@ internal object UserValidations {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun validateLogin(form: LoginForm): Either<InvalidLoginForm, User> {
|
fun validateLogin(form: LoginForm): Either<LoginError.InvalidLoginForm, User> {
|
||||||
val errors = loginValidator.validate(form).errors
|
val errors = loginValidator.validate(form).errors
|
||||||
return if (errors.isEmpty()) User(form.username!!, form.password!!).right()
|
return if (errors.isEmpty()) User(form.username!!, form.password!!).right()
|
||||||
else return InvalidLoginForm(errors).left()
|
else return LoginError.InvalidLoginForm(errors).left()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun validateRegister(form: RegisterForm): Either<InvalidRegisterForm, User> {
|
fun validateRegister(form: RegisterForm): Either<RegisterError.InvalidRegisterForm, User> {
|
||||||
val errors = loginValidator.validate(form).errors
|
val errors = loginValidator.validate(form).errors
|
||||||
return if (errors.isEmpty()) User(form.username!!, form.password!!).right()
|
return if (errors.isEmpty()) User(form.username!!, form.password!!).right()
|
||||||
else return InvalidRegisterForm(errors).left()
|
else return RegisterError.InvalidRegisterForm(errors).left()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val deleteValidator = Validation<DeleteForm> {
|
private val deleteValidator = Validation<DeleteForm> {
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
package be.simplenotes.domain
|
|
||||||
101
domain/test/UserServiceTest.kt
Normal file
101
domain/test/UserServiceTest.kt
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
@file:Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
|
||||||
|
package be.simplenotes.domain
|
||||||
|
|
||||||
|
import be.simplenotes.config.JwtConfig
|
||||||
|
import be.simplenotes.domain.security.BcryptPasswordHash
|
||||||
|
import be.simplenotes.domain.security.SimpleJwt
|
||||||
|
import be.simplenotes.domain.security.UserJwtMapper
|
||||||
|
import be.simplenotes.domain.testutils.isLeftOfType
|
||||||
|
import be.simplenotes.domain.testutils.isRight
|
||||||
|
import be.simplenotes.persistence.repositories.UserRepository
|
||||||
|
import be.simplenotes.persistence.transactions.TransactionService
|
||||||
|
import be.simplenotes.types.PersistedUser
|
||||||
|
import com.natpryce.hamkrest.assertion.assertThat
|
||||||
|
import com.natpryce.hamkrest.equalTo
|
||||||
|
import io.mockk.*
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
internal class UserServiceTest {
|
||||||
|
val userRepository = mockk<UserRepository>()
|
||||||
|
val passwordHash = BcryptPasswordHash(test = true)
|
||||||
|
val jwtConfig = JwtConfig("a secret", 1, TimeUnit.HOURS)
|
||||||
|
val simpleJwt = SimpleJwt(jwtConfig, UserJwtMapper())
|
||||||
|
val noopTransactionService = object : TransactionService {
|
||||||
|
override fun <T> use(block: () -> T) = block()
|
||||||
|
}
|
||||||
|
|
||||||
|
val userService = UserServiceImpl(
|
||||||
|
userRepository = userRepository,
|
||||||
|
passwordHash = passwordHash,
|
||||||
|
jwt = simpleJwt,
|
||||||
|
searcher = mockk(),
|
||||||
|
transactionService = noopTransactionService
|
||||||
|
)
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun resetMocks() {
|
||||||
|
clearMocks(userRepository)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `register should fail with invalid form`() {
|
||||||
|
val form = RegisterForm("", "a".repeat(10))
|
||||||
|
assertThat(userService.register(form), isLeftOfType<RegisterError.InvalidRegisterForm>())
|
||||||
|
verify { userRepository wasNot called }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Register should fail with existing username`() {
|
||||||
|
val form = RegisterForm("someuser", "somepassword")
|
||||||
|
every { userRepository.exists(form.username!!) } returns true
|
||||||
|
assertThat(userService.register(form), isLeftOfType<RegisterError.UserExists>())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Register should succeed with new user`() {
|
||||||
|
val form = RegisterForm("someuser", "somepassword")
|
||||||
|
every { userRepository.exists(form.username!!) } returns false
|
||||||
|
every { userRepository.create(any()) } returns PersistedUser(form.username!!, form.password!!, 1)
|
||||||
|
val res = userService.register(form)
|
||||||
|
assertThat(res, isRight())
|
||||||
|
res.map { assertThat(it.username, equalTo(form.username)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Login should fail with invalid form`() {
|
||||||
|
val form = LoginForm("", "a")
|
||||||
|
assertThat(userService.login(form), isLeftOfType<LoginError.InvalidLoginForm>())
|
||||||
|
verify { userRepository wasNot called }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Login should fail with non existing user`() {
|
||||||
|
val form = LoginForm("someusername", "somepassword")
|
||||||
|
every { userRepository.find(form.username!!) } returns null
|
||||||
|
assertThat(userService.login(form), isLeftOfType<LoginError.Unregistered>())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Login should fail with wrong password`() {
|
||||||
|
val form = LoginForm("someusername", "wrongpassword")
|
||||||
|
|
||||||
|
every { userRepository.find(form.username!!) } returns
|
||||||
|
PersistedUser(form.username!!, passwordHash.crypt("right password"), 1)
|
||||||
|
|
||||||
|
assertThat(userService.login(form), isLeftOfType<LoginError.WrongPassword>())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Login should succeed with existing user and correct password`() {
|
||||||
|
val loginForm = LoginForm("someusername", "somepassword")
|
||||||
|
|
||||||
|
every { userRepository.find(loginForm.username!!) } returns
|
||||||
|
PersistedUser(loginForm.username!!, passwordHash.crypt(loginForm.password!!), 1)
|
||||||
|
|
||||||
|
val res = userService.login(loginForm)
|
||||||
|
assertThat(res, isRight())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
package be.simplenotes.domain.security
|
package be.simplenotes.domain.security
|
||||||
|
|
||||||
import be.simplenotes.config.JwtConfig
|
import be.simplenotes.config.JwtConfig
|
||||||
import be.simplenotes.domain.usecases.users.login.Token
|
import be.simplenotes.domain.Token
|
||||||
import be.simplenotes.types.LoggedInUser
|
import be.simplenotes.types.LoggedInUser
|
||||||
import com.auth0.jwt.JWT
|
import com.auth0.jwt.JWT
|
||||||
import com.auth0.jwt.algorithms.Algorithm
|
import com.auth0.jwt.algorithms.Algorithm
|
||||||
|
|||||||
@ -1,65 +0,0 @@
|
|||||||
package be.simplenotes.domain.usecases.users.login
|
|
||||||
|
|
||||||
import be.simplenotes.config.JwtConfig
|
|
||||||
import be.simplenotes.domain.security.BcryptPasswordHash
|
|
||||||
import be.simplenotes.domain.security.SimpleJwt
|
|
||||||
import be.simplenotes.domain.security.UserJwtMapper
|
|
||||||
import be.simplenotes.domain.testutils.isLeftOfType
|
|
||||||
import be.simplenotes.domain.testutils.isRight
|
|
||||||
import be.simplenotes.persistence.repositories.UserRepository
|
|
||||||
import be.simplenotes.types.PersistedUser
|
|
||||||
import com.natpryce.hamkrest.assertion.assertThat
|
|
||||||
import io.mockk.*
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
internal class LoginUseCaseImplTest {
|
|
||||||
// region setup
|
|
||||||
private val mockUserRepository = mockk<UserRepository>()
|
|
||||||
private val passwordHash = BcryptPasswordHash(test = true)
|
|
||||||
private val jwtConfig = JwtConfig("a secret", 1, TimeUnit.HOURS)
|
|
||||||
private val simpleJwt = SimpleJwt(jwtConfig, UserJwtMapper())
|
|
||||||
private val loginUseCase = LoginUseCaseImpl(mockUserRepository, passwordHash, simpleJwt)
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun resetMocks() {
|
|
||||||
clearMocks(mockUserRepository)
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Login should fail with invalid form`() {
|
|
||||||
val form = LoginForm("", "a")
|
|
||||||
assertThat(loginUseCase.login(form), isLeftOfType<InvalidLoginForm>())
|
|
||||||
verify { mockUserRepository wasNot called }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Login should fail with non existing user`() {
|
|
||||||
val form = LoginForm("someusername", "somepassword")
|
|
||||||
every { mockUserRepository.find(form.username!!) } returns null
|
|
||||||
assertThat(loginUseCase.login(form), isLeftOfType<Unregistered>())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Login should fail with wrong password`() {
|
|
||||||
val form = LoginForm("someusername", "wrongpassword")
|
|
||||||
|
|
||||||
every { mockUserRepository.find(form.username!!) } returns
|
|
||||||
PersistedUser(form.username!!, passwordHash.crypt("right password"), 1)
|
|
||||||
|
|
||||||
assertThat(loginUseCase.login(form), isLeftOfType<WrongPassword>())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Login should succeed with existing user and correct password`() {
|
|
||||||
val loginForm = LoginForm("someusername", "somepassword")
|
|
||||||
|
|
||||||
every { mockUserRepository.find(loginForm.username!!) } returns
|
|
||||||
PersistedUser(loginForm.username!!, passwordHash.crypt(loginForm.password!!), 1)
|
|
||||||
|
|
||||||
val res = loginUseCase.login(loginForm)
|
|
||||||
assertThat(res, isRight())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
package be.simplenotes.domain.usecases.users.register
|
|
||||||
|
|
||||||
import be.simplenotes.domain.security.BcryptPasswordHash
|
|
||||||
import be.simplenotes.domain.testutils.isLeftOfType
|
|
||||||
import be.simplenotes.domain.testutils.isRight
|
|
||||||
import be.simplenotes.persistence.repositories.UserRepository
|
|
||||||
import be.simplenotes.persistence.transactions.TransactionService
|
|
||||||
import be.simplenotes.types.PersistedUser
|
|
||||||
import com.natpryce.hamkrest.assertion.assertThat
|
|
||||||
import com.natpryce.hamkrest.equalTo
|
|
||||||
import io.mockk.*
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun resetMocks() {
|
|
||||||
clearMocks(mockUserRepository)
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `register should fail with invalid form`() {
|
|
||||||
val form = RegisterForm("", "a".repeat(10))
|
|
||||||
assertThat(registerUseCase.register(form), isLeftOfType<InvalidRegisterForm>())
|
|
||||||
verify { mockUserRepository wasNot called }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Register should fail with existing username`() {
|
|
||||||
val form = RegisterForm("someuser", "somepassword")
|
|
||||||
every { mockUserRepository.exists(form.username!!) } returns true
|
|
||||||
assertThat(registerUseCase.register(form), isLeftOfType<UserExists>())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Register should succeed with new user`() {
|
|
||||||
val form = RegisterForm("someuser", "somepassword")
|
|
||||||
every { mockUserRepository.exists(form.username!!) } returns false
|
|
||||||
every { mockUserRepository.create(any()) } returns PersistedUser(form.username!!, form.password!!, 1)
|
|
||||||
val res = registerUseCase.register(form)
|
|
||||||
assertThat(res, isRight())
|
|
||||||
res.map { assertThat(it.username, equalTo(form.username)) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package be.simplenotes.domain.usecases.search
|
package be.simplenotes.domain.utils
|
||||||
|
|
||||||
import be.simplenotes.search.SearchTerms
|
import be.simplenotes.search.SearchTerms
|
||||||
import com.natpryce.hamkrest.assertion.assertThat
|
import com.natpryce.hamkrest.assertion.assertThat
|
||||||
@ -1,10 +1,10 @@
|
|||||||
package be.simplenotes.domain.validation
|
package be.simplenotes.domain.validation
|
||||||
|
|
||||||
|
import be.simplenotes.domain.LoginError
|
||||||
|
import be.simplenotes.domain.LoginForm
|
||||||
|
import be.simplenotes.domain.RegisterForm
|
||||||
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.domain.usecases.users.login.InvalidLoginForm
|
|
||||||
import be.simplenotes.domain.usecases.users.login.LoginForm
|
|
||||||
import be.simplenotes.domain.usecases.users.register.RegisterForm
|
|
||||||
import com.natpryce.hamkrest.assertion.assertThat
|
import com.natpryce.hamkrest.assertion.assertThat
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
||||||
import org.junit.jupiter.params.ParameterizedTest
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
@ -28,7 +28,7 @@ internal class UserValidationsTest {
|
|||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource("invalidLoginForms")
|
@MethodSource("invalidLoginForms")
|
||||||
fun `validate invalid logins`(form: LoginForm) {
|
fun `validate invalid logins`(form: LoginForm) {
|
||||||
assertThat(UserValidations.validateLogin(form), isLeftOfType<InvalidLoginForm>())
|
assertThat(UserValidations.validateLogin(form), isLeftOfType<LoginError.InvalidLoginForm>())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("Unused")
|
@Suppress("Unused")
|
||||||
@ -59,7 +59,7 @@ internal class UserValidationsTest {
|
|||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource("invalidRegisterForms")
|
@MethodSource("invalidRegisterForms")
|
||||||
fun `validate invalid register`(form: LoginForm) {
|
fun `validate invalid register`(form: LoginForm) {
|
||||||
assertThat(UserValidations.validateLogin(form), isLeftOfType<InvalidLoginForm>())
|
assertThat(UserValidations.validateLogin(form), isLeftOfType<LoginError.InvalidLoginForm>())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("Unused")
|
@Suppress("Unused")
|
||||||
|
|||||||
@ -37,7 +37,7 @@ class BaseView(@Named("styles") styles: String) : View(styles) {
|
|||||||
attributes["aria-label"] = "demo-search"
|
attributes["aria-label"] = "demo-search"
|
||||||
attributes["name"] = "search"
|
attributes["name"] = "search"
|
||||||
attributes["disabled"] = ""
|
attributes["disabled"] = ""
|
||||||
attributes["value"] = "tag:\"demo\""
|
attributes["value"] = "tag:demo"
|
||||||
}
|
}
|
||||||
span {
|
span {
|
||||||
id = "buttons"
|
id = "buttons"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user