From ab49d5a5d61cdaf1c58c75b64c3c26d984fd97c2 Mon Sep 17 00:00:00 2001 From: Elie Gambache Date: Thu, 26 Jun 2025 22:23:30 +0300 Subject: [PATCH] Add CategoriesDao with database operations and tests - Implemented `CategoriesDao` to handle category-related database queries and app retrieval by category. - Added `CategoriesDaoTest` to validate DAO functionality using a test database. - Updated README with `CategoriesDao` examples for usage. - Fixed duplicate `ktor-client-core` declaration in `libs.versions.toml`. --- README.MD | 49 +++++ .../database/dao/CategoriesDao.kt | 105 ++++++++++ .../database/dao/CategoriesDaoTest.kt | 185 ++++++++++++++++++ gradle/libs.versions.toml | 2 +- 4 files changed, 340 insertions(+), 1 deletion(-) create mode 100644 dao/src/commonMain/kotlin/io/github/kdroidfilter/database/dao/CategoriesDao.kt create mode 100644 dao/src/jvmTest/kotlin/io/github/kdroidfilter/database/dao/CategoriesDaoTest.kt diff --git a/README.MD b/README.MD index 2a58f50..91be7c3 100644 --- a/README.MD +++ b/README.MD @@ -473,6 +473,7 @@ dependencies { #### Key Components - **ApplicationsDao**: Provides methods for loading and searching applications in the database, as well as checking and retrieving recommended applications +- **CategoriesDao**: Provides methods for retrieving categories and applications by category - **VersionDao**: Handles database version management operations - **AppInfoWithExtras**: A data class that extends the GooglePlayApplicationInfo model with additional information - **Data Consistency Verification**: Utilities to ensure alignment between JSON policy files and database records @@ -535,6 +536,54 @@ val recommendedApps = ApplicationsDao.getRecommendedApplications( } ) +// Get all categories with localized names +val categories = CategoriesDao.getAllCategories( + database = database, + deviceLanguage = "en" +) + +// Get applications by category ID +val appsByCategoryId = CategoriesDao.getApplicationsByCategoryId( + database = database, + categoryId = 1, + deviceLanguage = "en", + creator = { id, categoryLocalizedName, appInfo -> + AppInfoWithExtras( + id = id, + categoryLocalizedName = categoryLocalizedName, + app = appInfo + ) + } +) + +// Get applications by category name +val appsByCategoryName = CategoriesDao.getApplicationsByCategoryName( + database = database, + categoryName = "NAVIGATION", + deviceLanguage = "en", + creator = { id, categoryLocalizedName, appInfo -> + AppInfoWithExtras( + id = id, + categoryLocalizedName = categoryLocalizedName, + app = appInfo + ) + } +) + +// Get applications by category enum +val appsByCategory = CategoriesDao.getApplicationsByCategory( + database = database, + category = AppCategory.NAVIGATION, + deviceLanguage = "en", + creator = { id, categoryLocalizedName, appInfo -> + AppInfoWithExtras( + id = id, + categoryLocalizedName = categoryLocalizedName, + app = appInfo + ) + } +) + // Get the current database version val currentVersion = VersionDao.getCurrentVersion(database) diff --git a/dao/src/commonMain/kotlin/io/github/kdroidfilter/database/dao/CategoriesDao.kt b/dao/src/commonMain/kotlin/io/github/kdroidfilter/database/dao/CategoriesDao.kt new file mode 100644 index 0000000..ac0a71b --- /dev/null +++ b/dao/src/commonMain/kotlin/io/github/kdroidfilter/database/dao/CategoriesDao.kt @@ -0,0 +1,105 @@ +package io.github.kdroidfilter.database.dao + +import io.github.kdroidfilter.database.store.Database +import io.github.kdroidfilter.database.core.AppCategory +import io.github.kdroidfilter.database.localization.LocalizedAppCategory +import io.github.kdroidfilter.database.store.App_categories +import io.github.kdroidfilter.database.store.Applications +import io.github.kdroidfilter.database.store.Developers +import io.github.kdroidfilter.storekit.gplay.core.model.GooglePlayApplicationInfo + +/** + * Data Access Object for Categories + * Contains functions for database operations related to categories and retrieving applications by category + */ +object CategoriesDao { + + /** + * Gets all categories from the database + * @param database The database instance + * @param deviceLanguage The device language for localized category names + * @return A list of pairs containing the category enum and its localized name + */ + fun getAllCategories( + database: Database, + deviceLanguage: String + ): List> { + val categoriesQueries = database.app_categoriesQueries + + return categoriesQueries.getAllCategories().executeAsList().map { category -> + val categoryEnum = AppCategory.valueOf(category.category_name) + val localizedName = LocalizedAppCategory.getLocalizedName(categoryEnum, deviceLanguage) + + Pair(categoryEnum, localizedName) + } + } + + /** + * Gets applications by category ID + * @param database The database instance + * @param categoryId The category ID + * @param deviceLanguage The device language for localized category names + * @param creator A function to create the return type from the application data + * @return A list of applications in the specified category + */ + fun getApplicationsByCategoryId( + database: Database, + categoryId: Long, + deviceLanguage: String, + creator: (Long, String, GooglePlayApplicationInfo) -> T + ): List { + val applicationsQueries = database.applicationsQueries + val developersQueries = database.developersQueries + val categoriesQueries = database.app_categoriesQueries + + return applicationsQueries.getApplicationsByCategory(categoryId).executeAsList().map { app -> + val developer = developersQueries.getDeveloperById(app.developer_id).executeAsOne() + val category = categoriesQueries.getCategoryById(app.app_category_id).executeAsOne() + + ApplicationsDao.createAppInfoWithExtras(app, developer, category, deviceLanguage, creator) + } + } + + /** + * Gets applications by category name + * @param database The database instance + * @param categoryName The category name (must match an AppCategory enum value) + * @param deviceLanguage The device language for localized category names + * @param creator A function to create the return type from the application data + * @return A list of applications in the specified category + */ + fun getApplicationsByCategoryName( + database: Database, + categoryName: String, + deviceLanguage: String, + creator: (Long, String, GooglePlayApplicationInfo) -> T + ): List { + val applicationsQueries = database.applicationsQueries + val developersQueries = database.developersQueries + val categoriesQueries = database.app_categoriesQueries + + return applicationsQueries.getApplicationsByCategoryName(categoryName).executeAsList().map { app -> + val developer = developersQueries.getDeveloperById(app.developer_id).executeAsOne() + val category = categoriesQueries.getCategoryById(app.app_category_id).executeAsOne() + + ApplicationsDao.createAppInfoWithExtras(app, developer, category, deviceLanguage, creator) + } + } + + /** + * Gets applications by category enum + * @param database The database instance + * @param category The AppCategory enum + * @param deviceLanguage The device language for localized category names + * @param creator A function to create the return type from the application data + * @return A list of applications in the specified category + */ + fun getApplicationsByCategory( + database: Database, + category: AppCategory, + deviceLanguage: String, + creator: (Long, String, GooglePlayApplicationInfo) -> T + ): List { + return getApplicationsByCategoryName(database, category.name, deviceLanguage, creator) + } +} \ No newline at end of file diff --git a/dao/src/jvmTest/kotlin/io/github/kdroidfilter/database/dao/CategoriesDaoTest.kt b/dao/src/jvmTest/kotlin/io/github/kdroidfilter/database/dao/CategoriesDaoTest.kt new file mode 100644 index 0000000..3f08c27 --- /dev/null +++ b/dao/src/jvmTest/kotlin/io/github/kdroidfilter/database/dao/CategoriesDaoTest.kt @@ -0,0 +1,185 @@ +package io.github.kdroidfilter.database.dao + +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver +import io.github.kdroidfilter.database.core.AppCategory +import io.github.kdroidfilter.database.downloader.DatabaseDownloader +import io.github.kdroidfilter.database.store.Database +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.io.File +import java.nio.file.Path +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +/** + * Tests for the CategoriesDao + * These tests download the database from GitHub releases and use it to test the DAO functionality + */ +class CategoriesDaoTest { + + private lateinit var database: Database + private lateinit var driver: SqlDriver + private lateinit var databaseFile: File + + @BeforeEach + fun setup(@TempDir tempDir: Path) { + // Download the database from GitHub releases + val language = "en" // Use English for tests + val dbFileName = "store-database-$language.db" + databaseFile = tempDir.resolve(dbFileName).toFile() + + println("[DEBUG_LOG] Temp directory: ${tempDir.toAbsolutePath()}") + println("[DEBUG_LOG] Database file: ${databaseFile.absolutePath}") + + // Download the database + runBlocking { + val downloader = DatabaseDownloader() + val success = downloader.downloadLatestStoreDatabaseForLanguage( + tempDir.toString(), + language + ) + + println("[DEBUG_LOG] Database download success: $success") + assertTrue(success, "Database download should succeed") + assertTrue(databaseFile.exists(), "Database file should exist") + assertTrue(databaseFile.length() > 0, "Database file should not be empty") + } + + // Create the database driver and connection + driver = JdbcSqliteDriver("jdbc:sqlite:${databaseFile.absolutePath}") + database = Database(driver) + + println("[DEBUG_LOG] Database connection established") + } + + @AfterEach + fun tearDown() { + driver.close() + } + + @Test + fun testGetAllCategories() { + // Test getting all categories + val categories = CategoriesDao.getAllCategories( + database = database, + deviceLanguage = "en" + ) + + println("[DEBUG_LOG] Loaded ${categories.size} categories") + assertTrue(categories.isNotEmpty(), "Should load at least one category") + + // Verify the categories contain valid data + categories.forEach { (category, localizedName) -> + println("[DEBUG_LOG] Category: ${category.name} - $localizedName") + assertNotNull(category, "Category should not be null") + assertNotNull(localizedName, "Localized name should not be null") + } + } + + @Test + fun testGetApplicationsByCategoryId() { + // First get all categories to find a valid category ID + val categories = database.app_categoriesQueries.getAllCategories().executeAsList() + assertTrue(categories.isNotEmpty(), "Should have categories to test") + + // Get the first category's ID + val categoryId = categories.first().id + println("[DEBUG_LOG] Testing getApplicationsByCategoryId for category ID: $categoryId") + + // Get applications by category ID + val applications = CategoriesDao.getApplicationsByCategoryId( + database = database, + categoryId = categoryId, + deviceLanguage = "en", + creator = { id, categoryLocalizedName, appInfo -> + AppInfoWithExtras( + id = id, + categoryLocalizedName = categoryLocalizedName, + app = appInfo + ) + } + ) + + println("[DEBUG_LOG] Found ${applications.size} applications for category ID $categoryId") + + // Verify all applications have the correct category ID + applications.forEach { app -> + val appDetails = database.applicationsQueries.getApplicationById(app.id).executeAsOne() + assertEquals(categoryId, appDetails.app_category_id, "Application should have the correct category ID") + } + } + + @Test + fun testGetApplicationsByCategoryName() { + // First get all categories to find a valid category name + val categories = database.app_categoriesQueries.getAllCategories().executeAsList() + assertTrue(categories.isNotEmpty(), "Should have categories to test") + + // Get the first category's name + val categoryName = categories.first().category_name + println("[DEBUG_LOG] Testing getApplicationsByCategoryName for category name: $categoryName") + + // Get applications by category name + val applications = CategoriesDao.getApplicationsByCategoryName( + database = database, + categoryName = categoryName, + deviceLanguage = "en", + creator = { id, categoryLocalizedName, appInfo -> + AppInfoWithExtras( + id = id, + categoryLocalizedName = categoryLocalizedName, + app = appInfo + ) + } + ) + + println("[DEBUG_LOG] Found ${applications.size} applications for category name $categoryName") + + // Verify all applications have the correct category name + applications.forEach { app -> + val appDetails = database.applicationsQueries.getApplicationById(app.id).executeAsOne() + val appCategory = database.app_categoriesQueries.getCategoryById(appDetails.app_category_id).executeAsOne() + assertEquals(categoryName, appCategory.category_name, "Application should have the correct category name") + } + } + + @Test + fun testGetApplicationsByCategory() { + // First get all categories to find a valid category + val dbCategories = database.app_categoriesQueries.getAllCategories().executeAsList() + assertTrue(dbCategories.isNotEmpty(), "Should have categories to test") + + // Get the first category's name and convert to enum + val categoryName = dbCategories.first().category_name + val category = AppCategory.valueOf(categoryName) + println("[DEBUG_LOG] Testing getApplicationsByCategory for category: $category") + + // Get applications by category enum + val applications = CategoriesDao.getApplicationsByCategory( + database = database, + category = category, + deviceLanguage = "en", + creator = { id, categoryLocalizedName, appInfo -> + AppInfoWithExtras( + id = id, + categoryLocalizedName = categoryLocalizedName, + app = appInfo + ) + } + ) + + println("[DEBUG_LOG] Found ${applications.size} applications for category $category") + + // Verify all applications have the correct category + applications.forEach { app -> + val appDetails = database.applicationsQueries.getApplicationById(app.id).executeAsOne() + val appCategory = database.app_categoriesQueries.getCategoryById(appDetails.app_category_id).executeAsOne() + assertEquals(category.name, appCategory.category_name, "Application should have the correct category") + } + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 368526b..2ad8f01 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -38,6 +38,7 @@ kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" } androidx-activityCompose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } @@ -49,7 +50,6 @@ storekit-aptoide-api = { module = "io.github.kdroidfilter:storekit-aptoide-api", storekit-gplayscrapper = { module = "io.github.kdroidfilter:storekit-gplay-scrapper", version.ref = "storekit" } storekit-gplaycore = { module ="io.github.kdroidfilter:storekit-gplay-core", version.ref = "storekit" } platform-tools-release-fetcher = {module = "io.github.kdroidfilter:platformtools.releasefetcher", version.ref = "platformtools"} -ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } sqldelight-runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqlDelight" } sqldelight-coroutines-extensions = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqlDelight" }