1
0
mirror of https://github.com/avinal/nikki.git synced 2026-07-03 21:40:09 +05:30

Add Room database with multiplatform support

- MemoEntity with JSON-serialized attachments and reactions columns
- MemoDao: observe all/by-id, upsert, delete operations via Flow
- Entity-domain mappers with JSON serialization for nested data
- DatabaseFactory: expect/actual pattern for platform-specific builders
- Android: Room.databaseBuilder with context
- iOS: Room.databaseBuilder with NSHomeDirectory path
- BundledSQLiteDriver for consistent cross-platform SQLite
- Destructive migration fallback for schema changes

Co-Authored-By: Claude Opus 4.6 (1M context)

Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
This commit is contained in:
2026-05-19 17:08:18 +05:30
parent b65673093a
commit e842355a6b
7 changed files with 225 additions and 0 deletions
@@ -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<MemosDatabase>(
context = ctx,
name = ctx.getDatabasePath("memos.db").absolutePath,
)
)
}
@@ -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>): MemosDatabase =
builder
.fallbackToDestructiveMigration(true)
.setDriver(BundledSQLiteDriver())
.setQueryCoroutineContext(Dispatchers.IO)
.build()
expect fun createPlatformDatabase(context: Any): MemosDatabase
@@ -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
}
@@ -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<List<MemoEntity>>
@Query("SELECT * FROM memos ORDER BY pinned DESC, updateTime DESC")
suspend fun getAll(): List<MemoEntity>
@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<MemoEntity?>
@Upsert
suspend fun upsert(memo: MemoEntity)
@Upsert
suspend fun upsertAll(memos: List<MemoEntity>)
@Query("DELETE FROM memos WHERE id = :id")
suspend fun deleteById(id: String)
@Query("DELETE FROM memos")
suspend fun deleteAll()
}
@@ -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>): String =
JsonArray(tags.map { JsonPrimitive(it) }).toString()
private fun deserializeTags(value: String): List<String> =
if (value.isEmpty() || value == "[]") emptyList()
else try {
json.parseToJsonElement(value).jsonArray.map { it.jsonPrimitive.content }
} catch (_: Exception) {
emptyList()
}
private fun serializeAttachments(attachments: List<Attachment>): 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<Attachment> =
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<Reaction>): String =
json.encodeToString(kotlinx.serialization.builtins.ListSerializer(ReactionJson.serializer()),
reactions.map { ReactionJson(it.id, it.creator, it.reactionType) }
)
private fun deserializeReactions(value: String): List<Reaction> =
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()
}
@@ -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,
)
@@ -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<MemosDatabase>(name = dbPath)
)
}
private fun NSHomeDirectory(): String =
platform.Foundation.NSHomeDirectory()