8 Commits

Author SHA1 Message Date
hubert 195c7c10ac Import feature 2023-05-09 22:46:07 +02:00
hubert 724fa0483e Tailwind 3 2023-05-09 22:40:39 +02:00
hubert f2bdc8d6c7 Update kotlin libs 2023-05-09 22:40:10 +02:00
hubert 5aa2e80c5f JDK 19 + Gradle 8.1.1 + kotlin 1.8.21 + spotless 2023-05-09 22:39:34 +02:00
hubert 235e8b6e3c Use gradle catalogs 2021-05-06 12:22:16 +02:00
hubert 1e0fe12396 Upgrade kotlin, kotlinx-html, gradle, shadow 2021-05-06 12:22:16 +02:00
hubert 7ad8b7039b Update config 2021-04-12 21:15:42 +02:00
hubert 204ae7988e Update ktorm, clean repositories and drop mariadb support 2021-04-12 18:35:21 +02:00
127 changed files with 1879 additions and 3058 deletions
+3 -1
View File
@@ -13,4 +13,6 @@ insert_final_newline = true
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,import-ordering ktlint_standard_no-wildcard-imports = disabled
ktlint_standard_import-ordering = disabled
ktlint_standard_multiline-if-else = disabled
-5
View File
@@ -1,6 +1 @@
# mariadb
MYSQL_ROOT_PASSWORD=
MYSQL_PASSWORD=
# simplenotes
DB_PASSWORD=
JWT_SECRET= JWT_SECRET=
+5 -8
View File
@@ -1,8 +1,8 @@
FROM openjdk:15-alpine as jdkbuilder FROM eclipse-temurin:19-alpine as jdkbuilder
RUN apk add --no-cache binutils RUN apk add --no-cache binutils
ENV MODULES java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.scripting,java.security.jgss,java.sql,java.sql.rowset,java.transaction.xa,java.xml,jdk.net ENV MODULES java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.scripting,java.security.jgss,java.sql,java.sql.rowset,java.transaction.xa,java.xml,jdk.net,jdk.zipfs
RUN jlink --output /myjdk --module-path $JAVA_HOME/jmods --add-modules $MODULES --no-header-files --no-man-pages --strip-debug --compress=2 RUN jlink --output /myjdk --module-path $JAVA_HOME/jmods --add-modules $MODULES --no-header-files --no-man-pages --strip-debug --compress=2
@@ -12,18 +12,15 @@ FROM alpine
RUN apk add --no-cache curl RUN apk add --no-cache curl
ENV APPLICATION_USER simplenotes
RUN adduser -D -g '' $APPLICATION_USER
RUN mkdir /app RUN mkdir /app
RUN chown -R $APPLICATION_USER /app RUN mkdir /app/data
USER $APPLICATION_USER
COPY --from=jdkbuilder /myjdk /myjdk COPY --from=jdkbuilder /myjdk /myjdk
COPY app/build/libs/app-with-dependencies*.jar /app/simplenotes.jar COPY app/build/libs/app-with-dependencies*.jar /app/simplenotes.jar
WORKDIR /app WORKDIR /app
VOLUME /app/data
ENV SERVER_HOST 0.0.0.0 ENV SERVER_HOST 0.0.0.0
CMD [ \ CMD [ \
+8 -11
View File
@@ -1,4 +1,3 @@
import be.simplenotes.Libs
import be.simplenotes.micronaut import be.simplenotes.micronaut
plugins { plugins {
@@ -14,20 +13,18 @@ dependencies {
implementation(project(":views")) implementation(project(":views"))
implementation(project(":css")) implementation(project(":css"))
implementation(Libs.Http4k.core) implementation(libs.http4k.core)
implementation(Libs.Jetty.server) implementation(libs.http4k.multipart)
implementation(Libs.Jetty.servlet) implementation(libs.bundles.jetty)
implementation(Libs.javaxServlet) implementation(libs.kotlinx.serialization.json)
implementation(Libs.Kotlinx.Serialization.json)
implementation(Libs.Slf4J.api) implementation(libs.slf4j.api)
runtimeOnly(Libs.Slf4J.logback) runtimeOnly(libs.slf4j.logback)
micronaut() micronaut()
testImplementation(Libs.Test.junit) testImplementation(libs.bundles.test)
testImplementation(Libs.Test.assertJ) testImplementation(libs.http4k.testing.hamkrest)
testImplementation(Libs.Http4k.testingHamkrest)
} }
docker { docker {
+3 -3
View File
@@ -1,10 +1,10 @@
package be.simplenotes.app package be.simplenotes.app
import jakarta.annotation.PostConstruct
import jakarta.annotation.PreDestroy
import jakarta.inject.Singleton
import org.http4k.server.Http4kServer import org.http4k.server.Http4kServer
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import javax.annotation.PostConstruct
import javax.annotation.PreDestroy
import javax.inject.Singleton
import be.simplenotes.config.ServerConfig as SimpleNotesServerConfig import be.simplenotes.config.ServerConfig as SimpleNotesServerConfig
@Singleton @Singleton
+5 -1
View File
@@ -4,7 +4,11 @@ import io.micronaut.context.ApplicationContext
import java.lang.Runtime.getRuntime import java.lang.Runtime.getRuntime
fun main() { fun main() {
val ctx = ApplicationContext.run() val env = if (System.getenv("ENV") == "dev") "dev" else "prod"
val ctx = ApplicationContext.builder()
.deduceEnvironment(false)
.environments(env)
.start()
ctx.createBean(Server::class.java) ctx.createBean(Server::class.java)
getRuntime().addShutdownHook(Thread { ctx.stop() }) getRuntime().addShutdownHook(Thread { ctx.stop() })
} }
+3 -3
View File
@@ -16,7 +16,7 @@ import org.http4k.core.Status.Companion.OK
import org.http4k.lens.Path import org.http4k.lens.Path
import org.http4k.lens.uuid import org.http4k.lens.uuid
import java.util.* import java.util.*
import javax.inject.Singleton import jakarta.inject.Singleton
@Singleton @Singleton
class ApiNoteController( class ApiNoteController(
@@ -28,7 +28,7 @@ class ApiNoteController(
val content = noteContentLens(request) val content = noteContentLens(request)
return noteService.create(loggedInUser, content).fold( return noteService.create(loggedInUser, content).fold(
{ Response(BAD_REQUEST) }, { Response(BAD_REQUEST) },
{ uuidContentLens(UuidContent(it.uuid), Response(OK)) } { uuidContentLens(UuidContent(it.uuid), Response(OK)) },
) )
} }
@@ -51,7 +51,7 @@ class ApiNoteController(
{ {
if (it == null) Response(NOT_FOUND) if (it == null) Response(NOT_FOUND)
else Response(OK) else Response(OK)
} },
) )
} }
+2 -2
View File
@@ -9,7 +9,7 @@ import org.http4k.core.Request
import org.http4k.core.Response import org.http4k.core.Response
import org.http4k.core.Status.Companion.BAD_REQUEST import org.http4k.core.Status.Companion.BAD_REQUEST
import org.http4k.core.Status.Companion.OK import org.http4k.core.Status.Companion.OK
import javax.inject.Singleton import jakarta.inject.Singleton
@Singleton @Singleton
class ApiUserController( class ApiUserController(
@@ -23,7 +23,7 @@ class ApiUserController(
.login(loginFormLens(request)) .login(loginFormLens(request))
.fold( .fold(
{ Response(BAD_REQUEST) }, { Response(BAD_REQUEST) },
{ tokenLens(Token(it), Response(OK)) } { tokenLens(Token(it), Response(OK)) },
) )
} }
+1 -1
View File
@@ -6,7 +6,7 @@ import be.simplenotes.views.BaseView
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
import javax.inject.Singleton import jakarta.inject.Singleton
@Singleton @Singleton
class BaseController(private val view: BaseView) { class BaseController(private val view: BaseView) {
@@ -1,14 +0,0 @@
package be.simplenotes.app.controllers
import be.simplenotes.domain.HealthCheckService
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE
import javax.inject.Singleton
@Singleton
class HealthCheckController(private val healthCheckService: HealthCheckService) {
fun healthCheck(@Suppress("UNUSED_PARAMETER") request: Request) =
if (healthCheckService.isOk()) Response(OK) else Response(SERVICE_UNAVAILABLE)
}
+9 -9
View File
@@ -15,7 +15,7 @@ import org.http4k.core.Status.Companion.OK
import org.http4k.core.body.form import org.http4k.core.body.form
import org.http4k.routing.path import org.http4k.routing.path
import java.util.* import java.util.*
import javax.inject.Singleton import jakarta.inject.Singleton
import kotlin.math.abs import kotlin.math.abs
@Singleton @Singleton
@@ -35,24 +35,24 @@ class NoteController(
MarkdownParsingError.MissingMeta -> view.noteEditor( MarkdownParsingError.MissingMeta -> view.noteEditor(
loggedInUser, loggedInUser,
error = "Missing note metadata", error = "Missing note metadata",
textarea = markdownForm textarea = markdownForm,
) )
MarkdownParsingError.InvalidMeta -> view.noteEditor( MarkdownParsingError.InvalidMeta -> view.noteEditor(
loggedInUser, loggedInUser,
error = "Invalid note metadata", error = "Invalid note metadata",
textarea = markdownForm textarea = markdownForm,
) )
is MarkdownParsingError.ValidationError -> view.noteEditor( is MarkdownParsingError.ValidationError -> view.noteEditor(
loggedInUser, loggedInUser,
validationErrors = it.validationErrors, validationErrors = it.validationErrors,
textarea = markdownForm textarea = markdownForm,
) )
} }
Response(BAD_REQUEST).html(html) Response(BAD_REQUEST).html(html)
}, },
{ {
Response.redirect("/notes/${it.uuid}") Response.redirect("/notes/${it.uuid}")
} },
) )
} }
@@ -114,24 +114,24 @@ class NoteController(
MarkdownParsingError.MissingMeta -> view.noteEditor( MarkdownParsingError.MissingMeta -> view.noteEditor(
loggedInUser, loggedInUser,
error = "Missing note metadata", error = "Missing note metadata",
textarea = markdownForm textarea = markdownForm,
) )
MarkdownParsingError.InvalidMeta -> view.noteEditor( MarkdownParsingError.InvalidMeta -> view.noteEditor(
loggedInUser, loggedInUser,
error = "Invalid note metadata", error = "Invalid note metadata",
textarea = markdownForm textarea = markdownForm,
) )
is MarkdownParsingError.ValidationError -> view.noteEditor( is MarkdownParsingError.ValidationError -> view.noteEditor(
loggedInUser, loggedInUser,
validationErrors = it.validationErrors, validationErrors = it.validationErrors,
textarea = markdownForm textarea = markdownForm,
) )
} }
Response(BAD_REQUEST).html(html) Response(BAD_REQUEST).html(html)
}, },
{ {
Response.redirect("/notes/${note.uuid}") Response.redirect("/notes/${note.uuid}")
} },
) )
} }
+18 -11
View File
@@ -2,21 +2,19 @@ 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.DeleteError import be.simplenotes.domain.*
import be.simplenotes.domain.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 jakarta.inject.Singleton
import org.http4k.core.* import org.http4k.core.*
import org.http4k.core.body.form import org.http4k.core.body.form
import org.http4k.core.cookie.invalidateCookie import org.http4k.core.cookie.invalidateCookie
import javax.inject.Singleton
@Singleton @Singleton
class SettingsController( class SettingsController(
private val userService: UserService, private val userService: UserService,
private val exportService: ExportService, private val exportService: ExportService,
private val importService: ImportService,
private val settingView: SettingView, private val settingView: SettingView,
) { ) {
fun settings(request: Request, loggedInUser: LoggedInUser): Response { fun settings(request: Request, loggedInUser: LoggedInUser): Response {
@@ -33,20 +31,21 @@ class SettingsController(
DeleteError.WrongPassword -> Response(Status.OK).html( DeleteError.WrongPassword -> Response(Status.OK).html(
settingView.settings( settingView.settings(
loggedInUser, loggedInUser,
error = "Wrong password" error = "Wrong password",
) ),
) )
is DeleteError.InvalidForm -> Response(Status.OK).html( is DeleteError.InvalidForm -> Response(Status.OK).html(
settingView.settings( settingView.settings(
loggedInUser, loggedInUser,
validationErrors = it.validationErrors validationErrors = it.validationErrors,
) ),
) )
} }
}, },
{ {
Response.redirect("/").invalidateCookie("Bearer") Response.redirect("/").invalidateCookie("Bearer")
} },
) )
} }
@@ -73,10 +72,18 @@ class SettingsController(
.body(exportService.exportAsJson(loggedInUser.userId)) .body(exportService.exportAsJson(loggedInUser.userId))
} else Response(Status.OK).body(exportService.exportAsJson(loggedInUser.userId)).header( } else Response(Status.OK).body(exportService.exportAsJson(loggedInUser.userId)).header(
"Content-Type", "Content-Type",
"application/json" "application/json",
) )
} }
private fun Request.deleteForm(loggedInUser: LoggedInUser) = private fun Request.deleteForm(loggedInUser: LoggedInUser) =
DeleteForm(loggedInUser.username, form("password"), form("checked") != null) DeleteForm(loggedInUser.username, form("password"), form("checked") != null)
fun import(request: Request, loggedInUser: LoggedInUser): Response {
val form = MultipartFormBody.from(request)
val file = form.file("file") ?: return Response(Status.BAD_REQUEST)
val json = file.content.bufferedReader().readText()
importService.importJson(loggedInUser, json)
return Response.redirect("/notes")
}
} }
+12 -12
View File
@@ -17,7 +17,7 @@ import org.http4k.core.cookie.SameSite
import org.http4k.core.cookie.cookie import org.http4k.core.cookie.cookie
import org.http4k.core.cookie.invalidateCookie import org.http4k.core.cookie.invalidateCookie
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Singleton import jakarta.inject.Singleton
@Singleton @Singleton
class UserController( class UserController(
@@ -27,7 +27,7 @@ class UserController(
) { ) {
fun register(request: Request, loggedInUser: LoggedInUser?): Response { fun register(request: Request, loggedInUser: LoggedInUser?): Response {
if (request.method == GET) return Response(OK).html( if (request.method == GET) return Response(OK).html(
userView.register(loggedInUser) userView.register(loggedInUser),
) )
val result = userService.register(request.registerForm()) val result = userService.register(request.registerForm())
@@ -37,19 +37,19 @@ class UserController(
val html = when (it) { val html = when (it) {
RegisterError.UserExists -> userView.register( RegisterError.UserExists -> userView.register(
loggedInUser, loggedInUser,
error = "User already exists" error = "User already exists",
) )
is RegisterError.InvalidRegisterForm -> is RegisterError.InvalidRegisterForm ->
userView.register( userView.register(
loggedInUser, loggedInUser,
validationErrors = it.validationErrors validationErrors = it.validationErrors,
) )
} }
Response(OK).html(html) Response(OK).html(html)
}, },
{ {
Response.redirect("/login") Response.redirect("/login")
} },
) )
} }
@@ -58,7 +58,7 @@ class UserController(
fun login(request: Request, loggedInUser: LoggedInUser?): Response { fun login(request: Request, loggedInUser: LoggedInUser?): Response {
if (request.method == GET) return Response(OK).html( if (request.method == GET) return Response(OK).html(
userView.login(loggedInUser) userView.login(loggedInUser),
) )
val result = userService.login(request.loginForm()) val result = userService.login(request.loginForm())
@@ -69,24 +69,24 @@ class UserController(
LoginError.Unregistered -> LoginError.Unregistered ->
userView.login( userView.login(
loggedInUser, loggedInUser,
error = "User does not exist" error = "User does not exist",
) )
LoginError.WrongPassword -> LoginError.WrongPassword ->
userView.login( userView.login(
loggedInUser, loggedInUser,
error = "Wrong password" error = "Wrong password",
) )
is LoginError.InvalidLoginForm -> is LoginError.InvalidLoginForm ->
userView.login( userView.login(
loggedInUser, loggedInUser,
validationErrors = it.validationErrors validationErrors = it.validationErrors,
) )
} }
Response(OK).html(html) Response(OK).html(html)
}, },
{ token -> { token ->
Response.redirect("/notes").loginCookie(token, request.isSecure()) Response.redirect("/notes").loginCookie(token, request.isSecure())
} },
) )
} }
@@ -101,8 +101,8 @@ class UserController(
httpOnly = true, httpOnly = true,
sameSite = SameSite.Lax, sameSite = SameSite.Lax,
maxAge = validityInSeconds, maxAge = validityInSeconds,
secure = secure secure = secure,
) ),
) )
} }
+3 -3
View File
@@ -24,13 +24,13 @@ fun Request.isSecure() = header("X-Forwarded-Proto")?.contains("https") ?: false
val bodyLens = httpBodyRoot( val bodyLens = httpBodyRoot(
listOf(Meta(true, "body", ParamMeta.ObjectParam, "body")), listOf(Meta(true, "body", ParamMeta.ObjectParam, "body")),
ContentType.APPLICATION_JSON.withNoDirectives(), ContentType.APPLICATION_JSON.withNoDirectives(),
ContentNegotiation.StrictNoDirective ContentNegotiation.StrictNoDirective,
).map( ).map(
{ it.payload.asString() }, { it.payload.asString() },
{ Body(it) } { Body(it) },
) )
inline fun <reified T> Json.auto(): BiDiBodyLensSpec<T> = bodyLens.map( inline fun <reified T> Json.auto(): BiDiBodyLensSpec<T> = bodyLens.map(
{ decodeFromString(it) }, { decodeFromString(it) },
{ encodeToString(it) } { encodeToString(it) },
) )
+1 -1
View File
@@ -10,7 +10,7 @@ import org.http4k.core.Status.Companion.NOT_IMPLEMENTED
import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.sql.SQLTransientException import java.sql.SQLTransientException
import javax.inject.Singleton import jakarta.inject.Singleton
@Singleton @Singleton
class ErrorFilter(private val errorView: ErrorView) : Filter { class ErrorFilter(private val errorView: ErrorView) : Filter {
+2 -2
View File
@@ -8,14 +8,14 @@ import org.eclipse.jetty.servlet.ServletHolder
import org.http4k.core.HttpHandler import org.http4k.core.HttpHandler
import org.http4k.server.Http4kServer import org.http4k.server.Http4kServer
import org.http4k.server.ServerConfig import org.http4k.server.ServerConfig
import org.http4k.servlet.asServlet import org.http4k.servlet.jakarta.asServlet
class Jetty(private val port: Int, private val server: Server) : ServerConfig { class Jetty(private val port: Int, private val server: Server) : ServerConfig {
constructor(port: Int, vararg inConnectors: ConnectorBuilder) : this( constructor(port: Int, vararg inConnectors: ConnectorBuilder) : this(
port, port,
Server().apply { Server().apply {
inConnectors.forEach { addConnector(it(this)) } inConnectors.forEach { addConnector(it(this)) }
} },
) )
override fun toServer(http: HttpHandler): Http4kServer { override fun toServer(http: HttpHandler): Http4kServer {
+3 -3
View File
@@ -7,8 +7,8 @@ import io.micronaut.context.annotation.Factory
import io.micronaut.context.annotation.Primary import io.micronaut.context.annotation.Primary
import org.http4k.core.RequestContexts import org.http4k.core.RequestContexts
import org.http4k.lens.RequestContextKey import org.http4k.lens.RequestContextKey
import javax.inject.Named import jakarta.inject.Named
import javax.inject.Singleton import jakarta.inject.Singleton
@Factory @Factory
class AuthModule { class AuthModule {
@@ -39,7 +39,7 @@ class AuthModule {
simpleJwt = simpleJwt, simpleJwt = simpleJwt,
lens = lens, lens = lens,
source = JwtSource.Header, source = JwtSource.Header,
redirect = false redirect = false,
) )
@Singleton @Singleton
+1 -1
View File
@@ -7,7 +7,7 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.SerializersModule
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.* import java.util.*
import javax.inject.Singleton import jakarta.inject.Singleton
@Factory @Factory
class JsonModule { class JsonModule {
+2 -2
View File
@@ -9,8 +9,8 @@ import io.micronaut.context.annotation.Factory
import org.eclipse.jetty.server.ServerConnector import org.eclipse.jetty.server.ServerConnector
import org.http4k.server.Http4kServer import org.http4k.server.Http4kServer
import org.http4k.server.asServer import org.http4k.server.asServer
import javax.inject.Named import jakarta.inject.Named
import javax.inject.Singleton import jakarta.inject.Singleton
import org.eclipse.jetty.server.Server as JettyServer import org.eclipse.jetty.server.Server as JettyServer
import org.http4k.server.ServerConfig as Http4kServerConfig import org.http4k.server.ServerConfig as Http4kServerConfig
+4 -5
View File
@@ -12,8 +12,8 @@ import org.http4k.routing.RoutingHttpHandler
import org.http4k.routing.bind import org.http4k.routing.bind
import org.http4k.routing.routes import org.http4k.routing.routes
import java.util.function.Supplier import java.util.function.Supplier
import javax.inject.Named import jakarta.inject.Named
import javax.inject.Singleton import jakarta.inject.Singleton
@Singleton @Singleton
class ApiRoutes( class ApiRoutes(
@@ -23,7 +23,6 @@ class ApiRoutes(
@Named("required") private val authLens: RequiredAuthLens, @Named("required") private val authLens: RequiredAuthLens,
) : Supplier<RoutingHttpHandler> { ) : Supplier<RoutingHttpHandler> {
override fun get(): RoutingHttpHandler { override fun get(): RoutingHttpHandler {
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)) }
@@ -38,9 +37,9 @@ class ApiRoutes(
"/search" bind POST to ::search, "/search" bind POST to ::search,
"/{uuid}" bind GET to ::note, "/{uuid}" bind GET to ::note,
"/{uuid}" bind PUT to ::update, "/{uuid}" bind PUT to ::update,
) ),
).withBasePath("/notes") ).withBasePath("/notes")
} },
).withBasePath("/api") ).withBasePath("/api")
} }
+6 -11
View File
@@ -1,7 +1,6 @@
package be.simplenotes.app.routes package be.simplenotes.app.routes
import be.simplenotes.app.controllers.BaseController import be.simplenotes.app.controllers.BaseController
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
@@ -14,12 +13,11 @@ import org.http4k.core.Request
import org.http4k.core.then import org.http4k.core.then
import org.http4k.routing.* import org.http4k.routing.*
import java.util.function.Supplier import java.util.function.Supplier
import javax.inject.Named import jakarta.inject.Named
import javax.inject.Singleton import jakarta.inject.Singleton
@Singleton @Singleton
class BasicRoutes( class BasicRoutes(
private val healthCheckController: HealthCheckController,
private val baseCtrl: BaseController, private val baseCtrl: BaseController,
private val userCtrl: UserController, private val userCtrl: UserController,
private val noteCtrl: NoteController, private val noteCtrl: NoteController,
@@ -28,7 +26,6 @@ class BasicRoutes(
) : Supplier<RoutingHttpHandler> { ) : Supplier<RoutingHttpHandler> {
override fun get(): RoutingHttpHandler { override fun get(): RoutingHttpHandler {
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)) }
@@ -36,8 +33,8 @@ class BasicRoutes(
static( static(
ResourceLoader.Classpath("/static"), ResourceLoader.Classpath("/static"),
"woff2" to ContentType("font/woff2"), "woff2" to ContentType("font/woff2"),
"webmanifest" to ContentType("application/manifest+json") "webmanifest" to ContentType("application/manifest+json"),
) ),
) )
return routes( return routes(
@@ -50,11 +47,9 @@ class BasicRoutes(
"/login" bind POST to userCtrl::login, "/login" bind POST to userCtrl::login,
"/logout" bind POST to userCtrl::logout, "/logout" bind POST to userCtrl::logout,
"/notes/public/{uuid}" bind GET to noteCtrl::public, "/notes/public/{uuid}" bind GET to noteCtrl::public,
) ),
), ),
staticHandler,
"/health" bind GET to healthCheckController::healthCheck,
staticHandler
) )
} }
} }
+3 -4
View File
@@ -12,8 +12,8 @@ import org.http4k.routing.RoutingHttpHandler
import org.http4k.routing.bind import org.http4k.routing.bind
import org.http4k.routing.routes import org.http4k.routing.routes
import java.util.function.Supplier import java.util.function.Supplier
import javax.inject.Named import jakarta.inject.Named
import javax.inject.Singleton import jakarta.inject.Singleton
@Singleton @Singleton
class NoteRoutes( class NoteRoutes(
@@ -22,7 +22,6 @@ class NoteRoutes(
@Named("required") private val authLens: RequiredAuthLens, @Named("required") private val authLens: RequiredAuthLens,
) : Supplier<RoutingHttpHandler> { ) : Supplier<RoutingHttpHandler> {
override fun get(): RoutingHttpHandler { override fun get(): RoutingHttpHandler {
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)) }
@@ -40,7 +39,7 @@ class NoteRoutes(
"/{uuid}/edit" bind POST to ::edit, "/{uuid}/edit" bind POST to ::edit,
"/deleted/{uuid}" bind POST to ::deleted, "/deleted/{uuid}" bind POST to ::deleted,
).withBasePath("/notes") ).withBasePath("/notes")
} },
) )
} }
} }
+2 -3
View File
@@ -9,7 +9,7 @@ import org.http4k.filter.ServerFilters.InitialiseRequestContext
import org.http4k.routing.RoutingHttpHandler import org.http4k.routing.RoutingHttpHandler
import org.http4k.routing.routes import org.http4k.routing.routes
import java.util.function.Supplier import java.util.function.Supplier
import javax.inject.Singleton import jakarta.inject.Singleton
@Singleton @Singleton
class Router( class Router(
@@ -18,9 +18,8 @@ class Router(
private val subRouters: List<Supplier<RoutingHttpHandler>>, private val subRouters: List<Supplier<RoutingHttpHandler>>,
) { ) {
operator fun invoke(): RoutingHttpHandler { operator fun invoke(): RoutingHttpHandler {
val routes = routes( val routes = routes(
*subRouters.map { it.get() }.toTypedArray() *subRouters.map { it.get() }.toTypedArray(),
) )
return errorFilter return errorFilter
+4 -4
View File
@@ -12,8 +12,8 @@ import org.http4k.routing.RoutingHttpHandler
import org.http4k.routing.bind import org.http4k.routing.bind
import org.http4k.routing.routes import org.http4k.routing.routes
import java.util.function.Supplier import java.util.function.Supplier
import javax.inject.Named import jakarta.inject.Named
import javax.inject.Singleton import jakarta.inject.Singleton
@Singleton @Singleton
class SettingsRoutes( class SettingsRoutes(
@@ -22,7 +22,6 @@ class SettingsRoutes(
@Named("required") private val authLens: RequiredAuthLens, @Named("required") private val authLens: RequiredAuthLens,
) : Supplier<RoutingHttpHandler> { ) : Supplier<RoutingHttpHandler> {
override fun get(): RoutingHttpHandler { override fun get(): RoutingHttpHandler {
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)) }
@@ -31,7 +30,8 @@ class SettingsRoutes(
"/settings" bind GET to settingsController::settings, "/settings" bind GET to settingsController::settings,
"/settings" bind POST to settingsController::settings, "/settings" bind POST to settingsController::settings,
"/export" bind POST to settingsController::export, "/export" bind POST to settingsController::export,
) "/import" bind POST to settingsController::import,
),
) )
} }
} }
@@ -17,6 +17,6 @@ internal class LocalDateTimeSerializer : KSerializer<LocalDateTime> {
} }
override fun deserialize(decoder: Decoder): LocalDateTime { override fun deserialize(decoder: Decoder): LocalDateTime {
TODO("Not implemented, isn't needed") return LocalDateTime.parse(decoder.decodeString())
} }
} }
+1 -1
View File
@@ -3,7 +3,7 @@ package be.simplenotes.app.utils
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import javax.inject.Singleton import jakarta.inject.Singleton
interface StaticFileResolver { interface StaticFileResolver {
fun resolve(name: String): String? fun resolve(name: String): String?
+2 -2
View File
@@ -58,8 +58,8 @@ internal class RequiredAuthFilterTest {
}, },
"/protected" bind GET to requiredAuth.then { request: Request -> "/protected" bind GET to requiredAuth.then { request: Request ->
Response(OK).body(requiredLens(request).toString()) Response(OK).body(requiredLens(request).toString())
} },
) ),
) )
// endregion // endregion
+5 -9
View File
@@ -2,18 +2,14 @@ plugins {
`kotlin-dsl` `kotlin-dsl`
} }
kotlinDslPluginOptions {
experimentalWarning.set(false)
}
repositories { repositories {
gradlePluginPortal() gradlePluginPortal()
} }
dependencies { dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.31") implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21")
implementation("org.jetbrains.kotlin:kotlin-serialization:1.4.31") implementation("org.jetbrains.kotlin:kotlin-serialization:1.8.21")
implementation("com.github.jengelman.gradle.plugins:shadow:6.1.0") implementation("com.github.johnrengelman:shadow:8.1.1")
implementation("org.jlleitschuh.gradle:ktlint-gradle:10.0.0") implementation("com.diffplug.spotless:spotless-plugin-gradle:6.13.0")
implementation("com.github.ben-manes:gradle-versions-plugin:0.28.0") implementation("com.github.ben-manes:gradle-versions-plugin:0.46.0")
} }
@@ -4,89 +4,10 @@ package be.simplenotes
object Libs { object Libs {
object Flexmark {
private const val version = "0.62.2"
const val core = "com.vladsch.flexmark:flexmark:$version"
const val tasklist = "com.vladsch.flexmark:flexmark-ext-gfm-tasklist:$version"
}
object Database {
const val flyway = "org.flywaydb:flyway-core:7.5.4"
const val hikariCP = "com.zaxxer:HikariCP:4.0.2"
object Drivers {
const val h2 = "com.h2database:h2:1.4.200"
const val mariadb = "org.mariadb.jdbc:mariadb-java-client:2.7.2"
}
object Ktorm {
private const val version = "3.0.0"
const val core = "me.liuwj.ktorm:ktorm-core:$version"
const val mysql = "me.liuwj.ktorm:ktorm-support-mysql:$version"
}
}
object Lucene {
private const val version = "8.8.1"
const val core = "org.apache.lucene:lucene-core:$version"
const val analyzersCommon = "org.apache.lucene:lucene-analyzers-common:$version"
const val queryParser = "org.apache.lucene:lucene-queryparser:$version"
}
object Http4k {
private const val version = "4.3.5.4"
const val core = "org.http4k:http4k-core:$version"
const val testingHamkrest = "org.http4k:http4k-testing-hamkrest:$version"
}
object Jetty {
private const val version = "10.0.1"
const val server = "org.eclipse.jetty:jetty-server:$version"
const val servlet = "org.eclipse.jetty:jetty-servlet:$version"
}
object Kotlinx {
const val html = "org.jetbrains.kotlinx:kotlinx-html-jvm:0.7.2"
object Serialization {
const val json = "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.1.0"
}
}
object Slf4J {
const val api = "org.slf4j:slf4j-api:2.0.0-alpha1"
const val logback = "ch.qos.logback:logback-classic:1.3.0-alpha5"
}
object Mapstruct {
private const val version = "1.4.2.Final"
const val core = "org.mapstruct:mapstruct:$version"
const val processor = "org.mapstruct:mapstruct-processor:$version"
}
object Micronaut { object Micronaut {
private const val version = "2.3.3" private const val version = "4.0.0-M2"
const val inject = "io.micronaut:micronaut-inject:$version" const val inject = "io.micronaut:micronaut-inject:$version"
const val processor = "io.micronaut:micronaut-inject-java:$version" const val processor = "io.micronaut:micronaut-inject-java:$version"
} }
const val arrowCoreData = "io.arrow-kt:arrow-core-data:0.11.0"
const val commonsCompress = "org.apache.commons:commons-compress:1.20"
const val javaJwt = "com.auth0:java-jwt:3.13.0"
const val javaxServlet = "javax.servlet:javax.servlet-api:4.0.1"
const val jbcrypt = "org.mindrot:jbcrypt:0.4"
const val konform = "io.konform:konform-jvm:0.2.0"
const val owaspHtmlSanitizer = "com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20200713.1"
const val prettytime = "org.ocpsoft.prettytime:prettytime:5.0.0.Final"
const val snakeyaml = "org.yaml:snakeyaml:1.28"
object Test {
const val assertJ = "org.assertj:assertj-core:3.19.0"
const val hamkrest = "com.natpryce:hamkrest:1.8.0.1"
const val junit = "org.junit.jupiter:junit-jupiter:5.7.1"
const val mockk = "io.mockk:mockk:1.10.6"
const val faker = "com.github.javafaker:javafaker:1.0.2"
const val mariaTestContainer = "org.testcontainers:mariadb:1.15.2"
}
} }
@@ -2,8 +2,10 @@ package be.simplenotes
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.plugins.JavaPluginConvention import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.tasks.SourceSetContainer
import org.gradle.kotlin.dsl.get import org.gradle.kotlin.dsl.get
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.register import org.gradle.kotlin.dsl.register
import java.io.File import java.io.File
@@ -17,7 +19,7 @@ class PostcssPlugin : Plugin<Project> {
getByName("processResources").dependsOn("postcss") getByName("processResources").dependsOn("postcss")
} }
val sourceSets = project.convention.getPlugin(JavaPluginConvention::class.java).sourceSets val sourceSets = project.extensions.getByType<SourceSetContainer>()
val root = File("${project.buildDir}/generated-resources/css") val root = File("${project.buildDir}/generated-resources/css")
sourceSets["main"].resources.srcDir(root) sourceSets["main"].resources.srcDir(root)
} }
@@ -11,6 +11,10 @@ tasks.withType<ShadowJar> {
archiveAppendix.set("with-dependencies") archiveAppendix.set("with-dependencies")
manifest.attributes["Main-Class"] = "be.simplenotes.app.SimpleNotesKt" manifest.attributes["Main-Class"] = "be.simplenotes.app.SimpleNotesKt"
// johnrengelman/shadow#449
// we need this for lucene-core
manifest.attributes["Multi-Release"] = "true"
mergeServiceFiles() mergeServiceFiles()
File(rootProject.projectDir, "buildSrc/src/main/resources/exclusions") File(rootProject.projectDir, "buildSrc/src/main/resources/exclusions")
@@ -4,5 +4,11 @@ plugins {
id("be.simplenotes.java-convention") id("be.simplenotes.java-convention")
id("be.simplenotes.kotlin-convention") id("be.simplenotes.kotlin-convention")
id("be.simplenotes.junit-convention") id("be.simplenotes.junit-convention")
id("org.jlleitschuh.gradle.ktlint") id("com.diffplug.spotless")
}
spotless {
kotlin {
ktlint("0.48.0").setEditorConfigPath(project.rootProject.file(".editorconfig"))
}
} }
@@ -6,19 +6,16 @@ plugins {
repositories { repositories {
mavenCentral() mavenCentral()
maven {
url = uri("https://kotlin.bintray.com/kotlinx")
// https://github.com/Kotlin/kotlinx.html/issues/173
content { includeModule("org.jetbrains.kotlinx", "kotlinx-html-jvm") }
}
} }
group = "be.simplenotes" group = "be.simplenotes"
version = "1.0-SNAPSHOT" version = "1.0-SNAPSHOT"
java { java {
sourceCompatibility = JavaVersion.VERSION_15 toolchain {
targetCompatibility = JavaVersion.VERSION_15 languageVersion.set(JavaLanguageVersion.of(19))
vendor.set(JvmVendorSpec.ORACLE)
}
} }
tasks.withType<JavaCompile> { tasks.withType<JavaCompile> {
@@ -1,5 +1,7 @@
package be.simplenotes package be.simplenotes
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
@@ -13,14 +15,14 @@ dependencies {
} }
tasks.withType<KotlinCompile> { tasks.withType<KotlinCompile> {
kotlinOptions { compilerOptions {
jvmTarget = "15" jvmTarget.set(JvmTarget.JVM_19)
javaParameters = true javaParameters.set(true)
freeCompilerArgs = listOf( freeCompilerArgs.addAll(
"-Xinline-classes", "-Xinline-classes",
"-Xno-param-assertions", "-Xno-param-assertions",
"-Xno-call-assertions", "-Xno-call-assertions",
"-Xno-receiver-assertions" "-Xno-receiver-assertions",
) )
} }
} }
@@ -10,12 +10,7 @@ tasks.named<DependencyUpdatesTask>("dependencyUpdates").configure {
resolutionStrategy { resolutionStrategy {
componentSelection { componentSelection {
all { all {
if ("RC" in candidate.version) reject("Release candidate") if ("alpha|beta|rc".toRegex().containsMatchIn(candidate.version.lowercase())) reject("Non stable version")
when {
candidate.group == "org.eclipse.jetty" && candidate.version.startsWith("11.") -> reject("javax -> jakarta")
candidate.group == "me.liuwj.ktorm" && candidate.version != "3.0.0" -> reject("SQL Case sensitivity changed")
}
} }
} }
} }
@@ -1,5 +1,6 @@
META-INF/maven/** META-INF/maven/**
META-INF/proguard/** META-INF/proguard/**
META-INF/com.android.tools/**
META-INF/*.kotlin_module META-INF/*.kotlin_module
META-INF/DEPENDENCIES* META-INF/DEPENDENCIES*
META-INF/NOTICE* META-INF/NOTICE*
@@ -7,6 +8,7 @@ META-INF/LICENSE*
LICENSE* LICENSE*
META-INF/README* META-INF/README*
META-INF/native-image/** META-INF/native-image/**
**/module-info.**
# Jetty # Jetty
about.html about.html
+3 -4
View File
@@ -1,4 +1,3 @@
import be.simplenotes.Libs
import be.simplenotes.micronaut import be.simplenotes.micronaut
plugins { plugins {
@@ -8,8 +7,8 @@ plugins {
dependencies { dependencies {
micronaut() micronaut()
runtimeOnly(libs.yaml)
testImplementation(Libs.Test.junit) testImplementation(libs.bundles.test)
testImplementation(Libs.Test.assertJ) testRuntimeOnly(libs.slf4j.logback)
testRuntimeOnly(Libs.Slf4J.logback)
} }
+2 -7
View File
@@ -1,10 +1,3 @@
db:
jdbc-url: jdbc:h2:./notes-db;
username: h2
password: ''
connection-timeout: 3000
maximum-pool-size: 10
jwt: jwt:
secret: 'PliLvfk7l4WF+cZJk66LR5Mpnh+ocbvJ2wfUCK2UCms=' secret: 'PliLvfk7l4WF+cZJk66LR5Mpnh+ocbvJ2wfUCK2UCms='
validity: 24 validity: 24
@@ -13,3 +6,5 @@ jwt:
server: server:
host: localhost host: localhost
port: 8080 port: 8080
data-dir: ./data
-32
View File
@@ -1,32 +0,0 @@
package be.simplenotes.config
import io.micronaut.context.annotation.ConfigurationInject
import io.micronaut.context.annotation.ConfigurationProperties
import java.util.concurrent.TimeUnit
@ConfigurationProperties("db")
data class DataSourceConfig @ConfigurationInject constructor(
val jdbcUrl: String,
val username: String,
val password: String,
val maximumPoolSize: Int,
val connectionTimeout: Long,
) {
override fun toString() = "DataSourceConfig(jdbcUrl='$jdbcUrl', username='$username', password='***', " +
"maximumPoolSize=$maximumPoolSize, connectionTimeout=$connectionTimeout)"
}
@ConfigurationProperties("jwt")
data class JwtConfig @ConfigurationInject constructor(
val secret: String,
val validity: Long,
val timeUnit: TimeUnit,
) {
override fun toString() = "JwtConfig(secret='***', validity=$validity, timeUnit=$timeUnit)"
}
@ConfigurationProperties("server")
data class ServerConfig @ConfigurationInject constructor(
val host: String,
val port: Int,
)
+57
View File
@@ -0,0 +1,57 @@
package be.simplenotes.config
import io.micronaut.context.annotation.*
import java.nio.file.Path
import java.util.concurrent.TimeUnit
import jakarta.inject.Singleton
data class DataConfig(val dataDir: String)
data class DataSourceConfig(
val jdbcUrl: String,
val maximumPoolSize: Int,
val connectionTimeout: Long,
)
@ConfigurationProperties("jwt")
data class JwtConfig @ConfigurationInject constructor(
val secret: String,
val validity: Long,
val timeUnit: TimeUnit,
) {
override fun toString() = "JwtConfig(secret='***', validity=$validity, timeUnit=$timeUnit)"
}
@ConfigurationProperties("server")
data class ServerConfig @ConfigurationInject constructor(
val host: String,
val port: Int,
)
@Factory
class ConfigFactory {
@Singleton
@Requires(notEnv = ["test"])
fun datasourceConfig(dataConfig: DataConfig) = DataSourceConfig(
jdbcUrl = "jdbc:h2:" + Path.of(dataConfig.dataDir, "simplenotes").normalize().toAbsolutePath(),
maximumPoolSize = 10,
connectionTimeout = 1000,
)
@Singleton
@Requires(env = ["test"])
fun testDatasourceConfig() = DataSourceConfig(
jdbcUrl = "jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1",
maximumPoolSize = 2,
connectionTimeout = 1000,
)
@Singleton
@Requires(notEnv = ["test"])
fun dataConfig(@Property(name = "data-dir") dataDir: String) = DataConfig(dataDir)
@Singleton
@Requires(env = ["test"])
fun testDataConfig() = DataConfig("/tmp")
}
+6 -7
View File
@@ -5,12 +5,11 @@
"//": "`gradle css`" "//": "`gradle css`"
}, },
"dependencies": { "dependencies": {
"autoprefixer": "^9.8.6", "autoprefixer": "^10.4.14",
"cssnano": "^4.1.10", "cssnano": "^6.0.1",
"postcss-cli": "^7.1.1", "postcss-cli": "^10.1.0",
"postcss-hash": "^2.0.0", "postcss-hash": "^3.0.0",
"postcss-import": "^12.0.1", "postcss-import": "^15.1.0",
"postcss-nested": "^4.2.3", "tailwindcss": "^3.3.2"
"tailwindcss": "^1.5.1"
} }
} }
+1 -1
View File
@@ -1,7 +1,7 @@
module.exports = { module.exports = {
plugins: [ plugins: [
require('postcss-import'), require('postcss-import'),
require('postcss-nested'), require('tailwindcss/nesting'),
require('tailwindcss'), require('tailwindcss'),
require('autoprefixer'), require('autoprefixer'),
require('cssnano')({ require('cssnano')({
+1 -1
View File
@@ -2,7 +2,7 @@
@apply font-semibold py-2 px-4 rounded; @apply font-semibold py-2 px-4 rounded;
&:focus { &:focus {
@apply outline-none shadow-outline; @apply outline-none ring;
} }
&:hover { &:hover {
+1 -1
View File
@@ -1,6 +1,6 @@
#note { #note {
a { a {
@apply text-blue-700 underline; @apply text-blue-500 underline;
} }
p { p {
+1 -1
View File
@@ -2,7 +2,7 @@
@apply italic font-semibold text-sm bg-teal-500 text-gray-900 rounded-full py-1 px-2 align-middle; @apply italic font-semibold text-sm bg-teal-500 text-gray-900 rounded-full py-1 px-2 align-middle;
&:focus { &:focus {
@apply outline-none shadow-outline bg-teal-800 text-white; @apply outline-none ring bg-teal-800 text-white;
} }
&:hover { &:hover {
+3 -5
View File
@@ -1,9 +1,7 @@
module.exports = { module.exports = {
purge: { content: [
content: [ process.env.PURGE
process.env.PURGE ],
]
},
theme: { theme: {
fontFamily: { fontFamily: {
'sans': [ 'sans': [
+822 -1433
View File
File diff suppressed because it is too large Load Diff
+4 -39
View File
@@ -1,54 +1,19 @@
version: '2.2' version: '2.2'
services: services:
db:
image: mariadb:10.5.5
container_name: simplenotes-mariadb
env_file:
- .env
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Brussels
- MYSQL_DATABASE=simplenotes
- MYSQL_USER=simplenotes
# .env:
# - MYSQL_ROOT_PASSWORD
# - MYSQL_PASSWORD
volumes:
- notes-db-volume:/var/lib/mysql
healthcheck:
test: "mysql --protocol=tcp -u simplenotes -p$MYSQL_PASSWORD -e 'show databases'"
interval: 5s
timeout: 1s
start_period: 2s
retries: 10
simplenotes: simplenotes:
image: hubv/simplenotes image: hubv/simplenotes
container_name: simplenotes container_name: simplenotes
env_file: env_file:
- .env - .env
environment: environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Brussels - TZ=Europe/Brussels
- DB_JDBC_URL=jdbc:mariadb://db:3306/simplenotes
- DB_USERNAME=simplenotes
# .env: # .env:
# - JWT_SECRET # - JWT_SECRET
# - DB_PASSWORD
ports: ports:
- 127.0.0.1:8080:8080 - 127.0.0.1:8080:8080
healthcheck: volumes:
test: "curl --fail -s http://localhost:8080/health" - ./simplenotes-data:/app/data
interval: 5s
timeout: 1s
start_period: 2s
retries: 3
depends_on:
db:
condition: service_healthy
volumes:
notes-db-volume:
+10 -14
View File
@@ -1,4 +1,3 @@
import be.simplenotes.Libs
import be.simplenotes.micronaut import be.simplenotes.micronaut
plugins { plugins {
@@ -13,21 +12,18 @@ dependencies {
implementation(project(":persistence")) implementation(project(":persistence"))
implementation(project(":search")) implementation(project(":search"))
api(Libs.arrowCoreData) api(libs.arrow.core)
api(Libs.konform) api(libs.konform)
micronaut() micronaut()
implementation(Libs.Kotlinx.Serialization.json) implementation(libs.kotlinx.serialization.json)
implementation(Libs.jbcrypt) implementation(libs.bcrypt)
implementation(Libs.javaJwt) implementation(libs.jwt)
implementation(Libs.Flexmark.core) implementation(libs.bundles.flexmark)
implementation(Libs.Flexmark.tasklist) implementation(libs.yaml)
implementation(Libs.snakeyaml) implementation(libs.owasp.html.sanitizer)
implementation(Libs.owaspHtmlSanitizer) implementation(libs.commonsCompress)
implementation(Libs.commonsCompress)
testImplementation(Libs.Test.hamkrest) testImplementation(libs.bundles.test)
testImplementation(Libs.Test.junit)
testImplementation(Libs.Test.mockk)
} }
+1 -1
View File
@@ -9,7 +9,7 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
import javax.inject.Singleton import jakarta.inject.Singleton
interface ExportService { interface ExportService {
fun exportAsJson(userId: Int): String fun exportAsJson(userId: Int): String
-13
View File
@@ -1,13 +0,0 @@
package be.simplenotes.domain
import be.simplenotes.persistence.DbHealthCheck
import javax.inject.Singleton
interface HealthCheckService {
fun isOk(): Boolean
}
@Singleton
class HealthCheckServiceImpl(private val dbHealthCheck: DbHealthCheck) : HealthCheckService {
override fun isOk() = dbHealthCheck.isOk()
}
+30
View File
@@ -0,0 +1,30 @@
package be.simplenotes.domain
import be.simplenotes.domain.security.HtmlSanitizer
import be.simplenotes.persistence.repositories.NoteRepository
import be.simplenotes.types.ExportedNote
import be.simplenotes.types.LoggedInUser
import jakarta.inject.Singleton
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json
interface ImportService {
fun importJson(user: LoggedInUser, content: String)
}
@Singleton
internal class ImportServiceImpl(
private val noteRepository: NoteRepository,
private val json: Json,
private val htmlSanitizer: HtmlSanitizer,
) : ImportService {
override fun importJson(user: LoggedInUser, content: String) {
val notes = json.decodeFromString(ListSerializer(ExportedNote.serializer()), content)
noteRepository.import(
user.userId,
notes.map {
it.copy(html = htmlSanitizer.sanitize(user, it.html))
},
)
}
}
+15 -18
View File
@@ -1,50 +1,47 @@
package be.simplenotes.domain package be.simplenotes.domain
import arrow.core.Either import arrow.core.Either
import arrow.core.computations.either import arrow.core.raise.either
import arrow.core.left import arrow.core.raise.ensure
import arrow.core.right
import be.simplenotes.domain.validation.NoteValidations 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 io.konform.validation.ValidationErrors
import jakarta.inject.Singleton
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
interface MarkdownService { interface MarkdownService {
fun renderDocument(input: String): Either<MarkdownParsingError, Document> fun renderDocument(input: String): Either<MarkdownParsingError, Document>
} }
private typealias MetaMdPair = Pair<String, String>
@Singleton @Singleton
internal class MarkdownServiceImpl( internal class MarkdownServiceImpl(
private val parser: Parser, private val parser: Parser,
private val renderer: HtmlRenderer, private val renderer: HtmlRenderer,
) : MarkdownService { ) : MarkdownService {
private val yamlBoundPattern = "-{3}".toRegex() private val yamlBoundPattern = "-{3}".toRegex()
private fun splitMetaFromDocument(input: String): Either<MarkdownParsingError.MissingMeta, MetaMdPair> { private fun splitMetaFromDocument(input: String) = either {
val split = input.split(yamlBoundPattern, 3) val split = input.split(yamlBoundPattern, 3)
if (split.size < 3) return MarkdownParsingError.MissingMeta.left() ensure(split.size >= 3) { MarkdownParsingError.MissingMeta }
return (split[1].trim() to split[2].trim()).right() split[1].trim() to split[2].trim()
} }
private val yaml = Yaml() private val yaml = Yaml()
private fun parseMeta(input: String): Either<MarkdownParsingError.InvalidMeta, NoteMetadata> { private fun parseMeta(input: String) = either {
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 MarkdownParsingError.InvalidMeta.left() raise(MarkdownParsingError.InvalidMeta)
} catch (e: ScannerException) { } catch (e: ScannerException) {
return MarkdownParsingError.InvalidMeta.left() raise(MarkdownParsingError.InvalidMeta)
} }
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 MarkdownParsingError.InvalidMeta.left() else -> raise(MarkdownParsingError.InvalidMeta)
} }
val tagsNode = load["tags"] val tagsNode = load["tags"]
@@ -53,15 +50,15 @@ internal class MarkdownServiceImpl(
else else
tagsNode.map { it.toString() } tagsNode.map { it.toString() }
return NoteMetadata(title, tags).right() NoteMetadata(title, tags)
} }
private fun renderMarkdown(markdown: String) = parser.parse(markdown).run(renderer::render) private fun renderMarkdown(markdown: String) = parser.parse(markdown).run(renderer::render)
override fun renderDocument(input: String) = either.eager<MarkdownParsingError, Document> { override fun renderDocument(input: String) = either {
val (meta, md) = !splitMetaFromDocument(input) val (meta, md) = splitMetaFromDocument(input).bind()
val parsedMeta = !parseMeta(meta) val parsedMeta = parseMeta(meta).bind()
!Either.fromNullable(NoteValidations.validateMetadata(parsedMeta)).swap() NoteValidations.validateMetadata(parsedMeta)?.let { raise(it) }
val html = renderMarkdown(md) val html = renderMarkdown(md)
Document(parsedMeta, html) Document(parsedMeta, html)
} }
+29 -28
View File
@@ -1,20 +1,18 @@
package be.simplenotes.domain package be.simplenotes.domain
import arrow.core.computations.either import arrow.core.raise.either
import be.simplenotes.domain.security.HtmlSanitizer import be.simplenotes.domain.security.HtmlSanitizer
import be.simplenotes.domain.utils.parseSearchTerms import be.simplenotes.domain.utils.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.search.NoteSearcher import be.simplenotes.search.NoteSearcher
import be.simplenotes.types.LoggedInUser import be.simplenotes.types.LoggedInUser
import be.simplenotes.types.Note import be.simplenotes.types.Note
import be.simplenotes.types.PersistedNote
import be.simplenotes.types.PersistedNoteMetadata import be.simplenotes.types.PersistedNoteMetadata
import jakarta.annotation.PostConstruct
import jakarta.annotation.PreDestroy
import jakarta.inject.Singleton
import java.util.* import java.util.*
import javax.annotation.PostConstruct
import javax.annotation.PreDestroy
import javax.inject.Singleton
@Singleton @Singleton
class NoteService( class NoteService(
@@ -23,30 +21,33 @@ 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) = transaction.use { fun create(user: LoggedInUser, markdownText: String) = either {
either.eager<MarkdownParsingError, PersistedNote> { markdownService.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(title = it.metadata.title, tags = it.metadata.tags, 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()
.bind() .also { searcher.indexNote(user.userId, it) }
.also { searcher.indexNote(user.userId, it) }
}
} }
fun update(user: LoggedInUser, uuid: UUID, markdownText: String) = transaction.use { fun update(user: LoggedInUser, uuid: UUID, markdownText: String) =
either.eager<MarkdownParsingError, PersistedNote?> { either {
markdownService.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(
title = it.metadata.title,
tags = it.metadata.tags,
markdown = markdownText,
html = it.html,
)
}
.map { noteRepository.update(user.userId, uuid, it) } .map { noteRepository.update(user.userId, uuid, it) }
.bind() .bind()
?.also { searcher.updateIndex(user.userId, it) } ?.also { searcher.updateIndex(user.userId, it) }
} }
}
fun paginatedNotes( fun paginatedNotes(
userId: Int, userId: Int,
@@ -64,22 +65,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 = transaction.use { fun trash(userId: Int, uuid: UUID): Boolean {
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)
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) 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) }
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) val res = noteRepository.delete(userId, uuid, permanent = true)
if (res) searcher.deleteIndex(userId, uuid) if (res) searcher.deleteIndex(userId, uuid)
res return res
} }
fun countDeleted(userId: Int) = noteRepository.count(userId, deleted = true) fun countDeleted(userId: Int) = noteRepository.count(userId, deleted = true)
@@ -99,8 +100,8 @@ class NoteService(
@PreDestroy @PreDestroy
fun dropAllIndexes() = searcher.dropAll() fun dropAllIndexes() = searcher.dropAll()
fun makePublic(userId: Int, uuid: UUID) = transaction.use { noteRepository.makePublic(userId, uuid) } fun makePublic(userId: Int, uuid: UUID) = noteRepository.makePublic(userId, uuid)
fun makePrivate(userId: Int, uuid: UUID) = transaction.use { noteRepository.makePrivate(userId, uuid) } fun makePrivate(userId: Int, uuid: UUID) = noteRepository.makePrivate(userId, uuid)
fun findPublic(uuid: UUID) = noteRepository.findPublic(uuid) fun findPublic(uuid: UUID) = noteRepository.findPublic(uuid)
} }
+24 -36
View File
@@ -1,21 +1,19 @@
package be.simplenotes.domain package be.simplenotes.domain
import arrow.core.Either import arrow.core.Either
import arrow.core.computations.either import arrow.core.raise.either
import arrow.core.filterOrElse import arrow.core.raise.ensure
import arrow.core.leftIfNull import arrow.core.raise.ensureNotNull
import arrow.core.rightIfNotNull
import be.simplenotes.domain.security.PasswordHash import be.simplenotes.domain.security.PasswordHash
import be.simplenotes.domain.security.SimpleJwt import be.simplenotes.domain.security.SimpleJwt
import be.simplenotes.domain.validation.UserValidations import be.simplenotes.domain.validation.UserValidations
import be.simplenotes.persistence.repositories.UserRepository import be.simplenotes.persistence.repositories.UserRepository
import be.simplenotes.persistence.transactions.TransactionService
import be.simplenotes.search.NoteSearcher import be.simplenotes.search.NoteSearcher
import be.simplenotes.types.LoggedInUser import be.simplenotes.types.LoggedInUser
import be.simplenotes.types.PersistedUser import be.simplenotes.types.PersistedUser
import io.konform.validation.ValidationErrors import io.konform.validation.ValidationErrors
import jakarta.inject.Singleton
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import javax.inject.Singleton
interface UserService { interface UserService {
fun register(form: RegisterForm): Either<RegisterError, PersistedUser> fun register(form: RegisterForm): Either<RegisterError, PersistedUser>
@@ -29,40 +27,30 @@ internal class UserServiceImpl(
private val passwordHash: PasswordHash, private val passwordHash: PasswordHash,
private val jwt: SimpleJwt<LoggedInUser>, private val jwt: SimpleJwt<LoggedInUser>,
private val searcher: NoteSearcher, private val searcher: NoteSearcher,
private val transactionService: TransactionService,
) : UserService { ) : UserService {
override fun register(form: RegisterForm) = transactionService.use { override fun register(form: RegisterForm) = either {
UserValidations.validateRegister(form) val user = UserValidations.validateRegister(form).bind()
.filterOrElse({ !userRepository.exists(it.username) }, { RegisterError.UserExists }) ensure(!userRepository.exists(user.username)) { RegisterError.UserExists }
.map { it.copy(password = passwordHash.crypt(it.password)) } ensureNotNull(userRepository.create(user.copy(password = passwordHash.crypt(user.password)))) {
.map { userRepository.create(it) } RegisterError.UserExists
.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)
} }
} }
override fun login(form: LoginForm) = either {
val user = UserValidations.validateLogin(form).bind()
val persistedUser = ensureNotNull(userRepository.find(user.username)) { LoginError.Unregistered }
ensure(passwordHash.verify(form.password!!, persistedUser.password)) { LoginError.WrongPassword }
jwt.sign(LoggedInUser(persistedUser))
}
override fun delete(form: DeleteForm) = either {
val user = UserValidations.validateDelete(form).bind()
val persistedUser = ensureNotNull(userRepository.find(user.username)) { DeleteError.Unregistered }
ensure(passwordHash.verify(user.password, persistedUser.password)) { DeleteError.WrongPassword }
ensure(userRepository.delete(persistedUser.id)) { DeleteError.Unregistered }
searcher.dropIndex(persistedUser.id)
}
} }
sealed class DeleteError { sealed class DeleteError {
+3 -3
View File
@@ -5,7 +5,7 @@ import com.vladsch.flexmark.html.HtmlRenderer
import com.vladsch.flexmark.parser.Parser import com.vladsch.flexmark.parser.Parser
import com.vladsch.flexmark.util.data.MutableDataSet import com.vladsch.flexmark.util.data.MutableDataSet
import io.micronaut.context.annotation.Factory import io.micronaut.context.annotation.Factory
import javax.inject.Singleton import jakarta.inject.Singleton
@Factory @Factory
class FlexmarkFactory { class FlexmarkFactory {
@@ -23,11 +23,11 @@ class FlexmarkFactory {
set(TaskListExtension.LOOSE_ITEM_CLASS, "") set(TaskListExtension.LOOSE_ITEM_CLASS, "")
set( set(
TaskListExtension.ITEM_DONE_MARKER, TaskListExtension.ITEM_DONE_MARKER,
"""<input type="checkbox" checked="checked" disabled="disabled" readonly="readonly" />&nbsp;""" """<input type="checkbox" checked="checked" disabled="disabled" readonly="readonly" />&nbsp;""",
) )
set( set(
TaskListExtension.ITEM_NOT_DONE_MARKER, TaskListExtension.ITEM_NOT_DONE_MARKER,
"""<input type="checkbox" disabled="disabled" readonly="readonly" />&nbsp;""" """<input type="checkbox" disabled="disabled" readonly="readonly" />&nbsp;""",
) )
set(HtmlRenderer.SOFT_BREAK, "<br>") set(HtmlRenderer.SOFT_BREAK, "<br>")
} }
+1 -1
View File
@@ -4,7 +4,7 @@ import be.simplenotes.types.LoggedInUser
import org.owasp.html.HtmlChangeListener import org.owasp.html.HtmlChangeListener
import org.owasp.html.HtmlPolicyBuilder import org.owasp.html.HtmlPolicyBuilder
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import javax.inject.Singleton import jakarta.inject.Singleton
@Singleton @Singleton
class HtmlSanitizer { class HtmlSanitizer {
+1 -1
View File
@@ -3,7 +3,7 @@ package be.simplenotes.domain.security
import be.simplenotes.types.LoggedInUser import be.simplenotes.types.LoggedInUser
import com.auth0.jwt.JWTCreator import com.auth0.jwt.JWTCreator
import com.auth0.jwt.interfaces.DecodedJWT import com.auth0.jwt.interfaces.DecodedJWT
import javax.inject.Singleton import jakarta.inject.Singleton
interface JwtMapper<T> { interface JwtMapper<T> {
fun extract(decodedJWT: DecodedJWT): T? fun extract(decodedJWT: DecodedJWT): T?
+2 -2
View File
@@ -1,8 +1,8 @@
package be.simplenotes.domain.security package be.simplenotes.domain.security
import org.mindrot.jbcrypt.BCrypt import org.mindrot.jbcrypt.BCrypt
import javax.inject.Inject import jakarta.inject.Inject
import javax.inject.Singleton import jakarta.inject.Singleton
internal interface PasswordHash { internal interface PasswordHash {
fun crypt(password: String): String fun crypt(password: String): String
+1 -1
View File
@@ -7,7 +7,7 @@ import com.auth0.jwt.algorithms.Algorithm
import com.auth0.jwt.exceptions.JWTVerificationException import com.auth0.jwt.exceptions.JWTVerificationException
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Singleton import jakarta.inject.Singleton
@Singleton @Singleton
class SimpleJwt<T>(jwtConfig: JwtConfig, private val mapper: JwtMapper<T>) { class SimpleJwt<T>(jwtConfig: JwtConfig, private val mapper: JwtMapper<T>) {
+1 -1
View File
@@ -89,6 +89,6 @@ internal fun parseSearchTerms(input: String): SearchTerms {
title = title, title = title,
tag = tag, tag = tag,
content = content, content = content,
all = all all = all,
) )
} }
+11 -12
View File
@@ -1,8 +1,7 @@
package be.simplenotes.domain.validation package be.simplenotes.domain.validation
import arrow.core.Either import arrow.core.raise.either
import arrow.core.left import arrow.core.raise.ensure
import arrow.core.right
import be.simplenotes.domain.* import be.simplenotes.domain.*
import be.simplenotes.types.User import be.simplenotes.types.User
import io.konform.validation.Validation import io.konform.validation.Validation
@@ -21,16 +20,16 @@ internal object UserValidations {
} }
} }
fun validateLogin(form: LoginForm): Either<LoginError.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() ensure(errors.isEmpty()) { LoginError.InvalidLoginForm(errors) }
else return LoginError.InvalidLoginForm(errors).left() User(form.username!!, form.password!!)
} }
fun validateRegister(form: RegisterForm): Either<RegisterError.InvalidRegisterForm, User> { fun validateRegister(form: RegisterForm) = either {
val errors = loginValidator.validate(form).errors val errors = loginValidator.validate(form).errors
return if (errors.isEmpty()) User(form.username!!, form.password!!).right() ensure(errors.isEmpty()) { RegisterError.InvalidRegisterForm(errors) }
else return RegisterError.InvalidRegisterForm(errors).left() User(form.username!!, form.password!!)
} }
private val deleteValidator = Validation<DeleteForm> { private val deleteValidator = Validation<DeleteForm> {
@@ -47,9 +46,9 @@ internal object UserValidations {
} }
} }
fun validateDelete(form: DeleteForm): Either<DeleteError.InvalidForm, User> { fun validateDelete(form: DeleteForm) = either {
val errors = deleteValidator.validate(form).errors val errors = deleteValidator.validate(form).errors
return if (errors.isEmpty()) User(form.username!!, form.password!!).right() ensure(errors.isEmpty()) { DeleteError.InvalidForm(errors) }
else return DeleteError.InvalidForm(errors).left() User(form.username!!, form.password!!)
} }
} }
-5
View File
@@ -9,7 +9,6 @@ import be.simplenotes.domain.security.UserJwtMapper
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.persistence.repositories.UserRepository import be.simplenotes.persistence.repositories.UserRepository
import be.simplenotes.persistence.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
@@ -23,16 +22,12 @@ internal class UserServiceTest {
val passwordHash = BcryptPasswordHash(test = true) val passwordHash = BcryptPasswordHash(test = true)
val jwtConfig = JwtConfig("a secret", 1, TimeUnit.HOURS) val jwtConfig = JwtConfig("a secret", 1, TimeUnit.HOURS)
val simpleJwt = SimpleJwt(jwtConfig, UserJwtMapper()) val simpleJwt = SimpleJwt(jwtConfig, UserJwtMapper())
val noopTransactionService = object : TransactionService {
override fun <T> use(block: () -> T) = block()
}
val userService = UserServiceImpl( val userService = UserServiceImpl(
userRepository = userRepository, userRepository = userRepository,
passwordHash = passwordHash, passwordHash = passwordHash,
jwt = simpleJwt, jwt = simpleJwt,
searcher = mockk(), searcher = mockk(),
transactionService = noopTransactionService
) )
@BeforeEach @BeforeEach
@@ -33,7 +33,7 @@ internal class LoggedInUserExtractorTest {
createToken(), createToken(),
createToken(username = "user", id = 1, secret = "not the correct secret"), createToken(username = "user", id = 1, secret = "not the correct secret"),
createToken(username = "user", id = 1) + "\"efesfsef", createToken(username = "user", id = 1) + "\"efesfsef",
"something that is not even a token" "something that is not even a token",
) )
@ParameterizedTest(name = "[{index}] token `{0}` should be invalid") @ParameterizedTest(name = "[{index}] token `{0}` should be invalid")
+2 -2
View File
@@ -21,7 +21,7 @@ fun isRight() = object : Matcher<Either<*, *>> {
override fun invoke(actual: Either<*, *>) = when (actual) { override fun invoke(actual: Either<*, *>) = when (actual) {
is Either.Right -> MatchResult.Match is Either.Right -> MatchResult.Match
is Either.Left -> { is Either.Left -> {
val valueA = actual.a val valueA = actual.value
MatchResult.Mismatch("is Either.Left<${if (valueA == null) "Null" else valueA::class.simpleName}>") MatchResult.Mismatch("is Either.Left<${if (valueA == null) "Null" else valueA::class.simpleName}>")
} }
} }
@@ -34,7 +34,7 @@ inline fun <reified A> isLeftOfType() = object : Matcher<Either<*, *>> {
override fun invoke(actual: Either<*, *>) = when (actual) { override fun invoke(actual: Either<*, *>) = when (actual) {
is Either.Right -> MatchResult.Mismatch("was Either.Right<>") is Either.Right -> MatchResult.Mismatch("was Either.Right<>")
is Either.Left -> { is Either.Left -> {
val valueA = actual.a val valueA = actual.value
if (valueA is A) MatchResult.Match if (valueA is A) MatchResult.Match
else MatchResult.Mismatch("was Left<${if (valueA == null) "Null" else valueA::class.simpleName}>") else MatchResult.Mismatch("was Left<${if (valueA == null) "Null" else valueA::class.simpleName}>")
} }
+2 -2
View File
@@ -33,7 +33,7 @@ internal class SearchTermsParserKtTest {
"tag:'example abc' title:'other with words' this is the end ", "tag:'example abc' title:'other with words' this is the end ",
title = "other with words", title = "other with words",
tag = "example abc", tag = "example abc",
all = "this is the end" all = "this is the end",
), ),
createResult("tag:blah", tag = "blah"), createResult("tag:blah", tag = "blah"),
createResult("tag:'some words'", tag = "some words"), createResult("tag:'some words'", tag = "some words"),
@@ -41,7 +41,7 @@ internal class SearchTermsParserKtTest {
createResult( createResult(
"tag:'double quote inside single \" ' global", "tag:'double quote inside single \" ' global",
tag = "double quote inside single \" ", tag = "double quote inside single \" ",
all = "global" all = "global",
), ),
) )
@@ -22,7 +22,7 @@ internal class UserValidationsTest {
LoginForm(username = "", password = ""), LoginForm(username = "", password = ""),
LoginForm(username = "a", password = "aaaa"), LoginForm(username = "a", password = "aaaa"),
LoginForm(username = "a".repeat(51), password = "a".repeat(8)), LoginForm(username = "a".repeat(51), password = "a".repeat(8)),
LoginForm(username = "a".repeat(10), password = "a".repeat(7)) LoginForm(username = "a".repeat(10), password = "a".repeat(7)),
) )
@ParameterizedTest @ParameterizedTest
@@ -34,7 +34,7 @@ internal class UserValidationsTest {
@Suppress("Unused") @Suppress("Unused")
fun validLoginForms(): Stream<LoginForm> = Stream.of( fun validLoginForms(): Stream<LoginForm> = Stream.of(
LoginForm(username = "a".repeat(50), password = "a".repeat(72)), LoginForm(username = "a".repeat(50), password = "a".repeat(72)),
LoginForm(username = "a".repeat(3), password = "a".repeat(8)) LoginForm(username = "a".repeat(3), password = "a".repeat(8)),
) )
@ParameterizedTest @ParameterizedTest
@@ -53,7 +53,7 @@ internal class UserValidationsTest {
RegisterForm(username = "", password = ""), RegisterForm(username = "", password = ""),
RegisterForm(username = "a", password = "aaaa"), RegisterForm(username = "a", password = "aaaa"),
RegisterForm(username = "a".repeat(51), password = "a".repeat(8)), RegisterForm(username = "a".repeat(51), password = "a".repeat(8)),
RegisterForm(username = "a".repeat(10), password = "a".repeat(7)) RegisterForm(username = "a".repeat(10), password = "a".repeat(7)),
) )
@ParameterizedTest @ParameterizedTest
@@ -65,7 +65,7 @@ internal class UserValidationsTest {
@Suppress("Unused") @Suppress("Unused")
fun validRegisterForms(): Stream<RegisterForm> = Stream.of( fun validRegisterForms(): Stream<RegisterForm> = Stream.of(
RegisterForm(username = "a".repeat(50), password = "a".repeat(72)), RegisterForm(username = "a".repeat(50), password = "a".repeat(72)),
RegisterForm(username = "a".repeat(3), password = "a".repeat(8)) RegisterForm(username = "a".repeat(3), password = "a".repeat(8)),
) )
@ParameterizedTest @ParameterizedTest
+1 -1
View File
@@ -1,3 +1,3 @@
org.gradle.jvmargs=-Xmx2048M -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 org.gradle.jvmargs=-Xmx2048M -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.caching=true org.gradle.caching=true
org.gradle.parallel=true org.gradle.parallel=true
+62
View File
@@ -0,0 +1,62 @@
[versions]
flexmark = "0.64.4"
lucene = "9.5.0"
http4k = "4.43.0.0"
jetty = "11.0.15"
mapstruct = "1.5.5.Final"
micronaut-inject = "4.0.0-M2"
[libraries]
flexmark-core = { module = "com.vladsch.flexmark:flexmark", version.ref = "flexmark" }
flexmark-tasklist = { module = "com.vladsch.flexmark:flexmark-ext-gfm-tasklist", version.ref = "flexmark" }
flyway = { module = "org.flywaydb:flyway-core", version = "9.17.0" }
hikariCP = { module = "com.zaxxer:HikariCP", version = "5.0.1" }
h2 = { module = "com.h2database:h2", version = "2.1.214" }
ktorm = { module = "org.ktorm:ktorm-core", version = "3.6.0" }
lucene-core = { module = "org.apache.lucene:lucene-core", version.ref = "lucene" }
lucene-analyzers-common = { module = "org.apache.lucene:lucene-analysis-common", version.ref = "lucene" }
lucene-queryparser = { module = "org.apache.lucene:lucene-queryparser", version.ref = "lucene" }
http4k-core = { module = "org.http4k:http4k-core", version.ref = "http4k" }
http4k-multipart = { module = "org.http4k:http4k-multipart", version.ref = "http4k" }
http4k-testing-hamkrest = { module = "org.http4k:http4k-testing-hamkrest", version.ref = "http4k" }
jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = "jetty" }
jetty-servlet = { module = "org.eclipse.jetty:jetty-servlet", version.ref = "jetty" }
javax-servlet = { module = "jakarta.servlet:jakarta.servlet-api", version = "6.0.0" }
kotlinx-html = { module = "org.jetbrains.kotlinx:kotlinx-html-jvm", version = "0.8.1" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm", version = "1.5.0" }
slf4j-api = { module = "org.slf4j:slf4j-api", version = "2.0.7" }
slf4j-logback = { module = "ch.qos.logback:logback-classic", version = "1.4.7" }
mapstruct-core = { module = "org.mapstruct:mapstruct", version.ref = "mapstruct" }
mapstruct-processor = { module = "org.mapstruct:mapstruct-processor", version.ref = "mapstruct" }
micronaut-inject = { module = "io.micronaut:micronaut-inject", version.ref = "micronaut-inject" }
micronaut-processor = { module = "io.micronaut:micronaut-inject-java", version.ref = "micronaut-inject" }
arrow-core = { module = "io.arrow-kt:arrow-core", version = "1.2.0-RC" }
commonsCompress = { module = "org.apache.commons:commons-compress", version = "1.23.0" }
jwt = { module = "com.auth0:java-jwt", version = "4.4.0" }
bcrypt = { module = "org.mindrot:jbcrypt", version = "0.4" }
konform = { module = "io.konform:konform-jvm", version = "0.4.0" }
owasp-html-sanitizer = { module = "com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer", version = "20220608.1" }
prettytime = { module = "org.ocpsoft.prettytime:prettytime", version = "5.0.6.Final" }
yaml = { module = "org.yaml:snakeyaml", version = "2.0" }
assertJ = { module = "org.assertj:assertj-core", version = "3.24.2" }
hamkrest = { module = "com.natpryce:hamkrest", version = "1.8.0.1" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version = "5.9.3" }
mockk = { module = "io.mockk:mockk", version = "1.13.5" }
faker = { module = "com.github.javafaker:javafaker", version = "1.0.2" }
[bundles]
flexmark = ["flexmark-core", "flexmark-tasklist"]
database = ["flyway", "hikariCP", "h2", "ktorm"]
lucene = ["lucene-core", "lucene-analyzers-common", "lucene-queryparser"]
jetty = ["jetty-server", "jetty-servlet", "javax-servlet"]
test = ["assertJ", "hamkrest", "junit-jupiter", "mockk"]
Binary file not shown.
+2 -1
View File
@@ -1,5 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
+11 -22
View File
@@ -1,4 +1,3 @@
import be.simplenotes.Libs
import be.simplenotes.micronaut import be.simplenotes.micronaut
plugins { plugins {
@@ -12,39 +11,29 @@ dependencies {
implementation(project(":types")) implementation(project(":types"))
implementation(project(":config")) implementation(project(":config"))
implementation(Libs.Database.Drivers.mariadb) implementation(libs.bundles.database)
implementation(Libs.Database.Drivers.h2)
implementation(Libs.Database.flyway)
implementation(Libs.Database.hikariCP)
implementation(Libs.Database.Ktorm.core)
runtimeOnly(Libs.Database.Ktorm.mysql)
implementation(Libs.Slf4J.api) implementation(libs.slf4j.api)
runtimeOnly(Libs.Slf4J.logback) runtimeOnly(libs.slf4j.logback)
compileOnly(Libs.Mapstruct.core) compileOnly(libs.mapstruct.core)
kapt(Libs.Mapstruct.processor) kapt(libs.mapstruct.processor)
testImplementation(Libs.Test.junit) testImplementation(libs.bundles.test)
testImplementation(Libs.Test.assertJ) testCompileOnly(libs.slf4j.logback)
testCompileOnly(Libs.Slf4J.logback)
testImplementation(Libs.Test.mariaTestContainer)
testFixturesImplementation(project(":types")) testFixturesImplementation(project(":types"))
testFixturesImplementation(project(":config")) testFixturesImplementation(project(":config"))
testFixturesImplementation(project(":persistence")) testFixturesImplementation(project(":persistence"))
testFixturesImplementation(Libs.Test.faker) { testFixturesImplementation(libs.faker) {
exclude(group = "org.yaml") exclude(group = "org.yaml")
} }
testFixturesImplementation(Libs.snakeyaml) testFixturesImplementation(libs.yaml)
testFixturesImplementation(Libs.Test.mariaTestContainer) testFixturesImplementation(libs.bundles.database)
testFixturesImplementation(Libs.Database.flyway) testFixturesImplementation(libs.junit.jupiter)
testFixturesImplementation(Libs.Test.junit)
testFixturesImplementation(Libs.Database.Ktorm.core)
testFixturesImplementation(Libs.Database.hikariCP)
micronaut() micronaut()
@@ -9,7 +9,7 @@ create table Users
create table Notes create table Notes
( (
uuid binary(16) not null primary key, uuid uuid not null primary key,
title varchar(50) not null, title varchar(50) not null,
markdown mediumtext not null, markdown mediumtext not null,
html mediumtext not null, html mediumtext not null,
@@ -17,7 +17,6 @@ create table Notes
updated_at datetime null, updated_at datetime null,
constraint Notes_fk_user foreign key (user_id) references Users (id) on delete cascade constraint Notes_fk_user foreign key (user_id) references Users (id) on delete cascade
); );
create index user_id on Notes (user_id); create index user_id on Notes (user_id);
@@ -26,7 +25,7 @@ create table Tags
( (
id int auto_increment primary key, id int auto_increment primary key,
name varchar(50) not null, name varchar(50) not null,
note_uuid binary(16) not null, note_uuid uuid not null,
constraint Tags_fk_note foreign key (note_uuid) references Notes (uuid) on delete cascade constraint Tags_fk_note foreign key (note_uuid) references Notes (uuid) on delete cascade
); );
@@ -0,0 +1,5 @@
CREATE VIEW NotesWithTags AS
SELECT n.*, coalesce(ARRAY_AGG(t.NAME), ARRAY[]) as tags
FROM Notes n
LEFT JOIN Tags t ON n.uuid = t.note_uuid
GROUP BY n.uuid
@@ -1,36 +0,0 @@
create table Users
(
id int auto_increment primary key,
username varchar(50) not null,
password varchar(255) not null,
constraint username unique (username)
) character set 'utf8mb4'
collate 'utf8mb4_general_ci';
create table Notes
(
uuid binary(16) not null primary key,
title varchar(50) not null,
markdown mediumtext not null,
html mediumtext not null,
user_id int not null,
updated_at datetime null,
constraint Notes_fk_user foreign key (user_id) references Users (id) on delete cascade
) character set 'utf8mb4'
collate 'utf8mb4_general_ci';
create index user_id on Notes (user_id);
create table Tags
(
id int auto_increment primary key,
name varchar(50) not null,
note_uuid binary(16) not null,
constraint Tags_fk_note foreign key (note_uuid) references Notes (uuid) on delete cascade
) character set 'utf8mb4'
collate 'utf8mb4_general_ci';
create index note_uuid on Tags (note_uuid);
@@ -1,2 +0,0 @@
alter table Notes
add column deleted bool not null default false
@@ -1,2 +0,0 @@
alter table Notes
add column public bool not null default false
+55
View File
@@ -0,0 +1,55 @@
package be.simplenotes.persistence
import org.ktorm.database.Database
import org.ktorm.entity.Entity
import org.ktorm.entity.sequenceOf
import java.time.LocalDateTime
import java.util.*
internal val Database.users get() = sequenceOf(Users, withReferences = false)
internal val Database.notes get() = sequenceOf(Notes, withReferences = false)
internal val Database.tags get() = sequenceOf(Tags, withReferences = false)
internal val Database.noteWithTags get() = sequenceOf(NotesWithTags, withReferences = false)
internal interface UserEntity : Entity<UserEntity> {
companion object : Entity.Factory<UserEntity>()
var id: Int
var username: String
var password: String
}
internal interface NoteEntity : Entity<NoteEntity> {
companion object : Entity.Factory<NoteEntity>()
var uuid: UUID
var title: String
var markdown: String
var html: String
var updatedAt: LocalDateTime
var deleted: Boolean
var public: Boolean
var user: UserEntity
}
internal interface TagEntity : Entity<TagEntity> {
companion object : Entity.Factory<TagEntity>()
val id: Int
var name: String
var note: NoteEntity
}
internal interface NoteWithTagsEntity : Entity<NoteWithTagsEntity> {
companion object : Entity.Factory<NoteWithTagsEntity>()
var uuid: UUID
var title: String
var markdown: String
var html: String
var updatedAt: LocalDateTime
var deleted: Boolean
var public: Boolean
var user: UserEntity
val tags: List<String>
}
-31
View File
@@ -1,31 +0,0 @@
package be.simplenotes.persistence
import be.simplenotes.config.DataSourceConfig
import be.simplenotes.persistence.utils.DbType
import be.simplenotes.persistence.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 javax.inject.Singleton
interface DbHealthCheck {
fun isOk(): Boolean
}
@Singleton
internal class DbHealthCheckImpl(
private val db: Database,
private val dataSourceConfig: DataSourceConfig,
) : DbHealthCheck {
override fun isOk() = if (dataSourceConfig.type() == DbType.H2) true
else try {
db.useConnection { connection ->
connection.prepareStatement("""SHOW DATABASES""").use {
it.executeQuery().asIterable().map { it.getString(1) }
}
}.any { it in dataSourceConfig.jdbcUrl }
} catch (e: SQLException) {
false
}
}
+4 -15
View File
@@ -1,10 +1,7 @@
package be.simplenotes.persistence package be.simplenotes.persistence
import be.simplenotes.config.DataSourceConfig
import be.simplenotes.persistence.utils.DbType
import be.simplenotes.persistence.utils.type
import org.flywaydb.core.Flyway import org.flywaydb.core.Flyway
import javax.inject.Singleton import jakarta.inject.Singleton
import javax.sql.DataSource import javax.sql.DataSource
interface DbMigrations { interface DbMigrations {
@@ -12,20 +9,12 @@ interface DbMigrations {
} }
@Singleton @Singleton
internal class DbMigrationsImpl( internal class DbMigrationsImpl(private val dataSource: DataSource) : DbMigrations {
private val dataSource: DataSource,
private val dataSourceConfig: DataSourceConfig,
) : DbMigrations {
override fun migrate() { override fun migrate() {
val migrationDir = when (dataSourceConfig.type()) {
DbType.H2 -> "db/migration/other"
DbType.MariaDb -> "db/migration/mariadb"
}
Flyway.configure() Flyway.configure()
.dataSource(dataSource) .dataSource(dataSource)
.locations(migrationDir) .locations("db/migration")
.loggers("slf4j")
.load() .load()
.migrate() .migrate()
} }
+15 -10
View File
@@ -1,12 +1,14 @@
package be.simplenotes.persistence package be.simplenotes.persistence
import be.simplenotes.config.DataSourceConfig import be.simplenotes.config.DataSourceConfig
import be.simplenotes.persistence.extensions.CustomSqlFormatter
import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource import com.zaxxer.hikari.HikariDataSource
import io.micronaut.context.annotation.Bean import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory import io.micronaut.context.annotation.Factory
import me.liuwj.ktorm.database.Database import org.ktorm.database.Database
import javax.inject.Singleton import org.ktorm.database.SqlDialect
import jakarta.inject.Singleton
import javax.sql.DataSource import javax.sql.DataSource
@Factory @Factory
@@ -15,7 +17,13 @@ class PersistenceModule {
@Singleton @Singleton
internal fun database(migrations: DbMigrations, dataSource: DataSource): Database { internal fun database(migrations: DbMigrations, dataSource: DataSource): Database {
migrations.migrate() migrations.migrate()
return Database.connect(dataSource) return Database.connect(
dataSource,
dialect = object : SqlDialect {
override fun createSqlFormatter(database: Database, beautifySql: Boolean, indentSize: Int) =
CustomSqlFormatter(database, beautifySql, indentSize)
},
)
} }
@Singleton @Singleton
@@ -23,15 +31,12 @@ class PersistenceModule {
internal fun dataSource(conf: DataSourceConfig): HikariDataSource { internal fun dataSource(conf: DataSourceConfig): HikariDataSource {
val hikariConfig = HikariConfig().also { val hikariConfig = HikariConfig().also {
it.jdbcUrl = conf.jdbcUrl it.jdbcUrl = conf.jdbcUrl
it.driverClassName = when { it.driverClassName = "org.h2.Driver"
conf.jdbcUrl.startsWith("jdbc:mariadb") -> "org.mariadb.jdbc.Driver" it.username = ""
conf.jdbcUrl.startsWith("jdbc:h2") -> "org.h2.Driver" it.password = ""
else -> error("Unsupported database")
}
it.username = conf.username
it.password = conf.password
it.maximumPoolSize = conf.maximumPoolSize it.maximumPoolSize = conf.maximumPoolSize
it.connectionTimeout = conf.connectionTimeout it.connectionTimeout = conf.connectionTimeout
it.dataSourceProperties["CASE_INSENSITIVE_IDENTIFIERS"] = "TRUE"
} }
return HikariDataSource(hikariConfig) return HikariDataSource(hikariConfig)
} }
+42
View File
@@ -0,0 +1,42 @@
package be.simplenotes.persistence
import be.simplenotes.persistence.extensions.varcharArray
import org.ktorm.schema.*
internal object Users : Table<UserEntity>("Users") {
val id = int("id").primaryKey().bindTo { it.id }
val username = varchar("username").bindTo { it.username }
val password = varchar("password").bindTo { it.password }
}
internal object Notes : Table<NoteEntity>("Notes") {
val uuid = uuid("uuid").primaryKey().bindTo { it.uuid }
val title = varchar("title").bindTo { it.title }
val markdown = text("markdown").bindTo { it.markdown }
val html = text("html").bindTo { it.html }
val userId = int("user_id").references(Users) { it.user }
val updatedAt = datetime("updated_at").bindTo { it.updatedAt }
val deleted = boolean("deleted").bindTo { it.deleted }
val public = boolean("public").bindTo { it.public }
val user get() = userId.referenceTable as Users
}
internal object Tags : Table<TagEntity>("Tags") {
val id = int("id").primaryKey().bindTo { it.id }
val name = varchar("name").bindTo { it.name }
val noteUuid = uuid("note_uuid").references(Notes) { it.note }
val note: Notes get() = noteUuid.referenceTable as Notes
}
internal object NotesWithTags : Table<NoteWithTagsEntity>("NotesWithTags") {
val uuid = uuid("uuid").primaryKey().bindTo { it.uuid }
val title = varchar("title").bindTo { it.title }
val markdown = text("markdown").bindTo { it.markdown }
val html = text("html").bindTo { it.html }
val userId = int("user_id").references(Users) { it.user }
val updatedAt = datetime("updated_at").bindTo { it.updatedAt }
val deleted = boolean("deleted").bindTo { it.deleted }
val public = boolean("public").bindTo { it.public }
val tags = varcharArray("tags").bindTo { it.tags }
val user get() = userId.referenceTable as Users
}
+34
View File
@@ -0,0 +1,34 @@
package be.simplenotes.persistence.converters
import be.simplenotes.persistence.NoteEntity
import be.simplenotes.persistence.NoteWithTagsEntity
import be.simplenotes.persistence.UserEntity
import be.simplenotes.types.*
import org.mapstruct.Mapper
import org.mapstruct.Mapping
import org.mapstruct.ReportingPolicy
import java.time.LocalDateTime
import java.util.*
@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE, componentModel = "jsr330")
internal interface UserConverter {
fun toPersistedUser(userEntity: UserEntity?): PersistedUser?
}
@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE, componentModel = "jsr330")
internal abstract class NoteConverter {
@Mapping(target = "trash", source = "deleted")
abstract fun toExportedNote(entity: NoteWithTagsEntity?): ExportedNote?
abstract fun toPersistedNote(entity: NoteWithTagsEntity?): PersistedNote?
abstract fun toPersistedNoteMetadata(entity: NoteWithTagsEntity?): PersistedNoteMetadata?
fun toEntity(note: Note, uuid: UUID, userId: Int, updatedAt: LocalDateTime) = NoteEntity {
this.title = note.title
this.markdown = note.markdown
this.html = note.html
this.uuid = uuid
this.user.id = userId
this.updatedAt = updatedAt
}
}
@@ -1,85 +0,0 @@
package be.simplenotes.persistence.converters
import be.simplenotes.persistence.notes.NoteEntity
import be.simplenotes.types.*
import me.liuwj.ktorm.entity.Entity
import org.mapstruct.Mapper
import org.mapstruct.Mapping
import org.mapstruct.Mappings
import org.mapstruct.ReportingPolicy
import java.time.LocalDateTime
import java.util.*
import javax.inject.Singleton
@Mapper(
uses = [NoteEntityFactory::class, UserEntityFactory::class],
unmappedTargetPolicy = ReportingPolicy.IGNORE,
componentModel = "jsr330"
)
internal abstract class NoteConverter {
fun toNote(entity: NoteEntity, tags: Tags) =
Note(NoteMetadata(title = entity.title, tags = tags), entity.markdown, entity.html)
@Mappings(
Mapping(target = ".", source = "entity"),
Mapping(target = "tags", source = "tags"),
)
abstract fun toNoteMetadata(entity: NoteEntity, tags: Tags): NoteMetadata
@Mappings(
Mapping(target = ".", source = "entity"),
Mapping(target = "tags", source = "tags"),
)
abstract fun toPersistedNoteMetadata(entity: NoteEntity, tags: Tags): PersistedNoteMetadata
fun toPersistedNote(entity: NoteEntity, tags: Tags) = PersistedNote(
NoteMetadata(title = entity.title, tags = tags),
entity.markdown,
entity.html,
entity.updatedAt,
entity.uuid,
entity.public
)
@Mappings(
Mapping(target = ".", source = "entity"),
Mapping(target = "trash", source = "entity.deleted"),
Mapping(target = "tags", source = "tags"),
)
abstract fun toExportedNote(entity: NoteEntity, tags: Tags): ExportedNote
fun toEntity(note: Note, uuid: UUID, userId: Int, updatedAt: LocalDateTime) = NoteEntity {
this.title = note.meta.title
this.markdown = note.markdown
this.html = note.html
this.uuid = uuid
this.deleted = false
this.public = false
this.user.id = userId
this.updatedAt = updatedAt
}
@Mappings(
Mapping(target = ".", source = "note"),
Mapping(target = "updatedAt", source = "updatedAt"),
Mapping(target = "uuid", source = "uuid"),
Mapping(target = "public", constant = "false"),
)
abstract fun toPersistedNote(note: Note, updatedAt: LocalDateTime, uuid: UUID): PersistedNote
abstract fun toEntity(persistedNoteMetadata: PersistedNoteMetadata): NoteEntity
abstract fun toEntity(noteMetadata: NoteMetadata): NoteEntity
@Mapping(target = "title", source = "meta.title")
abstract fun toEntity(persistedNote: PersistedNote): NoteEntity
@Mapping(target = "deleted", source = "trash")
abstract fun toEntity(exportedNote: ExportedNote): NoteEntity
}
typealias Tags = List<String>
@Singleton
internal class NoteEntityFactory : Entity.Factory<NoteEntity>()
@@ -1,24 +0,0 @@
package be.simplenotes.persistence.converters
import be.simplenotes.persistence.users.UserEntity
import be.simplenotes.types.PersistedUser
import be.simplenotes.types.User
import me.liuwj.ktorm.entity.Entity
import org.mapstruct.Mapper
import org.mapstruct.ReportingPolicy
import javax.inject.Singleton
@Mapper(
uses = [UserEntityFactory::class],
unmappedTargetPolicy = ReportingPolicy.IGNORE,
componentModel = "jsr330"
)
internal interface UserConverter {
fun toUser(userEntity: UserEntity): User
fun toPersistedUser(userEntity: UserEntity): PersistedUser
fun toEntity(user: User): UserEntity
fun toEntity(user: PersistedUser): UserEntity
}
@Singleton
internal class UserEntityFactory : Entity.Factory<UserEntity>()
+67 -15
View File
@@ -1,26 +1,78 @@
package be.simplenotes.persistence.extensions package be.simplenotes.persistence.extensions
import me.liuwj.ktorm.schema.BaseTable import org.ktorm.database.Database
import me.liuwj.ktorm.schema.SqlType import org.ktorm.expression.*
import java.nio.ByteBuffer import org.ktorm.schema.*
import java.sql.PreparedStatement import java.sql.PreparedStatement
import java.sql.ResultSet import java.sql.ResultSet
import java.sql.Types import java.sql.Types
import java.util.*
internal class UuidBinarySqlType : SqlType<UUID>(Types.BINARY, typeName = "uuidBinary") { internal class VarcharArraySqlType : SqlType<List<String>>(Types.ARRAY, typeName = "varchar[]") {
override fun doGetResult(rs: ResultSet, index: Int): UUID? { override fun doGetResult(rs: ResultSet, index: Int): List<String>? {
val value = rs.getBytes(index) ?: return null return when (val array = rs.getObject(index)) {
return ByteBuffer.wrap(value).let { b -> UUID(b.long, b.long) } null -> null
is java.sql.Array -> (array.array as Array<*>).filterNotNull().map { it.toString() }
is Array<*> -> array.map { it.toString() }
else -> error("Unable to deserialize varchar[]")
}
} }
override fun doSetParameter(ps: PreparedStatement, index: Int, parameter: UUID) { override fun doSetParameter(ps: PreparedStatement, index: Int, parameter: List<String>) {
val bytes = ByteBuffer.allocate(16) throw UnsupportedOperationException()
.putLong(parameter.mostSignificantBits)
.putLong(parameter.leastSignificantBits)
.array()
ps.setBytes(index, bytes)
} }
} }
internal fun <E : Any> BaseTable<E>.uuidBinary(name: String) = registerColumn(name, UuidBinarySqlType()) internal fun <E : Any> BaseTable<E>.varcharArray(name: String) = registerColumn(name, VarcharArraySqlType())
data class ArrayContainsExpression(
val left: ScalarExpression<*>,
val right: ScalarExpression<*>,
override val sqlType: SqlType<Boolean> = BooleanSqlType,
override val isLeafNode: Boolean = false,
) : ScalarExpression<Boolean>() {
override val extraProperties: Map<String, Any> get() = emptyMap()
}
infix fun ColumnDeclaring<*>.arrayContains(arguments: String): ArrayContainsExpression {
return ArrayContainsExpression(asExpression(), ArgumentExpression(arguments, VarcharSqlType))
}
class CustomSqlFormatter(database: Database, beautifySql: Boolean, indentSize: Int) :
SqlFormatter(database, beautifySql, indentSize) {
override fun visitUnknown(expr: SqlExpression): SqlExpression {
return if (expr is ArrayContainsExpression) {
write("ARRAY_CONTAINS(")
if (expr.left.removeBrackets) {
visit(expr.left)
} else {
write("(")
visit(expr.left)
removeLastBlank()
write(") ")
}
write(", ")
if (expr.right.removeBrackets) {
visit(expr.right)
} else {
write("(")
visit(expr.right)
removeLastBlank()
write(") ")
}
write(")")
return expr
} else super.visitUnknown(expr)
}
override fun writePagination(expr: QueryExpression) {
newLine(Indentation.SAME)
writeKeyword("limit ?, ? ")
_parameters += ArgumentExpression(expr.offset ?: 0, IntSqlType)
_parameters += ArgumentExpression(expr.limit ?: Int.MAX_VALUE, IntSqlType)
}
}
-219
View File
@@ -1,219 +0,0 @@
package be.simplenotes.persistence.notes
import be.simplenotes.persistence.converters.NoteConverter
import be.simplenotes.persistence.repositories.NoteRepository
import be.simplenotes.types.ExportedNote
import be.simplenotes.types.Note
import be.simplenotes.types.PersistedNote
import be.simplenotes.types.PersistedNoteMetadata
import me.liuwj.ktorm.database.Database
import me.liuwj.ktorm.dsl.*
import me.liuwj.ktorm.entity.*
import java.time.LocalDateTime
import java.util.*
import javax.inject.Singleton
import kotlin.collections.HashMap
@Singleton
internal class NoteRepositoryImpl(
private val db: Database,
private val converter: NoteConverter,
) : NoteRepository {
@Throws(IllegalArgumentException::class)
override fun findAll(
userId: Int,
limit: Int,
offset: Int,
tag: String?,
deleted: Boolean,
): List<PersistedNoteMetadata> {
require(limit > 0) { "limit should be positive" }
require(offset >= 0) { "offset should not be negative" }
val uuids1: List<UUID>? = if (tag != null) {
db.from(Tags)
.leftJoin(Notes, on = Notes.uuid eq Tags.noteUuid)
.select(Notes.uuid)
.where { (Notes.userId eq userId) and (Tags.name eq tag) and (Notes.deleted eq deleted) }
.map { it[Notes.uuid]!! }
} else null
var query = db.notes
.filterColumns { listOf(it.uuid, it.title, it.updatedAt) }
.filter { (it.userId eq userId) and (it.deleted eq deleted) }
if (uuids1 != null) query = query.filter { it.uuid inList uuids1 }
val notes = query
.sortedByDescending { it.updatedAt }
.take(limit)
.drop(offset)
.toList()
val tagsByUuid = notes.tagsByUuid()
return notes.map { note ->
val tags = tagsByUuid[note.uuid] ?: emptyList()
converter.toPersistedNoteMetadata(note, tags)
}
}
override fun exists(userId: Int, uuid: UUID): Boolean {
return db.notes.any { (it.userId eq userId) and (it.uuid eq uuid) and (it.deleted eq false) }
}
override fun create(userId: Int, note: Note): PersistedNote {
val uuid = UUID.randomUUID()
val entity = converter.toEntity(note, uuid, userId, LocalDateTime.now())
db.notes.add(entity)
db.batchInsert(Tags) {
note.meta.tags.forEach { tagName ->
item {
it.noteUuid to uuid
it.name to tagName
}
}
}
return converter.toPersistedNote(entity, note.meta.tags)
}
override fun find(userId: Int, uuid: UUID): PersistedNote? {
val note = db.notes
.filterColumns { it.columns - it.userId }
.filter { it.uuid eq uuid }
.find { (it.userId eq userId) and (it.deleted eq false) }
?: return null
val tags = db.from(Tags)
.select(Tags.name)
.where { Tags.noteUuid eq uuid }
.map { it[Tags.name]!! }
return converter.toPersistedNote(note, tags)
}
override fun update(userId: Int, uuid: UUID, note: Note): PersistedNote? {
val now = LocalDateTime.now()
val count = db.update(Notes) {
it.title to note.meta.title
it.markdown to note.markdown
it.html to note.html
it.updatedAt to now
where { (it.uuid eq uuid) and (it.userId eq userId) and (it.deleted eq false) }
}
if (count == 0) return null
// delete all tags
db.delete(Tags) {
it.noteUuid eq uuid
}
// put new ones
note.meta.tags.forEach { tagName ->
db.insert(Tags) {
it.name to tagName
it.noteUuid to uuid
}
}
return converter.toPersistedNote(note, now, uuid)
}
override fun delete(userId: Int, uuid: UUID, permanent: Boolean): Boolean {
return if (!permanent) {
db.update(Notes) {
it.deleted to true
it.updatedAt to LocalDateTime.now()
where { it.userId eq userId and (it.uuid eq uuid) }
} == 1
} else
db.delete(Notes) { it.uuid eq uuid and (it.userId eq userId) } == 1
}
override fun restore(userId: Int, uuid: UUID): Boolean {
return db.update(Notes) {
it.deleted to false
where { (it.userId eq userId) and (it.uuid eq uuid) }
} == 1
}
override fun getTags(userId: Int): List<String> =
db.from(Tags)
.leftJoin(Notes, on = Notes.uuid eq Tags.noteUuid)
.selectDistinct(Tags.name)
.where { (Notes.userId eq userId) and (Notes.deleted eq false) }
.map { it[Tags.name]!! }
override fun count(userId: Int, tag: String?, deleted: Boolean): Int {
return if (tag == null) db.notes.count { (it.userId eq userId) and (Notes.deleted eq deleted) }
else db.sequenceOf(Tags).count {
(it.name eq tag) and (it.note.userId eq userId) and (it.note.deleted eq deleted)
}
}
override fun export(userId: Int): List<ExportedNote> {
val notes = db.notes
.filterColumns { it.columns - it.userId }
.filter { it.userId eq userId }
.sortedByDescending { it.updatedAt }
.toList()
val tagsByUuid = notes.tagsByUuid()
return notes.map { note ->
val tags = tagsByUuid[note.uuid] ?: emptyList()
converter.toExportedNote(note, tags)
}
}
override fun findAllDetails(userId: Int): List<PersistedNote> {
val notes = db.notes
.filterColumns { it.columns - it.deleted }
.filter { (it.userId eq userId) and (it.deleted eq false) }
.toList()
val tagsByUuid = notes.tagsByUuid()
return notes.map { note ->
val tags = tagsByUuid[note.uuid] ?: emptyList()
converter.toPersistedNote(note, tags)
}
}
override fun makePublic(userId: Int, uuid: UUID) = db.update(Notes) {
it.public to true
where { Notes.userId eq userId and (Notes.uuid eq uuid) and (it.deleted eq false) }
} == 1
override fun makePrivate(userId: Int, uuid: UUID) = db.update(Notes) {
it.public to false
where { Notes.userId eq userId and (Notes.uuid eq uuid) and (it.deleted eq false) }
} == 1
override fun findPublic(uuid: UUID): PersistedNote? {
val note = db.notes
.filterColumns { it.columns - it.userId }
.filter { it.uuid eq uuid }
.filter { it.public eq true }
.find { it.deleted eq false }
?: return null
val tags = db.from(Tags)
.select(Tags.name)
.where { Tags.noteUuid eq uuid }
.map { it[Tags.name]!! }
return converter.toPersistedNote(note, tags)
}
private fun List<NoteEntity>.tagsByUuid(): Map<UUID, List<String>> {
return if (isEmpty()) emptyMap()
else db.tags
.filterColumns { listOf(it.noteUuid, it.name) }
.filter { it.noteUuid inList map { note -> note.uuid } }
.groupByTo(HashMap(), { it.note.uuid }, { it.name })
}
}
-45
View File
@@ -1,45 +0,0 @@
@file:Suppress("unused")
package be.simplenotes.persistence.notes
import be.simplenotes.persistence.extensions.uuidBinary
import be.simplenotes.persistence.users.UserEntity
import be.simplenotes.persistence.users.Users
import me.liuwj.ktorm.database.Database
import me.liuwj.ktorm.entity.Entity
import me.liuwj.ktorm.entity.sequenceOf
import me.liuwj.ktorm.schema.*
import java.time.LocalDateTime
import java.util.*
internal open class Notes(alias: String?) : Table<NoteEntity>("Notes", alias) {
companion object : Notes(null)
override fun aliased(alias: String) = Notes(alias)
val uuid = uuidBinary("uuid").primaryKey().bindTo { it.uuid }
val title = varchar("title").bindTo { it.title }
val markdown = text("markdown").bindTo { it.markdown }
val html = text("html").bindTo { it.html }
val userId = int("user_id").references(Users) { it.user }
val updatedAt = datetime("updated_at").bindTo { it.updatedAt }
val deleted = boolean("deleted").bindTo { it.deleted }
val public = boolean("public").bindTo { it.public }
val user get() = userId.referenceTable as Users
}
internal val Database.notes get() = this.sequenceOf(Notes, withReferences = false)
internal interface NoteEntity : Entity<NoteEntity> {
companion object : Entity.Factory<NoteEntity>()
var uuid: UUID
var title: String
var markdown: String
var html: String
var updatedAt: LocalDateTime
var deleted: Boolean
var public: Boolean
var user: UserEntity
}
-30
View File
@@ -1,30 +0,0 @@
package be.simplenotes.persistence.notes
import be.simplenotes.persistence.extensions.uuidBinary
import me.liuwj.ktorm.database.Database
import me.liuwj.ktorm.entity.Entity
import me.liuwj.ktorm.entity.sequenceOf
import me.liuwj.ktorm.schema.Table
import me.liuwj.ktorm.schema.int
import me.liuwj.ktorm.schema.varchar
internal open class Tags(alias: String?) : Table<TagEntity>("Tags", alias) {
companion object : Tags(null)
override fun aliased(alias: String) = Tags(alias)
val id = int("id").primaryKey().bindTo { it.id }
val name = varchar("name").bindTo { it.name }
val noteUuid = uuidBinary("note_uuid").references(Notes) { it.note }
val note get() = noteUuid.referenceTable as Notes
}
internal val Database.tags get() = this.sequenceOf(Tags, withReferences = false)
internal interface TagEntity : Entity<TagEntity> {
companion object : Entity.Factory<TagEntity>()
val id: Int
var name: String
var note: NoteEntity
}
@@ -13,7 +13,7 @@ interface NoteRepository {
limit: Int = 20, limit: Int = 20,
offset: Int = 0, offset: Int = 0,
tag: String? = null, tag: String? = null,
deleted: Boolean = false deleted: Boolean = false,
): List<PersistedNoteMetadata> ): List<PersistedNoteMetadata>
fun count(userId: Int, tag: String? = null, deleted: Boolean = false): Int fun count(userId: Int, tag: String? = null, deleted: Boolean = false): Int
@@ -26,6 +26,8 @@ interface NoteRepository {
fun create(userId: Int, note: Note): PersistedNote fun create(userId: Int, note: Note): PersistedNote
fun find(userId: Int, uuid: UUID): PersistedNote? fun find(userId: Int, uuid: UUID): PersistedNote?
fun update(userId: Int, uuid: UUID, note: Note): PersistedNote? fun update(userId: Int, uuid: UUID, note: Note): PersistedNote?
fun import(userId: Int, notes: List<ExportedNote>)
fun export(userId: Int): List<ExportedNote> fun export(userId: Int): List<ExportedNote>
fun findAllDetails(userId: Int): List<PersistedNote> fun findAllDetails(userId: Int): List<PersistedNote>
@@ -0,0 +1,186 @@
package be.simplenotes.persistence.repositories
import be.simplenotes.persistence.*
import be.simplenotes.persistence.converters.NoteConverter
import be.simplenotes.persistence.extensions.arrayContains
import be.simplenotes.types.ExportedNote
import be.simplenotes.types.Note
import be.simplenotes.types.PersistedNote
import org.ktorm.database.Database
import org.ktorm.dsl.*
import org.ktorm.entity.*
import java.time.LocalDateTime
import java.util.*
import jakarta.inject.Singleton
@Singleton
internal class NoteRepositoryImpl(
private val db: Database,
private val converter: NoteConverter,
) : NoteRepository {
override fun findAll(
userId: Int,
limit: Int,
offset: Int,
tag: String?,
deleted: Boolean,
) = db.noteWithTags
.filterColumns { listOf(it.uuid, it.title, it.updatedAt, it.tags) }
.filter { (it.userId eq userId) and (it.deleted eq deleted) }
.runIf(tag != null) { filter { it.tags.arrayContains(tag!!) } }
.sortedByDescending { it.updatedAt }
.take(limit)
.drop(offset)
.map { converter.toPersistedNoteMetadata(it)!! }
override fun exists(userId: Int, uuid: UUID) =
db.notes.any { (it.userId eq userId) and (it.uuid eq uuid) and (it.deleted eq false) }
override fun create(userId: Int, note: Note): PersistedNote = db.useTransaction {
val uuid = UUID.randomUUID()
val entity = converter.toEntity(note, uuid, userId, LocalDateTime.now())
db.notes.add(entity)
note.tags.takeIf { it.isNotEmpty() }?.run {
db.batchInsert(Tags) {
forEach { tagName ->
item {
set(it.noteUuid, uuid)
set(it.name, tagName)
}
}
}
}
return find(userId, uuid) ?: error("Note not found")
}
override fun find(userId: Int, uuid: UUID) = db.noteWithTags
.filterColumns { NotesWithTags.columns - NotesWithTags.userId - NotesWithTags.deleted }
.find { (it.uuid eq uuid) and (it.userId eq userId) and (it.deleted eq false) }
.let { converter.toPersistedNote(it) }
override fun update(userId: Int, uuid: UUID, note: Note): PersistedNote? = db.useTransaction {
val now = LocalDateTime.now()
val count = db.update(Notes) {
set(it.title, note.title)
set(it.markdown, note.markdown)
set(it.html, note.html)
set(it.updatedAt, now)
where { (it.uuid eq uuid) and (it.userId eq userId) and (it.deleted eq false) }
}
if (count == 0) return null
// delete all tags
db.delete(Tags) {
it.noteUuid eq uuid
}
// put new ones
note.tags.forEach { tagName ->
db.insert(Tags) {
set(it.name, tagName)
set(it.noteUuid, uuid)
}
}
return find(userId, uuid)
}
override fun delete(userId: Int, uuid: UUID, permanent: Boolean) = if (!permanent) {
db.update(Notes) {
set(it.deleted, true)
set(it.updatedAt, LocalDateTime.now())
where { it.userId eq userId and (it.uuid eq uuid) }
} == 1
} else db.delete(Notes) { it.uuid eq uuid and (it.userId eq userId) } == 1
override fun restore(userId: Int, uuid: UUID) = db.update(Notes) {
set(it.deleted, false)
where { (it.userId eq userId) and (it.uuid eq uuid) }
} == 1
override fun getTags(userId: Int): List<String> = db.from(Tags)
.leftJoin(Notes, on = Notes.uuid eq Tags.noteUuid)
.selectDistinct(Tags.name)
.where { (Notes.userId eq userId) and (Notes.deleted eq false) }
.map { it.getString(1)!! }
override fun count(userId: Int, tag: String?, deleted: Boolean) = db.from(Notes)
.runIf(tag != null) { leftJoin(Tags, on = Tags.noteUuid eq Notes.uuid) }
.select(count())
.whereWithConditions {
it += Notes.userId eq userId
it += Notes.deleted eq deleted
tag?.let { tag -> it += Tags.name eq tag }
}
.map { it.getInt(1) }
.first()
override fun import(userId: Int, notes: List<ExportedNote>) {
if (notes.isEmpty()) return
val notesByID = notes.associateBy { UUID.randomUUID() }
val tags = sequence<Pair<UUID, String>> {
notesByID.entries.forEach { (key, value) ->
value.tags.forEach { tag ->
yield(key to tag)
}
}
}.toList()
db.batchInsert(Notes) {
notesByID.forEach { (uuid, note) ->
item {
set(it.uuid, uuid)
set(it.userId, userId)
set(it.title, note.title)
set(it.html, note.html)
set(it.markdown, note.markdown)
set(it.updatedAt, note.updatedAt)
set(it.deleted, note.trash)
}
}
}
tags.takeIf { it.isNotEmpty() }?.run {
db.batchInsert(Tags) {
forEach { (uuid, tagName) ->
item {
set(it.noteUuid, uuid)
set(it.name, tagName)
}
}
}
}
}
override fun export(userId: Int) = db.noteWithTags
.filterColumns { it.columns - it.userId - it.public }
.filter { it.userId eq userId }
.sortedByDescending { it.updatedAt }
.map { converter.toExportedNote(it)!! }
override fun findAllDetails(userId: Int) = db.noteWithTags
.filterColumns { it.columns - it.userId - it.deleted }
.filter { (it.userId eq userId) and (it.deleted eq false) }
.map { converter.toPersistedNote(it)!! }
override fun makePublic(userId: Int, uuid: UUID) = db.update(Notes) {
set(it.public, true)
where { Notes.userId eq userId and (Notes.uuid eq uuid) and (it.deleted eq false) }
} == 1
override fun makePrivate(userId: Int, uuid: UUID) = db.update(Notes) {
set(it.public, false)
where { Notes.userId eq userId and (Notes.uuid eq uuid) and (it.deleted eq false) }
} == 1
override fun findPublic(uuid: UUID) = db.noteWithTags
.filterColumns { it.columns - it.userId - it.deleted }
.find { (it.uuid eq uuid) and (it.public eq true) and (it.deleted eq false) }
.let { converter.toPersistedNote(it) }
}
private inline fun <T> T.runIf(condition: Boolean, block: T.() -> T) = run { if (condition) block(this) else this }
@@ -0,0 +1,40 @@
package be.simplenotes.persistence.repositories
import be.simplenotes.persistence.Users
import be.simplenotes.persistence.converters.UserConverter
import be.simplenotes.persistence.users
import be.simplenotes.types.PersistedUser
import be.simplenotes.types.User
import org.ktorm.database.Database
import org.ktorm.dsl.*
import org.ktorm.entity.any
import org.ktorm.entity.find
import java.sql.SQLIntegrityConstraintViolationException
import jakarta.inject.Singleton
@Singleton
internal class UserRepositoryImpl(
private val db: Database,
private val converter: UserConverter,
) : UserRepository {
override fun create(user: User) = try {
val id = db.insertAndGenerateKey(Users) {
set(it.username, user.username)
set(it.password, user.password)
} as Int
PersistedUser(user.username, user.password, id)
} catch (e: SQLIntegrityConstraintViolationException) {
null
}
override fun find(username: String) = db.users.find { it.username eq username }
.let { converter.toPersistedUser(it) }
override fun find(id: Int) = db.users.find { it.id eq id }
.let { converter.toPersistedUser(it) }
override fun exists(username: String) = db.users.any { it.username eq username }
override fun exists(id: Int) = db.users.any { it.id eq id }
override fun delete(id: Int) = db.delete(Users) { it.id eq id } == 1
override fun findAll() = db.from(Users).select(Users.id).map { it.getInt(1) }
}
@@ -1,9 +0,0 @@
package be.simplenotes.persistence.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() }
}
@@ -1,5 +0,0 @@
package be.simplenotes.persistence.transactions
interface TransactionService {
fun <T> use(block: () -> T): T
}
@@ -1,42 +0,0 @@
package be.simplenotes.persistence.users
import be.simplenotes.persistence.converters.UserConverter
import be.simplenotes.persistence.repositories.UserRepository
import be.simplenotes.types.PersistedUser
import be.simplenotes.types.User
import me.liuwj.ktorm.database.Database
import me.liuwj.ktorm.dsl.*
import me.liuwj.ktorm.entity.any
import me.liuwj.ktorm.entity.find
import java.sql.SQLIntegrityConstraintViolationException
import javax.inject.Singleton
@Singleton
internal class UserRepositoryImpl(
private val db: Database,
private val converter: UserConverter,
) : UserRepository {
override fun create(user: User): PersistedUser? {
return try {
val id = db.insertAndGenerateKey(Users) {
it.username to user.username
it.password to user.password
} as Int
PersistedUser(user.username, user.password, id)
} catch (e: SQLIntegrityConstraintViolationException) {
null
}
}
override fun find(username: String) = db.users.find { it.username eq username }
?.let { converter.toPersistedUser(it) }
override fun find(id: Int) = db.users.find { it.id eq id }?.let {
converter.toPersistedUser(it)
}
override fun exists(username: String) = db.users.any { it.username eq username }
override fun exists(id: Int) = db.users.any { it.id eq id }
override fun delete(id: Int) = db.delete(Users) { it.id eq id } == 1
override fun findAll() = db.from(Users).select(Users.id).map { it[Users.id]!! }
}
-28
View File
@@ -1,28 +0,0 @@
package be.simplenotes.persistence.users
import me.liuwj.ktorm.database.Database
import me.liuwj.ktorm.entity.Entity
import me.liuwj.ktorm.entity.sequenceOf
import me.liuwj.ktorm.schema.Table
import me.liuwj.ktorm.schema.int
import me.liuwj.ktorm.schema.varchar
internal open class Users(alias: String?) : Table<UserEntity>("Users", alias) {
companion object : Users(null)
override fun aliased(alias: String) = Users(alias)
val id = int("id").primaryKey().bindTo { it.id }
val username = varchar("username").bindTo { it.username }
val password = varchar("password").bindTo { it.password }
}
internal interface UserEntity : Entity<UserEntity> {
companion object : Entity.Factory<UserEntity>()
var id: Int
var username: String
var password: String
}
internal val Database.users get() = this.sequenceOf(Users, withReferences = false)
@@ -1,8 +0,0 @@
package be.simplenotes.persistence.utils
import be.simplenotes.config.DataSourceConfig
enum class DbType { H2, MariaDb }
fun DataSourceConfig.type(): DbType = if (jdbcUrl.contains("mariadb")) DbType.MariaDb
else DbType.H2
-22
View File
@@ -1,22 +0,0 @@
package be.simplenotes.persistence
import be.simplenotes.config.DataSourceConfig
import org.testcontainers.containers.MariaDBContainer
class KMariadbContainer : MariaDBContainer<KMariadbContainer>("mariadb:10.5.5")
fun h2dataSourceConfig() = DataSourceConfig(
jdbcUrl = "jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1;",
username = "h2",
password = "",
maximumPoolSize = 2,
connectionTimeout = 3000
)
fun mariadbDataSourceConfig(jdbcUrl: String) = DataSourceConfig(
jdbcUrl = jdbcUrl,
username = "test",
password = "test",
maximumPoolSize = 2,
connectionTimeout = 3000
)

Some files were not shown because too many files have changed in this diff Show More