diff --git a/build-logic/convention/src/main/kotlin/com/peto/droidmorning/DroidMorningFeaturePlugin.kt b/build-logic/convention/src/main/kotlin/com/peto/droidmorning/DroidMorningFeaturePlugin.kt index c5a145d..cbc34a6 100644 --- a/build-logic/convention/src/main/kotlin/com/peto/droidmorning/DroidMorningFeaturePlugin.kt +++ b/build-logic/convention/src/main/kotlin/com/peto/droidmorning/DroidMorningFeaturePlugin.kt @@ -1,7 +1,5 @@ package com.peto.droidmorning -import com.peto.droidmorning.extentions.composeMultiplatformDependencies -import com.peto.droidmorning.extentions.koinDependencies import com.peto.droidmorning.extentions.library import com.peto.droidmorning.extentions.libs import com.peto.droidmorning.extentions.plugin diff --git a/build-logic/convention/src/main/kotlin/com/peto/droidmorning/KotlinMultiplatformConventionPlugin.kt b/build-logic/convention/src/main/kotlin/com/peto/droidmorning/KotlinMultiplatformConventionPlugin.kt index b38f1e5..ea1ecc1 100644 --- a/build-logic/convention/src/main/kotlin/com/peto/droidmorning/KotlinMultiplatformConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/com/peto/droidmorning/KotlinMultiplatformConventionPlugin.kt @@ -1,5 +1,7 @@ package com.peto.droidmorning +import com.peto.droidmorning.extentions.libs +import com.peto.droidmorning.extentions.plugin import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.apply @@ -7,9 +9,10 @@ import org.gradle.kotlin.dsl.apply class KotlinMultiplatformConventionPlugin : Plugin { override fun apply(target: Project) = with(target) { with(pluginManager) { - apply("org.jetbrains.kotlin.multiplatform") + apply(libs.plugin("kotlin-multiplatform").pluginId) } - + + apply() apply() apply() apply() diff --git a/build-logic/convention/src/main/kotlin/com/peto/droidmorning/extentions/ProjectExtensions.kt b/build-logic/convention/src/main/kotlin/com/peto/droidmorning/extentions/ProjectExtensions.kt index 0beef89..3c10ab4 100644 --- a/build-logic/convention/src/main/kotlin/com/peto/droidmorning/extentions/ProjectExtensions.kt +++ b/build-logic/convention/src/main/kotlin/com/peto/droidmorning/extentions/ProjectExtensions.kt @@ -39,7 +39,9 @@ internal fun Project.koinDependencies() { sourceSets.apply { commonMain { dependencies { - implementation(libs.bundle("koin")) + implementation(libs.library("koin-core")) + implementation(libs.library("koin-compose")) + implementation(libs.library("koin-compose-viewmodel")) } } commonTest { diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index f073ae6..818d0ce 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -54,6 +54,8 @@ kotlin { implementation(project(":domain")) implementation(project(":data")) implementation(project(":designsystem")) + implementation(project(":core:network")) + implementation(project(":core:datastore")) implementation(libs.bundles.compose.multiplatform) implementation(libs.androidx.navigation.compose) diff --git a/composeApp/src/commonMain/kotlin/com/peto/droidmorning/KoinInitializer.kt b/composeApp/src/commonMain/kotlin/com/peto/droidmorning/KoinInitializer.kt index f6d2e2d..3284246 100644 --- a/composeApp/src/commonMain/kotlin/com/peto/droidmorning/KoinInitializer.kt +++ b/composeApp/src/commonMain/kotlin/com/peto/droidmorning/KoinInitializer.kt @@ -1,5 +1,7 @@ package com.peto.droidmorning +import com.peto.droidmorning.core.datastore.di.dataStoreModule +import com.peto.droidmorning.core.network.di.networkModule import com.peto.droidmorning.data.di.dataModule import com.peto.droidmorning.di.navigationModule import com.peto.droidmorning.di.platformModule @@ -19,6 +21,8 @@ fun initKoin( extraModules + listOf( platformModule, + networkModule, + dataStoreModule, dataModule, viewModelModule, navigationModule, diff --git a/core/build.gradle.kts b/core/build.gradle.kts new file mode 100644 index 0000000..4f298e6 --- /dev/null +++ b/core/build.gradle.kts @@ -0,0 +1,2 @@ +plugins { +} diff --git a/core/datastore/build.gradle.kts b/core/datastore/build.gradle.kts new file mode 100644 index 0000000..c9ef75f --- /dev/null +++ b/core/datastore/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + alias(libs.plugins.droidmorning.kotlin.multiplatform) + alias(libs.plugins.droidmorning.android.library) + alias(libs.plugins.droidmorning.koin) +} + +kotlin { + sourceSets { + commonMain.dependencies { + implementation(libs.bundles.datastore) + } + iosMain.dependencies { + implementation(libs.okio) + } + } +} + +android { + namespace = "com.peto.droidmorning.core.datastore" +} diff --git a/data/src/androidMain/kotlin/com/peto/droidmorning/data/local/DataStoreFactory.android.kt b/core/datastore/src/androidMain/kotlin/com/peto/droidmorning/core/datastore/DataStoreFactory.android.kt similarity index 93% rename from data/src/androidMain/kotlin/com/peto/droidmorning/data/local/DataStoreFactory.android.kt rename to core/datastore/src/androidMain/kotlin/com/peto/droidmorning/core/datastore/DataStoreFactory.android.kt index 2830e5b..bb9c691 100644 --- a/data/src/androidMain/kotlin/com/peto/droidmorning/data/local/DataStoreFactory.android.kt +++ b/core/datastore/src/androidMain/kotlin/com/peto/droidmorning/core/datastore/DataStoreFactory.android.kt @@ -1,4 +1,4 @@ -package com.peto.droidmorning.data.local +package com.peto.droidmorning.core.datastore import android.content.Context import androidx.datastore.core.DataStore diff --git a/core/datastore/src/commonMain/kotlin/com/peto/droidmorning/core/datastore/DefaultTokenDataStore.kt b/core/datastore/src/commonMain/kotlin/com/peto/droidmorning/core/datastore/DefaultTokenDataStore.kt new file mode 100644 index 0000000..f7b0eb9 --- /dev/null +++ b/core/datastore/src/commonMain/kotlin/com/peto/droidmorning/core/datastore/DefaultTokenDataStore.kt @@ -0,0 +1,33 @@ +package com.peto.droidmorning.core.datastore + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import kotlinx.coroutines.flow.first + +class DefaultTokenDataStore( + private val dataStore: DataStore, +) : TokenDataStore { + override suspend fun userId(): String? = preferences()[KEY_USER_ID] + + override suspend fun hasUserId(): Boolean = preferences()[KEY_USER_ID] != null + + override suspend fun save(userId: String) { + dataStore.edit { preferences -> + preferences[KEY_USER_ID] = userId + } + } + + override suspend fun clear() { + dataStore.edit { preferences -> + preferences.remove(KEY_USER_ID) + } + } + + private suspend fun preferences(): Preferences = dataStore.data.first() + + private companion object { + private val KEY_USER_ID = stringPreferencesKey("user_id") + } +} diff --git a/core/datastore/src/commonMain/kotlin/com/peto/droidmorning/core/datastore/TokenDataStore.kt b/core/datastore/src/commonMain/kotlin/com/peto/droidmorning/core/datastore/TokenDataStore.kt new file mode 100644 index 0000000..269b39a --- /dev/null +++ b/core/datastore/src/commonMain/kotlin/com/peto/droidmorning/core/datastore/TokenDataStore.kt @@ -0,0 +1,11 @@ +package com.peto.droidmorning.core.datastore + +interface TokenDataStore { + suspend fun userId(): String? + + suspend fun hasUserId(): Boolean + + suspend fun save(userId: String) + + suspend fun clear() +} diff --git a/data/src/commonMain/kotlin/com/peto/droidmorning/data/local/TokenDataStoreFactory.kt b/core/datastore/src/commonMain/kotlin/com/peto/droidmorning/core/datastore/TokenDataStoreFactory.kt similarity index 83% rename from data/src/commonMain/kotlin/com/peto/droidmorning/data/local/TokenDataStoreFactory.kt rename to core/datastore/src/commonMain/kotlin/com/peto/droidmorning/core/datastore/TokenDataStoreFactory.kt index b0b1c6f..f0375da 100644 --- a/data/src/commonMain/kotlin/com/peto/droidmorning/data/local/TokenDataStoreFactory.kt +++ b/core/datastore/src/commonMain/kotlin/com/peto/droidmorning/core/datastore/TokenDataStoreFactory.kt @@ -1,4 +1,4 @@ -package com.peto.droidmorning.data.local +package com.peto.droidmorning.core.datastore import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences diff --git a/core/datastore/src/commonMain/kotlin/com/peto/droidmorning/core/datastore/di/DataStoreModule.kt b/core/datastore/src/commonMain/kotlin/com/peto/droidmorning/core/datastore/di/DataStoreModule.kt new file mode 100644 index 0000000..b3eed76 --- /dev/null +++ b/core/datastore/src/commonMain/kotlin/com/peto/droidmorning/core/datastore/di/DataStoreModule.kt @@ -0,0 +1,12 @@ +package com.peto.droidmorning.core.datastore.di + +import com.peto.droidmorning.core.datastore.DefaultTokenDataStore +import com.peto.droidmorning.core.datastore.TokenDataStore +import com.peto.droidmorning.core.datastore.createTokenDataStore +import org.koin.dsl.module + +val dataStoreModule = + module { + single { createTokenDataStore() } + single { DefaultTokenDataStore(get()) } + } diff --git a/data/src/iosMain/kotlin/com/peto/droidmorning/data/local/DataStoreFactory.ios.kt b/core/datastore/src/iosMain/kotlin/com/peto/droidmorning/core/datastore/DataStoreFactory.ios.kt similarity index 95% rename from data/src/iosMain/kotlin/com/peto/droidmorning/data/local/DataStoreFactory.ios.kt rename to core/datastore/src/iosMain/kotlin/com/peto/droidmorning/core/datastore/DataStoreFactory.ios.kt index 08c4444..ce4f31a 100644 --- a/data/src/iosMain/kotlin/com/peto/droidmorning/data/local/DataStoreFactory.ios.kt +++ b/core/datastore/src/iosMain/kotlin/com/peto/droidmorning/core/datastore/DataStoreFactory.ios.kt @@ -1,4 +1,4 @@ -package com.peto.droidmorning.data.local +package com.peto.droidmorning.core.datastore import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.PreferenceDataStoreFactory diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts new file mode 100644 index 0000000..4407dc5 --- /dev/null +++ b/core/network/build.gradle.kts @@ -0,0 +1,59 @@ +import com.codingfeline.buildkonfig.compiler.FieldSpec.Type +import java.util.Properties + +plugins { + alias(libs.plugins.droidmorning.kotlin.multiplatform) + alias(libs.plugins.droidmorning.android.library) + alias(libs.plugins.droidmorning.koin) + alias(libs.plugins.buildkonfig) +} + +kotlin { + sourceSets { + androidMain.dependencies { + implementation(libs.ktor.client.okhttp) + } + commonMain.dependencies { + implementation(libs.bundles.ktor.common) + + implementation(project.dependencies.platform(libs.supabase.bom)) + implementation(libs.bundles.supabase) + } + iosMain.dependencies { + implementation(libs.ktor.client.darwin) + } + } +} + +buildkonfig { + packageName = "com.peto.droidmorning" + exposeObjectWithName = "BuildKonfig" + + val props = + Properties().apply { + val file = rootProject.file("local.properties") + if (file.exists()) file.inputStream().use { load(it) } + } + + defaultConfigs { + buildConfigField( + Type.STRING, + "GOOGLE_CLIENT_ID", + props.getProperty("GOOGLE_CLIENT_ID"), + ) + buildConfigField( + Type.STRING, + "SUPABASE_URL", + props.getProperty("SUPABASE_URL"), + ) + buildConfigField( + Type.STRING, + "SUPABASE_KEY", + props.getProperty("SUPABASE_KEY"), + ) + } +} + +android { + namespace = "com.peto.droidmorning.core.network" +} diff --git a/core/network/src/commonMain/kotlin/com/peto/droidmorning/core/network/AuthClient.kt b/core/network/src/commonMain/kotlin/com/peto/droidmorning/core/network/AuthClient.kt new file mode 100644 index 0000000..0262a07 --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/peto/droidmorning/core/network/AuthClient.kt @@ -0,0 +1,9 @@ +package com.peto.droidmorning.core.network + +interface AuthClient { + suspend fun signInWithGoogleIdToken(idToken: String): String? + + suspend fun signOut() + + fun currentUserId(): String? +} diff --git a/core/network/src/commonMain/kotlin/com/peto/droidmorning/core/network/HttpClient.kt b/core/network/src/commonMain/kotlin/com/peto/droidmorning/core/network/HttpClient.kt new file mode 100644 index 0000000..b70bfa5 --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/peto/droidmorning/core/network/HttpClient.kt @@ -0,0 +1,18 @@ +package com.peto.droidmorning.core.network + +import com.peto.droidmorning.BuildKonfig +import io.github.jan.supabase.auth.Auth +import io.github.jan.supabase.createSupabaseClient +import io.github.jan.supabase.postgrest.Postgrest + +object HttpClient { + val client by lazy { + createSupabaseClient( + supabaseUrl = BuildKonfig.SUPABASE_URL, + supabaseKey = BuildKonfig.SUPABASE_KEY, + ) { + install(Auth) + install(Postgrest) + } + } +} diff --git a/core/network/src/commonMain/kotlin/com/peto/droidmorning/core/network/PostgrestClient.kt b/core/network/src/commonMain/kotlin/com/peto/droidmorning/core/network/PostgrestClient.kt new file mode 100644 index 0000000..19c9ded --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/peto/droidmorning/core/network/PostgrestClient.kt @@ -0,0 +1,33 @@ +package com.peto.droidmorning.core.network + +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject + +interface PostgrestClient { + suspend fun select( + table: String, + filters: List = emptyList(), + order: PostgrestOrder? = null, + ): String + + suspend fun rpc( + function: String, + parameters: JsonObject? = null, + ): String + + suspend fun insert( + table: String, + body: JsonObject, + ) + + suspend fun update( + table: String, + body: JsonElement, + filters: List = emptyList(), + ) + + suspend fun delete( + table: String, + filters: List = emptyList(), + ) +} diff --git a/core/network/src/commonMain/kotlin/com/peto/droidmorning/core/network/PostgrestFilter.kt b/core/network/src/commonMain/kotlin/com/peto/droidmorning/core/network/PostgrestFilter.kt new file mode 100644 index 0000000..b590e50 --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/peto/droidmorning/core/network/PostgrestFilter.kt @@ -0,0 +1,6 @@ +package com.peto.droidmorning.core.network + +data class PostgrestFilter( + val column: String, + val value: Any, +) diff --git a/core/network/src/commonMain/kotlin/com/peto/droidmorning/core/network/PostgrestOrder.kt b/core/network/src/commonMain/kotlin/com/peto/droidmorning/core/network/PostgrestOrder.kt new file mode 100644 index 0000000..639ede1 --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/peto/droidmorning/core/network/PostgrestOrder.kt @@ -0,0 +1,6 @@ +package com.peto.droidmorning.core.network + +data class PostgrestOrder( + val column: String, + val descending: Boolean = false, +) diff --git a/core/network/src/commonMain/kotlin/com/peto/droidmorning/core/network/SupabaseAuthClient.kt b/core/network/src/commonMain/kotlin/com/peto/droidmorning/core/network/SupabaseAuthClient.kt new file mode 100644 index 0000000..10b3b43 --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/peto/droidmorning/core/network/SupabaseAuthClient.kt @@ -0,0 +1,23 @@ +package com.peto.droidmorning.core.network + +import io.github.jan.supabase.auth.Auth +import io.github.jan.supabase.auth.providers.Google +import io.github.jan.supabase.auth.providers.builtin.IDToken + +class SupabaseAuthClient( + private val auth: Auth, +) : AuthClient { + override suspend fun signInWithGoogleIdToken(idToken: String): String? { + auth.signInWith(IDToken) { + this.idToken = idToken + provider = Google + } + return currentUserId() + } + + override suspend fun signOut() { + auth.signOut() + } + + override fun currentUserId(): String? = auth.currentSessionOrNull()?.user?.id +} diff --git a/core/network/src/commonMain/kotlin/com/peto/droidmorning/core/network/SupabasePostgrestClient.kt b/core/network/src/commonMain/kotlin/com/peto/droidmorning/core/network/SupabasePostgrestClient.kt new file mode 100644 index 0000000..f9c38f7 --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/peto/droidmorning/core/network/SupabasePostgrestClient.kt @@ -0,0 +1,85 @@ +package com.peto.droidmorning.core.network + +import io.github.jan.supabase.postgrest.Postgrest +import io.github.jan.supabase.postgrest.query.Columns +import io.github.jan.supabase.postgrest.query.Order +import io.github.jan.supabase.postgrest.rpc +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject + +class SupabasePostgrestClient( + private val postgrest: Postgrest, +) : PostgrestClient { + override suspend fun select( + table: String, + filters: List, + order: PostgrestOrder?, + ): String = + postgrest + .from(table) + .select(Columns.ALL) { + filter { + filters.forEach { filter -> + eq(filter.column, filter.value) + } + } + order?.let { + order( + it.column, + order = if (it.descending) Order.DESCENDING else Order.ASCENDING, + ) + } + }.data + + override suspend fun rpc( + function: String, + parameters: JsonObject?, + ): String = + postgrest + .rpc( + function = function, + parameters = parameters ?: buildJsonObject { }, + ).data + + override suspend fun insert( + table: String, + body: JsonObject, + ) { + postgrest + .from(table) + .insert(JsonArray(listOf(body))) + } + + override suspend fun update( + table: String, + body: JsonElement, + filters: List, + ) { + postgrest + .from(table) + .update(body) { + filter { + filters.forEach { filter -> + eq(filter.column, filter.value) + } + } + } + } + + override suspend fun delete( + table: String, + filters: List, + ) { + postgrest + .from(table) + .delete { + filter { + filters.forEach { filter -> + eq(filter.column, filter.value) + } + } + } + } +} diff --git a/core/network/src/commonMain/kotlin/com/peto/droidmorning/core/network/di/NetworkModule.kt b/core/network/src/commonMain/kotlin/com/peto/droidmorning/core/network/di/NetworkModule.kt new file mode 100644 index 0000000..6fbb34e --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/peto/droidmorning/core/network/di/NetworkModule.kt @@ -0,0 +1,26 @@ +package com.peto.droidmorning.core.network.di + +import com.peto.droidmorning.core.network.AuthClient +import com.peto.droidmorning.core.network.HttpClient +import com.peto.droidmorning.core.network.PostgrestClient +import com.peto.droidmorning.core.network.SupabaseAuthClient +import com.peto.droidmorning.core.network.SupabasePostgrestClient +import io.github.jan.supabase.SupabaseClient +import io.github.jan.supabase.auth.Auth +import io.github.jan.supabase.auth.auth +import io.github.jan.supabase.postgrest.Postgrest +import io.github.jan.supabase.postgrest.postgrest +import org.koin.dsl.module + +val networkModule = + module { + single { HttpClient.client } + + single { get().auth } + + single { get().postgrest } + + single { SupabaseAuthClient(get()) } + + single { SupabasePostgrestClient(get()) } + } diff --git a/data/build.gradle.kts b/data/build.gradle.kts index f494152..b8d90ed 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -1,48 +1,21 @@ -import com.codingfeline.buildkonfig.compiler.FieldSpec.Type -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import java.util.Properties - plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.library) + alias(libs.plugins.droidmorning.kotlin.multiplatform) + alias(libs.plugins.droidmorning.android.library) + alias(libs.plugins.droidmorning.koin) alias(libs.plugins.kotlin.serialization) - alias(libs.plugins.buildkonfig) } kotlin { - androidTarget { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - } - } - - listOf( - iosArm64(), - iosSimulatorArm64(), - ).forEach { iosTarget -> - iosTarget.binaries.framework { - baseName = "Data" - isStatic = true - } - } - sourceSets { androidMain.dependencies { - implementation(libs.ktor.client.okhttp) } commonMain.dependencies { implementation(project(":domain")) + implementation(project(":core:network")) + implementation(project(":core:datastore")) - implementation(libs.bundles.koin) - implementation(libs.bundles.ktor.common) implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.serialization.json) - - implementation(project.dependencies.platform(libs.supabase.bom)) - implementation(libs.bundles.supabase) - - implementation(libs.bundles.datastore) - implementation(libs.napier) } commonTest.dependencies { implementation(libs.kotlin.test) @@ -50,56 +23,10 @@ kotlin { implementation(libs.kotlinx.coroutines.test) } iosMain.dependencies { - implementation(libs.ktor.client.darwin) - implementation(libs.okio) } } } -buildkonfig { - packageName = "com.peto.droidmorning" - exposeObjectWithName = "BuildKonfig" - - val props = - Properties().apply { - val file = rootProject.file("local.properties") - if (file.exists()) file.inputStream().use { load(it) } - } - - defaultConfigs { - buildConfigField( - Type.STRING, - "GOOGLE_CLIENT_ID", - props.getProperty("GOOGLE_CLIENT_ID"), - ) - buildConfigField( - Type.STRING, - "SUPABASE_URL", - props.getProperty("SUPABASE_URL"), - ) - buildConfigField( - Type.STRING, - "SUPABASE_KEY", - props.getProperty("SUPABASE_KEY"), - ) - } -} - android { namespace = "com.peto.droidmorning.data" - compileSdk = - libs.versions.compileSdk - .get() - .toInt() - - defaultConfig { - minSdk = - libs.versions.minSdk - .get() - .toInt() - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } } diff --git a/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/answer/remote/DefaultRemoteAnswerDataSource.kt b/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/answer/remote/DefaultRemoteAnswerDataSource.kt index 9490903..6c48786 100644 --- a/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/answer/remote/DefaultRemoteAnswerDataSource.kt +++ b/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/answer/remote/DefaultRemoteAnswerDataSource.kt @@ -1,40 +1,58 @@ package com.peto.droidmorning.data.datasource.answer.remote +import com.peto.droidmorning.core.network.AuthClient +import com.peto.droidmorning.core.network.PostgrestClient +import com.peto.droidmorning.core.network.PostgrestFilter +import com.peto.droidmorning.core.network.PostgrestOrder import com.peto.droidmorning.data.model.request.CreateAnswerRequest import com.peto.droidmorning.data.model.request.RpcDefaultRequest import com.peto.droidmorning.data.model.request.UpdateAnswerRequest import com.peto.droidmorning.data.model.response.AnswerHistoryResponse import com.peto.droidmorning.data.model.response.CurrentAnswerResponse -import io.github.jan.supabase.auth.Auth -import io.github.jan.supabase.postgrest.Postgrest -import io.github.jan.supabase.postgrest.query.Columns -import io.github.jan.supabase.postgrest.query.Order -import io.github.jan.supabase.postgrest.rpc +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.encodeToJsonElement +import kotlinx.serialization.json.jsonObject class DefaultRemoteAnswerDataSource( - private val postgrest: Postgrest, - private val auth: Auth, + private val postgrest: PostgrestClient, + private val authClient: AuthClient, ) : RemoteAnswerDataSource { + private val json = Json { ignoreUnknownKeys = true } + override suspend fun fetchCurrentAnswer(questionId: Long): CurrentAnswerResponse? = postgrest - .from(ANSWERS_CURRENT_TABLE) - .select(Columns.ALL) { - filter { - eq(USER_ID_COLUMN, uid()) - eq(QUESTION_ID_COLUMN, questionId) - } - }.decodeSingleOrNull() + .select( + table = ANSWERS_CURRENT_TABLE, + filters = + listOf( + PostgrestFilter(USER_ID_COLUMN, uid()), + PostgrestFilter(QUESTION_ID_COLUMN, questionId), + ), + ).let { data -> + json + .decodeFromString( + ListSerializer(CurrentAnswerResponse.serializer()), + data, + ).firstOrNull() + } override suspend fun fetchAnswerHistory(questionId: Long): List = postgrest - .from(ANSWER_HISTORY_TABLE) - .select(Columns.ALL) { - filter { - eq(USER_ID_COLUMN, uid()) - eq(QUESTION_ID_COLUMN, questionId) - } - order(CREATED_AT_COLUMN, order = Order.DESCENDING) - }.decodeList() + .select( + table = ANSWER_HISTORY_TABLE, + filters = + listOf( + PostgrestFilter(USER_ID_COLUMN, uid()), + PostgrestFilter(QUESTION_ID_COLUMN, questionId), + ), + order = PostgrestOrder(CREATED_AT_COLUMN, descending = true), + ).let { data -> + json.decodeFromString( + ListSerializer(AnswerHistoryResponse.serializer()), + data, + ) + } override suspend fun createAnswer( questionId: Long, @@ -42,7 +60,10 @@ class DefaultRemoteAnswerDataSource( ) { postgrest.rpc( function = RPC_UPSERT_ANSWER_CURRENT, - parameters = CreateAnswerRequest(uid(), questionId, content), + parameters = + json + .encodeToJsonElement(CreateAnswerRequest(uid(), questionId, content)) + .jsonObject, ) } @@ -50,36 +71,40 @@ class DefaultRemoteAnswerDataSource( questionId: Long, content: String, ) { - postgrest - .from(ANSWERS_CURRENT_TABLE) - .update(UpdateAnswerRequest(content)) { - filter { - eq(USER_ID_COLUMN, uid()) - eq(QUESTION_ID_COLUMN, questionId) - } - } + postgrest.update( + table = ANSWERS_CURRENT_TABLE, + body = json.encodeToJsonElement(UpdateAnswerRequest(content)), + filters = + listOf( + PostgrestFilter(USER_ID_COLUMN, uid()), + PostgrestFilter(QUESTION_ID_COLUMN, questionId), + ), + ) } override suspend fun deleteCurrentAnswer(questionId: Long) { postgrest.rpc( function = RPC_DELETE_ANSWER_CURRENT, - parameters = RpcDefaultRequest(uid(), questionId), + parameters = + json + .encodeToJsonElement(RpcDefaultRequest(uid(), questionId)) + .jsonObject, ) } override suspend fun deleteAnswerHistory(historyId: Long) { - postgrest - .from(ANSWER_HISTORY_TABLE) - .delete { - filter { - eq(ID_COLUMN, historyId) - eq(USER_ID_COLUMN, uid()) - } - } + postgrest.delete( + table = ANSWER_HISTORY_TABLE, + filters = + listOf( + PostgrestFilter(ID_COLUMN, historyId), + PostgrestFilter(USER_ID_COLUMN, uid()), + ), + ) } private fun uid(): String = - auth.currentSessionOrNull()?.user?.id + authClient.currentUserId() ?: throw IllegalStateException("User not logged in") companion object { diff --git a/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/auth/local/DefaultLocalAuthDataSource.kt b/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/auth/local/DefaultLocalAuthDataSource.kt index 50d58cb..bee6ef0 100644 --- a/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/auth/local/DefaultLocalAuthDataSource.kt +++ b/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/auth/local/DefaultLocalAuthDataSource.kt @@ -1,33 +1,19 @@ package com.peto.droidmorning.data.datasource.auth.local -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.stringPreferencesKey -import kotlinx.coroutines.flow.first +import com.peto.droidmorning.core.datastore.TokenDataStore class DefaultLocalAuthDataSource( - private val dataStore: DataStore, + private val tokenDataStore: TokenDataStore, ) : LocalAuthDataSource { - override suspend fun userId(): String? = preferences()[KEY_USER_ID] + override suspend fun userId(): String? = tokenDataStore.userId() - override suspend fun hasUserId(): Boolean = preferences()[KEY_USER_ID] != null + override suspend fun hasUserId(): Boolean = tokenDataStore.hasUserId() override suspend fun save(userId: String) { - dataStore.edit { preferences -> - preferences[KEY_USER_ID] = userId - } + tokenDataStore.save(userId) } override suspend fun clear() { - dataStore.edit { preferences -> - preferences.remove(KEY_USER_ID) - } - } - - private suspend fun preferences(): Preferences = dataStore.data.first() - - companion object { - private val KEY_USER_ID = stringPreferencesKey("user_id") + tokenDataStore.clear() } } diff --git a/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/auth/remote/DefaultRemoteAuthDataSource.kt b/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/auth/remote/DefaultRemoteAuthDataSource.kt index c1b0226..f66f588 100644 --- a/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/auth/remote/DefaultRemoteAuthDataSource.kt +++ b/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/auth/remote/DefaultRemoteAuthDataSource.kt @@ -1,19 +1,11 @@ package com.peto.droidmorning.data.datasource.auth.remote -import io.github.jan.supabase.auth.Auth -import io.github.jan.supabase.auth.providers.Google -import io.github.jan.supabase.auth.providers.builtin.IDToken +import com.peto.droidmorning.core.network.AuthClient class DefaultRemoteAuthDataSource( - private val auth: Auth, + private val authClient: AuthClient, ) : RemoteAuthDataSource { - override suspend fun signIn(oauthIdToken: String): String? { - auth.signInWith(IDToken) { - idToken = oauthIdToken - provider = Google - } - return auth.currentSessionOrNull()?.user?.id - } + override suspend fun signIn(oauthIdToken: String): String? = authClient.signInWithGoogleIdToken(oauthIdToken) - override suspend fun signOut() = auth.signOut() + override suspend fun signOut() = authClient.signOut() } diff --git a/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/exam/remote/DefaultRemoteExamDataSource.kt b/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/exam/remote/DefaultRemoteExamDataSource.kt index 6228b9f..8bc294a 100644 --- a/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/exam/remote/DefaultRemoteExamDataSource.kt +++ b/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/exam/remote/DefaultRemoteExamDataSource.kt @@ -1,58 +1,82 @@ package com.peto.droidmorning.data.datasource.exam.remote +import com.peto.droidmorning.core.network.AuthClient +import com.peto.droidmorning.core.network.PostgrestClient +import com.peto.droidmorning.core.network.PostgrestFilter +import com.peto.droidmorning.core.network.PostgrestOrder import com.peto.droidmorning.data.model.request.toRequest import com.peto.droidmorning.data.model.response.ExamDetailResponse import com.peto.droidmorning.data.model.response.ExamHistoryResponse import com.peto.droidmorning.domain.model.category.Category import com.peto.droidmorning.domain.model.exam.Exams -import io.github.jan.supabase.auth.Auth -import io.github.jan.supabase.postgrest.Postgrest -import io.github.jan.supabase.postgrest.query.Columns -import io.github.jan.supabase.postgrest.query.Order -import io.github.jan.supabase.postgrest.rpc +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.encodeToJsonElement +import kotlinx.serialization.json.jsonObject class DefaultRemoteExamDataSource( - private val postgrest: Postgrest, - private val auth: Auth, + private val postgrest: PostgrestClient, + private val authClient: AuthClient, ) : RemoteExamDataSource { + private val json = Json { ignoreUnknownKeys = true } + override suspend fun submitExam( exam: Exams, categories: List, ): Long = postgrest - .rpc(RPC_CREATE_EXAM, exam.toRequest(uid(), categories)) - .decodeAs() + .rpc( + function = RPC_CREATE_EXAM, + parameters = + json + .encodeToJsonElement(exam.toRequest(uid(), categories)) + .jsonObject, + ).let { data -> + json.decodeFromString(Long.serializer(), data) + } override suspend fun fetchExamHistory(): List = postgrest - .from(TABLE_EXAMS) - .select(Columns.ALL) { - filter { - eq(USER_ID_COLUMN, uid()) - } - order(UPDATED_AT_COLUMN, Order.DESCENDING) - }.decodeList() - - override suspend fun fetchExamDetail(examId: Long): List { - val params = mapOf(RPC_PARAM_EXAM_ID to examId) - return postgrest - .rpc(RPC_GET_EXAM_DETAIL, params) - .decodeList() - } + .select( + table = TABLE_EXAMS, + filters = listOf(PostgrestFilter(USER_ID_COLUMN, uid())), + order = PostgrestOrder(UPDATED_AT_COLUMN, descending = true), + ).let { data -> + json.decodeFromString( + ListSerializer(ExamHistoryResponse.serializer()), + data, + ) + } - override suspend fun deleteExam(examId: Long) { + override suspend fun fetchExamDetail(examId: Long): List = postgrest - .from(TABLE_EXAMS) - .delete { - filter { - eq(ID_COLUMN, examId) - eq(USER_ID_COLUMN, uid()) - } + .rpc( + function = RPC_GET_EXAM_DETAIL, + parameters = + json + .encodeToJsonElement(mapOf(RPC_PARAM_EXAM_ID to examId)) + .jsonObject, + ).let { data -> + json.decodeFromString( + ListSerializer(ExamDetailResponse.serializer()), + data, + ) } + + override suspend fun deleteExam(examId: Long) { + postgrest.delete( + table = TABLE_EXAMS, + filters = + listOf( + PostgrestFilter(ID_COLUMN, examId), + PostgrestFilter(USER_ID_COLUMN, uid()), + ), + ) } private fun uid(): String = - auth.currentSessionOrNull()?.user?.id + authClient.currentUserId() ?: throw IllegalStateException("User not logged in") companion object { diff --git a/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/DefaultRemoteQuestionDataSource.kt b/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/DefaultRemoteQuestionDataSource.kt index 6eb3256..62a0202 100644 --- a/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/DefaultRemoteQuestionDataSource.kt +++ b/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/DefaultRemoteQuestionDataSource.kt @@ -1,60 +1,90 @@ package com.peto.droidmorning.data.datasource.question.remote +import com.peto.droidmorning.core.network.AuthClient +import com.peto.droidmorning.core.network.PostgrestClient +import com.peto.droidmorning.core.network.PostgrestFilter import com.peto.droidmorning.data.model.request.ExamQuestionRequest import com.peto.droidmorning.data.model.request.LikeRequest import com.peto.droidmorning.data.model.response.CategoryCountResponse import com.peto.droidmorning.data.model.response.ExamQuestionResponse import com.peto.droidmorning.data.model.response.QuestionResponse -import io.github.jan.supabase.auth.Auth -import io.github.jan.supabase.postgrest.Postgrest -import io.github.jan.supabase.postgrest.rpc +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.encodeToJsonElement +import kotlinx.serialization.json.jsonObject class DefaultRemoteQuestionDataSource( - private val postgrest: Postgrest, - private val auth: Auth, + private val postgrest: PostgrestClient, + private val authClient: AuthClient, ) : RemoteQuestionDataSource { - override suspend fun fetchExamQuestions(): List { - val params = mapOf(RPC_FETCH_QUESTIONS_PARAM_NAME to uid()) - return postgrest - .rpc(RPC_FETCH_QUESTIONS, params) - .decodeList() - } + private val json = Json { ignoreUnknownKeys = true } + + override suspend fun fetchExamQuestions(): List = + postgrest + .rpc( + function = RPC_FETCH_QUESTIONS, + parameters = + json + .encodeToJsonElement(mapOf(RPC_FETCH_QUESTIONS_PARAM_NAME to uid())) + .jsonObject, + ).let { data -> + json.decodeFromString( + ListSerializer(QuestionResponse.serializer()), + data, + ) + } override suspend fun fetchExamQuestions( category: List, count: Int, - ): List { - val params = ExamQuestionRequest(category, count) - return postgrest - .rpc(RPC_GENERATE_EXAM_QUESTIONS, params) - .decodeList() - } + ): List = + postgrest + .rpc( + function = RPC_GENERATE_EXAM_QUESTIONS, + parameters = + json + .encodeToJsonElement(ExamQuestionRequest(category, count)) + .jsonObject, + ).let { data -> + json.decodeFromString( + ListSerializer(ExamQuestionResponse.serializer()), + data, + ) + } override suspend fun addLike(questionId: Long) { - val request = LikeRequest(uid(), questionId) - postgrest - .from(FAVORITES_TABLE) - .insert(request) + postgrest.insert( + table = FAVORITES_TABLE, + body = + json + .encodeToJsonElement(LikeRequest(uid(), questionId)) + .jsonObject, + ) } override suspend fun removeLike(questionId: Long) { - postgrest - .from(FAVORITES_TABLE) - .delete { - filter { - eq(USER_ID_COLUMN, uid()) - eq(QUESTION_ID_COLUMN, questionId) - } - } + postgrest.delete( + table = FAVORITES_TABLE, + filters = + listOf( + PostgrestFilter(USER_ID_COLUMN, uid()), + PostgrestFilter(QUESTION_ID_COLUMN, questionId), + ), + ) } override suspend fun fetchCategoryCount(): List = postgrest .rpc(RPC_CATEGORY_COUNT) - .decodeList() + .let { data -> + json.decodeFromString( + ListSerializer(CategoryCountResponse.serializer()), + data, + ) + } private fun uid(): String = - auth.currentSessionOrNull()?.user?.id + authClient.currentUserId() ?: throw IllegalStateException("User not logged in") companion object { diff --git a/data/src/commonMain/kotlin/com/peto/droidmorning/data/di/DataModule.kt b/data/src/commonMain/kotlin/com/peto/droidmorning/data/di/DataModule.kt index d43a8d0..859598b 100644 --- a/data/src/commonMain/kotlin/com/peto/droidmorning/data/di/DataModule.kt +++ b/data/src/commonMain/kotlin/com/peto/droidmorning/data/di/DataModule.kt @@ -5,9 +5,7 @@ import org.koin.dsl.module val dataModule = module { includes( - networkModule, dataSourceModule, - dataStoreModule, repositoryModule, ) } diff --git a/data/src/commonMain/kotlin/com/peto/droidmorning/data/di/DataStoreModule.kt b/data/src/commonMain/kotlin/com/peto/droidmorning/data/di/DataStoreModule.kt deleted file mode 100644 index 5c56c9c..0000000 --- a/data/src/commonMain/kotlin/com/peto/droidmorning/data/di/DataStoreModule.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.peto.droidmorning.data.di - -import com.peto.droidmorning.data.local.createTokenDataStore -import org.koin.dsl.module - -internal val dataStoreModule = - module { - single { createTokenDataStore() } - } diff --git a/data/src/commonMain/kotlin/com/peto/droidmorning/data/di/NetworkModule.kt b/data/src/commonMain/kotlin/com/peto/droidmorning/data/di/NetworkModule.kt deleted file mode 100644 index 4f3dba8..0000000 --- a/data/src/commonMain/kotlin/com/peto/droidmorning/data/di/NetworkModule.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.peto.droidmorning.data.di - -import com.peto.droidmorning.BuildKonfig -import com.peto.droidmorning.data.network.client.HttpClientFactory -import io.github.jan.supabase.SupabaseClient -import io.github.jan.supabase.auth.Auth -import io.github.jan.supabase.auth.auth -import io.github.jan.supabase.createSupabaseClient -import io.github.jan.supabase.postgrest.Postgrest -import io.github.jan.supabase.postgrest.postgrest -import io.ktor.client.HttpClient -import org.koin.dsl.module - -internal val networkModule = - module { - single { - HttpClientFactory.create(enableLogging = true) - } - - single { - createSupabaseClient( - supabaseUrl = BuildKonfig.SUPABASE_URL, - supabaseKey = BuildKonfig.SUPABASE_KEY, - ) { - install(Auth) - install(Postgrest) - } - } - - single { get().auth } - - single { get().postgrest } - } diff --git a/data/src/commonMain/kotlin/com/peto/droidmorning/data/network/client/HttpClientFactory.kt b/data/src/commonMain/kotlin/com/peto/droidmorning/data/network/client/HttpClientFactory.kt deleted file mode 100644 index 34cef91..0000000 --- a/data/src/commonMain/kotlin/com/peto/droidmorning/data/network/client/HttpClientFactory.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.peto.droidmorning.data.network.client - -import io.ktor.client.HttpClient -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.plugins.defaultRequest -import io.ktor.client.plugins.logging.DEFAULT -import io.ktor.client.plugins.logging.LogLevel -import io.ktor.client.plugins.logging.Logger -import io.ktor.client.plugins.logging.Logging -import io.ktor.client.request.headers -import io.ktor.serialization.kotlinx.json.json -import kotlinx.serialization.json.Json - -object HttpClientFactory { - fun create(enableLogging: Boolean = false): HttpClient = - HttpClient { - install(ContentNegotiation) { - json( - Json { prettyPrint = true }, - ) - } - - if (enableLogging) { - install(Logging) { - logger = Logger.DEFAULT - level = LogLevel.HEADERS - } - } - - defaultRequest { - url("https://api.example.com/") - headers { - append("Content-Type", "application/json") - } - } - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts index 1522ace..1d916f7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,3 +33,10 @@ include(":composeApp") include(":domain") include(":data") include(":designsystem") +include(":core") +include(":core:network") +include(":core:datastore") + +project(":core").projectDir = file("core") +project(":core:network").projectDir = file("core/network") +project(":core:datastore").projectDir = file("core/datastore")