diff --git a/projects/kit/README.md b/projects/kit/README.md index c7c9f62..75dce5a 100644 --- a/projects/kit/README.md +++ b/projects/kit/README.md @@ -338,56 +338,69 @@ 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 +# Capacitor 8 +npm install @capacitor-community/sqlite@^8.1.0 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. +Use the plugin major matching the application's Capacitor major (`^6` for Capacitor 6, `^7` for Capacitor 7, +`^8.1.0` for Capacitor 8). -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 (`isSecretStored` / +`setEncryptionSecret` in the device keychain / Android keystore): ```ts -import { SecurePreferences } from '@capawesome-team/capacitor-secure-preferences'; -import { Sqlite } from '@capawesome-team/capacitor-sqlite'; +// capacitor.config.ts +plugins: { + CapacitorSQLite: { + iosDatabaseLocation: 'Library/CapacitorDatabase', + iosIsEncryption: true, + iosKeychainPrefix: '', + androidIsEncryption: true, + }, +}, +``` -const OFFLINE_DATABASE_KEY = 'product-offline-database-key'; +Follow the [@capacitor-community/sqlite installation guide](https://github.com/capacitor-community/sqlite#installation) +for platform-specific steps and SQLCipher export-compliance notes. -async function offlineDatabaseKey(): Promise { - const { value } = await SecurePreferences.get({ key: OFFLINE_DATABASE_KEY }); - if (value) return value; +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. - 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; -} +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', + sqliteConnection: new SQLiteConnection(CapacitorSQLite), + createEncryptionKey: createRandomOfflineEncryptionKey, // ...product policies, puller, and executor - sqlitePlugin: Sqlite, - encryptionKey: offlineDatabaseKey, }); ``` @@ -397,10 +410,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 +468,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 +494,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 +504,7 @@ provideOffline({ replicaSchema, replicaPuller: ProductReplicaPuller, commandExecutor: ProductCommandExecutor, - // ...request policies, sqlitePlugin, encryptionKey + // ...request policies, databaseName, createEncryptionKey }); ``` @@ -513,14 +519,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-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 10ea7b9..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 { CAPAWESOME_SQLITE, type CapawesomeSqlitePlugin, 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,15 +33,15 @@ 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; + /** Application-installed `@capacitor-community/sqlite` connection. Required only on iOS and Android. */ + sqliteConnection?: CommunitySqliteConnection; } /** * 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. */ @@ -48,11 +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: CAPAWESOME_SQLITE, useValue: options.sqlitePlugin ?? null }, + { + 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 61d8e4d..d8043e7 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 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'); }); 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; @@ -357,18 +428,18 @@ describe('SqliteOfflineRepository Capawesome adapter', () => { }); function createRepository( - encryptionKey: () => Promise = async () => 'secret', + createEncryptionKey: () => Promise = async () => 'secret', options: { replicaSchema?: OfflineReplicaSchemaBundle } = {}, ): SqliteOfflineRepository { TestBed.configureTestingModule({ providers: [ SqliteOfflineRepository, - { provide: CAPAWESOME_SQLITE, useValue: plugin }, + { provide: COMMUNITY_SQLITE, useValue: plugin }, { provide: OFFLINE_KIT_OPTIONS, useValue: { databaseName: 'test-offline', - encryptionKey, + createEncryptionKey, replicaSchema: options.replicaSchema ?? replicaSchemaV1, }, }, @@ -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,12 +1069,12 @@ describe('SqliteOfflineRepository replica rows', () => { TestBed.configureTestingModule({ providers: [ SqliteOfflineRepository, - { provide: CAPAWESOME_SQLITE, useValue: plugin }, + { provide: COMMUNITY_SQLITE, useValue: plugin }, { 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 04ae86c..c7ea4f8 100644 --- a/projects/kit/offline/src/lib/sqlite-offline-repository.ts +++ b/projects/kit/offline/src/lib/sqlite-offline-repository.ts @@ -19,9 +19,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 +32,74 @@ 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', { +/** 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: () => null, }); +/** Create the standard encrypted `@capacitor-community/sqlite` driver. */ +export function createCommunitySqliteDriver(connection: CommunitySqliteConnection): 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 +145,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 +309,10 @@ 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'); + if (!this.#sqlite) throw new Error('Native offline storage requires a community SQLite connection'); const { databaseId } = await this.#sqlite.open({ - path: `${this.#options.databaseName}.sqlite3`, - encryptionKey, - readOnly: false, + databaseName: this.#options.databaseName, + 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 b15128f..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", - "@capawesome-team/capacitor-sqlite": ">=0.3.0 <1.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 }, - "@capawesome-team/capacitor-sqlite": { - "optional": true - }, "@capacitor/preferences": { "optional": true },