Compare commits
17 Commits
b015f3a97e
...
mapstruct
| Author | SHA1 | Date | |
|---|---|---|---|
| 32337ec308 | |||
| 681fd635b3 | |||
| 9467db2382 | |||
| 7ed3494808 | |||
| ceb310bf02 | |||
| 2c3106c5c1 | |||
| 4effa8231a | |||
| b78420e106 | |||
| dd08763161 | |||
| e0b1514965 | |||
| 69c91ec86a | |||
| 1bc45461c3 | |||
| 0dfb2a7e03 | |||
| a7c8e63b11 | |||
| ad97ba029e | |||
| 31f538c7f5 | |||
| c7cf71441f |
@@ -133,3 +133,6 @@ app/src/main/resources/static/styles*
|
||||
|
||||
# lucene index
|
||||
.lucene/
|
||||
|
||||
# python
|
||||
__pycache__
|
||||
|
||||
@@ -32,6 +32,8 @@ RUN strip -p --strip-unneeded /myjdk/lib/server/libjvm.so
|
||||
|
||||
FROM alpine
|
||||
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
ENV APPLICATION_USER simplenotes
|
||||
RUN adduser -D -g '' $APPLICATION_USER
|
||||
|
||||
@@ -44,4 +46,4 @@ COPY --from=builder /tmp/app/target/app-*.jar /app/app.jar
|
||||
COPY --from=jdkbuilder /myjdk /myjdk
|
||||
WORKDIR /app
|
||||
|
||||
CMD ["/myjdk/bin/java", "-server", "-XX:+UnlockExperimentalVMOptions", "-Xms256m", "-Xmx1g", "-XX:+UseG1GC", "-XX:MaxGCPauseMillis=100", "-XX:+UseStringDeduplication", "-jar", "app.jar"]
|
||||
CMD ["/myjdk/bin/java", "-server", "-XX:+UnlockExperimentalVMOptions", "-Xms64m", "-Xmx256m", "-XX:+UseG1GC", "-XX:MaxGCPauseMillis=100", "-XX:+UseStringDeduplication", "-jar", "app.jar"]
|
||||
|
||||
@@ -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>
|
||||
<artifactId>parent</artifactId>
|
||||
<groupId>be.simplenotes</groupId>
|
||||
@@ -9,7 +11,7 @@
|
||||
<artifactId>app</artifactId>
|
||||
|
||||
<properties>
|
||||
<http4k.version>3.258.0</http4k.version>
|
||||
<http4k.version>3.268.0</http4k.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -36,12 +38,16 @@
|
||||
<dependency>
|
||||
<groupId>org.http4k</groupId>
|
||||
<artifactId>http4k-core</artifactId>
|
||||
<version>${http4k.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.http4k</groupId>
|
||||
<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>
|
||||
<groupId>org.jetbrains.kotlinx</groupId>
|
||||
@@ -50,7 +56,7 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlinx</groupId>
|
||||
<artifactId>kotlinx-serialization-runtime</artifactId>
|
||||
<artifactId>kotlinx-serialization-json-jvm</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.ocpsoft.prettytime</groupId>
|
||||
@@ -58,6 +64,21 @@
|
||||
<version>4.0.5.Final</version>
|
||||
</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>
|
||||
<groupId>be.simplenotes</groupId>
|
||||
<artifactId>shared</artifactId>
|
||||
@@ -68,17 +89,32 @@
|
||||
<dependency>
|
||||
<groupId>org.http4k</groupId>
|
||||
<artifactId>http4k-testing-hamkrest</artifactId>
|
||||
<version>${http4k.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>me.liuwj.ktorm</groupId>
|
||||
<artifactId>ktorm-core</artifactId>
|
||||
</dependency>
|
||||
</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>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.2.4</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
|
||||
@@ -6,7 +6,7 @@ import be.simplenotes.shared.config.ServerConfig
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object Config {
|
||||
class Config {
|
||||
//region Config loading
|
||||
private val properties: Properties = javaClass
|
||||
.getResource("/application.properties")
|
||||
|
||||
@@ -1,32 +1,23 @@
|
||||
package be.simplenotes.app
|
||||
|
||||
import org.eclipse.jetty.server.Server
|
||||
import org.eclipse.jetty.server.ServerConnector
|
||||
import org.http4k.routing.RoutingHttpHandler
|
||||
import org.http4k.server.ConnectorBuilder
|
||||
import org.http4k.server.Jetty
|
||||
import org.http4k.server.ServerConfig
|
||||
import org.http4k.server.asServer
|
||||
import org.http4k.server.Http4kServer
|
||||
import org.slf4j.LoggerFactory
|
||||
import be.simplenotes.shared.config.ServerConfig as SimpleNotesServeConfig
|
||||
import be.simplenotes.shared.config.ServerConfig as SimpleNotesServerConfig
|
||||
|
||||
class Server(
|
||||
private val config: SimpleNotesServeConfig,
|
||||
private val serverConfig: ServerConfig,
|
||||
private val router: RoutingHttpHandler,
|
||||
private val config: SimpleNotesServerConfig,
|
||||
private val http4kServer: Http4kServer,
|
||||
) {
|
||||
fun start() {
|
||||
router.asServer(serverConfig).start()
|
||||
LoggerFactory.getLogger(javaClass).info("Listening on http://${config.host}:${config.port}")
|
||||
}
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
fun start(): Server {
|
||||
http4kServer.start()
|
||||
logger.info("Listening on http://${config.host}:${config.port}")
|
||||
return this
|
||||
}
|
||||
|
||||
fun serverConfig(config: SimpleNotesServeConfig): ServerConfig {
|
||||
val builder: ConnectorBuilder = { server: Server ->
|
||||
ServerConnector(server).apply {
|
||||
port = config.port
|
||||
host = config.host
|
||||
fun stop() {
|
||||
logger.info("Stopping server")
|
||||
http4kServer.close()
|
||||
}
|
||||
}
|
||||
return Jetty(config.port, builder)
|
||||
}
|
||||
|
||||
@@ -1,109 +1,31 @@
|
||||
package be.simplenotes.app
|
||||
|
||||
import be.simplenotes.app.controllers.BaseController
|
||||
import be.simplenotes.app.controllers.NoteController
|
||||
import be.simplenotes.app.controllers.SettingsController
|
||||
import be.simplenotes.app.controllers.UserController
|
||||
import be.simplenotes.app.filters.AuthFilter
|
||||
import be.simplenotes.app.filters.AuthType
|
||||
import be.simplenotes.app.filters.ErrorFilter
|
||||
import be.simplenotes.app.routes.Router
|
||||
import be.simplenotes.app.utils.StaticFileResolver
|
||||
import be.simplenotes.app.utils.StaticFileResolverImpl
|
||||
import be.simplenotes.app.views.*
|
||||
import be.simplenotes.app.extensions.addShutdownHook
|
||||
import be.simplenotes.app.modules.*
|
||||
import be.simplenotes.domain.domainModule
|
||||
import be.simplenotes.domain.usecases.NoteService
|
||||
import be.simplenotes.persistance.DbMigrations
|
||||
import be.simplenotes.persistance.migrationModule
|
||||
import be.simplenotes.persistance.persistanceModule
|
||||
import be.simplenotes.search.searchModule
|
||||
import be.simplenotes.shared.config.DataSourceConfig
|
||||
import be.simplenotes.shared.config.JwtConfig
|
||||
import org.http4k.core.RequestContexts
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.core.qualifier.qualifier
|
||||
import org.koin.dsl.module
|
||||
import org.slf4j.LoggerFactory
|
||||
import be.simplenotes.shared.config.ServerConfig as SimpleNotesServeConfig
|
||||
import org.koin.core.context.unloadKoinModules
|
||||
|
||||
fun main() {
|
||||
val koin = startKoin {
|
||||
startKoin {
|
||||
modules(
|
||||
persistanceModule,
|
||||
configModule,
|
||||
domainModule,
|
||||
serverModule,
|
||||
userModule,
|
||||
persistanceModule,
|
||||
migrationModule,
|
||||
configModule,
|
||||
baseModule,
|
||||
userModule,
|
||||
noteModule,
|
||||
settingsModule,
|
||||
domainModule,
|
||||
searchModule,
|
||||
apiModule,
|
||||
jsonModule
|
||||
)
|
||||
}.koin
|
||||
}.addShutdownHook()
|
||||
|
||||
val dataSourceConfig = koin.get<DataSourceConfig>()
|
||||
val jwtConfig = koin.get<JwtConfig>()
|
||||
val serverConfig = koin.get<SimpleNotesServeConfig>()
|
||||
val logger = LoggerFactory.getLogger("SimpleNotes")
|
||||
logger.info("datasource: $dataSourceConfig")
|
||||
logger.info("jwt: $jwtConfig")
|
||||
logger.info("server: $serverConfig")
|
||||
|
||||
val migrations = koin.get<DbMigrations>()
|
||||
migrations.migrate()
|
||||
|
||||
val noteService = koin.get<NoteService>()
|
||||
noteService.dropAllIndexes()
|
||||
noteService.indexAll()
|
||||
|
||||
koin.get<Server>().start()
|
||||
}
|
||||
|
||||
val serverModule = module {
|
||||
single { Server(get(), get(), get()) }
|
||||
single<StaticFileResolver> { StaticFileResolverImpl() }
|
||||
single {
|
||||
Router(
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
requiredAuth = get(AuthType.Required.qualifier),
|
||||
optionalAuth = get(AuthType.Optional.qualifier),
|
||||
errorFilter = get(named("ErrorFilter")),
|
||||
get()
|
||||
)()
|
||||
}
|
||||
single { serverConfig(get()) }
|
||||
single { RequestContexts() }
|
||||
single(AuthType.Optional.qualifier) { AuthFilter(get(), AuthType.Optional, get())() }
|
||||
single(AuthType.Required.qualifier) { AuthFilter(get(), AuthType.Required, get())() }
|
||||
single(named("ErrorFilter")) { ErrorFilter(get())() }
|
||||
single { ErrorView(get()) }
|
||||
}
|
||||
|
||||
val userModule = module {
|
||||
single { UserController(get(), get(), get()) }
|
||||
single { UserView(get()) }
|
||||
}
|
||||
|
||||
val baseModule = module {
|
||||
single { BaseController(get()) }
|
||||
single { BaseView(get()) }
|
||||
}
|
||||
|
||||
val noteModule = module {
|
||||
single { NoteController(get(), get()) }
|
||||
single { NoteView(get()) }
|
||||
}
|
||||
|
||||
val settingsModule = module {
|
||||
single { SettingsController(get(), get()) }
|
||||
single { SettingView(get()) }
|
||||
}
|
||||
|
||||
val configModule = module {
|
||||
single { Config.dataSourceConfig }
|
||||
single { Config.jwtConfig }
|
||||
single { Config.serverConfig }
|
||||
unloadKoinModules(listOf(migrationModule, configModule))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package be.simplenotes.app.api
|
||||
|
||||
import be.simplenotes.app.extensions.json
|
||||
import be.simplenotes.app.utils.parseSearchTerms
|
||||
import be.simplenotes.domain.model.PersistedNote
|
||||
import be.simplenotes.domain.model.PersistedNoteMetadata
|
||||
import be.simplenotes.domain.security.JwtPayload
|
||||
import be.simplenotes.domain.usecases.NoteService
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.Response
|
||||
import org.http4k.core.Status.Companion.BAD_REQUEST
|
||||
import org.http4k.core.Status.Companion.NOT_FOUND
|
||||
import org.http4k.core.Status.Companion.OK
|
||||
import org.http4k.routing.path
|
||||
import java.util.*
|
||||
|
||||
class ApiNoteController(private val noteService: NoteService, private val json: Json) {
|
||||
|
||||
fun createNote(request: Request, jwtPayload: JwtPayload): Response {
|
||||
val content = json.decodeFromString(NoteContent.serializer(), request.bodyString()).content
|
||||
return noteService.create(jwtPayload.userId, content).fold(
|
||||
{
|
||||
Response(BAD_REQUEST)
|
||||
},
|
||||
{
|
||||
Response(OK).json(json.encodeToString(UuidContent.serializer(), UuidContent(it.uuid)))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun notes(request: Request, jwtPayload: JwtPayload): Response {
|
||||
val notes = noteService.paginatedNotes(jwtPayload.userId, page = 1).notes
|
||||
val json = json.encodeToString(ListSerializer(PersistedNoteMetadata.serializer()), notes)
|
||||
return Response(OK).json(json)
|
||||
}
|
||||
|
||||
fun note(request: Request, jwtPayload: JwtPayload): Response {
|
||||
val uuid = request.path("uuid")!!
|
||||
|
||||
return noteService.find(jwtPayload.userId, UUID.fromString(uuid))
|
||||
?.let { Response(OK).json(json.encodeToString(PersistedNote.serializer(), it)) }
|
||||
?: Response(NOT_FOUND)
|
||||
}
|
||||
|
||||
fun update(request: Request, jwtPayload: JwtPayload): Response {
|
||||
val uuid = UUID.fromString(request.path("uuid")!!)
|
||||
val content = json.decodeFromString(NoteContent.serializer(), request.bodyString()).content
|
||||
return noteService.update(jwtPayload.userId, uuid, content).fold({
|
||||
Response(BAD_REQUEST)
|
||||
}, {
|
||||
if (it == null) Response(NOT_FOUND)
|
||||
else Response(OK)
|
||||
})
|
||||
}
|
||||
|
||||
fun search(request: Request, jwtPayload: JwtPayload): Response {
|
||||
val query = json.decodeFromString(SearchContent.serializer(), request.bodyString()).query
|
||||
val terms = parseSearchTerms(query)
|
||||
val notes = noteService.search(jwtPayload.userId, terms)
|
||||
val json = json.encodeToString(ListSerializer(PersistedNoteMetadata.serializer()), notes)
|
||||
return Response(OK).json(json)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class NoteContent(val content: String)
|
||||
|
||||
@Serializable
|
||||
data class UuidContent(@Contextual val uuid: UUID)
|
||||
|
||||
@Serializable
|
||||
data class SearchContent(@Contextual val query: String)
|
||||
@@ -0,0 +1,26 @@
|
||||
package be.simplenotes.app.api
|
||||
|
||||
import be.simplenotes.app.extensions.json
|
||||
import be.simplenotes.domain.usecases.UserService
|
||||
import be.simplenotes.domain.usecases.users.login.LoginForm
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.Response
|
||||
import org.http4k.core.Status
|
||||
|
||||
class ApiUserController(private val userService: UserService, private val json: Json) {
|
||||
|
||||
fun login(request: Request): Response {
|
||||
val form = json.decodeFromString(LoginForm.serializer(), request.bodyString())
|
||||
val result = userService.login(form)
|
||||
return result.fold({
|
||||
Response(Status.BAD_REQUEST)
|
||||
}, {
|
||||
Response(Status.OK).json(json.encodeToString(Token.serializer(), Token(it)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Token(val token: String)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -7,10 +7,7 @@ import be.simplenotes.domain.security.JwtPayload
|
||||
import be.simplenotes.domain.usecases.UserService
|
||||
import be.simplenotes.domain.usecases.users.delete.DeleteError
|
||||
import be.simplenotes.domain.usecases.users.delete.DeleteForm
|
||||
import org.http4k.core.Method
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.Response
|
||||
import org.http4k.core.Status
|
||||
import org.http4k.core.*
|
||||
import org.http4k.core.body.form
|
||||
import org.http4k.core.cookie.invalidateCookie
|
||||
|
||||
@@ -44,23 +41,34 @@ class SettingsController(
|
||||
}
|
||||
},
|
||||
{
|
||||
Response.redirect("/").invalidateCookie("Authorization")
|
||||
Response.redirect("/").invalidateCookie("Bearer")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun attachment(filename: String, contentType: String) = { response: Response ->
|
||||
val name = filename.replace("[^a-zA-Z0-9-_.]".toRegex(), "_")
|
||||
response
|
||||
.header("Content-Disposition", "attachment; filename=\"$name\"")
|
||||
.header("Content-Type", contentType)
|
||||
}
|
||||
|
||||
fun export(request: Request, jwtPayload: JwtPayload): Response {
|
||||
val isDownload = request.form("download") != null
|
||||
val json = userService.export(jwtPayload.userId)
|
||||
val res = Response(Status.OK).body(json).header("Content-Type", "application/json")
|
||||
return if (isDownload) res.header(
|
||||
"Content-Disposition",
|
||||
"attachment; filename=\"simplenotes-export-${sanitizeFilename(jwtPayload.username)}.json\""
|
||||
)
|
||||
else res
|
||||
}
|
||||
|
||||
private fun sanitizeFilename(inputName: String): String = inputName.replace("[^a-zA-Z0-9-_.]".toRegex(), "_")
|
||||
return if (isDownload) {
|
||||
val filename = "simplenotes-export-${jwtPayload.username}"
|
||||
if (request.form("format") == "zip") {
|
||||
val zip = userService.exportAsZip(jwtPayload.userId)
|
||||
Response(Status.OK)
|
||||
.with(attachment("$filename.zip", "application/zip"))
|
||||
.body(zip)
|
||||
} else
|
||||
Response(Status.OK)
|
||||
.with(attachment("$filename.json", "application/json"))
|
||||
.body(userService.exportAsJson(jwtPayload.userId))
|
||||
} else Response(Status.OK).body(userService.exportAsJson(jwtPayload.userId)).header("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
private fun Request.deleteForm(jwtPayload: JwtPayload) =
|
||||
DeleteForm(jwtPayload.username, form("password"), form("checked") != null)
|
||||
|
||||
@@ -9,6 +9,8 @@ fun Response.html(html: String) = body(html)
|
||||
.header("Content-Type", "text/html; charset=utf-8")
|
||||
.header("Cache-Control", "no-cache")
|
||||
|
||||
fun Response.json(json: String) = body(json).header("Content-Type", "application/json")
|
||||
|
||||
fun Response.Companion.redirect(url: String, permanent: Boolean = false) =
|
||||
Response(if (permanent) MOVED_PERMANENTLY else FOUND).header("Location", url)
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package be.simplenotes.app.extensions
|
||||
|
||||
import org.koin.core.KoinApplication
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
fun KoinApplication.addShutdownHook() {
|
||||
Runtime.getRuntime().addShutdownHook(
|
||||
thread(start = false) {
|
||||
close()
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -3,10 +3,8 @@ package be.simplenotes.app.filters
|
||||
import be.simplenotes.app.extensions.redirect
|
||||
import be.simplenotes.domain.security.JwtPayload
|
||||
import be.simplenotes.domain.security.JwtPayloadExtractor
|
||||
import org.http4k.core.Filter
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.RequestContexts
|
||||
import org.http4k.core.Response
|
||||
import org.http4k.core.*
|
||||
import org.http4k.core.Status.Companion.UNAUTHORIZED
|
||||
import org.http4k.core.cookie.cookie
|
||||
|
||||
enum class AuthType {
|
||||
@@ -18,25 +16,43 @@ private const val authKey = "auth"
|
||||
class AuthFilter(
|
||||
private val extractor: JwtPayloadExtractor,
|
||||
private val authType: AuthType,
|
||||
private val ctx: RequestContexts
|
||||
) {
|
||||
operator fun invoke() = Filter { next ->
|
||||
{
|
||||
val jwtPayload = it.bearerToken()?.let { token -> extractor(token) }
|
||||
private val ctx: RequestContexts,
|
||||
private val source: JwtSource = JwtSource.Cookie,
|
||||
private val redirect: Boolean = true,
|
||||
) : Filter {
|
||||
override fun invoke(next: HttpHandler): HttpHandler = {
|
||||
val token = when (source) {
|
||||
JwtSource.Header -> it.bearerTokenHeader()
|
||||
JwtSource.Cookie -> it.bearerTokenCookie()
|
||||
}
|
||||
val jwtPayload = token?.let { token -> extractor(token) }
|
||||
when {
|
||||
jwtPayload != null -> {
|
||||
ctx[it][authKey] = jwtPayload
|
||||
next(it)
|
||||
}
|
||||
authType == AuthType.Required -> Response.redirect("/login")
|
||||
else -> next(it)
|
||||
authType == AuthType.Required -> {
|
||||
if (redirect) Response.redirect("/login")
|
||||
else Response(UNAUTHORIZED)
|
||||
}
|
||||
else -> next(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Request.jwtPayload(ctx: RequestContexts): JwtPayload? = ctx[this][authKey]
|
||||
|
||||
private fun Request.bearerToken(): String? = cookie("Bearer")
|
||||
enum class JwtSource {
|
||||
Header, Cookie
|
||||
}
|
||||
|
||||
private fun Request.bearerTokenCookie(): String? = cookie("Bearer")
|
||||
?.value
|
||||
?.trim()
|
||||
|
||||
private fun Request.bearerTokenHeader(): String? =
|
||||
header("Authorization")
|
||||
?.trim()
|
||||
?.takeIf { it.startsWith("Bearer") }
|
||||
?.substringAfter("Bearer")
|
||||
?.trim()
|
||||
|
||||
@@ -2,32 +2,44 @@ package be.simplenotes.app.filters
|
||||
|
||||
import be.simplenotes.app.extensions.html
|
||||
import be.simplenotes.app.views.ErrorView
|
||||
import be.simplenotes.app.views.ErrorView.Type.*
|
||||
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 java.sql.SQLTransientException
|
||||
|
||||
class ErrorFilter(private val errorView: ErrorView) {
|
||||
class ErrorFilter(private val errorView: ErrorView) : Filter {
|
||||
|
||||
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 {
|
||||
val response = next(it)
|
||||
if (response.status == Status.NOT_FOUND) Response(Status.NOT_FOUND)
|
||||
.html(errorView.error(ErrorView.Type.NotFound))
|
||||
val response = next(request)
|
||||
if (response.status == NOT_FOUND) errorResponse(NOT_FOUND)
|
||||
else response
|
||||
} catch (e: SQLTransientException) {
|
||||
logger.error(e.stackTraceToString())
|
||||
errorResponse(SERVICE_UNAVAILABLE)
|
||||
} catch (e: Exception) {
|
||||
logger.error(e.stackTraceToString())
|
||||
if (e is SQLTransientException)
|
||||
Response(Status.SERVICE_UNAVAILABLE).html(errorView.error(ErrorView.Type.SqlTransientError))
|
||||
.noCache()
|
||||
else
|
||||
Response(Status.INTERNAL_SERVER_ERROR).html(errorView.error(ErrorView.Type.Other)).noCache()
|
||||
errorResponse(INTERNAL_SERVER_ERROR)
|
||||
} catch (e: NotImplementedError) {
|
||||
logger.error(e.stackTraceToString())
|
||||
Response(Status.NOT_IMPLEMENTED).html(errorView.error(ErrorView.Type.Other)).noCache()
|
||||
}
|
||||
errorResponse(NOT_IMPLEMENTED)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,10 @@ package be.simplenotes.app.filters
|
||||
|
||||
import org.http4k.core.Filter
|
||||
import org.http4k.core.HttpHandler
|
||||
import org.http4k.core.Method
|
||||
import org.http4k.core.Request
|
||||
|
||||
object ImmutableFilter {
|
||||
operator fun invoke(): Filter {
|
||||
return Filter { next: HttpHandler ->
|
||||
{ request: Request ->
|
||||
val response = next(request)
|
||||
if (request.method == Method.GET)
|
||||
response.header("Cache-Control", "public, max-age=31536000, immutable")
|
||||
else response
|
||||
}
|
||||
}
|
||||
object ImmutableFilter : Filter {
|
||||
override fun invoke(next: HttpHandler) = { request: Request ->
|
||||
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.Request
|
||||
|
||||
object SecurityFilter {
|
||||
operator fun invoke() = Filter { next: HttpHandler ->
|
||||
{ request: Request ->
|
||||
object SecurityFilter : Filter {
|
||||
override fun invoke(next: HttpHandler): HttpHandler = { request: Request ->
|
||||
val response = next(request)
|
||||
.header("X-Content-Type-Options", "nosniff")
|
||||
|
||||
@@ -17,4 +16,3 @@ object SecurityFilter {
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package be.simplenotes.app.modules
|
||||
|
||||
import be.simplenotes.app.api.ApiNoteController
|
||||
import be.simplenotes.app.api.ApiUserController
|
||||
import be.simplenotes.app.filters.AuthFilter
|
||||
import be.simplenotes.app.filters.AuthType
|
||||
import be.simplenotes.app.filters.JwtSource
|
||||
import org.http4k.core.Filter
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val apiModule = module {
|
||||
single { ApiUserController(get(), get()) }
|
||||
single { ApiNoteController(get(), get()) }
|
||||
single<Filter>(named("apiAuthFilter")) {
|
||||
AuthFilter(
|
||||
extractor = get(),
|
||||
authType = AuthType.Required,
|
||||
ctx = get(),
|
||||
source = JwtSource.Header,
|
||||
redirect = false
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package be.simplenotes.app.modules
|
||||
|
||||
import be.simplenotes.app.Config
|
||||
import org.koin.dsl.module
|
||||
|
||||
val configModule = module {
|
||||
single { Config() }
|
||||
single { get<Config>().dataSourceConfig }
|
||||
single { get<Config>().jwtConfig }
|
||||
single { get<Config>().serverConfig }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package be.simplenotes.app.modules
|
||||
|
||||
import be.simplenotes.app.controllers.*
|
||||
import be.simplenotes.app.views.BaseView
|
||||
import be.simplenotes.app.views.NoteView
|
||||
import be.simplenotes.app.views.SettingView
|
||||
import be.simplenotes.app.views.UserView
|
||||
import org.koin.dsl.module
|
||||
|
||||
val userModule = module {
|
||||
single { UserController(get(), get(), get()) }
|
||||
single { UserView(get()) }
|
||||
}
|
||||
|
||||
val baseModule = module {
|
||||
single { HealthCheckController(get()) }
|
||||
single { BaseController(get()) }
|
||||
single { BaseView(get()) }
|
||||
}
|
||||
|
||||
val noteModule = module {
|
||||
single { NoteController(get(), get()) }
|
||||
single { NoteView(get()) }
|
||||
}
|
||||
|
||||
val settingsModule = module {
|
||||
single { SettingsController(get(), get()) }
|
||||
single { SettingView(get()) }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package be.simplenotes.app.modules
|
||||
|
||||
import be.simplenotes.app.serialization.LocalDateTimeSerializer
|
||||
import be.simplenotes.app.serialization.UuidSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import org.koin.dsl.module
|
||||
import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
|
||||
val jsonModule = module {
|
||||
single {
|
||||
Json {
|
||||
prettyPrint = true
|
||||
serializersModule = get()
|
||||
}
|
||||
}
|
||||
single {
|
||||
SerializersModule {
|
||||
contextual(LocalDateTime::class, LocalDateTimeSerializer())
|
||||
contextual(UUID::class, UuidSerializer())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package be.simplenotes.app.modules
|
||||
|
||||
import be.simplenotes.app.Server
|
||||
import be.simplenotes.app.filters.AuthFilter
|
||||
import be.simplenotes.app.filters.AuthType
|
||||
import be.simplenotes.app.filters.ErrorFilter
|
||||
import be.simplenotes.app.filters.TransactionFilter
|
||||
import be.simplenotes.app.routes.Router
|
||||
import be.simplenotes.app.utils.StaticFileResolver
|
||||
import be.simplenotes.app.utils.StaticFileResolverImpl
|
||||
import be.simplenotes.app.views.ErrorView
|
||||
import be.simplenotes.shared.config.ServerConfig
|
||||
import org.eclipse.jetty.server.ServerConnector
|
||||
import org.http4k.core.Filter
|
||||
import org.http4k.core.RequestContexts
|
||||
import org.http4k.routing.RoutingHttpHandler
|
||||
import org.http4k.server.ConnectorBuilder
|
||||
import org.http4k.server.Jetty
|
||||
import org.http4k.server.asServer
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.core.qualifier.qualifier
|
||||
import org.koin.dsl.module
|
||||
import org.koin.dsl.onClose
|
||||
import org.http4k.server.ServerConfig as Http4kServerConfig
|
||||
|
||||
val serverModule = module {
|
||||
single(createdAtStart = true) { Server(get(), get()).start() } onClose { it?.stop() }
|
||||
single { get<RoutingHttpHandler>().asServer(get()) }
|
||||
single<Http4kServerConfig> {
|
||||
val config = get<ServerConfig>()
|
||||
val builder: ConnectorBuilder = { server: org.eclipse.jetty.server.Server ->
|
||||
ServerConnector(server).apply {
|
||||
port = config.port
|
||||
host = config.host
|
||||
}
|
||||
}
|
||||
Jetty(config.port, builder)
|
||||
}
|
||||
single<StaticFileResolver> { StaticFileResolverImpl(get()) }
|
||||
single {
|
||||
Router(
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
requiredAuth = get(AuthType.Required.qualifier),
|
||||
optionalAuth = get(AuthType.Optional.qualifier),
|
||||
apiAuth = get(named("apiAuthFilter")),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
)()
|
||||
}
|
||||
single { RequestContexts() }
|
||||
single<Filter>(AuthType.Optional.qualifier) { AuthFilter(get(), AuthType.Optional, get()) }
|
||||
single<Filter>(AuthType.Required.qualifier) { AuthFilter(get(), AuthType.Required, get()) }
|
||||
single { ErrorFilter(get()) }
|
||||
single { TransactionFilter(get()) }
|
||||
single { ErrorView(get()) }
|
||||
}
|
||||
@@ -1,44 +1,44 @@
|
||||
package be.simplenotes.app.routes
|
||||
|
||||
import be.simplenotes.app.controllers.BaseController
|
||||
import be.simplenotes.app.controllers.NoteController
|
||||
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.app.api.ApiNoteController
|
||||
import be.simplenotes.app.api.ApiUserController
|
||||
import be.simplenotes.app.controllers.*
|
||||
import be.simplenotes.app.filters.*
|
||||
import be.simplenotes.domain.security.JwtPayload
|
||||
import org.http4k.core.*
|
||||
import org.http4k.core.Method.GET
|
||||
import org.http4k.core.Method.POST
|
||||
import org.http4k.filter.ResponseFilters
|
||||
import org.http4k.core.Method.*
|
||||
import org.http4k.filter.ResponseFilters.GZip
|
||||
import org.http4k.filter.ServerFilters.InitialiseRequestContext
|
||||
import org.http4k.routing.*
|
||||
import org.http4k.routing.ResourceLoader.Companion.Classpath
|
||||
|
||||
class Router(
|
||||
private val baseController: BaseController,
|
||||
private val userController: UserController,
|
||||
private val noteController: NoteController,
|
||||
private val settingsController: SettingsController,
|
||||
private val apiUserController: ApiUserController,
|
||||
private val apiNoteController: ApiNoteController,
|
||||
private val healthCheckController: HealthCheckController,
|
||||
private val requiredAuth: Filter,
|
||||
private val optionalAuth: Filter,
|
||||
private val errorFilter: Filter,
|
||||
private val apiAuth: Filter,
|
||||
private val errorFilter: ErrorFilter,
|
||||
private val transactionFilter: TransactionFilter,
|
||||
private val contexts: RequestContexts,
|
||||
) {
|
||||
operator fun invoke(): RoutingHttpHandler {
|
||||
|
||||
val resourceLoader = ResourceLoader.Classpath(("/static"))
|
||||
val basicRoutes = routes(
|
||||
ImmutableFilter().then(static(resourceLoader, "woff2" to ContentType("font/woff2"))),
|
||||
val basicRoutes =
|
||||
routes(
|
||||
"/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)) }
|
||||
infix fun PathMethod.protected(handler: ProtectedHandler) = this to { handler(it, it.jwtPayload(contexts)!!) }
|
||||
|
||||
val publicRoutes: RoutingHttpHandler = routes(
|
||||
val publicRoutes = routes(
|
||||
"/" bind GET public baseController::index,
|
||||
"/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 POST public userController::login,
|
||||
"/logout" bind POST to userController::logout,
|
||||
@@ -47,33 +47,59 @@ class Router(
|
||||
|
||||
val protectedRoutes = routes(
|
||||
"/settings" bind GET protected settingsController::settings,
|
||||
"/settings" bind POST protected settingsController::settings,
|
||||
"/settings" bind POST transactional settingsController::settings,
|
||||
"/export" bind POST protected settingsController::export,
|
||||
"/notes" bind GET protected noteController::list,
|
||||
"/notes" bind POST protected noteController::search,
|
||||
"/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/{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 POST protected noteController::edit,
|
||||
"/notes/deleted/{uuid}" bind POST protected noteController::deleted,
|
||||
"/notes/{uuid}/edit" bind POST transactional noteController::edit,
|
||||
"/notes/deleted/{uuid}" bind POST transactional noteController::deleted,
|
||||
)
|
||||
|
||||
val apiRoutes = routes(
|
||||
"/api/login" bind POST to apiUserController::login,
|
||||
)
|
||||
|
||||
val protectedApiRoutes = routes(
|
||||
"/api/notes" bind GET protected apiNoteController::notes,
|
||||
"/api/notes" bind POST transactional apiNoteController::createNote,
|
||||
"/api/notes/search" bind POST transactional apiNoteController::search,
|
||||
"/api/notes/{uuid}" bind GET protected apiNoteController::note,
|
||||
"/api/notes/{uuid}" bind PUT transactional apiNoteController::update,
|
||||
)
|
||||
|
||||
val routes = routes(
|
||||
basicRoutes,
|
||||
optionalAuth.then(publicRoutes),
|
||||
requiredAuth.then(protectedRoutes),
|
||||
apiAuth.then(protectedApiRoutes),
|
||||
apiRoutes,
|
||||
)
|
||||
|
||||
val globalFilters = errorFilter
|
||||
.then(InitialiseRequestContext(contexts))
|
||||
.then(SecurityFilter())
|
||||
.then(ResponseFilters.GZip())
|
||||
.then(SecurityFilter)
|
||||
.then(GZip())
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package be.simplenotes.app.serialization
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import java.time.LocalDateTime
|
||||
|
||||
internal class LocalDateTimeSerializer : KSerializer<LocalDateTime> {
|
||||
override val descriptor: SerialDescriptor
|
||||
get() = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: LocalDateTime) {
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): LocalDateTime {
|
||||
TODO("Not implemented, isn't needed")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package be.simplenotes.app.serialization
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import java.util.*
|
||||
|
||||
internal class UuidSerializer : KSerializer<UUID> {
|
||||
override val descriptor: SerialDescriptor
|
||||
get() = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: UUID) {
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): UUID {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
package be.simplenotes.app.utils
|
||||
|
||||
import kotlinx.serialization.json.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
interface StaticFileResolver {
|
||||
fun resolve(name: String): String?
|
||||
}
|
||||
|
||||
class StaticFileResolverImpl : StaticFileResolver {
|
||||
class StaticFileResolverImpl(json: Json) : StaticFileResolver {
|
||||
private val mappings: Map<String, String>
|
||||
|
||||
init {
|
||||
val json = Json {}
|
||||
val manifest = javaClass.getResource("/css-manifest.json").readText()
|
||||
val manifestObject = json.parseToJsonElement(manifest).jsonObject
|
||||
val keys = manifestObject.keys
|
||||
|
||||
@@ -3,8 +3,7 @@ package be.simplenotes.app.views
|
||||
import be.simplenotes.app.utils.StaticFileResolver
|
||||
import be.simplenotes.domain.security.JwtPayload
|
||||
import kotlinx.html.*
|
||||
import kotlinx.html.div
|
||||
import org.intellij.lang.annotations.Language
|
||||
import kotlinx.html.ThScope.col
|
||||
|
||||
class BaseView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) {
|
||||
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") {
|
||||
|
||||
unsafe {
|
||||
@Language("html")
|
||||
val html =
|
||||
"""
|
||||
<div aria-label="demo" class="md:order-1 order-2 flipped p-4 my-10 w-full md:w-1/2">
|
||||
<div class="flex justify-between mb-4">
|
||||
<h1 class="text-2xl underline">Notes</h1>
|
||||
<span>
|
||||
<span class="btn btn-teal pointer-events-none">Trash (3)</span>
|
||||
<span class="ml-2 btn btn-green pointer-events-none">New</span>
|
||||
</span>
|
||||
</div>
|
||||
<form class="md:space-x-2" id="search">
|
||||
<input aria-label="demo-search" name="search" disabled="" value="tag:"demo"">
|
||||
<span id="buttons">
|
||||
<button type="button" disabled="" class="btn btn-green pointer-events-none">search</button>
|
||||
<span class="btn btn-red pointer-events-none">clear</span>
|
||||
</span>
|
||||
</form>
|
||||
<div class="overflow-x-auto">
|
||||
<table id="notes">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="w-1/2">Title</th>
|
||||
<th scope="col" class="w-1/4">Updated</th>
|
||||
<th scope="col" class="w-1/4">Tags</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span class="text-blue-200 font-semibold underline">Formula 1</span></td>
|
||||
<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
|
||||
div("md:order-1 order-2 flipped p-4 my-10 w-full md:w-1/2") {
|
||||
attributes["aria-label"] = "demo"
|
||||
div("flex justify-between mb-4") {
|
||||
h1("text-2xl underline") { +"Notes" }
|
||||
span {
|
||||
span("btn btn-teal pointer-events-none") { +"Trash (3)" }
|
||||
span("ml-2 btn btn-green pointer-events-none") { +"New" }
|
||||
}
|
||||
}
|
||||
form(classes = "md:space-x-2") {
|
||||
id = "search"
|
||||
input {
|
||||
attributes["aria-label"] = "demo-search"
|
||||
attributes["name"] = "search"
|
||||
attributes["disabled"] = ""
|
||||
attributes["value"] = "tag:\"demo\""
|
||||
}
|
||||
span {
|
||||
id = "buttons"
|
||||
button(type = ButtonType.button, classes = "btn btn-green pointer-events-none") {
|
||||
attributes["disabled"] = ""
|
||||
+"search"
|
||||
}
|
||||
span("btn btn-red pointer-events-none") { +"clear" }
|
||||
}
|
||||
}
|
||||
div("overflow-x-auto") {
|
||||
demoTable()
|
||||
}
|
||||
}
|
||||
welcome()
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
div("w-full my-auto md:w-1/2 md:order-2 order-1 text-center") {
|
||||
div("m-4 rounded-lg p-6") {
|
||||
p("text-teal-400") {
|
||||
h2("text-3xl text-teal-400 underline") { +"Features:" }
|
||||
ul("list-disc text-lg list-inside") {
|
||||
li { +"Markdown support" }
|
||||
@@ -112,4 +106,3 @@ class BaseView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,14 +26,31 @@ class SettingView(staticFileResolver: StaticFileResolver) : View(staticFileResol
|
||||
}
|
||||
}
|
||||
|
||||
section("m-4 p-4 bg-gray-800 rounded") {
|
||||
p(classes = "mb-4") {
|
||||
+"Export all my data"
|
||||
section("m-4 p-4 bg-gray-800 rounded flex justify-around") {
|
||||
|
||||
form(method = FormMethod.post, action = "/export") {
|
||||
button(name = "display",
|
||||
classes = "inline btn btn-teal block",
|
||||
type = submit) { +"Display my data" }
|
||||
}
|
||||
|
||||
form(method = FormMethod.post, action = "/export") {
|
||||
button(name = "display", classes = "inline btn btn-teal block", type = submit) { +"Display my data" }
|
||||
button(name = "download", classes = "inline btn btn-green block ml-2 mt-2", type = submit) {
|
||||
|
||||
listOf("json", "zip").forEach { format ->
|
||||
div {
|
||||
radioInput(name = "format") {
|
||||
id = format
|
||||
attributes["value"] = format
|
||||
if(format == "json") attributes["checked"] = ""
|
||||
}
|
||||
label(classes = "ml-2") {
|
||||
attributes["for"] = format
|
||||
+format
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button(name = "download", classes = "inline btn btn-green block mt-2", type = submit) {
|
||||
+"Download my data"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ abstract class View(staticFileResolver: StaticFileResolver) {
|
||||
attributes["crossorigin"] = "anonymous"
|
||||
}
|
||||
link(rel = "stylesheet", href = styles)
|
||||
link(rel = "shortcut icon", href = "/favicon.ico", type = "image/x-icon")
|
||||
icons()
|
||||
scripts.forEach { 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")
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
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>
|
||||
|
After Width: | Height: | Size: 814 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
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 extractor = JwtPayloadExtractor(simpleJwt)
|
||||
private val ctx = RequestContexts()
|
||||
private val requiredAuth = AuthFilter(extractor, AuthType.Required, ctx)()
|
||||
private val optionalAuth = AuthFilter(extractor, AuthType.Optional, ctx)()
|
||||
private val requiredAuth = AuthFilter(extractor, AuthType.Required, ctx)
|
||||
private val optionalAuth = AuthFilter(extractor, AuthType.Optional, ctx)
|
||||
|
||||
private val echoJwtPayloadHandler = { request: Request -> Response(OK).body(request.jwtPayload(ctx).toString()) }
|
||||
|
||||
|
||||
@@ -11,7 +11,11 @@ simplenotes.be {
|
||||
import strict-transport
|
||||
header -Server
|
||||
|
||||
reverse_proxy http://localhost:8080
|
||||
reverse_proxy http://localhost:8080 {
|
||||
health_path /health
|
||||
health_interval 5s
|
||||
health_timeout 200ms
|
||||
}
|
||||
}
|
||||
|
||||
dev.simplenotes.be {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import configparser
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import appdirs
|
||||
import click
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self):
|
||||
config_dir: str = appdirs.user_config_dir("SimpleNotesCli")
|
||||
Path(config_dir).mkdir(exist_ok=True)
|
||||
self.file = os.path.join(config_dir, "config.ini")
|
||||
self.configparser = configparser.ConfigParser()
|
||||
|
||||
self.token = None
|
||||
self.base_url = None
|
||||
|
||||
def load(self):
|
||||
self.configparser.read(self.file)
|
||||
self.token = self.configparser.get("DEFAULT", "token", fallback=None)
|
||||
self.base_url = self.configparser.get(
|
||||
"DEFAULT", "base_url", fallback="https://simplenotes.be"
|
||||
)
|
||||
|
||||
def save(self):
|
||||
if self.token:
|
||||
self.configparser.set("DEFAULT", "token", self.token)
|
||||
|
||||
if self.base_url:
|
||||
self.configparser.set("DEFAULT", "base_url", self.base_url)
|
||||
|
||||
try:
|
||||
with open(self.file, "w") as f:
|
||||
self.configparser.write(f)
|
||||
except IOError:
|
||||
click.secho("An error occurred while saving config", fg="red", err=True)
|
||||
exit(1)
|
||||
@@ -0,0 +1,66 @@
|
||||
from typing import List, Optional
|
||||
|
||||
import click
|
||||
import requests
|
||||
from requests.models import Response
|
||||
|
||||
from domain import NoteMetadata
|
||||
|
||||
|
||||
class SimplenotesApi:
|
||||
def __init__(self, base_url: str):
|
||||
self.base_url = base_url
|
||||
self.s = requests.Session()
|
||||
self.s.hooks["response"] = [self.__exit_unauthorized]
|
||||
|
||||
def __url(self, path: str) -> str:
|
||||
return f"{self.base_url}{path}"
|
||||
|
||||
def __exit_unauthorized(self, response: Response, *args, **kwargs):
|
||||
if response.status_code == 401:
|
||||
click.secho("Unauthorized, please login again", fg="red", err=True)
|
||||
exit(1)
|
||||
|
||||
def login(self, username: str, password: str) -> Optional[str]:
|
||||
url = self.__url("/api/login")
|
||||
r = self.s.post(
|
||||
url,
|
||||
json={"username": username, "password": password},
|
||||
)
|
||||
if r.status_code == 200:
|
||||
return r.json()["token"]
|
||||
|
||||
def set_token(self, token: str):
|
||||
self.s.headers.update({"Authorization": f"Bearer {token}"})
|
||||
|
||||
def find_note(self, uuid: str) -> Optional[str]:
|
||||
url = self.__url(f"/api/notes/{uuid}")
|
||||
r = self.s.get(url)
|
||||
if r.status_code == 200:
|
||||
return r.json()["markdown"]
|
||||
|
||||
def list_notes(self) -> List[NoteMetadata]:
|
||||
url = self.__url("/api/notes")
|
||||
r = self.s.get(url)
|
||||
return list(map(lambda x: NoteMetadata(**x), r.json()))
|
||||
|
||||
def search_notes(self, query: str) -> List[NoteMetadata]:
|
||||
url = self.__url("/api/notes/search")
|
||||
r = self.s.post(url, json={"query": query})
|
||||
|
||||
if r.status_code == 200:
|
||||
j = r.json()
|
||||
return list(map(lambda x: NoteMetadata(**x), j))
|
||||
else:
|
||||
return []
|
||||
|
||||
def create_note(self, content: str) -> Optional[str]:
|
||||
url = self.__url("/api/notes/")
|
||||
r = self.s.post(url, json={"content": content})
|
||||
if r.status_code == 200:
|
||||
return r.json()["uuid"]
|
||||
|
||||
def update_note(self, uuid: str, content: str) -> bool:
|
||||
url = self.__url(f"/api/notes/{uuid}")
|
||||
r = self.s.put(url, json={"content": content})
|
||||
return r.status_code == 200
|
||||
@@ -0,0 +1,112 @@
|
||||
import click
|
||||
from click import Context
|
||||
|
||||
import SimpleNotesApi
|
||||
import utils
|
||||
from Config import Config
|
||||
from jwtutils import is_expired
|
||||
from utils import edit_md
|
||||
|
||||
api: SimpleNotesApi
|
||||
base_url: str
|
||||
conf = Config()
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.pass_context
|
||||
def cli(ctx: Context):
|
||||
global base_url
|
||||
global api
|
||||
|
||||
conf.load()
|
||||
base_url = conf.base_url
|
||||
api = SimpleNotesApi.SimplenotesApi(base_url)
|
||||
|
||||
if ctx.invoked_subcommand == "login" or ctx.invoked_subcommand == "config":
|
||||
return
|
||||
|
||||
token = conf.token
|
||||
if token is None:
|
||||
click.secho("Please login", err=True, fg="red")
|
||||
exit(1)
|
||||
elif is_expired(token):
|
||||
click.secho("Login expired, please login again", err=True, fg="red")
|
||||
exit(1)
|
||||
else:
|
||||
api.set_token(token)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--username", prompt=True)
|
||||
@click.option("--password", prompt=True, hide_input=True)
|
||||
def login(username: str, password: str):
|
||||
token = api.login(username, password)
|
||||
if token:
|
||||
conf.token = token
|
||||
conf.save()
|
||||
click.secho(f"Welcome {username}", fg="green")
|
||||
else:
|
||||
click.echo("Invalid credentials")
|
||||
exit(1)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--url", prompt=True)
|
||||
def config(url: str):
|
||||
conf.base_url = url
|
||||
conf.save()
|
||||
|
||||
|
||||
@cli.command(name="list")
|
||||
def list_notes():
|
||||
utils.print_notes(api.list_notes())
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("uuid")
|
||||
def edit(uuid: str):
|
||||
note = api.find_note(uuid)
|
||||
if not note:
|
||||
click.secho("Note not found", err=True, fg="red")
|
||||
exit(1)
|
||||
|
||||
edited = edit_md(note)
|
||||
|
||||
if edited == note:
|
||||
exit(1)
|
||||
|
||||
if not api.update_note(uuid, edited):
|
||||
click.secho("An error occurred", err=True, fg="red")
|
||||
exit(1)
|
||||
else:
|
||||
utils.print_note_url(uuid, "updated", conf.base_url)
|
||||
|
||||
|
||||
@cli.command()
|
||||
def new():
|
||||
placeholder = "---\ntitle: ''\ntags: []\n---\n"
|
||||
md = edit_md(placeholder)
|
||||
if md == placeholder:
|
||||
exit(1)
|
||||
uuid = api.create_note(md)
|
||||
if uuid:
|
||||
utils.print_note_url(uuid, "created", conf.base_url)
|
||||
else:
|
||||
click.secho("An error occurred", err=True, fg="red")
|
||||
exit(1)
|
||||
|
||||
|
||||
@cli.command(name="search")
|
||||
@click.argument("search", nargs=-1, required=True)
|
||||
def search_notes(search: str):
|
||||
query = " ".join(search)
|
||||
notes = api.search_notes(query)
|
||||
|
||||
if not notes:
|
||||
print("No match")
|
||||
else:
|
||||
utils.print_notes(notes)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
@@ -0,0 +1,12 @@
|
||||
from typing import List
|
||||
|
||||
|
||||
class NoteMetadata:
|
||||
def __init__(self, uuid: str, title: str, tags: List[str], updatedAt: str):
|
||||
self.uuid = uuid
|
||||
self.title = title
|
||||
self.tags = tags
|
||||
self.updated_at = updatedAt
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"NoteMetadata(uuid={self.uuid}, title={self.title}, tags={self.tags})"
|
||||
@@ -0,0 +1,10 @@
|
||||
from datetime import datetime
|
||||
|
||||
import jwt
|
||||
|
||||
|
||||
def is_expired(token: str) -> bool:
|
||||
exp = jwt.decode(token, verify=False)["exp"]
|
||||
dt = datetime.utcfromtimestamp(exp)
|
||||
now = datetime.utcnow()
|
||||
return dt < now
|
||||
@@ -0,0 +1,6 @@
|
||||
requests==2.24.0
|
||||
python-editor==1.0.4
|
||||
click==7.1.2
|
||||
PyJWT==1.7.1
|
||||
appdirs==1.4.4
|
||||
tabulate==0.8.7
|
||||
@@ -0,0 +1,32 @@
|
||||
from typing import List
|
||||
|
||||
import editor
|
||||
import click
|
||||
from tabulate import tabulate
|
||||
|
||||
from domain import NoteMetadata
|
||||
|
||||
|
||||
def edit_md(content: str = "") -> str:
|
||||
b = bytes(content, "utf-8")
|
||||
result = editor.edit(contents=b, suffix=".md")
|
||||
return result.decode("utf-8")
|
||||
|
||||
|
||||
def print_notes(notes: List[NoteMetadata]):
|
||||
data = []
|
||||
for n in notes:
|
||||
uuid = click.style(n.uuid, fg="blue")
|
||||
title = click.style(n.title, fg="green", bold=True)
|
||||
tags = ["#" + e for e in n.tags]
|
||||
tags = " ".join(tags)
|
||||
data.append([uuid, title, tags])
|
||||
|
||||
headers = ["UUID", "title", "tags"]
|
||||
click.echo(tabulate(data, headers=headers))
|
||||
|
||||
|
||||
def print_note_url(uuid: str, action: str, base_url: str):
|
||||
url = f"{base_url}/notes/{uuid}"
|
||||
s = click.style(url, fg="green", underline=True)
|
||||
click.echo(f"Note {action}: {s}")
|
||||
@@ -19,8 +19,10 @@ services:
|
||||
volumes:
|
||||
- notes-db-volume:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
|
||||
timeout: 10s
|
||||
test: "mysql --protocol=tcp -u simplenotes -p$MYSQL_PASSWORD -e 'show databases'"
|
||||
interval: 5s
|
||||
timeout: 1s
|
||||
start_period: 2s
|
||||
retries: 10
|
||||
|
||||
simplenotes:
|
||||
@@ -39,6 +41,12 @@ services:
|
||||
# - PASSWORD
|
||||
ports:
|
||||
- 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:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -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>
|
||||
<artifactId>parent</artifactId>
|
||||
<groupId>be.simplenotes</groupId>
|
||||
@@ -22,6 +24,30 @@
|
||||
<scope>test</scope>
|
||||
</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>
|
||||
<groupId>io.konform</groupId>
|
||||
<artifactId>konform-jvm</artifactId>
|
||||
@@ -59,7 +85,12 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlinx</groupId>
|
||||
<artifactId>kotlinx-serialization-runtime</artifactId>
|
||||
<artifactId>kotlinx-serialization-json-jvm</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-compress</artifactId>
|
||||
<version>1.20</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -21,12 +21,17 @@ import org.koin.dsl.module
|
||||
val domainModule = module {
|
||||
single<LoginUseCase> { LoginUseCaseImpl(get(), get(), get()) }
|
||||
single<RegisterUseCase> { RegisterUseCaseImpl(get(), get()) }
|
||||
single<DeleteUseCase> { DeleteUseCaseImpl(get(), get()) }
|
||||
single<DeleteUseCase> { DeleteUseCaseImpl(get(), get(), get()) }
|
||||
single { UserService(get(), get(), get(), get()) }
|
||||
single<PasswordHash> { BcryptPasswordHash() }
|
||||
single { SimpleJwt(get()) }
|
||||
single { JwtPayloadExtractor(get()) }
|
||||
single { NoteService(get(), get(), get(), get()) }
|
||||
single<MarkdownConverter> { MarkdownConverterImpl() }
|
||||
single<ExportUseCase> { ExportUseCaseImpl(get()) }
|
||||
single {
|
||||
NoteService(get(), get(), get(), get()).apply {
|
||||
dropAllIndexes()
|
||||
indexAll()
|
||||
}
|
||||
}
|
||||
single<MarkdownConverter> { MarkdownConverterImpl() }
|
||||
single<ExportUseCase> { ExportUseCaseImpl(get(), get()) }
|
||||
}
|
||||
|
||||
@@ -5,30 +5,34 @@ import kotlinx.serialization.Serializable
|
||||
import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
|
||||
@Serializable
|
||||
data class NoteMetadata(
|
||||
val title: String,
|
||||
val tags: List<String>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PersistedNoteMetadata(
|
||||
val title: String,
|
||||
val tags: List<String>,
|
||||
val updatedAt: LocalDateTime,
|
||||
val uuid: UUID,
|
||||
@Contextual val updatedAt: LocalDateTime,
|
||||
@Contextual val uuid: UUID,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Note(
|
||||
val meta: NoteMetadata,
|
||||
val markdown: String,
|
||||
val html: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PersistedNote(
|
||||
val meta: NoteMetadata,
|
||||
val markdown: String,
|
||||
val html: String,
|
||||
val updatedAt: LocalDateTime,
|
||||
val uuid: UUID,
|
||||
@Contextual val updatedAt: LocalDateTime,
|
||||
@Contextual val uuid: UUID,
|
||||
val public: Boolean,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package be.simplenotes.domain.usecases.export
|
||||
|
||||
import java.io.InputStream
|
||||
|
||||
interface ExportUseCase {
|
||||
fun export(userId: Int): String
|
||||
fun exportAsJson(userId: Int): String
|
||||
fun exportAsZip(userId: Int): InputStream
|
||||
}
|
||||
|
||||
@@ -2,42 +2,49 @@ package be.simplenotes.domain.usecases.export
|
||||
|
||||
import be.simplenotes.domain.model.ExportedNote
|
||||
import be.simplenotes.domain.usecases.repositories.NoteRepository
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import java.time.LocalDateTime
|
||||
|
||||
internal class ExportUseCaseImpl(private val noteRepository: NoteRepository) : ExportUseCase {
|
||||
override fun export(userId: Int): String {
|
||||
val module = SerializersModule {
|
||||
contextual(LocalDateTime::class, LocalDateTimeSerializer)
|
||||
}
|
||||
|
||||
val json = Json {
|
||||
prettyPrint = true
|
||||
serializersModule = module
|
||||
}
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
|
||||
internal class ExportUseCaseImpl(private val noteRepository: NoteRepository, private val json: Json) : ExportUseCase {
|
||||
override fun exportAsJson(userId: Int): String {
|
||||
val notes = noteRepository.export(userId)
|
||||
return json.encodeToString(ListSerializer(ExportedNote.serializer()), notes)
|
||||
}
|
||||
|
||||
private fun sanitizeFilename(inputName: String): String = inputName.replace("[^a-zA-Z0-9-_.]".toRegex(), "_")
|
||||
|
||||
override fun exportAsZip(userId: Int): InputStream {
|
||||
val notes = noteRepository.export(userId)
|
||||
val zipOutput = ZipOutput()
|
||||
zipOutput.use { zip ->
|
||||
notes.forEach {
|
||||
val name = sanitizeFilename(it.title)
|
||||
zip.write("notes/$name.md", it.markdown)
|
||||
}
|
||||
}
|
||||
return ByteArrayInputStream(zipOutput.outputStream.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
internal object LocalDateTimeSerializer : KSerializer<LocalDateTime> {
|
||||
override val descriptor: SerialDescriptor
|
||||
get() = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: LocalDateTime) {
|
||||
encoder.encodeString(value.toString())
|
||||
class ZipOutput : AutoCloseable {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
private val zipOutputStream = ZipArchiveOutputStream(outputStream)
|
||||
|
||||
fun write(path: String, content: String) {
|
||||
val entry = ZipArchiveEntry(path)
|
||||
zipOutputStream.putArchiveEntry(entry)
|
||||
zipOutputStream.write(content.toByteArray())
|
||||
zipOutputStream.closeArchiveEntry()
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): LocalDateTime {
|
||||
TODO("Not implemented, isn't needed")
|
||||
override fun close() {
|
||||
zipOutputStream.finish()
|
||||
zipOutputStream.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@ import arrow.core.extensions.fx
|
||||
import arrow.core.rightIfNotNull
|
||||
import be.simplenotes.domain.security.PasswordHash
|
||||
import be.simplenotes.domain.usecases.repositories.UserRepository
|
||||
import be.simplenotes.domain.usecases.search.NoteSearcher
|
||||
import be.simplenotes.domain.validation.UserValidations
|
||||
|
||||
internal class DeleteUseCaseImpl(
|
||||
private val userRepository: UserRepository,
|
||||
private val passwordHash: PasswordHash,
|
||||
private val searcher: NoteSearcher,
|
||||
) : DeleteUseCase {
|
||||
override fun delete(form: DeleteForm) = Either.fx<DeleteError, Unit> {
|
||||
val user = !UserValidations.validateDelete(form)
|
||||
@@ -20,5 +22,6 @@ internal class DeleteUseCaseImpl(
|
||||
{ DeleteError.WrongPassword }
|
||||
)
|
||||
!Either.cond(userRepository.delete(persistedUser.id), { Unit }, { DeleteError.Unregistered })
|
||||
searcher.dropIndex(persistedUser.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package be.simplenotes.domain.usecases.users.login
|
||||
|
||||
import arrow.core.Either
|
||||
import io.konform.validation.ValidationErrors
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
sealed class LoginError
|
||||
object Unregistered : LoginError()
|
||||
@@ -10,6 +11,7 @@ class InvalidLoginForm(val validationErrors: ValidationErrors) : LoginError()
|
||||
|
||||
typealias Token = String
|
||||
|
||||
@Serializable
|
||||
data class LoginForm(val username: String?, val password: String?)
|
||||
|
||||
interface LoginUseCase {
|
||||
|
||||
@@ -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>
|
||||
<artifactId>parent</artifactId>
|
||||
<groupId>be.simplenotes</groupId>
|
||||
@@ -9,6 +11,10 @@
|
||||
<artifactId>persistance</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>be.simplenotes</groupId>
|
||||
<artifactId>domain</artifactId>
|
||||
@@ -27,6 +33,17 @@
|
||||
<scope>test</scope>
|
||||
</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>
|
||||
<groupId>org.mariadb.jdbc</groupId>
|
||||
<artifactId>mariadb-java-client</artifactId>
|
||||
@@ -50,12 +67,10 @@
|
||||
<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>
|
||||
</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
|
||||
|
||||
import be.simplenotes.persistance.utils.DbType
|
||||
import be.simplenotes.persistance.utils.type
|
||||
import be.simplenotes.shared.config.DataSourceConfig
|
||||
import org.flywaydb.core.Flyway
|
||||
import javax.sql.DataSource
|
||||
|
||||
interface DbMigrations {
|
||||
fun migrate()
|
||||
}
|
||||
|
||||
internal class DbMigrationsImpl(
|
||||
private val dataSource: DataSource,
|
||||
private val dataSourceConfig: DataSourceConfig
|
||||
private val dataSourceConfig: DataSourceConfig,
|
||||
) : DbMigrations {
|
||||
override fun migrate() {
|
||||
|
||||
val migrationDir = when {
|
||||
dataSourceConfig.jdbcUrl.contains("mariadb") -> "db/migration/mariadb"
|
||||
else -> "db/migration/other"
|
||||
val migrationDir = when (dataSourceConfig.type()) {
|
||||
DbType.H2 -> "db/migration/other"
|
||||
DbType.MariaDb -> "db/migration/mariadb"
|
||||
}
|
||||
|
||||
Flyway.configure()
|
||||
@@ -2,19 +2,22 @@ package be.simplenotes.persistance
|
||||
|
||||
import be.simplenotes.domain.usecases.repositories.NoteRepository
|
||||
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.users.UserRepositoryImpl
|
||||
import be.simplenotes.shared.config.DataSourceConfig
|
||||
import com.zaxxer.hikari.HikariConfig
|
||||
import com.zaxxer.hikari.HikariDataSource
|
||||
import me.liuwj.ktorm.database.*
|
||||
import me.liuwj.ktorm.database.Database
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
import org.koin.dsl.onClose
|
||||
import org.mapstruct.factory.Mappers
|
||||
import javax.sql.DataSource
|
||||
|
||||
interface DbMigrations {
|
||||
fun migrate()
|
||||
}
|
||||
|
||||
private fun hikariDataSource(conf: DataSourceConfig): HikariDataSource {
|
||||
val hikariConfig = HikariConfig().also {
|
||||
it.jdbcUrl = conf.jdbcUrl
|
||||
@@ -27,10 +30,19 @@ private fun hikariDataSource(conf: DataSourceConfig): HikariDataSource {
|
||||
return HikariDataSource(hikariConfig)
|
||||
}
|
||||
|
||||
val persistanceModule = module {
|
||||
single<UserRepository> { UserRepositoryImpl(get()) }
|
||||
single<NoteRepository> { NoteRepositoryImpl(get()) }
|
||||
val migrationModule = module {
|
||||
single<DbMigrations> { DbMigrationsImpl(get(), get()) }
|
||||
single<DataSource> { hikariDataSource(get()) }
|
||||
single { Database.connect(get<DataSource>()) }
|
||||
}
|
||||
|
||||
val persistanceModule = module {
|
||||
single<NoteConverter> { NoteConverterImpl() }
|
||||
single<UserConverter> { UserConverterImpl() }
|
||||
single<UserRepository> { UserRepositoryImpl(get(), get()) }
|
||||
single<NoteRepository> { NoteRepositoryImpl(get(), get()) }
|
||||
single { hikariDataSource(get()) } bind DataSource::class onClose { it?.close() }
|
||||
single {
|
||||
get<DbMigrations>().migrate()
|
||||
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
|
||||
|
||||
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.persistance.converters.NoteConverter
|
||||
import me.liuwj.ktorm.database.Database
|
||||
import me.liuwj.ktorm.dsl.*
|
||||
import me.liuwj.ktorm.entity.*
|
||||
@@ -9,7 +13,7 @@ import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
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)
|
||||
override fun findAll(
|
||||
@@ -17,7 +21,7 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
|
||||
limit: Int,
|
||||
offset: Int,
|
||||
tag: String?,
|
||||
deleted: Boolean
|
||||
deleted: Boolean,
|
||||
): List<PersistedNoteMetadata> {
|
||||
require(limit > 0) { "limit should be positive" }
|
||||
require(offset >= 0) { "offset should not be negative" }
|
||||
@@ -46,7 +50,7 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
|
||||
|
||||
return notes.map { note ->
|
||||
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 {
|
||||
val uuid = UUID.randomUUID()
|
||||
val entity = note.toEntity(uuid, userId).apply {
|
||||
this.updatedAt = LocalDateTime.now()
|
||||
}
|
||||
db.useTransaction {
|
||||
val entity = converter.toEntity(note, uuid, userId, LocalDateTime.now())
|
||||
db.notes.add(entity)
|
||||
db.batchInsert(Tags) {
|
||||
note.meta.tags.forEach { tagName ->
|
||||
@@ -69,9 +70,7 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entity.toPersistedNote(note.meta.tags)
|
||||
return converter.toPersistedNote(entity, note.meta.tags)
|
||||
}
|
||||
|
||||
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 }
|
||||
.map { it[Tags.name]!! }
|
||||
|
||||
return note.toPersistedNote(tags)
|
||||
return converter.toPersistedNote(note, tags)
|
||||
}
|
||||
|
||||
override fun update(userId: Int, uuid: UUID, note: Note): PersistedNote? {
|
||||
db.useTransaction {
|
||||
|
||||
val now = LocalDateTime.now()
|
||||
val count = db.update(Notes) {
|
||||
it.title to note.meta.title
|
||||
@@ -116,39 +113,26 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
|
||||
}
|
||||
}
|
||||
|
||||
return PersistedNote(
|
||||
meta = note.meta,
|
||||
markdown = note.markdown,
|
||||
html = note.html,
|
||||
updatedAt = now,
|
||||
uuid = uuid,
|
||||
public = false, // TODO
|
||||
)
|
||||
}
|
||||
return converter.toPersistedNote(note, now, uuid)
|
||||
}
|
||||
|
||||
override fun delete(userId: Int, uuid: UUID, permanent: Boolean): Boolean {
|
||||
return if (!permanent) {
|
||||
db.useTransaction {
|
||||
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.useTransaction {
|
||||
} else
|
||||
db.delete(Notes) { it.uuid eq uuid and (it.userId eq userId) } == 1
|
||||
}
|
||||
}
|
||||
|
||||
override fun restore(userId: Int, uuid: UUID): Boolean {
|
||||
return db.useTransaction {
|
||||
db.update(Notes) {
|
||||
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)
|
||||
@@ -175,14 +159,8 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
|
||||
val tagsByUuid = notes.tagsByUuid()
|
||||
|
||||
return notes.map { note ->
|
||||
ExportedNote(
|
||||
title = note.title,
|
||||
tags = tagsByUuid[note.uuid] ?: emptyList(),
|
||||
markdown = note.markdown,
|
||||
html = note.html,
|
||||
updatedAt = note.updatedAt,
|
||||
trash = note.deleted,
|
||||
)
|
||||
val tags = tagsByUuid[note.uuid] ?: emptyList()
|
||||
converter.toExportedNote(note, tags)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +174,7 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
|
||||
|
||||
return notes.map { note ->
|
||||
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 }
|
||||
.map { it[Tags.name]!! }
|
||||
|
||||
return note.toPersistedNote(tags)
|
||||
return converter.toPersistedNote(note, tags)
|
||||
}
|
||||
|
||||
private fun List<NoteEntity>.tagsByUuid(): Map<UUID, List<String>> {
|
||||
|
||||
@@ -2,15 +2,12 @@
|
||||
|
||||
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.users.UserEntity
|
||||
import be.simplenotes.persistance.users.Users
|
||||
import me.liuwj.ktorm.database.*
|
||||
import me.liuwj.ktorm.entity.*
|
||||
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.*
|
||||
@@ -46,21 +43,3 @@ internal interface NoteEntity : Entity<NoteEntity> {
|
||||
|
||||
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.User
|
||||
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.entity.*
|
||||
import me.liuwj.ktorm.entity.any
|
||||
import me.liuwj.ktorm.entity.find
|
||||
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? {
|
||||
return try {
|
||||
db.useTransaction {
|
||||
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 }?.toPersistedUser()
|
||||
override fun find(id: Int) = db.users.find { it.id eq id }?.toPersistedUser()
|
||||
override fun find(username: String) = db.users.find { it.username eq username }
|
||||
?.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(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]!! }
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package be.simplenotes.persistance.users
|
||||
|
||||
import be.simplenotes.domain.model.PersistedUser
|
||||
import me.liuwj.ktorm.database.*
|
||||
import me.liuwj.ktorm.entity.*
|
||||
import me.liuwj.ktorm.schema.*
|
||||
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)
|
||||
@@ -18,11 +20,9 @@ internal open class Users(alias: String?) : Table<UserEntity>("Users", alias) {
|
||||
internal interface UserEntity : Entity<UserEntity> {
|
||||
companion object : Entity.Factory<UserEntity>()
|
||||
|
||||
val id: Int
|
||||
var id: Int
|
||||
var username: String
|
||||
var password: String
|
||||
}
|
||||
|
||||
internal fun UserEntity.toPersistedUser() = PersistedUser(username, password, id)
|
||||
|
||||
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,11 +4,16 @@ import be.simplenotes.domain.model.*
|
||||
import be.simplenotes.domain.usecases.repositories.NoteRepository
|
||||
import be.simplenotes.domain.usecases.repositories.UserRepository
|
||||
import be.simplenotes.persistance.DbMigrations
|
||||
import be.simplenotes.persistance.converters.NoteConverter
|
||||
import be.simplenotes.persistance.migrationModule
|
||||
import be.simplenotes.persistance.persistanceModule
|
||||
import be.simplenotes.shared.config.DataSourceConfig
|
||||
import me.liuwj.ktorm.database.*
|
||||
import me.liuwj.ktorm.dsl.*
|
||||
import me.liuwj.ktorm.entity.*
|
||||
import me.liuwj.ktorm.database.Database
|
||||
import me.liuwj.ktorm.dsl.eq
|
||||
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.assertThatThrownBy
|
||||
import org.flywaydb.core.Flyway
|
||||
@@ -16,6 +21,7 @@ import org.junit.jupiter.api.*
|
||||
import org.junit.jupiter.api.parallel.ResourceLock
|
||||
import org.koin.dsl.koinApplication
|
||||
import org.koin.dsl.module
|
||||
import org.mapstruct.factory.Mappers
|
||||
import java.sql.SQLIntegrityConstraintViolationException
|
||||
import java.util.*
|
||||
import javax.sql.DataSource
|
||||
@@ -27,7 +33,7 @@ internal class NoteRepositoryImplTest {
|
||||
}
|
||||
|
||||
private val koinApp = koinApplication {
|
||||
modules(persistanceModule, testModule)
|
||||
modules(persistanceModule, migrationModule, testModule)
|
||||
}
|
||||
|
||||
private fun dataSourceConfig() = DataSourceConfig(
|
||||
@@ -185,10 +191,12 @@ internal class NoteRepositoryImplTest {
|
||||
fun `find an existing note`() {
|
||||
createNote(user1.id, "1", listOf("a", "b"))
|
||||
|
||||
val converter = Mappers.getMapper(NoteConverter::class.java)
|
||||
|
||||
val note = db.notes.find { it.title eq "1" }!!
|
||||
.let { entity ->
|
||||
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))
|
||||
|
||||
@@ -3,6 +3,7 @@ package be.simplenotes.persistance.users
|
||||
import be.simplenotes.domain.model.User
|
||||
import be.simplenotes.domain.usecases.repositories.UserRepository
|
||||
import be.simplenotes.persistance.DbMigrations
|
||||
import be.simplenotes.persistance.migrationModule
|
||||
import be.simplenotes.persistance.persistanceModule
|
||||
import be.simplenotes.shared.config.DataSourceConfig
|
||||
import me.liuwj.ktorm.database.*
|
||||
@@ -28,7 +29,7 @@ internal class UserRepositoryImplTest {
|
||||
}
|
||||
|
||||
private val koinApp = koinApplication {
|
||||
modules(persistanceModule, testModule)
|
||||
modules(persistanceModule, migrationModule, testModule)
|
||||
}
|
||||
|
||||
private fun dataSourceConfig() = DataSourceConfig(
|
||||
|
||||
@@ -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>
|
||||
|
||||
<groupId>be.simplenotes</groupId>
|
||||
@@ -17,7 +19,7 @@
|
||||
|
||||
<properties>
|
||||
<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>
|
||||
|
||||
<kotlin.code.style>official</kotlin.code.style>
|
||||
@@ -27,6 +29,8 @@
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||
<kotlin.compiler.jvmTarget>${java.version}</kotlin.compiler.jvmTarget>
|
||||
|
||||
<org.mapstruct.version>1.4.1.Final</org.mapstruct.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -35,101 +39,83 @@
|
||||
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
</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>
|
||||
|
||||
<build>
|
||||
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
|
||||
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
|
||||
|
||||
<pluginManagement>
|
||||
<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>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M4</version>
|
||||
<version>3.0.0-M5</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.surefire</groupId>
|
||||
<artifactId>surefire-junit-platform</artifactId>
|
||||
<version>3.0.0-M4</version>
|
||||
<version>3.0.0-M5</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
|
||||
<plugins>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<version>3.1.2</version>
|
||||
<artifactId>maven-enforcer-plugin</artifactId>
|
||||
<version>3.0.0-M3</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>enforce</id>
|
||||
<goals>
|
||||
<goal>enforce</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<rules>
|
||||
<banDuplicatePomDependencyVersions/>
|
||||
<requireMavenVersion>
|
||||
<version>3.6</version>
|
||||
</requireMavenVersion>
|
||||
</rules>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<artifactId>kotlin-maven-plugin</artifactId>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<version>${kotlin.version}</version>
|
||||
<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>
|
||||
<id>compile</id>
|
||||
<phase>process-sources</phase>
|
||||
@@ -165,39 +151,81 @@
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib-jdk8</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>
|
||||
<artifactId>kotlin-bom</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlinx</groupId>
|
||||
<artifactId>kotlinx-serialization-runtime</artifactId>
|
||||
<version>1.0-M1-1.4.0-rc</version>
|
||||
<artifactId>kotlinx-serialization-json-jvm</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</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>
|
||||
</dependencyManagement>
|
||||
|
||||
|
||||
@@ -37,6 +37,22 @@
|
||||
<version>${lucene.version}</version>
|
||||
</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>
|
||||
<groupId>be.simplenotes</groupId>
|
||||
<artifactId>shared</artifactId>
|
||||
|
||||
@@ -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>
|
||||
<artifactId>parent</artifactId>
|
||||
<groupId>be.simplenotes</groupId>
|
||||
@@ -8,12 +10,24 @@
|
||||
|
||||
<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>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>2.6</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
|
||||