Extract database module
This commit is contained in:
1
database/src/main/AndroidManifest.xml
Normal file
1
database/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest />
|
||||
@@ -0,0 +1,30 @@
|
||||
package gq.kirmanak.mealient.database
|
||||
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import gq.kirmanak.mealient.database.recipe.RecipeDao
|
||||
import gq.kirmanak.mealient.database.recipe.entity.*
|
||||
|
||||
@Database(
|
||||
version = 2,
|
||||
entities = [
|
||||
CategoryEntity::class,
|
||||
CategoryRecipeEntity::class,
|
||||
TagEntity::class,
|
||||
TagRecipeEntity::class,
|
||||
RecipeSummaryEntity::class,
|
||||
RecipeEntity::class,
|
||||
RecipeIngredientEntity::class,
|
||||
RecipeInstructionEntity::class
|
||||
],
|
||||
exportSchema = true,
|
||||
autoMigrations = [
|
||||
AutoMigration(from = 1, to = 2)
|
||||
]
|
||||
)
|
||||
@TypeConverters(RoomTypeConverters::class)
|
||||
abstract class AppDb : RoomDatabase() {
|
||||
abstract fun recipeDao(): RecipeDao
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package gq.kirmanak.mealient.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface DatabaseModule {
|
||||
|
||||
companion object {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun createDb(@ApplicationContext context: Context): AppDb =
|
||||
Room.databaseBuilder(context, AppDb::class.java, "app.db").build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package gq.kirmanak.mealient.database
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import kotlinx.datetime.*
|
||||
|
||||
object RoomTypeConverters {
|
||||
@TypeConverter
|
||||
fun localDateTimeToTimestamp(localDateTime: LocalDateTime) =
|
||||
localDateTime.toInstant(TimeZone.UTC).toEpochMilliseconds()
|
||||
|
||||
@TypeConverter
|
||||
fun timestampToLocalDateTime(timestamp: Long) =
|
||||
Instant.fromEpochMilliseconds(timestamp).toLocalDateTime(TimeZone.UTC)
|
||||
|
||||
@TypeConverter
|
||||
fun localDateToTimeStamp(date: LocalDate) =
|
||||
localDateTimeToTimestamp(date.atTime(0, 0))
|
||||
|
||||
@TypeConverter
|
||||
fun timestampToLocalDate(timestamp: Long) =
|
||||
timestampToLocalDateTime(timestamp).date
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package gq.kirmanak.mealient.database.recipe
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.*
|
||||
import gq.kirmanak.mealient.database.recipe.entity.*
|
||||
|
||||
@Dao
|
||||
interface RecipeDao {
|
||||
@Query("SELECT * FROM tags")
|
||||
suspend fun queryAllTags(): List<TagEntity>
|
||||
|
||||
@Query("SELECT * FROM categories")
|
||||
suspend fun queryAllCategories(): List<CategoryEntity>
|
||||
|
||||
@Query("SELECT * FROM recipe_summaries ORDER BY date_added DESC")
|
||||
fun queryRecipesByPages(): PagingSource<Int, RecipeSummaryEntity>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertRecipe(recipeSummaryEntity: RecipeSummaryEntity)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertTag(tagEntity: TagEntity): Long
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertTagRecipeEntity(tagRecipeEntity: TagRecipeEntity)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertCategory(categoryEntity: CategoryEntity): Long
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertCategoryRecipeEntity(categoryRecipeEntity: CategoryRecipeEntity)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertTagRecipeEntities(tagRecipeEntities: Set<TagRecipeEntity>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertCategoryRecipeEntities(categoryRecipeEntities: Set<CategoryRecipeEntity>)
|
||||
|
||||
@Query("DELETE FROM recipe_summaries")
|
||||
suspend fun removeAllRecipes()
|
||||
|
||||
@Query("DELETE FROM tags")
|
||||
suspend fun removeAllTags()
|
||||
|
||||
@Query("DELETE FROM categories")
|
||||
suspend fun removeAllCategories()
|
||||
|
||||
@Query("SELECT * FROM recipe_summaries ORDER BY date_updated DESC")
|
||||
suspend fun queryAllRecipes(): List<RecipeSummaryEntity>
|
||||
|
||||
@Query("SELECT * FROM category_recipe")
|
||||
suspend fun queryAllCategoryRecipes(): List<CategoryRecipeEntity>
|
||||
|
||||
@Query("SELECT * FROM tag_recipe")
|
||||
suspend fun queryAllTagRecipes(): List<TagRecipeEntity>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertRecipe(recipe: RecipeEntity)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertRecipeInstructions(instructions: List<RecipeInstructionEntity>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertRecipeIngredients(ingredients: List<RecipeIngredientEntity>)
|
||||
|
||||
@Transaction
|
||||
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) // The lint is wrong, the columns are actually used
|
||||
@Query("SELECT * FROM recipe JOIN recipe_summaries ON recipe.remote_id = recipe_summaries.remote_id JOIN recipe_ingredient ON recipe_ingredient.recipe_id = recipe.remote_id JOIN recipe_instruction ON recipe_instruction.recipe_id = recipe.remote_id WHERE recipe.remote_id = :recipeId")
|
||||
suspend fun queryFullRecipeInfo(recipeId: Long): FullRecipeInfo?
|
||||
|
||||
@Query("DELETE FROM recipe_ingredient WHERE recipe_id = :recipeId")
|
||||
suspend fun deleteRecipeIngredients(recipeId: Long)
|
||||
|
||||
@Query("DELETE FROM recipe_instruction WHERE recipe_id = :recipeId")
|
||||
suspend fun deleteRecipeInstructions(recipeId: Long)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package gq.kirmanak.mealient.database.recipe.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "categories", indices = [Index(value = ["name"], unique = true)])
|
||||
data class CategoryEntity(
|
||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0,
|
||||
@ColumnInfo(name = "name") val name: String,
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
package gq.kirmanak.mealient.database.recipe.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
|
||||
@Entity(
|
||||
tableName = "category_recipe",
|
||||
primaryKeys = ["category_id", "recipe_id"],
|
||||
indices = [Index(value = ["category_id", "recipe_id"], unique = true)],
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = CategoryEntity::class,
|
||||
parentColumns = ["local_id"],
|
||||
childColumns = ["category_id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
onUpdate = ForeignKey.CASCADE
|
||||
), ForeignKey(
|
||||
entity = RecipeSummaryEntity::class,
|
||||
parentColumns = ["remote_id"],
|
||||
childColumns = ["recipe_id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
onUpdate = ForeignKey.CASCADE
|
||||
)]
|
||||
)
|
||||
data class CategoryRecipeEntity(
|
||||
@ColumnInfo(name = "category_id") val categoryId: Long,
|
||||
@ColumnInfo(name = "recipe_id", index = true) val recipeId: Long
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
package gq.kirmanak.mealient.database.recipe.entity
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Relation
|
||||
|
||||
data class FullRecipeInfo(
|
||||
@Embedded val recipeEntity: RecipeEntity,
|
||||
@Relation(
|
||||
parentColumn = "remote_id",
|
||||
entityColumn = "remote_id"
|
||||
)
|
||||
val recipeSummaryEntity: RecipeSummaryEntity,
|
||||
@Relation(
|
||||
parentColumn = "remote_id",
|
||||
entityColumn = "recipe_id"
|
||||
)
|
||||
val recipeIngredients: List<RecipeIngredientEntity>,
|
||||
@Relation(
|
||||
parentColumn = "remote_id",
|
||||
entityColumn = "recipe_id"
|
||||
)
|
||||
val recipeInstructions: List<RecipeInstructionEntity>,
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
package gq.kirmanak.mealient.database.recipe.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "recipe")
|
||||
data class RecipeEntity(
|
||||
@PrimaryKey @ColumnInfo(name = "remote_id") val remoteId: Long,
|
||||
@ColumnInfo(name = "recipe_yield") val recipeYield: String,
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
package gq.kirmanak.mealient.database.recipe.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "recipe_ingredient")
|
||||
data class RecipeIngredientEntity(
|
||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0,
|
||||
@ColumnInfo(name = "recipe_id") val recipeId: Long,
|
||||
@ColumnInfo(name = "title") val title: String,
|
||||
@ColumnInfo(name = "note") val note: String,
|
||||
@ColumnInfo(name = "unit") val unit: String,
|
||||
@ColumnInfo(name = "food") val food: String,
|
||||
@ColumnInfo(name = "disable_amount") val disableAmount: Boolean,
|
||||
@ColumnInfo(name = "quantity") val quantity: Int,
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package gq.kirmanak.mealient.database.recipe.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "recipe_instruction")
|
||||
data class RecipeInstructionEntity(
|
||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0,
|
||||
@ColumnInfo(name = "recipe_id") val recipeId: Long,
|
||||
@ColumnInfo(name = "title") val title: String,
|
||||
@ColumnInfo(name = "text") val text: String,
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
package gq.kirmanak.mealient.database.recipe.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
|
||||
@Entity(tableName = "recipe_summaries")
|
||||
data class RecipeSummaryEntity(
|
||||
@PrimaryKey @ColumnInfo(name = "remote_id") val remoteId: Long,
|
||||
@ColumnInfo(name = "name") val name: String,
|
||||
@ColumnInfo(name = "slug") val slug: String,
|
||||
@ColumnInfo(name = "image") val image: String?,
|
||||
@ColumnInfo(name = "description") val description: String,
|
||||
@ColumnInfo(name = "rating") val rating: Int?,
|
||||
@ColumnInfo(name = "date_added") val dateAdded: LocalDate,
|
||||
@ColumnInfo(name = "date_updated") val dateUpdated: LocalDateTime
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "RecipeSummaryEntity(remoteId=$remoteId, name='$name')"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package gq.kirmanak.mealient.database.recipe.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "tags", indices = [Index(value = ["name"], unique = true)])
|
||||
data class TagEntity(
|
||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0,
|
||||
@ColumnInfo(name = "name") val name: String
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
package gq.kirmanak.mealient.database.recipe.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
|
||||
@Entity(
|
||||
tableName = "tag_recipe",
|
||||
primaryKeys = ["tag_id", "recipe_id"],
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = TagEntity::class,
|
||||
parentColumns = ["local_id"],
|
||||
childColumns = ["tag_id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
onUpdate = ForeignKey.CASCADE
|
||||
), ForeignKey(
|
||||
entity = RecipeSummaryEntity::class,
|
||||
parentColumns = ["remote_id"],
|
||||
childColumns = ["recipe_id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
onUpdate = ForeignKey.CASCADE
|
||||
)]
|
||||
)
|
||||
data class TagRecipeEntity(
|
||||
@ColumnInfo(name = "tag_id") val tagId: Long,
|
||||
@ColumnInfo(name = "recipe_id", index = true) val recipeId: Long
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
package gq.kirmanak.mealient.database
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import org.junit.Test
|
||||
|
||||
class RoomTypeConvertersTest {
|
||||
@Test
|
||||
fun `when localDateTimeToTimestamp then correctly converts`() {
|
||||
val input = LocalDateTime.parse("2021-11-13T15:56:33")
|
||||
val actual = RoomTypeConverters.localDateTimeToTimestamp(input)
|
||||
assertThat(actual).isEqualTo(1636818993000)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when timestampToLocalDateTime then correctly converts`() {
|
||||
val expected = LocalDateTime.parse("2021-11-13T15:58:38")
|
||||
val actual = RoomTypeConverters.timestampToLocalDateTime(1636819118000)
|
||||
assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when localDateToTimeStamp then correctly converts`() {
|
||||
val input = LocalDate.parse("2021-11-13")
|
||||
val actual = RoomTypeConverters.localDateToTimeStamp(input)
|
||||
assertThat(actual).isEqualTo(1636761600000)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when timestampToLocalDate then correctly converts`() {
|
||||
val expected = LocalDate.parse("2021-11-13")
|
||||
val actual = RoomTypeConverters.timestampToLocalDate(1636761600000)
|
||||
assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user