From 319c58bcc2a1d97da71b431496bce3cb2898bdee Mon Sep 17 00:00:00 2001 From: Stefan Schallerl Date: Thu, 6 Feb 2025 11:30:25 +0100 Subject: [PATCH] Adds SQLDelight type converters. --- .../net/h34t/filemure/TemplateModifiers.kt | 3 +- .../main/kotlin/net/h34t/filemure}/Types.kt | 32 +++-- app/src/main/kotlin/net/h34t/filemure/Util.kt | 21 ++-- .../filemure/controller/DocumentController.kt | 15 ++- .../filemure/controller/LimboController.kt | 2 +- .../filemure/controller/OverviewController.kt | 8 +- .../filemure/repository/SqliteRepository.kt | 116 ++++++++++++------ .../net/h34t/filemure/db/Database.sq | 30 +++-- 8 files changed, 140 insertions(+), 87 deletions(-) rename {core/src/main/kotlin/net/h34t/filemure/core/entity => app/src/main/kotlin/net/h34t/filemure}/Types.kt (82%) diff --git a/app/src/main/kotlin/net/h34t/filemure/TemplateModifiers.kt b/app/src/main/kotlin/net/h34t/filemure/TemplateModifiers.kt index 3ac608c..837980a 100644 --- a/app/src/main/kotlin/net/h34t/filemure/TemplateModifiers.kt +++ b/app/src/main/kotlin/net/h34t/filemure/TemplateModifiers.kt @@ -5,7 +5,8 @@ import org.apache.commons.text.StringEscapeUtils import java.net.URLEncoder class TemplateModifiers : Frame.Modifiers, Limbo.Modifiers, DocumentCreateForm.Modifiers, Overview.Modifiers, - Document.Modifiers, FilePreview.Modifiers, DocumentEditForm.Modifiers, FileList.Modifiers { + FilePreview.Modifiers, DocumentEditForm.Modifiers, FileList.Modifiers, + net.h34t.filemure.tpl.Document.Modifiers { fun hashPrefix(arg: String): String { return URLEncoder.encode(arg, Charsets.UTF_8) diff --git a/core/src/main/kotlin/net/h34t/filemure/core/entity/Types.kt b/app/src/main/kotlin/net/h34t/filemure/Types.kt similarity index 82% rename from core/src/main/kotlin/net/h34t/filemure/core/entity/Types.kt rename to app/src/main/kotlin/net/h34t/filemure/Types.kt index 22c36b5..cb38101 100644 --- a/core/src/main/kotlin/net/h34t/filemure/core/entity/Types.kt +++ b/app/src/main/kotlin/net/h34t/filemure/Types.kt @@ -1,10 +1,26 @@ -package net.h34t.filemure.core.entity +package net.h34t.filemure import java.time.LocalDateTime + +@JvmInline +value class ExtId(val value: String) { + + companion object { + private val chars = ('a'..'z') + ('A'..'Z') + ('0'..'9') + + fun generate(): ExtId { + return ExtId((0..8).map { chars.random() }.joinToString("")) + } + } + + override fun toString() = value + +} + data class Document( val id: Long, - val extId: String, + val extId: ExtId, val title: String, val description: String, val tags: List, @@ -20,19 +36,11 @@ value class Tag(val value: String) { // TODO proper validation require(value.isNotBlank()) } - - companion object { - fun parse(ser: String?): List { - return ser?.let { if (it.isNotBlank()) it.split(",").map { Tag(it) } else emptyList() } ?: emptyList() - } - - fun List.serialize() = if (this.isEmpty()) "" else this.joinToString(",") { it.value } - } } data class FileRef( val id: Long, - val extId: String, + val extId: ExtId, val accountId: Long, val documentId: Long?, val filename: String, @@ -45,7 +53,7 @@ data class FileRef( data class FileContent( val id: Long, - val extId: String, + val extId: ExtId, val filename: String, val contentType: String?, val contentExtracted: String?, diff --git a/app/src/main/kotlin/net/h34t/filemure/Util.kt b/app/src/main/kotlin/net/h34t/filemure/Util.kt index 0d5c10e..2659289 100644 --- a/app/src/main/kotlin/net/h34t/filemure/Util.kt +++ b/app/src/main/kotlin/net/h34t/filemure/Util.kt @@ -36,20 +36,6 @@ fun Context.setSession(session: Session?) = this.sessionAttribute("session", ses fun Context.requireSession(): Session = this.getSession() ?: throw UnauthorizedResponse("Not logged in") -private val chars = ('a'..'z') + ('A'..'Z') + ('0'..'9') - - -@JvmInline -value class ExtId(val value: String) { - override fun toString() = value - - companion object { - fun generate(): ExtId { - return ExtId((0..8).map { chars.random() }.joinToString("")) - } - } -} - fun formatHumanReadableSize(bytes: Long) = when (bytes) { in 0L..<1024L -> "$bytes bytes" in 1025..<1024 * 1_000 -> "${bytes / 1000} kb" @@ -57,3 +43,10 @@ fun formatHumanReadableSize(bytes: Long) = when (bytes) { else -> "${bytes / 1_000_000_000} gb" } +object TagAdapter { + fun parse(ser: String?): List { + return ser?.let { if (it.isNotBlank()) it.split(",").map { Tag(it) } else emptyList() } ?: emptyList() + } + + fun List.serialize() = if (this.isEmpty()) "" else this.joinToString(",") { it.value } +} \ No newline at end of file diff --git a/app/src/main/kotlin/net/h34t/filemure/controller/DocumentController.kt b/app/src/main/kotlin/net/h34t/filemure/controller/DocumentController.kt index a27c05b..49a9191 100644 --- a/app/src/main/kotlin/net/h34t/filemure/controller/DocumentController.kt +++ b/app/src/main/kotlin/net/h34t/filemure/controller/DocumentController.kt @@ -5,10 +5,9 @@ import io.javalin.http.Context import io.javalin.http.ForbiddenResponse import io.javalin.http.Header import net.h34t.filemure.* -import net.h34t.filemure.core.entity.State -import net.h34t.filemure.core.entity.Tag import net.h34t.filemure.repository.SqliteRepository import net.h34t.filemure.tpl.* +import net.h34t.filemure.tpl.Document import java.io.File import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -32,7 +31,7 @@ class DocumentController(val modifiers: TemplateModifiers, val repository: Sqlit isTarget = true, content = Document( modifiers = modifiers, - extId = document.extId, + extId = document.extId.value, title = document.title, referenceDate = dtf.format(document.referenceDate), tags = { document.tags.map { TagsBlock(tag = it.value) }.asSequence() }, @@ -43,7 +42,7 @@ class DocumentController(val modifiers: TemplateModifiers, val repository: Sqlit files = { document.files.map { file -> FilesBlock( - extId = file.extId, + extId = file.extId.value, filename = file.filename, contentType = file.contentType ?: "?", size = formatHumanReadableSize(file.fileSize), @@ -60,7 +59,7 @@ class DocumentController(val modifiers: TemplateModifiers, val repository: Sqlit fun createDocumentForm(ctx: Context) { val session = ctx.requireSession() - val fileIds = ctx.queryParams("file_id") + val fileIds = ctx.queryParams("file_id").map { ExtId(it) } val limboFiles = repository.getFilesInLimbo(session.id) val limboFileIds = limboFiles.map { it.extId } @@ -96,7 +95,7 @@ class DocumentController(val modifiers: TemplateModifiers, val repository: Sqlit files = { selectedFiles.map { file -> FilesBlock( - extId = file.extId, + extId = file.extId.value, filename = file.filename, contentType = file.contentType ?: "?", size = formatHumanReadableSize(file.fileSize), @@ -174,7 +173,7 @@ class DocumentController(val modifiers: TemplateModifiers, val repository: Sqlit files = { document.files.map { file -> FilesBlock( - extId = file.extId, + extId = file.extId.value, filename = file.filename, contentType = file.contentType ?: "?", size = formatHumanReadableSize(file.fileSize), @@ -207,7 +206,7 @@ class DocumentController(val modifiers: TemplateModifiers, val repository: Sqlit id = document.id, title = title ?: "", referenceDate = LocalDateTime.parse(referenceDate ?: "", formDtf), - tags = Tag.parse(tags), + tags = TagAdapter.parse(tags), description = description ) diff --git a/app/src/main/kotlin/net/h34t/filemure/controller/LimboController.kt b/app/src/main/kotlin/net/h34t/filemure/controller/LimboController.kt index 5ede23c..b835886 100644 --- a/app/src/main/kotlin/net/h34t/filemure/controller/LimboController.kt +++ b/app/src/main/kotlin/net/h34t/filemure/controller/LimboController.kt @@ -26,7 +26,7 @@ class LimboController(val modifiers: TemplateModifiers, val repository: SqliteRe modifiers = modifiers, limboFileCount = files.size.toString(), file = { files.map { f -> FileBlock( - extId = f.extId, + extId = f.extId.value, file = f.filename, type = f.contentType ?: "", size = f.fileSize.toString(), diff --git a/app/src/main/kotlin/net/h34t/filemure/controller/OverviewController.kt b/app/src/main/kotlin/net/h34t/filemure/controller/OverviewController.kt index 076ebc7..3bb4c3f 100644 --- a/app/src/main/kotlin/net/h34t/filemure/controller/OverviewController.kt +++ b/app/src/main/kotlin/net/h34t/filemure/controller/OverviewController.kt @@ -12,7 +12,7 @@ import java.time.format.FormatStyle class OverviewController(val modifiers: TemplateModifiers, val repository: SqliteRepository) { - val dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT, FormatStyle.SHORT) + private val htmlDtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT, FormatStyle.SHORT) fun overview(ctx: Context) { val session = ctx.requireSession() @@ -24,7 +24,7 @@ class OverviewController(val modifiers: TemplateModifiers, val repository: Sqlit ctx.tempolin( Frame( modifiers = modifiers, - title = "Filemure Overview", + title = "Overview", isTarget = true, content = Overview( modifiers = modifiers, @@ -32,8 +32,8 @@ class OverviewController(val modifiers: TemplateModifiers, val repository: Sqlit document = { documents.map { document -> DocumentBlock( - extId = document.extId, - referenceDate = dtf.format(document.referenceDate), + extId = document.extId.value, + referenceDate = htmlDtf.format(document.referenceDate), title = document.title.ifBlank { "untitled" }, ) }.asSequence() diff --git a/app/src/main/kotlin/net/h34t/filemure/repository/SqliteRepository.kt b/app/src/main/kotlin/net/h34t/filemure/repository/SqliteRepository.kt index 8699817..ef2277a 100644 --- a/app/src/main/kotlin/net/h34t/filemure/repository/SqliteRepository.kt +++ b/app/src/main/kotlin/net/h34t/filemure/repository/SqliteRepository.kt @@ -1,34 +1,78 @@ package net.h34t.filemure.repository +import app.cash.sqldelight.ColumnAdapter import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver -import net.h34t.filemure.ExtId -import net.h34t.filemure.core.entity.* -import net.h34t.filemure.core.entity.Tag.Companion.serialize +import net.h34t.filemure.* +import net.h34t.filemure.TagAdapter.serialize import net.h34t.filemure.db.Database +import net.h34t.filemure.db.File_ import java.io.InputStream import java.time.LocalDateTime import java.time.format.DateTimeFormatter class SqliteRepository(url: String) { - private val sqliteDtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + private companion object Adapters { + private val sqliteDtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + + val localDateTimeAdapter = object : ColumnAdapter { + override fun decode(databaseValue: String): LocalDateTime { + return LocalDateTime.parse(databaseValue, sqliteDtf) + } + + override fun encode(value: LocalDateTime): String { + return value.format(sqliteDtf) + } + } + + val stateAdapter = object : ColumnAdapter { + override fun decode(databaseValue: Long): State = State.fromCode(databaseValue.toInt()) + override fun encode(value: State): Long = value.code.toLong() + } + + val tagsAdapter = object : ColumnAdapter, String> { + override fun decode(databaseValue: String): List = TagAdapter.parse(databaseValue) + override fun encode(value: List): String = value.serialize() + } + + val extIdAdapter = object : ColumnAdapter { + override fun decode(databaseValue: String): ExtId = ExtId(databaseValue) + override fun encode(value: ExtId): String = value.value + } + } - // private val connection: Connection = DriverManager.getConnection(url) private val database: Database init { - val driver: SqlDriver = JdbcSqliteDriver("jdbc:sqlite:test.db") - Database.Schema.create(driver) - database = Database(driver) - } + val driver: SqlDriver = JdbcSqliteDriver(url) - private fun toLDT(value: String) = LocalDateTime.parse(value, sqliteDtf) + try { + Database.Schema.create(driver) + } catch (_: Exception) { + } + + database = Database( + driver = driver, + documentAdapter = net.h34t.filemure.db.Document.Adapter( + tagsAdapter = tagsAdapter, + createdAdapter = localDateTimeAdapter, + reference_dateAdapter = localDateTimeAdapter, + stateAdapter = stateAdapter, + ext_idAdapter = extIdAdapter + ), + file_Adapter = File_.Adapter( + createdAdapter = localDateTimeAdapter, + stateAdapter = stateAdapter, + ext_idAdapter = extIdAdapter + ) + ) + } fun addFileToLimbo(accountId: Long, filename: String, contentType: String?, size: Long, content: InputStream) { database.databaseQueries.insertFileIntoLimbo( account_id = accountId, - ext_id = ExtId.generate().value, + ext_id = ExtId.generate(), filename = filename, content_type = contentType, file_size = size, @@ -37,12 +81,12 @@ class SqliteRepository(url: String) { } fun getLimboFileCount(accountId: Long, state: State = State.ACTIVE): Long { - return database.databaseQueries.getLimboFileCount(account_id = accountId, state = state.code.toLong()) + return database.databaseQueries.getLimboFileCount(account_id = accountId, state = state) .executeAsOne() } fun getFilesInLimbo(accountId: Long, state: State = State.ACTIVE): List { - return database.databaseQueries.getFilesInLimbo(account_id = accountId, state = state.code.toLong()) + return database.databaseQueries.getFilesInLimbo(account_id = accountId, state = state) .executeAsList() .map { FileRef( @@ -54,8 +98,8 @@ class SqliteRepository(url: String) { fileSize = it.file_size, contentType = it.content_type, contentExtracted = it.content_extracted, - created = toLDT(it.created), - state = State.fromCode(it.state.toInt()) + created = it.created, + state = it.state ) } } @@ -72,11 +116,11 @@ class SqliteRepository(url: String) { database.databaseQueries.transaction { database.databaseQueries.addDocument( account_id = accountId, - ext_id = extId.value, + ext_id = extId, title = title, description = description, - tags = tags.serialize(), - reference_date = referenceDate.format(sqliteDtf) + tags = tags, + reference_date = referenceDate ) val documentId = database.databaseQueries.getLastInsertRowId().executeAsOne() @@ -84,7 +128,7 @@ class SqliteRepository(url: String) { database.databaseQueries.attachLimboFilesToDocument( account_id = accountId, document_id = documentId, - ext_id = fileExtIds.map { it.value } + ext_id = fileExtIds ) } @@ -92,7 +136,7 @@ class SqliteRepository(url: String) { } fun getDocuments(accountId: Long, state: State = State.ACTIVE): List { - return database.databaseQueries.getDocuments(account_id = accountId, state = state.code.toLong()) + return database.databaseQueries.getDocuments(account_id = accountId, state = state) .executeAsList() .map { Document( @@ -100,10 +144,10 @@ class SqliteRepository(url: String) { extId = it.ext_id, title = it.title, description = it.description, - tags = Tag.parse(it.tags), - created = toLDT(it.created), - referenceDate = toLDT(it.reference_date), - state = State.fromCode(it.state.toInt()), + tags = it.tags, + created = it.created, + referenceDate = it.reference_date, + state = it.state, files = emptyList() ) } @@ -121,10 +165,10 @@ class SqliteRepository(url: String) { database.databaseQueries.updateDocument( id = id, title = title, - reference_date = referenceDate.format(sqliteDtf), - tags = tags.serialize(), + reference_date = referenceDate, + tags = tags, description = description, - state = state.code.toLong(), + state = state, account_id = accountId, ) } @@ -132,18 +176,18 @@ class SqliteRepository(url: String) { fun getDocumentByExtId(accountId: Long, extId: ExtId, state: State): Document { return database.databaseQueries.getDocumentByExtId( account_id = accountId, - ext_id = extId.value, - state = state.code.toLong() + ext_id = extId, + state = state ).executeAsOne().let { d -> Document( id = d.id, extId = d.ext_id, title = d.title, description = d.description, - tags = Tag.parse(d.tags), - created = toLDT(d.created), - referenceDate = toLDT(d.reference_date), - state = State.fromCode(d.state.toInt()), + tags = d.tags, + created = d.created, + referenceDate = d.reference_date, + state = d.state, files = database.databaseQueries.getFilesForDocument( document_id = d.id, account_id = accountId @@ -157,8 +201,8 @@ class SqliteRepository(url: String) { contentType = f.content_type, contentExtracted = f.content_extracted, fileSize = f.file_size, - created = toLDT(f.created), - state = State.fromCode(f.state.toInt()) + created = f.created, + state = f.state ) } ) @@ -169,7 +213,7 @@ class SqliteRepository(url: String) { fun loadFile(accountId: Long, extId: ExtId): FileContent { return database .databaseQueries - .getFile(account_id = accountId, ext_id = extId.value) + .getFile(account_id = accountId, ext_id = extId) .executeAsOne().let { f -> FileContent( id = f.id, diff --git a/app/src/main/sqldelight/net/h34t/filemure/db/Database.sq b/app/src/main/sqldelight/net/h34t/filemure/db/Database.sq index 5c626f5..ca70713 100644 --- a/app/src/main/sqldelight/net/h34t/filemure/db/Database.sq +++ b/app/src/main/sqldelight/net/h34t/filemure/db/Database.sq @@ -1,28 +1,36 @@ +import java.time.LocalDateTime; +import kotlin.collections.List; +import net.h34t.filemure.ExtId; +import net.h34t.filemure.State; +import net.h34t.filemure.Tag; + -- account definition CREATE TABLE account ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - email TEXT NOT NULL, + ext_id TEXT AS ExtId NOT NULL, + email TEXT NOT NULL, password TEXT NOT NULL, - created TEXT DEFAULT (CURRENT_TIMESTAMP) NOT NULL, - state INTEGER DEFAULT (1) NOT NULL + created TEXT AS LocalDateTime DEFAULT (CURRENT_TIMESTAMP) NOT NULL, + state INTEGER AS State DEFAULT (1) NOT NULL ); CREATE INDEX account_state_IDX ON account (state); CREATE UNIQUE INDEX account_email_IDX ON account (email); +CREATE UNIQUE INDEX account_extid_IDX ON account (ext_id); -- document definition CREATE TABLE document ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, account_id INTEGER NOT NULL, - ext_id TEXT NOT NULL, + ext_id TEXT AS ExtId NOT NULL, title TEXT NOT NULL, description TEXT NOT NULL, - tags TEXT NOT NULL, - created TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - reference_date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - state INTEGER NOT NULL DEFAULT (1), + tags TEXT AS List NOT NULL, + created TEXT AS LocalDateTime NOT NULL DEFAULT CURRENT_TIMESTAMP, + reference_date TEXT AS LocalDateTime NOT NULL DEFAULT CURRENT_TIMESTAMP, + state INTEGER AS State NOT NULL DEFAULT (1), CONSTRAINT document_account_FK FOREIGN KEY (account_id) REFERENCES account(id) ON DELETE CASCADE ); @@ -36,14 +44,14 @@ CREATE TABLE file ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, account_id INTEGER NOT NULL, document_id INTEGER DEFAULT NULL, - ext_id TEXT NOT NULL, + ext_id TEXT AS ExtId NOT NULL, filename TEXT NOT NULL, file_size INTEGER NOT NULL, content BLOB NOT NULL, content_type TEXT, content_extracted TEXT, - created TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - state INTEGER NOT NULL DEFAULT (1), + created TEXT AS LocalDateTime NOT NULL DEFAULT CURRENT_TIMESTAMP, + state INTEGER AS State NOT NULL DEFAULT (1), CONSTRAINT file_account_FK FOREIGN KEY (account_id) REFERENCES account(id) ON DELETE CASCADE, CONSTRAINT file_document_FK FOREIGN KEY (document_id) REFERENCES document(id) ON DELETE CASCADE );