Skip to content

Commit 5f2777d

Browse files
committed
feat: upgrade to Room 3 and expand cross-platform encryption support
- Update Android Gradle Plugin to 9.3.0 and Room to 3.0.0, migrating code to `androidx.room3` APIs. - Implement encryption-capable drivers for iOS (`IosCipherDriver`), JVM (`JdbcSQLiteDriver`), and WasmJS (`NamedWebWorkerSQLiteDriver`) using SQLCipher and SQLite3MultipleCiphers. - Add `room3.worker.js` with OPFS support to enable persistent, encrypted storage for the Web target. - Standardize the database filename as `notes.db` across all implementations and migrate the schema to version 2. - Refactor `SafeRepo` and `DatabaseHolder` to support asynchronous lifecycle management, including encryption, rekeying, and decryption. - Update build logic and documentation to support dynamic selection between Room and SQLDelight via `CORE_DATA_DB_MODULE`. - Improve error handling in `SplashViewModel` to better manage database initialization failures. - Add comprehensive integration tests for Room CRUD operations, paging, and encryption on JVM and WasmJS.
1 parent 22fdbbe commit 5f2777d

54 files changed

Lines changed: 2138 additions & 864 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
## Quick Reference
99

1010
### Project Structure & Module Organization
11-
- Core: `core/domain`, `core/presentation`, `core/data/db-sqldelight` (default), `core/data/db-room` (optional), `core/test/common`.
11+
- Core: `core/domain`, `core/presentation`, selectable `core/data/db-sqldelight` or `core/data/db-room`, `core/test/common`.
1212
- Features: `feature/backup/{domain,ui}`, `feature/console/{domain,presentation,ui}`, `feature/file-explorer/data`.
1313
- UI: `core/ui` (common Compose code and resources), `core/test/ui` (multiplatform Compose UI tests), `core/test/jvm` (JVM-specific UI test utilities).
1414
- Apps: `app/android`, `app/desktop`, `app/web`, `app/ios-kit` (SwiftPM-linked framework), `app/iosApp` (Xcode project).

CONTRIBUTING.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -460,16 +460,19 @@ open app/iosApp/iosApp.xcodeproj
460460
Edit `gradle.properties`:
461461

462462
```properties
463-
# Use SQLDelight (default)
463+
# Use SQLDelight
464464
CORE_DATA_DB_MODULE=:core:data:db-sqldelight
465465

466466
# Or use Room
467467
CORE_DATA_DB_MODULE=:core:data:db-room
468468
```
469469

470-
Then rebuild:
470+
Only the selected module is included by `settings.gradle.kts`; invalid values fail during
471+
settings evaluation. Keep exactly one assignment active, then run the full verification workflow:
471472
```bash
472-
./gradlew clean build
473+
./gradle/build_quick.sh
474+
./gradlew :app:android:connectedCheck
475+
./gradlew build
473476
```
474477

475478
## Testing Guidelines

app/android/src/androidTest/java/com/softartdev/notedelight/CryptInstrumentedTest.kt

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import android.text.SpannableStringBuilder
44
import androidx.test.ext.junit.runners.AndroidJUnit4
55
import androidx.test.filters.MediumTest
66
import co.touchlab.kermit.Logger
7+
import com.softartdev.notedelight.model.Note
78
import com.softartdev.notedelight.model.PlatformSQLiteState
89
import com.softartdev.notedelight.repository.SafeRepo
910
import kotlinx.coroutines.test.runTest
11+
import kotlinx.datetime.LocalDateTime
1012
import org.junit.Assert.assertEquals
13+
import org.junit.Assert.assertNotEquals
1114
import org.junit.Test
1215
import org.junit.runner.RunWith
1316
import org.koin.java.KoinJavaComponent.inject
@@ -31,4 +34,28 @@ class CryptInstrumentedTest {
3134
safeRepo.decrypt(SpannableStringBuilder(password))
3235
assertEquals(PlatformSQLiteState.UNENCRYPTED, safeRepo.databaseState)
3336
}
34-
}
37+
38+
@Test
39+
fun notePersistsAfterDatabaseReopen() = runTest {
40+
safeRepo.buildDbIfNeed()
41+
assertEquals("2", safeRepo.execute("PRAGMA user_version"))
42+
// SafeRoom must not be combined with a WAL mode that it does not manage.
43+
// Otherwise a cold process start can discard the previous process' WAL.
44+
assertNotEquals("wal", safeRepo.execute("PRAGMA journal_mode")?.lowercase())
45+
safeRepo.noteDAO.insert(
46+
Note(
47+
id = 0,
48+
title = "Persistent note",
49+
text = "Must survive closing and reopening the database",
50+
dateCreated = LocalDateTime(2026, 7, 21, 4, 27),
51+
dateModified = LocalDateTime(2026, 7, 21, 4, 27),
52+
)
53+
)
54+
assertEquals(1L, safeRepo.noteDAO.count())
55+
56+
safeRepo.closeDatabase()
57+
safeRepo.buildDbIfNeed()
58+
59+
assertEquals(1L, safeRepo.noteDAO.count())
60+
}
61+
}

app/iosApp/KotlinMultiplatformLinkedPackage/subpackages/KotlinMultiplatformLinkedPackageDylib/Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ let package = Package(
1313
)
1414
],
1515
dependencies: [
16-
.package(path: "../_core_data_db_sqldelight")
16+
.package(path: "../_core_data_db_room")
1717
],
1818
targets: [
1919
.target(
2020
name: "KotlinMultiplatformLinkedPackageDylib",
2121
dependencies: [
22-
.product(name: "_core_data_db_sqldelight", package: "_core_data_db_sqldelight")
22+
.product(name: "_core_data_db_room", package: "_core_data_db_room")
2323
]
2424
)
2525
]
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// swift-tools-version: 5.9
2+
import PackageDescription
3+
let package = Package(
4+
name: "_core_data_db_room",
5+
platforms: [
6+
.iOS("14.1")
7+
],
8+
products: [
9+
.library(
10+
name: "_core_data_db_room",
11+
type: .none,
12+
targets: ["_core_data_db_room"]
13+
)
14+
],
15+
dependencies: [
16+
.package(
17+
url: "https://github.com/sqlcipher/SQLCipher.swift.git",
18+
exact: "4.16.0"
19+
)
20+
],
21+
targets: [
22+
.target(
23+
name: "_core_data_db_room",
24+
dependencies: [
25+
.product(
26+
name: "SQLCipher",
27+
package: "SQLCipher.swift"
28+
)
29+
]
30+
)
31+
]
32+
)

app/iosApp/KotlinMultiplatformLinkedPackage/subpackages/_core_data_db_room/Sources/_core_data_db_room/_core_data_db_room.m

Whitespace-only changes.

app/iosApp/KotlinMultiplatformLinkedPackage/subpackages/_core_data_db_room/Sources/_core_data_db_room/include/_core_data_db_room.h

Whitespace-only changes.

app/web/README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ build/dist/wasmJs/productionExecutable/
125125
├── skiko.wasm # Skia graphics engine
126126
├── sqlite3.js # SQLite JavaScript
127127
├── sqlite3.wasm # SQLite3MultipleCiphers WASM (with encryption)
128-
├── sqlite.worker.js # Custom OPFS worker
128+
├── <selected ORM worker> # SQLDelight or bundled Room 3 OPFS worker
129129
├── coi-serviceworker.js # Service worker for headers
130130
└── sql-wasm.wasm # Legacy SQL.js (fallback)
131131
```
@@ -578,4 +578,3 @@ try {
578578
- [SQL.js](https://sql.js.org/)
579579
- [WebAssembly](https://webassembly.org/)
580580
- [PWA Documentation](https://web.dev/progressive-web-apps/)
581-

app/web/build.gradle.kts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ plugins {
1313
alias(libs.plugins.download)
1414
}
1515

16+
val coreDataDbModule = providers.gradleProperty("CORE_DATA_DB_MODULE").get()
17+
1618
kotlin {
1719
wasmJs {
1820
outputModuleName.set("composeApp")
@@ -43,8 +45,14 @@ kotlin {
4345
implementation(libs.koin.compose)
4446
implementation(libs.kermit)
4547
implementation(npm("firebase", libs.versions.firebaseWeb.get()))
48+
implementation(devNpm("copy-webpack-plugin", "9.1.0"))
4649
}
4750
resources.srcDir(layout.buildDirectory.dir("sqlite"))
51+
if (coreDataDbModule == ":core:data:db-room") {
52+
resources.srcDir(
53+
project(coreDataDbModule).layout.projectDirectory.dir("src/wasmJsMain/resources")
54+
)
55+
}
4856
}
4957
wasmJsTest {
5058
dependencies {

app/web/src/wasmJsMain/resources/sqlite.worker.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ async function createDatabase() {
2121
try {
2222
const rc = capi.sqlite3mc_vfs_create("opfs", 0);
2323
if (rc === 0) {
24-
db = new sqlite3.oo1.DB("file:database.db?vfs=multipleciphers-opfs", "c");
24+
db = new sqlite3.oo1.DB("file:notes.db?vfs=multipleciphers-opfs", "c");
2525
return;
2626
}
2727
} catch (error) {
@@ -30,7 +30,7 @@ async function createDatabase() {
3030

3131
// Fallback: try opfs without encryption
3232
try {
33-
db = new sqlite3.oo1.DB("file:database.db?vfs=opfs", "c");
33+
db = new sqlite3.oo1.DB("file:notes.db?vfs=opfs", "c");
3434
return;
3535
} catch (error) {
3636
try {

0 commit comments

Comments
 (0)