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.LocationDao
import de.bollwerk.app.data.db.dao.MessageDao import de.bollwerk.app.data.db.dao.MessageDao
import de.bollwerk.app.data.db.dao.PendingSyncOpDao 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.dao.SettingsDao
import de.bollwerk.app.data.db.entity.CategoryEntity import de.bollwerk.app.data.db.entity.CategoryEntity
import de.bollwerk.app.data.db.entity.ItemEntity import de.bollwerk.app.data.db.entity.ItemEntity
import de.bollwerk.app.data.db.entity.LocationEntity import de.bollwerk.app.data.db.entity.LocationEntity
import de.bollwerk.app.data.db.entity.MessageEntity import de.bollwerk.app.data.db.entity.MessageEntity
import de.bollwerk.app.data.db.entity.PendingSyncOpEntity import de.bollwerk.app.data.db.entity.PendingSyncOpEntity
import de.bollwerk.app.data.db.entity.ResourceEntity
import de.bollwerk.app.data.db.entity.SettingsEntity import de.bollwerk.app.data.db.entity.SettingsEntity
@Database( @Database(
entities = [CategoryEntity::class, LocationEntity::class, ItemEntity::class, SettingsEntity::class, PendingSyncOpEntity::class, MessageEntity::class], entities = [CategoryEntity::class, LocationEntity::class, ItemEntity::class, SettingsEntity::class, PendingSyncOpEntity::class, MessageEntity::class, ResourceEntity::class],
version = 8, version = 9,
exportSchema = true, exportSchema = true,
autoMigrations = [ autoMigrations = [
AutoMigration(from = 5, to = 6), AutoMigration(from = 5, to = 6),
AutoMigration(from = 6, to = 7), AutoMigration(from = 6, to = 7),
AutoMigration(from = 7, to = 8) AutoMigration(from = 7, to = 8),
AutoMigration(from = 8, to = 9)
] ]
) )
@TypeConverters(LocalDateConverter::class) @TypeConverters(LocalDateConverter::class)
@ -35,4 +38,5 @@ internal abstract class BollwerkDatabase : RoomDatabase() {
abstract fun settingsDao(): SettingsDao abstract fun settingsDao(): SettingsDao
abstract fun pendingSyncOpDao(): PendingSyncOpDao abstract fun pendingSyncOpDao(): PendingSyncOpDao
abstract fun messageDao(): MessageDao 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.LocationDao
import de.bollwerk.app.data.db.dao.MessageDao import de.bollwerk.app.data.db.dao.MessageDao
import de.bollwerk.app.data.db.dao.PendingSyncOpDao 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.dao.SettingsDao
import de.bollwerk.app.data.export.DatabaseTransaction import de.bollwerk.app.data.export.DatabaseTransaction
import javax.inject.Singleton import javax.inject.Singleton
@ -67,6 +68,9 @@ internal object DatabaseModule {
@Provides @Provides
fun provideMessageDao(db: BollwerkDatabase): MessageDao = db.messageDao() fun provideMessageDao(db: BollwerkDatabase): MessageDao = db.messageDao()
@Provides
fun provideResourceDao(db: BollwerkDatabase): ResourceDao = db.resourceDao()
@Provides @Provides
@Singleton @Singleton
fun provideDatabaseTransaction(db: BollwerkDatabase): DatabaseTransaction = 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.ItemRepositoryImpl
import de.bollwerk.app.data.repository.LocationRepositoryImpl import de.bollwerk.app.data.repository.LocationRepositoryImpl
import de.bollwerk.app.data.repository.MessageRepositoryImpl 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.SettingsRepositoryImpl
import de.bollwerk.app.data.repository.UpdateRepositoryImpl import de.bollwerk.app.data.repository.UpdateRepositoryImpl
import de.bollwerk.app.domain.repository.CategoryRepository 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.ItemRepository
import de.bollwerk.app.domain.repository.LocationRepository import de.bollwerk.app.domain.repository.LocationRepository
import de.bollwerk.app.domain.repository.MessageRepository 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.SettingsRepository
import de.bollwerk.app.domain.repository.UpdateRepository import de.bollwerk.app.domain.repository.UpdateRepository
import de.bollwerk.app.domain.usecase.ApkInstaller import de.bollwerk.app.domain.usecase.ApkInstaller
@ -57,4 +59,8 @@ internal abstract class RepositoryModule {
@Binds @Binds
@Singleton @Singleton
abstract fun bindApkInstaller(impl: ApkInstallerImpl): ApkInstaller 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
}