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 import java.net.URLEncoder
class TemplateModifiers : Frame.Modifiers, Limbo.Modifiers, DocumentCreateForm.Modifiers, Overview.Modifiers, 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 { fun hashPrefix(arg: String): String {
return URLEncoder.encode(arg, Charsets.UTF_8) 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 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( data class Document(
val id: Long, val id: Long,
val extId: String, val extId: ExtId,
val title: String, val title: String,
val description: String, val description: String,
val tags: List<Tag>, val tags: List<Tag>,
@ -20,19 +36,11 @@ value class Tag(val value: String) {
// TODO proper validation // TODO proper validation
require(value.isNotBlank()) 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( data class FileRef(
val id: Long, val id: Long,
val extId: String, val extId: ExtId,
val accountId: Long, val accountId: Long,
val documentId: Long?, val documentId: Long?,
val filename: String, val filename: String,
@ -45,7 +53,7 @@ data class FileRef(
data class FileContent( data class FileContent(
val id: Long, val id: Long,
val extId: String, val extId: ExtId,
val filename: String, val filename: String,
val contentType: String?, val contentType: String?,
val contentExtracted: 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") 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) { fun formatHumanReadableSize(bytes: Long) = when (bytes) {
in 0L..<1024L -> "$bytes bytes" in 0L..<1024L -> "$bytes bytes"
in 1025..<1024 * 1_000 -> "${bytes / 1000} kb" 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" 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.ForbiddenResponse
import io.javalin.http.Header import io.javalin.http.Header
import net.h34t.filemure.* 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.repository.SqliteRepository
import net.h34t.filemure.tpl.* import net.h34t.filemure.tpl.*
import net.h34t.filemure.tpl.Document
import java.io.File import java.io.File
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@ -32,7 +31,7 @@ class DocumentController(val modifiers: TemplateModifiers, val repository: Sqlit
isTarget = true, isTarget = true,
content = Document( content = Document(
modifiers = modifiers, modifiers = modifiers,
extId = document.extId, extId = document.extId.value,
title = document.title, title = document.title,
referenceDate = dtf.format(document.referenceDate), referenceDate = dtf.format(document.referenceDate),
tags = { document.tags.map { TagsBlock(tag = it.value) }.asSequence() }, tags = { document.tags.map { TagsBlock(tag = it.value) }.asSequence() },
@ -43,7 +42,7 @@ class DocumentController(val modifiers: TemplateModifiers, val repository: Sqlit
files = { files = {
document.files.map { file -> document.files.map { file ->
FilesBlock( FilesBlock(
extId = file.extId, extId = file.extId.value,
filename = file.filename, filename = file.filename,
contentType = file.contentType ?: "?", contentType = file.contentType ?: "?",
size = formatHumanReadableSize(file.fileSize), size = formatHumanReadableSize(file.fileSize),
@ -60,7 +59,7 @@ class DocumentController(val modifiers: TemplateModifiers, val repository: Sqlit
fun createDocumentForm(ctx: Context) { fun createDocumentForm(ctx: Context) {
val session = ctx.requireSession() 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 limboFiles = repository.getFilesInLimbo(session.id)
val limboFileIds = limboFiles.map { it.extId } val limboFileIds = limboFiles.map { it.extId }
@ -96,7 +95,7 @@ class DocumentController(val modifiers: TemplateModifiers, val repository: Sqlit
files = { files = {
selectedFiles.map { file -> selectedFiles.map { file ->
FilesBlock( FilesBlock(
extId = file.extId, extId = file.extId.value,
filename = file.filename, filename = file.filename,
contentType = file.contentType ?: "?", contentType = file.contentType ?: "?",
size = formatHumanReadableSize(file.fileSize), size = formatHumanReadableSize(file.fileSize),
@ -174,7 +173,7 @@ class DocumentController(val modifiers: TemplateModifiers, val repository: Sqlit
files = { files = {
document.files.map { file -> document.files.map { file ->
FilesBlock( FilesBlock(
extId = file.extId, extId = file.extId.value,
filename = file.filename, filename = file.filename,
contentType = file.contentType ?: "?", contentType = file.contentType ?: "?",
size = formatHumanReadableSize(file.fileSize), size = formatHumanReadableSize(file.fileSize),
@ -207,7 +206,7 @@ class DocumentController(val modifiers: TemplateModifiers, val repository: Sqlit
id = document.id, id = document.id,
title = title ?: "", title = title ?: "",
referenceDate = LocalDateTime.parse(referenceDate ?: "", formDtf), referenceDate = LocalDateTime.parse(referenceDate ?: "", formDtf),
tags = Tag.parse(tags), tags = TagAdapter.parse(tags),
description = description description = description
) )

View file

@ -26,7 +26,7 @@ class LimboController(val modifiers: TemplateModifiers, val repository: SqliteRe
modifiers = modifiers, limboFileCount = files.size.toString(), file = { modifiers = modifiers, limboFileCount = files.size.toString(), file = {
files.map { f -> files.map { f ->
FileBlock( FileBlock(
extId = f.extId, extId = f.extId.value,
file = f.filename, file = f.filename,
type = f.contentType ?: "", type = f.contentType ?: "",
size = f.fileSize.toString(), size = f.fileSize.toString(),

View file

@ -12,7 +12,7 @@ import java.time.format.FormatStyle
class OverviewController(val modifiers: TemplateModifiers, val repository: SqliteRepository) { 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) { fun overview(ctx: Context) {
val session = ctx.requireSession() val session = ctx.requireSession()
@ -24,7 +24,7 @@ class OverviewController(val modifiers: TemplateModifiers, val repository: Sqlit
ctx.tempolin( ctx.tempolin(
Frame( Frame(
modifiers = modifiers, modifiers = modifiers,
title = "Filemure Overview", title = "Overview",
isTarget = true, isTarget = true,
content = Overview( content = Overview(
modifiers = modifiers, modifiers = modifiers,
@ -32,8 +32,8 @@ class OverviewController(val modifiers: TemplateModifiers, val repository: Sqlit
document = { document = {
documents.map { document -> documents.map { document ->
DocumentBlock( DocumentBlock(
extId = document.extId, extId = document.extId.value,
referenceDate = dtf.format(document.referenceDate), referenceDate = htmlDtf.format(document.referenceDate),
title = document.title.ifBlank { "untitled" }, title = document.title.ifBlank { "untitled" },
) )
}.asSequence() }.asSequence()

View file

@ -1,34 +1,78 @@
package net.h34t.filemure.repository package net.h34t.filemure.repository
import app.cash.sqldelight.ColumnAdapter
import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
import net.h34t.filemure.ExtId import net.h34t.filemure.*
import net.h34t.filemure.core.entity.* import net.h34t.filemure.TagAdapter.serialize
import net.h34t.filemure.core.entity.Tag.Companion.serialize
import net.h34t.filemure.db.Database import net.h34t.filemure.db.Database
import net.h34t.filemure.db.File_
import java.io.InputStream import java.io.InputStream
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
class SqliteRepository(url: String) { 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 private val database: Database
init { init {
val driver: SqlDriver = JdbcSqliteDriver("jdbc:sqlite:test.db") val driver: SqlDriver = JdbcSqliteDriver(url)
Database.Schema.create(driver)
database = Database(driver)
}
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) { fun addFileToLimbo(accountId: Long, filename: String, contentType: String?, size: Long, content: InputStream) {
database.databaseQueries.insertFileIntoLimbo( database.databaseQueries.insertFileIntoLimbo(
account_id = accountId, account_id = accountId,
ext_id = ExtId.generate().value, ext_id = ExtId.generate(),
filename = filename, filename = filename,
content_type = contentType, content_type = contentType,
file_size = size, file_size = size,
@ -37,12 +81,12 @@ class SqliteRepository(url: String) {
} }
fun getLimboFileCount(accountId: Long, state: State = State.ACTIVE): Long { 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() .executeAsOne()
} }
fun getFilesInLimbo(accountId: Long, state: State = State.ACTIVE): List<FileRef> { 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() .executeAsList()
.map { .map {
FileRef( FileRef(
@ -54,8 +98,8 @@ class SqliteRepository(url: String) {
fileSize = it.file_size, fileSize = it.file_size,
contentType = it.content_type, contentType = it.content_type,
contentExtracted = it.content_extracted, contentExtracted = it.content_extracted,
created = toLDT(it.created), created = it.created,
state = State.fromCode(it.state.toInt()) state = it.state
) )
} }
} }
@ -72,11 +116,11 @@ class SqliteRepository(url: String) {
database.databaseQueries.transaction { database.databaseQueries.transaction {
database.databaseQueries.addDocument( database.databaseQueries.addDocument(
account_id = accountId, account_id = accountId,
ext_id = extId.value, ext_id = extId,
title = title, title = title,
description = description, description = description,
tags = tags.serialize(), tags = tags,
reference_date = referenceDate.format(sqliteDtf) reference_date = referenceDate
) )
val documentId = database.databaseQueries.getLastInsertRowId().executeAsOne() val documentId = database.databaseQueries.getLastInsertRowId().executeAsOne()
@ -84,7 +128,7 @@ class SqliteRepository(url: String) {
database.databaseQueries.attachLimboFilesToDocument( database.databaseQueries.attachLimboFilesToDocument(
account_id = accountId, account_id = accountId,
document_id = documentId, 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> { 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() .executeAsList()
.map { .map {
Document( Document(
@ -100,10 +144,10 @@ class SqliteRepository(url: String) {
extId = it.ext_id, extId = it.ext_id,
title = it.title, title = it.title,
description = it.description, description = it.description,
tags = Tag.parse(it.tags), tags = it.tags,
created = toLDT(it.created), created = it.created,
referenceDate = toLDT(it.reference_date), referenceDate = it.reference_date,
state = State.fromCode(it.state.toInt()), state = it.state,
files = emptyList() files = emptyList()
) )
} }
@ -121,10 +165,10 @@ class SqliteRepository(url: String) {
database.databaseQueries.updateDocument( database.databaseQueries.updateDocument(
id = id, id = id,
title = title, title = title,
reference_date = referenceDate.format(sqliteDtf), reference_date = referenceDate,
tags = tags.serialize(), tags = tags,
description = description, description = description,
state = state.code.toLong(), state = state,
account_id = accountId, account_id = accountId,
) )
} }
@ -132,18 +176,18 @@ class SqliteRepository(url: String) {
fun getDocumentByExtId(accountId: Long, extId: ExtId, state: State): Document { fun getDocumentByExtId(accountId: Long, extId: ExtId, state: State): Document {
return database.databaseQueries.getDocumentByExtId( return database.databaseQueries.getDocumentByExtId(
account_id = accountId, account_id = accountId,
ext_id = extId.value, ext_id = extId,
state = state.code.toLong() state = state
).executeAsOne().let { d -> ).executeAsOne().let { d ->
Document( Document(
id = d.id, id = d.id,
extId = d.ext_id, extId = d.ext_id,
title = d.title, title = d.title,
description = d.description, description = d.description,
tags = Tag.parse(d.tags), tags = d.tags,
created = toLDT(d.created), created = d.created,
referenceDate = toLDT(d.reference_date), referenceDate = d.reference_date,
state = State.fromCode(d.state.toInt()), state = d.state,
files = database.databaseQueries.getFilesForDocument( files = database.databaseQueries.getFilesForDocument(
document_id = d.id, document_id = d.id,
account_id = accountId account_id = accountId
@ -157,8 +201,8 @@ class SqliteRepository(url: String) {
contentType = f.content_type, contentType = f.content_type,
contentExtracted = f.content_extracted, contentExtracted = f.content_extracted,
fileSize = f.file_size, fileSize = f.file_size,
created = toLDT(f.created), created = f.created,
state = State.fromCode(f.state.toInt()) state = f.state
) )
} }
) )
@ -169,7 +213,7 @@ class SqliteRepository(url: String) {
fun loadFile(accountId: Long, extId: ExtId): FileContent { fun loadFile(accountId: Long, extId: ExtId): FileContent {
return database return database
.databaseQueries .databaseQueries
.getFile(account_id = accountId, ext_id = extId.value) .getFile(account_id = accountId, ext_id = extId)
.executeAsOne().let { f -> .executeAsOne().let { f ->
FileContent( FileContent(
id = f.id, 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 -- account definition
CREATE TABLE account ( CREATE TABLE account (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 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, password TEXT NOT NULL,
created TEXT DEFAULT (CURRENT_TIMESTAMP) NOT NULL, created TEXT AS LocalDateTime DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
state INTEGER DEFAULT (1) NOT NULL state INTEGER AS State DEFAULT (1) NOT NULL
); );
CREATE INDEX account_state_IDX ON account (state); CREATE INDEX account_state_IDX ON account (state);
CREATE UNIQUE INDEX account_email_IDX ON account (email); CREATE UNIQUE INDEX account_email_IDX ON account (email);
CREATE UNIQUE INDEX account_extid_IDX ON account (ext_id);
-- document definition -- document definition
CREATE TABLE document ( CREATE TABLE document (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL, account_id INTEGER NOT NULL,
ext_id TEXT NOT NULL, ext_id TEXT AS ExtId NOT NULL,
title TEXT NOT NULL, title TEXT NOT NULL,
description TEXT NOT NULL, description TEXT NOT NULL,
tags TEXT NOT NULL, tags TEXT AS List<Tag> NOT NULL,
created TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, created TEXT AS LocalDateTime NOT NULL DEFAULT CURRENT_TIMESTAMP,
reference_date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, reference_date TEXT AS LocalDateTime NOT NULL DEFAULT CURRENT_TIMESTAMP,
state INTEGER NOT NULL DEFAULT (1), state INTEGER AS State NOT NULL DEFAULT (1),
CONSTRAINT document_account_FK FOREIGN KEY (account_id) REFERENCES account(id) ON DELETE CASCADE 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, id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL, account_id INTEGER NOT NULL,
document_id INTEGER DEFAULT NULL, document_id INTEGER DEFAULT NULL,
ext_id TEXT NOT NULL, ext_id TEXT AS ExtId NOT NULL,
filename TEXT NOT NULL, filename TEXT NOT NULL,
file_size INTEGER NOT NULL, file_size INTEGER NOT NULL,
content BLOB NOT NULL, content BLOB NOT NULL,
content_type TEXT, content_type TEXT,
content_extracted TEXT, content_extracted TEXT,
created TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, created TEXT AS LocalDateTime NOT NULL DEFAULT CURRENT_TIMESTAMP,
state INTEGER NOT NULL DEFAULT (1), state INTEGER AS State NOT NULL DEFAULT (1),
CONSTRAINT file_account_FK FOREIGN KEY (account_id) REFERENCES account(id) ON DELETE CASCADE, 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 CONSTRAINT file_document_FK FOREIGN KEY (document_id) REFERENCES document(id) ON DELETE CASCADE
); );