diff --git a/buildSrc/src/main/kotlin/be/simplenotes/Libs.kt b/buildSrc/src/main/kotlin/be/simplenotes/Libs.kt index ece768e..1182b42 100644 --- a/buildSrc/src/main/kotlin/be/simplenotes/Libs.kt +++ b/buildSrc/src/main/kotlin/be/simplenotes/Libs.kt @@ -16,13 +16,10 @@ object Libs { object Drivers { const val h2 = "com.h2database:h2:1.4.200" - const val mariadb = "org.mariadb.jdbc:mariadb-java-client:2.7.2" } object Ktorm { - private const val version = "3.0.0" - const val core = "me.liuwj.ktorm:ktorm-core:$version" - const val mysql = "me.liuwj.ktorm:ktorm-support-mysql:$version" + const val core = "org.ktorm:ktorm-core:3.3.0" } } diff --git a/persistence/build.gradle.kts b/persistence/build.gradle.kts index 5d7e826..2f4f248 100644 --- a/persistence/build.gradle.kts +++ b/persistence/build.gradle.kts @@ -12,12 +12,10 @@ dependencies { implementation(project(":types")) implementation(project(":config")) - implementation(Libs.Database.Drivers.mariadb) implementation(Libs.Database.Drivers.h2) implementation(Libs.Database.flyway) implementation(Libs.Database.hikariCP) implementation(Libs.Database.Ktorm.core) - runtimeOnly(Libs.Database.Ktorm.mysql) implementation(Libs.Slf4J.api) runtimeOnly(Libs.Slf4J.logback) diff --git a/persistence/resources/db/migration/other/V1__Create_tables.sql b/persistence/resources/db/migration/V1__Create_tables.sql similarity index 89% rename from persistence/resources/db/migration/other/V1__Create_tables.sql rename to persistence/resources/db/migration/V1__Create_tables.sql index 5f60fd8..8f00cd3 100644 --- a/persistence/resources/db/migration/other/V1__Create_tables.sql +++ b/persistence/resources/db/migration/V1__Create_tables.sql @@ -9,7 +9,7 @@ create table Users create table Notes ( - uuid binary(16) not null primary key, + uuid uuid not null primary key, title varchar(50) not null, markdown mediumtext not null, html mediumtext not null, @@ -17,7 +17,6 @@ create table Notes updated_at datetime null, constraint Notes_fk_user foreign key (user_id) references Users (id) on delete cascade - ); create index user_id on Notes (user_id); @@ -26,7 +25,7 @@ create table Tags ( id int auto_increment primary key, name varchar(50) not null, - note_uuid binary(16) not null, + note_uuid uuid not null, constraint Tags_fk_note foreign key (note_uuid) references Notes (uuid) on delete cascade ); diff --git a/persistence/resources/db/migration/mariadb/V2__Add_deleted_column.sql b/persistence/resources/db/migration/V2__Add_deleted_column.sql similarity index 100% rename from persistence/resources/db/migration/mariadb/V2__Add_deleted_column.sql rename to persistence/resources/db/migration/V2__Add_deleted_column.sql diff --git a/persistence/resources/db/migration/mariadb/V3__Add_public_column.sql b/persistence/resources/db/migration/V3__Add_public_column.sql similarity index 100% rename from persistence/resources/db/migration/mariadb/V3__Add_public_column.sql rename to persistence/resources/db/migration/V3__Add_public_column.sql diff --git a/persistence/resources/db/migration/V4__Add_notes_with_tags_view.sql b/persistence/resources/db/migration/V4__Add_notes_with_tags_view.sql new file mode 100644 index 0000000..ffd5baa --- /dev/null +++ b/persistence/resources/db/migration/V4__Add_notes_with_tags_view.sql @@ -0,0 +1,5 @@ +CREATE VIEW NotesWithTags AS + SELECT n.*, coalesce(ARRAY_AGG(t.NAME), ARRAY[]) as tags + FROM Notes n + LEFT JOIN Tags t ON n.uuid = t.note_uuid + GROUP BY n.uuid diff --git a/persistence/resources/db/migration/mariadb/V1__Create_tables.sql b/persistence/resources/db/migration/mariadb/V1__Create_tables.sql deleted file mode 100644 index a395f25..0000000 --- a/persistence/resources/db/migration/mariadb/V1__Create_tables.sql +++ /dev/null @@ -1,36 +0,0 @@ -create table Users -( - id int auto_increment primary key, - username varchar(50) not null, - password varchar(255) not null, - - constraint username unique (username) -) character set 'utf8mb4' - collate 'utf8mb4_general_ci'; - -create table Notes -( - uuid binary(16) not null primary key, - title varchar(50) not null, - markdown mediumtext not null, - html mediumtext not null, - user_id int not null, - updated_at datetime null, - - constraint Notes_fk_user foreign key (user_id) references Users (id) on delete cascade - -) character set 'utf8mb4' - collate 'utf8mb4_general_ci'; - -create index user_id on Notes (user_id); - -create table Tags -( - id int auto_increment primary key, - name varchar(50) not null, - note_uuid binary(16) not null, - constraint Tags_fk_note foreign key (note_uuid) references Notes (uuid) on delete cascade -) character set 'utf8mb4' - collate 'utf8mb4_general_ci'; - -create index note_uuid on Tags (note_uuid); diff --git a/persistence/resources/db/migration/other/V2__Add_deleted_column.sql b/persistence/resources/db/migration/other/V2__Add_deleted_column.sql deleted file mode 100644 index d704e62..0000000 --- a/persistence/resources/db/migration/other/V2__Add_deleted_column.sql +++ /dev/null @@ -1,2 +0,0 @@ -alter table Notes - add column deleted bool not null default false diff --git a/persistence/resources/db/migration/other/V3__Add_public_column.sql b/persistence/resources/db/migration/other/V3__Add_public_column.sql deleted file mode 100644 index c109304..0000000 --- a/persistence/resources/db/migration/other/V3__Add_public_column.sql +++ /dev/null @@ -1,2 +0,0 @@ -alter table Notes - add column public bool not null default false diff --git a/persistence/src/Entities.kt b/persistence/src/Entities.kt new file mode 100644 index 0000000..cd14573 --- /dev/null +++ b/persistence/src/Entities.kt @@ -0,0 +1,55 @@ +package be.simplenotes.persistence + +import org.ktorm.database.Database +import org.ktorm.entity.Entity +import org.ktorm.entity.sequenceOf +import java.time.LocalDateTime +import java.util.* + +internal val Database.users get() = sequenceOf(Users, withReferences = false) +internal val Database.notes get() = sequenceOf(Notes, withReferences = false) +internal val Database.tags get() = sequenceOf(Tags, withReferences = false) +internal val Database.noteWithTags get() = sequenceOf(NotesWithTags, withReferences = false) + +internal interface UserEntity : Entity { + companion object : Entity.Factory() + + var id: Int + var username: String + var password: String +} + +internal interface NoteEntity : Entity { + companion object : Entity.Factory() + + 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 { + companion object : Entity.Factory() + + val id: Int + var name: String + var note: NoteEntity +} + +internal interface NoteWithTagsEntity : Entity { + companion object : Entity.Factory() + + 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 +} diff --git a/persistence/src/HealthCheck.kt b/persistence/src/HealthCheck.kt deleted file mode 100644 index b966647..0000000 --- a/persistence/src/HealthCheck.kt +++ /dev/null @@ -1,31 +0,0 @@ -package be.simplenotes.persistence - -import be.simplenotes.config.DataSourceConfig -import be.simplenotes.persistence.utils.DbType -import be.simplenotes.persistence.utils.type -import me.liuwj.ktorm.database.Database -import me.liuwj.ktorm.database.asIterable -import me.liuwj.ktorm.database.use -import java.sql.SQLException -import javax.inject.Singleton - -interface DbHealthCheck { - fun isOk(): Boolean -} - -@Singleton -internal class DbHealthCheckImpl( - private val db: Database, - private val dataSourceConfig: DataSourceConfig, -) : DbHealthCheck { - override fun isOk() = if (dataSourceConfig.type() == DbType.H2) true - else try { - db.useConnection { connection -> - connection.prepareStatement("""SHOW DATABASES""").use { - it.executeQuery().asIterable().map { it.getString(1) } - } - }.any { it in dataSourceConfig.jdbcUrl } - } catch (e: SQLException) { - false - } -} diff --git a/persistence/src/Migrations.kt b/persistence/src/Migrations.kt index 0a32465..f8192a3 100644 --- a/persistence/src/Migrations.kt +++ b/persistence/src/Migrations.kt @@ -1,8 +1,5 @@ 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 javax.inject.Singleton import javax.sql.DataSource @@ -12,20 +9,11 @@ interface DbMigrations { } @Singleton -internal class DbMigrationsImpl( - private val dataSource: DataSource, - private val dataSourceConfig: DataSourceConfig, -) : DbMigrations { +internal class DbMigrationsImpl(private val dataSource: DataSource) : DbMigrations { override fun migrate() { - - val migrationDir = when (dataSourceConfig.type()) { - DbType.H2 -> "db/migration/other" - DbType.MariaDb -> "db/migration/mariadb" - } - Flyway.configure() .dataSource(dataSource) - .locations(migrationDir) + .locations("db/migration") .load() .migrate() } diff --git a/persistence/src/PersistenceModule.kt b/persistence/src/PersistenceModule.kt index b4dd6b6..ecd6171 100644 --- a/persistence/src/PersistenceModule.kt +++ b/persistence/src/PersistenceModule.kt @@ -1,11 +1,13 @@ package be.simplenotes.persistence import be.simplenotes.config.DataSourceConfig +import be.simplenotes.persistence.extensions.CustomSqlFormatter import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import io.micronaut.context.annotation.Bean import io.micronaut.context.annotation.Factory -import me.liuwj.ktorm.database.Database +import org.ktorm.database.Database +import org.ktorm.database.SqlDialect import javax.inject.Singleton import javax.sql.DataSource @@ -15,7 +17,10 @@ class PersistenceModule { @Singleton internal fun database(migrations: DbMigrations, dataSource: DataSource): Database { migrations.migrate() - return Database.connect(dataSource) + return Database.connect(dataSource, dialect = object : SqlDialect { + override fun createSqlFormatter(database: Database, beautifySql: Boolean, indentSize: Int) = + CustomSqlFormatter(database, beautifySql, indentSize) + }) } @Singleton @@ -23,15 +28,12 @@ class PersistenceModule { internal fun dataSource(conf: DataSourceConfig): HikariDataSource { val hikariConfig = HikariConfig().also { it.jdbcUrl = conf.jdbcUrl - it.driverClassName = when { - conf.jdbcUrl.startsWith("jdbc:mariadb") -> "org.mariadb.jdbc.Driver" - conf.jdbcUrl.startsWith("jdbc:h2") -> "org.h2.Driver" - else -> error("Unsupported database") - } + it.driverClassName = "org.h2.Driver" it.username = conf.username it.password = conf.password it.maximumPoolSize = conf.maximumPoolSize it.connectionTimeout = conf.connectionTimeout + it.dataSourceProperties["CASE_INSENSITIVE_IDENTIFIERS"] = "TRUE" } return HikariDataSource(hikariConfig) } diff --git a/persistence/src/Tables.kt b/persistence/src/Tables.kt new file mode 100644 index 0000000..9231abd --- /dev/null +++ b/persistence/src/Tables.kt @@ -0,0 +1,42 @@ +package be.simplenotes.persistence + +import be.simplenotes.persistence.extensions.varcharArray +import org.ktorm.schema.* + +internal object Users : Table("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("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("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("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 +} diff --git a/persistence/src/converters/Converters.kt b/persistence/src/converters/Converters.kt new file mode 100644 index 0000000..fd12b2a --- /dev/null +++ b/persistence/src/converters/Converters.kt @@ -0,0 +1,34 @@ +package be.simplenotes.persistence.converters + +import be.simplenotes.persistence.NoteEntity +import be.simplenotes.persistence.NoteWithTagsEntity +import be.simplenotes.persistence.UserEntity +import be.simplenotes.types.* +import org.mapstruct.Mapper +import org.mapstruct.Mapping +import org.mapstruct.ReportingPolicy +import java.time.LocalDateTime +import java.util.* + +@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE, componentModel = "jsr330") +internal interface UserConverter { + fun toPersistedUser(userEntity: UserEntity?): PersistedUser? +} + +@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE, componentModel = "jsr330") +internal abstract class NoteConverter { + + @Mapping(target = "trash", source = "deleted") + abstract fun toExportedNote(entity: NoteWithTagsEntity?): ExportedNote? + abstract fun toPersistedNote(entity: NoteWithTagsEntity?): PersistedNote? + abstract fun toPersistedNoteMetadata(entity: NoteWithTagsEntity?): PersistedNoteMetadata? + + fun toEntity(note: Note, uuid: UUID, userId: Int, updatedAt: LocalDateTime) = NoteEntity { + this.title = note.title + this.markdown = note.markdown + this.html = note.html + this.uuid = uuid + this.user.id = userId + this.updatedAt = updatedAt + } +} diff --git a/persistence/src/converters/NoteConverter.kt b/persistence/src/converters/NoteConverter.kt deleted file mode 100644 index ec9e777..0000000 --- a/persistence/src/converters/NoteConverter.kt +++ /dev/null @@ -1,85 +0,0 @@ -package be.simplenotes.persistence.converters - -import be.simplenotes.persistence.notes.NoteEntity -import be.simplenotes.types.* -import me.liuwj.ktorm.entity.Entity -import org.mapstruct.Mapper -import org.mapstruct.Mapping -import org.mapstruct.Mappings -import org.mapstruct.ReportingPolicy -import java.time.LocalDateTime -import java.util.* -import javax.inject.Singleton - -@Mapper( - uses = [NoteEntityFactory::class, UserEntityFactory::class], - unmappedTargetPolicy = ReportingPolicy.IGNORE, - componentModel = "jsr330" -) -internal abstract class NoteConverter { - - fun toNote(entity: NoteEntity, tags: Tags) = - Note(NoteMetadata(title = entity.title, tags = tags), entity.markdown, entity.html) - - @Mappings( - Mapping(target = ".", source = "entity"), - Mapping(target = "tags", source = "tags"), - ) - abstract fun toNoteMetadata(entity: NoteEntity, tags: Tags): NoteMetadata - - @Mappings( - Mapping(target = ".", source = "entity"), - Mapping(target = "tags", source = "tags"), - ) - abstract fun toPersistedNoteMetadata(entity: NoteEntity, tags: Tags): PersistedNoteMetadata - - fun toPersistedNote(entity: NoteEntity, tags: Tags) = PersistedNote( - NoteMetadata(title = entity.title, tags = tags), - entity.markdown, - entity.html, - entity.updatedAt, - entity.uuid, - entity.public - ) - - @Mappings( - Mapping(target = ".", source = "entity"), - Mapping(target = "trash", source = "entity.deleted"), - Mapping(target = "tags", source = "tags"), - ) - abstract fun toExportedNote(entity: NoteEntity, tags: Tags): ExportedNote - - fun toEntity(note: Note, uuid: UUID, userId: Int, updatedAt: LocalDateTime) = NoteEntity { - this.title = note.meta.title - this.markdown = note.markdown - this.html = note.html - this.uuid = uuid - this.deleted = false - this.public = false - this.user.id = userId - this.updatedAt = updatedAt - } - - @Mappings( - Mapping(target = ".", source = "note"), - Mapping(target = "updatedAt", source = "updatedAt"), - Mapping(target = "uuid", source = "uuid"), - Mapping(target = "public", constant = "false"), - ) - abstract fun toPersistedNote(note: Note, updatedAt: LocalDateTime, uuid: UUID): PersistedNote - - abstract fun toEntity(persistedNoteMetadata: PersistedNoteMetadata): NoteEntity - - abstract fun toEntity(noteMetadata: NoteMetadata): NoteEntity - - @Mapping(target = "title", source = "meta.title") - abstract fun toEntity(persistedNote: PersistedNote): NoteEntity - - @Mapping(target = "deleted", source = "trash") - abstract fun toEntity(exportedNote: ExportedNote): NoteEntity -} - -typealias Tags = List - -@Singleton -internal class NoteEntityFactory : Entity.Factory() diff --git a/persistence/src/converters/UserConverter.kt b/persistence/src/converters/UserConverter.kt deleted file mode 100644 index 9071e6f..0000000 --- a/persistence/src/converters/UserConverter.kt +++ /dev/null @@ -1,24 +0,0 @@ -package be.simplenotes.persistence.converters - -import be.simplenotes.persistence.users.UserEntity -import be.simplenotes.types.PersistedUser -import be.simplenotes.types.User -import me.liuwj.ktorm.entity.Entity -import org.mapstruct.Mapper -import org.mapstruct.ReportingPolicy -import javax.inject.Singleton - -@Mapper( - uses = [UserEntityFactory::class], - unmappedTargetPolicy = ReportingPolicy.IGNORE, - componentModel = "jsr330" -) -internal interface UserConverter { - fun toUser(userEntity: UserEntity): User - fun toPersistedUser(userEntity: UserEntity): PersistedUser - fun toEntity(user: User): UserEntity - fun toEntity(user: PersistedUser): UserEntity -} - -@Singleton -internal class UserEntityFactory : Entity.Factory() diff --git a/persistence/src/extensions/KtormExtensions.kt b/persistence/src/extensions/KtormExtensions.kt index bf6cdc6..7a07c63 100644 --- a/persistence/src/extensions/KtormExtensions.kt +++ b/persistence/src/extensions/KtormExtensions.kt @@ -1,26 +1,77 @@ package be.simplenotes.persistence.extensions -import me.liuwj.ktorm.schema.BaseTable -import me.liuwj.ktorm.schema.SqlType -import java.nio.ByteBuffer +import org.ktorm.database.Database +import org.ktorm.expression.* +import org.ktorm.schema.* import java.sql.PreparedStatement import java.sql.ResultSet import java.sql.Types -import java.util.* -internal class UuidBinarySqlType : SqlType(Types.BINARY, typeName = "uuidBinary") { - override fun doGetResult(rs: ResultSet, index: Int): UUID? { - val value = rs.getBytes(index) ?: return null - return ByteBuffer.wrap(value).let { b -> UUID(b.long, b.long) } +internal class VarcharArraySqlType : SqlType>(Types.ARRAY, typeName = "varchar[]") { + override fun doGetResult(rs: ResultSet, index: Int): List? { + return when (val array = rs.getObject(index)) { + null -> null + is Array<*> -> array.map { it.toString() } + else -> error("") + } } - override fun doSetParameter(ps: PreparedStatement, index: Int, parameter: UUID) { - val bytes = ByteBuffer.allocate(16) - .putLong(parameter.mostSignificantBits) - .putLong(parameter.leastSignificantBits) - .array() - ps.setBytes(index, bytes) + override fun doSetParameter(ps: PreparedStatement, index: Int, parameter: List) { + throw UnsupportedOperationException() } } -internal fun BaseTable.uuidBinary(name: String) = registerColumn(name, UuidBinarySqlType()) +internal fun BaseTable.varcharArray(name: String) = registerColumn(name, VarcharArraySqlType()) + +data class ArrayContainsExpression( + val left: ScalarExpression<*>, + val right: ScalarExpression<*>, + override val sqlType: SqlType = BooleanSqlType, + override val isLeafNode: Boolean = false +) : ScalarExpression() { + override val extraProperties: Map 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) + } +} diff --git a/persistence/src/notes/NoteRepositoryImpl.kt b/persistence/src/notes/NoteRepositoryImpl.kt deleted file mode 100644 index 01d5d60..0000000 --- a/persistence/src/notes/NoteRepositoryImpl.kt +++ /dev/null @@ -1,219 +0,0 @@ -package be.simplenotes.persistence.notes - -import be.simplenotes.persistence.converters.NoteConverter -import be.simplenotes.persistence.repositories.NoteRepository -import be.simplenotes.types.ExportedNote -import be.simplenotes.types.Note -import be.simplenotes.types.PersistedNote -import be.simplenotes.types.PersistedNoteMetadata -import me.liuwj.ktorm.database.Database -import me.liuwj.ktorm.dsl.* -import me.liuwj.ktorm.entity.* -import java.time.LocalDateTime -import java.util.* -import javax.inject.Singleton -import kotlin.collections.HashMap - -@Singleton -internal class NoteRepositoryImpl( - private val db: Database, - private val converter: NoteConverter, -) : NoteRepository { - - @Throws(IllegalArgumentException::class) - override fun findAll( - userId: Int, - limit: Int, - offset: Int, - tag: String?, - deleted: Boolean, - ): List { - require(limit > 0) { "limit should be positive" } - require(offset >= 0) { "offset should not be negative" } - - val uuids1: List? = 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 = - 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 { - - 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 { - 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.tagsByUuid(): Map> { - 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 }) - } -} diff --git a/persistence/src/notes/Notes.kt b/persistence/src/notes/Notes.kt deleted file mode 100644 index 1237dfe..0000000 --- a/persistence/src/notes/Notes.kt +++ /dev/null @@ -1,45 +0,0 @@ -@file:Suppress("unused") - -package be.simplenotes.persistence.notes - -import be.simplenotes.persistence.extensions.uuidBinary -import be.simplenotes.persistence.users.UserEntity -import be.simplenotes.persistence.users.Users -import me.liuwj.ktorm.database.Database -import me.liuwj.ktorm.entity.Entity -import me.liuwj.ktorm.entity.sequenceOf -import me.liuwj.ktorm.schema.* -import java.time.LocalDateTime -import java.util.* - -internal open class Notes(alias: String?) : Table("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 { - companion object : Entity.Factory() - - var uuid: UUID - var title: String - var markdown: String - var html: String - var updatedAt: LocalDateTime - var deleted: Boolean - var public: Boolean - - var user: UserEntity -} diff --git a/persistence/src/notes/Tags.kt b/persistence/src/notes/Tags.kt deleted file mode 100644 index 609f7f1..0000000 --- a/persistence/src/notes/Tags.kt +++ /dev/null @@ -1,30 +0,0 @@ -package be.simplenotes.persistence.notes - -import be.simplenotes.persistence.extensions.uuidBinary -import me.liuwj.ktorm.database.Database -import me.liuwj.ktorm.entity.Entity -import me.liuwj.ktorm.entity.sequenceOf -import me.liuwj.ktorm.schema.Table -import me.liuwj.ktorm.schema.int -import me.liuwj.ktorm.schema.varchar - -internal open class Tags(alias: String?) : Table("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 { - companion object : Entity.Factory() - - val id: Int - var name: String - var note: NoteEntity -} diff --git a/persistence/src/repositories/NoteRepositoryImpl.kt b/persistence/src/repositories/NoteRepositoryImpl.kt new file mode 100644 index 0000000..992c41a --- /dev/null +++ b/persistence/src/repositories/NoteRepositoryImpl.kt @@ -0,0 +1,144 @@ +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 = 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.runIf(condition: Boolean, block: T.() -> T) = run { if (condition) block(this) else this } diff --git a/persistence/src/repositories/UserRepositoryImpl.kt b/persistence/src/repositories/UserRepositoryImpl.kt new file mode 100644 index 0000000..7ff9d89 --- /dev/null +++ b/persistence/src/repositories/UserRepositoryImpl.kt @@ -0,0 +1,40 @@ +package be.simplenotes.persistence.repositories + +import be.simplenotes.persistence.Users +import be.simplenotes.persistence.converters.UserConverter +import be.simplenotes.persistence.users +import be.simplenotes.types.PersistedUser +import be.simplenotes.types.User +import org.ktorm.database.Database +import org.ktorm.dsl.* +import org.ktorm.entity.any +import org.ktorm.entity.find +import java.sql.SQLIntegrityConstraintViolationException +import 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) } +} diff --git a/persistence/src/transactions/KtormTransactionService.kt b/persistence/src/transactions/KtormTransactionService.kt deleted file mode 100644 index 37e03d3..0000000 --- a/persistence/src/transactions/KtormTransactionService.kt +++ /dev/null @@ -1,9 +0,0 @@ -package be.simplenotes.persistence.transactions - -import me.liuwj.ktorm.database.Database -import javax.inject.Singleton - -@Singleton -internal class KtormTransactionService(private val database: Database) : TransactionService { - override fun use(block: () -> T) = database.useTransaction { block() } -} diff --git a/persistence/src/transactions/TransactionService.kt b/persistence/src/transactions/TransactionService.kt deleted file mode 100644 index d87de66..0000000 --- a/persistence/src/transactions/TransactionService.kt +++ /dev/null @@ -1,5 +0,0 @@ -package be.simplenotes.persistence.transactions - -interface TransactionService { - fun use(block: () -> T): T -} diff --git a/persistence/src/users/UserRepositoryImpl.kt b/persistence/src/users/UserRepositoryImpl.kt deleted file mode 100644 index 8e50748..0000000 --- a/persistence/src/users/UserRepositoryImpl.kt +++ /dev/null @@ -1,42 +0,0 @@ -package be.simplenotes.persistence.users - -import be.simplenotes.persistence.converters.UserConverter -import be.simplenotes.persistence.repositories.UserRepository -import be.simplenotes.types.PersistedUser -import be.simplenotes.types.User -import me.liuwj.ktorm.database.Database -import me.liuwj.ktorm.dsl.* -import me.liuwj.ktorm.entity.any -import me.liuwj.ktorm.entity.find -import java.sql.SQLIntegrityConstraintViolationException -import javax.inject.Singleton - -@Singleton -internal class UserRepositoryImpl( - private val db: Database, - private val converter: UserConverter, -) : UserRepository { - override fun create(user: User): PersistedUser? { - return try { - val id = db.insertAndGenerateKey(Users) { - it.username to user.username - it.password to user.password - } as Int - PersistedUser(user.username, user.password, id) - } catch (e: SQLIntegrityConstraintViolationException) { - null - } - } - - override fun find(username: String) = db.users.find { it.username eq username } - ?.let { converter.toPersistedUser(it) } - - override fun find(id: Int) = db.users.find { it.id eq id }?.let { - converter.toPersistedUser(it) - } - - override fun exists(username: String) = db.users.any { it.username eq username } - override fun exists(id: Int) = db.users.any { it.id eq id } - override fun delete(id: Int) = db.delete(Users) { it.id eq id } == 1 - override fun findAll() = db.from(Users).select(Users.id).map { it[Users.id]!! } -} diff --git a/persistence/src/users/Users.kt b/persistence/src/users/Users.kt deleted file mode 100644 index 81940e2..0000000 --- a/persistence/src/users/Users.kt +++ /dev/null @@ -1,28 +0,0 @@ -package be.simplenotes.persistence.users - -import me.liuwj.ktorm.database.Database -import me.liuwj.ktorm.entity.Entity -import me.liuwj.ktorm.entity.sequenceOf -import me.liuwj.ktorm.schema.Table -import me.liuwj.ktorm.schema.int -import me.liuwj.ktorm.schema.varchar - -internal open class Users(alias: String?) : Table("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 { - companion object : Entity.Factory() - - var id: Int - var username: String - var password: String -} - -internal val Database.users get() = this.sequenceOf(Users, withReferences = false) diff --git a/persistence/src/utils/DataSourceConfigUtils.kt b/persistence/src/utils/DataSourceConfigUtils.kt deleted file mode 100644 index 4c2236e..0000000 --- a/persistence/src/utils/DataSourceConfigUtils.kt +++ /dev/null @@ -1,8 +0,0 @@ -package be.simplenotes.persistence.utils - -import be.simplenotes.config.DataSourceConfig - -enum class DbType { H2, MariaDb } - -fun DataSourceConfig.type(): DbType = if (jdbcUrl.contains("mariadb")) DbType.MariaDb -else DbType.H2 diff --git a/persistence/test/DataSources.kt b/persistence/test/DataSources.kt deleted file mode 100644 index adacdbc..0000000 --- a/persistence/test/DataSources.kt +++ /dev/null @@ -1,22 +0,0 @@ -package be.simplenotes.persistence - -import be.simplenotes.config.DataSourceConfig -import org.testcontainers.containers.MariaDBContainer - -class KMariadbContainer : MariaDBContainer("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 -) diff --git a/persistence/test/DbHealthCheckImplTest.kt b/persistence/test/DbHealthCheckImplTest.kt deleted file mode 100644 index 2782e1a..0000000 --- a/persistence/test/DbHealthCheckImplTest.kt +++ /dev/null @@ -1,37 +0,0 @@ -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().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() - assertThat(healthCheck.isOk()).isTrue - mariaDB.stop() - assertThat(healthCheck.isOk()).isFalse - } -} diff --git a/persistence/test/DbTest.kt b/persistence/test/DbTest.kt index 5f76564..0d688ce 100644 --- a/persistence/test/DbTest.kt +++ b/persistence/test/DbTest.kt @@ -6,21 +6,28 @@ import org.flywaydb.core.Flyway import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.parallel.ResourceLock import javax.sql.DataSource +@ResourceLock("h2") abstract class DbTest { - abstract fun dataSourceConfig(): DataSourceConfig - - val beanContext = BeanContext - .build() + val beanContext = BeanContext.build() inline fun BeanContext.getBean(): T = getBean(T::class.java) @BeforeAll fun setComponent() { beanContext - .registerSingleton(dataSourceConfig()) + .registerSingleton( + DataSourceConfig( + jdbcUrl = "jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1", + username = "h2", + password = "", + maximumPoolSize = 2, + connectionTimeout = 3000 + ) + ) .start() } diff --git a/persistence/test/notes/BaseNoteRepositoryImplTest.kt b/persistence/test/NoteRepositoryImplTest.kt similarity index 81% rename from persistence/test/notes/BaseNoteRepositoryImplTest.kt rename to persistence/test/NoteRepositoryImplTest.kt index a4fdae3..b4eaf04 100644 --- a/persistence/test/notes/BaseNoteRepositoryImplTest.kt +++ b/persistence/test/NoteRepositoryImplTest.kt @@ -1,24 +1,29 @@ -package be.simplenotes.persistence.notes +package be.simplenotes.persistence -import be.simplenotes.persistence.DbTest -import be.simplenotes.persistence.converters.NoteConverter +import be.simplenotes.persistence.notes.* import be.simplenotes.persistence.repositories.NoteRepository import be.simplenotes.persistence.repositories.UserRepository import be.simplenotes.persistence.users.createFakeUser import be.simplenotes.types.ExportedNote +import be.simplenotes.types.PersistedNote 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.assertThatThrownBy -import org.junit.jupiter.api.* +import org.junit.jupiter.api.BeforeEach +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 -internal abstract class BaseNoteRepositoryImplTest : DbTest() { + +internal class NoteRepositoryImplTest : DbTest() { private lateinit var noteRepo: NoteRepository private lateinit var userRepo: UserRepository @@ -94,27 +99,6 @@ internal abstract class BaseNoteRepositoryImplTest : DbTest() { 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 fun `find all notes with tag`() { with(noteRepo) { @@ -145,12 +129,18 @@ internal abstract class BaseNoteRepositoryImplTest : DbTest() { fun `find an existing note`() { val fakeNote = noteRepo.insertFakeNote(user1) - val converter = beanContext.getBean(NoteConverter::class.java) - - val note = db.notes.find { it.title eq fakeNote.meta.title }!! + val note = db.notes.find { Notes.title eq fakeNote.title }!! .let { entity -> - val tags = db.tags.filter { it.noteUuid eq entity.uuid }.mapColumns { it.name } as List - converter.toPersistedNote(entity, tags) + val tags = db.tags.filter { be.simplenotes.persistence.Tags.noteUuid eq entity.uuid }.mapColumns { be.simplenotes.persistence.Tags.name } as List + PersistedNote( + 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) @@ -199,11 +189,11 @@ internal abstract class BaseNoteRepositoryImplTest : DbTest() { val notes1 = noteRepo.insertFakeNotes(user1, count = 3) val notes2 = noteRepo.insertFakeNotes(user2, count = 1) - val user1Tags = notes1.flatMap { it.meta.tags }.toSet() + val user1Tags = notes1.flatMap { it.tags }.toSet() assertThat(noteRepo.getTags(user1.id)) .containsExactlyInAnyOrderElementsOf(user1Tags) - val user2Tags = notes2.flatMap { it.meta.tags }.toSet() + val user2Tags = notes2.flatMap { it.tags }.toSet() assertThat(noteRepo.getTags(user2.id)) .containsExactlyInAnyOrderElementsOf(user2Tags) @@ -229,9 +219,7 @@ internal abstract class BaseNoteRepositoryImplTest : DbTest() { .isEqualTo(newNote1) val note2 = noteRepo.insertFakeNote(user1) - val newNote2 = fakeNote().let { - it.copy(meta = it.meta.copy(tags = tagGenerator().take(3).toList())) - } + val newNote2 = fakeNote().copy(tags = tagGenerator().take(3).toList()) assertThat(noteRepo.update(user1.id, note2.uuid, newNote2)) .isNotNull @@ -253,7 +241,7 @@ internal abstract class BaseNoteRepositoryImplTest : DbTest() { .isTrue val isDeleted = db.notes - .find { it.uuid eq note1.uuid } + .find { Notes.uuid eq note1.uuid } ?.deleted assertThat(isDeleted).`as`("Check that Notes.deleted is true").isTrue @@ -261,7 +249,7 @@ internal abstract class BaseNoteRepositoryImplTest : DbTest() { assertThat(noteRepo.restore(user1.id, note1.uuid)).isTrue val isDeleted2 = db.notes - .find { it.uuid eq note1.uuid } + .find { Notes.uuid eq note1.uuid } ?.deleted assertThat(isDeleted2).`as`("Check that Notes.deleted is false after restore()").isFalse @@ -302,8 +290,8 @@ internal abstract class BaseNoteRepositoryImplTest : DbTest() { val expected = notes.mapIndexed { i, n -> ExportedNote( - title = n.meta.title, - tags = n.meta.tags, + title = n.title, + tags = n.tags, markdown = n.markdown, html = n.html, updatedAt = n.updatedAt, diff --git a/persistence/test/users/BaseUserRepositoryImplTest.kt b/persistence/test/UserRepositoryImplTest.kt similarity index 85% rename from persistence/test/users/BaseUserRepositoryImplTest.kt rename to persistence/test/UserRepositoryImplTest.kt index 82ee254..8864472 100644 --- a/persistence/test/users/BaseUserRepositoryImplTest.kt +++ b/persistence/test/UserRepositoryImplTest.kt @@ -1,17 +1,17 @@ -package be.simplenotes.persistence.users +package be.simplenotes.persistence -import be.simplenotes.persistence.DbTest import be.simplenotes.persistence.repositories.UserRepository -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 be.simplenotes.persistence.users.fakeUser import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested 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 abstract class BaseUserRepositoryImplTest : DbTest() { +internal class UserRepositoryImplTest : DbTest() { private lateinit var userRepo: UserRepository private lateinit var db: Database @@ -30,7 +30,7 @@ internal abstract class BaseUserRepositoryImplTest : DbTest() { .extracting("username", "password") .contains(user.username, user.password) - assertThat(db.users.find { it.username eq user.username }).isNotNull + assertThat(db.users.find { Users.username eq user.username }).isNotNull assertThat(db.users.toList()).hasSize(1) assertThat(userRepo.create(user)).isNull() } diff --git a/persistence/test/converters/NoteConverterTest.kt b/persistence/test/converters/NoteConverterTest.kt deleted file mode 100644 index 3b3ec30..0000000 --- a/persistence/test/converters/NoteConverterTest.kt +++ /dev/null @@ -1,164 +0,0 @@ -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) - } - } -} diff --git a/persistence/test/converters/UserConverterTest.kt b/persistence/test/converters/UserConverterTest.kt deleted file mode 100644 index c8707d9..0000000 --- a/persistence/test/converters/UserConverterTest.kt +++ /dev/null @@ -1,48 +0,0 @@ -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") - } -} diff --git a/persistence/test/notes/H2NoteRepositoryImplTests.kt b/persistence/test/notes/H2NoteRepositoryImplTests.kt deleted file mode 100644 index 36b41bc..0000000 --- a/persistence/test/notes/H2NoteRepositoryImplTests.kt +++ /dev/null @@ -1,31 +0,0 @@ -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) - } -} diff --git a/persistence/test/users/UserRepositoryImplTests.kt b/persistence/test/users/UserRepositoryImplTests.kt deleted file mode 100644 index b9cb863..0000000 --- a/persistence/test/users/UserRepositoryImplTests.kt +++ /dev/null @@ -1,31 +0,0 @@ -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) - } -} diff --git a/persistence/testfixtures/notes/NotesFixtures.kt b/persistence/testfixtures/notes/NotesFixtures.kt index a82485b..9b3a6cd 100644 --- a/persistence/testfixtures/notes/NotesFixtures.kt +++ b/persistence/testfixtures/notes/NotesFixtures.kt @@ -12,10 +12,10 @@ fun fakeTitle() = faker.lorem().characters(3, 50)!! fun tagGenerator() = generateSequence { faker.lorem().word() } fun fakeTags() = tagGenerator().take(faker.random().nextInt(0, 3)).toList() fun fakeContent() = faker.lorem().paragraph(faker.random().nextInt(0, 3))!! -fun fakeNote() = Note(NoteMetadata(fakeTitle(), fakeTags()), fakeContent(), fakeContent()) +fun fakeNote() = Note(fakeTitle(), fakeTags(), fakeContent(), fakeContent()) fun PersistedNote.toPersistedMeta() = - PersistedNoteMetadata(meta.title, meta.tags, updatedAt, uuid) + PersistedNoteMetadata(title, tags, updatedAt, uuid) fun NoteRepository.insertFakeNote( userId: Int, @@ -23,7 +23,7 @@ fun NoteRepository.insertFakeNote( tags: List = emptyList(), md: String = "md", html: String = "html", -): PersistedNote = create(userId, Note(NoteMetadata(title, tags), md, html)) +): PersistedNote = create(userId, Note(title, tags, md, html)) fun NoteRepository.insertFakeNote( user: PersistedUser, diff --git a/persistence/testresources/logback.xml b/persistence/testresources/logback.xml index 74710a0..fb914cf 100644 --- a/persistence/testresources/logback.xml +++ b/persistence/testresources/logback.xml @@ -5,10 +5,10 @@ - + - - - + + + diff --git a/types/src/Note.kt b/types/src/Note.kt index 75936b1..eb5d989 100644 --- a/types/src/Note.kt +++ b/types/src/Note.kt @@ -1,7 +1,9 @@ +@file:UseContextualSerialization(LocalDateTime::class, UUID::class) + package be.simplenotes.types -import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable +import kotlinx.serialization.UseContextualSerialization import java.time.LocalDateTime import java.util.* @@ -15,24 +17,26 @@ data class NoteMetadata( data class PersistedNoteMetadata( val title: String, val tags: List, - @Contextual val updatedAt: LocalDateTime, - @Contextual val uuid: UUID, + val updatedAt: LocalDateTime, + val uuid: UUID, ) @Serializable data class Note( - val meta: NoteMetadata, + val title: String, + val tags: List, val markdown: String, val html: String, ) @Serializable data class PersistedNote( - val meta: NoteMetadata, + val uuid: UUID, + val title: String, + val tags: List, val markdown: String, val html: String, - @Contextual val updatedAt: LocalDateTime, - @Contextual val uuid: UUID, + val updatedAt: LocalDateTime, val public: Boolean, ) @@ -42,6 +46,6 @@ data class ExportedNote( val tags: List, val markdown: String, val html: String, - @Contextual val updatedAt: LocalDateTime, + val updatedAt: LocalDateTime, val trash: Boolean, )