Compare commits
No commits in common. "7ad8b7039b45d02098d39aad55115b0e4f7b6785" and "a4bf998c5b6a09741ee7d7ec9ffaf02097734027" have entirely different histories.
7ad8b7039b
...
a4bf998c5b
@ -1 +1,6 @@
|
|||||||
|
# mariadb
|
||||||
|
MYSQL_ROOT_PASSWORD=
|
||||||
|
MYSQL_PASSWORD=
|
||||||
|
# simplenotes
|
||||||
|
DB_PASSWORD=
|
||||||
JWT_SECRET=
|
JWT_SECRET=
|
||||||
|
|||||||
@ -12,15 +12,18 @@ FROM alpine
|
|||||||
|
|
||||||
RUN apk add --no-cache curl
|
RUN apk add --no-cache curl
|
||||||
|
|
||||||
|
ENV APPLICATION_USER simplenotes
|
||||||
|
RUN adduser -D -g '' $APPLICATION_USER
|
||||||
|
|
||||||
RUN mkdir /app
|
RUN mkdir /app
|
||||||
RUN mkdir /app/data
|
RUN chown -R $APPLICATION_USER /app
|
||||||
|
|
||||||
|
USER $APPLICATION_USER
|
||||||
|
|
||||||
COPY --from=jdkbuilder /myjdk /myjdk
|
COPY --from=jdkbuilder /myjdk /myjdk
|
||||||
COPY app/build/libs/app-with-dependencies*.jar /app/simplenotes.jar
|
COPY app/build/libs/app-with-dependencies*.jar /app/simplenotes.jar
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
VOLUME /app/data
|
|
||||||
|
|
||||||
ENV SERVER_HOST 0.0.0.0
|
ENV SERVER_HOST 0.0.0.0
|
||||||
|
|
||||||
CMD [ \
|
CMD [ \
|
||||||
|
|||||||
@ -4,11 +4,7 @@ import io.micronaut.context.ApplicationContext
|
|||||||
import java.lang.Runtime.getRuntime
|
import java.lang.Runtime.getRuntime
|
||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
val env = if (System.getenv("ENV") == "dev") "dev" else "prod"
|
val ctx = ApplicationContext.run()
|
||||||
val ctx = ApplicationContext.builder()
|
|
||||||
.deduceEnvironment(false)
|
|
||||||
.environments(env)
|
|
||||||
.start()
|
|
||||||
ctx.createBean(Server::class.java)
|
ctx.createBean(Server::class.java)
|
||||||
getRuntime().addShutdownHook(Thread { ctx.stop() })
|
getRuntime().addShutdownHook(Thread { ctx.stop() })
|
||||||
}
|
}
|
||||||
|
|||||||
14
app/src/controllers/HealthCheckController.kt
Normal file
14
app/src/controllers/HealthCheckController.kt
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package be.simplenotes.app.controllers
|
||||||
|
|
||||||
|
import be.simplenotes.domain.HealthCheckService
|
||||||
|
import org.http4k.core.Request
|
||||||
|
import org.http4k.core.Response
|
||||||
|
import org.http4k.core.Status.Companion.OK
|
||||||
|
import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class HealthCheckController(private val healthCheckService: HealthCheckService) {
|
||||||
|
fun healthCheck(@Suppress("UNUSED_PARAMETER") request: Request) =
|
||||||
|
if (healthCheckService.isOk()) Response(OK) else Response(SERVICE_UNAVAILABLE)
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
package be.simplenotes.app.routes
|
package be.simplenotes.app.routes
|
||||||
|
|
||||||
import be.simplenotes.app.controllers.BaseController
|
import be.simplenotes.app.controllers.BaseController
|
||||||
|
import be.simplenotes.app.controllers.HealthCheckController
|
||||||
import be.simplenotes.app.controllers.NoteController
|
import be.simplenotes.app.controllers.NoteController
|
||||||
import be.simplenotes.app.controllers.UserController
|
import be.simplenotes.app.controllers.UserController
|
||||||
import be.simplenotes.app.filters.ImmutableFilter
|
import be.simplenotes.app.filters.ImmutableFilter
|
||||||
@ -18,6 +19,7 @@ import javax.inject.Singleton
|
|||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class BasicRoutes(
|
class BasicRoutes(
|
||||||
|
private val healthCheckController: HealthCheckController,
|
||||||
private val baseCtrl: BaseController,
|
private val baseCtrl: BaseController,
|
||||||
private val userCtrl: UserController,
|
private val userCtrl: UserController,
|
||||||
private val noteCtrl: NoteController,
|
private val noteCtrl: NoteController,
|
||||||
@ -50,6 +52,8 @@ class BasicRoutes(
|
|||||||
"/notes/public/{uuid}" bind GET to noteCtrl::public,
|
"/notes/public/{uuid}" bind GET to noteCtrl::public,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
|
"/health" bind GET to healthCheckController::healthCheck,
|
||||||
staticHandler
|
staticHandler
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,10 +16,13 @@ object Libs {
|
|||||||
|
|
||||||
object Drivers {
|
object Drivers {
|
||||||
const val h2 = "com.h2database:h2:1.4.200"
|
const val h2 = "com.h2database:h2:1.4.200"
|
||||||
|
const val mariadb = "org.mariadb.jdbc:mariadb-java-client:2.7.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
object Ktorm {
|
object Ktorm {
|
||||||
const val core = "org.ktorm:ktorm-core:3.3.0"
|
private const val version = "3.0.0"
|
||||||
|
const val core = "me.liuwj.ktorm:ktorm-core:$version"
|
||||||
|
const val mysql = "me.liuwj.ktorm:ktorm-support-mysql:$version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,10 @@
|
|||||||
|
db:
|
||||||
|
jdbc-url: jdbc:h2:./notes-db;
|
||||||
|
username: h2
|
||||||
|
password: ''
|
||||||
|
connection-timeout: 3000
|
||||||
|
maximum-pool-size: 10
|
||||||
|
|
||||||
jwt:
|
jwt:
|
||||||
secret: 'PliLvfk7l4WF+cZJk66LR5Mpnh+ocbvJ2wfUCK2UCms='
|
secret: 'PliLvfk7l4WF+cZJk66LR5Mpnh+ocbvJ2wfUCK2UCms='
|
||||||
validity: 24
|
validity: 24
|
||||||
@ -6,5 +13,3 @@ jwt:
|
|||||||
server:
|
server:
|
||||||
host: localhost
|
host: localhost
|
||||||
port: 8080
|
port: 8080
|
||||||
|
|
||||||
data-dir: ./data
|
|
||||||
|
|||||||
32
config/src/Config.kt
Normal file
32
config/src/Config.kt
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package be.simplenotes.config
|
||||||
|
|
||||||
|
import io.micronaut.context.annotation.ConfigurationInject
|
||||||
|
import io.micronaut.context.annotation.ConfigurationProperties
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@ConfigurationProperties("db")
|
||||||
|
data class DataSourceConfig @ConfigurationInject constructor(
|
||||||
|
val jdbcUrl: String,
|
||||||
|
val username: String,
|
||||||
|
val password: String,
|
||||||
|
val maximumPoolSize: Int,
|
||||||
|
val connectionTimeout: Long,
|
||||||
|
) {
|
||||||
|
override fun toString() = "DataSourceConfig(jdbcUrl='$jdbcUrl', username='$username', password='***', " +
|
||||||
|
"maximumPoolSize=$maximumPoolSize, connectionTimeout=$connectionTimeout)"
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigurationProperties("jwt")
|
||||||
|
data class JwtConfig @ConfigurationInject constructor(
|
||||||
|
val secret: String,
|
||||||
|
val validity: Long,
|
||||||
|
val timeUnit: TimeUnit,
|
||||||
|
) {
|
||||||
|
override fun toString() = "JwtConfig(secret='***', validity=$validity, timeUnit=$timeUnit)"
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigurationProperties("server")
|
||||||
|
data class ServerConfig @ConfigurationInject constructor(
|
||||||
|
val host: String,
|
||||||
|
val port: Int,
|
||||||
|
)
|
||||||
@ -1,58 +0,0 @@
|
|||||||
package be.simplenotes.config
|
|
||||||
|
|
||||||
import io.micronaut.context.annotation.*
|
|
||||||
import java.nio.file.Path
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
data class DataConfig(val dataDir: String)
|
|
||||||
|
|
||||||
data class DataSourceConfig(
|
|
||||||
val jdbcUrl: String,
|
|
||||||
val maximumPoolSize: Int,
|
|
||||||
val connectionTimeout: Long,
|
|
||||||
)
|
|
||||||
|
|
||||||
@ConfigurationProperties("jwt")
|
|
||||||
data class JwtConfig @ConfigurationInject constructor(
|
|
||||||
val secret: String,
|
|
||||||
val validity: Long,
|
|
||||||
val timeUnit: TimeUnit,
|
|
||||||
) {
|
|
||||||
override fun toString() = "JwtConfig(secret='***', validity=$validity, timeUnit=$timeUnit)"
|
|
||||||
}
|
|
||||||
|
|
||||||
@ConfigurationProperties("server")
|
|
||||||
data class ServerConfig @ConfigurationInject constructor(
|
|
||||||
val host: String,
|
|
||||||
val port: Int,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Factory
|
|
||||||
class ConfigFactory {
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@Requires(notEnv = ["test"])
|
|
||||||
fun datasourceConfig(dataConfig: DataConfig) = DataSourceConfig(
|
|
||||||
jdbcUrl = "jdbc:h2:" + Path.of(dataConfig.dataDir, "simplenotes").normalize().toAbsolutePath(),
|
|
||||||
maximumPoolSize = 10,
|
|
||||||
connectionTimeout = 1000
|
|
||||||
)
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@Requires(env = ["test"])
|
|
||||||
fun testDatasourceConfig() = DataSourceConfig(
|
|
||||||
jdbcUrl = "jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1",
|
|
||||||
maximumPoolSize = 2,
|
|
||||||
connectionTimeout = 1000
|
|
||||||
)
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@Requires(notEnv = ["test"])
|
|
||||||
fun dataConfig(@Property(name = "data-dir") dataDir: String) = DataConfig(dataDir)
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@Requires(env = ["test"])
|
|
||||||
fun testDataConfig() = DataConfig("/tmp")
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,19 +1,54 @@
|
|||||||
version: '2.2'
|
version: '2.2'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
simplenotes:
|
|
||||||
image: hubv/simplenotes
|
db:
|
||||||
container_name: simplenotes
|
image: mariadb:10.5.5
|
||||||
|
container_name: simplenotes-mariadb
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- PUID=1000
|
- PUID=1000
|
||||||
- PGID=1000
|
- PGID=1000
|
||||||
- TZ=Europe/Brussels
|
- TZ=Europe/Brussels
|
||||||
|
- MYSQL_DATABASE=simplenotes
|
||||||
|
- MYSQL_USER=simplenotes
|
||||||
|
# .env:
|
||||||
|
# - MYSQL_ROOT_PASSWORD
|
||||||
|
# - MYSQL_PASSWORD
|
||||||
|
volumes:
|
||||||
|
- notes-db-volume:/var/lib/mysql
|
||||||
|
healthcheck:
|
||||||
|
test: "mysql --protocol=tcp -u simplenotes -p$MYSQL_PASSWORD -e 'show databases'"
|
||||||
|
interval: 5s
|
||||||
|
timeout: 1s
|
||||||
|
start_period: 2s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
simplenotes:
|
||||||
|
image: hubv/simplenotes
|
||||||
|
container_name: simplenotes
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- TZ=Europe/Brussels
|
||||||
|
- DB_JDBC_URL=jdbc:mariadb://db:3306/simplenotes
|
||||||
|
- DB_USERNAME=simplenotes
|
||||||
# .env:
|
# .env:
|
||||||
# - JWT_SECRET
|
# - JWT_SECRET
|
||||||
|
# - DB_PASSWORD
|
||||||
ports:
|
ports:
|
||||||
- 127.0.0.1:8080:8080
|
- 127.0.0.1:8080:8080
|
||||||
volumes:
|
healthcheck:
|
||||||
- ./simplenotes-data:/app/data
|
test: "curl --fail -s http://localhost:8080/health"
|
||||||
|
interval: 5s
|
||||||
|
timeout: 1s
|
||||||
|
start_period: 2s
|
||||||
|
retries: 3
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
notes-db-volume:
|
||||||
|
|||||||
13
domain/src/HealthCheckService.kt
Normal file
13
domain/src/HealthCheckService.kt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package be.simplenotes.domain
|
||||||
|
|
||||||
|
import be.simplenotes.persistence.DbHealthCheck
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
interface HealthCheckService {
|
||||||
|
fun isOk(): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class HealthCheckServiceImpl(private val dbHealthCheck: DbHealthCheck) : HealthCheckService {
|
||||||
|
override fun isOk() = dbHealthCheck.isOk()
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ import be.simplenotes.domain.security.HtmlSanitizer
|
|||||||
import be.simplenotes.domain.utils.parseSearchTerms
|
import be.simplenotes.domain.utils.parseSearchTerms
|
||||||
import be.simplenotes.persistence.repositories.NoteRepository
|
import be.simplenotes.persistence.repositories.NoteRepository
|
||||||
import be.simplenotes.persistence.repositories.UserRepository
|
import be.simplenotes.persistence.repositories.UserRepository
|
||||||
|
import be.simplenotes.persistence.transactions.TransactionService
|
||||||
import be.simplenotes.search.NoteSearcher
|
import be.simplenotes.search.NoteSearcher
|
||||||
import be.simplenotes.types.LoggedInUser
|
import be.simplenotes.types.LoggedInUser
|
||||||
import be.simplenotes.types.Note
|
import be.simplenotes.types.Note
|
||||||
@ -21,34 +22,31 @@ 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
|
private val htmlSanitizer: HtmlSanitizer,
|
||||||
|
private val transaction: TransactionService,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun create(user: LoggedInUser, markdownText: String) = either.eager<MarkdownParsingError, PersistedNote> {
|
fun create(user: LoggedInUser, markdownText: String) = transaction.use {
|
||||||
markdownService.renderDocument(markdownText)
|
either.eager<MarkdownParsingError, PersistedNote> {
|
||||||
.map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) }
|
markdownService.renderDocument(markdownText)
|
||||||
.map { Note(title = it.metadata.title, tags = it.metadata.tags, markdown = markdownText, html = it.html) }
|
.map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) }
|
||||||
.map { noteRepository.create(user.userId, it) }
|
.map { Note(it.metadata, markdown = markdownText, html = it.html) }
|
||||||
.bind()
|
.map { noteRepository.create(user.userId, it) }
|
||||||
.also { searcher.indexNote(user.userId, it) }
|
.bind()
|
||||||
|
.also { searcher.indexNote(user.userId, it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(user: LoggedInUser, uuid: UUID, markdownText: String) =
|
fun update(user: LoggedInUser, uuid: UUID, markdownText: String) = transaction.use {
|
||||||
either.eager<MarkdownParsingError, PersistedNote?> {
|
either.eager<MarkdownParsingError, PersistedNote?> {
|
||||||
markdownService.renderDocument(markdownText)
|
markdownService.renderDocument(markdownText)
|
||||||
.map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) }
|
.map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) }
|
||||||
.map {
|
.map { Note(it.metadata, markdown = markdownText, html = it.html) }
|
||||||
Note(
|
|
||||||
title = it.metadata.title,
|
|
||||||
tags = it.metadata.tags,
|
|
||||||
markdown = markdownText,
|
|
||||||
html = it.html
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.map { noteRepository.update(user.userId, uuid, it) }
|
.map { noteRepository.update(user.userId, uuid, it) }
|
||||||
.bind()
|
.bind()
|
||||||
?.also { searcher.updateIndex(user.userId, it) }
|
?.also { searcher.updateIndex(user.userId, it) }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun paginatedNotes(
|
fun paginatedNotes(
|
||||||
userId: Int,
|
userId: Int,
|
||||||
@ -66,22 +64,22 @@ class NoteService(
|
|||||||
|
|
||||||
fun find(userId: Int, uuid: UUID) = noteRepository.find(userId, uuid)
|
fun find(userId: Int, uuid: UUID) = noteRepository.find(userId, uuid)
|
||||||
|
|
||||||
fun trash(userId: Int, uuid: UUID): Boolean {
|
fun trash(userId: Int, uuid: UUID): Boolean = transaction.use {
|
||||||
val res = noteRepository.delete(userId, uuid, permanent = false)
|
val res = noteRepository.delete(userId, uuid, permanent = false)
|
||||||
if (res) searcher.deleteIndex(userId, uuid)
|
if (res) searcher.deleteIndex(userId, uuid)
|
||||||
return res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restore(userId: Int, uuid: UUID): Boolean {
|
fun restore(userId: Int, uuid: UUID): Boolean = transaction.use {
|
||||||
val res = noteRepository.restore(userId, uuid)
|
val res = noteRepository.restore(userId, uuid)
|
||||||
if (res) find(userId, uuid)?.let { note -> searcher.indexNote(userId, note) }
|
if (res) find(userId, uuid)?.let { note -> searcher.indexNote(userId, note) }
|
||||||
return res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
fun delete(userId: Int, uuid: UUID): Boolean {
|
fun delete(userId: Int, uuid: UUID): Boolean = transaction.use {
|
||||||
val res = noteRepository.delete(userId, uuid, permanent = true)
|
val res = noteRepository.delete(userId, uuid, permanent = true)
|
||||||
if (res) searcher.deleteIndex(userId, uuid)
|
if (res) searcher.deleteIndex(userId, uuid)
|
||||||
return res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
fun countDeleted(userId: Int) = noteRepository.count(userId, deleted = true)
|
fun countDeleted(userId: Int) = noteRepository.count(userId, deleted = true)
|
||||||
@ -101,8 +99,8 @@ class NoteService(
|
|||||||
@PreDestroy
|
@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) = transaction.use { noteRepository.makePublic(userId, uuid) }
|
||||||
fun makePrivate(userId: Int, uuid: UUID) = noteRepository.makePrivate(userId, uuid)
|
fun makePrivate(userId: Int, uuid: UUID) = transaction.use { noteRepository.makePrivate(userId, uuid) }
|
||||||
fun findPublic(uuid: UUID) = noteRepository.findPublic(uuid)
|
fun findPublic(uuid: UUID) = noteRepository.findPublic(uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import be.simplenotes.domain.security.PasswordHash
|
|||||||
import be.simplenotes.domain.security.SimpleJwt
|
import be.simplenotes.domain.security.SimpleJwt
|
||||||
import be.simplenotes.domain.validation.UserValidations
|
import be.simplenotes.domain.validation.UserValidations
|
||||||
import be.simplenotes.persistence.repositories.UserRepository
|
import be.simplenotes.persistence.repositories.UserRepository
|
||||||
|
import be.simplenotes.persistence.transactions.TransactionService
|
||||||
import be.simplenotes.search.NoteSearcher
|
import be.simplenotes.search.NoteSearcher
|
||||||
import be.simplenotes.types.LoggedInUser
|
import be.simplenotes.types.LoggedInUser
|
||||||
import be.simplenotes.types.PersistedUser
|
import be.simplenotes.types.PersistedUser
|
||||||
@ -28,13 +29,16 @@ internal class UserServiceImpl(
|
|||||||
private val passwordHash: PasswordHash,
|
private val passwordHash: PasswordHash,
|
||||||
private val jwt: SimpleJwt<LoggedInUser>,
|
private val jwt: SimpleJwt<LoggedInUser>,
|
||||||
private val searcher: NoteSearcher,
|
private val searcher: NoteSearcher,
|
||||||
|
private val transactionService: TransactionService,
|
||||||
) : UserService {
|
) : UserService {
|
||||||
|
|
||||||
override fun register(form: RegisterForm) = UserValidations.validateRegister(form)
|
override fun register(form: RegisterForm) = transactionService.use {
|
||||||
.filterOrElse({ !userRepository.exists(it.username) }, { RegisterError.UserExists })
|
UserValidations.validateRegister(form)
|
||||||
.map { it.copy(password = passwordHash.crypt(it.password)) }
|
.filterOrElse({ !userRepository.exists(it.username) }, { RegisterError.UserExists })
|
||||||
.map { userRepository.create(it) }
|
.map { it.copy(password = passwordHash.crypt(it.password)) }
|
||||||
.leftIfNull { RegisterError.UserExists }
|
.map { userRepository.create(it) }
|
||||||
|
.leftIfNull { RegisterError.UserExists }
|
||||||
|
}
|
||||||
|
|
||||||
override fun login(form: LoginForm) = either.eager<LoginError, Token> {
|
override fun login(form: LoginForm) = either.eager<LoginError, Token> {
|
||||||
UserValidations.validateLogin(form)
|
UserValidations.validateLogin(form)
|
||||||
@ -46,16 +50,18 @@ internal class UserServiceImpl(
|
|||||||
.bind()
|
.bind()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun delete(form: DeleteForm) = either.eager<DeleteError, Unit> {
|
override fun delete(form: DeleteForm) = transactionService.use {
|
||||||
val user = !UserValidations.validateDelete(form)
|
either.eager<DeleteError, Unit> {
|
||||||
val persistedUser = !userRepository.find(user.username).rightIfNotNull { DeleteError.Unregistered }
|
val user = !UserValidations.validateDelete(form)
|
||||||
!Either.conditionally(
|
val persistedUser = !userRepository.find(user.username).rightIfNotNull { DeleteError.Unregistered }
|
||||||
passwordHash.verify(user.password, persistedUser.password),
|
!Either.conditionally(
|
||||||
{ DeleteError.WrongPassword },
|
passwordHash.verify(user.password, persistedUser.password),
|
||||||
{ }
|
{ DeleteError.WrongPassword },
|
||||||
)
|
{ }
|
||||||
!Either.conditionally(userRepository.delete(persistedUser.id), { DeleteError.Unregistered }, { })
|
)
|
||||||
searcher.dropIndex(persistedUser.id)
|
!Either.conditionally(userRepository.delete(persistedUser.id), { DeleteError.Unregistered }, { })
|
||||||
|
searcher.dropIndex(persistedUser.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import be.simplenotes.domain.security.UserJwtMapper
|
|||||||
import be.simplenotes.domain.testutils.isLeftOfType
|
import be.simplenotes.domain.testutils.isLeftOfType
|
||||||
import be.simplenotes.domain.testutils.isRight
|
import be.simplenotes.domain.testutils.isRight
|
||||||
import be.simplenotes.persistence.repositories.UserRepository
|
import be.simplenotes.persistence.repositories.UserRepository
|
||||||
|
import be.simplenotes.persistence.transactions.TransactionService
|
||||||
import be.simplenotes.types.PersistedUser
|
import be.simplenotes.types.PersistedUser
|
||||||
import com.natpryce.hamkrest.assertion.assertThat
|
import com.natpryce.hamkrest.assertion.assertThat
|
||||||
import com.natpryce.hamkrest.equalTo
|
import com.natpryce.hamkrest.equalTo
|
||||||
@ -22,12 +23,16 @@ internal class UserServiceTest {
|
|||||||
val passwordHash = BcryptPasswordHash(test = true)
|
val passwordHash = BcryptPasswordHash(test = true)
|
||||||
val jwtConfig = JwtConfig("a secret", 1, TimeUnit.HOURS)
|
val jwtConfig = JwtConfig("a secret", 1, TimeUnit.HOURS)
|
||||||
val simpleJwt = SimpleJwt(jwtConfig, UserJwtMapper())
|
val simpleJwt = SimpleJwt(jwtConfig, UserJwtMapper())
|
||||||
|
val noopTransactionService = object : TransactionService {
|
||||||
|
override fun <T> use(block: () -> T) = block()
|
||||||
|
}
|
||||||
|
|
||||||
val userService = UserServiceImpl(
|
val userService = UserServiceImpl(
|
||||||
userRepository = userRepository,
|
userRepository = userRepository,
|
||||||
passwordHash = passwordHash,
|
passwordHash = passwordHash,
|
||||||
jwt = simpleJwt,
|
jwt = simpleJwt,
|
||||||
searcher = mockk(),
|
searcher = mockk(),
|
||||||
|
transactionService = noopTransactionService
|
||||||
)
|
)
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
|
|||||||
@ -12,10 +12,12 @@ dependencies {
|
|||||||
implementation(project(":types"))
|
implementation(project(":types"))
|
||||||
implementation(project(":config"))
|
implementation(project(":config"))
|
||||||
|
|
||||||
|
implementation(Libs.Database.Drivers.mariadb)
|
||||||
implementation(Libs.Database.Drivers.h2)
|
implementation(Libs.Database.Drivers.h2)
|
||||||
implementation(Libs.Database.flyway)
|
implementation(Libs.Database.flyway)
|
||||||
implementation(Libs.Database.hikariCP)
|
implementation(Libs.Database.hikariCP)
|
||||||
implementation(Libs.Database.Ktorm.core)
|
implementation(Libs.Database.Ktorm.core)
|
||||||
|
runtimeOnly(Libs.Database.Ktorm.mysql)
|
||||||
|
|
||||||
implementation(Libs.Slf4J.api)
|
implementation(Libs.Slf4J.api)
|
||||||
runtimeOnly(Libs.Slf4J.logback)
|
runtimeOnly(Libs.Slf4J.logback)
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
CREATE VIEW NotesWithTags AS
|
|
||||||
SELECT n.*, coalesce(ARRAY_AGG(t.NAME), ARRAY[]) as tags
|
|
||||||
FROM Notes n
|
|
||||||
LEFT JOIN Tags t ON n.uuid = t.note_uuid
|
|
||||||
GROUP BY n.uuid
|
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
create table Users
|
||||||
|
(
|
||||||
|
id int auto_increment primary key,
|
||||||
|
username varchar(50) not null,
|
||||||
|
password varchar(255) not null,
|
||||||
|
|
||||||
|
constraint username unique (username)
|
||||||
|
) character set 'utf8mb4'
|
||||||
|
collate 'utf8mb4_general_ci';
|
||||||
|
|
||||||
|
create table Notes
|
||||||
|
(
|
||||||
|
uuid binary(16) not null primary key,
|
||||||
|
title varchar(50) not null,
|
||||||
|
markdown mediumtext not null,
|
||||||
|
html mediumtext not null,
|
||||||
|
user_id int not null,
|
||||||
|
updated_at datetime null,
|
||||||
|
|
||||||
|
constraint Notes_fk_user foreign key (user_id) references Users (id) on delete cascade
|
||||||
|
|
||||||
|
) character set 'utf8mb4'
|
||||||
|
collate 'utf8mb4_general_ci';
|
||||||
|
|
||||||
|
create index user_id on Notes (user_id);
|
||||||
|
|
||||||
|
create table Tags
|
||||||
|
(
|
||||||
|
id int auto_increment primary key,
|
||||||
|
name varchar(50) not null,
|
||||||
|
note_uuid binary(16) not null,
|
||||||
|
constraint Tags_fk_note foreign key (note_uuid) references Notes (uuid) on delete cascade
|
||||||
|
) character set 'utf8mb4'
|
||||||
|
collate 'utf8mb4_general_ci';
|
||||||
|
|
||||||
|
create index note_uuid on Tags (note_uuid);
|
||||||
@ -9,7 +9,7 @@ create table Users
|
|||||||
|
|
||||||
create table Notes
|
create table Notes
|
||||||
(
|
(
|
||||||
uuid uuid not null primary key,
|
uuid binary(16) not null primary key,
|
||||||
title varchar(50) not null,
|
title varchar(50) not null,
|
||||||
markdown mediumtext not null,
|
markdown mediumtext not null,
|
||||||
html mediumtext not null,
|
html mediumtext not null,
|
||||||
@ -17,6 +17,7 @@ create table Notes
|
|||||||
updated_at datetime null,
|
updated_at datetime null,
|
||||||
|
|
||||||
constraint Notes_fk_user foreign key (user_id) references Users (id) on delete cascade
|
constraint Notes_fk_user foreign key (user_id) references Users (id) on delete cascade
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
create index user_id on Notes (user_id);
|
create index user_id on Notes (user_id);
|
||||||
@ -25,7 +26,7 @@ create table Tags
|
|||||||
(
|
(
|
||||||
id int auto_increment primary key,
|
id int auto_increment primary key,
|
||||||
name varchar(50) not null,
|
name varchar(50) not null,
|
||||||
note_uuid uuid not null,
|
note_uuid binary(16) not null,
|
||||||
constraint Tags_fk_note foreign key (note_uuid) references Notes (uuid) on delete cascade
|
constraint Tags_fk_note foreign key (note_uuid) references Notes (uuid) on delete cascade
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
alter table Notes
|
||||||
|
add column deleted bool not null default false
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
alter table Notes
|
||||||
|
add column public bool not null default false
|
||||||
@ -1,55 +0,0 @@
|
|||||||
package be.simplenotes.persistence
|
|
||||||
|
|
||||||
import org.ktorm.database.Database
|
|
||||||
import org.ktorm.entity.Entity
|
|
||||||
import org.ktorm.entity.sequenceOf
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
internal val Database.users get() = sequenceOf(Users, withReferences = false)
|
|
||||||
internal val Database.notes get() = sequenceOf(Notes, withReferences = false)
|
|
||||||
internal val Database.tags get() = sequenceOf(Tags, withReferences = false)
|
|
||||||
internal val Database.noteWithTags get() = sequenceOf(NotesWithTags, withReferences = false)
|
|
||||||
|
|
||||||
internal interface UserEntity : Entity<UserEntity> {
|
|
||||||
companion object : Entity.Factory<UserEntity>()
|
|
||||||
|
|
||||||
var id: Int
|
|
||||||
var username: String
|
|
||||||
var password: String
|
|
||||||
}
|
|
||||||
|
|
||||||
internal interface NoteEntity : Entity<NoteEntity> {
|
|
||||||
companion object : Entity.Factory<NoteEntity>()
|
|
||||||
|
|
||||||
var uuid: UUID
|
|
||||||
var title: String
|
|
||||||
var markdown: String
|
|
||||||
var html: String
|
|
||||||
var updatedAt: LocalDateTime
|
|
||||||
var deleted: Boolean
|
|
||||||
var public: Boolean
|
|
||||||
var user: UserEntity
|
|
||||||
}
|
|
||||||
|
|
||||||
internal interface TagEntity : Entity<TagEntity> {
|
|
||||||
companion object : Entity.Factory<TagEntity>()
|
|
||||||
|
|
||||||
val id: Int
|
|
||||||
var name: String
|
|
||||||
var note: NoteEntity
|
|
||||||
}
|
|
||||||
|
|
||||||
internal interface NoteWithTagsEntity : Entity<NoteWithTagsEntity> {
|
|
||||||
companion object : Entity.Factory<NoteWithTagsEntity>()
|
|
||||||
|
|
||||||
var uuid: UUID
|
|
||||||
var title: String
|
|
||||||
var markdown: String
|
|
||||||
var html: String
|
|
||||||
var updatedAt: LocalDateTime
|
|
||||||
var deleted: Boolean
|
|
||||||
var public: Boolean
|
|
||||||
var user: UserEntity
|
|
||||||
val tags: List<String>
|
|
||||||
}
|
|
||||||
31
persistence/src/HealthCheck.kt
Normal file
31
persistence/src/HealthCheck.kt
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package be.simplenotes.persistence
|
||||||
|
|
||||||
|
import be.simplenotes.config.DataSourceConfig
|
||||||
|
import be.simplenotes.persistence.utils.DbType
|
||||||
|
import be.simplenotes.persistence.utils.type
|
||||||
|
import me.liuwj.ktorm.database.Database
|
||||||
|
import me.liuwj.ktorm.database.asIterable
|
||||||
|
import me.liuwj.ktorm.database.use
|
||||||
|
import java.sql.SQLException
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
interface DbHealthCheck {
|
||||||
|
fun isOk(): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
internal class DbHealthCheckImpl(
|
||||||
|
private val db: Database,
|
||||||
|
private val dataSourceConfig: DataSourceConfig,
|
||||||
|
) : DbHealthCheck {
|
||||||
|
override fun isOk() = if (dataSourceConfig.type() == DbType.H2) true
|
||||||
|
else try {
|
||||||
|
db.useConnection { connection ->
|
||||||
|
connection.prepareStatement("""SHOW DATABASES""").use {
|
||||||
|
it.executeQuery().asIterable().map { it.getString(1) }
|
||||||
|
}
|
||||||
|
}.any { it in dataSourceConfig.jdbcUrl }
|
||||||
|
} catch (e: SQLException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,8 @@
|
|||||||
package be.simplenotes.persistence
|
package be.simplenotes.persistence
|
||||||
|
|
||||||
|
import be.simplenotes.config.DataSourceConfig
|
||||||
|
import be.simplenotes.persistence.utils.DbType
|
||||||
|
import be.simplenotes.persistence.utils.type
|
||||||
import org.flywaydb.core.Flyway
|
import org.flywaydb.core.Flyway
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import javax.sql.DataSource
|
import javax.sql.DataSource
|
||||||
@ -9,11 +12,20 @@ interface DbMigrations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
internal class DbMigrationsImpl(private val dataSource: DataSource) : DbMigrations {
|
internal class DbMigrationsImpl(
|
||||||
|
private val dataSource: DataSource,
|
||||||
|
private val dataSourceConfig: DataSourceConfig,
|
||||||
|
) : DbMigrations {
|
||||||
override fun migrate() {
|
override fun migrate() {
|
||||||
|
|
||||||
|
val migrationDir = when (dataSourceConfig.type()) {
|
||||||
|
DbType.H2 -> "db/migration/other"
|
||||||
|
DbType.MariaDb -> "db/migration/mariadb"
|
||||||
|
}
|
||||||
|
|
||||||
Flyway.configure()
|
Flyway.configure()
|
||||||
.dataSource(dataSource)
|
.dataSource(dataSource)
|
||||||
.locations("db/migration")
|
.locations(migrationDir)
|
||||||
.load()
|
.load()
|
||||||
.migrate()
|
.migrate()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
package be.simplenotes.persistence
|
package be.simplenotes.persistence
|
||||||
|
|
||||||
import be.simplenotes.config.DataSourceConfig
|
import be.simplenotes.config.DataSourceConfig
|
||||||
import be.simplenotes.persistence.extensions.CustomSqlFormatter
|
|
||||||
import com.zaxxer.hikari.HikariConfig
|
import com.zaxxer.hikari.HikariConfig
|
||||||
import com.zaxxer.hikari.HikariDataSource
|
import com.zaxxer.hikari.HikariDataSource
|
||||||
import io.micronaut.context.annotation.Bean
|
import io.micronaut.context.annotation.Bean
|
||||||
import io.micronaut.context.annotation.Factory
|
import io.micronaut.context.annotation.Factory
|
||||||
import org.ktorm.database.Database
|
import me.liuwj.ktorm.database.Database
|
||||||
import org.ktorm.database.SqlDialect
|
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import javax.sql.DataSource
|
import javax.sql.DataSource
|
||||||
|
|
||||||
@ -17,10 +15,7 @@ class PersistenceModule {
|
|||||||
@Singleton
|
@Singleton
|
||||||
internal fun database(migrations: DbMigrations, dataSource: DataSource): Database {
|
internal fun database(migrations: DbMigrations, dataSource: DataSource): Database {
|
||||||
migrations.migrate()
|
migrations.migrate()
|
||||||
return Database.connect(dataSource, dialect = object : SqlDialect {
|
return Database.connect(dataSource)
|
||||||
override fun createSqlFormatter(database: Database, beautifySql: Boolean, indentSize: Int) =
|
|
||||||
CustomSqlFormatter(database, beautifySql, indentSize)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@ -28,12 +23,15 @@ class PersistenceModule {
|
|||||||
internal fun dataSource(conf: DataSourceConfig): HikariDataSource {
|
internal fun dataSource(conf: DataSourceConfig): HikariDataSource {
|
||||||
val hikariConfig = HikariConfig().also {
|
val hikariConfig = HikariConfig().also {
|
||||||
it.jdbcUrl = conf.jdbcUrl
|
it.jdbcUrl = conf.jdbcUrl
|
||||||
it.driverClassName = "org.h2.Driver"
|
it.driverClassName = when {
|
||||||
it.username = ""
|
conf.jdbcUrl.startsWith("jdbc:mariadb") -> "org.mariadb.jdbc.Driver"
|
||||||
it.password = ""
|
conf.jdbcUrl.startsWith("jdbc:h2") -> "org.h2.Driver"
|
||||||
|
else -> error("Unsupported database")
|
||||||
|
}
|
||||||
|
it.username = conf.username
|
||||||
|
it.password = conf.password
|
||||||
it.maximumPoolSize = conf.maximumPoolSize
|
it.maximumPoolSize = conf.maximumPoolSize
|
||||||
it.connectionTimeout = conf.connectionTimeout
|
it.connectionTimeout = conf.connectionTimeout
|
||||||
it.dataSourceProperties["CASE_INSENSITIVE_IDENTIFIERS"] = "TRUE"
|
|
||||||
}
|
}
|
||||||
return HikariDataSource(hikariConfig)
|
return HikariDataSource(hikariConfig)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,42 +0,0 @@
|
|||||||
package be.simplenotes.persistence
|
|
||||||
|
|
||||||
import be.simplenotes.persistence.extensions.varcharArray
|
|
||||||
import org.ktorm.schema.*
|
|
||||||
|
|
||||||
internal object Users : Table<UserEntity>("Users") {
|
|
||||||
val id = int("id").primaryKey().bindTo { it.id }
|
|
||||||
val username = varchar("username").bindTo { it.username }
|
|
||||||
val password = varchar("password").bindTo { it.password }
|
|
||||||
}
|
|
||||||
|
|
||||||
internal object Notes : Table<NoteEntity>("Notes") {
|
|
||||||
val uuid = uuid("uuid").primaryKey().bindTo { it.uuid }
|
|
||||||
val title = varchar("title").bindTo { it.title }
|
|
||||||
val markdown = text("markdown").bindTo { it.markdown }
|
|
||||||
val html = text("html").bindTo { it.html }
|
|
||||||
val userId = int("user_id").references(Users) { it.user }
|
|
||||||
val updatedAt = datetime("updated_at").bindTo { it.updatedAt }
|
|
||||||
val deleted = boolean("deleted").bindTo { it.deleted }
|
|
||||||
val public = boolean("public").bindTo { it.public }
|
|
||||||
val user get() = userId.referenceTable as Users
|
|
||||||
}
|
|
||||||
|
|
||||||
internal object Tags : Table<TagEntity>("Tags") {
|
|
||||||
val id = int("id").primaryKey().bindTo { it.id }
|
|
||||||
val name = varchar("name").bindTo { it.name }
|
|
||||||
val noteUuid = uuid("note_uuid").references(Notes) { it.note }
|
|
||||||
val note: Notes get() = noteUuid.referenceTable as Notes
|
|
||||||
}
|
|
||||||
|
|
||||||
internal object NotesWithTags : Table<NoteWithTagsEntity>("NotesWithTags") {
|
|
||||||
val uuid = uuid("uuid").primaryKey().bindTo { it.uuid }
|
|
||||||
val title = varchar("title").bindTo { it.title }
|
|
||||||
val markdown = text("markdown").bindTo { it.markdown }
|
|
||||||
val html = text("html").bindTo { it.html }
|
|
||||||
val userId = int("user_id").references(Users) { it.user }
|
|
||||||
val updatedAt = datetime("updated_at").bindTo { it.updatedAt }
|
|
||||||
val deleted = boolean("deleted").bindTo { it.deleted }
|
|
||||||
val public = boolean("public").bindTo { it.public }
|
|
||||||
val tags = varcharArray("tags").bindTo { it.tags }
|
|
||||||
val user get() = userId.referenceTable as Users
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
package be.simplenotes.persistence.converters
|
|
||||||
|
|
||||||
import be.simplenotes.persistence.NoteEntity
|
|
||||||
import be.simplenotes.persistence.NoteWithTagsEntity
|
|
||||||
import be.simplenotes.persistence.UserEntity
|
|
||||||
import be.simplenotes.types.*
|
|
||||||
import org.mapstruct.Mapper
|
|
||||||
import org.mapstruct.Mapping
|
|
||||||
import org.mapstruct.ReportingPolicy
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE, componentModel = "jsr330")
|
|
||||||
internal interface UserConverter {
|
|
||||||
fun toPersistedUser(userEntity: UserEntity?): PersistedUser?
|
|
||||||
}
|
|
||||||
|
|
||||||
@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE, componentModel = "jsr330")
|
|
||||||
internal abstract class NoteConverter {
|
|
||||||
|
|
||||||
@Mapping(target = "trash", source = "deleted")
|
|
||||||
abstract fun toExportedNote(entity: NoteWithTagsEntity?): ExportedNote?
|
|
||||||
abstract fun toPersistedNote(entity: NoteWithTagsEntity?): PersistedNote?
|
|
||||||
abstract fun toPersistedNoteMetadata(entity: NoteWithTagsEntity?): PersistedNoteMetadata?
|
|
||||||
|
|
||||||
fun toEntity(note: Note, uuid: UUID, userId: Int, updatedAt: LocalDateTime) = NoteEntity {
|
|
||||||
this.title = note.title
|
|
||||||
this.markdown = note.markdown
|
|
||||||
this.html = note.html
|
|
||||||
this.uuid = uuid
|
|
||||||
this.user.id = userId
|
|
||||||
this.updatedAt = updatedAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
85
persistence/src/converters/NoteConverter.kt
Normal file
85
persistence/src/converters/NoteConverter.kt
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package be.simplenotes.persistence.converters
|
||||||
|
|
||||||
|
import be.simplenotes.persistence.notes.NoteEntity
|
||||||
|
import be.simplenotes.types.*
|
||||||
|
import me.liuwj.ktorm.entity.Entity
|
||||||
|
import org.mapstruct.Mapper
|
||||||
|
import org.mapstruct.Mapping
|
||||||
|
import org.mapstruct.Mappings
|
||||||
|
import org.mapstruct.ReportingPolicy
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.util.*
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Mapper(
|
||||||
|
uses = [NoteEntityFactory::class, UserEntityFactory::class],
|
||||||
|
unmappedTargetPolicy = ReportingPolicy.IGNORE,
|
||||||
|
componentModel = "jsr330"
|
||||||
|
)
|
||||||
|
internal abstract class NoteConverter {
|
||||||
|
|
||||||
|
fun toNote(entity: NoteEntity, tags: Tags) =
|
||||||
|
Note(NoteMetadata(title = entity.title, tags = tags), entity.markdown, entity.html)
|
||||||
|
|
||||||
|
@Mappings(
|
||||||
|
Mapping(target = ".", source = "entity"),
|
||||||
|
Mapping(target = "tags", source = "tags"),
|
||||||
|
)
|
||||||
|
abstract fun toNoteMetadata(entity: NoteEntity, tags: Tags): NoteMetadata
|
||||||
|
|
||||||
|
@Mappings(
|
||||||
|
Mapping(target = ".", source = "entity"),
|
||||||
|
Mapping(target = "tags", source = "tags"),
|
||||||
|
)
|
||||||
|
abstract fun toPersistedNoteMetadata(entity: NoteEntity, tags: Tags): PersistedNoteMetadata
|
||||||
|
|
||||||
|
fun toPersistedNote(entity: NoteEntity, tags: Tags) = PersistedNote(
|
||||||
|
NoteMetadata(title = entity.title, tags = tags),
|
||||||
|
entity.markdown,
|
||||||
|
entity.html,
|
||||||
|
entity.updatedAt,
|
||||||
|
entity.uuid,
|
||||||
|
entity.public
|
||||||
|
)
|
||||||
|
|
||||||
|
@Mappings(
|
||||||
|
Mapping(target = ".", source = "entity"),
|
||||||
|
Mapping(target = "trash", source = "entity.deleted"),
|
||||||
|
Mapping(target = "tags", source = "tags"),
|
||||||
|
)
|
||||||
|
abstract fun toExportedNote(entity: NoteEntity, tags: Tags): ExportedNote
|
||||||
|
|
||||||
|
fun toEntity(note: Note, uuid: UUID, userId: Int, updatedAt: LocalDateTime) = NoteEntity {
|
||||||
|
this.title = note.meta.title
|
||||||
|
this.markdown = note.markdown
|
||||||
|
this.html = note.html
|
||||||
|
this.uuid = uuid
|
||||||
|
this.deleted = false
|
||||||
|
this.public = false
|
||||||
|
this.user.id = userId
|
||||||
|
this.updatedAt = updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mappings(
|
||||||
|
Mapping(target = ".", source = "note"),
|
||||||
|
Mapping(target = "updatedAt", source = "updatedAt"),
|
||||||
|
Mapping(target = "uuid", source = "uuid"),
|
||||||
|
Mapping(target = "public", constant = "false"),
|
||||||
|
)
|
||||||
|
abstract fun toPersistedNote(note: Note, updatedAt: LocalDateTime, uuid: UUID): PersistedNote
|
||||||
|
|
||||||
|
abstract fun toEntity(persistedNoteMetadata: PersistedNoteMetadata): NoteEntity
|
||||||
|
|
||||||
|
abstract fun toEntity(noteMetadata: NoteMetadata): NoteEntity
|
||||||
|
|
||||||
|
@Mapping(target = "title", source = "meta.title")
|
||||||
|
abstract fun toEntity(persistedNote: PersistedNote): NoteEntity
|
||||||
|
|
||||||
|
@Mapping(target = "deleted", source = "trash")
|
||||||
|
abstract fun toEntity(exportedNote: ExportedNote): NoteEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
typealias Tags = List<String>
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
internal class NoteEntityFactory : Entity.Factory<NoteEntity>()
|
||||||
24
persistence/src/converters/UserConverter.kt
Normal file
24
persistence/src/converters/UserConverter.kt
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package be.simplenotes.persistence.converters
|
||||||
|
|
||||||
|
import be.simplenotes.persistence.users.UserEntity
|
||||||
|
import be.simplenotes.types.PersistedUser
|
||||||
|
import be.simplenotes.types.User
|
||||||
|
import me.liuwj.ktorm.entity.Entity
|
||||||
|
import org.mapstruct.Mapper
|
||||||
|
import org.mapstruct.ReportingPolicy
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Mapper(
|
||||||
|
uses = [UserEntityFactory::class],
|
||||||
|
unmappedTargetPolicy = ReportingPolicy.IGNORE,
|
||||||
|
componentModel = "jsr330"
|
||||||
|
)
|
||||||
|
internal interface UserConverter {
|
||||||
|
fun toUser(userEntity: UserEntity): User
|
||||||
|
fun toPersistedUser(userEntity: UserEntity): PersistedUser
|
||||||
|
fun toEntity(user: User): UserEntity
|
||||||
|
fun toEntity(user: PersistedUser): UserEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
internal class UserEntityFactory : Entity.Factory<UserEntity>()
|
||||||
@ -1,77 +1,26 @@
|
|||||||
package be.simplenotes.persistence.extensions
|
package be.simplenotes.persistence.extensions
|
||||||
|
|
||||||
import org.ktorm.database.Database
|
import me.liuwj.ktorm.schema.BaseTable
|
||||||
import org.ktorm.expression.*
|
import me.liuwj.ktorm.schema.SqlType
|
||||||
import org.ktorm.schema.*
|
import java.nio.ByteBuffer
|
||||||
import java.sql.PreparedStatement
|
import java.sql.PreparedStatement
|
||||||
import java.sql.ResultSet
|
import java.sql.ResultSet
|
||||||
import java.sql.Types
|
import java.sql.Types
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
internal class VarcharArraySqlType : SqlType<List<String>>(Types.ARRAY, typeName = "varchar[]") {
|
internal class UuidBinarySqlType : SqlType<UUID>(Types.BINARY, typeName = "uuidBinary") {
|
||||||
override fun doGetResult(rs: ResultSet, index: Int): List<String>? {
|
override fun doGetResult(rs: ResultSet, index: Int): UUID? {
|
||||||
return when (val array = rs.getObject(index)) {
|
val value = rs.getBytes(index) ?: return null
|
||||||
null -> null
|
return ByteBuffer.wrap(value).let { b -> UUID(b.long, b.long) }
|
||||||
is Array<*> -> array.map { it.toString() }
|
|
||||||
else -> error("")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun doSetParameter(ps: PreparedStatement, index: Int, parameter: List<String>) {
|
override fun doSetParameter(ps: PreparedStatement, index: Int, parameter: UUID) {
|
||||||
throw UnsupportedOperationException()
|
val bytes = ByteBuffer.allocate(16)
|
||||||
|
.putLong(parameter.mostSignificantBits)
|
||||||
|
.putLong(parameter.leastSignificantBits)
|
||||||
|
.array()
|
||||||
|
ps.setBytes(index, bytes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun <E : Any> BaseTable<E>.varcharArray(name: String) = registerColumn(name, VarcharArraySqlType())
|
internal fun <E : Any> BaseTable<E>.uuidBinary(name: String) = registerColumn(name, UuidBinarySqlType())
|
||||||
|
|
||||||
data class ArrayContainsExpression(
|
|
||||||
val left: ScalarExpression<*>,
|
|
||||||
val right: ScalarExpression<*>,
|
|
||||||
override val sqlType: SqlType<Boolean> = BooleanSqlType,
|
|
||||||
override val isLeafNode: Boolean = false
|
|
||||||
) : ScalarExpression<Boolean>() {
|
|
||||||
override val extraProperties: Map<String, Any> get() = emptyMap()
|
|
||||||
}
|
|
||||||
|
|
||||||
infix fun ColumnDeclaring<*>.arrayContains(arguments: String): ArrayContainsExpression {
|
|
||||||
return ArrayContainsExpression(asExpression(), ArgumentExpression(arguments, VarcharSqlType))
|
|
||||||
}
|
|
||||||
|
|
||||||
class CustomSqlFormatter(database: Database, beautifySql: Boolean, indentSize: Int) :
|
|
||||||
SqlFormatter(database, beautifySql, indentSize) {
|
|
||||||
override fun visitUnknown(expr: SqlExpression): SqlExpression {
|
|
||||||
return if (expr is ArrayContainsExpression) {
|
|
||||||
write("ARRAY_CONTAINS(")
|
|
||||||
|
|
||||||
if (expr.left.removeBrackets) {
|
|
||||||
visit(expr.left)
|
|
||||||
} else {
|
|
||||||
write("(")
|
|
||||||
visit(expr.left)
|
|
||||||
removeLastBlank()
|
|
||||||
write(") ")
|
|
||||||
}
|
|
||||||
|
|
||||||
write(", ")
|
|
||||||
|
|
||||||
if (expr.right.removeBrackets) {
|
|
||||||
visit(expr.right)
|
|
||||||
} else {
|
|
||||||
write("(")
|
|
||||||
visit(expr.right)
|
|
||||||
removeLastBlank()
|
|
||||||
write(") ")
|
|
||||||
}
|
|
||||||
|
|
||||||
write(")")
|
|
||||||
|
|
||||||
return expr
|
|
||||||
} else super.visitUnknown(expr)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun writePagination(expr: QueryExpression) {
|
|
||||||
newLine(Indentation.SAME)
|
|
||||||
writeKeyword("limit ?, ? ")
|
|
||||||
_parameters += ArgumentExpression(expr.offset ?: 0, IntSqlType)
|
|
||||||
_parameters += ArgumentExpression(expr.limit ?: Int.MAX_VALUE, IntSqlType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
219
persistence/src/notes/NoteRepositoryImpl.kt
Normal file
219
persistence/src/notes/NoteRepositoryImpl.kt
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
package be.simplenotes.persistence.notes
|
||||||
|
|
||||||
|
import be.simplenotes.persistence.converters.NoteConverter
|
||||||
|
import be.simplenotes.persistence.repositories.NoteRepository
|
||||||
|
import be.simplenotes.types.ExportedNote
|
||||||
|
import be.simplenotes.types.Note
|
||||||
|
import be.simplenotes.types.PersistedNote
|
||||||
|
import be.simplenotes.types.PersistedNoteMetadata
|
||||||
|
import me.liuwj.ktorm.database.Database
|
||||||
|
import me.liuwj.ktorm.dsl.*
|
||||||
|
import me.liuwj.ktorm.entity.*
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.util.*
|
||||||
|
import javax.inject.Singleton
|
||||||
|
import kotlin.collections.HashMap
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
internal class NoteRepositoryImpl(
|
||||||
|
private val db: Database,
|
||||||
|
private val converter: NoteConverter,
|
||||||
|
) : NoteRepository {
|
||||||
|
|
||||||
|
@Throws(IllegalArgumentException::class)
|
||||||
|
override fun findAll(
|
||||||
|
userId: Int,
|
||||||
|
limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
tag: String?,
|
||||||
|
deleted: Boolean,
|
||||||
|
): List<PersistedNoteMetadata> {
|
||||||
|
require(limit > 0) { "limit should be positive" }
|
||||||
|
require(offset >= 0) { "offset should not be negative" }
|
||||||
|
|
||||||
|
val uuids1: List<UUID>? = if (tag != null) {
|
||||||
|
db.from(Tags)
|
||||||
|
.leftJoin(Notes, on = Notes.uuid eq Tags.noteUuid)
|
||||||
|
.select(Notes.uuid)
|
||||||
|
.where { (Notes.userId eq userId) and (Tags.name eq tag) and (Notes.deleted eq deleted) }
|
||||||
|
.map { it[Notes.uuid]!! }
|
||||||
|
} else null
|
||||||
|
|
||||||
|
var query = db.notes
|
||||||
|
.filterColumns { listOf(it.uuid, it.title, it.updatedAt) }
|
||||||
|
.filter { (it.userId eq userId) and (it.deleted eq deleted) }
|
||||||
|
|
||||||
|
if (uuids1 != null) query = query.filter { it.uuid inList uuids1 }
|
||||||
|
|
||||||
|
val notes = query
|
||||||
|
.sortedByDescending { it.updatedAt }
|
||||||
|
.take(limit)
|
||||||
|
.drop(offset)
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
val tagsByUuid = notes.tagsByUuid()
|
||||||
|
|
||||||
|
return notes.map { note ->
|
||||||
|
val tags = tagsByUuid[note.uuid] ?: emptyList()
|
||||||
|
converter.toPersistedNoteMetadata(note, tags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun exists(userId: Int, uuid: UUID): Boolean {
|
||||||
|
return db.notes.any { (it.userId eq userId) and (it.uuid eq uuid) and (it.deleted eq false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun create(userId: Int, note: Note): PersistedNote {
|
||||||
|
val uuid = UUID.randomUUID()
|
||||||
|
val entity = converter.toEntity(note, uuid, userId, LocalDateTime.now())
|
||||||
|
db.notes.add(entity)
|
||||||
|
db.batchInsert(Tags) {
|
||||||
|
note.meta.tags.forEach { tagName ->
|
||||||
|
item {
|
||||||
|
it.noteUuid to uuid
|
||||||
|
it.name to tagName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return converter.toPersistedNote(entity, note.meta.tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun find(userId: Int, uuid: UUID): PersistedNote? {
|
||||||
|
val note = db.notes
|
||||||
|
.filterColumns { it.columns - it.userId }
|
||||||
|
.filter { it.uuid eq uuid }
|
||||||
|
.find { (it.userId eq userId) and (it.deleted eq false) }
|
||||||
|
?: return null
|
||||||
|
|
||||||
|
val tags = db.from(Tags)
|
||||||
|
.select(Tags.name)
|
||||||
|
.where { Tags.noteUuid eq uuid }
|
||||||
|
.map { it[Tags.name]!! }
|
||||||
|
|
||||||
|
return converter.toPersistedNote(note, tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(userId: Int, uuid: UUID, note: Note): PersistedNote? {
|
||||||
|
val now = LocalDateTime.now()
|
||||||
|
val count = db.update(Notes) {
|
||||||
|
it.title to note.meta.title
|
||||||
|
it.markdown to note.markdown
|
||||||
|
it.html to note.html
|
||||||
|
it.updatedAt to now
|
||||||
|
where { (it.uuid eq uuid) and (it.userId eq userId) and (it.deleted eq false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count == 0) return null
|
||||||
|
|
||||||
|
// delete all tags
|
||||||
|
db.delete(Tags) {
|
||||||
|
it.noteUuid eq uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
// put new ones
|
||||||
|
note.meta.tags.forEach { tagName ->
|
||||||
|
db.insert(Tags) {
|
||||||
|
it.name to tagName
|
||||||
|
it.noteUuid to uuid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return converter.toPersistedNote(note, now, uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun delete(userId: Int, uuid: UUID, permanent: Boolean): Boolean {
|
||||||
|
return if (!permanent) {
|
||||||
|
db.update(Notes) {
|
||||||
|
it.deleted to true
|
||||||
|
it.updatedAt to LocalDateTime.now()
|
||||||
|
where { it.userId eq userId and (it.uuid eq uuid) }
|
||||||
|
} == 1
|
||||||
|
} else
|
||||||
|
db.delete(Notes) { it.uuid eq uuid and (it.userId eq userId) } == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun restore(userId: Int, uuid: UUID): Boolean {
|
||||||
|
return db.update(Notes) {
|
||||||
|
it.deleted to false
|
||||||
|
where { (it.userId eq userId) and (it.uuid eq uuid) }
|
||||||
|
} == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTags(userId: Int): List<String> =
|
||||||
|
db.from(Tags)
|
||||||
|
.leftJoin(Notes, on = Notes.uuid eq Tags.noteUuid)
|
||||||
|
.selectDistinct(Tags.name)
|
||||||
|
.where { (Notes.userId eq userId) and (Notes.deleted eq false) }
|
||||||
|
.map { it[Tags.name]!! }
|
||||||
|
|
||||||
|
override fun count(userId: Int, tag: String?, deleted: Boolean): Int {
|
||||||
|
return if (tag == null) db.notes.count { (it.userId eq userId) and (Notes.deleted eq deleted) }
|
||||||
|
else db.sequenceOf(Tags).count {
|
||||||
|
(it.name eq tag) and (it.note.userId eq userId) and (it.note.deleted eq deleted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun export(userId: Int): List<ExportedNote> {
|
||||||
|
|
||||||
|
val notes = db.notes
|
||||||
|
.filterColumns { it.columns - it.userId }
|
||||||
|
.filter { it.userId eq userId }
|
||||||
|
.sortedByDescending { it.updatedAt }
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
val tagsByUuid = notes.tagsByUuid()
|
||||||
|
|
||||||
|
return notes.map { note ->
|
||||||
|
val tags = tagsByUuid[note.uuid] ?: emptyList()
|
||||||
|
converter.toExportedNote(note, tags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findAllDetails(userId: Int): List<PersistedNote> {
|
||||||
|
val notes = db.notes
|
||||||
|
.filterColumns { it.columns - it.deleted }
|
||||||
|
.filter { (it.userId eq userId) and (it.deleted eq false) }
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
val tagsByUuid = notes.tagsByUuid()
|
||||||
|
|
||||||
|
return notes.map { note ->
|
||||||
|
val tags = tagsByUuid[note.uuid] ?: emptyList()
|
||||||
|
converter.toPersistedNote(note, tags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun makePublic(userId: Int, uuid: UUID) = db.update(Notes) {
|
||||||
|
it.public to true
|
||||||
|
where { Notes.userId eq userId and (Notes.uuid eq uuid) and (it.deleted eq false) }
|
||||||
|
} == 1
|
||||||
|
|
||||||
|
override fun makePrivate(userId: Int, uuid: UUID) = db.update(Notes) {
|
||||||
|
it.public to false
|
||||||
|
where { Notes.userId eq userId and (Notes.uuid eq uuid) and (it.deleted eq false) }
|
||||||
|
} == 1
|
||||||
|
|
||||||
|
override fun findPublic(uuid: UUID): PersistedNote? {
|
||||||
|
val note = db.notes
|
||||||
|
.filterColumns { it.columns - it.userId }
|
||||||
|
.filter { it.uuid eq uuid }
|
||||||
|
.filter { it.public eq true }
|
||||||
|
.find { it.deleted eq false }
|
||||||
|
?: return null
|
||||||
|
|
||||||
|
val tags = db.from(Tags)
|
||||||
|
.select(Tags.name)
|
||||||
|
.where { Tags.noteUuid eq uuid }
|
||||||
|
.map { it[Tags.name]!! }
|
||||||
|
|
||||||
|
return converter.toPersistedNote(note, tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<NoteEntity>.tagsByUuid(): Map<UUID, List<String>> {
|
||||||
|
return if (isEmpty()) emptyMap()
|
||||||
|
else db.tags
|
||||||
|
.filterColumns { listOf(it.noteUuid, it.name) }
|
||||||
|
.filter { it.noteUuid inList map { note -> note.uuid } }
|
||||||
|
.groupByTo(HashMap(), { it.note.uuid }, { it.name })
|
||||||
|
}
|
||||||
|
}
|
||||||
45
persistence/src/notes/Notes.kt
Normal file
45
persistence/src/notes/Notes.kt
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
@file:Suppress("unused")
|
||||||
|
|
||||||
|
package be.simplenotes.persistence.notes
|
||||||
|
|
||||||
|
import be.simplenotes.persistence.extensions.uuidBinary
|
||||||
|
import be.simplenotes.persistence.users.UserEntity
|
||||||
|
import be.simplenotes.persistence.users.Users
|
||||||
|
import me.liuwj.ktorm.database.Database
|
||||||
|
import me.liuwj.ktorm.entity.Entity
|
||||||
|
import me.liuwj.ktorm.entity.sequenceOf
|
||||||
|
import me.liuwj.ktorm.schema.*
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
internal open class Notes(alias: String?) : Table<NoteEntity>("Notes", alias) {
|
||||||
|
companion object : Notes(null)
|
||||||
|
|
||||||
|
override fun aliased(alias: String) = Notes(alias)
|
||||||
|
|
||||||
|
val uuid = uuidBinary("uuid").primaryKey().bindTo { it.uuid }
|
||||||
|
val title = varchar("title").bindTo { it.title }
|
||||||
|
val markdown = text("markdown").bindTo { it.markdown }
|
||||||
|
val html = text("html").bindTo { it.html }
|
||||||
|
val userId = int("user_id").references(Users) { it.user }
|
||||||
|
val updatedAt = datetime("updated_at").bindTo { it.updatedAt }
|
||||||
|
val deleted = boolean("deleted").bindTo { it.deleted }
|
||||||
|
val public = boolean("public").bindTo { it.public }
|
||||||
|
val user get() = userId.referenceTable as Users
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val Database.notes get() = this.sequenceOf(Notes, withReferences = false)
|
||||||
|
|
||||||
|
internal interface NoteEntity : Entity<NoteEntity> {
|
||||||
|
companion object : Entity.Factory<NoteEntity>()
|
||||||
|
|
||||||
|
var uuid: UUID
|
||||||
|
var title: String
|
||||||
|
var markdown: String
|
||||||
|
var html: String
|
||||||
|
var updatedAt: LocalDateTime
|
||||||
|
var deleted: Boolean
|
||||||
|
var public: Boolean
|
||||||
|
|
||||||
|
var user: UserEntity
|
||||||
|
}
|
||||||
30
persistence/src/notes/Tags.kt
Normal file
30
persistence/src/notes/Tags.kt
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package be.simplenotes.persistence.notes
|
||||||
|
|
||||||
|
import be.simplenotes.persistence.extensions.uuidBinary
|
||||||
|
import me.liuwj.ktorm.database.Database
|
||||||
|
import me.liuwj.ktorm.entity.Entity
|
||||||
|
import me.liuwj.ktorm.entity.sequenceOf
|
||||||
|
import me.liuwj.ktorm.schema.Table
|
||||||
|
import me.liuwj.ktorm.schema.int
|
||||||
|
import me.liuwj.ktorm.schema.varchar
|
||||||
|
|
||||||
|
internal open class Tags(alias: String?) : Table<TagEntity>("Tags", alias) {
|
||||||
|
companion object : Tags(null)
|
||||||
|
|
||||||
|
override fun aliased(alias: String) = Tags(alias)
|
||||||
|
|
||||||
|
val id = int("id").primaryKey().bindTo { it.id }
|
||||||
|
val name = varchar("name").bindTo { it.name }
|
||||||
|
val noteUuid = uuidBinary("note_uuid").references(Notes) { it.note }
|
||||||
|
val note get() = noteUuid.referenceTable as Notes
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val Database.tags get() = this.sequenceOf(Tags, withReferences = false)
|
||||||
|
|
||||||
|
internal interface TagEntity : Entity<TagEntity> {
|
||||||
|
companion object : Entity.Factory<TagEntity>()
|
||||||
|
|
||||||
|
val id: Int
|
||||||
|
var name: String
|
||||||
|
var note: NoteEntity
|
||||||
|
}
|
||||||
@ -1,144 +0,0 @@
|
|||||||
package be.simplenotes.persistence.repositories
|
|
||||||
|
|
||||||
import be.simplenotes.persistence.*
|
|
||||||
import be.simplenotes.persistence.converters.NoteConverter
|
|
||||||
import be.simplenotes.persistence.extensions.arrayContains
|
|
||||||
import be.simplenotes.types.Note
|
|
||||||
import be.simplenotes.types.PersistedNote
|
|
||||||
import org.ktorm.database.Database
|
|
||||||
import org.ktorm.dsl.*
|
|
||||||
import org.ktorm.entity.*
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
internal class NoteRepositoryImpl(
|
|
||||||
private val db: Database,
|
|
||||||
private val converter: NoteConverter,
|
|
||||||
) : NoteRepository {
|
|
||||||
|
|
||||||
override fun findAll(
|
|
||||||
userId: Int,
|
|
||||||
limit: Int,
|
|
||||||
offset: Int,
|
|
||||||
tag: String?,
|
|
||||||
deleted: Boolean,
|
|
||||||
) = db.noteWithTags
|
|
||||||
.filterColumns { listOf(it.uuid, it.title, it.updatedAt, it.tags) }
|
|
||||||
.filter { (it.userId eq userId) and (it.deleted eq deleted) }
|
|
||||||
.runIf(tag != null) { filter { it.tags.arrayContains(tag!!) } }
|
|
||||||
.sortedByDescending { it.updatedAt }
|
|
||||||
.take(limit)
|
|
||||||
.drop(offset)
|
|
||||||
.map { converter.toPersistedNoteMetadata(it)!! }
|
|
||||||
|
|
||||||
override fun exists(userId: Int, uuid: UUID) =
|
|
||||||
db.notes.any { (it.userId eq userId) and (it.uuid eq uuid) and (it.deleted eq false) }
|
|
||||||
|
|
||||||
override fun create(userId: Int, note: Note): PersistedNote = db.useTransaction {
|
|
||||||
val uuid = UUID.randomUUID()
|
|
||||||
val entity = converter.toEntity(note, uuid, userId, LocalDateTime.now())
|
|
||||||
db.notes.add(entity)
|
|
||||||
db.batchInsert(Tags) {
|
|
||||||
note.tags.forEach { tagName ->
|
|
||||||
item {
|
|
||||||
set(it.noteUuid, uuid)
|
|
||||||
set(it.name, tagName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return find(userId, uuid) ?: error("Note not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun find(userId: Int, uuid: UUID) = db.noteWithTags
|
|
||||||
.filterColumns { NotesWithTags.columns - NotesWithTags.userId - NotesWithTags.deleted }
|
|
||||||
.find { (it.uuid eq uuid) and (it.userId eq userId) and (it.deleted eq false) }
|
|
||||||
.let { converter.toPersistedNote(it) }
|
|
||||||
|
|
||||||
override fun update(userId: Int, uuid: UUID, note: Note): PersistedNote? = db.useTransaction {
|
|
||||||
val now = LocalDateTime.now()
|
|
||||||
val count = db.update(Notes) {
|
|
||||||
set(it.title, note.title)
|
|
||||||
set(it.markdown, note.markdown)
|
|
||||||
set(it.html, note.html)
|
|
||||||
set(it.updatedAt, now)
|
|
||||||
where { (it.uuid eq uuid) and (it.userId eq userId) and (it.deleted eq false) }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count == 0) return null
|
|
||||||
|
|
||||||
// delete all tags
|
|
||||||
db.delete(Tags) {
|
|
||||||
it.noteUuid eq uuid
|
|
||||||
}
|
|
||||||
|
|
||||||
// put new ones
|
|
||||||
note.tags.forEach { tagName ->
|
|
||||||
db.insert(Tags) {
|
|
||||||
set(it.name, tagName)
|
|
||||||
set(it.noteUuid, uuid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return find(userId, uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun delete(userId: Int, uuid: UUID, permanent: Boolean) = if (!permanent) {
|
|
||||||
db.update(Notes) {
|
|
||||||
set(it.deleted, true)
|
|
||||||
set(it.updatedAt, LocalDateTime.now())
|
|
||||||
where { it.userId eq userId and (it.uuid eq uuid) }
|
|
||||||
} == 1
|
|
||||||
} else db.delete(Notes) { it.uuid eq uuid and (it.userId eq userId) } == 1
|
|
||||||
|
|
||||||
override fun restore(userId: Int, uuid: UUID) = db.update(Notes) {
|
|
||||||
set(it.deleted, false)
|
|
||||||
where { (it.userId eq userId) and (it.uuid eq uuid) }
|
|
||||||
} == 1
|
|
||||||
|
|
||||||
override fun getTags(userId: Int): List<String> = db.from(Tags)
|
|
||||||
.leftJoin(Notes, on = Notes.uuid eq Tags.noteUuid)
|
|
||||||
.selectDistinct(Tags.name)
|
|
||||||
.where { (Notes.userId eq userId) and (Notes.deleted eq false) }
|
|
||||||
.map { it.getString(1)!! }
|
|
||||||
|
|
||||||
override fun count(userId: Int, tag: String?, deleted: Boolean) = db.from(Notes)
|
|
||||||
.runIf(tag != null) { leftJoin(Tags, on = Tags.noteUuid eq Notes.uuid) }
|
|
||||||
.select(count())
|
|
||||||
.whereWithConditions {
|
|
||||||
it += Notes.userId eq userId
|
|
||||||
it += Notes.deleted eq deleted
|
|
||||||
tag?.let { tag -> it += Tags.name eq tag }
|
|
||||||
}
|
|
||||||
.map { it.getInt(1) }
|
|
||||||
.first()
|
|
||||||
|
|
||||||
override fun export(userId: Int) = db.noteWithTags
|
|
||||||
.filterColumns { it.columns - it.userId - it.public }
|
|
||||||
.filter { it.userId eq userId }
|
|
||||||
.sortedByDescending { it.updatedAt }
|
|
||||||
.map { converter.toExportedNote(it)!! }
|
|
||||||
|
|
||||||
override fun findAllDetails(userId: Int) = db.noteWithTags
|
|
||||||
.filterColumns { it.columns - it.userId - it.deleted }
|
|
||||||
.filter { (it.userId eq userId) and (it.deleted eq false) }
|
|
||||||
.map { converter.toPersistedNote(it)!! }
|
|
||||||
|
|
||||||
override fun makePublic(userId: Int, uuid: UUID) = db.update(Notes) {
|
|
||||||
set(it.public, true)
|
|
||||||
where { Notes.userId eq userId and (Notes.uuid eq uuid) and (it.deleted eq false) }
|
|
||||||
} == 1
|
|
||||||
|
|
||||||
override fun makePrivate(userId: Int, uuid: UUID) = db.update(Notes) {
|
|
||||||
set(it.public, false)
|
|
||||||
where { Notes.userId eq userId and (Notes.uuid eq uuid) and (it.deleted eq false) }
|
|
||||||
} == 1
|
|
||||||
|
|
||||||
override fun findPublic(uuid: UUID) = db.noteWithTags
|
|
||||||
.filterColumns { it.columns - it.userId - it.deleted }
|
|
||||||
.find { (it.uuid eq uuid) and (it.public eq true) and (it.deleted eq false) }
|
|
||||||
.let { converter.toPersistedNote(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <T> T.runIf(condition: Boolean, block: T.() -> T) = run { if (condition) block(this) else this }
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
package be.simplenotes.persistence.repositories
|
|
||||||
|
|
||||||
import be.simplenotes.persistence.Users
|
|
||||||
import be.simplenotes.persistence.converters.UserConverter
|
|
||||||
import be.simplenotes.persistence.users
|
|
||||||
import be.simplenotes.types.PersistedUser
|
|
||||||
import be.simplenotes.types.User
|
|
||||||
import org.ktorm.database.Database
|
|
||||||
import org.ktorm.dsl.*
|
|
||||||
import org.ktorm.entity.any
|
|
||||||
import org.ktorm.entity.find
|
|
||||||
import java.sql.SQLIntegrityConstraintViolationException
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
internal class UserRepositoryImpl(
|
|
||||||
private val db: Database,
|
|
||||||
private val converter: UserConverter,
|
|
||||||
) : UserRepository {
|
|
||||||
override fun create(user: User) = try {
|
|
||||||
val id = db.insertAndGenerateKey(Users) {
|
|
||||||
set(it.username, user.username)
|
|
||||||
set(it.password, user.password)
|
|
||||||
} as Int
|
|
||||||
PersistedUser(user.username, user.password, id)
|
|
||||||
} catch (e: SQLIntegrityConstraintViolationException) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun find(username: String) = db.users.find { it.username eq username }
|
|
||||||
.let { converter.toPersistedUser(it) }
|
|
||||||
|
|
||||||
override fun find(id: Int) = db.users.find { it.id eq id }
|
|
||||||
.let { converter.toPersistedUser(it) }
|
|
||||||
|
|
||||||
override fun exists(username: String) = db.users.any { it.username eq username }
|
|
||||||
override fun exists(id: Int) = db.users.any { it.id eq id }
|
|
||||||
override fun delete(id: Int) = db.delete(Users) { it.id eq id } == 1
|
|
||||||
override fun findAll() = db.from(Users).select(Users.id).map { it.getInt(1) }
|
|
||||||
}
|
|
||||||
9
persistence/src/transactions/KtormTransactionService.kt
Normal file
9
persistence/src/transactions/KtormTransactionService.kt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package be.simplenotes.persistence.transactions
|
||||||
|
|
||||||
|
import me.liuwj.ktorm.database.Database
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
internal class KtormTransactionService(private val database: Database) : TransactionService {
|
||||||
|
override fun <T> use(block: () -> T) = database.useTransaction { block() }
|
||||||
|
}
|
||||||
5
persistence/src/transactions/TransactionService.kt
Normal file
5
persistence/src/transactions/TransactionService.kt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package be.simplenotes.persistence.transactions
|
||||||
|
|
||||||
|
interface TransactionService {
|
||||||
|
fun <T> use(block: () -> T): T
|
||||||
|
}
|
||||||
42
persistence/src/users/UserRepositoryImpl.kt
Normal file
42
persistence/src/users/UserRepositoryImpl.kt
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package be.simplenotes.persistence.users
|
||||||
|
|
||||||
|
import be.simplenotes.persistence.converters.UserConverter
|
||||||
|
import be.simplenotes.persistence.repositories.UserRepository
|
||||||
|
import be.simplenotes.types.PersistedUser
|
||||||
|
import be.simplenotes.types.User
|
||||||
|
import me.liuwj.ktorm.database.Database
|
||||||
|
import me.liuwj.ktorm.dsl.*
|
||||||
|
import me.liuwj.ktorm.entity.any
|
||||||
|
import me.liuwj.ktorm.entity.find
|
||||||
|
import java.sql.SQLIntegrityConstraintViolationException
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
internal class UserRepositoryImpl(
|
||||||
|
private val db: Database,
|
||||||
|
private val converter: UserConverter,
|
||||||
|
) : UserRepository {
|
||||||
|
override fun create(user: User): PersistedUser? {
|
||||||
|
return try {
|
||||||
|
val id = db.insertAndGenerateKey(Users) {
|
||||||
|
it.username to user.username
|
||||||
|
it.password to user.password
|
||||||
|
} as Int
|
||||||
|
PersistedUser(user.username, user.password, id)
|
||||||
|
} catch (e: SQLIntegrityConstraintViolationException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun find(username: String) = db.users.find { it.username eq username }
|
||||||
|
?.let { converter.toPersistedUser(it) }
|
||||||
|
|
||||||
|
override fun find(id: Int) = db.users.find { it.id eq id }?.let {
|
||||||
|
converter.toPersistedUser(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun exists(username: String) = db.users.any { it.username eq username }
|
||||||
|
override fun exists(id: Int) = db.users.any { it.id eq id }
|
||||||
|
override fun delete(id: Int) = db.delete(Users) { it.id eq id } == 1
|
||||||
|
override fun findAll() = db.from(Users).select(Users.id).map { it[Users.id]!! }
|
||||||
|
}
|
||||||
28
persistence/src/users/Users.kt
Normal file
28
persistence/src/users/Users.kt
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package be.simplenotes.persistence.users
|
||||||
|
|
||||||
|
import me.liuwj.ktorm.database.Database
|
||||||
|
import me.liuwj.ktorm.entity.Entity
|
||||||
|
import me.liuwj.ktorm.entity.sequenceOf
|
||||||
|
import me.liuwj.ktorm.schema.Table
|
||||||
|
import me.liuwj.ktorm.schema.int
|
||||||
|
import me.liuwj.ktorm.schema.varchar
|
||||||
|
|
||||||
|
internal open class Users(alias: String?) : Table<UserEntity>("Users", alias) {
|
||||||
|
companion object : Users(null)
|
||||||
|
|
||||||
|
override fun aliased(alias: String) = Users(alias)
|
||||||
|
|
||||||
|
val id = int("id").primaryKey().bindTo { it.id }
|
||||||
|
val username = varchar("username").bindTo { it.username }
|
||||||
|
val password = varchar("password").bindTo { it.password }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface UserEntity : Entity<UserEntity> {
|
||||||
|
companion object : Entity.Factory<UserEntity>()
|
||||||
|
|
||||||
|
var id: Int
|
||||||
|
var username: String
|
||||||
|
var password: String
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val Database.users get() = this.sequenceOf(Users, withReferences = false)
|
||||||
8
persistence/src/utils/DataSourceConfigUtils.kt
Normal file
8
persistence/src/utils/DataSourceConfigUtils.kt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package be.simplenotes.persistence.utils
|
||||||
|
|
||||||
|
import be.simplenotes.config.DataSourceConfig
|
||||||
|
|
||||||
|
enum class DbType { H2, MariaDb }
|
||||||
|
|
||||||
|
fun DataSourceConfig.type(): DbType = if (jdbcUrl.contains("mariadb")) DbType.MariaDb
|
||||||
|
else DbType.H2
|
||||||
22
persistence/test/DataSources.kt
Normal file
22
persistence/test/DataSources.kt
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package be.simplenotes.persistence
|
||||||
|
|
||||||
|
import be.simplenotes.config.DataSourceConfig
|
||||||
|
import org.testcontainers.containers.MariaDBContainer
|
||||||
|
|
||||||
|
class KMariadbContainer : MariaDBContainer<KMariadbContainer>("mariadb:10.5.5")
|
||||||
|
|
||||||
|
fun h2dataSourceConfig() = DataSourceConfig(
|
||||||
|
jdbcUrl = "jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1;",
|
||||||
|
username = "h2",
|
||||||
|
password = "",
|
||||||
|
maximumPoolSize = 2,
|
||||||
|
connectionTimeout = 3000
|
||||||
|
)
|
||||||
|
|
||||||
|
fun mariadbDataSourceConfig(jdbcUrl: String) = DataSourceConfig(
|
||||||
|
jdbcUrl = jdbcUrl,
|
||||||
|
username = "test",
|
||||||
|
password = "test",
|
||||||
|
maximumPoolSize = 2,
|
||||||
|
connectionTimeout = 3000
|
||||||
|
)
|
||||||
37
persistence/test/DbHealthCheckImplTest.kt
Normal file
37
persistence/test/DbHealthCheckImplTest.kt
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package be.simplenotes.persistence
|
||||||
|
|
||||||
|
import be.simplenotes.config.DataSourceConfig
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.jupiter.api.Tag
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.parallel.ResourceLock
|
||||||
|
|
||||||
|
@ResourceLock("h2")
|
||||||
|
class H2DbHealthCheckImplTest : DbTest() {
|
||||||
|
override fun dataSourceConfig() = h2dataSourceConfig()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun healthCheck() {
|
||||||
|
assertThat(beanContext.getBean<DbHealthCheck>().isOk()).isTrue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Tag("slow")
|
||||||
|
@ResourceLock("mariadb")
|
||||||
|
class MariaDbHealthCheckImplTest : DbTest() {
|
||||||
|
lateinit var mariaDB: KMariadbContainer
|
||||||
|
|
||||||
|
override fun dataSourceConfig(): DataSourceConfig {
|
||||||
|
mariaDB = KMariadbContainer()
|
||||||
|
mariaDB.start()
|
||||||
|
return mariadbDataSourceConfig(mariaDB.jdbcUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun healthCheck() {
|
||||||
|
val healthCheck = beanContext.getBean<DbHealthCheck>()
|
||||||
|
assertThat(healthCheck.isOk()).isTrue
|
||||||
|
mariaDB.stop()
|
||||||
|
assertThat(healthCheck.isOk()).isFalse
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,20 +1,29 @@
|
|||||||
package be.simplenotes.persistence
|
package be.simplenotes.persistence
|
||||||
|
|
||||||
import io.micronaut.context.ApplicationContext
|
import be.simplenotes.config.DataSourceConfig
|
||||||
import io.micronaut.context.BeanContext
|
import io.micronaut.context.BeanContext
|
||||||
import org.flywaydb.core.Flyway
|
import org.flywaydb.core.Flyway
|
||||||
import org.junit.jupiter.api.AfterAll
|
import org.junit.jupiter.api.AfterAll
|
||||||
|
import org.junit.jupiter.api.BeforeAll
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.parallel.ResourceLock
|
|
||||||
import javax.sql.DataSource
|
import javax.sql.DataSource
|
||||||
|
|
||||||
@ResourceLock("h2")
|
|
||||||
abstract class DbTest {
|
abstract class DbTest {
|
||||||
|
|
||||||
val beanContext = ApplicationContext.build().deduceEnvironment(false).environments("test").start()
|
abstract fun dataSourceConfig(): DataSourceConfig
|
||||||
|
|
||||||
|
val beanContext = BeanContext
|
||||||
|
.build()
|
||||||
|
|
||||||
inline fun <reified T> BeanContext.getBean(): T = getBean(T::class.java)
|
inline fun <reified T> BeanContext.getBean(): T = getBean(T::class.java)
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
fun setComponent() {
|
||||||
|
beanContext
|
||||||
|
.registerSingleton(dataSourceConfig())
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun beforeEach() {
|
fun beforeEach() {
|
||||||
val migration = beanContext.getBean<DbMigrations>()
|
val migration = beanContext.getBean<DbMigrations>()
|
||||||
|
|||||||
164
persistence/test/converters/NoteConverterTest.kt
Normal file
164
persistence/test/converters/NoteConverterTest.kt
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
package be.simplenotes.persistence.converters
|
||||||
|
|
||||||
|
import be.simplenotes.persistence.notes.NoteEntity
|
||||||
|
import be.simplenotes.types.*
|
||||||
|
import io.micronaut.context.BeanContext
|
||||||
|
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 java.time.LocalDateTime
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
internal class NoteConverterTest {
|
||||||
|
|
||||||
|
private val ctx = BeanContext.run()
|
||||||
|
val converter = ctx.getBean(NoteConverter::class.java)
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Entity -> Models")
|
||||||
|
inner class EntityToModels {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `convert NoteEntity to Note`() {
|
||||||
|
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 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 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 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 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 noteMetadata = NoteMetadata("title", emptyList())
|
||||||
|
val entity = converter.toEntity(noteMetadata)
|
||||||
|
|
||||||
|
assertThat(entity)
|
||||||
|
.hasFieldOrPropertyWithValue("title", "title")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `convert PersistedNote to NoteEntity`() {
|
||||||
|
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 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
persistence/test/converters/UserConverterTest.kt
Normal file
48
persistence/test/converters/UserConverterTest.kt
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package be.simplenotes.persistence.converters
|
||||||
|
|
||||||
|
import be.simplenotes.persistence.users.UserEntity
|
||||||
|
import be.simplenotes.types.PersistedUser
|
||||||
|
import be.simplenotes.types.User
|
||||||
|
import io.micronaut.context.BeanContext
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
internal class UserConverterTest {
|
||||||
|
|
||||||
|
private val ctx = BeanContext.run()
|
||||||
|
private val converter = ctx.getBean(UserConverter::class.java)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `convert UserEntity to User`() {
|
||||||
|
val entity = UserEntity {
|
||||||
|
username = "test"
|
||||||
|
password = "test2"
|
||||||
|
}.apply {
|
||||||
|
this["id"] = 2
|
||||||
|
}
|
||||||
|
val user = converter.toUser(entity)
|
||||||
|
assertThat(user).isEqualTo(User("test", "test2"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `convert UserEntity to PersistedUser`() {
|
||||||
|
val entity = UserEntity {
|
||||||
|
username = "test"
|
||||||
|
password = "test2"
|
||||||
|
}.apply {
|
||||||
|
this["id"] = 2
|
||||||
|
}
|
||||||
|
val user = converter.toPersistedUser(entity)
|
||||||
|
assertThat(user).isEqualTo(PersistedUser("test", "test2", 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `convert User to UserEntity`() {
|
||||||
|
val user = User("test", "test2")
|
||||||
|
val entity = converter.toEntity(user)
|
||||||
|
|
||||||
|
assertThat(entity)
|
||||||
|
.hasFieldOrPropertyWithValue("username", "test")
|
||||||
|
.hasFieldOrPropertyWithValue("password", "test2")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,29 +1,24 @@
|
|||||||
package be.simplenotes.persistence
|
package be.simplenotes.persistence.notes
|
||||||
|
|
||||||
import be.simplenotes.persistence.notes.*
|
import be.simplenotes.persistence.DbTest
|
||||||
|
import be.simplenotes.persistence.converters.NoteConverter
|
||||||
import be.simplenotes.persistence.repositories.NoteRepository
|
import be.simplenotes.persistence.repositories.NoteRepository
|
||||||
import be.simplenotes.persistence.repositories.UserRepository
|
import be.simplenotes.persistence.repositories.UserRepository
|
||||||
import be.simplenotes.persistence.users.createFakeUser
|
import be.simplenotes.persistence.users.createFakeUser
|
||||||
import be.simplenotes.types.ExportedNote
|
import be.simplenotes.types.ExportedNote
|
||||||
import be.simplenotes.types.PersistedNote
|
|
||||||
import be.simplenotes.types.PersistedUser
|
import be.simplenotes.types.PersistedUser
|
||||||
|
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.assertThat
|
||||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.*
|
||||||
import org.junit.jupiter.api.DisplayName
|
|
||||||
import org.junit.jupiter.api.Nested
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import org.junit.jupiter.api.parallel.ResourceLock
|
|
||||||
import org.ktorm.database.Database
|
|
||||||
import org.ktorm.dsl.eq
|
|
||||||
import org.ktorm.entity.filter
|
|
||||||
import org.ktorm.entity.find
|
|
||||||
import org.ktorm.entity.mapColumns
|
|
||||||
import org.ktorm.entity.toList
|
|
||||||
import java.sql.SQLIntegrityConstraintViolationException
|
import java.sql.SQLIntegrityConstraintViolationException
|
||||||
|
|
||||||
|
internal abstract class BaseNoteRepositoryImplTest : DbTest() {
|
||||||
internal class NoteRepositoryImplTest : DbTest() {
|
|
||||||
|
|
||||||
private lateinit var noteRepo: NoteRepository
|
private lateinit var noteRepo: NoteRepository
|
||||||
private lateinit var userRepo: UserRepository
|
private lateinit var userRepo: UserRepository
|
||||||
@ -99,6 +94,27 @@ internal class NoteRepositoryImplTest : DbTest() {
|
|||||||
assertThat(noteRepo.findAll(1000)).isEmpty()
|
assertThat(noteRepo.findAll(1000)).isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: datetime -> timestamp migration
|
||||||
|
@Disabled("Not working with mariadb, inserts are too fast so the updated_at is the same")
|
||||||
|
@Test
|
||||||
|
fun pagination() {
|
||||||
|
(50 downTo 1).forEach { i ->
|
||||||
|
noteRepo.insertFakeNote(user1, "$i")
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(noteRepo.findAll(user1.id, limit = 20, offset = 0).onEach { println(it) })
|
||||||
|
.hasSize(20)
|
||||||
|
.allMatch { it.title.toInt() in 1..20 }
|
||||||
|
|
||||||
|
assertThat(noteRepo.findAll(user1.id, limit = 20, offset = 20))
|
||||||
|
.hasSize(20)
|
||||||
|
.allMatch { it.title.toInt() in 21..40 }
|
||||||
|
|
||||||
|
assertThat(noteRepo.findAll(user1.id, limit = 20, offset = 40))
|
||||||
|
.hasSize(10)
|
||||||
|
.allMatch { it.title.toInt() in 41..50 }
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `find all notes with tag`() {
|
fun `find all notes with tag`() {
|
||||||
with(noteRepo) {
|
with(noteRepo) {
|
||||||
@ -129,18 +145,12 @@ internal class NoteRepositoryImplTest : DbTest() {
|
|||||||
fun `find an existing note`() {
|
fun `find an existing note`() {
|
||||||
val fakeNote = noteRepo.insertFakeNote(user1)
|
val fakeNote = noteRepo.insertFakeNote(user1)
|
||||||
|
|
||||||
val note = db.notes.find { Notes.title eq fakeNote.title }!!
|
val converter = beanContext.getBean(NoteConverter::class.java)
|
||||||
|
|
||||||
|
val note = db.notes.find { it.title eq fakeNote.meta.title }!!
|
||||||
.let { entity ->
|
.let { entity ->
|
||||||
val tags = db.tags.filter { be.simplenotes.persistence.Tags.noteUuid eq entity.uuid }.mapColumns { be.simplenotes.persistence.Tags.name } as List<String>
|
val tags = db.tags.filter { it.noteUuid eq entity.uuid }.mapColumns { it.name } as List<String>
|
||||||
PersistedNote(
|
converter.toPersistedNote(entity, tags)
|
||||||
uuid = entity.uuid,
|
|
||||||
title = entity.title,
|
|
||||||
tags = tags,
|
|
||||||
markdown = entity.markdown,
|
|
||||||
html = entity.html,
|
|
||||||
updatedAt = entity.updatedAt,
|
|
||||||
public = entity.public,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assertThat(noteRepo.find(user1.id, note.uuid)).isEqualTo(note)
|
assertThat(noteRepo.find(user1.id, note.uuid)).isEqualTo(note)
|
||||||
@ -189,11 +199,11 @@ internal class NoteRepositoryImplTest : DbTest() {
|
|||||||
val notes1 = noteRepo.insertFakeNotes(user1, count = 3)
|
val notes1 = noteRepo.insertFakeNotes(user1, count = 3)
|
||||||
val notes2 = noteRepo.insertFakeNotes(user2, count = 1)
|
val notes2 = noteRepo.insertFakeNotes(user2, count = 1)
|
||||||
|
|
||||||
val user1Tags = notes1.flatMap { it.tags }.toSet()
|
val user1Tags = notes1.flatMap { it.meta.tags }.toSet()
|
||||||
assertThat(noteRepo.getTags(user1.id))
|
assertThat(noteRepo.getTags(user1.id))
|
||||||
.containsExactlyInAnyOrderElementsOf(user1Tags)
|
.containsExactlyInAnyOrderElementsOf(user1Tags)
|
||||||
|
|
||||||
val user2Tags = notes2.flatMap { it.tags }.toSet()
|
val user2Tags = notes2.flatMap { it.meta.tags }.toSet()
|
||||||
assertThat(noteRepo.getTags(user2.id))
|
assertThat(noteRepo.getTags(user2.id))
|
||||||
.containsExactlyInAnyOrderElementsOf(user2Tags)
|
.containsExactlyInAnyOrderElementsOf(user2Tags)
|
||||||
|
|
||||||
@ -219,7 +229,9 @@ internal class NoteRepositoryImplTest : DbTest() {
|
|||||||
.isEqualTo(newNote1)
|
.isEqualTo(newNote1)
|
||||||
|
|
||||||
val note2 = noteRepo.insertFakeNote(user1)
|
val note2 = noteRepo.insertFakeNote(user1)
|
||||||
val newNote2 = fakeNote().copy(tags = tagGenerator().take(3).toList())
|
val newNote2 = fakeNote().let {
|
||||||
|
it.copy(meta = it.meta.copy(tags = tagGenerator().take(3).toList()))
|
||||||
|
}
|
||||||
assertThat(noteRepo.update(user1.id, note2.uuid, newNote2))
|
assertThat(noteRepo.update(user1.id, note2.uuid, newNote2))
|
||||||
.isNotNull
|
.isNotNull
|
||||||
|
|
||||||
@ -241,7 +253,7 @@ internal class NoteRepositoryImplTest : DbTest() {
|
|||||||
.isTrue
|
.isTrue
|
||||||
|
|
||||||
val isDeleted = db.notes
|
val isDeleted = db.notes
|
||||||
.find { Notes.uuid eq note1.uuid }
|
.find { it.uuid eq note1.uuid }
|
||||||
?.deleted
|
?.deleted
|
||||||
|
|
||||||
assertThat(isDeleted).`as`("Check that Notes.deleted is true").isTrue
|
assertThat(isDeleted).`as`("Check that Notes.deleted is true").isTrue
|
||||||
@ -249,7 +261,7 @@ internal class NoteRepositoryImplTest : DbTest() {
|
|||||||
assertThat(noteRepo.restore(user1.id, note1.uuid)).isTrue
|
assertThat(noteRepo.restore(user1.id, note1.uuid)).isTrue
|
||||||
|
|
||||||
val isDeleted2 = db.notes
|
val isDeleted2 = db.notes
|
||||||
.find { Notes.uuid eq note1.uuid }
|
.find { it.uuid eq note1.uuid }
|
||||||
?.deleted
|
?.deleted
|
||||||
|
|
||||||
assertThat(isDeleted2).`as`("Check that Notes.deleted is false after restore()").isFalse
|
assertThat(isDeleted2).`as`("Check that Notes.deleted is false after restore()").isFalse
|
||||||
@ -290,8 +302,8 @@ internal class NoteRepositoryImplTest : DbTest() {
|
|||||||
|
|
||||||
val expected = notes.mapIndexed { i, n ->
|
val expected = notes.mapIndexed { i, n ->
|
||||||
ExportedNote(
|
ExportedNote(
|
||||||
title = n.title,
|
title = n.meta.title,
|
||||||
tags = n.tags,
|
tags = n.meta.tags,
|
||||||
markdown = n.markdown,
|
markdown = n.markdown,
|
||||||
html = n.html,
|
html = n.html,
|
||||||
updatedAt = n.updatedAt,
|
updatedAt = n.updatedAt,
|
||||||
31
persistence/test/notes/H2NoteRepositoryImplTests.kt
Normal file
31
persistence/test/notes/H2NoteRepositoryImplTests.kt
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package be.simplenotes.persistence.notes
|
||||||
|
|
||||||
|
import be.simplenotes.config.DataSourceConfig
|
||||||
|
import be.simplenotes.persistence.KMariadbContainer
|
||||||
|
import be.simplenotes.persistence.h2dataSourceConfig
|
||||||
|
import be.simplenotes.persistence.mariadbDataSourceConfig
|
||||||
|
import org.junit.jupiter.api.AfterAll
|
||||||
|
import org.junit.jupiter.api.Tag
|
||||||
|
import org.junit.jupiter.api.parallel.ResourceLock
|
||||||
|
|
||||||
|
@ResourceLock("h2")
|
||||||
|
internal class H2NoteRepositoryImplTests : BaseNoteRepositoryImplTest() {
|
||||||
|
override fun dataSourceConfig() = h2dataSourceConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Tag("slow")
|
||||||
|
@ResourceLock("mariadb")
|
||||||
|
internal class MariaDbNoteRepositoryImplTests : BaseNoteRepositoryImplTest() {
|
||||||
|
lateinit var mariaDB: KMariadbContainer
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
fun stopMariaDB() {
|
||||||
|
mariaDB.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dataSourceConfig(): DataSourceConfig {
|
||||||
|
mariaDB = KMariadbContainer()
|
||||||
|
mariaDB.start()
|
||||||
|
return mariadbDataSourceConfig(mariaDB.jdbcUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,17 +1,17 @@
|
|||||||
package be.simplenotes.persistence
|
package be.simplenotes.persistence.users
|
||||||
|
|
||||||
|
import be.simplenotes.persistence.DbTest
|
||||||
import be.simplenotes.persistence.repositories.UserRepository
|
import be.simplenotes.persistence.repositories.UserRepository
|
||||||
import be.simplenotes.persistence.users.fakeUser
|
import me.liuwj.ktorm.database.Database
|
||||||
|
import me.liuwj.ktorm.dsl.eq
|
||||||
|
import me.liuwj.ktorm.entity.find
|
||||||
|
import me.liuwj.ktorm.entity.toList
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
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.ktorm.database.Database
|
|
||||||
import org.ktorm.dsl.eq
|
|
||||||
import org.ktorm.entity.find
|
|
||||||
import org.ktorm.entity.toList
|
|
||||||
|
|
||||||
internal class UserRepositoryImplTest : DbTest() {
|
internal abstract class BaseUserRepositoryImplTest : DbTest() {
|
||||||
|
|
||||||
private lateinit var userRepo: UserRepository
|
private lateinit var userRepo: UserRepository
|
||||||
private lateinit var db: Database
|
private lateinit var db: Database
|
||||||
@ -30,7 +30,7 @@ internal class UserRepositoryImplTest : DbTest() {
|
|||||||
.extracting("username", "password")
|
.extracting("username", "password")
|
||||||
.contains(user.username, user.password)
|
.contains(user.username, user.password)
|
||||||
|
|
||||||
assertThat(db.users.find { Users.username eq user.username }).isNotNull
|
assertThat(db.users.find { it.username eq user.username }).isNotNull
|
||||||
assertThat(db.users.toList()).hasSize(1)
|
assertThat(db.users.toList()).hasSize(1)
|
||||||
assertThat(userRepo.create(user)).isNull()
|
assertThat(userRepo.create(user)).isNull()
|
||||||
}
|
}
|
||||||
31
persistence/test/users/UserRepositoryImplTests.kt
Normal file
31
persistence/test/users/UserRepositoryImplTests.kt
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package be.simplenotes.persistence.users
|
||||||
|
|
||||||
|
import be.simplenotes.config.DataSourceConfig
|
||||||
|
import be.simplenotes.persistence.KMariadbContainer
|
||||||
|
import be.simplenotes.persistence.h2dataSourceConfig
|
||||||
|
import be.simplenotes.persistence.mariadbDataSourceConfig
|
||||||
|
import org.junit.jupiter.api.AfterAll
|
||||||
|
import org.junit.jupiter.api.Tag
|
||||||
|
import org.junit.jupiter.api.parallel.ResourceLock
|
||||||
|
|
||||||
|
@ResourceLock("h2")
|
||||||
|
internal class UserRepositoryImplTest : BaseUserRepositoryImplTest() {
|
||||||
|
override fun dataSourceConfig() = h2dataSourceConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Tag("slow")
|
||||||
|
@ResourceLock("mariadb")
|
||||||
|
internal class MariaDbUserRepositoryImplTest : BaseUserRepositoryImplTest() {
|
||||||
|
lateinit var mariaDB: KMariadbContainer
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
fun stopMariaDB() {
|
||||||
|
mariaDB.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dataSourceConfig(): DataSourceConfig {
|
||||||
|
mariaDB = KMariadbContainer()
|
||||||
|
mariaDB.start()
|
||||||
|
return mariadbDataSourceConfig(mariaDB.jdbcUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,10 +12,10 @@ fun fakeTitle() = faker.lorem().characters(3, 50)!!
|
|||||||
fun tagGenerator() = generateSequence { faker.lorem().word() }
|
fun tagGenerator() = generateSequence { faker.lorem().word() }
|
||||||
fun fakeTags() = tagGenerator().take(faker.random().nextInt(0, 3)).toList()
|
fun fakeTags() = tagGenerator().take(faker.random().nextInt(0, 3)).toList()
|
||||||
fun fakeContent() = faker.lorem().paragraph(faker.random().nextInt(0, 3))!!
|
fun fakeContent() = faker.lorem().paragraph(faker.random().nextInt(0, 3))!!
|
||||||
fun fakeNote() = Note(fakeTitle(), fakeTags(), fakeContent(), fakeContent())
|
fun fakeNote() = Note(NoteMetadata(fakeTitle(), fakeTags()), fakeContent(), fakeContent())
|
||||||
|
|
||||||
fun PersistedNote.toPersistedMeta() =
|
fun PersistedNote.toPersistedMeta() =
|
||||||
PersistedNoteMetadata(title, tags, updatedAt, uuid)
|
PersistedNoteMetadata(meta.title, meta.tags, updatedAt, uuid)
|
||||||
|
|
||||||
fun NoteRepository.insertFakeNote(
|
fun NoteRepository.insertFakeNote(
|
||||||
userId: Int,
|
userId: Int,
|
||||||
@ -23,7 +23,7 @@ fun NoteRepository.insertFakeNote(
|
|||||||
tags: List<String> = emptyList(),
|
tags: List<String> = emptyList(),
|
||||||
md: String = "md",
|
md: String = "md",
|
||||||
html: String = "html",
|
html: String = "html",
|
||||||
): PersistedNote = create(userId, Note(title, tags, md, html))
|
): PersistedNote = create(userId, Note(NoteMetadata(title, tags), md, html))
|
||||||
|
|
||||||
fun NoteRepository.insertFakeNote(
|
fun NoteRepository.insertFakeNote(
|
||||||
user: PersistedUser,
|
user: PersistedUser,
|
||||||
|
|||||||
@ -5,10 +5,10 @@
|
|||||||
</pattern>
|
</pattern>
|
||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
<root level="INFO">
|
<root level="DEBUG">
|
||||||
<appender-ref ref="STDOUT"/>
|
<appender-ref ref="STDOUT"/>
|
||||||
</root>
|
</root>
|
||||||
<logger name="org.ktorm.database" level="DEBUG"/>
|
<logger name="me.liuwj.ktorm.database" level="DEBUG"/>
|
||||||
<logger name="com.zaxxer.hikari" level="ERROR"/>
|
<logger name="com.zaxxer.hikari" level="INFO"/>
|
||||||
<logger name="org.flywaydb.core" level="ERROR"/>
|
<logger name="org.flywaydb.core" level="DEBUG"/>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|||||||
@ -8,7 +8,6 @@ plugins {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":types"))
|
implementation(project(":types"))
|
||||||
implementation(project(":config"))
|
|
||||||
|
|
||||||
implementation(Libs.Lucene.core)
|
implementation(Libs.Lucene.core)
|
||||||
implementation(Libs.Lucene.queryParser)
|
implementation(Libs.Lucene.queryParser)
|
||||||
|
|||||||
@ -7,15 +7,18 @@ import org.apache.lucene.document.Field
|
|||||||
import org.apache.lucene.document.StringField
|
import org.apache.lucene.document.StringField
|
||||||
import org.apache.lucene.document.TextField
|
import org.apache.lucene.document.TextField
|
||||||
|
|
||||||
internal fun PersistedNote.toDocument() = Document().apply {
|
internal fun PersistedNote.toDocument(): Document {
|
||||||
// non searchable fields
|
val note = this
|
||||||
add(StringField(uuidField, UuidFieldConverter.toDoc(uuid), Field.Store.YES))
|
return Document().apply {
|
||||||
add(StringField(updatedAtField, LocalDateTimeFieldConverter.toDoc(updatedAt), Field.Store.YES))
|
// non searchable fields
|
||||||
|
add(StringField(uuidField, UuidFieldConverter.toDoc(note.uuid), Field.Store.YES))
|
||||||
|
add(StringField(updatedAtField, LocalDateTimeFieldConverter.toDoc(note.updatedAt), Field.Store.YES))
|
||||||
|
|
||||||
// searchable fields
|
// searchable fields
|
||||||
add(TextField(titleField, title, Field.Store.YES))
|
add(TextField(titleField, note.meta.title, Field.Store.YES))
|
||||||
add(TextField(tagsField, TagsFieldConverter.toDoc(tags), Field.Store.YES))
|
add(TextField(tagsField, TagsFieldConverter.toDoc(note.meta.tags), Field.Store.YES))
|
||||||
add(TextField(contentField, markdown, Field.Store.YES))
|
add(TextField(contentField, note.markdown, Field.Store.YES))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun Document.toNoteMeta() = PersistedNoteMetadata(
|
internal fun Document.toNoteMeta() = PersistedNoteMetadata(
|
||||||
|
|||||||
@ -16,7 +16,12 @@ import javax.inject.Named
|
|||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
internal class NoteSearcherImpl(@Named("search-index") basePath: Path) : NoteSearcher {
|
internal class NoteSearcherImpl(
|
||||||
|
@Named("search-index")
|
||||||
|
basePath: Path,
|
||||||
|
) : NoteSearcher {
|
||||||
|
|
||||||
|
constructor() : this(Path.of("/tmp", "lucene"))
|
||||||
|
|
||||||
private val baseFile = basePath.toFile()
|
private val baseFile = basePath.toFile()
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package be.simplenotes.search
|
package be.simplenotes.search
|
||||||
|
|
||||||
import be.simplenotes.config.DataConfig
|
|
||||||
import io.micronaut.context.annotation.Factory
|
import io.micronaut.context.annotation.Factory
|
||||||
import io.micronaut.context.annotation.Prototype
|
import io.micronaut.context.annotation.Prototype
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
@ -11,5 +10,5 @@ class SearchModule {
|
|||||||
|
|
||||||
@Named("search-index")
|
@Named("search-index")
|
||||||
@Prototype
|
@Prototype
|
||||||
internal fun luceneIndex(dataConfig: DataConfig) = Path.of(dataConfig.dataDir).resolve("index")
|
internal fun luceneIndex() = Path.of(".lucene")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package be.simplenotes.search
|
package be.simplenotes.search
|
||||||
|
|
||||||
|
import be.simplenotes.types.NoteMetadata
|
||||||
import be.simplenotes.types.PersistedNote
|
import be.simplenotes.types.PersistedNote
|
||||||
import be.simplenotes.types.PersistedNoteMetadata
|
import be.simplenotes.types.PersistedNoteMetadata
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
@ -8,7 +9,6 @@ import org.junit.jupiter.api.AfterAll
|
|||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.parallel.ResourceLock
|
import org.junit.jupiter.api.parallel.ResourceLock
|
||||||
import java.nio.file.Path
|
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ import java.util.*
|
|||||||
internal class NoteSearcherImplTest {
|
internal class NoteSearcherImplTest {
|
||||||
|
|
||||||
// region setup
|
// region setup
|
||||||
private val searcher = NoteSearcherImpl(Path.of("/tmp", "lucene"))
|
private val searcher = NoteSearcherImpl()
|
||||||
|
|
||||||
private fun index(
|
private fun index(
|
||||||
title: String,
|
title: String,
|
||||||
@ -25,12 +25,11 @@ internal class NoteSearcherImplTest {
|
|||||||
uuid: UUID = UUID.randomUUID(),
|
uuid: UUID = UUID.randomUUID(),
|
||||||
): PersistedNote {
|
): PersistedNote {
|
||||||
val note = PersistedNote(
|
val note = PersistedNote(
|
||||||
title = title,
|
NoteMetadata(title, tags),
|
||||||
tags = tags,
|
|
||||||
markdown = content,
|
markdown = content,
|
||||||
html = "",
|
html = "",
|
||||||
updatedAt = LocalDateTime.MIN,
|
LocalDateTime.MIN,
|
||||||
uuid = uuid,
|
uuid,
|
||||||
public = false
|
public = false
|
||||||
)
|
)
|
||||||
searcher.indexNote(1, note)
|
searcher.indexNote(1, note)
|
||||||
@ -151,7 +150,7 @@ internal class NoteSearcherImplTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `update index`() {
|
fun `update index`() {
|
||||||
val note = index("first")
|
val note = index("first")
|
||||||
searcher.updateIndex(1, note.copy(title = "new"))
|
searcher.updateIndex(1, note.copy(meta = note.meta.copy(title = "new")))
|
||||||
assertThat(search("first")).isEmpty()
|
assertThat(search("first")).isEmpty()
|
||||||
assertThat(search("new")).hasSize(1)
|
assertThat(search("new")).hasSize(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
@file:UseContextualSerialization(LocalDateTime::class, UUID::class)
|
|
||||||
|
|
||||||
package be.simplenotes.types
|
package be.simplenotes.types
|
||||||
|
|
||||||
|
import kotlinx.serialization.Contextual
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.UseContextualSerialization
|
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@ -17,26 +15,24 @@ data class NoteMetadata(
|
|||||||
data class PersistedNoteMetadata(
|
data class PersistedNoteMetadata(
|
||||||
val title: String,
|
val title: String,
|
||||||
val tags: List<String>,
|
val tags: List<String>,
|
||||||
val updatedAt: LocalDateTime,
|
@Contextual val updatedAt: LocalDateTime,
|
||||||
val uuid: UUID,
|
@Contextual val uuid: UUID,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Note(
|
data class Note(
|
||||||
val title: String,
|
val meta: NoteMetadata,
|
||||||
val tags: List<String>,
|
|
||||||
val markdown: String,
|
val markdown: String,
|
||||||
val html: String,
|
val html: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PersistedNote(
|
data class PersistedNote(
|
||||||
val uuid: UUID,
|
val meta: NoteMetadata,
|
||||||
val title: String,
|
|
||||||
val tags: List<String>,
|
|
||||||
val markdown: String,
|
val markdown: String,
|
||||||
val html: String,
|
val html: String,
|
||||||
val updatedAt: LocalDateTime,
|
@Contextual val updatedAt: LocalDateTime,
|
||||||
|
@Contextual val uuid: UUID,
|
||||||
val public: Boolean,
|
val public: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -46,6 +42,6 @@ data class ExportedNote(
|
|||||||
val tags: List<String>,
|
val tags: List<String>,
|
||||||
val markdown: String,
|
val markdown: String,
|
||||||
val html: String,
|
val html: String,
|
||||||
val updatedAt: LocalDateTime,
|
@Contextual val updatedAt: LocalDateTime,
|
||||||
val trash: Boolean,
|
val trash: Boolean,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -121,7 +121,7 @@ class NoteView(@Named("styles") styles: String) : View(styles) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun renderedNote(loggedInUser: LoggedInUser?, note: PersistedNote, shared: Boolean) = renderPage(
|
fun renderedNote(loggedInUser: LoggedInUser?, note: PersistedNote, shared: Boolean) = renderPage(
|
||||||
note.title,
|
note.meta.title,
|
||||||
loggedInUser = loggedInUser,
|
loggedInUser = loggedInUser,
|
||||||
scripts = listOf("/highlight.10.1.2.js", "/init-highlight.0.0.1.js")
|
scripts = listOf("/highlight.10.1.2.js", "/init-highlight.0.0.1.js")
|
||||||
) {
|
) {
|
||||||
@ -136,9 +136,9 @@ class NoteView(@Named("styles") styles: String) : View(styles) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
div("flex items-center justify-between mb-4") {
|
div("flex items-center justify-between mb-4") {
|
||||||
h1("text-3xl fond-bold underline") { +note.title }
|
h1("text-3xl fond-bold underline") { +note.meta.title }
|
||||||
span("space-x-2") {
|
span("space-x-2") {
|
||||||
note.tags.forEach {
|
note.meta.tags.forEach {
|
||||||
a(href = "/notes?tag=$it", classes = "tag") {
|
a(href = "/notes?tag=$it", classes = "tag") {
|
||||||
+"#$it"
|
+"#$it"
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user