diff --git a/database/src/main/kotlin/dev/msfjarvis/claw/database/injection/DatabaseModule.kt b/database/src/main/kotlin/dev/msfjarvis/claw/database/injection/DatabaseModule.kt index 17ceb416..67202826 100644 --- a/database/src/main/kotlin/dev/msfjarvis/claw/database/injection/DatabaseModule.kt +++ b/database/src/main/kotlin/dev/msfjarvis/claw/database/injection/DatabaseModule.kt @@ -15,6 +15,7 @@ import com.squareup.anvil.annotations.ContributesTo import dagger.Module import dagger.Provides import dev.msfjarvis.claw.database.LobstersDatabase +import dev.msfjarvis.claw.database.local.CachedNewestPost import dev.msfjarvis.claw.database.local.PostComments import dev.msfjarvis.claw.database.local.SavedPost import dev.msfjarvis.claw.database.model.CSVAdapter @@ -37,8 +38,10 @@ object DatabaseModule { ) return LobstersDatabase( driver = driver, - PostCommentsAdapter = PostComments.Adapter(CSVAdapter()), - SavedPostAdapter = SavedPost.Adapter(IntColumnAdapter, CSVAdapter()), + PostCommentsAdapter = PostComments.Adapter(CSVAdapter), + SavedPostAdapter = SavedPost.Adapter(IntColumnAdapter, CSVAdapter), + CachedNewestPostAdapter = + CachedNewestPost.Adapter(IntColumnAdapter, IntColumnAdapter, CSVAdapter), ) } } diff --git a/database/src/main/kotlin/dev/msfjarvis/claw/database/model/CSVAdapter.kt b/database/src/main/kotlin/dev/msfjarvis/claw/database/model/CSVAdapter.kt index 0488d658..747fe490 100644 --- a/database/src/main/kotlin/dev/msfjarvis/claw/database/model/CSVAdapter.kt +++ b/database/src/main/kotlin/dev/msfjarvis/claw/database/model/CSVAdapter.kt @@ -8,7 +8,9 @@ package dev.msfjarvis.claw.database.model import app.cash.sqldelight.ColumnAdapter -class CSVAdapter : ColumnAdapter, String> { +object CSVAdapter : ColumnAdapter, String> { + private const val SEPARATOR = "," + override fun decode(databaseValue: String): List { return databaseValue.split(SEPARATOR) } @@ -16,8 +18,4 @@ class CSVAdapter : ColumnAdapter, String> { override fun encode(value: List): String { return value.joinToString(SEPARATOR) } - - private companion object { - private const val SEPARATOR = "," - } } diff --git a/database/src/main/sqldelight/dev/msfjarvis/claw/database/local/CachedNewestPost.sq b/database/src/main/sqldelight/dev/msfjarvis/claw/database/local/CachedNewestPost.sq new file mode 100644 index 00000000..7f760e2b --- /dev/null +++ b/database/src/main/sqldelight/dev/msfjarvis/claw/database/local/CachedNewestPost.sq @@ -0,0 +1,31 @@ +import kotlin.Int; +import kotlin.String; +import kotlin.collections.List; + +CREATE TABLE IF NOT EXISTS CachedNewestPost( + pageNumber INTEGER AS Int, + shortId TEXT NOT NULL PRIMARY KEY, + title TEXT NOT NULL, + url TEXT NOT NULL, + description TEXT NOT NULL, + commentCount INTEGER AS Int, + commentsUrl TEXT NOT NULL, + tags TEXT AS List NOT NULL +); + +insertPost: +INSERT OR REPLACE +INTO CachedNewestPost +VALUES ?; + +getPage: +SELECT * +FROM CachedNewestPost +WHERE pageNumber = ?; + +clearPage: +DELETE FROM CachedNewestPost +WHERE pageNumber = ?; + +deleteAll: +DELETE FROM CachedNewestPost; diff --git a/database/src/main/sqldelight/migrations/5.sqm b/database/src/main/sqldelight/migrations/5.sqm new file mode 100644 index 00000000..1bef9181 --- /dev/null +++ b/database/src/main/sqldelight/migrations/5.sqm @@ -0,0 +1,14 @@ +import kotlin.Int; +import kotlin.String; +import kotlin.collections.List; + +CREATE TABLE IF NOT EXISTS CachedNewestPost( + pageNumber INTEGER AS Int, + shortId TEXT NOT NULL PRIMARY KEY, + title TEXT NOT NULL, + url TEXT NOT NULL, + description TEXT NOT NULL, + commentCount INTEGER AS Int, + commentsUrl TEXT NOT NULL, + tags TEXT AS List NOT NULL +); diff --git a/database/src/test/kotlin/dev/msfjarvis/claw/database/local/PostCommentsQueriesTest.kt b/database/src/test/kotlin/dev/msfjarvis/claw/database/local/PostCommentsQueriesTest.kt index 0e5a2ad3..42a05f40 100644 --- a/database/src/test/kotlin/dev/msfjarvis/claw/database/local/PostCommentsQueriesTest.kt +++ b/database/src/test/kotlin/dev/msfjarvis/claw/database/local/PostCommentsQueriesTest.kt @@ -25,8 +25,9 @@ class PostCommentsQueriesTest { val database = LobstersDatabase( driver, - PostComments.Adapter(CSVAdapter()), - SavedPost.Adapter(IntColumnAdapter, CSVAdapter()), + CachedNewestPost.Adapter(IntColumnAdapter, IntColumnAdapter, CSVAdapter), + PostComments.Adapter(CSVAdapter), + SavedPost.Adapter(IntColumnAdapter, CSVAdapter), ) postQueries = database.postCommentsQueries } diff --git a/database/src/test/kotlin/dev/msfjarvis/claw/database/local/SavedPostQueriesTest.kt b/database/src/test/kotlin/dev/msfjarvis/claw/database/local/SavedPostQueriesTest.kt index c0371f77..5220f0aa 100644 --- a/database/src/test/kotlin/dev/msfjarvis/claw/database/local/SavedPostQueriesTest.kt +++ b/database/src/test/kotlin/dev/msfjarvis/claw/database/local/SavedPostQueriesTest.kt @@ -24,8 +24,9 @@ class SavedPostQueriesTest { val database = LobstersDatabase( driver, - PostComments.Adapter(CSVAdapter()), - SavedPost.Adapter(IntColumnAdapter, CSVAdapter()), + CachedNewestPost.Adapter(IntColumnAdapter, IntColumnAdapter, CSVAdapter), + PostComments.Adapter(CSVAdapter), + SavedPost.Adapter(IntColumnAdapter, CSVAdapter), ) postQueries = database.savedPostQueries } diff --git a/store/build.gradle.kts b/store/build.gradle.kts index ccf6563a..322c8c03 100644 --- a/store/build.gradle.kts +++ b/store/build.gradle.kts @@ -19,8 +19,13 @@ anvil { generateDaggerFactories.set(true) } dependencies { api(projects.database) api(projects.model) + api(projects.core) implementation(projects.api) + implementation(libs.dagger) + implementation(libs.javax.inject) implementation(libs.kotlinx.atomicfu) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.sqldelight.extensions.coroutines) implementation(libs.store5) } diff --git a/store/lint-baseline.xml b/store/lint-baseline.xml new file mode 100644 index 00000000..938f9bf2 --- /dev/null +++ b/store/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/store/src/main/AndroidManifest.xml b/store/src/main/AndroidManifest.xml index a1db17f4..87353de8 100644 --- a/store/src/main/AndroidManifest.xml +++ b/store/src/main/AndroidManifest.xml @@ -1,4 +1,9 @@ - + diff --git a/store/src/main/kotlin/dev/msfjarvis/claw/store/NewestPostsStore.kt b/store/src/main/kotlin/dev/msfjarvis/claw/store/NewestPostsStore.kt new file mode 100644 index 00000000..f6ae06f8 --- /dev/null +++ b/store/src/main/kotlin/dev/msfjarvis/claw/store/NewestPostsStore.kt @@ -0,0 +1,68 @@ +/* + * Copyright © 2023 Harsh Shandilya. + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + */ +package dev.msfjarvis.claw.store + +import app.cash.sqldelight.coroutines.asFlow +import app.cash.sqldelight.coroutines.mapToList +import com.slack.eithernet.ApiResult +import dev.msfjarvis.claw.api.LobstersApi +import dev.msfjarvis.claw.core.injection.DatabaseDispatcher +import dev.msfjarvis.claw.core.injection.IODispatcher +import dev.msfjarvis.claw.database.LobstersDatabase +import dev.msfjarvis.claw.database.local.CachedNewestPost +import dev.msfjarvis.claw.model.LobstersPost +import dev.msfjarvis.claw.store.utils.toCachedNewest +import java.io.IOException +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext +import org.mobilenativefoundation.store.store5.Fetcher +import org.mobilenativefoundation.store.store5.SourceOfTruth +import org.mobilenativefoundation.store.store5.Store +import org.mobilenativefoundation.store.store5.StoreBuilder + +class NewestPostsStore +@Inject +constructor( + private val api: LobstersApi, + private val database: LobstersDatabase, + @IODispatcher private val ioDispatcher: CoroutineDispatcher, + @DatabaseDispatcher private val dbDispatcher: CoroutineDispatcher, +) : + Store> by StoreBuilder.from< + Int, List, List + >( + fetcher = + Fetcher.of { key -> + withContext(ioDispatcher) { + when (val result = api.getNewestPosts(key)) { + is ApiResult.Success -> result.value + is ApiResult.Failure.NetworkFailure -> throw result.error + is ApiResult.Failure.UnknownFailure -> throw result.error + is ApiResult.Failure.HttpFailure, + is ApiResult.Failure.ApiFailure -> + throw IOException("API returned an invalid response") + } + } + }, + sourceOfTruth = + SourceOfTruth.of( + reader = { page -> + database.cachedNewestPostQueries.getPage(page).asFlow().mapToList(dbDispatcher) + }, + writer = { page, items -> + database.transaction { + items + .map { it.toCachedNewest(page) } + .forEach { database.cachedNewestPostQueries.insertPost(it) } + } + }, + delete = { page -> database.cachedNewestPostQueries.clearPage(page) }, + deleteAll = { database.cachedNewestPostQueries.deleteAll() }, + ), + ) + .build() diff --git a/store/src/main/kotlin/dev/msfjarvis/claw/store/utils/ext.kt b/store/src/main/kotlin/dev/msfjarvis/claw/store/utils/ext.kt new file mode 100644 index 00000000..5b562bcc --- /dev/null +++ b/store/src/main/kotlin/dev/msfjarvis/claw/store/utils/ext.kt @@ -0,0 +1,23 @@ +/* + * Copyright © 2023 Harsh Shandilya. + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + */ +package dev.msfjarvis.claw.store.utils + +import dev.msfjarvis.claw.database.local.CachedNewestPost +import dev.msfjarvis.claw.model.LobstersPost + +internal fun LobstersPost.toCachedNewest(page: Int): CachedNewestPost { + return CachedNewestPost( + pageNumber = page, + shortId = shortId, + title = title, + url = url, + description = description, + commentCount = commentCount, + commentsUrl = commentsUrl, + tags = tags, + ) +}