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:
@@ -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()
|
||||
Reference in New Issue
Block a user