Compare commits

...

4 Commits

28 changed files with 211 additions and 224 deletions

View File

@ -1,10 +1,6 @@
## can be generated with `openssl rand -base64 32` # mariadb
JWT_SECRET=
#
## can be generated with `openssl rand -base64 32`
MYSQL_ROOT_PASSWORD= MYSQL_ROOT_PASSWORD=
#
## can be generated with `openssl rand -base64 32`
MYSQL_PASSWORD= MYSQL_PASSWORD=
# password should be the same as mysql_password # simplenotes
PASSWORD= DB_PASSWORD=
JWT_SECRET=

View File

@ -15,5 +15,4 @@
## Configuration ## Configuration
The app is configured with environments variables. The app is configured with environments variables.
If no match is found within the env, a default value is read from a properties file in /app/src/main/resources/application.properties. If no match is found within the env, a default value is read from a yaml file in simplenotes-app/src/main/resources/application.yaml.
Don't use the default values for secrets ! Every value inside *.env.dist* should be changed.

View File

@ -32,13 +32,13 @@ services:
- .env - .env
environment: environment:
- TZ=Europe/Brussels - TZ=Europe/Brussels
- HOST=0.0.0.0 - SERVER_HOST=0.0.0.0
- JDBCURL=jdbc:mariadb://db:3306/simplenotes - DB_JDBC_URL=jdbc:mariadb://db:3306/simplenotes
- DRIVERCLASSNAME=org.mariadb.jdbc.Driver - DB_DRIVER_CLASS_NAME=org.mariadb.jdbc.Driver
- USERNAME=simplenotes - DB_USERNAME=simplenotes
# .env: # .env:
# - JWT_SECRET # - JWT_SECRET
# - PASSWORD # - DB_PASSWORD
ports: ports:
- 127.0.0.1:8080:8080 - 127.0.0.1:8080:8080
healthcheck: healthcheck:

View File

@ -30,9 +30,6 @@ dependencies {
implementation(Libs.micronaut) implementation(Libs.micronaut)
kapt(Libs.micronautProcessor) kapt(Libs.micronautProcessor)
testImplementation(Libs.micronaut)
kaptTest(Libs.micronautProcessor)
testImplementation(Libs.junit) testImplementation(Libs.junit)
testImplementation(Libs.assertJ) testImplementation(Libs.assertJ)
testImplementation(Libs.http4kTestingHamkrest) testImplementation(Libs.http4kTestingHamkrest)

View File

@ -1,13 +1,13 @@
package be.simplenotes.app package be.simplenotes.app
import io.micronaut.context.annotation.Context
import org.http4k.server.Http4kServer import org.http4k.server.Http4kServer
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import javax.annotation.PostConstruct import javax.annotation.PostConstruct
import javax.annotation.PreDestroy import javax.annotation.PreDestroy
import javax.inject.Singleton
import be.simplenotes.config.ServerConfig as SimpleNotesServerConfig import be.simplenotes.config.ServerConfig as SimpleNotesServerConfig
@Context @Singleton
class Server( class Server(
private val config: SimpleNotesServerConfig, private val config: SimpleNotesServerConfig,
private val http4kServer: Http4kServer, private val http4kServer: Http4kServer,
@ -17,7 +17,7 @@ class Server(
@PostConstruct @PostConstruct
fun start(): Server { fun start(): Server {
http4kServer.start() http4kServer.start()
logger.info("Listening on http://${config.host}:${config.port}") logger.info("Listening on http://${config.host}:${http4kServer.port()}")
return this return this
} }

View File

@ -1,12 +1,10 @@
package be.simplenotes.app package be.simplenotes.app
import io.micronaut.context.ApplicationContext import io.micronaut.context.ApplicationContext
import java.lang.Runtime.getRuntime
fun main() { fun main() {
val ctx = ApplicationContext.run().start() val ctx = ApplicationContext.run()
Runtime.getRuntime().addShutdownHook( ctx.createBean(Server::class.java)
Thread { getRuntime().addShutdownHook(Thread { ctx.stop() })
ctx.stop()
}
)
} }

View File

@ -27,7 +27,7 @@ class ApiNoteController(
fun createNote(request: Request, loggedInUser: LoggedInUser): Response { fun createNote(request: Request, loggedInUser: LoggedInUser): Response {
val content = noteContentLens(request) val content = noteContentLens(request)
return noteService.create(loggedInUser.userId, content).fold( return noteService.create(loggedInUser, content).fold(
{ Response(BAD_REQUEST) }, { Response(BAD_REQUEST) },
{ uuidContentLens(UuidContent(it.uuid), Response(OK)) } { uuidContentLens(UuidContent(it.uuid), Response(OK)) }
) )
@ -45,7 +45,7 @@ class ApiNoteController(
fun update(request: Request, loggedInUser: LoggedInUser): Response { fun update(request: Request, loggedInUser: LoggedInUser): Response {
val content = noteContentLens(request) val content = noteContentLens(request)
return noteService.update(loggedInUser.userId, uuidLens(request), content).fold( return noteService.update(loggedInUser, uuidLens(request), content).fold(
{ {
Response(BAD_REQUEST) Response(BAD_REQUEST)
}, },

View File

@ -32,7 +32,7 @@ class NoteController(
val markdownForm = request.form("markdown") ?: "" val markdownForm = request.form("markdown") ?: ""
return noteService.create(loggedInUser.userId, markdownForm).fold( return noteService.create(loggedInUser, markdownForm).fold(
{ {
val html = when (it) { val html = when (it) {
MissingMeta -> view.noteEditor( MissingMeta -> view.noteEditor(
@ -112,7 +112,7 @@ class NoteController(
val markdownForm = request.form("markdown") ?: "" val markdownForm = request.form("markdown") ?: ""
return noteService.update(loggedInUser.userId, note.uuid, markdownForm).fold( return noteService.update(loggedInUser, note.uuid, markdownForm).fold(
{ {
val html = when (it) { val html = when (it) {
MissingMeta -> view.noteEditor( MissingMeta -> view.noteEditor(

View File

@ -44,7 +44,7 @@ class ApiRoutes(
"/" bind POST to transaction.then(::createNote), "/" bind POST to transaction.then(::createNote),
"/search" bind POST to ::search, "/search" bind POST to ::search,
"/{uuid}" bind GET to ::note, "/{uuid}" bind GET to ::note,
"/{uuid}" bind PUT to transaction.then(::note), "/{uuid}" bind PUT to transaction.then(::update),
) )
).withBasePath("/notes") ).withBasePath("/notes")
} }

View File

@ -14,4 +14,5 @@
<logger name="com.zaxxer.hikari" level="INFO"/> <logger name="com.zaxxer.hikari" level="INFO"/>
<logger name="org.flywaydb.core" level="INFO"/> <logger name="org.flywaydb.core" level="INFO"/>
<logger name="io.micronaut" level="INFO"/> <logger name="io.micronaut" level="INFO"/>
<logger name="io.micronaut.context.lifecycle" level="DEBUG"/>
</configuration> </configuration>

View File

@ -1,8 +1,11 @@
package be.simplenotes.config package be.simplenotes.config
import io.micronaut.context.annotation.ConfigurationInject
import io.micronaut.context.annotation.ConfigurationProperties
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
data class DataSourceConfig( @ConfigurationProperties("db")
data class DataSourceConfig @ConfigurationInject constructor(
val jdbcUrl: String, val jdbcUrl: String,
val driverClassName: String, val driverClassName: String,
val username: String, val username: String,
@ -14,7 +17,8 @@ data class DataSourceConfig(
"username='$username', password='***', maximumPoolSize=$maximumPoolSize, connectionTimeout=$connectionTimeout)" "username='$username', password='***', maximumPoolSize=$maximumPoolSize, connectionTimeout=$connectionTimeout)"
} }
data class JwtConfig( @ConfigurationProperties("jwt")
data class JwtConfig @ConfigurationInject constructor(
val secret: String, val secret: String,
val validity: Long, val validity: Long,
val timeUnit: TimeUnit, val timeUnit: TimeUnit,
@ -22,7 +26,8 @@ data class JwtConfig(
override fun toString() = "JwtConfig(secret='***', validity=$validity, timeUnit=$timeUnit)" override fun toString() = "JwtConfig(secret='***', validity=$validity, timeUnit=$timeUnit)"
} }
data class ServerConfig( @ConfigurationProperties("server")
data class ServerConfig @ConfigurationInject constructor(
val host: String, val host: String,
val port: Int, val port: Int,
) )

View File

@ -1,47 +0,0 @@
package be.simplenotes.config
import java.util.*
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@Singleton
class ConfigLoader {
//region Config loading
private val properties: Properties = javaClass
.getResource("/application.properties")
.openStream()
.use {
Properties().apply { load(it) }
}
private val env = System.getenv()
private fun value(key: String): String =
env[key.toUpperCase().replace(".", "_")]
?: properties.getProperty(key)
?: error("Missing config key $key")
//endregion
val jwtConfig
get() = JwtConfig(
secret = value("jwt.secret"),
validity = value("jwt.validity").toLong(),
timeUnit = TimeUnit.HOURS,
)
val dataSourceConfig
get() = DataSourceConfig(
jdbcUrl = value("jdbcUrl"),
driverClassName = value("driverClassName"),
username = value("username"),
password = value("password"),
maximumPoolSize = value("maximumPoolSize").toInt(),
connectionTimeout = value("connectionTimeout").toLong()
)
val serverConfig
get() = ServerConfig(
host = value("host"),
port = value("port").toInt(),
)
}

View File

@ -1,17 +0,0 @@
package be.simplenotes.config
import io.micronaut.context.annotation.Factory
import javax.inject.Singleton
@Factory
class ConfigModule {
@Singleton
internal fun dataSourceConfig(configLoader: ConfigLoader) = configLoader.dataSourceConfig
@Singleton
internal fun jwtConfig(configLoader: ConfigLoader) = configLoader.jwtConfig
@Singleton
internal fun serverConfig(configLoader: ConfigLoader) = configLoader.serverConfig
}

View File

@ -1,12 +0,0 @@
host=localhost
port=8080
#
jdbcUrl=jdbc:h2:./notes-db;
driverClassName=org.h2.Driver
username=h2
password=
maximumPoolSize=10
connectionTimeout=3000
#
jwt.secret=PliLvfk7l4WF+cZJk66LR5Mpnh+ocbvJ2wfUCK2UCms=
jwt.validity=24

View File

@ -0,0 +1,16 @@
db:
jdbc-url: jdbc:h2:./notes-db;
driver-class-name: org.h2.Driver
username: h2
password: ''
connection-timeout: 3000
maximum-pool-size: 10
jwt:
secret: 'PliLvfk7l4WF+cZJk66LR5Mpnh+ocbvJ2wfUCK2UCms='
validity: 24
time-unit: hours
server:
host: localhost
port: 8080

View File

@ -1,8 +1,13 @@
package be.simplenotes.domain.security package be.simplenotes.domain.security
import be.simplenotes.types.LoggedInUser
import org.owasp.html.HtmlChangeListener
import org.owasp.html.HtmlPolicyBuilder import org.owasp.html.HtmlPolicyBuilder
import org.slf4j.LoggerFactory
import javax.inject.Singleton
internal object HtmlSanitizer { @Singleton
class HtmlSanitizer {
private val htmlPolicy = HtmlPolicyBuilder() private val htmlPolicy = HtmlPolicyBuilder()
.allowElements("a") .allowElements("a")
.allowCommonBlockElements() .allowCommonBlockElements()
@ -16,5 +21,18 @@ internal object HtmlSanitizer {
.requireRelNofollowOnLinks() .requireRelNofollowOnLinks()
.toFactory()!! .toFactory()!!
fun sanitize(unsafeHtml: String) = htmlPolicy.sanitize(unsafeHtml)!! private val logger = LoggerFactory.getLogger(javaClass)
private val htmlChangeListener = object : HtmlChangeListener<LoggedInUser> {
override fun discardedTag(context: LoggedInUser?, elementName: String) {
logger.warn("Discarded tag $elementName for user $context")
}
override fun discardedAttributes(context: LoggedInUser?, tagName: String, vararg attributeNames: String) {
logger.warn("Discarded attributes ${attributeNames.contentToString()} on tag $tagName for user $context")
}
}
fun sanitize(userId: LoggedInUser, unsafeHtml: String) =
htmlPolicy.sanitize(unsafeHtml, htmlChangeListener, userId)!!
} }

View File

@ -8,10 +8,13 @@ import be.simplenotes.persistance.repositories.NoteRepository
import be.simplenotes.persistance.repositories.UserRepository import be.simplenotes.persistance.repositories.UserRepository
import be.simplenotes.search.NoteSearcher import be.simplenotes.search.NoteSearcher
import be.simplenotes.search.SearchTerms import be.simplenotes.search.SearchTerms
import be.simplenotes.types.LoggedInUser
import be.simplenotes.types.Note import be.simplenotes.types.Note
import be.simplenotes.types.PersistedNote import be.simplenotes.types.PersistedNote
import be.simplenotes.types.PersistedNoteMetadata import be.simplenotes.types.PersistedNoteMetadata
import java.util.* import java.util.*
import javax.annotation.PostConstruct
import javax.annotation.PreDestroy
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
@ -20,25 +23,30 @@ class NoteService(
private val noteRepository: NoteRepository, private val noteRepository: NoteRepository,
private val userRepository: UserRepository, private val userRepository: UserRepository,
private val searcher: NoteSearcher, private val searcher: NoteSearcher,
private val htmlSanitizer: HtmlSanitizer,
) { ) {
fun create(userId: Int, markdownText: String) = either.eager<MarkdownParsingError, PersistedNote> { fun create(user: LoggedInUser, markdownText: String) = either.eager<MarkdownParsingError, PersistedNote> {
val persistedNote = !markdownConverter.renderDocument(markdownText) val persistedNote = !markdownConverter.renderDocument(markdownText)
.map { it.copy(html = HtmlSanitizer.sanitize(it.html)) } .map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) }
.map { Note(it.metadata, markdown = markdownText, html = it.html) } .map { Note(it.metadata, markdown = markdownText, html = it.html) }
.map { noteRepository.create(userId, it) } .map { noteRepository.create(user.userId, it) }
searcher.indexNote(userId, persistedNote) searcher.indexNote(user.userId, persistedNote)
persistedNote persistedNote
} }
fun update(userId: Int, uuid: UUID, markdownText: String) = either.eager<MarkdownParsingError, PersistedNote?> { fun update(
user: LoggedInUser,
uuid: UUID,
markdownText: String,
) = either.eager<MarkdownParsingError, PersistedNote?> {
val persistedNote = !markdownConverter.renderDocument(markdownText) val persistedNote = !markdownConverter.renderDocument(markdownText)
.map { it.copy(html = HtmlSanitizer.sanitize(it.html)) } .map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) }
.map { Note(it.metadata, markdown = markdownText, html = it.html) } .map { Note(it.metadata, markdown = markdownText, html = it.html) }
.map { noteRepository.update(userId, uuid, it) } .map { noteRepository.update(user.userId, uuid, it) }
persistedNote?.let { searcher.updateIndex(userId, it) } persistedNote?.let { searcher.updateIndex(user.userId, it) }
persistedNote persistedNote
} }
@ -78,7 +86,9 @@ class NoteService(
fun countDeleted(userId: Int) = noteRepository.count(userId, deleted = true) fun countDeleted(userId: Int) = noteRepository.count(userId, deleted = true)
@PostConstruct
fun indexAll() { fun indexAll() {
dropAllIndexes()
val userIds = userRepository.findAll() val userIds = userRepository.findAll()
userIds.forEach { id -> userIds.forEach { id ->
val notes = noteRepository.findAllDetails(id) val notes = noteRepository.findAllDetails(id)
@ -88,6 +98,7 @@ class NoteService(
fun search(userId: Int, searchTerms: SearchTerms) = searcher.search(userId, searchTerms) fun search(userId: Int, searchTerms: SearchTerms) = searcher.search(userId, searchTerms)
@PreDestroy
fun dropAllIndexes() = searcher.dropAll() fun dropAllIndexes() = searcher.dropAll()
fun makePublic(userId: Int, uuid: UUID) = noteRepository.makePublic(userId, uuid) fun makePublic(userId: Int, uuid: UUID) = noteRepository.makePublic(userId, uuid)

View File

@ -0,0 +1,34 @@
package be.simplenotes.domain.usecases.markdown
import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension
import com.vladsch.flexmark.html.HtmlRenderer
import com.vladsch.flexmark.parser.Parser
import com.vladsch.flexmark.util.data.MutableDataSet
import io.micronaut.context.annotation.Factory
import javax.inject.Singleton
@Factory
class FlexmarkFactory {
@Singleton
fun parser(options: MutableDataSet) = Parser.builder(options).build()
@Singleton
fun htmlRenderer(options: MutableDataSet) = HtmlRenderer.builder(options).build()
@Singleton
fun options() = MutableDataSet().apply {
set(Parser.EXTENSIONS, listOf(TaskListExtension.create()))
set(TaskListExtension.TIGHT_ITEM_CLASS, "")
set(TaskListExtension.LOOSE_ITEM_CLASS, "")
set(
TaskListExtension.ITEM_DONE_MARKER,
"""<input type="checkbox" checked="checked" disabled="disabled" readonly="readonly" />&nbsp;"""
)
set(
TaskListExtension.ITEM_NOT_DONE_MARKER,
"""<input type="checkbox" disabled="disabled" readonly="readonly" />&nbsp;"""
)
set(HtmlRenderer.SOFT_BREAK, "<br>")
}
}

View File

@ -1,20 +1,8 @@
package be.simplenotes.domain.usecases.markdown package be.simplenotes.domain.usecases.markdown
import arrow.core.Either import arrow.core.Either
import arrow.core.computations.either
import arrow.core.left
import arrow.core.right
import be.simplenotes.domain.validation.NoteValidations
import be.simplenotes.types.NoteMetadata import be.simplenotes.types.NoteMetadata
import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension
import com.vladsch.flexmark.html.HtmlRenderer
import com.vladsch.flexmark.parser.Parser
import com.vladsch.flexmark.util.data.MutableDataSet
import io.konform.validation.ValidationErrors import io.konform.validation.ValidationErrors
import org.yaml.snakeyaml.Yaml
import org.yaml.snakeyaml.parser.ParserException
import org.yaml.snakeyaml.scanner.ScannerException
import javax.inject.Singleton
sealed class MarkdownParsingError sealed class MarkdownParsingError
object MissingMeta : MarkdownParsingError() object MissingMeta : MarkdownParsingError()
@ -23,63 +11,6 @@ class ValidationError(val validationErrors: ValidationErrors) : MarkdownParsingE
data class Document(val metadata: NoteMetadata, val html: String) data class Document(val metadata: NoteMetadata, val html: String)
typealias MetaMdPair = Pair<String, String>
interface MarkdownConverter { interface MarkdownConverter {
fun renderDocument(input: String): Either<MarkdownParsingError, Document> fun renderDocument(input: String): Either<MarkdownParsingError, Document>
} }
@Singleton
internal class MarkdownConverterImpl : MarkdownConverter {
private val yamlBoundPattern = "-{3}".toRegex()
private fun splitMetaFromDocument(input: String): Either<MissingMeta, MetaMdPair> {
val split = input.split(yamlBoundPattern, 3)
if (split.size < 3) return MissingMeta.left()
return (split[1].trim() to split[2].trim()).right()
}
private val yaml = Yaml()
private fun parseMeta(input: String): Either<InvalidMeta, NoteMetadata> {
val load: Map<String, Any> = try {
yaml.load(input)
} catch (e: ParserException) {
return InvalidMeta.left()
} catch (e: ScannerException) {
return InvalidMeta.left()
}
val title = when (val titleNode = load["title"]) {
is String, is Number -> titleNode.toString()
else -> return InvalidMeta.left()
}
val tagsNode = load["tags"]
val tags = if (tagsNode !is List<*>)
emptyList()
else
tagsNode.map { it.toString() }
return NoteMetadata(title, tags).right()
}
private val parser: Parser
private val renderer: HtmlRenderer
init {
val options = MutableDataSet()
options.set(Parser.EXTENSIONS, listOf(TaskListExtension.create()))
options.set(HtmlRenderer.SOFT_BREAK, "<br>")
parser = Parser.builder(options).build()
renderer = HtmlRenderer.builder(options).build()
}
private fun renderMarkdown(markdown: String) = parser.parse(markdown).run(renderer::render)
override fun renderDocument(input: String) = either.eager<MarkdownParsingError, Document> {
val (meta, md) = !splitMetaFromDocument(input)
val parsedMeta = !parseMeta(meta)
!Either.fromNullable(NoteValidations.validateMetadata(parsedMeta)).swap()
val html = renderMarkdown(md)
Document(parsedMeta, html)
}
}

View File

@ -0,0 +1,63 @@
package be.simplenotes.domain.usecases.markdown
import arrow.core.Either
import arrow.core.computations.either
import arrow.core.left
import arrow.core.right
import be.simplenotes.domain.validation.NoteValidations
import be.simplenotes.types.NoteMetadata
import com.vladsch.flexmark.html.HtmlRenderer
import com.vladsch.flexmark.parser.Parser
import org.yaml.snakeyaml.Yaml
import org.yaml.snakeyaml.parser.ParserException
import org.yaml.snakeyaml.scanner.ScannerException
import javax.inject.Singleton
private typealias MetaMdPair = Pair<String, String>
@Singleton
internal class MarkdownConverterImpl(
private val parser: Parser,
private val renderer: HtmlRenderer,
) : MarkdownConverter {
private val yamlBoundPattern = "-{3}".toRegex()
private fun splitMetaFromDocument(input: String): Either<MissingMeta, MetaMdPair> {
val split = input.split(yamlBoundPattern, 3)
if (split.size < 3) return MissingMeta.left()
return (split[1].trim() to split[2].trim()).right()
}
private val yaml = Yaml()
private fun parseMeta(input: String): Either<InvalidMeta, NoteMetadata> {
val load: Map<String, Any> = try {
yaml.load(input)
} catch (e: ParserException) {
return InvalidMeta.left()
} catch (e: ScannerException) {
return InvalidMeta.left()
}
val title = when (val titleNode = load["title"]) {
is String, is Number -> titleNode.toString()
else -> return InvalidMeta.left()
}
val tagsNode = load["tags"]
val tags = if (tagsNode !is List<*>)
emptyList()
else
tagsNode.map { it.toString() }
return NoteMetadata(title, tags).right()
}
private fun renderMarkdown(markdown: String) = parser.parse(markdown).run(renderer::render)
override fun renderDocument(input: String) = either.eager<MarkdownParsingError, Document> {
val (meta, md) = !splitMetaFromDocument(input)
val parsedMeta = !parseMeta(meta)
!Either.fromNullable(NoteValidations.validateMetadata(parsedMeta)).swap()
val html = renderMarkdown(md)
Document(parsedMeta, html)
}
}

View File

@ -16,16 +16,14 @@ dependencies {
implementation(Libs.hikariCP) implementation(Libs.hikariCP)
implementation(Libs.ktormCore) implementation(Libs.ktormCore)
implementation(Libs.ktormMysql) implementation(Libs.ktormMysql)
implementation(Libs.logbackClassic)
implementation(Libs.mapstruct) compileOnly(Libs.mapstruct)
kapt(Libs.mapstructProcessor) kapt(Libs.mapstructProcessor)
implementation(Libs.micronaut) implementation(Libs.micronaut)
kapt(Libs.micronautProcessor) kapt(Libs.micronautProcessor)
testImplementation(Libs.micronaut)
kaptTest(Libs.micronautProcessor)
testImplementation(Libs.junit) testImplementation(Libs.junit)
testImplementation(Libs.assertJ) testImplementation(Libs.assertJ)
testImplementation(Libs.logbackClassic) testImplementation(Libs.logbackClassic)

View File

@ -6,6 +6,7 @@ import be.simplenotes.persistance.utils.type
import me.liuwj.ktorm.database.Database import me.liuwj.ktorm.database.Database
import me.liuwj.ktorm.database.asIterable import me.liuwj.ktorm.database.asIterable
import me.liuwj.ktorm.database.use import me.liuwj.ktorm.database.use
import java.io.EOFException
import java.sql.SQLTransientException import java.sql.SQLTransientException
import javax.inject.Singleton import javax.inject.Singleton
@ -27,5 +28,7 @@ internal class DbHealthCheckImpl(
}.any { it in dataSourceConfig.jdbcUrl } }.any { it in dataSourceConfig.jdbcUrl }
} catch (e: SQLTransientException) { } catch (e: SQLTransientException) {
false false
} catch (e: EOFException) {
false
} }
} }

View File

@ -1,26 +1,17 @@
package be.simplenotes.persistance package be.simplenotes.persistance
import be.simplenotes.config.DataSourceConfig import be.simplenotes.config.DataSourceConfig
import be.simplenotes.persistance.converters.NoteConverter
import be.simplenotes.persistance.converters.UserConverter
import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource import com.zaxxer.hikari.HikariDataSource
import io.micronaut.context.annotation.Bean import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory import io.micronaut.context.annotation.Factory
import me.liuwj.ktorm.database.Database import me.liuwj.ktorm.database.Database
import org.mapstruct.factory.Mappers
import javax.inject.Singleton import javax.inject.Singleton
import javax.sql.DataSource import javax.sql.DataSource
@Factory @Factory
class PersistanceModule { class PersistanceModule {
@Singleton
internal fun noteConverter() = Mappers.getMapper(NoteConverter::class.java)
@Singleton
internal fun userConverter() = Mappers.getMapper(UserConverter::class.java)
@Singleton @Singleton
internal fun database(migrations: DbMigrations, dataSource: DataSource): Database { internal fun database(migrations: DbMigrations, dataSource: DataSource): Database {
migrations.migrate() migrations.migrate()

View File

@ -9,12 +9,13 @@ import org.mapstruct.Mappings
import org.mapstruct.ReportingPolicy import org.mapstruct.ReportingPolicy
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.* import java.util.*
import javax.inject.Singleton
/** @Mapper(
* This is an abstract class because kotlin default methods in interface are not seen as default in kapt uses = [NoteEntityFactory::class, UserEntityFactory::class],
* @see [KT-25960](https://youtrack.jetbrains.com/issue/KT-25960) unmappedTargetPolicy = ReportingPolicy.IGNORE,
*/ componentModel = "jsr330"
@Mapper(uses = [NoteEntityFactory::class, UserEntityFactory::class], unmappedTargetPolicy = ReportingPolicy.IGNORE) )
internal abstract class NoteConverter { internal abstract class NoteConverter {
fun toNote(entity: NoteEntity, tags: Tags) = fun toNote(entity: NoteEntity, tags: Tags) =
@ -80,4 +81,5 @@ internal abstract class NoteConverter {
typealias Tags = List<String> typealias Tags = List<String>
@Singleton
internal class NoteEntityFactory : Entity.Factory<NoteEntity>() internal class NoteEntityFactory : Entity.Factory<NoteEntity>()

View File

@ -6,8 +6,13 @@ import be.simplenotes.types.User
import me.liuwj.ktorm.entity.Entity import me.liuwj.ktorm.entity.Entity
import org.mapstruct.Mapper import org.mapstruct.Mapper
import org.mapstruct.ReportingPolicy import org.mapstruct.ReportingPolicy
import javax.inject.Singleton
@Mapper(uses = [UserEntityFactory::class], unmappedTargetPolicy = ReportingPolicy.IGNORE) @Mapper(
uses = [UserEntityFactory::class],
unmappedTargetPolicy = ReportingPolicy.IGNORE,
componentModel = "jsr330"
)
internal interface UserConverter { internal interface UserConverter {
fun toUser(userEntity: UserEntity): User fun toUser(userEntity: UserEntity): User
fun toPersistedUser(userEntity: UserEntity): PersistedUser fun toPersistedUser(userEntity: UserEntity): PersistedUser
@ -15,4 +20,5 @@ internal interface UserConverter {
fun toEntity(user: PersistedUser): UserEntity fun toEntity(user: PersistedUser): UserEntity
} }
@Singleton
internal class UserEntityFactory : Entity.Factory<UserEntity>() internal class UserEntityFactory : Entity.Factory<UserEntity>()

View File

@ -2,23 +2,25 @@ package be.simplenotes.persistance.converters
import be.simplenotes.persistance.notes.NoteEntity import be.simplenotes.persistance.notes.NoteEntity
import be.simplenotes.types.* import be.simplenotes.types.*
import io.micronaut.context.BeanContext
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.mapstruct.factory.Mappers
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.* import java.util.*
internal class NoteConverterTest { internal class NoteConverterTest {
private val ctx = BeanContext.run()
val converter = ctx.getBean(NoteConverter::class.java)
@Nested @Nested
@DisplayName("Entity -> Models") @DisplayName("Entity -> Models")
inner class EntityToModels { inner class EntityToModels {
@Test @Test
fun `convert NoteEntity to Note`() { fun `convert NoteEntity to Note`() {
val converter = Mappers.getMapper(NoteConverter::class.java)
val entity = NoteEntity { val entity = NoteEntity {
title = "title" title = "title"
markdown = "md" markdown = "md"
@ -39,7 +41,6 @@ internal class NoteConverterTest {
@Test @Test
fun `convert NoteEntity to ExportedNote`() { fun `convert NoteEntity to ExportedNote`() {
val converter = Mappers.getMapper(NoteConverter::class.java)
val entity = NoteEntity { val entity = NoteEntity {
title = "title" title = "title"
markdown = "md" markdown = "md"
@ -62,7 +63,6 @@ internal class NoteConverterTest {
@Test @Test
fun `convert NoteEntity to PersistedNoteMetadata`() { fun `convert NoteEntity to PersistedNoteMetadata`() {
val converter = Mappers.getMapper(NoteConverter::class.java)
val entity = NoteEntity { val entity = NoteEntity {
uuid = UUID.randomUUID() uuid = UUID.randomUUID()
title = "title" title = "title"
@ -89,7 +89,6 @@ internal class NoteConverterTest {
@Test @Test
fun `convert Note to NoteEntity`() { fun `convert Note to NoteEntity`() {
val converter = Mappers.getMapper(NoteConverter::class.java)
val note = Note(NoteMetadata("title", emptyList()), "md", "html") val note = Note(NoteMetadata("title", emptyList()), "md", "html")
val entity = converter.toEntity(note, UUID.randomUUID(), 2, LocalDateTime.MIN) val entity = converter.toEntity(note, UUID.randomUUID(), 2, LocalDateTime.MIN)
@ -103,7 +102,6 @@ internal class NoteConverterTest {
@Test @Test
fun `convert PersistedNoteMetadata to NoteEntity`() { fun `convert PersistedNoteMetadata to NoteEntity`() {
val converter = Mappers.getMapper(NoteConverter::class.java)
val persistedNoteMetadata = val persistedNoteMetadata =
PersistedNoteMetadata("title", emptyList(), LocalDateTime.MIN, UUID.randomUUID()) PersistedNoteMetadata("title", emptyList(), LocalDateTime.MIN, UUID.randomUUID())
val entity = converter.toEntity(persistedNoteMetadata) val entity = converter.toEntity(persistedNoteMetadata)
@ -116,7 +114,6 @@ internal class NoteConverterTest {
@Test @Test
fun `convert NoteMetadata to NoteEntity`() { fun `convert NoteMetadata to NoteEntity`() {
val converter = Mappers.getMapper(NoteConverter::class.java)
val noteMetadata = NoteMetadata("title", emptyList()) val noteMetadata = NoteMetadata("title", emptyList())
val entity = converter.toEntity(noteMetadata) val entity = converter.toEntity(noteMetadata)
@ -126,7 +123,6 @@ internal class NoteConverterTest {
@Test @Test
fun `convert PersistedNote to NoteEntity`() { fun `convert PersistedNote to NoteEntity`() {
val converter = Mappers.getMapper(NoteConverter::class.java)
val persistedNote = PersistedNote( val persistedNote = PersistedNote(
NoteMetadata("title", emptyList()), NoteMetadata("title", emptyList()),
markdown = "md", markdown = "md",
@ -148,7 +144,6 @@ internal class NoteConverterTest {
@Test @Test
fun `convert ExportedNote to NoteEntity`() { fun `convert ExportedNote to NoteEntity`() {
val converter = Mappers.getMapper(NoteConverter::class.java)
val exportedNote = ExportedNote( val exportedNote = ExportedNote(
"title", "title",
emptyList(), emptyList(),

View File

@ -3,15 +3,17 @@ package be.simplenotes.persistance.converters
import be.simplenotes.persistance.users.UserEntity import be.simplenotes.persistance.users.UserEntity
import be.simplenotes.types.PersistedUser import be.simplenotes.types.PersistedUser
import be.simplenotes.types.User import be.simplenotes.types.User
import io.micronaut.context.BeanContext
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.mapstruct.factory.Mappers
internal class UserConverterTest { internal class UserConverterTest {
private val ctx = BeanContext.run()
private val converter = ctx.getBean(UserConverter::class.java)
@Test @Test
fun `convert UserEntity to User`() { fun `convert UserEntity to User`() {
val converter = Mappers.getMapper(UserConverter::class.java)
val entity = UserEntity { val entity = UserEntity {
username = "test" username = "test"
password = "test2" password = "test2"
@ -24,7 +26,6 @@ internal class UserConverterTest {
@Test @Test
fun `convert UserEntity to PersistedUser`() { fun `convert UserEntity to PersistedUser`() {
val converter = Mappers.getMapper(UserConverter::class.java)
val entity = UserEntity { val entity = UserEntity {
username = "test" username = "test"
password = "test2" password = "test2"
@ -37,7 +38,6 @@ internal class UserConverterTest {
@Test @Test
fun `convert User to UserEntity`() { fun `convert User to UserEntity`() {
val converter = Mappers.getMapper(UserConverter::class.java)
val user = User("test", "test2") val user = User("test", "test2")
val entity = converter.toEntity(user) val entity = converter.toEntity(user)

View File

@ -16,7 +16,6 @@ import me.liuwj.ktorm.entity.toList
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.jupiter.api.* import org.junit.jupiter.api.*
import org.mapstruct.factory.Mappers
import java.sql.SQLIntegrityConstraintViolationException import java.sql.SQLIntegrityConstraintViolationException
internal abstract class BaseNoteRepositoryImplTest : DbTest() { internal abstract class BaseNoteRepositoryImplTest : DbTest() {
@ -142,7 +141,7 @@ internal abstract class BaseNoteRepositoryImplTest : DbTest() {
fun `find an existing note`() { fun `find an existing note`() {
val fakeNote = noteRepo.insertFakeNote(user1) val fakeNote = noteRepo.insertFakeNote(user1)
val converter = Mappers.getMapper(NoteConverter::class.java) val converter = beanContext.getBean(NoteConverter::class.java)
val note = db.notes.find { it.title eq fakeNote.meta.title }!! val note = db.notes.find { it.title eq fakeNote.meta.title }!!
.let { entity -> .let { entity ->