Feature/domain (#95)

* feature: Move analytics

* feature: remote dashboard

* fix: DI

* feature: Dashboard

* fix: DI

* feature: Move files

* feature: Move database

* feature: Move files

* feature: Move

* feature: Move

* feature: Move files

* feature: Move files

* feature: Move

* feature: Move images

* feature: Move sharedprefe

* fix: Merge

* feature: Network

* fix: Network

* feature: Move files

* feature: Move

* fix: DI

* fix: DI

* fix: Discussion

* fix: Discussion

* fix: Delete

* fix: Comment

* fix: Discussion

* fix: Build

* fix textfield

* fix textfield

---------

Co-authored-by: TEYSSANDIER Raphael <rteyssandier@sephora.fr>
Co-authored-by: Florent Champigny <florent@bere.al>
This commit is contained in:
Raphael Teyssandier 2025-08-15 15:20:19 +02:00 committed by GitHub
parent d22bd8642f
commit 08698acf25
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
298 changed files with 2131 additions and 1976 deletions

View file

@ -1,20 +1,33 @@
package com.flocon.data.remote
import com.flocon.data.remote.analytics.analyticsModule
import com.flocon.data.remote.dashboard.dashboardModule
import com.flocon.data.remote.database.databaseModule
import com.flocon.data.remote.deeplink.deeplinkModule
import com.flocon.data.remote.files.filesModule
import com.flocon.data.remote.messages.messagesModule
import com.flocon.data.remote.network.networkModule
import com.flocon.data.remote.sharedpreference.sharedPreferencesModule
import com.flocon.data.remote.table.tableModule
import kotlinx.serialization.json.Json
import org.koin.dsl.module
val dataRemoteModule = module {
includes(
analyticsModule,
dashboardModule,
databaseModule,
deeplinkModule,
filesModule,
messagesModule,
networkModule,
sharedPreferencesModule,
tableModule,
tableModule
)
single {
Json {
ignoreUnknownKeys = true
}
}
}

View file

@ -1,149 +0,0 @@
@file:Suppress("ktlint")
package com.flocon.data.remote
object Protocol {
object FromDevice {
object Analytics {
const val Plugin = "Analytics"
object Method {
const val AddItems = "AddItems"
}
}
object Dashboard {
const val Plugin = "Dashboard"
object Method {
const val Update = "update"
}
}
object Database {
const val Plugin = "database"
object Method {
const val Query = "query"
const val GetDatabases = "getDatabases"
}
}
object Deeplink {
const val Plugin = "Deeplink"
object Method {
const val GetDeeplinks = "GetDeeplinks"
}
}
object Files {
const val Plugin = "files"
object Method {
const val ListFiles = "listFiles"
}
}
object Images {
const val Plugin = "images"
object Method {
const val LogNetworkImage = "logNetworkCall"
}
}
object Network {
const val Plugin = "network"
object Method {
const val LogNetworkCall = "logNetworkCall"
const val LogNetworkCallRequest = "logNetworkCallRequest"
const val LogNetworkCallResponse = "logNetworkCallResponse"
}
}
object SharedPreferences {
const val Plugin = "sharedPreferences"
object Method {
const val GetSharedPreferences = "getSharedPreferences"
const val GetSharedPreferenceValue = "getSharedPreferenceValue"
}
}
object Table {
const val Plugin = "Table"
object Method {
const val AddItems = "AddItems"
}
}
}
object ToDevice {
object Analytics {
const val Plugin = "Analytics"
object Method {
const val ClearItems = "ClearItems"
}
}
object Dashboard {
const val Plugin = "Dashboard"
object Method {
const val OnClick = "onClick"
const val OnTextFieldSubmitted = "onTextFieldSubmitted"
const val OnCheckBoxValueChanged = "onCheckBoxValueChanged"
}
}
object Database {
const val Plugin = "database"
object Method {
const val Query = "query"
const val GetDatabases = "getDatabases"
}
}
object Files {
const val Plugin = "files"
object Method {
const val ListFiles = "listFiles"
const val DeleteFile = "deleteFile"
const val DeleteFolderContent = "deleteFolderContent"
}
}
object Network {
const val Plugin = "network"
object Method {
const val SetupMocks = "setupMocks"
}
}
object SharedPreferences {
const val Plugin = "sharedPreferences"
object Method {
const val GetSharedPreferences = "getSharedPreferences"
const val GetSharedPreferenceValue = "getSharedPreferenceValue"
const val SetSharedPreferenceValue = "setSharedPreferenceValue"
}
}
object Table {
const val Plugin = "Table"
object Method {
const val ClearItems = "ClearItems"
}
}
}
}

View file

@ -1,16 +1,22 @@
package com.flocon.data.remote.analytics.datasource
import com.flocon.data.remote.Protocol
import com.flocon.data.remote.analytics.mapper.toDomain
import com.flocon.data.remote.analytics.model.AnalyticsItemDataModel
import com.flocon.data.remote.models.FloconOutgoingMessageDataModel
import com.flocon.data.remote.models.toRemote
import com.flocon.data.remote.server.Server
import io.github.openflocon.data.core.analytics.datasource.AnalyticsRemoteDataSource
import io.github.openflocon.domain.Protocol
import io.github.openflocon.domain.analytics.models.AnalyticsItemDomainModel
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import io.github.openflocon.domain.messages.models.FloconIncomingMessageDomainModel
import kotlinx.serialization.json.Json
class AnalyticsRemoteDataSourceImpl(
private val server: Server,
private val json: Json
) : AnalyticsRemoteDataSource {
override suspend fun clearReceivedItem(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, items: List<String>) {
server.sendMessageToClient(
deviceIdAndPackageName = deviceIdAndPackageName.toRemote(),
@ -21,4 +27,20 @@ class AnalyticsRemoteDataSourceImpl(
),
)
}
override fun getItems(message: FloconIncomingMessageDomainModel): List<AnalyticsItemDomainModel> {
return decodeAddItems(message).takeIf { it.isNotEmpty() }
?.let { list -> list.map { toDomain(it) } }
?.takeIf { it.isNotEmpty() }
.orEmpty()
}
private fun decodeAddItems(message: FloconIncomingMessageDomainModel): List<AnalyticsItemDataModel> = try {
json.decodeFromString<List<AnalyticsItemDataModel>>(message.body)
} catch (t: Throwable) {
t.printStackTrace()
emptyList()
}
}

View file

@ -0,0 +1,18 @@
package com.flocon.data.remote.analytics.mapper
import com.flocon.data.remote.analytics.model.AnalyticsItemDataModel
import io.github.openflocon.domain.analytics.models.AnalyticsItemDomainModel
import io.github.openflocon.domain.analytics.models.AnalyticsPropertyDomainModel
internal fun toDomain(dataModel: AnalyticsItemDataModel) = AnalyticsItemDomainModel(
analyticsTableId = dataModel.analyticsTableId,
itemId = dataModel.id,
createdAt = dataModel.createdAt,
eventName = dataModel.eventName,
properties = dataModel.properties?.map {
AnalyticsPropertyDomainModel(
name = it.name,
value = it.value,
)
} ?: emptyList(),
)

View file

@ -0,0 +1,12 @@
package com.flocon.data.remote.analytics.model
import kotlinx.serialization.Serializable
@Serializable
data class AnalyticsItemDataModel(
val id: String,
val analyticsTableId: String,
val eventName: String,
val createdAt: Long,
val properties: List<AnalyticsPropertyDataModel>? = emptyList(),
)

View file

@ -0,0 +1,9 @@
package com.flocon.data.remote.analytics.model
import kotlinx.serialization.Serializable
@Serializable
data class AnalyticsPropertyDataModel(
val name: String,
val value: String,
)

View file

@ -0,0 +1,9 @@
package com.flocon.data.remote.common
import kotlinx.serialization.json.Json
internal inline fun <reified T> Json.safeDecodeFromString(data: String): T? = try {
decodeFromString(data)
} catch (e: Throwable) {
null
}

View file

@ -0,0 +1,11 @@
package com.flocon.data.remote.dashboard
import com.flocon.data.remote.dashboard.datasource.ToDeviceDashboardDataSourceImpl
import io.github.openflocon.data.core.dashboard.datasource.ToDeviceDashboardDataSource
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.bind
import org.koin.dsl.module
internal val dashboardModule = module {
singleOf(::ToDeviceDashboardDataSourceImpl) bind ToDeviceDashboardDataSource::class
}

View file

@ -0,0 +1,91 @@
package com.flocon.data.remote.dashboard.datasource
import com.flocon.data.remote.dashboard.mapper.toDomain
import com.flocon.data.remote.dashboard.models.DashboardConfigDataModel
import com.flocon.data.remote.dashboard.models.ToDeviceCheckBoxValueChangedMessage
import com.flocon.data.remote.dashboard.models.ToDeviceSubmittedTextFieldMessage
import com.flocon.data.remote.models.FloconOutgoingMessageDataModel
import com.flocon.data.remote.models.toRemote
import com.flocon.data.remote.server.Server
import io.github.openflocon.data.core.dashboard.datasource.ToDeviceDashboardDataSource
import io.github.openflocon.domain.Protocol
import io.github.openflocon.domain.dashboard.models.DashboardDomainModel
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import io.github.openflocon.domain.messages.models.FloconIncomingMessageDomainModel
import kotlinx.serialization.json.Json
import kotlin.uuid.ExperimentalUuidApi
class ToDeviceDashboardDataSourceImpl(
private val server: Server,
private val json: Json
) : ToDeviceDashboardDataSource {
@OptIn(ExperimentalUuidApi::class)
override suspend fun sendClickEvent(
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
buttonId: String,
) {
server.sendMessageToClient(
deviceIdAndPackageName = deviceIdAndPackageName.toRemote(),
message = FloconOutgoingMessageDataModel(
plugin = Protocol.ToDevice.Dashboard.Plugin,
method = Protocol.ToDevice.Dashboard.Method.OnClick,
body = buttonId,
),
)
}
@OptIn(ExperimentalUuidApi::class)
override suspend fun submitTextFieldEvent(
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
textFieldId: String,
value: String,
) {
server.sendMessageToClient(
deviceIdAndPackageName = deviceIdAndPackageName.toRemote(),
message = FloconOutgoingMessageDataModel(
plugin = Protocol.ToDevice.Dashboard.Plugin,
method = Protocol.ToDevice.Dashboard.Method.OnTextFieldSubmitted,
body = json.encodeToString(
ToDeviceSubmittedTextFieldMessage(
id = textFieldId,
value = value,
),
),
),
)
}
@OptIn(ExperimentalUuidApi::class)
override suspend fun sendUpdateCheckBoxEvent(
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
checkBoxId: String,
value: Boolean,
) {
server.sendMessageToClient(
deviceIdAndPackageName = deviceIdAndPackageName.toRemote(),
message = FloconOutgoingMessageDataModel(
plugin = Protocol.ToDevice.Dashboard.Plugin,
method = Protocol.ToDevice.Dashboard.Method.OnCheckBoxValueChanged,
body = json.encodeToString(
ToDeviceCheckBoxValueChangedMessage(
id = checkBoxId,
value = value,
),
),
),
)
}
override fun getItem(message: FloconIncomingMessageDomainModel): DashboardDomainModel? {
return decode(message)?.let { toDomain(it) }
}
private fun decode(message: FloconIncomingMessageDomainModel): DashboardConfigDataModel? = try {
json.decodeFromString<DashboardConfigDataModel>(message.body)
} catch (t: Throwable) {
t.printStackTrace()
null
}
}

View file

@ -0,0 +1,64 @@
package com.flocon.data.remote.dashboard.mapper
import com.flocon.data.remote.dashboard.models.ButtonConfigDataModel
import com.flocon.data.remote.dashboard.models.CheckBoxConfigDataModel
import com.flocon.data.remote.dashboard.models.DashboardConfigDataModel
import com.flocon.data.remote.dashboard.models.DashboardElementDataModel
import com.flocon.data.remote.dashboard.models.SectionConfigDataModel
import com.flocon.data.remote.dashboard.models.TextConfigDataModel
import com.flocon.data.remote.dashboard.models.TextFieldConfigDataModel
import io.github.openflocon.domain.dashboard.models.DashboardDomainModel
import io.github.openflocon.domain.dashboard.models.DashboardElementDomainModel
import io.github.openflocon.domain.dashboard.models.DashboardSectionDomainModel
fun toDomain(model: DashboardConfigDataModel): DashboardDomainModel = DashboardDomainModel(
dashboardId = model.dashboardId,
sections = model.sections.map {
toDomain(it)
},
)
fun toDomain(model: SectionConfigDataModel): DashboardSectionDomainModel = DashboardSectionDomainModel(
name = model.name,
elements = model.elements.mapNotNull { toDomain(it) },
)
fun toDomain(model: DashboardElementDataModel): DashboardElementDomainModel? {
model.text?.let {
return toDomain(it)
}
model.button?.let {
return toDomain(it)
}
model.textField?.let {
return toDomain(it)
}
model.checkBox?.let {
return toDomain(it)
}
return null
}
fun toDomain(model: ButtonConfigDataModel): DashboardElementDomainModel.Button = DashboardElementDomainModel.Button(
text = model.text,
id = model.id,
)
fun toDomain(model: TextConfigDataModel): DashboardElementDomainModel.Text = DashboardElementDomainModel.Text(
label = model.label,
value = model.value,
color = model.color,
)
fun toDomain(model: TextFieldConfigDataModel): DashboardElementDomainModel.TextField = DashboardElementDomainModel.TextField(
value = model.value,
label = model.label,
placeHolder = model.placeHolder,
id = model.id,
)
fun toDomain(model: CheckBoxConfigDataModel): DashboardElementDomainModel.CheckBox = DashboardElementDomainModel.CheckBox(
value = model.value,
label = model.label,
id = model.id,
)

View file

@ -0,0 +1,59 @@
package com.flocon.data.remote.dashboard.models
import kotlinx.serialization.Serializable
@Serializable
data class DashboardConfigDataModel(
val dashboardId: String,
val sections: List<SectionConfigDataModel>,
)
@Serializable
data class SectionConfigDataModel(
val name: String,
val elements: List<DashboardElementDataModel>,
)
@Serializable
data class DashboardElementDataModel(
val button: ButtonConfigDataModel? = null,
val text: TextConfigDataModel? = null,
val plainText: PlainTextConfigDataModel? = null,
val textField: TextFieldConfigDataModel? = null,
val checkBox: CheckBoxConfigDataModel? = null,
)
@Serializable
data class ButtonConfigDataModel(
val text: String,
val id: String,
)
@Serializable
data class TextFieldConfigDataModel(
val label: String,
val placeHolder: String? = null,
val value: String,
val id: String,
)
@Serializable
data class CheckBoxConfigDataModel(
val label: String,
val value: Boolean,
val id: String,
)
@Serializable
data class TextConfigDataModel(
val label: String,
val value: String,
val color: Int? = null,
)
@Serializable
data class PlainTextConfigDataModel(
val label: String,
val value: String,
val type: String,
)

View file

@ -0,0 +1,9 @@
package com.flocon.data.remote.dashboard.models
import kotlinx.serialization.Serializable
@Serializable
data class ToDeviceCheckBoxValueChangedMessage(
val id: String,
val value: Boolean,
)

View file

@ -0,0 +1,9 @@
package com.flocon.data.remote.dashboard.models
import kotlinx.serialization.Serializable
@Serializable
data class ToDeviceSubmittedTextFieldMessage(
val id: String,
val value: String,
)

View file

@ -1,6 +1,6 @@
package com.flocon.data.remote.database.datasource
import com.flocon.data.remote.Protocol
import io.github.openflocon.domain.Protocol
import com.flocon.data.remote.models.FloconOutgoingMessageDataModel
import com.flocon.data.remote.models.toRemote
import com.flocon.data.remote.server.Server

View file

@ -1,21 +1,26 @@
package com.flocon.data.remote.database.datasource
import com.flocon.data.remote.Protocol
import com.flocon.data.remote.database.mapper.decodeDeviceDatabases
import com.flocon.data.remote.database.mapper.decodeReceivedQuery
import com.flocon.data.remote.database.models.DatabaseExecuteSqlResponseDataModel
import com.flocon.data.remote.database.models.DatabaseOutgoingQueryMessage
import com.flocon.data.remote.database.models.ResponseAndRequestIdDataModel
import com.flocon.data.remote.database.models.toDeviceDatabasesDomain
import com.flocon.data.remote.models.FloconOutgoingMessageDataModel
import com.flocon.data.remote.models.toRemote
import com.flocon.data.remote.server.Server
import com.flocon.data.remote.server.newRequestId
import io.github.openflocon.data.core.database.datasource.QueryDatabaseRemoteDataSource
import io.github.openflocon.domain.Protocol
import io.github.openflocon.domain.common.Either
import io.github.openflocon.domain.common.Failure
import io.github.openflocon.domain.common.Success
import io.github.openflocon.domain.database.models.DatabaseExecuteSqlResponseDomainModel
import io.github.openflocon.domain.database.models.DeviceDataBaseDomainModel
import io.github.openflocon.domain.database.models.DeviceDataBaseId
import io.github.openflocon.domain.database.models.ResponseAndRequestIdDomainModel
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import io.github.openflocon.domain.messages.models.FloconIncomingMessageDomainModel
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
@ -27,6 +32,7 @@ import kotlin.uuid.ExperimentalUuidApi
class QueryDatabaseRemoteDataSourceImpl(
private val server: Server,
private val json: Json
) : QueryDatabaseRemoteDataSource {
private val queryResultReceived = MutableStateFlow<Set<ResponseAndRequestIdDomainModel>>(emptySet())
@ -77,9 +83,17 @@ class QueryDatabaseRemoteDataSourceImpl(
return Failure(e)
}
}
override fun getDeviceDatabases(message: FloconIncomingMessageDomainModel): List<DeviceDataBaseDomainModel> {
return toDeviceDatabasesDomain(json.decodeDeviceDatabases(message.body).orEmpty())
}
override fun getReceiveQuery(message: FloconIncomingMessageDomainModel): ResponseAndRequestIdDomainModel? {
return json.decodeReceivedQuery(message.body)?.toDomain()
}
}
// TODO Move
// TODO internal
fun ResponseAndRequestIdDataModel.toDomain() = ResponseAndRequestIdDomainModel(
requestId = requestId,
response = response.toDomain(),

View file

@ -0,0 +1,43 @@
package com.flocon.data.remote.database.mapper
import com.flocon.data.remote.database.models.DatabaseExecuteSqlResponseDataModel
import com.flocon.data.remote.database.models.DeviceDataBaseDataModel
import com.flocon.data.remote.database.models.QueryResultReceivedDataModel
import com.flocon.data.remote.database.models.ResponseAndRequestIdDataModel
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@Serializable
data class ReceivedQueryWrapper(
val type: String,
val body: String,
)
internal fun Json.decodeReceivedQuery(body: String): ResponseAndRequestIdDataModel? = try {
val result = decodeFromString<QueryResultReceivedDataModel>(body)
val queryWrapper = decodeFromString<ReceivedQueryWrapper>(result.result)
when (queryWrapper.type) {
"Error" -> decodeFromString<DatabaseExecuteSqlResponseDataModel.Error>(queryWrapper.body)
"Insert" -> decodeFromString<DatabaseExecuteSqlResponseDataModel.Insert>(queryWrapper.body)
"RawSuccess" -> decodeFromString<DatabaseExecuteSqlResponseDataModel.RawSuccess>(queryWrapper.body)
"Select" -> decodeFromString<DatabaseExecuteSqlResponseDataModel.Select>(queryWrapper.body)
"UpdateDelete" -> decodeFromString<DatabaseExecuteSqlResponseDataModel.UpdateDelete>(queryWrapper.body)
else -> null
}?.let {
ResponseAndRequestIdDataModel(
requestId = result.requestId,
response = it,
)
}
} catch (t: Throwable) {
t.printStackTrace()
null
}
internal fun Json.decodeDeviceDatabases(body: String): List<DeviceDataBaseDataModel>? = try {
decodeFromString<List<DeviceDataBaseDataModel>>(body)
} catch (t: Throwable) {
t.printStackTrace()
null
}

View file

@ -0,0 +1,11 @@
package com.flocon.data.remote.deeplink
import com.flocon.data.remote.deeplink.datasource.DeeplinkRemoteDataSourceImpl
import io.github.openflocon.data.core.deeplink.datasource.DeeplinkRemoteDataSource
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.bind
import org.koin.dsl.module
internal val deeplinkModule = module {
singleOf(::DeeplinkRemoteDataSourceImpl) bind DeeplinkRemoteDataSource::class
}

View file

@ -0,0 +1,21 @@
package com.flocon.data.remote.deeplink.datasource
import com.flocon.data.remote.common.safeDecodeFromString
import com.flocon.data.remote.deeplink.models.DeeplinksReceivedDataModel
import com.flocon.data.remote.deeplink.models.toDomain
import io.github.openflocon.data.core.deeplink.datasource.DeeplinkRemoteDataSource
import io.github.openflocon.domain.deeplink.models.DeeplinkDomainModel
import io.github.openflocon.domain.messages.models.FloconIncomingMessageDomainModel
import kotlinx.serialization.json.Json
internal class DeeplinkRemoteDataSourceImpl(
private val json: Json
) : DeeplinkRemoteDataSource {
override fun getItems(message: FloconIncomingMessageDomainModel): List<DeeplinkDomainModel> {
return json.safeDecodeFromString<DeeplinksReceivedDataModel>(message.body)
?.toDomain()
.orEmpty()
}
}

View file

@ -0,0 +1,10 @@
package com.flocon.data.remote.deeplink.models
import kotlinx.serialization.Serializable
@Serializable
internal data class DeeplinkReceivedDataModel(
val label: String? = null,
val link: String,
val description: String? = null,
)

View file

@ -0,0 +1,17 @@
package com.flocon.data.remote.deeplink.models
import io.github.openflocon.domain.deeplink.models.DeeplinkDomainModel
import kotlinx.serialization.Serializable
@Serializable
internal data class DeeplinksReceivedDataModel(
val deeplinks: List<DeeplinkReceivedDataModel>,
)
internal fun DeeplinksReceivedDataModel.toDomain(): List<DeeplinkDomainModel> = deeplinks.map {
DeeplinkDomainModel(
label = it.label,
link = it.link,
description = it.description,
)
}

View file

@ -1,14 +1,17 @@
package com.flocon.data.remote.files.datasource
import com.flocon.data.remote.Protocol
import com.flocon.data.remote.common.safeDecodeFromString
import com.flocon.data.remote.files.models.FromDeviceFilesResultDataModel
import com.flocon.data.remote.files.models.ToDeviceDeleteFileMessage
import com.flocon.data.remote.files.models.ToDeviceDeleteFolderContentMessage
import com.flocon.data.remote.files.models.ToDeviceGetFilesMessage
import com.flocon.data.remote.files.models.toDomain
import com.flocon.data.remote.models.FloconOutgoingMessageDataModel
import com.flocon.data.remote.models.toRemote
import com.flocon.data.remote.server.Server
import com.flocon.data.remote.server.newRequestId
import io.github.openflocon.data.core.files.datasource.FilesRemoteDataSource
import io.github.openflocon.domain.Protocol
import io.github.openflocon.domain.common.Either
import io.github.openflocon.domain.common.Failure
import io.github.openflocon.domain.common.Success
@ -16,6 +19,7 @@ import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainMod
import io.github.openflocon.domain.files.models.FileDomainModel
import io.github.openflocon.domain.files.models.FilePathDomainModel
import io.github.openflocon.domain.files.models.FromDeviceFilesResultDomainModel
import io.github.openflocon.domain.messages.models.FloconIncomingMessageDomainModel
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
@ -28,6 +32,7 @@ import kotlin.uuid.ExperimentalUuidApi
class FilesRemoteDataSourceImpl(
private val server: Server,
private val json: Json
) : FilesRemoteDataSource {
private val getFilesResultReceived =
MutableStateFlow<Set<FromDeviceFilesResultDomainModel>>(emptySet())
@ -144,6 +149,11 @@ class FilesRemoteDataSourceImpl(
return waitForResult(requestId)
}
override fun getItems(message: FloconIncomingMessageDomainModel): FromDeviceFilesResultDomainModel? {
return json.safeDecodeFromString<FromDeviceFilesResultDataModel>(message.body)
?.toDomain()
}
private suspend fun waitForResult(requestId: String): Either<Exception, List<FileDomainModel>> {
try {
val result = withTimeout(3_000) {
@ -172,7 +182,7 @@ class FilesRemoteDataSourceImpl(
isDirectory = it.isDirectory,
path = FilePathDomainModel.Real(it.path),
size = it.size,
lastModified = Instant.Companion.fromEpochMilliseconds(it.lastModified),
lastModified = Instant.fromEpochMilliseconds(it.lastModified),
)
}
}

View file

@ -1,20 +0,0 @@
package com.flocon.data.remote.files.mapper
import com.flocon.data.remote.files.models.FromDeviceFilesDataModel
import com.flocon.data.remote.files.models.FromDeviceFilesResultDataModel
import io.github.openflocon.domain.files.models.FromDeviceFilesDomainModel
import io.github.openflocon.domain.files.models.FromDeviceFilesResultDomainModel
// TODO INTERNAL
fun FromDeviceFilesResultDataModel.toDomain() = FromDeviceFilesResultDomainModel(
requestId = requestId,
files = files.map(FromDeviceFilesDataModel::toDomain),
)
fun FromDeviceFilesDataModel.toDomain() = FromDeviceFilesDomainModel(
isDirectory = isDirectory,
lastModified = lastModified,
name = name,
size = size,
path = path,
)

View file

@ -1,12 +1,21 @@
package com.flocon.data.remote.files.models
import io.github.openflocon.domain.files.models.FromDeviceFilesDomainModel
import kotlinx.serialization.Serializable
@Serializable
data class FromDeviceFilesDataModel(
internal data class FromDeviceFilesDataModel(
val name: String,
val isDirectory: Boolean,
val path: String,
val size: Long,
val lastModified: Long,
)
internal fun FromDeviceFilesDataModel.toDomain() = FromDeviceFilesDomainModel(
name = name,
isDirectory = isDirectory,
path = path,
size = size,
lastModified = lastModified
)

View file

@ -1,9 +1,15 @@
package com.flocon.data.remote.files.models
import io.github.openflocon.domain.files.models.FromDeviceFilesResultDomainModel
import kotlinx.serialization.Serializable
@Serializable
data class FromDeviceFilesResultDataModel(
internal data class FromDeviceFilesResultDataModel(
val requestId: String,
val files: List<FromDeviceFilesDataModel>,
)
internal fun FromDeviceFilesResultDataModel.toDomain() = FromDeviceFilesResultDomainModel(
requestId = requestId,
files = files.map(FromDeviceFilesDataModel::toDomain)
)

View file

@ -0,0 +1,14 @@
package com.flocon.data.remote.messages
import com.flocon.data.remote.messages.datasource.MessageRemoteDataSourceImpl
import com.flocon.data.remote.server.Server
import com.flocon.data.remote.server.getServer
import io.github.openflocon.data.core.messages.datasource.MessageRemoteDataSource
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.bind
import org.koin.dsl.module
internal val messagesModule = module {
single<Server> { getServer() }
singleOf(::MessageRemoteDataSourceImpl) bind MessageRemoteDataSource::class
}

View file

@ -0,0 +1,23 @@
package com.flocon.data.remote.messages.datasource
import com.flocon.data.remote.messages.mapper.toDomain
import com.flocon.data.remote.models.FloconIncomingMessageDataModel
import com.flocon.data.remote.server.Server
import io.github.openflocon.data.core.messages.datasource.MessageRemoteDataSource
import io.github.openflocon.domain.messages.models.FloconIncomingMessageDomainModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
internal class MessageRemoteDataSourceImpl(
private val server: Server
) : MessageRemoteDataSource {
override fun startServer() {
server.start()
}
override fun listenMessages(): Flow<FloconIncomingMessageDomainModel> {
return server.receivedMessages
.map(FloconIncomingMessageDataModel::toDomain)
}
}

View file

@ -0,0 +1,14 @@
package com.flocon.data.remote.messages.mapper
import com.flocon.data.remote.models.FloconIncomingMessageDataModel
import io.github.openflocon.domain.messages.models.FloconIncomingMessageDomainModel
internal fun FloconIncomingMessageDataModel.toDomain() = FloconIncomingMessageDomainModel(
deviceName = deviceName,
deviceId = deviceId,
appName = appName,
appPackageName = appPackageName,
method = method,
body = body,
plugin = plugin
)

View file

@ -1,17 +1,28 @@
package com.flocon.data.remote.network.datasource
import com.flocon.data.remote.Protocol
import com.flocon.data.remote.common.safeDecodeFromString
import com.flocon.data.remote.models.FloconOutgoingMessageDataModel
import com.flocon.data.remote.models.toRemote
import com.flocon.data.remote.network.mapper.listToRemote
import com.flocon.data.remote.network.models.FloconNetworkCallIdDataModel
import com.flocon.data.remote.network.models.FloconNetworkRequestDataModel
import com.flocon.data.remote.network.models.FloconNetworkResponseDataModel
import com.flocon.data.remote.network.models.toDomain
import com.flocon.data.remote.server.Server
import io.github.openflocon.data.core.network.datasource.NetworkRemoteDataSource
import io.github.openflocon.domain.Protocol
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import io.github.openflocon.domain.messages.models.FloconIncomingMessageDomainModel
import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel
import io.github.openflocon.domain.network.models.FloconNetworkCallIdDomainModel
import io.github.openflocon.domain.network.models.FloconNetworkResponseDomainModel
import io.github.openflocon.domain.network.models.FloconNetworkResponseOnlyDomainModel
import io.github.openflocon.domain.network.models.MockNetworkDomainModel
import kotlinx.serialization.json.Json
class NetworkRemoteDataSourceImpl(
private val server: Server,
private val json: Json
) : NetworkRemoteDataSource {
override suspend fun setupMocks(
@ -29,4 +40,19 @@ class NetworkRemoteDataSourceImpl(
),
)
}
override fun getRequestData(message: FloconIncomingMessageDomainModel): FloconNetworkCallDomainModel? {
return json.safeDecodeFromString<FloconNetworkRequestDataModel>(message.body)
?.let { com.flocon.data.remote.network.mapper.toDomain(it) }
}
override fun getCallId(message: FloconIncomingMessageDomainModel): FloconNetworkCallIdDomainModel? {
return json.safeDecodeFromString<FloconNetworkCallIdDataModel>(message.body)
?.let(FloconNetworkCallIdDataModel::toDomain)
}
override fun getResponseData(message: FloconIncomingMessageDomainModel): FloconNetworkResponseOnlyDomainModel? {
return json.safeDecodeFromString<FloconNetworkResponseDataModel>(message.body)
?.let(FloconNetworkResponseDataModel::toDomain)
}
}

View file

@ -1,7 +1,15 @@
package com.flocon.data.remote.network.mapper
import com.flocon.data.remote.network.models.FloconNetworkRequestDataModel
import com.flocon.data.remote.network.models.MockNetworkResponseDataModel
import io.github.openflocon.data.core.network.graphql.model.GraphQlExtracted
import io.github.openflocon.data.core.network.graphql.model.GraphQlRequestBody
import io.github.openflocon.data.core.network.graphql.model.GraphQlResponseBody
import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel
import io.github.openflocon.domain.network.models.FloconNetworkRequestDomainModel
import io.github.openflocon.domain.network.models.MockNetworkDomainModel
import kotlinx.serialization.json.Json
import kotlin.uuid.ExperimentalUuidApi
fun listToRemote(mocks: List<MockNetworkDomainModel>): List<MockNetworkResponseDataModel> = mocks.map { toRemote(it) }
@ -18,3 +26,99 @@ fun toRemote(mock: MockNetworkDomainModel): MockNetworkResponseDataModel = MockN
headers = mock.response.headers,
),
)
@OptIn(ExperimentalUuidApi::class)
fun toDomain(decoded: FloconNetworkRequestDataModel): FloconNetworkCallDomainModel? = try {
val graphQl = extractGraphQl(decoded)
val callId = decoded.floconCallId!!
val networkRequest = FloconNetworkRequestDomainModel(
url = decoded.url!!,
startTime = decoded.startTime!!,
method = decoded.method!!,
headers = decoded.requestHeaders!!,
body = decoded.requestBody,
byteSize = decoded.requestSize ?: 0L,
isMocked = decoded.isMocked ?: false,
)
when {
graphQl != null -> FloconNetworkCallDomainModel.GraphQl(
callId = callId,
request = FloconNetworkCallDomainModel.GraphQl.Request(
query = graphQl.request.queryName ?: "anonymous",
operationType = graphQl.request.operationType,
networkRequest = networkRequest,
),
response = null,
)
decoded.floconNetworkType == "grpc" -> FloconNetworkCallDomainModel.Grpc(
callId = callId,
networkRequest = networkRequest,
response = null,
)
// decoded.floconNetworkType == "http"
else -> {
FloconNetworkCallDomainModel.Http(
callId = callId,
networkRequest = networkRequest,
response = null,
)
}
}
} catch (t: Throwable) {
t.printStackTrace()
null
}
private val graphQlParser = Json {
ignoreUnknownKeys = true
}
// maybe use graphql-java
fun extractGraphQl(decoded: FloconNetworkRequestDataModel): GraphQlExtracted? {
val request = decoded.requestBody?.let {
try {
val requestBody = graphQlParser.decodeFromString<GraphQlRequestBody>(it)
val queryName = extractOperationName(requestBody.query)
val operationType = extractOperationType(requestBody.query)
GraphQlExtracted.Request(
requestBody = requestBody,
queryName = queryName,
operationType = operationType ?: return null,
)
} catch (t: Throwable) {
null
}
}
return if (request != null) {
GraphQlExtracted(
request = request,
)
} else null
}
fun extractOperationType(query: String): String? {
val regex = Regex("""\b(query|mutation|subscription)\b""")
val matchResult = regex.find(query.trim())
return matchResult?.value
}
private fun extractOperationName(query: String): String? {
val regex = Regex("""\b(query|mutation|subscription)\s+(\w+)""")
val matchResult = regex.find(query.trim())
return matchResult?.groups?.get(2)?.value
}
fun computeIsGraphQlSuccess(
responseHttpCode: Int,
response: GraphQlResponseBody?,
): Boolean {
if (responseHttpCode !in 200..299) return false
return response?.errors?.takeUnless { it.isEmpty() } == null
}

View file

@ -0,0 +1,76 @@
package com.flocon.data.remote.network.models
import io.github.openflocon.domain.network.models.FloconNetworkCallIdDomainModel
import io.github.openflocon.domain.network.models.FloconNetworkRequestDomainModel
import io.github.openflocon.domain.network.models.FloconNetworkResponseDomainModel
import io.github.openflocon.domain.network.models.FloconNetworkResponseOnlyDomainModel
import kotlinx.serialization.Serializable
@Serializable
data class FloconNetworkRequestDataModel(
val floconCallId: String? = null,
val floconNetworkType: String? = null,
val url: String? = null,
val method: String? = null,
val startTime: Long? = null,
// request
val requestHeaders: Map<String, String>? = null,
val requestBody: String? = null,
val requestSize: Long? = null,
val isMocked: Boolean? = null,
)
@Serializable
data class FloconNetworkCallIdDataModel(
val floconCallId: String? = null,
)
@Serializable
data class FloconNetworkResponseDataModel(
val floconCallId: String? = null,
val floconNetworkType: String? = null,
val durationMs: Double? = null,
val responseHttpCode: Int? = null, // ex: 200
val responseContentType: String? = null,
val responseBody: String? = null,
val responseHeaders: Map<String, String>? = null,
val responseSize: Long? = null,
val responseGrpcStatus: String? = null,
)
internal fun FloconNetworkResponseDataModel.toDomain() : FloconNetworkResponseOnlyDomainModel? {
return try {
val callId = floconCallId!!
val networkResponse = FloconNetworkResponseDomainModel(
durationMs = durationMs ?: 0.0,
body = responseBody,
byteSize = responseSize ?: 0L,
headers = responseHeaders.orEmpty(),
contentType = responseContentType,
)
when (floconNetworkType) {
"grpc" -> FloconNetworkResponseOnlyDomainModel.Grpc(
floconCallId = callId,
networkResponse = networkResponse,
grpcStatus = responseGrpcStatus!!,
)
// otherwise tread like http
else -> FloconNetworkResponseOnlyDomainModel.Http(
floconCallId = callId,
networkResponse = networkResponse,
httpCode = responseHttpCode!!,
)
}
} catch (t: Throwable) {
t.printStackTrace()
return null
}
}
internal fun FloconNetworkCallIdDataModel.toDomain(): FloconNetworkCallIdDomainModel? {
return FloconNetworkCallIdDomainModel(
floconCallId = floconCallId ?: return null
)
}

View file

@ -1,9 +1,11 @@
package com.flocon.data.remote.sharedpreference
import com.flocon.data.remote.sharedpreference.datasource.DeviceSharedPreferencesRemoteDataSource
import com.flocon.data.remote.sharedpreference.datasource.DeviceSharedPreferencesRemoteDataSourceImpl
import io.github.openflocon.data.core.sharedpreference.datasource.DeviceSharedPreferencesRemoteDataSource
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.bind
import org.koin.dsl.module
internal val sharedPreferencesModule = module {
singleOf(::DeviceSharedPreferencesRemoteDataSource)
singleOf(::DeviceSharedPreferencesRemoteDataSourceImpl) bind DeviceSharedPreferencesRemoteDataSource::class
}

View file

@ -1,22 +1,32 @@
package com.flocon.data.remote.sharedpreference.datasource
import com.flocon.data.remote.Protocol
import com.flocon.data.remote.common.safeDecodeFromString
import com.flocon.data.remote.models.FloconOutgoingMessageDataModel
import com.flocon.data.remote.models.toRemote
import com.flocon.data.remote.server.Server
import com.flocon.data.remote.server.newRequestId
import com.flocon.data.remote.sharedpreference.mapper.SharedPreferenceValuesResponse
import com.flocon.data.remote.sharedpreference.mapper.toSharedPreferenceValuesResponseDomain
import com.flocon.data.remote.sharedpreference.models.DeviceSharedPreferenceDataModel
import com.flocon.data.remote.sharedpreference.models.ToDeviceEditSharedPreferenceValueMessage
import com.flocon.data.remote.sharedpreference.models.ToDeviceGetSharedPreferenceValueMessage
import com.flocon.data.remote.sharedpreference.models.toDeviceSharedPreferenceDomain
import io.github.openflocon.data.core.sharedpreference.datasource.DeviceSharedPreferencesRemoteDataSource
import io.github.openflocon.domain.Protocol
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import io.github.openflocon.domain.messages.models.FloconIncomingMessageDomainModel
import io.github.openflocon.domain.sharedpreference.models.DeviceSharedPreferenceDomainModel
import io.github.openflocon.domain.sharedpreference.models.DeviceSharedPreferenceId
import io.github.openflocon.domain.sharedpreference.models.SharedPreferenceRowDomainModel
import io.github.openflocon.domain.sharedpreference.models.SharedPreferenceValuesResponseDomainModel
import kotlinx.serialization.json.Json
class DeviceSharedPreferencesRemoteDataSource(
class DeviceSharedPreferencesRemoteDataSourceImpl(
private val server: Server,
) {
suspend fun askForDeviceSharedPreferences(
private val json: Json
) : DeviceSharedPreferencesRemoteDataSource {
override suspend fun askForDeviceSharedPreferences(
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
) {
server.sendMessageToClient(
@ -29,7 +39,7 @@ class DeviceSharedPreferencesRemoteDataSource(
)
}
suspend fun getDeviceSharedPreferencesValues(
override suspend fun getDeviceSharedPreferencesValues(
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
sharedPreferenceId: DeviceSharedPreferenceId,
) {
@ -49,7 +59,7 @@ class DeviceSharedPreferencesRemoteDataSource(
)
}
suspend fun editSharedPrefField(
override suspend fun editSharedPrefField(
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
sharedPreference: DeviceSharedPreferenceDomainModel,
key: String,
@ -77,4 +87,16 @@ class DeviceSharedPreferencesRemoteDataSource(
),
)
}
override fun getPreferences(message: FloconIncomingMessageDomainModel): List<DeviceSharedPreferenceDomainModel> {
return json.safeDecodeFromString<List<DeviceSharedPreferenceDataModel>>(message.body)
?.let { toDeviceSharedPreferenceDomain(it) }
.orEmpty()
}
override fun getValues(message: FloconIncomingMessageDomainModel): SharedPreferenceValuesResponseDomainModel? {
return json.safeDecodeFromString<SharedPreferenceValuesResponse>(message.body)
?.let { toSharedPreferenceValuesResponseDomain(it) }
}
}

View file

@ -1,24 +0,0 @@
package com.flocon.data.remote.sharedpreference.mapper
import com.flocon.data.remote.sharedpreference.models.DeviceSharedPreferenceDataModel
import kotlinx.serialization.json.Json
// maybe inject
private val sharedPreferencesJsonParser = Json {
ignoreUnknownKeys = true
}
// TODO Internal
fun decodeDeviceSharedPreferences(body: String): List<DeviceSharedPreferenceDataModel>? = try {
sharedPreferencesJsonParser.decodeFromString<List<DeviceSharedPreferenceDataModel>>(body)
} catch (t: Throwable) {
t.printStackTrace()
null
}
fun decodeSharedPreferenceValuesResponse(body: String): SharedPreferenceValuesResponse? = try {
sharedPreferencesJsonParser.decodeFromString<SharedPreferenceValuesResponse>(body)
} catch (t: Throwable) {
t.printStackTrace()
null
}

View file

@ -5,7 +5,7 @@ import io.github.openflocon.domain.sharedpreference.models.SharedPreferenceValue
import kotlinx.serialization.Serializable
@Serializable
data class SharedPreferenceValuesResponse(
internal data class SharedPreferenceValuesResponse(
val requestId: String,
val sharedPreferenceName: String,
val rows: List<Row>,
@ -22,48 +22,49 @@ data class SharedPreferenceValuesResponse(
)
}
fun toSharedPreferenceValuesResponseDomain(data: SharedPreferenceValuesResponse): SharedPreferenceValuesResponseDomainModel = SharedPreferenceValuesResponseDomainModel(
requestId = data.requestId,
sharedPreferenceName = data.sharedPreferenceName,
rows = data.rows.mapNotNull { row ->
row.stringValue?.let { value ->
return@mapNotNull SharedPreferenceRowDomainModel(
key = row.key,
value = SharedPreferenceRowDomainModel.Value.StringValue(value = value),
)
}
row.intValue?.let { value ->
return@mapNotNull SharedPreferenceRowDomainModel(
key = row.key,
value = SharedPreferenceRowDomainModel.Value.IntValue(value),
)
}
row.floatValue?.let { value ->
return@mapNotNull SharedPreferenceRowDomainModel(
key = row.key,
value = SharedPreferenceRowDomainModel.Value.FloatValue(value),
)
}
row.booleanValue?.let { value ->
return@mapNotNull SharedPreferenceRowDomainModel(
key = row.key,
value = SharedPreferenceRowDomainModel.Value.BooleanValue(value),
)
}
row.longValue?.let { value ->
return@mapNotNull SharedPreferenceRowDomainModel(
key = row.key,
value = SharedPreferenceRowDomainModel.Value.LongValue(value),
)
}
row.setStringValue?.let { value ->
return@mapNotNull SharedPreferenceRowDomainModel(
key = row.key,
value = SharedPreferenceRowDomainModel.Value.StringSetValue(value.toSet()),
)
}
},
)
internal fun toSharedPreferenceValuesResponseDomain(data: SharedPreferenceValuesResponse): SharedPreferenceValuesResponseDomainModel =
SharedPreferenceValuesResponseDomainModel(
requestId = data.requestId,
sharedPreferenceName = data.sharedPreferenceName,
rows = data.rows.mapNotNull { row ->
row.stringValue?.let { value ->
return@mapNotNull SharedPreferenceRowDomainModel(
key = row.key,
value = SharedPreferenceRowDomainModel.Value.StringValue(value = value),
)
}
row.intValue?.let { value ->
return@mapNotNull SharedPreferenceRowDomainModel(
key = row.key,
value = SharedPreferenceRowDomainModel.Value.IntValue(value),
)
}
row.floatValue?.let { value ->
return@mapNotNull SharedPreferenceRowDomainModel(
key = row.key,
value = SharedPreferenceRowDomainModel.Value.FloatValue(value),
)
}
row.booleanValue?.let { value ->
return@mapNotNull SharedPreferenceRowDomainModel(
key = row.key,
value = SharedPreferenceRowDomainModel.Value.BooleanValue(value),
)
}
row.longValue?.let { value ->
return@mapNotNull SharedPreferenceRowDomainModel(
key = row.key,
value = SharedPreferenceRowDomainModel.Value.LongValue(value),
)
}
row.setStringValue?.let { value ->
return@mapNotNull SharedPreferenceRowDomainModel(
key = row.key,
value = SharedPreferenceRowDomainModel.Value.StringSetValue(value.toSet()),
)
}
},
)
private fun decodeStringToSet(encodedString: String): Set<String> {
// Si la chaîne est vide, cela signifie que le set original était vide.

View file

@ -1,25 +1,38 @@
package com.flocon.data.remote.table.datasource
import com.flocon.data.remote.Protocol
import com.flocon.data.remote.common.safeDecodeFromString
import com.flocon.data.remote.models.FloconOutgoingMessageDataModel
import com.flocon.data.remote.models.toRemote
import com.flocon.data.remote.server.Server
import com.flocon.data.remote.table.mapper.toDomain
import com.flocon.data.remote.table.model.TableItemDataModel
import io.github.openflocon.data.core.table.datasource.TableRemoteDataSource
import io.github.openflocon.domain.Protocol
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import io.github.openflocon.domain.messages.models.FloconIncomingMessageDomainModel
import io.github.openflocon.domain.table.models.TableDomainModel
import kotlinx.serialization.json.Json
class TableRemoteDataSourceImpl(
private val server: Server,
private val json: Json
) : TableRemoteDataSource {
override suspend fun clearReceivedItem(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, items: List<String>) {
server.sendMessageToClient(
deviceIdAndPackageName = deviceIdAndPackageName.toRemote(),
message =
FloconOutgoingMessageDataModel(
message = FloconOutgoingMessageDataModel(
plugin = Protocol.ToDevice.Table.Plugin,
method = Protocol.ToDevice.Table.Method.ClearItems,
body = Json.Default.encodeToString(items),
),
)
}
override fun getItems(message: FloconIncomingMessageDomainModel): List<TableDomainModel> {
return json.safeDecodeFromString<List<TableItemDataModel>>(message.body)
?.map { toDomain(it) }
.orEmpty()
}
}

View file

@ -0,0 +1,16 @@
package com.flocon.data.remote.table.mapper
import io.github.openflocon.domain.table.models.TableDomainModel
import com.flocon.data.remote.table.model.TableItemDataModel
internal fun toDomain(dataModel: TableItemDataModel): TableDomainModel = TableDomainModel(
name = dataModel.name,
items = listOf(
TableDomainModel.TableItem(
itemId = dataModel.id,
createdAt = dataModel.createdAt,
values = dataModel.columns.map { it.value },
columns = dataModel.columns.map { it.column },
),
),
)

View file

@ -0,0 +1,9 @@
package com.flocon.data.remote.table.model
import kotlinx.serialization.Serializable
@Serializable
data class TableColumnDataModel(
val column: String,
val value: String,
)

View file

@ -0,0 +1,11 @@
package com.flocon.data.remote.table.model
import kotlinx.serialization.Serializable
@Serializable
data class TableItemDataModel(
val id: String,
val name: String,
val createdAt: Long,
val columns: List<TableColumnDataModel>,
)