10 Commits

Author SHA1 Message Date
hubert 32337ec308 Use mapstruct 2020-10-21 22:02:34 +02:00
hubert 681fd635b3 Add health check 2020-10-21 16:30:42 +02:00
hubert 9467db2382 Use transactions at the http layer 2020-10-20 23:26:20 +02:00
hubert 7ed3494808 Clean pom common dependencies 2020-10-20 19:21:29 +02:00
hubert ceb310bf02 Fix favicon extension 2020-10-20 16:10:13 +02:00
hubert 2c3106c5c1 Clean homepage html 2020-10-20 15:56:24 +02:00
hubert 4effa8231a Change icons 2020-10-20 15:55:11 +02:00
hubert b78420e106 Update some dependencies 2020-10-17 03:39:44 +02:00
hubert dd08763161 We don't need that much RAM 2020-09-30 18:57:46 +02:00
hubert e0b1514965 Unload bootstraping modules after startup 2020-09-30 18:56:52 +02:00
50 changed files with 1016 additions and 440 deletions
+2
View File
@@ -32,6 +32,8 @@ RUN strip -p --strip-unneeded /myjdk/lib/server/libjvm.so
FROM alpine FROM alpine
RUN apk add --no-cache curl
ENV APPLICATION_USER simplenotes ENV APPLICATION_USER simplenotes
RUN adduser -D -g '' $APPLICATION_USER RUN adduser -D -g '' $APPLICATION_USER
+43 -7
View File
@@ -1,4 +1,6 @@
<project> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent> <parent>
<artifactId>parent</artifactId> <artifactId>parent</artifactId>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
@@ -9,7 +11,7 @@
<artifactId>app</artifactId> <artifactId>app</artifactId>
<properties> <properties>
<http4k.version>3.258.0</http4k.version> <http4k.version>3.268.0</http4k.version>
</properties> </properties>
<dependencies> <dependencies>
@@ -36,12 +38,16 @@
<dependency> <dependency>
<groupId>org.http4k</groupId> <groupId>org.http4k</groupId>
<artifactId>http4k-core</artifactId> <artifactId>http4k-core</artifactId>
<version>${http4k.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.http4k</groupId> <groupId>org.http4k</groupId>
<artifactId>http4k-server-jetty</artifactId> <artifactId>http4k-server-jetty</artifactId>
<version>${http4k.version}</version> <exclusions>
<exclusion>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>javax-websocket-server-impl</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.jetbrains.kotlinx</groupId> <groupId>org.jetbrains.kotlinx</groupId>
@@ -50,7 +56,7 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.jetbrains.kotlinx</groupId> <groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-serialization-runtime</artifactId> <artifactId>kotlinx-serialization-json-jvm</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.ocpsoft.prettytime</groupId> <groupId>org.ocpsoft.prettytime</groupId>
@@ -58,6 +64,21 @@
<version>4.0.5.Final</version> <version>4.0.5.Final</version>
</dependency> </dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
<artifactId>shared</artifactId> <artifactId>shared</artifactId>
@@ -68,17 +89,32 @@
<dependency> <dependency>
<groupId>org.http4k</groupId> <groupId>org.http4k</groupId>
<artifactId>http4k-testing-hamkrest</artifactId> <artifactId>http4k-testing-hamkrest</artifactId>
<version>${http4k.version}</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>me.liuwj.ktorm</groupId>
<artifactId>ktorm-core</artifactId>
</dependency>
</dependencies> </dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.http4k</groupId>
<artifactId>http4k-bom</artifactId>
<version>${http4k.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build> <build>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId> <artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions> <executions>
<execution> <execution>
<phase>package</phase> <phase>package</phase>
@@ -0,0 +1,12 @@
package be.simplenotes.app.controllers
import be.simplenotes.persistance.DbHealthCheck
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
class HealthCheckController(private val dbHealthCheck: DbHealthCheck) {
fun healthCheck(request: Request) =
if (dbHealthCheck.isOk()) Response(OK) else Response(SERVICE_UNAVAILABLE)
}
+2 -4
View File
@@ -19,9 +19,8 @@ class AuthFilter(
private val ctx: RequestContexts, private val ctx: RequestContexts,
private val source: JwtSource = JwtSource.Cookie, private val source: JwtSource = JwtSource.Cookie,
private val redirect: Boolean = true, private val redirect: Boolean = true,
) { ) : Filter {
operator fun invoke() = Filter { next -> override fun invoke(next: HttpHandler): HttpHandler = {
{
val token = when (source) { val token = when (source) {
JwtSource.Header -> it.bearerTokenHeader() JwtSource.Header -> it.bearerTokenHeader()
JwtSource.Cookie -> it.bearerTokenCookie() JwtSource.Cookie -> it.bearerTokenCookie()
@@ -40,7 +39,6 @@ class AuthFilter(
} }
} }
} }
}
fun Request.jwtPayload(ctx: RequestContexts): JwtPayload? = ctx[this][authKey] fun Request.jwtPayload(ctx: RequestContexts): JwtPayload? = ctx[this][authKey]
+25 -13
View File
@@ -2,32 +2,44 @@ package be.simplenotes.app.filters
import be.simplenotes.app.extensions.html import be.simplenotes.app.extensions.html
import be.simplenotes.app.views.ErrorView import be.simplenotes.app.views.ErrorView
import be.simplenotes.app.views.ErrorView.Type.*
import org.http4k.core.* import org.http4k.core.*
import org.http4k.core.Status.Companion.INTERNAL_SERVER_ERROR
import org.http4k.core.Status.Companion.NOT_FOUND
import org.http4k.core.Status.Companion.NOT_IMPLEMENTED
import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.sql.SQLTransientException import java.sql.SQLTransientException
class ErrorFilter(private val errorView: ErrorView) { class ErrorFilter(private val errorView: ErrorView) : Filter {
private val logger = LoggerFactory.getLogger(javaClass) private val logger = LoggerFactory.getLogger(javaClass)
operator fun invoke(): Filter = Filter { next -> private fun errorResponse(status: Status): Response {
{ val type = when (status) {
SERVICE_UNAVAILABLE -> SqlTransientError
NOT_FOUND -> NotFound
NOT_IMPLEMENTED -> Other
else -> Other
}
return Response(status).html(errorView.error(type)).noCache()
}
override fun invoke(next: HttpHandler): HttpHandler = { request ->
try { try {
val response = next(it) val response = next(request)
if (response.status == Status.NOT_FOUND) Response(Status.NOT_FOUND) if (response.status == NOT_FOUND) errorResponse(NOT_FOUND)
.html(errorView.error(ErrorView.Type.NotFound))
else response else response
} catch (e: SQLTransientException) {
logger.error(e.stackTraceToString())
errorResponse(SERVICE_UNAVAILABLE)
} catch (e: Exception) { } catch (e: Exception) {
logger.error(e.stackTraceToString()) logger.error(e.stackTraceToString())
if (e is SQLTransientException) errorResponse(INTERNAL_SERVER_ERROR)
Response(Status.SERVICE_UNAVAILABLE).html(errorView.error(ErrorView.Type.SqlTransientError))
.noCache()
else
Response(Status.INTERNAL_SERVER_ERROR).html(errorView.error(ErrorView.Type.Other)).noCache()
} catch (e: NotImplementedError) { } catch (e: NotImplementedError) {
logger.error(e.stackTraceToString()) logger.error(e.stackTraceToString())
Response(Status.NOT_IMPLEMENTED).html(errorView.error(ErrorView.Type.Other)).noCache() errorResponse(NOT_IMPLEMENTED)
}
} }
} }
} }
@@ -2,13 +2,10 @@ package be.simplenotes.app.filters
import org.http4k.core.Filter import org.http4k.core.Filter
import org.http4k.core.HttpHandler import org.http4k.core.HttpHandler
import org.http4k.core.Method
import org.http4k.core.Request import org.http4k.core.Request
object ImmutableFilter { object ImmutableFilter : Filter {
operator fun invoke() = Filter { next: HttpHandler -> override fun invoke(next: HttpHandler) = { request: Request ->
{ request: Request ->
next(request).header("Cache-Control", "public, max-age=31536000, immutable") next(request).header("Cache-Control", "public, max-age=31536000, immutable")
} }
} }
}
@@ -4,9 +4,8 @@ import org.http4k.core.Filter
import org.http4k.core.HttpHandler import org.http4k.core.HttpHandler
import org.http4k.core.Request import org.http4k.core.Request
object SecurityFilter { object SecurityFilter : Filter {
operator fun invoke() = Filter { next: HttpHandler -> override fun invoke(next: HttpHandler): HttpHandler = { request: Request ->
{ request: Request ->
val response = next(request) val response = next(request)
.header("X-Content-Type-Options", "nosniff") .header("X-Content-Type-Options", "nosniff")
@@ -17,4 +16,3 @@ object SecurityFilter {
} else response } else response
} }
} }
}
@@ -0,0 +1,13 @@
package be.simplenotes.app.filters
import me.liuwj.ktorm.database.Database
import org.http4k.core.Filter
import org.http4k.core.HttpHandler
class TransactionFilter(private val db: Database) : Filter {
override fun invoke(next: HttpHandler): HttpHandler = { request ->
db.useTransaction {
next(request)
}
}
}
+3 -2
View File
@@ -5,19 +5,20 @@ import be.simplenotes.app.api.ApiUserController
import be.simplenotes.app.filters.AuthFilter import be.simplenotes.app.filters.AuthFilter
import be.simplenotes.app.filters.AuthType import be.simplenotes.app.filters.AuthType
import be.simplenotes.app.filters.JwtSource import be.simplenotes.app.filters.JwtSource
import org.http4k.core.Filter
import org.koin.core.qualifier.named import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
val apiModule = module { val apiModule = module {
single { ApiUserController(get(), get()) } single { ApiUserController(get(), get()) }
single { ApiNoteController(get(), get()) } single { ApiNoteController(get(), get()) }
single(named("apiAuthFilter")) { single<Filter>(named("apiAuthFilter")) {
AuthFilter( AuthFilter(
extractor = get(), extractor = get(),
authType = AuthType.Required, authType = AuthType.Required,
ctx = get(), ctx = get(),
source = JwtSource.Header, source = JwtSource.Header,
redirect = false redirect = false
)() )
} }
} }
+1 -8
View File
@@ -2,16 +2,9 @@ package be.simplenotes.app.modules
import be.simplenotes.app.Config import be.simplenotes.app.Config
import org.koin.dsl.module import org.koin.dsl.module
import org.koin.dsl.onClose
val configModule = module { val configModule = module {
single { Config() } onClose { single { Config() }
println("Unloaded config")
println("Unloaded config")
println("Unloaded config")
println("Unloaded config")
}
single { get<Config>().dataSourceConfig } single { get<Config>().dataSourceConfig }
single { get<Config>().jwtConfig } single { get<Config>().jwtConfig }
single { get<Config>().serverConfig } single { get<Config>().serverConfig }
+2 -4
View File
@@ -1,9 +1,6 @@
package be.simplenotes.app.modules package be.simplenotes.app.modules
import be.simplenotes.app.controllers.BaseController import be.simplenotes.app.controllers.*
import be.simplenotes.app.controllers.NoteController
import be.simplenotes.app.controllers.SettingsController
import be.simplenotes.app.controllers.UserController
import be.simplenotes.app.views.BaseView import be.simplenotes.app.views.BaseView
import be.simplenotes.app.views.NoteView import be.simplenotes.app.views.NoteView
import be.simplenotes.app.views.SettingView import be.simplenotes.app.views.SettingView
@@ -16,6 +13,7 @@ val userModule = module {
} }
val baseModule = module { val baseModule = module {
single { HealthCheckController(get()) }
single { BaseController(get()) } single { BaseController(get()) }
single { BaseView(get()) } single { BaseView(get()) }
} }
+10 -5
View File
@@ -4,12 +4,14 @@ import be.simplenotes.app.Server
import be.simplenotes.app.filters.AuthFilter import be.simplenotes.app.filters.AuthFilter
import be.simplenotes.app.filters.AuthType import be.simplenotes.app.filters.AuthType
import be.simplenotes.app.filters.ErrorFilter import be.simplenotes.app.filters.ErrorFilter
import be.simplenotes.app.filters.TransactionFilter
import be.simplenotes.app.routes.Router import be.simplenotes.app.routes.Router
import be.simplenotes.app.utils.StaticFileResolver import be.simplenotes.app.utils.StaticFileResolver
import be.simplenotes.app.utils.StaticFileResolverImpl import be.simplenotes.app.utils.StaticFileResolverImpl
import be.simplenotes.app.views.ErrorView import be.simplenotes.app.views.ErrorView
import be.simplenotes.shared.config.ServerConfig import be.simplenotes.shared.config.ServerConfig
import org.eclipse.jetty.server.ServerConnector import org.eclipse.jetty.server.ServerConnector
import org.http4k.core.Filter
import org.http4k.core.RequestContexts import org.http4k.core.RequestContexts
import org.http4k.routing.RoutingHttpHandler import org.http4k.routing.RoutingHttpHandler
import org.http4k.server.ConnectorBuilder import org.http4k.server.ConnectorBuilder
@@ -43,16 +45,19 @@ val serverModule = module {
get(), get(),
get(), get(),
get(), get(),
get(),
requiredAuth = get(AuthType.Required.qualifier), requiredAuth = get(AuthType.Required.qualifier),
optionalAuth = get(AuthType.Optional.qualifier), optionalAuth = get(AuthType.Optional.qualifier),
errorFilter = get(named("ErrorFilter")),
apiAuth = get(named("apiAuthFilter")), apiAuth = get(named("apiAuthFilter")),
get() get(),
get(),
get(),
)() )()
} }
single { RequestContexts() } single { RequestContexts() }
single(AuthType.Optional.qualifier) { AuthFilter(get(), AuthType.Optional, get())() } single<Filter>(AuthType.Optional.qualifier) { AuthFilter(get(), AuthType.Optional, get()) }
single(AuthType.Required.qualifier) { AuthFilter(get(), AuthType.Required, get())() } single<Filter>(AuthType.Required.qualifier) { AuthFilter(get(), AuthType.Required, get()) }
single(named("ErrorFilter")) { ErrorFilter(get())() } single { ErrorFilter(get()) }
single { TransactionFilter(get()) }
single { ErrorView(get()) } single { ErrorView(get()) }
} }
+35 -27
View File
@@ -2,19 +2,15 @@ package be.simplenotes.app.routes
import be.simplenotes.app.api.ApiNoteController import be.simplenotes.app.api.ApiNoteController
import be.simplenotes.app.api.ApiUserController import be.simplenotes.app.api.ApiUserController
import be.simplenotes.app.controllers.BaseController import be.simplenotes.app.controllers.*
import be.simplenotes.app.controllers.NoteController import be.simplenotes.app.filters.*
import be.simplenotes.app.controllers.SettingsController
import be.simplenotes.app.controllers.UserController
import be.simplenotes.app.filters.ImmutableFilter
import be.simplenotes.app.filters.SecurityFilter
import be.simplenotes.app.filters.jwtPayload
import be.simplenotes.domain.security.JwtPayload import be.simplenotes.domain.security.JwtPayload
import org.http4k.core.* import org.http4k.core.*
import org.http4k.core.Method.* import org.http4k.core.Method.*
import org.http4k.filter.ResponseFilters import org.http4k.filter.ResponseFilters.GZip
import org.http4k.filter.ServerFilters.InitialiseRequestContext import org.http4k.filter.ServerFilters.InitialiseRequestContext
import org.http4k.routing.* import org.http4k.routing.*
import org.http4k.routing.ResourceLoader.Companion.Classpath
class Router( class Router(
private val baseController: BaseController, private val baseController: BaseController,
@@ -23,26 +19,26 @@ class Router(
private val settingsController: SettingsController, private val settingsController: SettingsController,
private val apiUserController: ApiUserController, private val apiUserController: ApiUserController,
private val apiNoteController: ApiNoteController, private val apiNoteController: ApiNoteController,
private val healthCheckController: HealthCheckController,
private val requiredAuth: Filter, private val requiredAuth: Filter,
private val optionalAuth: Filter, private val optionalAuth: Filter,
private val errorFilter: Filter,
private val apiAuth: Filter, private val apiAuth: Filter,
private val errorFilter: ErrorFilter,
private val transactionFilter: TransactionFilter,
private val contexts: RequestContexts, private val contexts: RequestContexts,
) { ) {
operator fun invoke(): RoutingHttpHandler { operator fun invoke(): RoutingHttpHandler {
val resourceLoader = ResourceLoader.Classpath(("/static")) val basicRoutes =
val basicRoutes = routes( routes(
ImmutableFilter().then(static(resourceLoader, "woff2" to ContentType("font/woff2"))), "/health" bind GET to healthCheckController::healthCheck,
ImmutableFilter.then(static(Classpath("/static"), "woff2" to ContentType("font/woff2")))
) )
infix fun PathMethod.public(handler: PublicHandler) = this to { handler(it, it.jwtPayload(contexts)) } val publicRoutes = routes(
infix fun PathMethod.protected(handler: ProtectedHandler) = this to { handler(it, it.jwtPayload(contexts)!!) }
val publicRoutes: RoutingHttpHandler = routes(
"/" bind GET public baseController::index, "/" bind GET public baseController::index,
"/register" bind GET public userController::register, "/register" bind GET public userController::register,
"/register" bind POST public userController::register, "/register" bind POST `public transactional` userController::register,
"/login" bind GET public userController::login, "/login" bind GET public userController::login,
"/login" bind POST public userController::login, "/login" bind POST public userController::login,
"/logout" bind POST to userController::logout, "/logout" bind POST to userController::logout,
@@ -51,18 +47,18 @@ class Router(
val protectedRoutes = routes( val protectedRoutes = routes(
"/settings" bind GET protected settingsController::settings, "/settings" bind GET protected settingsController::settings,
"/settings" bind POST protected settingsController::settings, "/settings" bind POST transactional settingsController::settings,
"/export" bind POST protected settingsController::export, "/export" bind POST protected settingsController::export,
"/notes" bind GET protected noteController::list, "/notes" bind GET protected noteController::list,
"/notes" bind POST protected noteController::search, "/notes" bind POST protected noteController::search,
"/notes/new" bind GET protected noteController::new, "/notes/new" bind GET protected noteController::new,
"/notes/new" bind POST protected noteController::new, "/notes/new" bind POST transactional noteController::new,
"/notes/trash" bind GET protected noteController::trash, "/notes/trash" bind GET protected noteController::trash,
"/notes/{uuid}" bind GET protected noteController::note, "/notes/{uuid}" bind GET protected noteController::note,
"/notes/{uuid}" bind POST protected noteController::note, "/notes/{uuid}" bind POST transactional noteController::note,
"/notes/{uuid}/edit" bind GET protected noteController::edit, "/notes/{uuid}/edit" bind GET protected noteController::edit,
"/notes/{uuid}/edit" bind POST protected noteController::edit, "/notes/{uuid}/edit" bind POST transactional noteController::edit,
"/notes/deleted/{uuid}" bind POST protected noteController::deleted, "/notes/deleted/{uuid}" bind POST transactional noteController::deleted,
) )
val apiRoutes = routes( val apiRoutes = routes(
@@ -71,10 +67,10 @@ class Router(
val protectedApiRoutes = routes( val protectedApiRoutes = routes(
"/api/notes" bind GET protected apiNoteController::notes, "/api/notes" bind GET protected apiNoteController::notes,
"/api/notes" bind POST protected apiNoteController::createNote, "/api/notes" bind POST transactional apiNoteController::createNote,
"/api/notes/search" bind POST protected apiNoteController::search, "/api/notes/search" bind POST transactional apiNoteController::search,
"/api/notes/{uuid}" bind GET protected apiNoteController::note, "/api/notes/{uuid}" bind GET protected apiNoteController::note,
"/api/notes/{uuid}" bind PUT protected apiNoteController::update, "/api/notes/{uuid}" bind PUT transactional apiNoteController::update,
) )
val routes = routes( val routes = routes(
@@ -87,11 +83,23 @@ class Router(
val globalFilters = errorFilter val globalFilters = errorFilter
.then(InitialiseRequestContext(contexts)) .then(InitialiseRequestContext(contexts))
.then(SecurityFilter()) .then(SecurityFilter)
.then(ResponseFilters.GZip()) .then(GZip())
return globalFilters.then(routes) return globalFilters.then(routes)
} }
private inline infix fun PathMethod.public(crossinline handler: PublicHandler) =
this to { handler(it, it.jwtPayload(contexts)) }
private inline infix fun PathMethod.protected(crossinline handler: ProtectedHandler) =
this to { handler(it, it.jwtPayload(contexts)!!) }
private inline infix fun PathMethod.transactional(crossinline handler: ProtectedHandler) =
this to transactionFilter.then { handler(it, it.jwtPayload(contexts)!!) }
private inline infix fun PathMethod.`public transactional`(crossinline handler: PublicHandler) =
this to transactionFilter.then { handler(it, it.jwtPayload(contexts)) }
} }
private typealias PublicHandler = (Request, JwtPayload?) -> Response private typealias PublicHandler = (Request, JwtPayload?) -> Response
+63 -70
View File
@@ -3,8 +3,7 @@ package be.simplenotes.app.views
import be.simplenotes.app.utils.StaticFileResolver import be.simplenotes.app.utils.StaticFileResolver
import be.simplenotes.domain.security.JwtPayload import be.simplenotes.domain.security.JwtPayload
import kotlinx.html.* import kotlinx.html.*
import kotlinx.html.div import kotlinx.html.ThScope.col
import org.intellij.lang.annotations.Language
class BaseView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) { class BaseView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) {
fun renderHome(jwtPayload: JwtPayload?) = renderPage( fun renderHome(jwtPayload: JwtPayload?) = renderPage(
@@ -21,74 +20,70 @@ class BaseView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
div("container mx-auto flex flex-wrap justify-center content-center") { div("container mx-auto flex flex-wrap justify-center content-center") {
unsafe { div("md:order-1 order-2 flipped p-4 my-10 w-full md:w-1/2") {
@Language("html") attributes["aria-label"] = "demo"
val html = div("flex justify-between mb-4") {
""" h1("text-2xl underline") { +"Notes" }
<div aria-label="demo" class="md:order-1 order-2 flipped p-4 my-10 w-full md:w-1/2"> span {
<div class="flex justify-between mb-4"> span("btn btn-teal pointer-events-none") { +"Trash (3)" }
<h1 class="text-2xl underline">Notes</h1> span("ml-2 btn btn-green pointer-events-none") { +"New" }
<span> }
<span class="btn btn-teal pointer-events-none">Trash (3)</span> }
<span class="ml-2 btn btn-green pointer-events-none">New</span> form(classes = "md:space-x-2") {
</span> id = "search"
</div> input {
<form class="md:space-x-2" id="search"> attributes["aria-label"] = "demo-search"
<input aria-label="demo-search" name="search" disabled="" value="tag:&quot;demo&quot;"> attributes["name"] = "search"
<span id="buttons"> attributes["disabled"] = ""
<button type="button" disabled="" class="btn btn-green pointer-events-none">search</button> attributes["value"] = "tag:\"demo\""
<span class="btn btn-red pointer-events-none">clear</span> }
</span> span {
</form> id = "buttons"
<div class="overflow-x-auto"> button(type = ButtonType.button, classes = "btn btn-green pointer-events-none") {
<table id="notes"> attributes["disabled"] = ""
<thead> +"search"
<tr> }
<th scope="col" class="w-1/2">Title</th> span("btn btn-red pointer-events-none") { +"clear" }
<th scope="col" class="w-1/4">Updated</th> }
<th scope="col" class="w-1/4">Tags</th> }
</tr> div("overflow-x-auto") {
</thead> demoTable()
<tbody> }
<tr> }
<td><span class="text-blue-200 font-semibold underline">Formula 1</span></td> welcome()
<td class="text-center">moments ago</td> }
<td>
<ul class="inline flex flex-wrap justify-center">
<li class="mx-2 my-1"><span class="tag disabled">#demo</span ></li>
</ul>
</td>
</tr>
<tr>
<td><span class="text-blue-200 font-semibold underline">Syntax highlighting</span></td>
<td class="text-center">2 hours ago</td>
<td>
<ul class="inline flex flex-wrap justify-center">
<li class="mx-2 my-1"><span class="tag disabled">#features</span></li>
<li class="mx-2 my-1"><span class="tag disabled">#demo</span></li>
</ul>
</td>
</tr>
<tr>
<td><span class="text-blue-200 font-semibold underline">report</span></td>
<td class="text-center">5 days ago</td>
<td>
<ul class="inline flex flex-wrap justify-center">
<li class="mx-2 my-1"><span class="tag disabled">#study</span></li>
<li class="mx-2 my-1"><span class="tag disabled">#demo</span></li>
</ul>
</td>
</tr>
</tbody>
</table>
</div>
</div>
""".trimIndent()
+html
} }
welcome() @Suppress("NOTHING_TO_INLINE")
private inline fun DIV.demoTable() {
table {
id = "notes"
thead {
tr {
th(scope = col, classes = "w-1/2") { +"Title" }
th(scope = col, classes = "w-1/4") { +"Updated" }
th(scope = col, classes = "w-1/4") { +"Tags" }
}
}
tbody {
listOf(
Triple("Formula 1", "moments ago", arrayOf("#demo")),
Triple("Syntax highlighting", "2 hours ago", arrayOf("#features", "#demo")),
Triple("report", "5 days ago", arrayOf("#study", "#demo")),
).forEach { (title, ago, tags) ->
tr {
td { span("text-blue-200 font-semibold underline") { +title } }
td("text-center") { +ago }
td {
ul("inline flex flex-wrap justify-center") {
tags.forEach { tag ->
li("mx-2 my-1") { span("tag disabled") { +tag } }
}
}
}
}
}
}
} }
} }
@@ -96,7 +91,6 @@ class BaseView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
private inline fun DIV.welcome() { private inline fun DIV.welcome() {
div("w-full my-auto md:w-1/2 md:order-2 order-1 text-center") { div("w-full my-auto md:w-1/2 md:order-2 order-1 text-center") {
div("m-4 rounded-lg p-6") { div("m-4 rounded-lg p-6") {
p("text-teal-400") {
h2("text-3xl text-teal-400 underline") { +"Features:" } h2("text-3xl text-teal-400 underline") { +"Features:" }
ul("list-disc text-lg list-inside") { ul("list-disc text-lg list-inside") {
li { +"Markdown support" } li { +"Markdown support" }
@@ -112,4 +106,3 @@ class BaseView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
} }
} }
} }
}
+12 -1
View File
@@ -31,7 +31,7 @@ abstract class View(staticFileResolver: StaticFileResolver) {
attributes["crossorigin"] = "anonymous" attributes["crossorigin"] = "anonymous"
} }
link(rel = "stylesheet", href = styles) link(rel = "stylesheet", href = styles)
link(rel = "shortcut icon", href = "/favicon.ico", type = "image/x-icon") icons()
scripts.forEach { src -> scripts.forEach { src ->
script(src = src) {} script(src = src) {}
} }
@@ -42,4 +42,15 @@ abstract class View(staticFileResolver: StaticFileResolver) {
} }
} }
} }
@Suppress("NOTHING_TO_INLINE")
private inline fun HEAD.icons() {
link(rel = "apple-touch-icon", href = "/apple-touch-icon.png") { attributes["sizes"] = "180x180" }
link(rel = "icon", href = "/favicon-32x32.png", type = "image/png") { attributes["sizes"] = "32x32" }
link(rel = "icon", href = "/favicon-16x16.png", type = "image/png") { attributes["sizes"] = "16x16" }
link(rel = "manifest", href = "/site.webmanifest")
link(rel = "mask-icon", href = "/safari-pinned-tab.svg") { attributes["color"] = "#2c7a7b" }
meta(name = "msapplication-TileColor", content = "#00aba9")
meta(name = "theme-color", content = "#2c7a7b")
}
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#00aba9</TileColor>
</tile>
</msapplication>
</browserconfig>
Binary file not shown.

After

Width:  |  Height:  |  Size: 814 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,33 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1255 6993 c-179 -23 -313 -62 -461 -133 -396 -187 -665 -533 -766
-981 l-23 -104 0 -2275 0 -2275 23 -102 c125 -565 530 -970 1095 -1095 l102
-23 2275 0 2275 0 102 23 c565 125 970 530 1095 1095 l23 102 0 2275 0 2275
-23 102 c-125 566 -521 964 -1090 1095 l-97 22 -2250 2 c-1237 1 -2263 -1
-2280 -3z m1024 -1979 c128 -18 287 -70 394 -127 262 -139 448 -395 472 -649
3 -34 8 -78 11 -95 l5 -33 -255 0 -254 0 -6 28 c-2 15 -7 44 -11 65 -27 168
-204 335 -416 393 -94 26 -317 24 -421 -4 -218 -59 -345 -196 -356 -384 -6
-105 16 -173 80 -243 81 -88 227 -148 563 -230 509 -123 742 -228 916 -412
123 -131 170 -248 176 -448 9 -245 -55 -420 -212 -580 -162 -165 -387 -270
-667 -311 -154 -22 -461 -15 -595 15 -280 62 -513 193 -662 373 -72 85 -162
262 -185 358 -8 36 -18 95 -22 133 l-6 67 254 0 255 0 6 -53 c22 -179 135
-332 310 -417 127 -61 195 -74 387 -75 195 0 279 16 399 76 179 89 260 234
231 415 -22 137 -98 231 -243 302 -109 55 -202 84 -447 142 -237 57 -306 76
-427 120 -340 125 -535 303 -600 550 -23 87 -23 271 0 360 87 335 409 595 827
665 108 19 371 18 499 -1z m3170 0 c202 -35 325 -95 453 -224 77 -77 93 -100
141 -201 30 -63 63 -143 72 -179 49 -183 48 -159 52 -1307 l4 -1083 -256 0
-255 0 0 1034 c0 1160 1 1133 -70 1282 -92 192 -259 271 -548 262 -117 -4
-149 -9 -221 -33 -177 -60 -340 -200 -440 -376 l-31 -56 0 -1056 0 -1057 -255
0 -255 0 0 1480 0 1480 239 0 238 0 6 -82 c4 -46 7 -120 7 -165 0 -45 3 -88 6
-97 4 -11 28 8 92 72 163 164 356 267 575 307 103 19 334 18 446 -1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,19 @@
{
"name": "SimpleNotes",
"short_name": "SimpleNotes",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
@@ -27,8 +27,8 @@ internal class AuthFilterTest {
private val simpleJwt = SimpleJwt(jwtConfig) private val simpleJwt = SimpleJwt(jwtConfig)
private val extractor = JwtPayloadExtractor(simpleJwt) private val extractor = JwtPayloadExtractor(simpleJwt)
private val ctx = RequestContexts() private val ctx = RequestContexts()
private val requiredAuth = AuthFilter(extractor, AuthType.Required, ctx)() private val requiredAuth = AuthFilter(extractor, AuthType.Required, ctx)
private val optionalAuth = AuthFilter(extractor, AuthType.Optional, ctx)() private val optionalAuth = AuthFilter(extractor, AuthType.Optional, ctx)
private val echoJwtPayloadHandler = { request: Request -> Response(OK).body(request.jwtPayload(ctx).toString()) } private val echoJwtPayloadHandler = { request: Request -> Response(OK).body(request.jwtPayload(ctx).toString()) }
+5 -1
View File
@@ -11,7 +11,11 @@ simplenotes.be {
import strict-transport import strict-transport
header -Server header -Server
reverse_proxy http://localhost:8080 reverse_proxy http://localhost:8080 {
health_path /health
health_interval 5s
health_timeout 200ms
}
} }
dev.simplenotes.be { dev.simplenotes.be {
+10 -2
View File
@@ -19,8 +19,10 @@ services:
volumes: volumes:
- notes-db-volume:/var/lib/mysql - notes-db-volume:/var/lib/mysql
healthcheck: healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"] test: "mysql --protocol=tcp -u simplenotes -p$MYSQL_PASSWORD -e 'show databases'"
timeout: 10s interval: 5s
timeout: 1s
start_period: 2s
retries: 10 retries: 10
simplenotes: simplenotes:
@@ -39,6 +41,12 @@ services:
# - PASSWORD # - PASSWORD
ports: ports:
- 127.0.0.1:8080:8080 - 127.0.0.1:8080:8080
healthcheck:
test: "curl --fail -s http://localhost:8080/health"
interval: 5s
timeout: 1s
start_period: 2s
retries: 3
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
+28 -2
View File
@@ -1,4 +1,6 @@
<project> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent> <parent>
<artifactId>parent</artifactId> <artifactId>parent</artifactId>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
@@ -22,6 +24,30 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>io.arrow-kt</groupId>
<artifactId>arrow-core</artifactId>
</dependency>
<dependency>
<groupId>org.koin</groupId>
<artifactId>koin-core</artifactId>
</dependency>
<dependency>
<groupId>com.natpryce</groupId>
<artifactId>hamkrest</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>io.konform</groupId> <groupId>io.konform</groupId>
<artifactId>konform-jvm</artifactId> <artifactId>konform-jvm</artifactId>
@@ -59,7 +85,7 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.jetbrains.kotlinx</groupId> <groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-serialization-runtime</artifactId> <artifactId>kotlinx-serialization-json-jvm</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
+18 -3
View File
@@ -1,4 +1,6 @@
<project> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent> <parent>
<artifactId>parent</artifactId> <artifactId>parent</artifactId>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
@@ -9,6 +11,10 @@
<artifactId>persistance</artifactId> <artifactId>persistance</artifactId>
<dependencies> <dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
</dependency>
<dependency> <dependency>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
<artifactId>domain</artifactId> <artifactId>domain</artifactId>
@@ -27,6 +33,17 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.mariadb.jdbc</groupId> <groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId> <artifactId>mariadb-java-client</artifactId>
@@ -50,12 +67,10 @@
<dependency> <dependency>
<groupId>me.liuwj.ktorm</groupId> <groupId>me.liuwj.ktorm</groupId>
<artifactId>ktorm-core</artifactId> <artifactId>ktorm-core</artifactId>
<version>3.0.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>me.liuwj.ktorm</groupId> <groupId>me.liuwj.ktorm</groupId>
<artifactId>ktorm-support-mysql</artifactId> <artifactId>ktorm-support-mysql</artifactId>
<version>3.0.0</version>
</dependency> </dependency>
</dependencies> </dependencies>
@@ -0,0 +1,28 @@
package be.simplenotes.persistance
import be.simplenotes.persistance.utils.DbType
import be.simplenotes.persistance.utils.type
import be.simplenotes.shared.config.DataSourceConfig
import me.liuwj.ktorm.database.Database
import me.liuwj.ktorm.database.asIterable
import java.sql.SQLTransientException
interface DbHealthCheck {
fun isOk(): Boolean
}
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: SQLTransientException) {
false
}
}
@@ -1,18 +1,24 @@
package be.simplenotes.persistance package be.simplenotes.persistance
import be.simplenotes.persistance.utils.DbType
import be.simplenotes.persistance.utils.type
import be.simplenotes.shared.config.DataSourceConfig import be.simplenotes.shared.config.DataSourceConfig
import org.flywaydb.core.Flyway import org.flywaydb.core.Flyway
import javax.sql.DataSource import javax.sql.DataSource
interface DbMigrations {
fun migrate()
}
internal class DbMigrationsImpl( internal class DbMigrationsImpl(
private val dataSource: DataSource, private val dataSource: DataSource,
private val dataSourceConfig: DataSourceConfig private val dataSourceConfig: DataSourceConfig,
) : DbMigrations { ) : DbMigrations {
override fun migrate() { override fun migrate() {
val migrationDir = when { val migrationDir = when (dataSourceConfig.type()) {
dataSourceConfig.jdbcUrl.contains("mariadb") -> "db/migration/mariadb" DbType.H2 -> "db/migration/other"
else -> "db/migration/other" DbType.MariaDb -> "db/migration/mariadb"
} }
Flyway.configure() Flyway.configure()
@@ -2,6 +2,10 @@ package be.simplenotes.persistance
import be.simplenotes.domain.usecases.repositories.NoteRepository import be.simplenotes.domain.usecases.repositories.NoteRepository
import be.simplenotes.domain.usecases.repositories.UserRepository import be.simplenotes.domain.usecases.repositories.UserRepository
import be.simplenotes.persistance.converters.NoteConverter
import be.simplenotes.persistance.converters.NoteConverterImpl
import be.simplenotes.persistance.converters.UserConverter
import be.simplenotes.persistance.converters.UserConverterImpl
import be.simplenotes.persistance.notes.NoteRepositoryImpl import be.simplenotes.persistance.notes.NoteRepositoryImpl
import be.simplenotes.persistance.users.UserRepositoryImpl import be.simplenotes.persistance.users.UserRepositoryImpl
import be.simplenotes.shared.config.DataSourceConfig import be.simplenotes.shared.config.DataSourceConfig
@@ -11,12 +15,9 @@ import me.liuwj.ktorm.database.Database
import org.koin.dsl.bind import org.koin.dsl.bind
import org.koin.dsl.module import org.koin.dsl.module
import org.koin.dsl.onClose import org.koin.dsl.onClose
import org.mapstruct.factory.Mappers
import javax.sql.DataSource import javax.sql.DataSource
interface DbMigrations {
fun migrate()
}
private fun hikariDataSource(conf: DataSourceConfig): HikariDataSource { private fun hikariDataSource(conf: DataSourceConfig): HikariDataSource {
val hikariConfig = HikariConfig().also { val hikariConfig = HikariConfig().also {
it.jdbcUrl = conf.jdbcUrl it.jdbcUrl = conf.jdbcUrl
@@ -34,11 +35,14 @@ val migrationModule = module {
} }
val persistanceModule = module { val persistanceModule = module {
single<UserRepository> { UserRepositoryImpl(get()) } single<NoteConverter> { NoteConverterImpl() }
single<NoteRepository> { NoteRepositoryImpl(get()) } single<UserConverter> { UserConverterImpl() }
single<UserRepository> { UserRepositoryImpl(get(), get()) }
single<NoteRepository> { NoteRepositoryImpl(get(), get()) }
single { hikariDataSource(get()) } bind DataSource::class onClose { it?.close() } single { hikariDataSource(get()) } bind DataSource::class onClose { it?.close() }
single { single {
get<DbMigrations>().migrate() get<DbMigrations>().migrate()
Database.connect(get<DataSource>()) Database.connect(get<DataSource>())
} }
single<DbHealthCheck> { DbHealthCheckImpl(get(), get()) }
} }
@@ -0,0 +1,80 @@
package be.simplenotes.persistance.converters
import be.simplenotes.domain.model.*
import be.simplenotes.persistance.notes.NoteEntity
import me.liuwj.ktorm.entity.Entity
import org.mapstruct.Mapper
import org.mapstruct.Mapping
import org.mapstruct.Mappings
import java.time.LocalDateTime
import java.util.*
/**
* This is an abstract class because kotlin default methods in interface are not seen as default in kapt
* @see [KT-25960](https://youtrack.jetbrains.com/issue/KT-25960)
*/
@Mapper(uses = [NoteEntityFactory::class, UserEntityFactory::class])
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>
internal class NoteEntityFactory : Entity.Factory<NoteEntity>()
@@ -0,0 +1,16 @@
package be.simplenotes.persistance.converters
import be.simplenotes.domain.model.PersistedUser
import be.simplenotes.domain.model.User
import be.simplenotes.persistance.users.UserEntity
import me.liuwj.ktorm.entity.Entity
import org.mapstruct.Mapper
@Mapper(uses = [UserEntityFactory::class])
internal interface UserConverter {
fun convertToUser(userEntity: UserEntity): User
fun convertToPersistedUser(userEntity: UserEntity): PersistedUser
fun convertToEntity(user: User): UserEntity
}
internal class UserEntityFactory : Entity.Factory<UserEntity>()
@@ -1,7 +1,11 @@
package be.simplenotes.persistance.notes package be.simplenotes.persistance.notes
import be.simplenotes.domain.model.* import be.simplenotes.domain.model.ExportedNote
import be.simplenotes.domain.model.Note
import be.simplenotes.domain.model.PersistedNote
import be.simplenotes.domain.model.PersistedNoteMetadata
import be.simplenotes.domain.usecases.repositories.NoteRepository import be.simplenotes.domain.usecases.repositories.NoteRepository
import be.simplenotes.persistance.converters.NoteConverter
import me.liuwj.ktorm.database.Database import me.liuwj.ktorm.database.Database
import me.liuwj.ktorm.dsl.* import me.liuwj.ktorm.dsl.*
import me.liuwj.ktorm.entity.* import me.liuwj.ktorm.entity.*
@@ -9,7 +13,7 @@ import java.time.LocalDateTime
import java.util.* import java.util.*
import kotlin.collections.HashMap import kotlin.collections.HashMap
internal class NoteRepositoryImpl(private val db: Database) : NoteRepository { internal class NoteRepositoryImpl(private val db: Database, private val converter: NoteConverter) : NoteRepository {
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)
override fun findAll( override fun findAll(
@@ -17,7 +21,7 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
limit: Int, limit: Int,
offset: Int, offset: Int,
tag: String?, tag: String?,
deleted: Boolean deleted: Boolean,
): List<PersistedNoteMetadata> { ): List<PersistedNoteMetadata> {
require(limit > 0) { "limit should be positive" } require(limit > 0) { "limit should be positive" }
require(offset >= 0) { "offset should not be negative" } require(offset >= 0) { "offset should not be negative" }
@@ -46,7 +50,7 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
return notes.map { note -> return notes.map { note ->
val tags = tagsByUuid[note.uuid] ?: emptyList() val tags = tagsByUuid[note.uuid] ?: emptyList()
note.toPersistedMetadata(tags) converter.toPersistedNoteMetadata(note, tags)
} }
} }
@@ -56,10 +60,7 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
override fun create(userId: Int, note: Note): PersistedNote { override fun create(userId: Int, note: Note): PersistedNote {
val uuid = UUID.randomUUID() val uuid = UUID.randomUUID()
val entity = note.toEntity(uuid, userId).apply { val entity = converter.toEntity(note, uuid, userId, LocalDateTime.now())
this.updatedAt = LocalDateTime.now()
}
db.useTransaction {
db.notes.add(entity) db.notes.add(entity)
db.batchInsert(Tags) { db.batchInsert(Tags) {
note.meta.tags.forEach { tagName -> note.meta.tags.forEach { tagName ->
@@ -69,9 +70,7 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
} }
} }
} }
} return converter.toPersistedNote(entity, note.meta.tags)
return entity.toPersistedNote(note.meta.tags)
} }
override fun find(userId: Int, uuid: UUID): PersistedNote? { override fun find(userId: Int, uuid: UUID): PersistedNote? {
@@ -86,12 +85,10 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
.where { Tags.noteUuid eq uuid } .where { Tags.noteUuid eq uuid }
.map { it[Tags.name]!! } .map { it[Tags.name]!! }
return note.toPersistedNote(tags) return converter.toPersistedNote(note, tags)
} }
override fun update(userId: Int, uuid: UUID, note: Note): PersistedNote? { override fun update(userId: Int, uuid: UUID, note: Note): PersistedNote? {
db.useTransaction {
val now = LocalDateTime.now() val now = LocalDateTime.now()
val count = db.update(Notes) { val count = db.update(Notes) {
it.title to note.meta.title it.title to note.meta.title
@@ -116,39 +113,26 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
} }
} }
return PersistedNote( return converter.toPersistedNote(note, now, uuid)
meta = note.meta,
markdown = note.markdown,
html = note.html,
updatedAt = now,
uuid = uuid,
public = false, // TODO
)
}
} }
override fun delete(userId: Int, uuid: UUID, permanent: Boolean): Boolean { override fun delete(userId: Int, uuid: UUID, permanent: Boolean): Boolean {
return if (!permanent) { return if (!permanent) {
db.useTransaction {
db.update(Notes) { db.update(Notes) {
it.deleted to true it.deleted to true
it.updatedAt to LocalDateTime.now() it.updatedAt to LocalDateTime.now()
where { it.userId eq userId and (it.uuid eq uuid) } where { it.userId eq userId and (it.uuid eq uuid) }
}
} == 1 } == 1
} else db.useTransaction { } else
db.delete(Notes) { it.uuid eq uuid and (it.userId eq userId) } == 1 db.delete(Notes) { it.uuid eq uuid and (it.userId eq userId) } == 1
} }
}
override fun restore(userId: Int, uuid: UUID): Boolean { override fun restore(userId: Int, uuid: UUID): Boolean {
return db.useTransaction { return db.update(Notes) {
db.update(Notes) {
it.deleted to false it.deleted to false
where { (it.userId eq userId) and (it.uuid eq uuid) } where { (it.userId eq userId) and (it.uuid eq uuid) }
} == 1 } == 1
} }
}
override fun getTags(userId: Int): List<String> = override fun getTags(userId: Int): List<String> =
db.from(Tags) db.from(Tags)
@@ -175,14 +159,8 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
val tagsByUuid = notes.tagsByUuid() val tagsByUuid = notes.tagsByUuid()
return notes.map { note -> return notes.map { note ->
ExportedNote( val tags = tagsByUuid[note.uuid] ?: emptyList()
title = note.title, converter.toExportedNote(note, tags)
tags = tagsByUuid[note.uuid] ?: emptyList(),
markdown = note.markdown,
html = note.html,
updatedAt = note.updatedAt,
trash = note.deleted,
)
} }
} }
@@ -196,7 +174,7 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
return notes.map { note -> return notes.map { note ->
val tags = tagsByUuid[note.uuid] ?: emptyList() val tags = tagsByUuid[note.uuid] ?: emptyList()
note.toPersistedNote(tags) converter.toPersistedNote(note, tags)
} }
} }
@@ -223,7 +201,7 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
.where { Tags.noteUuid eq uuid } .where { Tags.noteUuid eq uuid }
.map { it[Tags.name]!! } .map { it[Tags.name]!! }
return note.toPersistedNote(tags) return converter.toPersistedNote(note, tags)
} }
private fun List<NoteEntity>.tagsByUuid(): Map<UUID, List<String>> { private fun List<NoteEntity>.tagsByUuid(): Map<UUID, List<String>> {
+3 -24
View File
@@ -2,15 +2,12 @@
package be.simplenotes.persistance.notes package be.simplenotes.persistance.notes
import be.simplenotes.domain.model.Note
import be.simplenotes.domain.model.NoteMetadata
import be.simplenotes.domain.model.PersistedNote
import be.simplenotes.domain.model.PersistedNoteMetadata
import be.simplenotes.persistance.extensions.uuidBinary import be.simplenotes.persistance.extensions.uuidBinary
import be.simplenotes.persistance.users.UserEntity import be.simplenotes.persistance.users.UserEntity
import be.simplenotes.persistance.users.Users import be.simplenotes.persistance.users.Users
import me.liuwj.ktorm.database.* import me.liuwj.ktorm.database.Database
import me.liuwj.ktorm.entity.* import me.liuwj.ktorm.entity.Entity
import me.liuwj.ktorm.entity.sequenceOf
import me.liuwj.ktorm.schema.* import me.liuwj.ktorm.schema.*
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.* import java.util.*
@@ -46,21 +43,3 @@ internal interface NoteEntity : Entity<NoteEntity> {
var user: UserEntity var user: UserEntity
} }
internal fun NoteEntity.toPersistedMetadata(tags: List<String>) = PersistedNoteMetadata(title, tags, updatedAt, uuid)
internal fun NoteEntity.toPersistedNote(tags: List<String>) =
PersistedNote(NoteMetadata(title, tags), markdown, html, updatedAt, uuid, public)
internal fun Note.toEntity(uuid: UUID, userId: Int): NoteEntity {
val note = this
return 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
}
}
@@ -3,30 +3,35 @@ package be.simplenotes.persistance.users
import be.simplenotes.domain.model.PersistedUser import be.simplenotes.domain.model.PersistedUser
import be.simplenotes.domain.model.User import be.simplenotes.domain.model.User
import be.simplenotes.domain.usecases.repositories.UserRepository import be.simplenotes.domain.usecases.repositories.UserRepository
import me.liuwj.ktorm.database.* import be.simplenotes.persistance.converters.UserConverter
import me.liuwj.ktorm.database.Database
import me.liuwj.ktorm.dsl.* import me.liuwj.ktorm.dsl.*
import me.liuwj.ktorm.entity.* import me.liuwj.ktorm.entity.any
import me.liuwj.ktorm.entity.find
import java.sql.SQLIntegrityConstraintViolationException import java.sql.SQLIntegrityConstraintViolationException
internal class UserRepositoryImpl(private val db: Database) : UserRepository { internal class UserRepositoryImpl(private val db: Database, private val converter: UserConverter) : UserRepository {
override fun create(user: User): PersistedUser? { override fun create(user: User): PersistedUser? {
return try { return try {
db.useTransaction {
val id = db.insertAndGenerateKey(Users) { val id = db.insertAndGenerateKey(Users) {
it.username to user.username it.username to user.username
it.password to user.password it.password to user.password
} as Int } as Int
PersistedUser(user.username, user.password, id) PersistedUser(user.username, user.password, id)
}
} catch (e: SQLIntegrityConstraintViolationException) { } catch (e: SQLIntegrityConstraintViolationException) {
null null
} }
} }
override fun find(username: String) = db.users.find { it.username eq username }?.toPersistedUser() override fun find(username: String) = db.users.find { it.username eq username }
override fun find(id: Int) = db.users.find { it.id eq id }?.toPersistedUser() ?.let { converter.convertToPersistedUser(it) }
override fun find(id: Int) = db.users.find { it.id eq id }?.let {
converter.convertToPersistedUser(it)
}
override fun exists(username: String) = db.users.any { it.username eq username } 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 exists(id: Int) = db.users.any { it.id eq id }
override fun delete(id: Int) = db.useTransaction { db.delete(Users) { it.id eq id } == 1 } 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]!! } override fun findAll() = db.from(Users).select(Users.id).map { it[Users.id]!! }
} }
+7 -7
View File
@@ -1,9 +1,11 @@
package be.simplenotes.persistance.users package be.simplenotes.persistance.users
import be.simplenotes.domain.model.PersistedUser import me.liuwj.ktorm.database.Database
import me.liuwj.ktorm.database.* import me.liuwj.ktorm.entity.Entity
import me.liuwj.ktorm.entity.* import me.liuwj.ktorm.entity.sequenceOf
import me.liuwj.ktorm.schema.* 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) { internal open class Users(alias: String?) : Table<UserEntity>("Users", alias) {
companion object : Users(null) companion object : Users(null)
@@ -18,11 +20,9 @@ internal open class Users(alias: String?) : Table<UserEntity>("Users", alias) {
internal interface UserEntity : Entity<UserEntity> { internal interface UserEntity : Entity<UserEntity> {
companion object : Entity.Factory<UserEntity>() companion object : Entity.Factory<UserEntity>()
val id: Int var id: Int
var username: String var username: String
var password: String var password: String
} }
internal fun UserEntity.toPersistedUser() = PersistedUser(username, password, id)
internal val Database.users get() = this.sequenceOf(Users, withReferences = false) internal val Database.users get() = this.sequenceOf(Users, withReferences = false)
@@ -0,0 +1,8 @@
package be.simplenotes.persistance.utils
import be.simplenotes.shared.config.DataSourceConfig
enum class DbType { H2, MariaDb }
fun DataSourceConfig.type(): DbType = if (jdbcUrl.contains("mariadb")) DbType.MariaDb
else DbType.H2
@@ -0,0 +1,168 @@
package be.simplenotes.persistance.converters
import be.simplenotes.domain.model.*
import be.simplenotes.persistance.notes.NoteEntity
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.mapstruct.factory.Mappers
import java.time.LocalDateTime
import java.util.*
internal class NoteConverterTest {
@Nested
@DisplayName("Entity -> Models")
inner class EntityToModels {
@Test
fun `convert NoteEntity to Note`() {
val converter = Mappers.getMapper(NoteConverter::class.java)
val entity = NoteEntity {
title = "title"
markdown = "md"
html = "html"
}
val tags = listOf("a", "b")
val note = converter.toNote(entity, tags)
val expectedNote = Note(NoteMetadata(
title = "title",
tags = tags,
), markdown = "md", html = "html")
assertThat(note).isEqualTo(expectedNote)
}
@Test
fun `convert NoteEntity to ExportedNote`() {
val converter = Mappers.getMapper(NoteConverter::class.java)
val entity = NoteEntity {
title = "title"
markdown = "md"
html = "html"
updatedAt = LocalDateTime.MIN
deleted = true
}
val tags = listOf("a", "b")
val note = converter.toExportedNote(entity, tags)
val expectedNote = ExportedNote(
title = "title",
tags = tags,
markdown = "md",
html = "html",
updatedAt = LocalDateTime.MIN,
trash = true
)
assertThat(note).isEqualTo(expectedNote)
}
@Test
fun `convert NoteEntity to PersistedNoteMetadata`() {
val converter = Mappers.getMapper(NoteConverter::class.java)
val entity = NoteEntity {
uuid = UUID.randomUUID()
title = "title"
markdown = "md"
html = "html"
updatedAt = LocalDateTime.MIN
deleted = true
}
val tags = listOf("a", "b")
val note = converter.toPersistedNoteMetadata(entity, tags)
val expectedNote = PersistedNoteMetadata(
title = "title",
tags = tags,
updatedAt = LocalDateTime.MIN,
uuid = entity.uuid
)
assertThat(note).isEqualTo(expectedNote)
}
}
@Nested
@DisplayName("Models -> Entity")
inner class ModelsToEntity {
@Test
fun `convert Note to NoteEntity`() {
val converter = Mappers.getMapper(NoteConverter::class.java)
val note = Note(NoteMetadata("title", emptyList()), "md", "html")
val entity = converter.toEntity(note, UUID.randomUUID(), 2, LocalDateTime.MIN)
assertThat(entity)
.hasFieldOrPropertyWithValue("markdown", "md")
.hasFieldOrPropertyWithValue("html", "html")
.hasFieldOrPropertyWithValue("title", "title")
.hasFieldOrPropertyWithValue("uuid", entity.uuid)
.hasFieldOrPropertyWithValue("updatedAt", LocalDateTime.MIN)
}
@Test
fun `convert PersistedNoteMetadata to NoteEntity`() {
val converter = Mappers.getMapper(NoteConverter::class.java)
val persistedNoteMetadata =
PersistedNoteMetadata("title", emptyList(), LocalDateTime.MIN, UUID.randomUUID())
val entity = converter.toEntity(persistedNoteMetadata)
assertThat(entity)
.hasFieldOrPropertyWithValue("uuid", persistedNoteMetadata.uuid)
.hasFieldOrPropertyWithValue("updatedAt", persistedNoteMetadata.updatedAt)
.hasFieldOrPropertyWithValue("title", "title")
}
@Test
fun `convert NoteMetadata to NoteEntity`() {
val converter = Mappers.getMapper(NoteConverter::class.java)
val noteMetadata = NoteMetadata("title", emptyList())
val entity = converter.toEntity(noteMetadata)
assertThat(entity)
.hasFieldOrPropertyWithValue("title", "title")
}
@Test
fun `convert PersistedNote to NoteEntity`() {
val converter = Mappers.getMapper(NoteConverter::class.java)
val persistedNote = PersistedNote(
NoteMetadata("title", emptyList()),
markdown = "md",
html = "html",
updatedAt = LocalDateTime.MIN,
uuid = UUID.randomUUID(),
public = true
)
val entity = converter.toEntity(persistedNote)
assertThat(entity)
.hasFieldOrPropertyWithValue("title", "title")
.hasFieldOrPropertyWithValue("markdown", "md")
.hasFieldOrPropertyWithValue("uuid", persistedNote.uuid)
.hasFieldOrPropertyWithValue("updatedAt", persistedNote.updatedAt)
.hasFieldOrPropertyWithValue("deleted", false)
.hasFieldOrPropertyWithValue("public", true)
}
@Test
fun `convert ExportedNote to NoteEntity`() {
val converter = Mappers.getMapper(NoteConverter::class.java)
val exportedNote = ExportedNote(
"title",
emptyList(),
markdown = "md",
html = "html",
updatedAt = LocalDateTime.MIN,
trash = true
)
val entity = converter.toEntity(exportedNote)
assertThat(entity)
.hasFieldOrPropertyWithValue("title", "title")
.hasFieldOrPropertyWithValue("markdown", "md")
.hasFieldOrPropertyWithValue("updatedAt", exportedNote.updatedAt)
.hasFieldOrPropertyWithValue("deleted", true)
}
}
}
@@ -0,0 +1,48 @@
package be.simplenotes.persistance.converters
import be.simplenotes.domain.model.PersistedUser
import be.simplenotes.domain.model.User
import be.simplenotes.persistance.users.UserEntity
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.mapstruct.factory.Mappers
internal class UserConverterTest {
@Test
fun `convert UserEntity to User`() {
val converter = Mappers.getMapper(UserConverter::class.java)
val entity = UserEntity {
username = "test"
password = "test2"
}.apply {
this["id"] = 2
}
val user = converter.convertToUser(entity)
assertThat(user).isEqualTo(User("test", "test2"))
}
@Test
fun `convert UserEntity to PersistedUser`() {
val converter = Mappers.getMapper(UserConverter::class.java)
val entity = UserEntity {
username = "test"
password = "test2"
}.apply {
this["id"] = 2
}
val user = converter.convertToPersistedUser(entity)
assertThat(user).isEqualTo(PersistedUser("test", "test2", 2))
}
@Test
fun `convert User to UserEntity`() {
val converter = Mappers.getMapper(UserConverter::class.java)
val user = User("test", "test2")
val entity = converter.convertToEntity(user)
assertThat(entity)
.hasFieldOrPropertyWithValue("username", "test")
.hasFieldOrPropertyWithValue("password", "test2")
}
}
@@ -4,12 +4,16 @@ import be.simplenotes.domain.model.*
import be.simplenotes.domain.usecases.repositories.NoteRepository import be.simplenotes.domain.usecases.repositories.NoteRepository
import be.simplenotes.domain.usecases.repositories.UserRepository import be.simplenotes.domain.usecases.repositories.UserRepository
import be.simplenotes.persistance.DbMigrations import be.simplenotes.persistance.DbMigrations
import be.simplenotes.persistance.converters.NoteConverter
import be.simplenotes.persistance.migrationModule import be.simplenotes.persistance.migrationModule
import be.simplenotes.persistance.persistanceModule import be.simplenotes.persistance.persistanceModule
import be.simplenotes.shared.config.DataSourceConfig import be.simplenotes.shared.config.DataSourceConfig
import me.liuwj.ktorm.database.* import me.liuwj.ktorm.database.Database
import me.liuwj.ktorm.dsl.* import me.liuwj.ktorm.dsl.eq
import me.liuwj.ktorm.entity.* import me.liuwj.ktorm.entity.filter
import me.liuwj.ktorm.entity.find
import me.liuwj.ktorm.entity.mapColumns
import me.liuwj.ktorm.entity.toList
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.Assertions.assertThatThrownBy
import org.flywaydb.core.Flyway import org.flywaydb.core.Flyway
@@ -17,6 +21,7 @@ import org.junit.jupiter.api.*
import org.junit.jupiter.api.parallel.ResourceLock import org.junit.jupiter.api.parallel.ResourceLock
import org.koin.dsl.koinApplication import org.koin.dsl.koinApplication
import org.koin.dsl.module import org.koin.dsl.module
import org.mapstruct.factory.Mappers
import java.sql.SQLIntegrityConstraintViolationException import java.sql.SQLIntegrityConstraintViolationException
import java.util.* import java.util.*
import javax.sql.DataSource import javax.sql.DataSource
@@ -186,10 +191,12 @@ internal class NoteRepositoryImplTest {
fun `find an existing note`() { fun `find an existing note`() {
createNote(user1.id, "1", listOf("a", "b")) createNote(user1.id, "1", listOf("a", "b"))
val converter = Mappers.getMapper(NoteConverter::class.java)
val note = db.notes.find { it.title eq "1" }!! val note = db.notes.find { it.title eq "1" }!!
.let { entity -> .let { entity ->
val tags = db.tags.filter { it.noteUuid eq entity.uuid }.mapColumns { it.name } as List<String> val tags = db.tags.filter { it.noteUuid eq entity.uuid }.mapColumns { it.name } as List<String>
entity.toPersistedNote(tags) converter.toPersistedNote(entity, tags)
} }
assertThat(noteRepo.find(user1.id, note.uuid)) assertThat(noteRepo.find(user1.id, note.uuid))
+124 -96
View File
@@ -1,4 +1,6 @@
<project> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
@@ -17,7 +19,7 @@
<properties> <properties>
<java.version>14</java.version> <java.version>14</java.version>
<kotlin.version>1.4.0</kotlin.version> <kotlin.version>1.4.10</kotlin.version>
<junit.version>5.6.2</junit.version> <junit.version>5.6.2</junit.version>
<kotlin.code.style>official</kotlin.code.style> <kotlin.code.style>official</kotlin.code.style>
@@ -27,6 +29,8 @@
<maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target> <maven.compiler.target>${java.version}</maven.compiler.target>
<kotlin.compiler.jvmTarget>${java.version}</kotlin.compiler.jvmTarget> <kotlin.compiler.jvmTarget>${java.version}</kotlin.compiler.jvmTarget>
<org.mapstruct.version>1.4.1.Final</org.mapstruct.version>
</properties> </properties>
<dependencies> <dependencies>
@@ -35,101 +39,83 @@
<artifactId>kotlin-stdlib-jdk8</artifactId> <artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version> <version>${kotlin.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.koin</groupId>
<artifactId>koin-core</artifactId>
<version>2.1.6</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>io.arrow-kt</groupId>
<artifactId>arrow-core</artifactId>
<version>0.10.5</version>
</dependency>
<!-- region tests -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test-junit</artifactId>
<version>${kotlin.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk</artifactId>
<version>1.10.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.koin</groupId>
<artifactId>koin-test</artifactId>
<version>2.1.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.natpryce</groupId>
<artifactId>hamkrest</artifactId>
<version>1.7.0.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.16.1</version>
<scope>test</scope>
</dependency>
<!-- endregion -->
</dependencies> </dependencies>
<build> <build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory> <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory> <testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<pluginManagement>
<plugins> <plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
</plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M4</version> <version>3.0.0-M5</version>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>org.apache.maven.surefire</groupId> <groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire-junit-platform</artifactId> <artifactId>surefire-junit-platform</artifactId>
<version>3.0.0-M4</version> <version>3.0.0-M5</version>
</dependency> </dependency>
</dependencies> </dependencies>
</plugin> </plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-enforcer-plugin</artifactId>
<version>3.8.1</version> <version>3.0.0-M3</version>
</plugin> <executions>
<plugin> <execution>
<groupId>org.apache.maven.plugins</groupId> <id>enforce</id>
<artifactId>maven-dependency-plugin</artifactId> <goals>
<version>3.1.2</version> <goal>enforce</goal>
</goals>
<configuration>
<rules>
<banDuplicatePomDependencyVersions/>
<requireMavenVersion>
<version>3.6</version>
</requireMavenVersion>
</rules>
</configuration>
</execution>
</executions>
</plugin> </plugin>
<plugin> <plugin>
<artifactId>kotlin-maven-plugin</artifactId> <artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId> <groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin.version}</version> <version>${kotlin.version}</version>
<executions> <executions>
<execution>
<id>kapt</id>
<goals>
<goal>kapt</goal>
</goals>
<configuration>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</execution>
<execution> <execution>
<id>compile</id> <id>compile</id>
<phase>process-sources</phase> <phase>process-sources</phase>
@@ -165,39 +151,81 @@
</plugins> </plugins>
</build> </build>
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>org.jetbrains.kotlin</groupId> <groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId> <artifactId>kotlin-bom</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk7</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-common</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<version>${kotlin.version}</version> <version>${kotlin.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.jetbrains.kotlinx</groupId> <groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-serialization-runtime</artifactId> <artifactId>kotlinx-serialization-json-jvm</artifactId>
<version>1.0-M1-1.4.0-rc</version> <version>1.0.0</version>
</dependency> </dependency>
<dependency>
<groupId>org.koin</groupId>
<artifactId>koin-core</artifactId>
<version>2.1.6</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>io.arrow-kt</groupId>
<artifactId>arrow-core</artifactId>
<version>0.10.5</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>me.liuwj.ktorm</groupId>
<artifactId>ktorm-core</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>me.liuwj.ktorm</groupId>
<artifactId>ktorm-support-mysql</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<!-- region tests -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
</dependency>
<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>com.natpryce</groupId>
<artifactId>hamkrest</artifactId>
<version>1.7.0.3</version>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.16.1</version>
</dependency>
<!-- endregion -->
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>
+16
View File
@@ -37,6 +37,22 @@
<version>${lucene.version}</version> <version>${lucene.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
<artifactId>shared</artifactId> <artifactId>shared</artifactId>
+16 -2
View File
@@ -1,4 +1,6 @@
<project> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent> <parent>
<artifactId>parent</artifactId> <artifactId>parent</artifactId>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
@@ -8,12 +10,24 @@
<artifactId>shared</artifactId> <artifactId>shared</artifactId>
<dependencies>
<dependency>
<groupId>io.arrow-kt</groupId>
<artifactId>arrow-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.natpryce</groupId>
<artifactId>hamkrest</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build> <build>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId> <artifactId>maven-jar-plugin</artifactId>
<version>2.6</version>
<executions> <executions>
<execution> <execution>
<goals> <goals>