Adds SQLDelight type converters.

This commit is contained in:
Stefan Schallerl 2025-02-06 11:30:25 +01:00
parent b4f2f51032
commit 319c58bcc2
8 changed files with 140 additions and 87 deletions

View file

@ -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)

View file

@ -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<Tag>,
@ -20,19 +36,11 @@ value class Tag(val value: String) {
// TODO proper validation
require(value.isNotBlank())
}
companion object {
fun parse(ser: String?): List<Tag> {
return ser?.let { if (it.isNotBlank()) it.split(",").map { Tag(it) } else emptyList() } ?: emptyList()
}
fun List<Tag>.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?,

View file

@ -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<Tag> {
return ser?.let { if (it.isNotBlank()) it.split(",").map { Tag(it) } else emptyList() } ?: emptyList()
}
fun List<Tag>.serialize() = if (this.isEmpty()) "" else this.joinToString(",") { it.value }
}

View file

@ -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
)

View file

@ -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(),

View file

@ -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()

View file

@ -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<LocalDateTime, String> {
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<State, Long> {
override fun decode(databaseValue: Long): State = State.fromCode(databaseValue.toInt())
override fun encode(value: State): Long = value.code.toLong()
}
val tagsAdapter = object : ColumnAdapter<List<Tag>, String> {
override fun decode(databaseValue: String): List<Tag> = TagAdapter.parse(databaseValue)
override fun encode(value: List<Tag>): String = value.serialize()
}
val extIdAdapter = object : ColumnAdapter<ExtId, String> {
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<FileRef> {
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<Document> {
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,

View file

@ -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<Tag> 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
);