feat(app): add ResourceEntity, Dao, Repository + DB migration 8→9

Closes #121
This commit is contained in:
Jens Reinemann 2026-05-18 22:10:51 +02:00
parent 6fc37ee203
commit 542fbb0941
7 changed files with 162 additions and 3 deletions

View file

@ -9,22 +9,25 @@ import de.bollwerk.app.data.db.dao.ItemDao
import de.bollwerk.app.data.db.dao.LocationDao
import de.bollwerk.app.data.db.dao.MessageDao
import de.bollwerk.app.data.db.dao.PendingSyncOpDao
import de.bollwerk.app.data.db.dao.ResourceDao
import de.bollwerk.app.data.db.dao.SettingsDao
import de.bollwerk.app.data.db.entity.CategoryEntity
import de.bollwerk.app.data.db.entity.ItemEntity
import de.bollwerk.app.data.db.entity.LocationEntity
import de.bollwerk.app.data.db.entity.MessageEntity
import de.bollwerk.app.data.db.entity.PendingSyncOpEntity
import de.bollwerk.app.data.db.entity.ResourceEntity
import de.bollwerk.app.data.db.entity.SettingsEntity
@Database(
entities = [CategoryEntity::class, LocationEntity::class, ItemEntity::class, SettingsEntity::class, PendingSyncOpEntity::class, MessageEntity::class],
version = 8,
entities = [CategoryEntity::class, LocationEntity::class, ItemEntity::class, SettingsEntity::class, PendingSyncOpEntity::class, MessageEntity::class, ResourceEntity::class],
version = 9,
exportSchema = true,
autoMigrations = [
AutoMigration(from = 5, to = 6),
AutoMigration(from = 6, to = 7),
AutoMigration(from = 7, to = 8)
AutoMigration(from = 7, to = 8),
AutoMigration(from = 8, to = 9)
]
)
@TypeConverters(LocalDateConverter::class)
@ -35,4 +38,5 @@ internal abstract class BollwerkDatabase : RoomDatabase() {
abstract fun settingsDao(): SettingsDao
abstract fun pendingSyncOpDao(): PendingSyncOpDao
abstract fun messageDao(): MessageDao
abstract fun resourceDao(): ResourceDao
}

View file

@ -0,0 +1,24 @@
package de.bollwerk.app.data.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import de.bollwerk.app.data.db.entity.ResourceEntity
import kotlinx.coroutines.flow.Flow
@Dao
internal interface ResourceDao {
@Query("SELECT * FROM resources")
fun getAll(): Flow<List<ResourceEntity>>
@Query("SELECT * FROM resources WHERE guid = :guid")
suspend fun getByGuid(guid: String): ResourceEntity?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(resources: List<ResourceEntity>)
@Query("DELETE FROM resources")
suspend fun deleteAll()
}

View file

@ -0,0 +1,23 @@
package de.bollwerk.app.data.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "resources")
internal data class ResourceEntity(
@PrimaryKey val guid: String,
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "description") val description: String,
@ColumnInfo(name = "tags") val tags: String, // JSON array string
@ColumnInfo(name = "file_format") val fileFormat: String,
@ColumnInfo(name = "mime_type") val mimeType: String,
@ColumnInfo(name = "file_size") val fileSize: Long,
@ColumnInfo(name = "release_date") val releaseDate: String?,
@ColumnInfo(name = "created_at") val createdAt: Long,
@ColumnInfo(name = "updated_at") val updatedAt: Long,
@ColumnInfo(name = "author") val author: String?,
@ColumnInfo(name = "language") val language: String?,
@ColumnInfo(name = "edition") val edition: String?,
@ColumnInfo(name = "download_url") val downloadUrl: String
)

View file

@ -0,0 +1,88 @@
package de.bollwerk.app.data.repository
import de.bollwerk.app.data.db.dao.ResourceDao
import de.bollwerk.app.data.db.entity.ResourceEntity
import de.bollwerk.app.domain.model.SettingsKey.StringKey
import de.bollwerk.app.domain.repository.ResourceRepository
import de.bollwerk.app.domain.repository.SettingsRepository
import de.bollwerk.shared.model.ResourceDto
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.statement.bodyAsBytes
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json
import javax.inject.Inject
internal class ResourceRepositoryImpl @Inject constructor(
private val dao: ResourceDao,
private val httpClient: HttpClient,
private val settingsRepository: SettingsRepository
) : ResourceRepository {
override fun getAll(): Flow<List<ResourceDto>> =
dao.getAll().map { entities -> entities.map { it.toDto() } }
override suspend fun refreshFromServer() = withContext(Dispatchers.IO) {
val serverUrl = settingsRepository.getString(StringKey.ServerUrl)
val token = settingsRepository.getString(StringKey.AuthAccessToken)
if (serverUrl.isBlank() || token.isBlank()) return@withContext
val response = httpClient.get("$serverUrl/api/resources") {
header("Authorization", "Bearer $token")
}
val resources: List<ResourceDto> = response.body()
dao.deleteAll()
dao.insertAll(resources.map { it.toEntity() })
}
override suspend fun downloadResource(guid: String): ByteArray = withContext(Dispatchers.IO) {
val serverUrl = settingsRepository.getString(StringKey.ServerUrl)
val token = settingsRepository.getString(StringKey.AuthAccessToken)
val response = httpClient.get("$serverUrl/api/resources/$guid/download") {
header("Authorization", "Bearer $token")
}
response.bodyAsBytes()
}
private fun ResourceEntity.toDto() = ResourceDto(
guid = guid,
title = title,
description = description,
tags = try { Json.decodeFromString<List<String>>(tags) } catch (_: Exception) { emptyList() },
fileFormat = fileFormat,
mimeType = mimeType,
fileSize = fileSize,
releaseDate = releaseDate,
createdAt = createdAt,
updatedAt = updatedAt,
author = author,
language = language,
edition = edition,
downloadUrl = downloadUrl
)
private fun ResourceDto.toEntity() = ResourceEntity(
guid = guid,
title = title,
description = description,
tags = Json.encodeToString(ListSerializer(String.serializer()), tags),
fileFormat = fileFormat,
mimeType = mimeType,
fileSize = fileSize,
releaseDate = releaseDate,
createdAt = createdAt,
updatedAt = updatedAt,
author = author,
language = language,
edition = edition,
downloadUrl = downloadUrl
)
}

View file

@ -16,6 +16,7 @@ import de.bollwerk.app.data.db.dao.ItemDao
import de.bollwerk.app.data.db.dao.LocationDao
import de.bollwerk.app.data.db.dao.MessageDao
import de.bollwerk.app.data.db.dao.PendingSyncOpDao
import de.bollwerk.app.data.db.dao.ResourceDao
import de.bollwerk.app.data.db.dao.SettingsDao
import de.bollwerk.app.data.export.DatabaseTransaction
import javax.inject.Singleton
@ -67,6 +68,9 @@ internal object DatabaseModule {
@Provides
fun provideMessageDao(db: BollwerkDatabase): MessageDao = db.messageDao()
@Provides
fun provideResourceDao(db: BollwerkDatabase): ResourceDao = db.resourceDao()
@Provides
@Singleton
fun provideDatabaseTransaction(db: BollwerkDatabase): DatabaseTransaction =

View file

@ -10,6 +10,7 @@ import de.bollwerk.app.data.repository.CategoryRepositoryImpl
import de.bollwerk.app.data.repository.ItemRepositoryImpl
import de.bollwerk.app.data.repository.LocationRepositoryImpl
import de.bollwerk.app.data.repository.MessageRepositoryImpl
import de.bollwerk.app.data.repository.ResourceRepositoryImpl
import de.bollwerk.app.data.repository.SettingsRepositoryImpl
import de.bollwerk.app.data.repository.UpdateRepositoryImpl
import de.bollwerk.app.domain.repository.CategoryRepository
@ -17,6 +18,7 @@ import de.bollwerk.app.domain.repository.ImportExportRepository
import de.bollwerk.app.domain.repository.ItemRepository
import de.bollwerk.app.domain.repository.LocationRepository
import de.bollwerk.app.domain.repository.MessageRepository
import de.bollwerk.app.domain.repository.ResourceRepository
import de.bollwerk.app.domain.repository.SettingsRepository
import de.bollwerk.app.domain.repository.UpdateRepository
import de.bollwerk.app.domain.usecase.ApkInstaller
@ -57,4 +59,8 @@ internal abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindApkInstaller(impl: ApkInstallerImpl): ApkInstaller
@Binds
@Singleton
abstract fun bindResourceRepository(impl: ResourceRepositoryImpl): ResourceRepository
}

View file

@ -0,0 +1,10 @@
package de.bollwerk.app.domain.repository
import de.bollwerk.shared.model.ResourceDto
import kotlinx.coroutines.flow.Flow
internal interface ResourceRepository {
fun getAll(): Flow<List<ResourceDto>>
suspend fun refreshFromServer()
suspend fun downloadResource(guid: String): ByteArray
}