feat(export): add ImportExportRepository with JSON export/import
domain/repository/ImportExportRepository.kt: new interface with
exportToJson() and importFromJson(json) suspend functions.
data/export/ExportData.kt: serializable data class bundling all
entity lists for JSON serialization via kotlinx.serialization.
data/export/ImportExportRepositoryImpl.kt: implementation using
kotlinx.serialization + Dispatchers.IO; exportToJson fetches all
DAOs and serializes, importFromJson deserializes and upserts back.
data/db/dao/{Category,Item,Location,Settings}Dao.kt: added @Upsert
upsertAll() suspend function to each DAO to support bulk import.
di/RepositoryModule.kt: bound ImportExportRepositoryImpl to
ImportExportRepository via Hilt @Binds.
test/.../FakeXxxDao.kt: upsertAll() implemented in all four fake
DAOs for unit test coverage.
This commit is contained in:
parent
4aba9f24a4
commit
388532c946
12 changed files with 182 additions and 0 deletions
|
|
@ -5,6 +5,7 @@ import androidx.room.Delete
|
|||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import androidx.room.Upsert
|
||||
import de.krisenvorrat.app.data.db.entity.CategoryEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
|
@ -25,4 +26,7 @@ internal interface CategoryDao {
|
|||
|
||||
@Query("SELECT * FROM categories WHERE id = :id")
|
||||
suspend fun getById(id: Int): CategoryEntity?
|
||||
|
||||
@Upsert
|
||||
suspend fun upsertAll(categories: List<CategoryEntity>)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.room.Delete
|
|||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import androidx.room.Upsert
|
||||
import de.krisenvorrat.app.data.db.entity.ItemEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.time.LocalDate
|
||||
|
|
@ -35,4 +36,7 @@ internal interface ItemDao {
|
|||
|
||||
@Query("SELECT * FROM items WHERE expiry_date IS NOT NULL AND expiry_date <= :cutoff")
|
||||
fun getExpiringSoonByCutoff(cutoff: LocalDate): Flow<List<ItemEntity>>
|
||||
|
||||
@Upsert
|
||||
suspend fun upsertAll(items: List<ItemEntity>)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.room.Delete
|
|||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import androidx.room.Upsert
|
||||
import de.krisenvorrat.app.data.db.entity.LocationEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
|
@ -25,4 +26,7 @@ internal interface LocationDao {
|
|||
|
||||
@Query("SELECT * FROM locations WHERE id = :id")
|
||||
suspend fun getById(id: Int): LocationEntity?
|
||||
|
||||
@Upsert
|
||||
suspend fun upsertAll(locations: List<LocationEntity>)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,4 +17,7 @@ internal interface SettingsDao {
|
|||
|
||||
@Query("SELECT * FROM settings")
|
||||
fun getAll(): Flow<List<SettingsEntity>>
|
||||
|
||||
@Upsert
|
||||
suspend fun upsertAll(settings: List<SettingsEntity>)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
package de.krisenvorrat.app.data.export
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal data class ExportData(
|
||||
val version: Int = 1,
|
||||
val categories: List<CategoryExport>,
|
||||
val locations: List<LocationExport>,
|
||||
val items: List<ItemExport>,
|
||||
val settings: List<SettingExport>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class CategoryExport(
|
||||
val id: Int,
|
||||
val name: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class LocationExport(
|
||||
val id: Int,
|
||||
val name: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class ItemExport(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val categoryId: Int,
|
||||
val quantity: Double,
|
||||
val unit: String,
|
||||
val unitPrice: Double,
|
||||
val kcalPer100g: Int?,
|
||||
val expiryDate: String?,
|
||||
val locationId: Int,
|
||||
val minStock: Double,
|
||||
val notes: String,
|
||||
val lastUpdated: Long
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class SettingExport(
|
||||
val key: String,
|
||||
val value: String
|
||||
)
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package de.krisenvorrat.app.data.export
|
||||
|
||||
import de.krisenvorrat.app.data.db.dao.CategoryDao
|
||||
import de.krisenvorrat.app.data.db.dao.ItemDao
|
||||
import de.krisenvorrat.app.data.db.dao.LocationDao
|
||||
import de.krisenvorrat.app.data.db.dao.SettingsDao
|
||||
import de.krisenvorrat.app.data.db.entity.CategoryEntity
|
||||
import de.krisenvorrat.app.data.db.entity.ItemEntity
|
||||
import de.krisenvorrat.app.data.db.entity.LocationEntity
|
||||
import de.krisenvorrat.app.data.db.entity.SettingsEntity
|
||||
import de.krisenvorrat.app.domain.repository.ImportExportRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.LocalDate
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class ImportExportRepositoryImpl @Inject constructor(
|
||||
private val categoryDao: CategoryDao,
|
||||
private val locationDao: LocationDao,
|
||||
private val itemDao: ItemDao,
|
||||
private val settingsDao: SettingsDao
|
||||
) : ImportExportRepository {
|
||||
|
||||
private val jsonSerializer = Json { prettyPrint = false }
|
||||
|
||||
override suspend fun exportToJson(): String = withContext(Dispatchers.IO) {
|
||||
val categories = categoryDao.getAll().first()
|
||||
val locations = locationDao.getAll().first()
|
||||
val items = itemDao.getAll().first()
|
||||
val settings = settingsDao.getAll().first()
|
||||
|
||||
val exportData = ExportData(
|
||||
categories = categories.map { CategoryExport(id = it.id, name = it.name) },
|
||||
locations = locations.map { LocationExport(id = it.id, name = it.name) },
|
||||
items = items.map { item ->
|
||||
ItemExport(
|
||||
id = item.id,
|
||||
name = item.name,
|
||||
categoryId = item.categoryId,
|
||||
quantity = item.quantity,
|
||||
unit = item.unit,
|
||||
unitPrice = item.unitPrice,
|
||||
kcalPer100g = item.kcalPer100g,
|
||||
expiryDate = item.expiryDate?.toString(),
|
||||
locationId = item.locationId,
|
||||
minStock = item.minStock,
|
||||
notes = item.notes,
|
||||
lastUpdated = item.lastUpdated
|
||||
)
|
||||
},
|
||||
settings = settings.map { SettingExport(key = it.key, value = it.value) }
|
||||
)
|
||||
jsonSerializer.encodeToString(ExportData.serializer(), exportData)
|
||||
}
|
||||
|
||||
override suspend fun importFromJson(json: String) = withContext(Dispatchers.IO) {
|
||||
val exportData = jsonSerializer.decodeFromString(ExportData.serializer(), json)
|
||||
categoryDao.upsertAll(exportData.categories.map { CategoryEntity(id = it.id, name = it.name) })
|
||||
locationDao.upsertAll(exportData.locations.map { LocationEntity(id = it.id, name = it.name) })
|
||||
itemDao.upsertAll(exportData.items.map { item ->
|
||||
ItemEntity(
|
||||
id = item.id,
|
||||
name = item.name,
|
||||
categoryId = item.categoryId,
|
||||
quantity = item.quantity,
|
||||
unit = item.unit,
|
||||
unitPrice = item.unitPrice,
|
||||
kcalPer100g = item.kcalPer100g,
|
||||
expiryDate = item.expiryDate?.let { LocalDate.parse(it) },
|
||||
locationId = item.locationId,
|
||||
minStock = item.minStock,
|
||||
notes = item.notes,
|
||||
lastUpdated = item.lastUpdated
|
||||
)
|
||||
})
|
||||
settingsDao.upsertAll(exportData.settings.map { SettingsEntity(key = it.key, value = it.value) })
|
||||
}
|
||||
}
|
||||
|
|
@ -4,11 +4,13 @@ import dagger.Binds
|
|||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import de.krisenvorrat.app.data.export.ImportExportRepositoryImpl
|
||||
import de.krisenvorrat.app.data.repository.CategoryRepositoryImpl
|
||||
import de.krisenvorrat.app.data.repository.ItemRepositoryImpl
|
||||
import de.krisenvorrat.app.data.repository.LocationRepositoryImpl
|
||||
import de.krisenvorrat.app.data.repository.SettingsRepositoryImpl
|
||||
import de.krisenvorrat.app.domain.repository.CategoryRepository
|
||||
import de.krisenvorrat.app.domain.repository.ImportExportRepository
|
||||
import de.krisenvorrat.app.domain.repository.ItemRepository
|
||||
import de.krisenvorrat.app.domain.repository.LocationRepository
|
||||
import de.krisenvorrat.app.domain.repository.SettingsRepository
|
||||
|
|
@ -33,4 +35,8 @@ internal abstract class RepositoryModule {
|
|||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindSettingsRepository(impl: SettingsRepositoryImpl): SettingsRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindImportExportRepository(impl: ImportExportRepositoryImpl): ImportExportRepository
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package de.krisenvorrat.app.domain.repository
|
||||
|
||||
internal interface ImportExportRepository {
|
||||
suspend fun exportToJson(): String
|
||||
suspend fun importFromJson(json: String)
|
||||
}
|
||||
|
|
@ -36,6 +36,14 @@ private class FakeCategoryDao : CategoryDao {
|
|||
override fun getAll(): Flow<List<CategoryEntity>> = flow
|
||||
|
||||
override suspend fun getById(id: Int): CategoryEntity? = items.find { it.id == id }
|
||||
|
||||
override suspend fun upsertAll(categories: List<CategoryEntity>) {
|
||||
categories.forEach { category ->
|
||||
val idx = items.indexOfFirst { it.id == category.id }
|
||||
if (idx >= 0) items[idx] = category else items.add(category)
|
||||
}
|
||||
flow.value = items.toList()
|
||||
}
|
||||
}
|
||||
|
||||
class CategoryRepositoryImplTest {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,14 @@ private class FakeItemDao : ItemDao {
|
|||
|
||||
override fun getExpiringSoonByCutoff(cutoff: LocalDate): Flow<List<ItemEntity>> =
|
||||
expiringSoonFlow
|
||||
|
||||
override suspend fun upsertAll(items: List<ItemEntity>) {
|
||||
items.forEach { item ->
|
||||
val idx = this.items.indexOfFirst { it.id == item.id }
|
||||
if (idx >= 0) this.items[idx] = item else this.items.add(item)
|
||||
}
|
||||
emit()
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildItem(
|
||||
|
|
|
|||
|
|
@ -36,6 +36,14 @@ private class FakeLocationDao : LocationDao {
|
|||
override fun getAll(): Flow<List<LocationEntity>> = flow
|
||||
|
||||
override suspend fun getById(id: Int): LocationEntity? = items.find { it.id == id }
|
||||
|
||||
override suspend fun upsertAll(locations: List<LocationEntity>) {
|
||||
locations.forEach { location ->
|
||||
val idx = items.indexOfFirst { it.id == location.id }
|
||||
if (idx >= 0) items[idx] = location else items.add(location)
|
||||
}
|
||||
flow.value = items.toList()
|
||||
}
|
||||
}
|
||||
|
||||
class LocationRepositoryImplTest {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ private class FakeSettingsDao : SettingsDao {
|
|||
override suspend fun getValue(key: String): String? = store[key]
|
||||
|
||||
override fun getAll(): Flow<List<SettingsEntity>> = flow
|
||||
|
||||
override suspend fun upsertAll(settings: List<SettingsEntity>) {
|
||||
settings.forEach { store[it.key] = it.value }
|
||||
flow.value = store.map { SettingsEntity(key = it.key, value = it.value) }
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsRepositoryImplTest {
|
||||
|
|
|
|||
Loading…
Reference in a new issue