From eb5c7201c087980bb8947634f1d0b385b4f20086 Mon Sep 17 00:00:00 2001 From: rdlabo Date: Thu, 23 Jul 2026 09:33:12 +0900 Subject: [PATCH 1/2] feat(offline): use community sqlite for native storage --- package-lock.json | 108 ++++++++++++++++++ package.json | 1 + projects/kit/README.md | 88 ++++++-------- .../kit/offline/src/lib/offline-provider.ts | 7 +- .../src/lib/sqlite-offline-repository.spec.ts | 101 +++++++++++++--- .../src/lib/sqlite-offline-repository.ts | 90 ++++++++++++--- projects/kit/package.json | 4 +- 7 files changed, 309 insertions(+), 90 deletions(-) diff --git a/package-lock.json b/package-lock.json index bdb1f95..7ed41c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "@capacitor-community/apple-sign-in": "github:rdlabo/apple-sign-in#cap8_build", "@capacitor-community/facebook-login": "^8.0.0", "@capacitor-community/in-app-review": "^8.0.0", + "@capacitor-community/sqlite": "^8.1.0", "@capacitor/cli": ">=6.0.0 <9.0.0", "@capacitor/haptics": "^8.0.2", "@capacitor/keyboard": "^8.0.5", @@ -3512,6 +3513,22 @@ "@capacitor/core": ">=8.0.0" } }, + "node_modules/@capacitor-community/sqlite": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@capacitor-community/sqlite/-/sqlite-8.1.0.tgz", + "integrity": "sha512-yhKZDAVPDPcM3QE6UGB3LXyV25a6Rve1SjZ1aUpTE0E2isnYTVM0PG9+JOI241f+NdsHzPTE7ESJiYSqKsKnuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jeep-sqlite": "^2.7.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, "node_modules/@capacitor/android": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-8.4.0.tgz", @@ -11321,6 +11338,13 @@ "node": ">=8" } }, + "node_modules/browser-fs-access": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/browser-fs-access/-/browser-fs-access-0.35.0.tgz", + "integrity": "sha512-sLoadumpRfsjprP8XzVjpQc0jK8yqHBx0PtUTGYj2fftT+P/t+uyDAQdMgGAPKD011in/O+YYGh7fIs0oG/viw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/browser-process-hrtime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", @@ -16857,6 +16881,20 @@ "node": ">=10" } }, + "node_modules/jeep-sqlite": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/jeep-sqlite/-/jeep-sqlite-2.8.0.tgz", + "integrity": "sha512-FWNUP6OAmrUHwiW7H1xH5YUQ8tN2O4l4psT1sLd7DQtHd5PfrA1nvNdeKPNj+wQBtu7elJa8WoUibTytNTaaCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@stencil/core": "^4.20.0", + "browser-fs-access": "^0.35.0", + "jszip": "^3.10.1", + "localforage": "^1.10.0", + "sql.js": "^1.11.0" + } + }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -17138,6 +17176,62 @@ "node": ">=0.6.0" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/karma-source-map-support": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", @@ -23218,6 +23312,13 @@ "optional": true, "peer": true }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -23681,6 +23782,13 @@ "node": ">= 10.x" } }, + "node_modules/sql.js": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.1.tgz", + "integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==", + "dev": true, + "license": "MIT" + }, "node_modules/sshpk": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", diff --git a/package.json b/package.json index 252f8b7..3d2a0c0 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "@capacitor-community/apple-sign-in": "github:rdlabo/apple-sign-in#cap8_build", "@capacitor-community/facebook-login": "^8.0.0", "@capacitor-community/in-app-review": "^8.0.0", + "@capacitor-community/sqlite": "^8.1.0", "@capacitor/cli": ">=6.0.0 <9.0.0", "@capacitor/haptics": "^8.0.2", "@capacitor/keyboard": "^8.0.5", diff --git a/projects/kit/README.md b/projects/kit/README.md index c7c9f62..dab5606 100644 --- a/projects/kit/README.md +++ b/projects/kit/README.md @@ -338,56 +338,51 @@ session boundary, cursor-based delta pull, aggregate-ordered replay, optimistic read-only request-policy interceptor. Applications provide URL/DTO read policies, a replica puller, and a command executor through `provideOffline(...)`. Mutations are queued explicitly with `OfflineSyncService.enqueue`, not through HTTP interceptor policy. -Web storage uses Ionic Storage; iOS and Android use encrypted Capawesome SQLite. Importing either the -primary entry point or `/offline` does not pull the private plugin into existing applications. +Web storage uses Ionic Storage; iOS and Android use encrypted `@capacitor-community/sqlite`. Importing either the +primary entry point or `/offline` does not pull the optional native SQLite plugin into web-only applications. The offline interceptor observes real transport responses to update API reachability. For matched `GET` requests only, a transport failure with `status=0` may return a local replica response tagged `X-Offline-Response: local`. `POST` and other write methods always go to transport unchanged; outbox replay requests bypass policy with `OFFLINE_BYPASS` while still using the same transport observation. -The native offline runtime currently requires Capacitor 8, `@capawesome-team/capacitor-sqlite` 0.3.x, and -`@capawesome-team/capacitor-secure-preferences` 0.2.x. Configure the private Insiders registry with the license key -before installing the SQLite and Secure Preferences packages plus the SQLite WASM runtime. Supply the license key -through a local/CI secret; never commit it to `.npmrc`. +The native offline runtime uses `@capacitor-community/sqlite` on iOS and Android. Install the plugin in the app and +sync native projects: ```bash -npm config set @capawesome-team:registry https://npm.registry.capawesome.io -npm config set //npm.registry.capawesome.io/:_authToken "$CAPAWESOME_LICENSE_KEY" -npm install @capawesome-team/capacitor-sqlite@^0.3.0 \ - @capawesome-team/capacitor-secure-preferences@^0.2.0 \ - @sqlite.org/sqlite-wasm +npm install @capacitor-community/sqlite npx cap sync ``` -Applications on an older Capacitor major must not install those versions; upgrade to Capacitor 8 before enabling -the standard native offline runtime. After installation, follow both plugins' platform steps. In particular, exclude -`CAPAWESOME_SECURE_PREFERENCES.xml` from Android 11-and-lower `fullBackupContent` and Android 12+ cloud backup rules, -so the database key is not restored independently of its device keystore material. - -Pass the `Sqlite` export and a database key loaded from secure device storage to the kit. Never hard-code or derive -the database key from a user identifier or access token. +Add the required `CapacitorSQLite` plugin block to `capacitor.config.ts`. Encryption must be enabled — the kit opens +databases in encrypted mode and relies on the plugin's built-in secure secret storage (`setEncryptionSecret` / +`checkEncryptionSecret` in the device keychain / Android keystore): ```ts -import { SecurePreferences } from '@capawesome-team/capacitor-secure-preferences'; -import { Sqlite } from '@capawesome-team/capacitor-sqlite'; - -const OFFLINE_DATABASE_KEY = 'product-offline-database-key'; +// capacitor.config.ts +plugins: { + CapacitorSQLite: { + iosDatabaseLocation: 'Library/CapacitorDatabase', + iosIsEncryption: true, + iosKeychainPrefix: '', + androidIsEncryption: true, + }, +}, +``` -async function offlineDatabaseKey(): Promise { - const { value } = await SecurePreferences.get({ key: OFFLINE_DATABASE_KEY }); - if (value) return value; +Follow the [@capacitor-community/sqlite installation guide](https://github.com/capacitor-community/sqlite#installation) +for platform-specific steps and SQLCipher export-compliance notes. - const bytes = crypto.getRandomValues(new Uint8Array(32)); - const generated = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join(''); - await SecurePreferences.set({ key: OFFLINE_DATABASE_KEY, value: generated }); - return generated; -} +Pass a stable `databaseName` and an `encryptionKey` generator to `provideOffline`. The kit invokes the generator only +when the plugin has no secret yet, then stores the result in the plugin's Keychain / Android keystore. Later opens use +that stored secret without invoking the generator. Never hard-code or derive the key from a user identifier, device +identifier, or access token; generate a cryptographically random value for the first installation. +```ts provideOffline({ + databaseName: 'product-offline', + encryptionKey: loadOfflineEncryptionKey, // ...product policies, puller, and executor - sqlitePlugin: Sqlite, - encryptionKey: offlineDatabaseKey, }); ``` @@ -397,10 +392,10 @@ Immediately before each send, the executor receives the latest `{ localId, serve SQLite; a successful create adds `serverId` without replacing `localId`. Entity projection and outbox append/removal are committed in one local transaction. -| Identity | SQLite column | Before synchronization | After server acknowledgement | -| --- | --- | --- | --- | -| `localId` | `local_id` | client-generated UUID | unchanged UUID | -| `serverId` | `server_id` | `NULL` for a new entity | positive server `AUTO_INCREMENT` id | +| Identity | SQLite column | Before synchronization | After server acknowledgement | +| ---------- | ------------- | ----------------------- | ----------------------------------- | +| `localId` | `local_id` | client-generated UUID | unchanged UUID | +| `serverId` | `server_id` | `NULL` for a new entity | positive server `AUTO_INCREMENT` id | The write lifecycle is: update the replica immediately → append an outbox command in the same transaction → render the optimistic value → replay in the background → validate the server revision → store the confirmed value and @@ -455,13 +450,7 @@ shape or `null` to delete a row. Identity and sync metadata (`localId`, `serverI migration `fromVersion`/`statements` — never function bodies. ```typescript -import { - defineOfflineReplicaSchema, - defineReplicaEntity, - provideOffline, - serverId, - text, -} from '@rdlabo/ionic-angular-kit/offline'; +import { defineOfflineReplicaSchema, defineReplicaEntity, provideOffline, serverId, text } from '@rdlabo/ionic-angular-kit/offline'; // This is the Hono package's existing `typeof items.$inferSelect` export. import type { Items as ItemSelect } from '@product/hono/db/schema'; @@ -487,8 +476,7 @@ const replicaSchema = defineOfflineReplicaSchema({ migrateWebRow: (row) => ({ sourceKey: row.sourceKey, values: { ...row.values, subtitle: '' }, - confirmedValues: - row.confirmedValues === null ? null : { ...row.confirmedValues, subtitle: '' }, + confirmedValues: row.confirmedValues === null ? null : { ...row.confirmedValues, subtitle: '' }, }), }, ], @@ -498,7 +486,7 @@ provideOffline({ replicaSchema, replicaPuller: ProductReplicaPuller, commandExecutor: ProductCommandExecutor, - // ...request policies, sqlitePlugin, encryptionKey + // ...request policies, databaseName, encryptionKey }); ``` @@ -513,14 +501,6 @@ removing, or changing nullability of a Drizzle column breaks the app build until At runtime, `values` contains only the mapped column projection; `localId` and `serverId` remain dedicated replica fields and ignored server fields are never persisted. -Encrypted native builds also require the plugin's SQLCipher platform setup: enable -`capawesomeCapacitorSqliteIncludeSqlcipher = true` on Android; select the `SQLCipher` pod when using -CocoaPods, or enable the `SQLCipher` package trait when using Swift Package Manager on iOS. Follow -the [Capawesome SQLite installation guide](https://capawesome.io/docs/plugins/sqlite/#installation) -for the exact native configuration and export-compliance notes. -Also follow the [Capawesome Secure Preferences installation guide](https://capawesome.io/docs/plugins/secure-preferences/#installation), -including its Android backup exclusion rules. - - **Status classification**: `0`→`onNetworkError` (connected only), `429`→`onRateLimited`, `502/503/504`→`onServerBusy`, `400/422/500`+message→`onServerError`, `401`→`onUnauthorized`, `403`→`onForbidden`. Other statuses (e.g. `404`) are left to the caller. - **Universal 60s timeout** — every request fails with a synthetic (retryable) `408` if it hangs for 60s. Deliberately generous (catches a dead server without cutting off a large upload / AI generation; `timeout({ each })` resets per emission, so streaming is unaffected). Not configurable — one fleet-wide behavior. - **Optional `treatAsError(response)`** — reject a 2xx (e.g. `204`/`206`) as an error when a backend uses it to signal a condition. The one genuinely app-specific bit (some apps receive a normal `204`), kept optional so class interceptors with a 2xx-as-error convention can migrate to `provideKitHttp`. diff --git a/projects/kit/offline/src/lib/offline-provider.ts b/projects/kit/offline/src/lib/offline-provider.ts index 10ea7b9..91c8ada 100644 --- a/projects/kit/offline/src/lib/offline-provider.ts +++ b/projects/kit/offline/src/lib/offline-provider.ts @@ -14,7 +14,7 @@ import { provideOfflineRequestPolicy } from './offline-request-policy'; import type { OfflineReplicaPuller } from './offline-replica-puller'; import { OFFLINE_REPLICA_PULLER } from './offline-replica-puller'; import { OfflineSessionService } from './offline-session.service'; -import { CAPAWESOME_SQLITE, type CapawesomeSqlitePlugin, SqliteOfflineRepository } from './sqlite-offline-repository'; +import { SqliteOfflineRepository } from './sqlite-offline-repository'; /** Configuration for the standard offline repository, outbox, and request-policy runtime. */ export interface ProvideOfflineOptions extends OfflineKitOptions { @@ -28,15 +28,13 @@ export interface ProvideOfflineOptions extends OfflineKitOptions { commandHooks?: Type; /** Optional additional providers required by product adapters. */ providers?: readonly Provider[]; - /** Capawesome `Sqlite` plugin. Required only when this runtime is selected on iOS or Android. */ - sqlitePlugin?: CapawesomeSqlitePlugin; } /** * Provide the standard scoped offline runtime. * * @remarks - * Web uses Ionic Storage. Native iOS/Android uses encrypted Capawesome SQLite. The application owns + * Web uses Ionic Storage. Native iOS/Android uses encrypted `@capacitor-community/sqlite`. The application owns * URL/DTO policy and command execution; the kit owns persistence, ordering, retries, and session * isolation. */ @@ -52,7 +50,6 @@ export function provideOffline(options: ProvideOfflineOptions): EnvironmentProvi replicaSchema: options.replicaSchema, }, }, - { provide: CAPAWESOME_SQLITE, useValue: options.sqlitePlugin ?? null }, { provide: OFFLINE_REPOSITORY, useFactory: () => selectOfflineRepository(Capacitor.getPlatform(), inject(IonicOfflineRepository), inject(SqliteOfflineRepository)), diff --git a/projects/kit/offline/src/lib/sqlite-offline-repository.spec.ts b/projects/kit/offline/src/lib/sqlite-offline-repository.spec.ts index 61d8e4d..de066d1 100644 --- a/projects/kit/offline/src/lib/sqlite-offline-repository.spec.ts +++ b/projects/kit/offline/src/lib/sqlite-offline-repository.spec.ts @@ -10,7 +10,14 @@ import { text, type OfflineReplicaSchemaBundle, } from './offline-replica-schema'; -import { CAPAWESOME_SQLITE, type CapawesomeSqlitePlugin, SqliteOfflineRepository } from './sqlite-offline-repository'; +import { + COMMUNITY_SQLITE, + type CommunitySqliteConnection, + type CommunitySqliteDatabase, + type CommunitySqliteDriver, + createCommunitySqliteDriver, + SqliteOfflineRepository, +} from './sqlite-offline-repository'; type TestItemSelect = { id: number; title: string }; type TestItemWithSubtitleSelect = { id: number; title: string; subtitle: string }; @@ -82,9 +89,71 @@ const replicaSchemaV1HashDrift = defineOfflineReplicaSchema({ migrations: [], }); -describe('SqliteOfflineRepository Capawesome adapter', () => { +describe('createCommunitySqliteDriver', () => { + const createDatabase = (): CommunitySqliteDatabase => ({ + open: vi.fn(async () => undefined), + run: vi.fn(async () => ({})), + query: vi.fn(async () => ({ values: [{ id: 1 }] })), + beginTransaction: vi.fn(async () => ({})), + commitTransaction: vi.fn(async () => ({})), + rollbackTransaction: vi.fn(async () => ({})), + }); + + it('first open stores a generated secret and opens an encrypted connection', async () => { + const database = createDatabase(); + const connection: CommunitySqliteConnection = { + isSecretStored: vi.fn(async () => ({ result: false })), + setEncryptionSecret: vi.fn(async () => undefined), + createConnection: vi.fn(async () => database), + }; + const createEncryptionKey = vi.fn(async () => 'random-install-secret'); + const driver = createCommunitySqliteDriver(connection); + + await expect(driver.open({ databaseName: 'product-offline', createEncryptionKey })).resolves.toEqual({ + databaseId: 'product-offline', + }); + expect(createEncryptionKey).toHaveBeenCalledOnce(); + expect(connection.setEncryptionSecret).toHaveBeenCalledWith('random-install-secret'); + expect(connection.createConnection).toHaveBeenCalledWith('product-offline', true, 'secret', 1, false); + expect(database.open).toHaveBeenCalledOnce(); + }); + + it('later opens use the plugin secret without generating or receiving it again', async () => { + const database = createDatabase(); + const connection: CommunitySqliteConnection = { + isSecretStored: vi.fn(async () => ({ result: true })), + setEncryptionSecret: vi.fn(async () => undefined), + createConnection: vi.fn(async () => database), + }; + const createEncryptionKey = vi.fn(async () => 'must-not-be-read'); + + await createCommunitySqliteDriver(connection).open({ databaseName: 'product-offline', createEncryptionKey }); + + expect(createEncryptionKey).not.toHaveBeenCalled(); + expect(connection.setEncryptionSecret).not.toHaveBeenCalled(); + }); + + it('rejects first open when the generator returns an empty key', async () => { + const connection: CommunitySqliteConnection = { + isSecretStored: vi.fn(async () => ({ result: false })), + setEncryptionSecret: vi.fn(async () => undefined), + createConnection: vi.fn(async () => createDatabase()), + }; + + await expect( + createCommunitySqliteDriver(connection).open({ + databaseName: 'product-offline', + createEncryptionKey: async () => '', + }), + ).rejects.toThrow('non-empty encryption key on first open'); + expect(connection.setEncryptionSecret).not.toHaveBeenCalled(); + expect(connection.createConnection).not.toHaveBeenCalled(); + }); +}); + +describe('SqliteOfflineRepository community sqlite driver', () => { let plugin: { - [K in keyof CapawesomeSqlitePlugin]: ReturnType; + [K in keyof CommunitySqliteDriver]: ReturnType; }; let storedReplicaMetadata: { version: number; schemaHash: string } | null; let replicaSchemaV1Hash: string; @@ -123,13 +192,18 @@ describe('SqliteOfflineRepository Capawesome adapter', () => { plugin.open.mockRejectedValueOnce(error); const repository = createRepository(); await expect(repository.initialize()).rejects.toBe(error); - expect(plugin.open).toHaveBeenCalledWith({ path: 'test-offline.sqlite3', encryptionKey: 'secret', readOnly: false }); + expect(plugin.open).toHaveBeenCalledWith({ + databaseName: 'test-offline', + createEncryptionKey: expect.any(Function), + }); }); - it('暗号鍵が無い場合はdatabaseを開かない', async () => { - const repository = createRepository(async () => ''); - await expect(repository.initialize()).rejects.toThrow('non-empty encryption key'); - expect(plugin.open).not.toHaveBeenCalled(); + it('暗号鍵の生成関数をcommunity driverへ渡す', async () => { + const encryptionKey = vi.fn(async () => 'first-install-secret'); + const repository = createRepository(encryptionKey); + await repository.initialize(); + const options = plugin.open.mock.calls[0]?.[0] as { createEncryptionKey?: () => Promise }; + await expect(options.createEncryptionKey?.()).resolves.toBe('first-install-secret'); }); it('group scopeのoutboxを単一transactionで削除する', async () => { @@ -190,10 +264,7 @@ describe('SqliteOfflineRepository Capawesome adapter', () => { const scopeQuery = plugin.query.mock.calls.find(([options]) => { const statement = (options as { statement: string }).statement; - return ( - statement === - 'SELECT * FROM offline_sync_commands WHERE user_id = ? AND group_id = ? ORDER BY created_at ASC, command_id ASC' - ); + return statement === 'SELECT * FROM offline_sync_commands WHERE user_id = ? AND group_id = ? ORDER BY created_at ASC, command_id ASC'; })?.[0] as { statement: string } | undefined; const userQuery = plugin.query.mock.calls.find(([options]) => { const statement = (options as { statement: string }).statement; @@ -363,7 +434,7 @@ describe('SqliteOfflineRepository Capawesome adapter', () => { TestBed.configureTestingModule({ providers: [ SqliteOfflineRepository, - { provide: CAPAWESOME_SQLITE, useValue: plugin }, + { provide: COMMUNITY_SQLITE, useValue: plugin }, { provide: OFFLINE_KIT_OPTIONS, useValue: { @@ -380,7 +451,7 @@ describe('SqliteOfflineRepository Capawesome adapter', () => { describe('SqliteOfflineRepository replica rows', () => { let plugin: { - [K in keyof CapawesomeSqlitePlugin]: ReturnType; + [K in keyof CommunitySqliteDriver]: ReturnType; }; let storedReplicaMetadata: { version: number; schemaHash: string } | null; let replicaSchemaV1Hash: string; @@ -998,7 +1069,7 @@ describe('SqliteOfflineRepository replica rows', () => { TestBed.configureTestingModule({ providers: [ SqliteOfflineRepository, - { provide: CAPAWESOME_SQLITE, useValue: plugin }, + { provide: COMMUNITY_SQLITE, useValue: plugin }, { provide: OFFLINE_KIT_OPTIONS, useValue: { diff --git a/projects/kit/offline/src/lib/sqlite-offline-repository.ts b/projects/kit/offline/src/lib/sqlite-offline-repository.ts index 04ae86c..97fa89f 100644 --- a/projects/kit/offline/src/lib/sqlite-offline-repository.ts +++ b/projects/kit/offline/src/lib/sqlite-offline-repository.ts @@ -1,4 +1,5 @@ import { inject, Injectable, InjectionToken } from '@angular/core'; +import { CapacitorSQLite, SQLiteConnection } from '@capacitor-community/sqlite'; import { OFFLINE_KIT_OPTIONS } from './offline-kit-options'; import { decodeOfflineReplicaValues, @@ -19,9 +20,9 @@ import { type OfflineScope, } from './offline-repository'; -/** Minimal Capawesome SQLite surface required by the offline repository. */ -export interface CapawesomeSqlitePlugin { - open(options: { path: string; encryptionKey: string; readOnly: false }): Promise<{ databaseId: string }>; +/** Minimal native SQLite driver surface required by the offline repository. */ +export interface CommunitySqliteDriver { + open(options: { databaseName: string; createEncryptionKey?: () => Promise }): Promise<{ databaseId: string }>; execute(options: { databaseId: string; statement: string; values?: SQLiteValue[] }): Promise; query(options: { databaseId: string; statement: string; values?: SQLiteValue[] }): Promise<{ columns?: string[]; @@ -32,11 +33,76 @@ export interface CapawesomeSqlitePlugin { rollbackTransaction(options: { databaseId: string }): Promise; } -/** DI token for the optional application-installed Capawesome SQLite plugin. */ -export const CAPAWESOME_SQLITE = new InjectionToken('CAPAWESOME_SQLITE', { - factory: () => null, +/** Open community SQLite database surface used by the standard driver. */ +export interface CommunitySqliteDatabase { + open(): Promise; + run(statement: string, values?: unknown[], transaction?: boolean): Promise; + query(statement: string, values?: unknown[]): Promise<{ values?: unknown[] }>; + beginTransaction(): Promise; + commitTransaction(): Promise; + rollbackTransaction(): Promise; +} + +/** Community SQLite connection surface used to provision encrypted databases. */ +export interface CommunitySqliteConnection { + isSecretStored(): Promise<{ result?: boolean }>; + setEncryptionSecret(passphrase: string): Promise; + createConnection( + database: string, + encrypted: boolean, + mode: string, + version: number, + readonly: boolean, + ): Promise; +} + +/** DI token for the native community SQLite driver. */ +export const COMMUNITY_SQLITE = new InjectionToken('COMMUNITY_SQLITE', { + factory: createCommunitySqliteDriver, }); +/** Create the standard encrypted `@capacitor-community/sqlite` driver. */ +export function createCommunitySqliteDriver( + connection: CommunitySqliteConnection = new SQLiteConnection(CapacitorSQLite), +): CommunitySqliteDriver { + const databases = new Map(); + const database = (databaseId: string): CommunitySqliteDatabase => { + const value = databases.get(databaseId); + if (!value) throw new Error(`Offline SQLite database "${databaseId}" is not open`); + return value; + }; + return { + async open({ databaseName, createEncryptionKey }) { + const stored = await connection.isSecretStored(); + if (!stored.result) { + const encryptionKey = await createEncryptionKey?.(); + if (!encryptionKey) throw new Error('Native offline storage requires a non-empty encryption key on first open'); + await connection.setEncryptionSecret(encryptionKey); + } + const value = await connection.createConnection(databaseName, true, 'secret', 1, false); + await value.open(); + databases.set(databaseName, value); + return { databaseId: databaseName }; + }, + async execute({ databaseId, statement, values = [] }) { + await database(databaseId).run(statement, values, false); + }, + async query({ databaseId, statement, values = [] }) { + const result = await database(databaseId).query(statement, values); + return { rows: result.values ?? [] }; + }, + async beginTransaction({ databaseId }) { + await database(databaseId).beginTransaction(); + }, + async commitTransaction({ databaseId }) { + await database(databaseId).commitTransaction(); + }, + async rollbackTransaction({ databaseId }) { + await database(databaseId).rollbackTransaction(); + }, + }; +} + type SQLiteValue = string | number | null; type SQLiteRow = Record; @@ -82,10 +148,10 @@ const SCHEMA = [ )`, ]; -/** Native iOS/Android uses the encrypted Capawesome SQLite plugin supplied by the application. */ +/** Native iOS/Android repository backed by encrypted `@capacitor-community/sqlite`. */ @Injectable({ providedIn: 'root' }) export class SqliteOfflineRepository implements OfflineRepository { - readonly #sqlite = inject(CAPAWESOME_SQLITE); + readonly #sqlite = inject(COMMUNITY_SQLITE); readonly #options = inject(OFFLINE_KIT_OPTIONS); #databaseId: string | null = null; #initialization: Promise | null = null; @@ -246,13 +312,9 @@ export class SqliteOfflineRepository implements OfflineRepository { } async #open(): Promise { - if (!this.#sqlite) throw new Error('Native offline storage requires the Capawesome SQLite plugin'); - const encryptionKey = await this.#options.encryptionKey?.(); - if (!encryptionKey) throw new Error('Native offline storage requires a non-empty encryption key'); const { databaseId } = await this.#sqlite.open({ - path: `${this.#options.databaseName}.sqlite3`, - encryptionKey, - readOnly: false, + databaseName: this.#options.databaseName, + createEncryptionKey: this.#options.encryptionKey, }); this.#databaseId = databaseId; for (const statement of SCHEMA) await this.#execute(databaseId, statement); diff --git a/projects/kit/package.json b/projects/kit/package.json index b15128f..fa5c10b 100644 --- a/projects/kit/package.json +++ b/projects/kit/package.json @@ -15,7 +15,7 @@ "@capacitor/core": ">=6.0.0 <9.0.0", "@capacitor/app": ">=6.0.0 <9.0.0", "@capawesome/capacitor-live-update": ">=6.0.0 <9.0.0", - "@capawesome-team/capacitor-sqlite": ">=0.3.0 <1.0.0", + "@capacitor-community/sqlite": ">=6.0.0 <9.0.0", "@capacitor/haptics": ">=6.0.0 <9.0.0", "@capacitor/keyboard": ">=6.0.0 <9.0.0", "@capacitor/network": ">=6.0.0 <9.0.0", @@ -37,7 +37,7 @@ "@capawesome/capacitor-live-update": { "optional": true }, - "@capawesome-team/capacitor-sqlite": { + "@capacitor-community/sqlite": { "optional": true }, "@capacitor/preferences": { From f96b8bbca2aa892a2f5c6163a602f1f57d2dd315 Mon Sep 17 00:00:00 2001 From: rdlabo Date: Thu, 23 Jul 2026 09:39:23 +0900 Subject: [PATCH 2/2] fix(offline): isolate native sqlite connection --- package-lock.json | 108 ------------------ package.json | 1 - projects/kit/README.md | 30 ++++- .../offline/src/lib/offline-kit-options.ts | 4 +- .../kit/offline/src/lib/offline-provider.ts | 15 ++- .../src/lib/sqlite-offline-repository.spec.ts | 10 +- .../src/lib/sqlite-offline-repository.ts | 12 +- projects/kit/package.json | 4 - 8 files changed, 49 insertions(+), 135 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7ed41c8..bdb1f95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,6 @@ "@capacitor-community/apple-sign-in": "github:rdlabo/apple-sign-in#cap8_build", "@capacitor-community/facebook-login": "^8.0.0", "@capacitor-community/in-app-review": "^8.0.0", - "@capacitor-community/sqlite": "^8.1.0", "@capacitor/cli": ">=6.0.0 <9.0.0", "@capacitor/haptics": "^8.0.2", "@capacitor/keyboard": "^8.0.5", @@ -3513,22 +3512,6 @@ "@capacitor/core": ">=8.0.0" } }, - "node_modules/@capacitor-community/sqlite": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@capacitor-community/sqlite/-/sqlite-8.1.0.tgz", - "integrity": "sha512-yhKZDAVPDPcM3QE6UGB3LXyV25a6Rve1SjZ1aUpTE0E2isnYTVM0PG9+JOI241f+NdsHzPTE7ESJiYSqKsKnuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jeep-sqlite": "^2.7.2" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@capacitor/core": ">=8.0.0" - } - }, "node_modules/@capacitor/android": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-8.4.0.tgz", @@ -11338,13 +11321,6 @@ "node": ">=8" } }, - "node_modules/browser-fs-access": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/browser-fs-access/-/browser-fs-access-0.35.0.tgz", - "integrity": "sha512-sLoadumpRfsjprP8XzVjpQc0jK8yqHBx0PtUTGYj2fftT+P/t+uyDAQdMgGAPKD011in/O+YYGh7fIs0oG/viw==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/browser-process-hrtime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", @@ -16881,20 +16857,6 @@ "node": ">=10" } }, - "node_modules/jeep-sqlite": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/jeep-sqlite/-/jeep-sqlite-2.8.0.tgz", - "integrity": "sha512-FWNUP6OAmrUHwiW7H1xH5YUQ8tN2O4l4psT1sLd7DQtHd5PfrA1nvNdeKPNj+wQBtu7elJa8WoUibTytNTaaCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@stencil/core": "^4.20.0", - "browser-fs-access": "^0.35.0", - "jszip": "^3.10.1", - "localforage": "^1.10.0", - "sql.js": "^1.11.0" - } - }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -17176,62 +17138,6 @@ "node": ">=0.6.0" } }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "dev": true, - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/jszip/node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/jszip/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/jszip/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/jszip/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/karma-source-map-support": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", @@ -23312,13 +23218,6 @@ "optional": true, "peer": true }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true, - "license": "MIT" - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -23782,13 +23681,6 @@ "node": ">= 10.x" } }, - "node_modules/sql.js": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.1.tgz", - "integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==", - "dev": true, - "license": "MIT" - }, "node_modules/sshpk": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", diff --git a/package.json b/package.json index 3d2a0c0..252f8b7 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,6 @@ "@capacitor-community/apple-sign-in": "github:rdlabo/apple-sign-in#cap8_build", "@capacitor-community/facebook-login": "^8.0.0", "@capacitor-community/in-app-review": "^8.0.0", - "@capacitor-community/sqlite": "^8.1.0", "@capacitor/cli": ">=6.0.0 <9.0.0", "@capacitor/haptics": "^8.0.2", "@capacitor/keyboard": "^8.0.5", diff --git a/projects/kit/README.md b/projects/kit/README.md index dab5606..75dce5a 100644 --- a/projects/kit/README.md +++ b/projects/kit/README.md @@ -350,13 +350,17 @@ The native offline runtime uses `@capacitor-community/sqlite` on iOS and Android sync native projects: ```bash -npm install @capacitor-community/sqlite +# Capacitor 8 +npm install @capacitor-community/sqlite@^8.1.0 npx cap sync ``` +Use the plugin major matching the application's Capacitor major (`^6` for Capacitor 6, `^7` for Capacitor 7, +`^8.1.0` for Capacitor 8). + Add the required `CapacitorSQLite` plugin block to `capacitor.config.ts`. Encryption must be enabled — the kit opens -databases in encrypted mode and relies on the plugin's built-in secure secret storage (`setEncryptionSecret` / -`checkEncryptionSecret` in the device keychain / Android keystore): +databases in encrypted mode and relies on the plugin's built-in secure secret storage (`isSecretStored` / +`setEncryptionSecret` in the device keychain / Android keystore): ```ts // capacitor.config.ts @@ -373,15 +377,29 @@ plugins: { Follow the [@capacitor-community/sqlite installation guide](https://github.com/capacitor-community/sqlite#installation) for platform-specific steps and SQLCipher export-compliance notes. -Pass a stable `databaseName` and an `encryptionKey` generator to `provideOffline`. The kit invokes the generator only +Android must not back up or transfer the encrypted database independently from its keystore secret. Set +`android:allowBackup="false"`, `android:fullBackupContent="false"`, and +`android:dataExtractionRules="@xml/data_extraction_rules"` on ``. The referenced Android 12+ rules must +exclude at least the `database`, `sharedpref`, `root`, and `external` domains from both `cloud-backup` and +`device-transfer`, as shown in the plugin installation guide. + +Create the community plugin connection in the application, then pass it with a stable `databaseName` and a +`createEncryptionKey` generator to `provideOffline`. Keeping the runtime object application-supplied prevents the optional +native plugin from entering web-only `/offline` bundles. The kit invokes the generator only when the plugin has no secret yet, then stores the result in the plugin's Keychain / Android keystore. Later opens use that stored secret without invoking the generator. Never hard-code or derive the key from a user identifier, device identifier, or access token; generate a cryptographically random value for the first installation. ```ts +import { CapacitorSQLite, SQLiteConnection } from '@capacitor-community/sqlite'; + +const createRandomOfflineEncryptionKey = async () => + Array.from(crypto.getRandomValues(new Uint8Array(32)), (byte) => byte.toString(16).padStart(2, '0')).join(''); + provideOffline({ databaseName: 'product-offline', - encryptionKey: loadOfflineEncryptionKey, + sqliteConnection: new SQLiteConnection(CapacitorSQLite), + createEncryptionKey: createRandomOfflineEncryptionKey, // ...product policies, puller, and executor }); ``` @@ -486,7 +504,7 @@ provideOffline({ replicaSchema, replicaPuller: ProductReplicaPuller, commandExecutor: ProductCommandExecutor, - // ...request policies, databaseName, encryptionKey + // ...request policies, databaseName, createEncryptionKey }); ``` diff --git a/projects/kit/offline/src/lib/offline-kit-options.ts b/projects/kit/offline/src/lib/offline-kit-options.ts index b7484f5..dcce633 100644 --- a/projects/kit/offline/src/lib/offline-kit-options.ts +++ b/projects/kit/offline/src/lib/offline-kit-options.ts @@ -5,8 +5,8 @@ import type { OfflineReplicaSchemaBundle } from './offline-replica-schema'; export interface OfflineKitOptions { /** Encrypted SQLite database name used on iOS and Android. */ databaseName: string; - /** Resolves the native database key from secure device storage. Required on iOS and Android. */ - encryptionKey?: () => Promise; + /** Creates the native database encryption key on first install. Required on iOS and Android. */ + createEncryptionKey?: () => Promise; /** Versioned product replica schema applied to native SQLite during initialization. */ replicaSchema: OfflineReplicaSchemaBundle; } diff --git a/projects/kit/offline/src/lib/offline-provider.ts b/projects/kit/offline/src/lib/offline-provider.ts index 91c8ada..22f8375 100644 --- a/projects/kit/offline/src/lib/offline-provider.ts +++ b/projects/kit/offline/src/lib/offline-provider.ts @@ -14,7 +14,12 @@ import { provideOfflineRequestPolicy } from './offline-request-policy'; import type { OfflineReplicaPuller } from './offline-replica-puller'; import { OFFLINE_REPLICA_PULLER } from './offline-replica-puller'; import { OfflineSessionService } from './offline-session.service'; -import { SqliteOfflineRepository } from './sqlite-offline-repository'; +import { + COMMUNITY_SQLITE, + type CommunitySqliteConnection, + createCommunitySqliteDriver, + SqliteOfflineRepository, +} from './sqlite-offline-repository'; /** Configuration for the standard offline repository, outbox, and request-policy runtime. */ export interface ProvideOfflineOptions extends OfflineKitOptions { @@ -28,6 +33,8 @@ export interface ProvideOfflineOptions extends OfflineKitOptions { commandHooks?: Type; /** Optional additional providers required by product adapters. */ providers?: readonly Provider[]; + /** Application-installed `@capacitor-community/sqlite` connection. Required only on iOS and Android. */ + sqliteConnection?: CommunitySqliteConnection; } /** @@ -46,10 +53,14 @@ export function provideOffline(options: ProvideOfflineOptions): EnvironmentProvi provide: OFFLINE_KIT_OPTIONS, useValue: { databaseName: options.databaseName, - encryptionKey: options.encryptionKey, + createEncryptionKey: options.createEncryptionKey, replicaSchema: options.replicaSchema, }, }, + { + provide: COMMUNITY_SQLITE, + useValue: options.sqliteConnection ? createCommunitySqliteDriver(options.sqliteConnection) : null, + }, { provide: OFFLINE_REPOSITORY, useFactory: () => selectOfflineRepository(Capacitor.getPlatform(), inject(IonicOfflineRepository), inject(SqliteOfflineRepository)), diff --git a/projects/kit/offline/src/lib/sqlite-offline-repository.spec.ts b/projects/kit/offline/src/lib/sqlite-offline-repository.spec.ts index de066d1..d8043e7 100644 --- a/projects/kit/offline/src/lib/sqlite-offline-repository.spec.ts +++ b/projects/kit/offline/src/lib/sqlite-offline-repository.spec.ts @@ -199,8 +199,8 @@ describe('SqliteOfflineRepository community sqlite driver', () => { }); it('暗号鍵の生成関数をcommunity driverへ渡す', async () => { - const encryptionKey = vi.fn(async () => 'first-install-secret'); - const repository = createRepository(encryptionKey); + const createEncryptionKey = vi.fn(async () => 'first-install-secret'); + const repository = createRepository(createEncryptionKey); await repository.initialize(); const options = plugin.open.mock.calls[0]?.[0] as { createEncryptionKey?: () => Promise }; await expect(options.createEncryptionKey?.()).resolves.toBe('first-install-secret'); @@ -428,7 +428,7 @@ describe('SqliteOfflineRepository community sqlite driver', () => { }); function createRepository( - encryptionKey: () => Promise = async () => 'secret', + createEncryptionKey: () => Promise = async () => 'secret', options: { replicaSchema?: OfflineReplicaSchemaBundle } = {}, ): SqliteOfflineRepository { TestBed.configureTestingModule({ @@ -439,7 +439,7 @@ describe('SqliteOfflineRepository community sqlite driver', () => { provide: OFFLINE_KIT_OPTIONS, useValue: { databaseName: 'test-offline', - encryptionKey, + createEncryptionKey, replicaSchema: options.replicaSchema ?? replicaSchemaV1, }, }, @@ -1074,7 +1074,7 @@ describe('SqliteOfflineRepository replica rows', () => { provide: OFFLINE_KIT_OPTIONS, useValue: { databaseName: 'test-offline', - encryptionKey: async () => 'secret', + createEncryptionKey: async () => 'secret', replicaSchema: replicaSchemaV1WithGroup, }, }, diff --git a/projects/kit/offline/src/lib/sqlite-offline-repository.ts b/projects/kit/offline/src/lib/sqlite-offline-repository.ts index 97fa89f..c7ea4f8 100644 --- a/projects/kit/offline/src/lib/sqlite-offline-repository.ts +++ b/projects/kit/offline/src/lib/sqlite-offline-repository.ts @@ -1,5 +1,4 @@ import { inject, Injectable, InjectionToken } from '@angular/core'; -import { CapacitorSQLite, SQLiteConnection } from '@capacitor-community/sqlite'; import { OFFLINE_KIT_OPTIONS } from './offline-kit-options'; import { decodeOfflineReplicaValues, @@ -57,14 +56,12 @@ export interface CommunitySqliteConnection { } /** DI token for the native community SQLite driver. */ -export const COMMUNITY_SQLITE = new InjectionToken('COMMUNITY_SQLITE', { - factory: createCommunitySqliteDriver, +export const COMMUNITY_SQLITE = new InjectionToken('COMMUNITY_SQLITE', { + factory: () => null, }); /** Create the standard encrypted `@capacitor-community/sqlite` driver. */ -export function createCommunitySqliteDriver( - connection: CommunitySqliteConnection = new SQLiteConnection(CapacitorSQLite), -): CommunitySqliteDriver { +export function createCommunitySqliteDriver(connection: CommunitySqliteConnection): CommunitySqliteDriver { const databases = new Map(); const database = (databaseId: string): CommunitySqliteDatabase => { const value = databases.get(databaseId); @@ -312,9 +309,10 @@ export class SqliteOfflineRepository implements OfflineRepository { } async #open(): Promise { + if (!this.#sqlite) throw new Error('Native offline storage requires a community SQLite connection'); const { databaseId } = await this.#sqlite.open({ databaseName: this.#options.databaseName, - createEncryptionKey: this.#options.encryptionKey, + createEncryptionKey: this.#options.createEncryptionKey, }); this.#databaseId = databaseId; for (const statement of SCHEMA) await this.#execute(databaseId, statement); diff --git a/projects/kit/package.json b/projects/kit/package.json index fa5c10b..3ab1a3b 100644 --- a/projects/kit/package.json +++ b/projects/kit/package.json @@ -15,7 +15,6 @@ "@capacitor/core": ">=6.0.0 <9.0.0", "@capacitor/app": ">=6.0.0 <9.0.0", "@capawesome/capacitor-live-update": ">=6.0.0 <9.0.0", - "@capacitor-community/sqlite": ">=6.0.0 <9.0.0", "@capacitor/haptics": ">=6.0.0 <9.0.0", "@capacitor/keyboard": ">=6.0.0 <9.0.0", "@capacitor/network": ">=6.0.0 <9.0.0", @@ -37,9 +36,6 @@ "@capawesome/capacitor-live-update": { "optional": true }, - "@capacitor-community/sqlite": { - "optional": true - }, "@capacitor/preferences": { "optional": true },