diff --git a/composeApp/src/androidMain/kotlin/com/avinal/memos/db/DatabaseFactory.android.kt b/composeApp/src/androidMain/kotlin/com/avinal/memos/db/DatabaseFactory.android.kt new file mode 100644 index 0000000..88a5962 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/avinal/memos/db/DatabaseFactory.android.kt @@ -0,0 +1,14 @@ +package com.avinal.memos.db + +import android.content.Context +import androidx.room.Room + +actual fun createPlatformDatabase(context: Any): MemosDatabase { + val ctx = context as Context + return buildDatabase( + Room.databaseBuilder( + context = ctx, + name = ctx.getDatabasePath("memos.db").absolutePath, + ) + ) +} diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/db/DatabaseFactory.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/db/DatabaseFactory.kt new file mode 100644 index 0000000..fb5d095 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/db/DatabaseFactory.kt @@ -0,0 +1,15 @@ +package com.avinal.memos.db + +import androidx.room.RoomDatabase +import androidx.sqlite.driver.bundled.BundledSQLiteDriver +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO + +fun buildDatabase(builder: RoomDatabase.Builder): MemosDatabase = + builder + .fallbackToDestructiveMigration(true) + .setDriver(BundledSQLiteDriver()) + .setQueryCoroutineContext(Dispatchers.IO) + .build() + +expect fun createPlatformDatabase(context: Any): MemosDatabase diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/db/MemosDatabase.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/db/MemosDatabase.kt new file mode 100644 index 0000000..e08bea9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/db/MemosDatabase.kt @@ -0,0 +1,11 @@ +package com.avinal.memos.db + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.avinal.memos.db.dao.MemoDao +import com.avinal.memos.db.entity.MemoEntity + +@Database(entities = [MemoEntity::class], version = 3) +abstract class MemosDatabase : RoomDatabase() { + abstract fun memoDao(): MemoDao +} diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/db/dao/MemoDao.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/db/dao/MemoDao.kt new file mode 100644 index 0000000..52ea08e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/db/dao/MemoDao.kt @@ -0,0 +1,35 @@ +package com.avinal.memos.db.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Upsert +import com.avinal.memos.db.entity.MemoEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface MemoDao { + + @Query("SELECT * FROM memos ORDER BY pinned DESC, updateTime DESC") + fun observeAll(): Flow> + + @Query("SELECT * FROM memos ORDER BY pinned DESC, updateTime DESC") + suspend fun getAll(): List + + @Query("SELECT * FROM memos WHERE id = :id") + suspend fun getById(id: String): MemoEntity? + + @Query("SELECT * FROM memos WHERE id = :id") + fun observeById(id: String): Flow + + @Upsert + suspend fun upsert(memo: MemoEntity) + + @Upsert + suspend fun upsertAll(memos: List) + + @Query("DELETE FROM memos WHERE id = :id") + suspend fun deleteById(id: String) + + @Query("DELETE FROM memos") + suspend fun deleteAll() +} diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/db/entity/EntityMappers.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/db/entity/EntityMappers.kt new file mode 100644 index 0000000..ee5acf4 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/db/entity/EntityMappers.kt @@ -0,0 +1,111 @@ +package com.avinal.memos.db.entity + +import com.avinal.memos.domain.Attachment +import com.avinal.memos.domain.Memo +import com.avinal.memos.domain.MemoVisibility +import com.avinal.memos.domain.Reaction +import kotlin.time.Instant +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive + +private val json = Json { ignoreUnknownKeys = true } + +@Serializable +private data class AttachmentJson( + val name: String, + val filename: String, + val mimeType: String, + val size: Long, + val externalLink: String, +) + +@Serializable +private data class ReactionJson( + val id: String, + val creator: String, + val reactionType: String, +) + +fun MemoEntity.toDomain(): Memo = Memo( + id = id, + uid = uid, + content = content, + visibility = MemoVisibility.fromApiString(visibility), + pinned = pinned, + state = state, + createTime = Instant.fromEpochMilliseconds(createTime), + updateTime = Instant.fromEpochMilliseconds(updateTime), + displayTime = Instant.fromEpochMilliseconds(displayTime), + creator = creator, + hasTaskList = hasTaskList, + hasIncompleteTasks = hasIncompleteTasks, + title = title, + tags = deserializeTags(tags), + snippet = snippet, + attachments = deserializeAttachments(attachmentsJson), + reactions = deserializeReactions(reactionsJson), +) + +fun Memo.toEntity(cachedAt: Long): MemoEntity = MemoEntity( + id = id, + uid = uid, + content = content, + visibility = visibility.toApiString(), + pinned = pinned, + state = state, + createTime = createTime.toEpochMilliseconds(), + updateTime = updateTime.toEpochMilliseconds(), + displayTime = displayTime.toEpochMilliseconds(), + creator = creator, + hasTaskList = hasTaskList, + hasIncompleteTasks = hasIncompleteTasks, + title = title, + tags = serializeTags(tags), + snippet = snippet, + attachmentsJson = serializeAttachments(attachments), + reactionsJson = serializeReactions(reactions), + cachedAt = cachedAt, +) + +private fun serializeTags(tags: List): String = + JsonArray(tags.map { JsonPrimitive(it) }).toString() + +private fun deserializeTags(value: String): List = + if (value.isEmpty() || value == "[]") emptyList() + else try { + json.parseToJsonElement(value).jsonArray.map { it.jsonPrimitive.content } + } catch (_: Exception) { + emptyList() + } + +private fun serializeAttachments(attachments: List): String = + json.encodeToString(kotlinx.serialization.builtins.ListSerializer(AttachmentJson.serializer()), + attachments.map { AttachmentJson(it.name, it.filename, it.mimeType, it.size, it.externalLink) } + ) + +private fun deserializeAttachments(value: String): List = + if (value.isEmpty() || value == "[]") emptyList() + else try { + json.decodeFromString(kotlinx.serialization.builtins.ListSerializer(AttachmentJson.serializer()), value) + .map { Attachment(it.name, it.filename, it.mimeType, it.size, it.externalLink) } + } catch (_: Exception) { + emptyList() + } + +private fun serializeReactions(reactions: List): String = + json.encodeToString(kotlinx.serialization.builtins.ListSerializer(ReactionJson.serializer()), + reactions.map { ReactionJson(it.id, it.creator, it.reactionType) } + ) + +private fun deserializeReactions(value: String): List = + if (value.isEmpty() || value == "[]") emptyList() + else try { + json.decodeFromString(kotlinx.serialization.builtins.ListSerializer(ReactionJson.serializer()), value) + .map { Reaction(it.id, it.creator, it.reactionType) } + } catch (_: Exception) { + emptyList() + } diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/db/entity/MemoEntity.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/db/entity/MemoEntity.kt new file mode 100644 index 0000000..05f1d55 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/db/entity/MemoEntity.kt @@ -0,0 +1,26 @@ +package com.avinal.memos.db.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "memos") +data class MemoEntity( + @PrimaryKey val id: String, + val uid: String, + val content: String, + val visibility: String, + val pinned: Boolean, + val state: String, + val createTime: Long, + val updateTime: Long, + val displayTime: Long, + val creator: String, + val hasTaskList: Boolean, + val hasIncompleteTasks: Boolean, + val title: String, + val tags: String, + val snippet: String, + val attachmentsJson: String = "[]", + val reactionsJson: String = "[]", + val cachedAt: Long, +) diff --git a/composeApp/src/iosMain/kotlin/com/avinal/memos/db/DatabaseFactory.ios.kt b/composeApp/src/iosMain/kotlin/com/avinal/memos/db/DatabaseFactory.ios.kt new file mode 100644 index 0000000..a79ad0b --- /dev/null +++ b/composeApp/src/iosMain/kotlin/com/avinal/memos/db/DatabaseFactory.ios.kt @@ -0,0 +1,13 @@ +package com.avinal.memos.db + +import androidx.room.Room + +actual fun createPlatformDatabase(context: Any): MemosDatabase { + val dbPath = NSHomeDirectory() + "/Documents/memos.db" + return buildDatabase( + Room.databaseBuilder(name = dbPath) + ) +} + +private fun NSHomeDirectory(): String = + platform.Foundation.NSHomeDirectory()