11 changed files with 4095 additions and 2125 deletions
@ -0,0 +1,267 @@ |
|||
--- |
|||
description: |
|||
globs: |
|||
alwaysApply: true |
|||
--- |
|||
# wa-sqlite Usage Guide |
|||
|
|||
## Table of Contents |
|||
- [1. Overview](#1-overview) |
|||
- [2. Installation](#2-installation) |
|||
- [3. Basic Setup](#3-basic-setup) |
|||
- [3.1 Import and Initialize](#31-import-and-initialize) |
|||
- [3.2 Basic Database Operations](#32-basic-database-operations) |
|||
- [4. Virtual File Systems (VFS)](#4-virtual-file-systems-vfs) |
|||
- [4.1 Available VFS Options](#41-available-vfs-options) |
|||
- [4.2 Using a VFS](#42-using-a-vfs) |
|||
- [5. Best Practices](#5-best-practices) |
|||
- [5.1 Error Handling](#51-error-handling) |
|||
- [5.2 Transaction Management](#52-transaction-management) |
|||
- [5.3 Prepared Statements](#53-prepared-statements) |
|||
- [6. Performance Considerations](#6-performance-considerations) |
|||
- [7. Common Issues and Solutions](#7-common-issues-and-solutions) |
|||
- [8. TypeScript Support](#8-typescript-support) |
|||
|
|||
## 1. Overview |
|||
wa-sqlite is a WebAssembly build of SQLite that enables SQLite database operations in web browsers and JavaScript environments. It provides both synchronous and asynchronous builds, with support for custom virtual file systems (VFS) for persistent storage. |
|||
|
|||
## 2. Installation |
|||
```bash |
|||
npm install wa-sqlite |
|||
# or |
|||
yarn add wa-sqlite |
|||
``` |
|||
|
|||
## 3. Basic Setup |
|||
|
|||
### 3.1 Import and Initialize |
|||
```javascript |
|||
// Choose one of these imports based on your needs: |
|||
// - wa-sqlite.mjs: Synchronous build |
|||
// - wa-sqlite-async.mjs: Asynchronous build (required for async VFS) |
|||
// - wa-sqlite-jspi.mjs: JSPI-based async build (experimental, Chromium only) |
|||
import SQLiteESMFactory from 'wa-sqlite/dist/wa-sqlite.mjs'; |
|||
import * as SQLite from 'wa-sqlite'; |
|||
|
|||
async function initDatabase() { |
|||
// Initialize SQLite module |
|||
const module = await SQLiteESMFactory(); |
|||
const sqlite3 = SQLite.Factory(module); |
|||
|
|||
// Open database (returns a Promise) |
|||
const db = await sqlite3.open_v2('myDatabase'); |
|||
return { sqlite3, db }; |
|||
} |
|||
``` |
|||
|
|||
### 3.2 Basic Database Operations |
|||
```javascript |
|||
async function basicOperations() { |
|||
const { sqlite3, db } = await initDatabase(); |
|||
|
|||
try { |
|||
// Create a table |
|||
await sqlite3.exec(db, ` |
|||
CREATE TABLE IF NOT EXISTS users ( |
|||
id INTEGER PRIMARY KEY, |
|||
name TEXT NOT NULL, |
|||
email TEXT UNIQUE |
|||
) |
|||
`); |
|||
|
|||
// Insert data |
|||
await sqlite3.exec(db, ` |
|||
INSERT INTO users (name, email) |
|||
VALUES ('John Doe', 'john@example.com') |
|||
`); |
|||
|
|||
// Query data |
|||
const results = []; |
|||
await sqlite3.exec(db, 'SELECT * FROM users', (row, columns) => { |
|||
results.push({ row, columns }); |
|||
}); |
|||
|
|||
return results; |
|||
} finally { |
|||
// Always close the database when done |
|||
await sqlite3.close(db); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## 4. Virtual File Systems (VFS) |
|||
|
|||
### 4.1 Available VFS Options |
|||
wa-sqlite provides several VFS implementations for persistent storage: |
|||
|
|||
1. **IDBBatchAtomicVFS** (Recommended for general use) |
|||
- Uses IndexedDB with batch atomic writes |
|||
- Works in all contexts (Window, Worker, Service Worker) |
|||
- Supports WAL mode |
|||
- Best performance with `PRAGMA synchronous=normal` |
|||
|
|||
2. **IDBMirrorVFS** |
|||
- Keeps files in memory, persists to IndexedDB |
|||
- Works in all contexts |
|||
- Good for smaller databases |
|||
|
|||
3. **OPFS-based VFS** (Origin Private File System) |
|||
- Various implementations available: |
|||
- AccessHandlePoolVFS |
|||
- OPFSAdaptiveVFS |
|||
- OPFSCoopSyncVFS |
|||
- OPFSPermutedVFS |
|||
- Better performance but limited to Worker contexts |
|||
|
|||
### 4.2 Using a VFS |
|||
```javascript |
|||
import { IDBBatchAtomicVFS } from 'wa-sqlite/src/examples/IDBBatchAtomicVFS.js'; |
|||
import SQLiteESMFactory from 'wa-sqlite/dist/wa-sqlite-async.mjs'; |
|||
import * as SQLite from 'wa-sqlite'; |
|||
|
|||
async function initDatabaseWithVFS() { |
|||
const module = await SQLiteESMFactory(); |
|||
const sqlite3 = SQLite.Factory(module); |
|||
|
|||
// Register VFS |
|||
const vfs = await IDBBatchAtomicVFS.create('myApp', module); |
|||
sqlite3.vfs_register(vfs, true); |
|||
|
|||
// Open database with VFS |
|||
const db = await sqlite3.open_v2('myDatabase'); |
|||
|
|||
// Configure for better performance |
|||
await sqlite3.exec(db, 'PRAGMA synchronous = normal'); |
|||
await sqlite3.exec(db, 'PRAGMA journal_mode = WAL'); |
|||
|
|||
return { sqlite3, db }; |
|||
} |
|||
``` |
|||
|
|||
## 5. Best Practices |
|||
|
|||
### 5.1 Error Handling |
|||
```javascript |
|||
async function safeDatabaseOperation() { |
|||
const { sqlite3, db } = await initDatabase(); |
|||
|
|||
try { |
|||
await sqlite3.exec(db, 'SELECT * FROM non_existent_table'); |
|||
} catch (error) { |
|||
if (error.code === SQLite.SQLITE_ERROR) { |
|||
console.error('SQL error:', error.message); |
|||
} else { |
|||
console.error('Database error:', error); |
|||
} |
|||
} finally { |
|||
await sqlite3.close(db); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 5.2 Transaction Management |
|||
```javascript |
|||
async function transactionExample() { |
|||
const { sqlite3, db } = await initDatabase(); |
|||
|
|||
try { |
|||
await sqlite3.exec(db, 'BEGIN TRANSACTION'); |
|||
|
|||
// Perform multiple operations |
|||
await sqlite3.exec(db, 'INSERT INTO users (name) VALUES (?)', ['Alice']); |
|||
await sqlite3.exec(db, 'INSERT INTO users (name) VALUES (?)', ['Bob']); |
|||
|
|||
await sqlite3.exec(db, 'COMMIT'); |
|||
} catch (error) { |
|||
await sqlite3.exec(db, 'ROLLBACK'); |
|||
throw error; |
|||
} finally { |
|||
await sqlite3.close(db); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 5.3 Prepared Statements |
|||
```javascript |
|||
async function preparedStatementExample() { |
|||
const { sqlite3, db } = await initDatabase(); |
|||
|
|||
try { |
|||
// Prepare statement |
|||
const stmt = await sqlite3.prepare(db, 'SELECT * FROM users WHERE id = ?'); |
|||
|
|||
// Execute with different parameters |
|||
await sqlite3.bind(stmt, 1, 1); |
|||
while (await sqlite3.step(stmt) === SQLite.SQLITE_ROW) { |
|||
const row = sqlite3.row(stmt); |
|||
console.log(row); |
|||
} |
|||
|
|||
// Reset and reuse |
|||
await sqlite3.reset(stmt); |
|||
await sqlite3.bind(stmt, 1, 2); |
|||
// ... execute again |
|||
|
|||
await sqlite3.finalize(stmt); |
|||
} finally { |
|||
await sqlite3.close(db); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## 6. Performance Considerations |
|||
|
|||
1. **VFS Selection** |
|||
- Use IDBBatchAtomicVFS for general-purpose applications |
|||
- Consider OPFS-based VFS for better performance in Worker contexts |
|||
- Use MemoryVFS for temporary databases |
|||
|
|||
2. **Configuration** |
|||
- Set appropriate page size (default is usually fine) |
|||
- Use WAL mode for better concurrency |
|||
- Consider `PRAGMA synchronous=normal` for better performance |
|||
- Adjust cache size based on your needs |
|||
|
|||
3. **Concurrency** |
|||
- Use transactions for multiple operations |
|||
- Be aware of VFS-specific concurrency limitations |
|||
- Consider using Web Workers for heavy database operations |
|||
|
|||
## 7. Common Issues and Solutions |
|||
|
|||
1. **Database Locking** |
|||
- Use appropriate transaction isolation levels |
|||
- Implement retry logic for busy errors |
|||
- Consider using WAL mode |
|||
|
|||
2. **Storage Limitations** |
|||
- Be aware of browser storage quotas |
|||
- Implement cleanup strategies |
|||
- Monitor database size |
|||
|
|||
3. **Cross-Context Access** |
|||
- Use appropriate VFS for your context |
|||
- Consider message passing for cross-context communication |
|||
- Be aware of storage access limitations |
|||
|
|||
## 8. TypeScript Support |
|||
wa-sqlite includes TypeScript definitions. The main types are: |
|||
|
|||
```typescript |
|||
type SQLiteCompatibleType = number | string | Uint8Array | Array<number> | bigint | null; |
|||
|
|||
interface SQLiteAPI { |
|||
open_v2(filename: string, flags?: number, zVfs?: string): Promise<number>; |
|||
exec(db: number, sql: string, callback?: (row: any[], columns: string[]) => void): Promise<number>; |
|||
close(db: number): Promise<number>; |
|||
// ... other methods |
|||
} |
|||
``` |
|||
|
|||
## Additional Resources |
|||
|
|||
- [Official GitHub Repository](https://github.com/rhashimoto/wa-sqlite) |
|||
- [Online Demo](https://rhashimoto.github.io/wa-sqlite/demo/) |
|||
- [API Reference](https://rhashimoto.github.io/wa-sqlite/docs/) |
|||
- [FAQ](https://github.com/rhashimoto/wa-sqlite/issues?q=is%3Aissue+label%3Afaq+) |
|||
- [Discussion Forums](https://github.com/rhashimoto/wa-sqlite/discussions) |
@ -0,0 +1,389 @@ |
|||
# Dexie to SQLite Mapping Guide |
|||
|
|||
## Schema Mapping |
|||
|
|||
### Current Dexie Schema |
|||
```typescript |
|||
// Current Dexie schema |
|||
const db = new Dexie('TimeSafariDB'); |
|||
|
|||
db.version(1).stores({ |
|||
accounts: 'did, publicKeyHex, createdAt, updatedAt', |
|||
settings: 'key, value, updatedAt', |
|||
contacts: 'id, did, name, createdAt, updatedAt' |
|||
}); |
|||
``` |
|||
|
|||
### New SQLite Schema |
|||
```sql |
|||
-- New SQLite schema |
|||
CREATE TABLE accounts ( |
|||
did TEXT PRIMARY KEY, |
|||
public_key_hex TEXT NOT NULL, |
|||
created_at INTEGER NOT NULL, |
|||
updated_at INTEGER NOT NULL |
|||
); |
|||
|
|||
CREATE TABLE settings ( |
|||
key TEXT PRIMARY KEY, |
|||
value TEXT NOT NULL, |
|||
updated_at INTEGER NOT NULL |
|||
); |
|||
|
|||
CREATE TABLE contacts ( |
|||
id TEXT PRIMARY KEY, |
|||
did TEXT NOT NULL, |
|||
name TEXT, |
|||
created_at INTEGER NOT NULL, |
|||
updated_at INTEGER NOT NULL, |
|||
FOREIGN KEY (did) REFERENCES accounts(did) |
|||
); |
|||
|
|||
-- Indexes for performance |
|||
CREATE INDEX idx_accounts_created_at ON accounts(created_at); |
|||
CREATE INDEX idx_contacts_did ON contacts(did); |
|||
CREATE INDEX idx_settings_updated_at ON settings(updated_at); |
|||
``` |
|||
|
|||
## Query Mapping |
|||
|
|||
### 1. Account Operations |
|||
|
|||
#### Get Account by DID |
|||
```typescript |
|||
// Dexie |
|||
const account = await db.accounts.get(did); |
|||
|
|||
// SQLite |
|||
const account = await db.selectOne(` |
|||
SELECT * FROM accounts WHERE did = ? |
|||
`, [did]); |
|||
``` |
|||
|
|||
#### Get All Accounts |
|||
```typescript |
|||
// Dexie |
|||
const accounts = await db.accounts.toArray(); |
|||
|
|||
// SQLite |
|||
const accounts = await db.selectAll(` |
|||
SELECT * FROM accounts ORDER BY created_at DESC |
|||
`); |
|||
``` |
|||
|
|||
#### Add Account |
|||
```typescript |
|||
// Dexie |
|||
await db.accounts.add({ |
|||
did, |
|||
publicKeyHex, |
|||
createdAt: Date.now(), |
|||
updatedAt: Date.now() |
|||
}); |
|||
|
|||
// SQLite |
|||
await db.execute(` |
|||
INSERT INTO accounts (did, public_key_hex, created_at, updated_at) |
|||
VALUES (?, ?, ?, ?) |
|||
`, [did, publicKeyHex, Date.now(), Date.now()]); |
|||
``` |
|||
|
|||
#### Update Account |
|||
```typescript |
|||
// Dexie |
|||
await db.accounts.update(did, { |
|||
publicKeyHex, |
|||
updatedAt: Date.now() |
|||
}); |
|||
|
|||
// SQLite |
|||
await db.execute(` |
|||
UPDATE accounts |
|||
SET public_key_hex = ?, updated_at = ? |
|||
WHERE did = ? |
|||
`, [publicKeyHex, Date.now(), did]); |
|||
``` |
|||
|
|||
### 2. Settings Operations |
|||
|
|||
#### Get Setting |
|||
```typescript |
|||
// Dexie |
|||
const setting = await db.settings.get(key); |
|||
|
|||
// SQLite |
|||
const setting = await db.selectOne(` |
|||
SELECT * FROM settings WHERE key = ? |
|||
`, [key]); |
|||
``` |
|||
|
|||
#### Set Setting |
|||
```typescript |
|||
// Dexie |
|||
await db.settings.put({ |
|||
key, |
|||
value, |
|||
updatedAt: Date.now() |
|||
}); |
|||
|
|||
// SQLite |
|||
await db.execute(` |
|||
INSERT INTO settings (key, value, updated_at) |
|||
VALUES (?, ?, ?) |
|||
ON CONFLICT(key) DO UPDATE SET |
|||
value = excluded.value, |
|||
updated_at = excluded.updated_at |
|||
`, [key, value, Date.now()]); |
|||
``` |
|||
|
|||
### 3. Contact Operations |
|||
|
|||
#### Get Contacts by Account |
|||
```typescript |
|||
// Dexie |
|||
const contacts = await db.contacts |
|||
.where('did') |
|||
.equals(accountDid) |
|||
.toArray(); |
|||
|
|||
// SQLite |
|||
const contacts = await db.selectAll(` |
|||
SELECT * FROM contacts |
|||
WHERE did = ? |
|||
ORDER BY created_at DESC |
|||
`, [accountDid]); |
|||
``` |
|||
|
|||
#### Add Contact |
|||
```typescript |
|||
// Dexie |
|||
await db.contacts.add({ |
|||
id: generateId(), |
|||
did: accountDid, |
|||
name, |
|||
createdAt: Date.now(), |
|||
updatedAt: Date.now() |
|||
}); |
|||
|
|||
// SQLite |
|||
await db.execute(` |
|||
INSERT INTO contacts (id, did, name, created_at, updated_at) |
|||
VALUES (?, ?, ?, ?, ?) |
|||
`, [generateId(), accountDid, name, Date.now(), Date.now()]); |
|||
``` |
|||
|
|||
## Transaction Mapping |
|||
|
|||
### Batch Operations |
|||
```typescript |
|||
// Dexie |
|||
await db.transaction('rw', [db.accounts, db.contacts], async () => { |
|||
await db.accounts.add(account); |
|||
await db.contacts.bulkAdd(contacts); |
|||
}); |
|||
|
|||
// SQLite |
|||
await db.transaction(async (tx) => { |
|||
await tx.execute(` |
|||
INSERT INTO accounts (did, public_key_hex, created_at, updated_at) |
|||
VALUES (?, ?, ?, ?) |
|||
`, [account.did, account.publicKeyHex, account.createdAt, account.updatedAt]); |
|||
|
|||
for (const contact of contacts) { |
|||
await tx.execute(` |
|||
INSERT INTO contacts (id, did, name, created_at, updated_at) |
|||
VALUES (?, ?, ?, ?, ?) |
|||
`, [contact.id, contact.did, contact.name, contact.createdAt, contact.updatedAt]); |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
## Migration Helper Functions |
|||
|
|||
### 1. Data Export (Dexie to JSON) |
|||
```typescript |
|||
async function exportDexieData(): Promise<MigrationData> { |
|||
const db = new Dexie('TimeSafariDB'); |
|||
|
|||
return { |
|||
accounts: await db.accounts.toArray(), |
|||
settings: await db.settings.toArray(), |
|||
contacts: await db.contacts.toArray(), |
|||
metadata: { |
|||
version: '1.0.0', |
|||
timestamp: Date.now(), |
|||
dexieVersion: Dexie.version |
|||
} |
|||
}; |
|||
} |
|||
``` |
|||
|
|||
### 2. Data Import (JSON to SQLite) |
|||
```typescript |
|||
async function importToSQLite(data: MigrationData): Promise<void> { |
|||
const db = await getSQLiteConnection(); |
|||
|
|||
await db.transaction(async (tx) => { |
|||
// Import accounts |
|||
for (const account of data.accounts) { |
|||
await tx.execute(` |
|||
INSERT INTO accounts (did, public_key_hex, created_at, updated_at) |
|||
VALUES (?, ?, ?, ?) |
|||
`, [account.did, account.publicKeyHex, account.createdAt, account.updatedAt]); |
|||
} |
|||
|
|||
// Import settings |
|||
for (const setting of data.settings) { |
|||
await tx.execute(` |
|||
INSERT INTO settings (key, value, updated_at) |
|||
VALUES (?, ?, ?) |
|||
`, [setting.key, setting.value, setting.updatedAt]); |
|||
} |
|||
|
|||
// Import contacts |
|||
for (const contact of data.contacts) { |
|||
await tx.execute(` |
|||
INSERT INTO contacts (id, did, name, created_at, updated_at) |
|||
VALUES (?, ?, ?, ?, ?) |
|||
`, [contact.id, contact.did, contact.name, contact.createdAt, contact.updatedAt]); |
|||
} |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
### 3. Verification |
|||
```typescript |
|||
async function verifyMigration(dexieData: MigrationData): Promise<boolean> { |
|||
const db = await getSQLiteConnection(); |
|||
|
|||
// Verify account count |
|||
const accountCount = await db.selectValue( |
|||
'SELECT COUNT(*) FROM accounts' |
|||
); |
|||
if (accountCount !== dexieData.accounts.length) { |
|||
return false; |
|||
} |
|||
|
|||
// Verify settings count |
|||
const settingsCount = await db.selectValue( |
|||
'SELECT COUNT(*) FROM settings' |
|||
); |
|||
if (settingsCount !== dexieData.settings.length) { |
|||
return false; |
|||
} |
|||
|
|||
// Verify contacts count |
|||
const contactsCount = await db.selectValue( |
|||
'SELECT COUNT(*) FROM contacts' |
|||
); |
|||
if (contactsCount !== dexieData.contacts.length) { |
|||
return false; |
|||
} |
|||
|
|||
// Verify data integrity |
|||
for (const account of dexieData.accounts) { |
|||
const migratedAccount = await db.selectOne( |
|||
'SELECT * FROM accounts WHERE did = ?', |
|||
[account.did] |
|||
); |
|||
if (!migratedAccount || |
|||
migratedAccount.public_key_hex !== account.publicKeyHex) { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
``` |
|||
|
|||
## Performance Considerations |
|||
|
|||
### 1. Indexing |
|||
- Dexie automatically creates indexes based on the schema |
|||
- SQLite requires explicit index creation |
|||
- Added indexes for frequently queried fields |
|||
|
|||
### 2. Batch Operations |
|||
- Dexie has built-in bulk operations |
|||
- SQLite uses transactions for batch operations |
|||
- Consider chunking large datasets |
|||
|
|||
### 3. Query Optimization |
|||
- Dexie uses IndexedDB's native indexing |
|||
- SQLite requires explicit query optimization |
|||
- Use prepared statements for repeated queries |
|||
|
|||
## Error Handling |
|||
|
|||
### 1. Common Errors |
|||
```typescript |
|||
// Dexie errors |
|||
try { |
|||
await db.accounts.add(account); |
|||
} catch (error) { |
|||
if (error instanceof Dexie.ConstraintError) { |
|||
// Handle duplicate key |
|||
} |
|||
} |
|||
|
|||
// SQLite errors |
|||
try { |
|||
await db.execute(` |
|||
INSERT INTO accounts (did, public_key_hex, created_at, updated_at) |
|||
VALUES (?, ?, ?, ?) |
|||
`, [account.did, account.publicKeyHex, account.createdAt, account.updatedAt]); |
|||
} catch (error) { |
|||
if (error.code === 'SQLITE_CONSTRAINT') { |
|||
// Handle duplicate key |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 2. Transaction Recovery |
|||
```typescript |
|||
// Dexie transaction |
|||
try { |
|||
await db.transaction('rw', db.accounts, async () => { |
|||
// Operations |
|||
}); |
|||
} catch (error) { |
|||
// Dexie automatically rolls back |
|||
} |
|||
|
|||
// SQLite transaction |
|||
const db = await getSQLiteConnection(); |
|||
try { |
|||
await db.transaction(async (tx) => { |
|||
// Operations |
|||
}); |
|||
} catch (error) { |
|||
// SQLite automatically rolls back |
|||
await db.execute('ROLLBACK'); |
|||
} |
|||
``` |
|||
|
|||
## Migration Strategy |
|||
|
|||
1. **Preparation** |
|||
- Export all Dexie data |
|||
- Verify data integrity |
|||
- Create SQLite schema |
|||
- Setup indexes |
|||
|
|||
2. **Migration** |
|||
- Import data in transactions |
|||
- Verify each batch |
|||
- Handle errors gracefully |
|||
- Maintain backup |
|||
|
|||
3. **Verification** |
|||
- Compare record counts |
|||
- Verify data integrity |
|||
- Test common queries |
|||
- Validate relationships |
|||
|
|||
4. **Cleanup** |
|||
- Remove Dexie database |
|||
- Clear IndexedDB storage |
|||
- Update application code |
|||
- Remove old dependencies |
@ -0,0 +1,554 @@ |
|||
# Migration Guide: Dexie to wa-sqlite |
|||
|
|||
## Overview |
|||
|
|||
This document outlines the migration process from Dexie.js to wa-sqlite for the TimeSafari app's storage implementation. The migration aims to provide a consistent SQLite-based storage solution across all platforms while maintaining data integrity and ensuring a smooth transition for users. |
|||
|
|||
## Migration Goals |
|||
|
|||
1. **Data Integrity** |
|||
- Preserve all existing data |
|||
- Maintain data relationships |
|||
- Ensure data consistency |
|||
|
|||
2. **Performance** |
|||
- Improve query performance |
|||
- Reduce storage overhead |
|||
- Optimize for platform-specific features |
|||
|
|||
3. **Security** |
|||
- Maintain or improve encryption |
|||
- Preserve access controls |
|||
- Enhance data protection |
|||
|
|||
4. **User Experience** |
|||
- Zero data loss |
|||
- Minimal downtime |
|||
- Automatic migration where possible |
|||
|
|||
## Prerequisites |
|||
|
|||
1. **Backup Requirements** |
|||
```typescript |
|||
interface MigrationBackup { |
|||
timestamp: number; |
|||
accounts: Account[]; |
|||
settings: Setting[]; |
|||
contacts: Contact[]; |
|||
metadata: { |
|||
version: string; |
|||
platform: string; |
|||
dexieVersion: string; |
|||
}; |
|||
} |
|||
``` |
|||
|
|||
2. **Storage Requirements** |
|||
- Sufficient IndexedDB quota |
|||
- Available disk space for SQLite |
|||
- Backup storage space |
|||
|
|||
3. **Platform Support** |
|||
- Web: Modern browser with IndexedDB support |
|||
- iOS: iOS 13+ with SQLite support |
|||
- Android: Android 5+ with SQLite support |
|||
- Electron: Latest version with SQLite support |
|||
|
|||
## Migration Process |
|||
|
|||
### 1. Preparation |
|||
|
|||
```typescript |
|||
// src/services/storage/migration/MigrationService.ts |
|||
export class MigrationService { |
|||
private static instance: MigrationService; |
|||
private backup: MigrationBackup | null = null; |
|||
|
|||
async prepare(): Promise<void> { |
|||
try { |
|||
// 1. Check prerequisites |
|||
await this.checkPrerequisites(); |
|||
|
|||
// 2. Create backup |
|||
this.backup = await this.createBackup(); |
|||
|
|||
// 3. Verify backup integrity |
|||
await this.verifyBackup(); |
|||
|
|||
// 4. Initialize wa-sqlite |
|||
await this.initializeWaSqlite(); |
|||
} catch (error) { |
|||
throw new StorageError( |
|||
'Migration preparation failed', |
|||
StorageErrorCodes.MIGRATION_FAILED, |
|||
error |
|||
); |
|||
} |
|||
} |
|||
|
|||
private async checkPrerequisites(): Promise<void> { |
|||
// Check IndexedDB availability |
|||
if (!window.indexedDB) { |
|||
throw new StorageError( |
|||
'IndexedDB not available', |
|||
StorageErrorCodes.INITIALIZATION_FAILED |
|||
); |
|||
} |
|||
|
|||
// Check storage quota |
|||
const quota = await navigator.storage.estimate(); |
|||
if (quota.quota && quota.usage && quota.usage > quota.quota * 0.9) { |
|||
throw new StorageError( |
|||
'Insufficient storage space', |
|||
StorageErrorCodes.STORAGE_FULL |
|||
); |
|||
} |
|||
|
|||
// Check platform support |
|||
const capabilities = await PlatformDetection.getCapabilities(); |
|||
if (!capabilities.hasFileSystem) { |
|||
throw new StorageError( |
|||
'Platform does not support required features', |
|||
StorageErrorCodes.INITIALIZATION_FAILED |
|||
); |
|||
} |
|||
} |
|||
|
|||
private async createBackup(): Promise<MigrationBackup> { |
|||
const dexieDB = new Dexie('TimeSafariDB'); |
|||
|
|||
return { |
|||
timestamp: Date.now(), |
|||
accounts: await dexieDB.accounts.toArray(), |
|||
settings: await dexieDB.settings.toArray(), |
|||
contacts: await dexieDB.contacts.toArray(), |
|||
metadata: { |
|||
version: '1.0.0', |
|||
platform: await PlatformDetection.getPlatform(), |
|||
dexieVersion: Dexie.version |
|||
} |
|||
}; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 2. Data Migration |
|||
|
|||
```typescript |
|||
// src/services/storage/migration/DataMigration.ts |
|||
export class DataMigration { |
|||
async migrate(backup: MigrationBackup): Promise<void> { |
|||
try { |
|||
// 1. Create new database schema |
|||
await this.createSchema(); |
|||
|
|||
// 2. Migrate accounts |
|||
await this.migrateAccounts(backup.accounts); |
|||
|
|||
// 3. Migrate settings |
|||
await this.migrateSettings(backup.settings); |
|||
|
|||
// 4. Migrate contacts |
|||
await this.migrateContacts(backup.contacts); |
|||
|
|||
// 5. Verify migration |
|||
await this.verifyMigration(backup); |
|||
} catch (error) { |
|||
// 6. Handle failure |
|||
await this.handleMigrationFailure(error, backup); |
|||
} |
|||
} |
|||
|
|||
private async migrateAccounts(accounts: Account[]): Promise<void> { |
|||
const db = await this.getWaSqliteConnection(); |
|||
|
|||
// Use transaction for atomicity |
|||
await db.transaction(async (tx) => { |
|||
for (const account of accounts) { |
|||
await tx.execute(` |
|||
INSERT INTO accounts (did, public_key_hex, created_at, updated_at) |
|||
VALUES (?, ?, ?, ?) |
|||
`, [ |
|||
account.did, |
|||
account.publicKeyHex, |
|||
account.createdAt, |
|||
account.updatedAt |
|||
]); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private async verifyMigration(backup: MigrationBackup): Promise<void> { |
|||
const db = await this.getWaSqliteConnection(); |
|||
|
|||
// Verify account count |
|||
const accountCount = await db.selectValue( |
|||
'SELECT COUNT(*) FROM accounts' |
|||
); |
|||
if (accountCount !== backup.accounts.length) { |
|||
throw new StorageError( |
|||
'Account count mismatch', |
|||
StorageErrorCodes.VERIFICATION_FAILED |
|||
); |
|||
} |
|||
|
|||
// Verify data integrity |
|||
await this.verifyDataIntegrity(backup); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 3. Rollback Strategy |
|||
|
|||
```typescript |
|||
// src/services/storage/migration/RollbackService.ts |
|||
export class RollbackService { |
|||
async rollback(backup: MigrationBackup): Promise<void> { |
|||
try { |
|||
// 1. Stop all database operations |
|||
await this.stopDatabaseOperations(); |
|||
|
|||
// 2. Restore from backup |
|||
await this.restoreFromBackup(backup); |
|||
|
|||
// 3. Verify restoration |
|||
await this.verifyRestoration(backup); |
|||
|
|||
// 4. Clean up wa-sqlite |
|||
await this.cleanupWaSqlite(); |
|||
} catch (error) { |
|||
throw new StorageError( |
|||
'Rollback failed', |
|||
StorageErrorCodes.ROLLBACK_FAILED, |
|||
error |
|||
); |
|||
} |
|||
} |
|||
|
|||
private async restoreFromBackup(backup: MigrationBackup): Promise<void> { |
|||
const dexieDB = new Dexie('TimeSafariDB'); |
|||
|
|||
// Restore accounts |
|||
await dexieDB.accounts.bulkPut(backup.accounts); |
|||
|
|||
// Restore settings |
|||
await dexieDB.settings.bulkPut(backup.settings); |
|||
|
|||
// Restore contacts |
|||
await dexieDB.contacts.bulkPut(backup.contacts); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## Migration UI |
|||
|
|||
```vue |
|||
<!-- src/components/MigrationProgress.vue --> |
|||
<template> |
|||
<div class="migration-progress"> |
|||
<h2>Database Migration</h2> |
|||
|
|||
<div class="progress-container"> |
|||
<div class="progress-bar" :style="{ width: `${progress}%` }" /> |
|||
<div class="progress-text">{{ progress }}%</div> |
|||
</div> |
|||
|
|||
<div class="status-message">{{ statusMessage }}</div> |
|||
|
|||
<div v-if="error" class="error-message"> |
|||
{{ error }} |
|||
<button @click="retryMigration">Retry</button> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { ref, onMounted } from 'vue'; |
|||
import { MigrationService } from '@/services/storage/migration/MigrationService'; |
|||
|
|||
const progress = ref(0); |
|||
const statusMessage = ref('Preparing migration...'); |
|||
const error = ref<string | null>(null); |
|||
|
|||
const migrationService = MigrationService.getInstance(); |
|||
|
|||
async function startMigration() { |
|||
try { |
|||
// 1. Preparation |
|||
statusMessage.value = 'Creating backup...'; |
|||
await migrationService.prepare(); |
|||
progress.value = 20; |
|||
|
|||
// 2. Data migration |
|||
statusMessage.value = 'Migrating data...'; |
|||
await migrationService.migrate(); |
|||
progress.value = 80; |
|||
|
|||
// 3. Verification |
|||
statusMessage.value = 'Verifying migration...'; |
|||
await migrationService.verify(); |
|||
progress.value = 100; |
|||
|
|||
statusMessage.value = 'Migration completed successfully!'; |
|||
} catch (err) { |
|||
error.value = err instanceof Error ? err.message : 'Migration failed'; |
|||
statusMessage.value = 'Migration failed'; |
|||
} |
|||
} |
|||
|
|||
async function retryMigration() { |
|||
error.value = null; |
|||
progress.value = 0; |
|||
await startMigration(); |
|||
} |
|||
|
|||
onMounted(() => { |
|||
startMigration(); |
|||
}); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.migration-progress { |
|||
padding: 2rem; |
|||
max-width: 600px; |
|||
margin: 0 auto; |
|||
} |
|||
|
|||
.progress-container { |
|||
position: relative; |
|||
height: 20px; |
|||
background: #eee; |
|||
border-radius: 10px; |
|||
overflow: hidden; |
|||
margin: 1rem 0; |
|||
} |
|||
|
|||
.progress-bar { |
|||
position: absolute; |
|||
height: 100%; |
|||
background: #4CAF50; |
|||
transition: width 0.3s ease; |
|||
} |
|||
|
|||
.progress-text { |
|||
position: absolute; |
|||
width: 100%; |
|||
text-align: center; |
|||
line-height: 20px; |
|||
color: #000; |
|||
} |
|||
|
|||
.status-message { |
|||
text-align: center; |
|||
margin: 1rem 0; |
|||
} |
|||
|
|||
.error-message { |
|||
color: #f44336; |
|||
text-align: center; |
|||
margin: 1rem 0; |
|||
} |
|||
|
|||
button { |
|||
margin-top: 1rem; |
|||
padding: 0.5rem 1rem; |
|||
background: #2196F3; |
|||
color: white; |
|||
border: none; |
|||
border-radius: 4px; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
button:hover { |
|||
background: #1976D2; |
|||
} |
|||
</style> |
|||
``` |
|||
|
|||
## Testing Strategy |
|||
|
|||
1. **Unit Tests** |
|||
```typescript |
|||
// src/services/storage/migration/__tests__/MigrationService.spec.ts |
|||
describe('MigrationService', () => { |
|||
it('should create valid backup', async () => { |
|||
const service = MigrationService.getInstance(); |
|||
const backup = await service.createBackup(); |
|||
|
|||
expect(backup).toBeDefined(); |
|||
expect(backup.accounts).toBeInstanceOf(Array); |
|||
expect(backup.settings).toBeInstanceOf(Array); |
|||
expect(backup.contacts).toBeInstanceOf(Array); |
|||
}); |
|||
|
|||
it('should migrate data correctly', async () => { |
|||
const service = MigrationService.getInstance(); |
|||
const backup = await service.createBackup(); |
|||
|
|||
await service.migrate(backup); |
|||
|
|||
// Verify migration |
|||
const accounts = await service.getMigratedAccounts(); |
|||
expect(accounts).toHaveLength(backup.accounts.length); |
|||
}); |
|||
|
|||
it('should handle rollback correctly', async () => { |
|||
const service = MigrationService.getInstance(); |
|||
const backup = await service.createBackup(); |
|||
|
|||
// Simulate failed migration |
|||
await service.migrate(backup); |
|||
await service.simulateFailure(); |
|||
|
|||
// Perform rollback |
|||
await service.rollback(backup); |
|||
|
|||
// Verify rollback |
|||
const accounts = await service.getOriginalAccounts(); |
|||
expect(accounts).toHaveLength(backup.accounts.length); |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
2. **Integration Tests** |
|||
```typescript |
|||
// src/services/storage/migration/__tests__/integration/Migration.spec.ts |
|||
describe('Migration Integration', () => { |
|||
it('should handle concurrent access during migration', async () => { |
|||
const service = MigrationService.getInstance(); |
|||
|
|||
// Start migration |
|||
const migrationPromise = service.migrate(); |
|||
|
|||
// Simulate concurrent access |
|||
const accessPromises = Array(5).fill(null).map(() => |
|||
service.getAccount('did:test:123') |
|||
); |
|||
|
|||
// Wait for all operations |
|||
const [migrationResult, ...accessResults] = await Promise.allSettled([ |
|||
migrationPromise, |
|||
...accessPromises |
|||
]); |
|||
|
|||
// Verify results |
|||
expect(migrationResult.status).toBe('fulfilled'); |
|||
expect(accessResults.some(r => r.status === 'rejected')).toBe(true); |
|||
}); |
|||
|
|||
it('should maintain data integrity during platform transition', async () => { |
|||
const service = MigrationService.getInstance(); |
|||
|
|||
// Simulate platform change |
|||
await service.simulatePlatformChange(); |
|||
|
|||
// Verify data |
|||
const accounts = await service.getAllAccounts(); |
|||
const settings = await service.getAllSettings(); |
|||
const contacts = await service.getAllContacts(); |
|||
|
|||
expect(accounts).toBeDefined(); |
|||
expect(settings).toBeDefined(); |
|||
expect(contacts).toBeDefined(); |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
## Success Criteria |
|||
|
|||
1. **Data Integrity** |
|||
- [ ] All accounts migrated successfully |
|||
- [ ] All settings preserved |
|||
- [ ] All contacts transferred |
|||
- [ ] No data corruption |
|||
|
|||
2. **Performance** |
|||
- [ ] Migration completes within acceptable time |
|||
- [ ] No significant performance degradation |
|||
- [ ] Efficient storage usage |
|||
- [ ] Smooth user experience |
|||
|
|||
3. **Security** |
|||
- [ ] Encrypted data remains secure |
|||
- [ ] Access controls maintained |
|||
- [ ] No sensitive data exposure |
|||
- [ ] Secure backup process |
|||
|
|||
4. **User Experience** |
|||
- [ ] Clear migration progress |
|||
- [ ] Informative error messages |
|||
- [ ] Automatic recovery from failures |
|||
- [ ] No data loss |
|||
|
|||
## Rollback Plan |
|||
|
|||
1. **Automatic Rollback** |
|||
- Triggered by migration failure |
|||
- Restores from verified backup |
|||
- Maintains data consistency |
|||
- Logs rollback reason |
|||
|
|||
2. **Manual Rollback** |
|||
- Available through settings |
|||
- Requires user confirmation |
|||
- Preserves backup data |
|||
- Provides rollback status |
|||
|
|||
3. **Emergency Recovery** |
|||
- Manual backup restoration |
|||
- Database repair tools |
|||
- Data recovery procedures |
|||
- Support contact information |
|||
|
|||
## Post-Migration |
|||
|
|||
1. **Verification** |
|||
- Data integrity checks |
|||
- Performance monitoring |
|||
- Error rate tracking |
|||
- User feedback collection |
|||
|
|||
2. **Cleanup** |
|||
- Remove old database |
|||
- Clear migration artifacts |
|||
- Update application state |
|||
- Archive backup data |
|||
|
|||
3. **Monitoring** |
|||
- Track migration success rate |
|||
- Monitor performance metrics |
|||
- Collect error reports |
|||
- Gather user feedback |
|||
|
|||
## Support |
|||
|
|||
For assistance with migration: |
|||
1. Check the troubleshooting guide |
|||
2. Review error logs |
|||
3. Contact support team |
|||
4. Submit issue report |
|||
|
|||
## Timeline |
|||
|
|||
1. **Preparation Phase** (1 week) |
|||
- Backup system implementation |
|||
- Migration service development |
|||
- Testing framework setup |
|||
|
|||
2. **Testing Phase** (2 weeks) |
|||
- Unit testing |
|||
- Integration testing |
|||
- Performance testing |
|||
- Security testing |
|||
|
|||
3. **Deployment Phase** (1 week) |
|||
- Staged rollout |
|||
- Monitoring |
|||
- Support preparation |
|||
- Documentation updates |
|||
|
|||
4. **Post-Deployment** (2 weeks) |
|||
- Monitoring |
|||
- Bug fixes |
|||
- Performance optimization |
|||
- User feedback collection |
File diff suppressed because it is too large
@ -0,0 +1,306 @@ |
|||
# Storage Implementation Checklist |
|||
|
|||
## Core Services |
|||
|
|||
### 1. Storage Service Layer |
|||
- [ ] Create base `StorageService` interface |
|||
- [ ] Define common methods for all platforms |
|||
- [ ] Add platform-specific method signatures |
|||
- [ ] Include error handling types |
|||
- [ ] Add migration support methods |
|||
|
|||
- [ ] Implement platform-specific services |
|||
- [ ] `WebSQLiteService` (wa-sqlite) |
|||
- [ ] Database initialization |
|||
- [ ] VFS setup |
|||
- [ ] Connection management |
|||
- [ ] Query builder |
|||
- [ ] `NativeSQLiteService` (iOS/Android) |
|||
- [ ] SQLCipher integration |
|||
- [ ] Native bridge setup |
|||
- [ ] File system access |
|||
- [ ] `ElectronSQLiteService` |
|||
- [ ] Node SQLite integration |
|||
- [ ] IPC communication |
|||
- [ ] File system access |
|||
|
|||
### 2. Migration Services |
|||
- [ ] Implement `MigrationService` |
|||
- [ ] Backup creation |
|||
- [ ] Data verification |
|||
- [ ] Rollback procedures |
|||
- [ ] Progress tracking |
|||
- [ ] Create `MigrationUI` components |
|||
- [ ] Progress indicators |
|||
- [ ] Error handling |
|||
- [ ] User notifications |
|||
- [ ] Manual triggers |
|||
|
|||
### 3. Security Layer |
|||
- [ ] Implement `EncryptionService` |
|||
- [ ] Key management |
|||
- [ ] Encryption/decryption |
|||
- [ ] Secure storage |
|||
- [ ] Add `BiometricService` |
|||
- [ ] Platform detection |
|||
- [ ] Authentication flow |
|||
- [ ] Fallback mechanisms |
|||
|
|||
## Platform-Specific Implementation |
|||
|
|||
### Web Platform |
|||
- [ ] Setup wa-sqlite |
|||
- [ ] Install dependencies |
|||
```json |
|||
{ |
|||
"@wa-sqlite/sql.js": "^0.8.12", |
|||
"@wa-sqlite/sql.js-httpvfs": "^0.8.12" |
|||
} |
|||
``` |
|||
- [ ] Configure VFS |
|||
- [ ] Setup worker threads |
|||
- [ ] Implement connection pooling |
|||
|
|||
- [ ] Update build configuration |
|||
- [ ] Modify `vite.config.ts` |
|||
- [ ] Add worker configuration |
|||
- [ ] Update chunk splitting |
|||
- [ ] Configure asset handling |
|||
|
|||
- [ ] Implement IndexedDB fallback |
|||
- [ ] Create fallback service |
|||
- [ ] Add data synchronization |
|||
- [ ] Handle quota exceeded |
|||
|
|||
### iOS Platform |
|||
- [ ] Setup SQLCipher |
|||
- [ ] Install pod dependencies |
|||
- [ ] Configure encryption |
|||
- [ ] Setup keychain access |
|||
- [ ] Implement secure storage |
|||
|
|||
- [ ] Update Capacitor config |
|||
- [ ] Modify `capacitor.config.ts` |
|||
- [ ] Add iOS permissions |
|||
- [ ] Configure backup |
|||
- [ ] Setup app groups |
|||
|
|||
### Android Platform |
|||
- [ ] Setup SQLCipher |
|||
- [ ] Add Gradle dependencies |
|||
- [ ] Configure encryption |
|||
- [ ] Setup keystore |
|||
- [ ] Implement secure storage |
|||
|
|||
- [ ] Update Capacitor config |
|||
- [ ] Modify `capacitor.config.ts` |
|||
- [ ] Add Android permissions |
|||
- [ ] Configure backup |
|||
- [ ] Setup file provider |
|||
|
|||
### Electron Platform |
|||
- [ ] Setup Node SQLite |
|||
- [ ] Install dependencies |
|||
- [ ] Configure IPC |
|||
- [ ] Setup file system access |
|||
- [ ] Implement secure storage |
|||
|
|||
- [ ] Update Electron config |
|||
- [ ] Modify `electron.config.ts` |
|||
- [ ] Add security policies |
|||
- [ ] Configure file access |
|||
- [ ] Setup auto-updates |
|||
|
|||
## Data Models and Types |
|||
|
|||
### 1. Database Schema |
|||
- [ ] Define tables |
|||
```sql |
|||
-- Accounts table |
|||
CREATE TABLE accounts ( |
|||
did TEXT PRIMARY KEY, |
|||
public_key_hex TEXT NOT NULL, |
|||
created_at INTEGER NOT NULL, |
|||
updated_at INTEGER NOT NULL |
|||
); |
|||
|
|||
-- Settings table |
|||
CREATE TABLE settings ( |
|||
key TEXT PRIMARY KEY, |
|||
value TEXT NOT NULL, |
|||
updated_at INTEGER NOT NULL |
|||
); |
|||
|
|||
-- Contacts table |
|||
CREATE TABLE contacts ( |
|||
id TEXT PRIMARY KEY, |
|||
did TEXT NOT NULL, |
|||
name TEXT, |
|||
created_at INTEGER NOT NULL, |
|||
updated_at INTEGER NOT NULL, |
|||
FOREIGN KEY (did) REFERENCES accounts(did) |
|||
); |
|||
``` |
|||
|
|||
- [ ] Create indexes |
|||
- [ ] Define constraints |
|||
- [ ] Add triggers |
|||
- [ ] Setup migrations |
|||
|
|||
### 2. Type Definitions |
|||
- [ ] Create interfaces |
|||
```typescript |
|||
interface Account { |
|||
did: string; |
|||
publicKeyHex: string; |
|||
createdAt: number; |
|||
updatedAt: number; |
|||
} |
|||
|
|||
interface Setting { |
|||
key: string; |
|||
value: string; |
|||
updatedAt: number; |
|||
} |
|||
|
|||
interface Contact { |
|||
id: string; |
|||
did: string; |
|||
name?: string; |
|||
createdAt: number; |
|||
updatedAt: number; |
|||
} |
|||
``` |
|||
|
|||
- [ ] Add validation |
|||
- [ ] Create DTOs |
|||
- [ ] Define enums |
|||
- [ ] Add type guards |
|||
|
|||
## UI Components |
|||
|
|||
### 1. Migration UI |
|||
- [ ] Create components |
|||
- [ ] `MigrationProgress.vue` |
|||
- [ ] `MigrationError.vue` |
|||
- [ ] `MigrationSettings.vue` |
|||
- [ ] `MigrationStatus.vue` |
|||
|
|||
### 2. Settings UI |
|||
- [ ] Update components |
|||
- [ ] Add storage settings |
|||
- [ ] Add migration controls |
|||
- [ ] Add backup options |
|||
- [ ] Add security settings |
|||
|
|||
### 3. Error Handling UI |
|||
- [ ] Create components |
|||
- [ ] `StorageError.vue` |
|||
- [ ] `QuotaExceeded.vue` |
|||
- [ ] `MigrationFailed.vue` |
|||
- [ ] `RecoveryOptions.vue` |
|||
|
|||
## Testing |
|||
|
|||
### 1. Unit Tests |
|||
- [ ] Test services |
|||
- [ ] Storage service tests |
|||
- [ ] Migration service tests |
|||
- [ ] Security service tests |
|||
- [ ] Platform detection tests |
|||
|
|||
### 2. Integration Tests |
|||
- [ ] Test migrations |
|||
- [ ] Web platform tests |
|||
- [ ] iOS platform tests |
|||
- [ ] Android platform tests |
|||
- [ ] Electron platform tests |
|||
|
|||
### 3. E2E Tests |
|||
- [ ] Test workflows |
|||
- [ ] Account management |
|||
- [ ] Settings management |
|||
- [ ] Contact management |
|||
- [ ] Migration process |
|||
|
|||
## Documentation |
|||
|
|||
### 1. Technical Documentation |
|||
- [ ] Update architecture docs |
|||
- [ ] Add API documentation |
|||
- [ ] Create migration guides |
|||
- [ ] Document security measures |
|||
|
|||
### 2. User Documentation |
|||
- [ ] Update user guides |
|||
- [ ] Add troubleshooting guides |
|||
- [ ] Create FAQ |
|||
- [ ] Document new features |
|||
|
|||
## Deployment |
|||
|
|||
### 1. Build Process |
|||
- [ ] Update build scripts |
|||
- [ ] Add platform-specific builds |
|||
- [ ] Configure CI/CD |
|||
- [ ] Setup automated testing |
|||
|
|||
### 2. Release Process |
|||
- [ ] Create release checklist |
|||
- [ ] Add version management |
|||
- [ ] Setup rollback procedures |
|||
- [ ] Configure monitoring |
|||
|
|||
## Monitoring and Analytics |
|||
|
|||
### 1. Error Tracking |
|||
- [ ] Setup error logging |
|||
- [ ] Add performance monitoring |
|||
- [ ] Configure alerts |
|||
- [ ] Create dashboards |
|||
|
|||
### 2. Usage Analytics |
|||
- [ ] Add storage metrics |
|||
- [ ] Track migration success |
|||
- [ ] Monitor performance |
|||
- [ ] Collect user feedback |
|||
|
|||
## Security Audit |
|||
|
|||
### 1. Code Review |
|||
- [ ] Review encryption |
|||
- [ ] Check access controls |
|||
- [ ] Verify data handling |
|||
- [ ] Audit dependencies |
|||
|
|||
### 2. Penetration Testing |
|||
- [ ] Test data access |
|||
- [ ] Verify encryption |
|||
- [ ] Check authentication |
|||
- [ ] Review permissions |
|||
|
|||
## Success Criteria |
|||
|
|||
### 1. Performance |
|||
- [ ] Query response time < 100ms |
|||
- [ ] Migration time < 5s per 1000 records |
|||
- [ ] Storage overhead < 10% |
|||
- [ ] Memory usage < 50MB |
|||
|
|||
### 2. Reliability |
|||
- [ ] 99.9% uptime |
|||
- [ ] Zero data loss |
|||
- [ ] Automatic recovery |
|||
- [ ] Backup verification |
|||
|
|||
### 3. Security |
|||
- [ ] AES-256 encryption |
|||
- [ ] Secure key storage |
|||
- [ ] Access control |
|||
- [ ] Audit logging |
|||
|
|||
### 4. User Experience |
|||
- [ ] Smooth migration |
|||
- [ ] Clear error messages |
|||
- [ ] Progress indicators |
|||
- [ ] Recovery options |
@ -1,29 +0,0 @@ |
|||
const { app, BrowserWindow } = require('electron'); |
|||
const path = require('path'); |
|||
|
|||
function createWindow() { |
|||
const win = new BrowserWindow({ |
|||
width: 1200, |
|||
height: 800, |
|||
webPreferences: { |
|||
nodeIntegration: true, |
|||
contextIsolation: false |
|||
} |
|||
}); |
|||
|
|||
win.loadFile(path.join(__dirname, 'dist-electron/www/index.html')); |
|||
} |
|||
|
|||
app.whenReady().then(createWindow); |
|||
|
|||
app.on('window-all-closed', () => { |
|||
if (process.platform !== 'darwin') { |
|||
app.quit(); |
|||
} |
|||
}); |
|||
|
|||
app.on('activate', () => { |
|||
if (BrowserWindow.getAllWindows().length === 0) { |
|||
createWindow(); |
|||
} |
|||
}); |
@ -0,0 +1,293 @@ |
|||
/** |
|||
* SQLite Database Initialization |
|||
* |
|||
* This module handles database initialization, including: |
|||
* - Database connection management |
|||
* - Schema creation and migration |
|||
* - Connection pooling and lifecycle |
|||
* - Error handling and recovery |
|||
*/ |
|||
|
|||
import { Database, SQLite3 } from '@wa-sqlite/sql.js'; |
|||
import { DATABASE_SCHEMA, SQLiteTable } from './types'; |
|||
import { logger } from '../../utils/logger'; |
|||
|
|||
// ============================================================================
|
|||
// Database Connection Management
|
|||
// ============================================================================
|
|||
|
|||
export interface DatabaseConnection { |
|||
db: Database; |
|||
sqlite3: SQLite3; |
|||
isOpen: boolean; |
|||
lastUsed: number; |
|||
} |
|||
|
|||
let connection: DatabaseConnection | null = null; |
|||
const CONNECTION_TIMEOUT = 5 * 60 * 1000; // 5 minutes
|
|||
|
|||
/** |
|||
* Initialize the SQLite database connection |
|||
*/ |
|||
export async function initDatabase(): Promise<DatabaseConnection> { |
|||
if (connection?.isOpen) { |
|||
connection.lastUsed = Date.now(); |
|||
return connection; |
|||
} |
|||
|
|||
try { |
|||
const sqlite3 = await import('@wa-sqlite/sql.js'); |
|||
const db = await sqlite3.open(':memory:'); // TODO: Configure storage location
|
|||
|
|||
// Enable foreign keys
|
|||
await db.exec('PRAGMA foreign_keys = ON;'); |
|||
|
|||
// Configure for better performance
|
|||
await db.exec(` |
|||
PRAGMA journal_mode = WAL; |
|||
PRAGMA synchronous = NORMAL; |
|||
PRAGMA cache_size = -2000; -- Use 2MB of cache |
|||
`);
|
|||
|
|||
connection = { |
|||
db, |
|||
sqlite3, |
|||
isOpen: true, |
|||
lastUsed: Date.now() |
|||
}; |
|||
|
|||
// Start connection cleanup interval
|
|||
startConnectionCleanup(); |
|||
|
|||
return connection; |
|||
} catch (error) { |
|||
logger.error('[SQLite] Database initialization failed:', error); |
|||
throw new Error('Failed to initialize database'); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Close the database connection |
|||
*/ |
|||
export async function closeDatabase(): Promise<void> { |
|||
if (!connection?.isOpen) return; |
|||
|
|||
try { |
|||
await connection.db.close(); |
|||
connection.isOpen = false; |
|||
connection = null; |
|||
} catch (error) { |
|||
logger.error('[SQLite] Database close failed:', error); |
|||
throw new Error('Failed to close database'); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Cleanup inactive connections |
|||
*/ |
|||
function startConnectionCleanup(): void { |
|||
setInterval(() => { |
|||
if (connection && Date.now() - connection.lastUsed > CONNECTION_TIMEOUT) { |
|||
closeDatabase().catch(error => { |
|||
logger.error('[SQLite] Connection cleanup failed:', error); |
|||
}); |
|||
} |
|||
}, 60000); // Check every minute
|
|||
} |
|||
|
|||
// ============================================================================
|
|||
// Schema Management
|
|||
// ============================================================================
|
|||
|
|||
/** |
|||
* Create the database schema |
|||
*/ |
|||
export async function createSchema(): Promise<void> { |
|||
const { db } = await initDatabase(); |
|||
|
|||
try { |
|||
await db.transaction(async () => { |
|||
for (const table of DATABASE_SCHEMA) { |
|||
await createTable(db, table); |
|||
} |
|||
}); |
|||
} catch (error) { |
|||
logger.error('[SQLite] Schema creation failed:', error); |
|||
throw new Error('Failed to create database schema'); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Create a single table |
|||
*/ |
|||
async function createTable(db: Database, table: SQLiteTable): Promise<void> { |
|||
const columnDefs = table.columns.map(col => { |
|||
const constraints = [ |
|||
col.primaryKey ? 'PRIMARY KEY' : '', |
|||
col.unique ? 'UNIQUE' : '', |
|||
!col.nullable ? 'NOT NULL' : '', |
|||
col.references ? `REFERENCES ${col.references.table}(${col.references.column})` : '', |
|||
col.default !== undefined ? `DEFAULT ${formatDefaultValue(col.default)}` : '' |
|||
].filter(Boolean).join(' '); |
|||
|
|||
return `${col.name} ${col.type} ${constraints}`.trim(); |
|||
}); |
|||
|
|||
const createTableSQL = ` |
|||
CREATE TABLE IF NOT EXISTS ${table.name} ( |
|||
${columnDefs.join(',\n ')} |
|||
); |
|||
`;
|
|||
|
|||
await db.exec(createTableSQL); |
|||
|
|||
// Create indexes
|
|||
if (table.indexes) { |
|||
for (const index of table.indexes) { |
|||
const createIndexSQL = ` |
|||
CREATE INDEX IF NOT EXISTS ${index.name} |
|||
ON ${table.name} (${index.columns.join(', ')}) |
|||
${index.unique ? 'UNIQUE' : ''}; |
|||
`;
|
|||
await db.exec(createIndexSQL); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Format default value for SQL |
|||
*/ |
|||
function formatDefaultValue(value: unknown): string { |
|||
if (value === null) return 'NULL'; |
|||
if (typeof value === 'string') return `'${value.replace(/'/g, "''")}'`; |
|||
if (typeof value === 'number') return value.toString(); |
|||
if (typeof value === 'boolean') return value ? '1' : '0'; |
|||
throw new Error(`Unsupported default value type: ${typeof value}`); |
|||
} |
|||
|
|||
// ============================================================================
|
|||
// Database Health Checks
|
|||
// ============================================================================
|
|||
|
|||
/** |
|||
* Check database health |
|||
*/ |
|||
export async function checkDatabaseHealth(): Promise<{ |
|||
isHealthy: boolean; |
|||
tables: string[]; |
|||
error?: string; |
|||
}> { |
|||
try { |
|||
const { db } = await initDatabase(); |
|||
|
|||
// Check if we can query the database
|
|||
const tables = await db.selectAll<{ name: string }>(` |
|||
SELECT name FROM sqlite_master |
|||
WHERE type='table' AND name NOT LIKE 'sqlite_%' |
|||
`);
|
|||
|
|||
return { |
|||
isHealthy: true, |
|||
tables: tables.map(t => t.name) |
|||
}; |
|||
} catch (error) { |
|||
logger.error('[SQLite] Health check failed:', error); |
|||
return { |
|||
isHealthy: false, |
|||
tables: [], |
|||
error: error instanceof Error ? error.message : 'Unknown error' |
|||
}; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Verify database integrity |
|||
*/ |
|||
export async function verifyDatabaseIntegrity(): Promise<{ |
|||
isIntegrityOk: boolean; |
|||
errors: string[]; |
|||
}> { |
|||
const { db } = await initDatabase(); |
|||
const errors: string[] = []; |
|||
|
|||
try { |
|||
// Run integrity check
|
|||
const result = await db.selectAll<{ integrity_check: string }>('PRAGMA integrity_check;'); |
|||
|
|||
if (result[0]?.integrity_check !== 'ok') { |
|||
errors.push('Database integrity check failed'); |
|||
} |
|||
|
|||
// Check foreign key constraints
|
|||
const fkResult = await db.selectAll<{ table: string; rowid: number; parent: string; fkid: number }>(` |
|||
PRAGMA foreign_key_check; |
|||
`);
|
|||
|
|||
if (fkResult.length > 0) { |
|||
errors.push('Foreign key constraint violations found'); |
|||
} |
|||
|
|||
return { |
|||
isIntegrityOk: errors.length === 0, |
|||
errors |
|||
}; |
|||
} catch (error) { |
|||
logger.error('[SQLite] Integrity check failed:', error); |
|||
return { |
|||
isIntegrityOk: false, |
|||
errors: [error instanceof Error ? error.message : 'Unknown error'] |
|||
}; |
|||
} |
|||
} |
|||
|
|||
// ============================================================================
|
|||
// Database Backup and Recovery
|
|||
// ============================================================================
|
|||
|
|||
/** |
|||
* Create a database backup |
|||
*/ |
|||
export async function createBackup(): Promise<Uint8Array> { |
|||
const { db } = await initDatabase(); |
|||
|
|||
try { |
|||
// Export the database to a binary array
|
|||
return await db.export(); |
|||
} catch (error) { |
|||
logger.error('[SQLite] Backup creation failed:', error); |
|||
throw new Error('Failed to create database backup'); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Restore database from backup |
|||
*/ |
|||
export async function restoreFromBackup(backup: Uint8Array): Promise<void> { |
|||
const { db } = await initDatabase(); |
|||
|
|||
try { |
|||
// Close current connection
|
|||
await closeDatabase(); |
|||
|
|||
// Create new connection and import backup
|
|||
const sqlite3 = await import('@wa-sqlite/sql.js'); |
|||
const newDb = await sqlite3.open(backup); |
|||
|
|||
// Verify integrity
|
|||
const { isIntegrityOk, errors } = await verifyDatabaseIntegrity(); |
|||
if (!isIntegrityOk) { |
|||
throw new Error(`Backup integrity check failed: ${errors.join(', ')}`); |
|||
} |
|||
|
|||
// Replace current connection
|
|||
connection = { |
|||
db: newDb, |
|||
sqlite3, |
|||
isOpen: true, |
|||
lastUsed: Date.now() |
|||
}; |
|||
} catch (error) { |
|||
logger.error('[SQLite] Backup restoration failed:', error); |
|||
throw new Error('Failed to restore database from backup'); |
|||
} |
|||
} |
@ -0,0 +1,374 @@ |
|||
/** |
|||
* SQLite Migration Utilities |
|||
* |
|||
* This module handles the migration of data from Dexie to SQLite, |
|||
* including data transformation, validation, and rollback capabilities. |
|||
*/ |
|||
|
|||
import { Database } from '@wa-sqlite/sql.js'; |
|||
import { initDatabase, createSchema, createBackup } from './init'; |
|||
import { |
|||
MigrationData, |
|||
MigrationResult, |
|||
SQLiteAccount, |
|||
SQLiteContact, |
|||
SQLiteContactMethod, |
|||
SQLiteSettings, |
|||
SQLiteLog, |
|||
SQLiteSecret, |
|||
isSQLiteAccount, |
|||
isSQLiteContact, |
|||
isSQLiteSettings |
|||
} from './types'; |
|||
import { logger } from '../../utils/logger'; |
|||
|
|||
// ============================================================================
|
|||
// Migration Types
|
|||
// ============================================================================
|
|||
|
|||
interface MigrationContext { |
|||
db: Database; |
|||
startTime: number; |
|||
stats: MigrationResult['stats']; |
|||
errors: Error[]; |
|||
} |
|||
|
|||
// ============================================================================
|
|||
// Migration Functions
|
|||
// ============================================================================
|
|||
|
|||
/** |
|||
* Migrate data from Dexie to SQLite |
|||
*/ |
|||
export async function migrateFromDexie(data: MigrationData): Promise<MigrationResult> { |
|||
const startTime = Date.now(); |
|||
const context: MigrationContext = { |
|||
db: (await initDatabase()).db, |
|||
startTime, |
|||
stats: { |
|||
accounts: 0, |
|||
contacts: 0, |
|||
contactMethods: 0, |
|||
settings: 0, |
|||
logs: 0, |
|||
secrets: 0 |
|||
}, |
|||
errors: [] |
|||
}; |
|||
|
|||
try { |
|||
// Create backup before migration
|
|||
const backup = await createBackup(); |
|||
|
|||
// Create schema if needed
|
|||
await createSchema(); |
|||
|
|||
// Perform migration in a transaction
|
|||
await context.db.transaction(async () => { |
|||
// Migrate in order of dependencies
|
|||
await migrateAccounts(context, data.accounts); |
|||
await migrateContacts(context, data.contacts); |
|||
await migrateContactMethods(context, data.contactMethods); |
|||
await migrateSettings(context, data.settings); |
|||
await migrateLogs(context, data.logs); |
|||
await migrateSecrets(context, data.secrets); |
|||
}); |
|||
|
|||
// Verify migration
|
|||
const verificationResult = await verifyMigration(context, data); |
|||
if (!verificationResult.success) { |
|||
throw new Error(`Migration verification failed: ${verificationResult.error}`); |
|||
} |
|||
|
|||
return { |
|||
success: true, |
|||
stats: context.stats, |
|||
duration: Date.now() - startTime |
|||
}; |
|||
|
|||
} catch (error) { |
|||
logger.error('[SQLite] Migration failed:', error); |
|||
|
|||
// Attempt rollback
|
|||
try { |
|||
await rollbackMigration(backup); |
|||
} catch (rollbackError) { |
|||
logger.error('[SQLite] Rollback failed:', rollbackError); |
|||
context.errors.push(new Error('Migration and rollback failed')); |
|||
} |
|||
|
|||
return { |
|||
success: false, |
|||
error: error instanceof Error ? error : new Error('Unknown migration error'), |
|||
stats: context.stats, |
|||
duration: Date.now() - startTime |
|||
}; |
|||
} |
|||
} |
|||
|
|||
// ============================================================================
|
|||
// Migration Helpers
|
|||
// ============================================================================
|
|||
|
|||
/** |
|||
* Migrate accounts |
|||
*/ |
|||
async function migrateAccounts(context: MigrationContext, accounts: SQLiteAccount[]): Promise<void> { |
|||
for (const account of accounts) { |
|||
try { |
|||
if (!isSQLiteAccount(account)) { |
|||
throw new Error(`Invalid account data: ${JSON.stringify(account)}`); |
|||
} |
|||
|
|||
await context.db.exec(` |
|||
INSERT INTO accounts ( |
|||
did, public_key_hex, created_at, updated_at, |
|||
identity_json, mnemonic_encrypted, passkey_cred_id_hex, derivation_path |
|||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?) |
|||
`, [
|
|||
account.did, |
|||
account.public_key_hex, |
|||
account.created_at, |
|||
account.updated_at, |
|||
account.identity_json || null, |
|||
account.mnemonic_encrypted || null, |
|||
account.passkey_cred_id_hex || null, |
|||
account.derivation_path || null |
|||
]); |
|||
|
|||
context.stats.accounts++; |
|||
} catch (error) { |
|||
context.errors.push(new Error(`Failed to migrate account ${account.did}: ${error}`)); |
|||
throw error; // Re-throw to trigger transaction rollback
|
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Migrate contacts |
|||
*/ |
|||
async function migrateContacts(context: MigrationContext, contacts: SQLiteContact[]): Promise<void> { |
|||
for (const contact of contacts) { |
|||
try { |
|||
if (!isSQLiteContact(contact)) { |
|||
throw new Error(`Invalid contact data: ${JSON.stringify(contact)}`); |
|||
} |
|||
|
|||
await context.db.exec(` |
|||
INSERT INTO contacts ( |
|||
id, did, name, notes, profile_image_url, |
|||
public_key_base64, next_pub_key_hash_b64, |
|||
sees_me, registered, created_at, updated_at |
|||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
|||
`, [
|
|||
contact.id, |
|||
contact.did, |
|||
contact.name || null, |
|||
contact.notes || null, |
|||
contact.profile_image_url || null, |
|||
contact.public_key_base64 || null, |
|||
contact.next_pub_key_hash_b64 || null, |
|||
contact.sees_me ? 1 : 0, |
|||
contact.registered ? 1 : 0, |
|||
contact.created_at, |
|||
contact.updated_at |
|||
]); |
|||
|
|||
context.stats.contacts++; |
|||
} catch (error) { |
|||
context.errors.push(new Error(`Failed to migrate contact ${contact.id}: ${error}`)); |
|||
throw error; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Migrate contact methods |
|||
*/ |
|||
async function migrateContactMethods( |
|||
context: MigrationContext, |
|||
methods: SQLiteContactMethod[] |
|||
): Promise<void> { |
|||
for (const method of methods) { |
|||
try { |
|||
await context.db.exec(` |
|||
INSERT INTO contact_methods ( |
|||
id, contact_id, label, type, value, |
|||
created_at, updated_at |
|||
) VALUES (?, ?, ?, ?, ?, ?, ?) |
|||
`, [
|
|||
method.id, |
|||
method.contact_id, |
|||
method.label, |
|||
method.type, |
|||
method.value, |
|||
method.created_at, |
|||
method.updated_at |
|||
]); |
|||
|
|||
context.stats.contactMethods++; |
|||
} catch (error) { |
|||
context.errors.push(new Error(`Failed to migrate contact method ${method.id}: ${error}`)); |
|||
throw error; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Migrate settings |
|||
*/ |
|||
async function migrateSettings(context: MigrationContext, settings: SQLiteSettings[]): Promise<void> { |
|||
for (const setting of settings) { |
|||
try { |
|||
if (!isSQLiteSettings(setting)) { |
|||
throw new Error(`Invalid settings data: ${JSON.stringify(setting)}`); |
|||
} |
|||
|
|||
await context.db.exec(` |
|||
INSERT INTO settings ( |
|||
key, account_did, value_json, created_at, updated_at |
|||
) VALUES (?, ?, ?, ?, ?) |
|||
`, [
|
|||
setting.key, |
|||
setting.account_did || null, |
|||
setting.value_json, |
|||
setting.created_at, |
|||
setting.updated_at |
|||
]); |
|||
|
|||
context.stats.settings++; |
|||
} catch (error) { |
|||
context.errors.push(new Error(`Failed to migrate setting ${setting.key}: ${error}`)); |
|||
throw error; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Migrate logs |
|||
*/ |
|||
async function migrateLogs(context: MigrationContext, logs: SQLiteLog[]): Promise<void> { |
|||
for (const log of logs) { |
|||
try { |
|||
await context.db.exec(` |
|||
INSERT INTO logs ( |
|||
id, level, message, metadata_json, created_at |
|||
) VALUES (?, ?, ?, ?, ?) |
|||
`, [
|
|||
log.id, |
|||
log.level, |
|||
log.message, |
|||
log.metadata_json || null, |
|||
log.created_at |
|||
]); |
|||
|
|||
context.stats.logs++; |
|||
} catch (error) { |
|||
context.errors.push(new Error(`Failed to migrate log ${log.id}: ${error}`)); |
|||
throw error; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Migrate secrets |
|||
*/ |
|||
async function migrateSecrets(context: MigrationContext, secrets: SQLiteSecret[]): Promise<void> { |
|||
for (const secret of secrets) { |
|||
try { |
|||
await context.db.exec(` |
|||
INSERT INTO secrets ( |
|||
key, value_encrypted, created_at, updated_at |
|||
) VALUES (?, ?, ?, ?) |
|||
`, [
|
|||
secret.key, |
|||
secret.value_encrypted, |
|||
secret.created_at, |
|||
secret.updated_at |
|||
]); |
|||
|
|||
context.stats.secrets++; |
|||
} catch (error) { |
|||
context.errors.push(new Error(`Failed to migrate secret ${secret.key}: ${error}`)); |
|||
throw error; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// ============================================================================
|
|||
// Verification and Rollback
|
|||
// ============================================================================
|
|||
|
|||
/** |
|||
* Verify migration success |
|||
*/ |
|||
async function verifyMigration( |
|||
context: MigrationContext, |
|||
data: MigrationData |
|||
): Promise<{ success: boolean; error?: string }> { |
|||
try { |
|||
// Verify counts
|
|||
const counts = await context.db.selectAll<{ table: string; count: number }>(` |
|||
SELECT 'accounts' as table, COUNT(*) as count FROM accounts |
|||
UNION ALL |
|||
SELECT 'contacts', COUNT(*) FROM contacts |
|||
UNION ALL |
|||
SELECT 'contact_methods', COUNT(*) FROM contact_methods |
|||
UNION ALL |
|||
SELECT 'settings', COUNT(*) FROM settings |
|||
UNION ALL |
|||
SELECT 'logs', COUNT(*) FROM logs |
|||
UNION ALL |
|||
SELECT 'secrets', COUNT(*) FROM secrets |
|||
`);
|
|||
|
|||
const countMap = new Map(counts.map(c => [c.table, c.count])); |
|||
|
|||
if (countMap.get('accounts') !== data.accounts.length) { |
|||
return { success: false, error: 'Account count mismatch' }; |
|||
} |
|||
if (countMap.get('contacts') !== data.contacts.length) { |
|||
return { success: false, error: 'Contact count mismatch' }; |
|||
} |
|||
if (countMap.get('contact_methods') !== data.contactMethods.length) { |
|||
return { success: false, error: 'Contact method count mismatch' }; |
|||
} |
|||
if (countMap.get('settings') !== data.settings.length) { |
|||
return { success: false, error: 'Settings count mismatch' }; |
|||
} |
|||
if (countMap.get('logs') !== data.logs.length) { |
|||
return { success: false, error: 'Log count mismatch' }; |
|||
} |
|||
if (countMap.get('secrets') !== data.secrets.length) { |
|||
return { success: false, error: 'Secret count mismatch' }; |
|||
} |
|||
|
|||
return { success: true }; |
|||
} catch (error) { |
|||
return { |
|||
success: false, |
|||
error: error instanceof Error ? error.message : 'Unknown verification error' |
|||
}; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Rollback migration |
|||
*/ |
|||
async function rollbackMigration(backup: Uint8Array): Promise<void> { |
|||
const { db } = await initDatabase(); |
|||
|
|||
try { |
|||
// Close current connection
|
|||
await db.close(); |
|||
|
|||
// Restore from backup
|
|||
const sqlite3 = await import('@wa-sqlite/sql.js'); |
|||
await sqlite3.open(backup); |
|||
|
|||
logger.info('[SQLite] Migration rollback successful'); |
|||
} catch (error) { |
|||
logger.error('[SQLite] Migration rollback failed:', error); |
|||
throw new Error('Failed to rollback migration'); |
|||
} |
|||
} |
@ -0,0 +1,449 @@ |
|||
/** |
|||
* SQLite Database Operations |
|||
* |
|||
* This module provides utility functions for common database operations, |
|||
* including CRUD operations, queries, and transactions. |
|||
*/ |
|||
|
|||
import { Database } from '@wa-sqlite/sql.js'; |
|||
import { initDatabase } from './init'; |
|||
import { |
|||
SQLiteAccount, |
|||
SQLiteContact, |
|||
SQLiteContactMethod, |
|||
SQLiteSettings, |
|||
SQLiteLog, |
|||
SQLiteSecret, |
|||
isSQLiteAccount, |
|||
isSQLiteContact, |
|||
isSQLiteSettings |
|||
} from './types'; |
|||
import { logger } from '../../utils/logger'; |
|||
|
|||
// ============================================================================
|
|||
// Transaction Helpers
|
|||
// ============================================================================
|
|||
|
|||
/** |
|||
* Execute a function within a transaction |
|||
*/ |
|||
export async function withTransaction<T>( |
|||
operation: (db: Database) => Promise<T> |
|||
): Promise<T> { |
|||
const { db } = await initDatabase(); |
|||
|
|||
try { |
|||
return await db.transaction(operation); |
|||
} catch (error) { |
|||
logger.error('[SQLite] Transaction failed:', error); |
|||
throw error; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Execute a function with retries |
|||
*/ |
|||
export async function withRetry<T>( |
|||
operation: () => Promise<T>, |
|||
maxRetries = 3, |
|||
delay = 1000 |
|||
): Promise<T> { |
|||
let lastError: Error | undefined; |
|||
|
|||
for (let i = 0; i < maxRetries; i++) { |
|||
try { |
|||
return await operation(); |
|||
} catch (error) { |
|||
lastError = error instanceof Error ? error : new Error(String(error)); |
|||
|
|||
if (i < maxRetries - 1) { |
|||
await new Promise(resolve => setTimeout(resolve, delay * (i + 1))); |
|||
} |
|||
} |
|||
} |
|||
|
|||
throw lastError; |
|||
} |
|||
|
|||
// ============================================================================
|
|||
// Account Operations
|
|||
// ============================================================================
|
|||
|
|||
/** |
|||
* Get account by DID |
|||
*/ |
|||
export async function getAccountByDid(did: string): Promise<SQLiteAccount | null> { |
|||
const { db } = await initDatabase(); |
|||
|
|||
const accounts = await db.selectAll<SQLiteAccount>( |
|||
'SELECT * FROM accounts WHERE did = ?', |
|||
[did] |
|||
); |
|||
|
|||
return accounts[0] || null; |
|||
} |
|||
|
|||
/** |
|||
* Get all accounts |
|||
*/ |
|||
export async function getAllAccounts(): Promise<SQLiteAccount[]> { |
|||
const { db } = await initDatabase(); |
|||
|
|||
return db.selectAll<SQLiteAccount>( |
|||
'SELECT * FROM accounts ORDER BY created_at DESC' |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Create or update account |
|||
*/ |
|||
export async function upsertAccount(account: SQLiteAccount): Promise<void> { |
|||
if (!isSQLiteAccount(account)) { |
|||
throw new Error('Invalid account data'); |
|||
} |
|||
|
|||
await withTransaction(async (db) => { |
|||
const existing = await db.selectOne<{ did: string }>( |
|||
'SELECT did FROM accounts WHERE did = ?', |
|||
[account.did] |
|||
); |
|||
|
|||
if (existing) { |
|||
await db.exec(` |
|||
UPDATE accounts SET |
|||
public_key_hex = ?, |
|||
updated_at = ?, |
|||
identity_json = ?, |
|||
mnemonic_encrypted = ?, |
|||
passkey_cred_id_hex = ?, |
|||
derivation_path = ? |
|||
WHERE did = ? |
|||
`, [
|
|||
account.public_key_hex, |
|||
Date.now(), |
|||
account.identity_json || null, |
|||
account.mnemonic_encrypted || null, |
|||
account.passkey_cred_id_hex || null, |
|||
account.derivation_path || null, |
|||
account.did |
|||
]); |
|||
} else { |
|||
await db.exec(` |
|||
INSERT INTO accounts ( |
|||
did, public_key_hex, created_at, updated_at, |
|||
identity_json, mnemonic_encrypted, passkey_cred_id_hex, derivation_path |
|||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?) |
|||
`, [
|
|||
account.did, |
|||
account.public_key_hex, |
|||
account.created_at, |
|||
account.updated_at, |
|||
account.identity_json || null, |
|||
account.mnemonic_encrypted || null, |
|||
account.passkey_cred_id_hex || null, |
|||
account.derivation_path || null |
|||
]); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
// ============================================================================
|
|||
// Contact Operations
|
|||
// ============================================================================
|
|||
|
|||
/** |
|||
* Get contact by ID |
|||
*/ |
|||
export async function getContactById(id: string): Promise<SQLiteContact | null> { |
|||
const { db } = await initDatabase(); |
|||
|
|||
const contacts = await db.selectAll<SQLiteContact>( |
|||
'SELECT * FROM contacts WHERE id = ?', |
|||
[id] |
|||
); |
|||
|
|||
return contacts[0] || null; |
|||
} |
|||
|
|||
/** |
|||
* Get contacts by account DID |
|||
*/ |
|||
export async function getContactsByAccountDid(did: string): Promise<SQLiteContact[]> { |
|||
const { db } = await initDatabase(); |
|||
|
|||
return db.selectAll<SQLiteContact>( |
|||
'SELECT * FROM contacts WHERE did = ? ORDER BY created_at DESC', |
|||
[did] |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Get contact methods for a contact |
|||
*/ |
|||
export async function getContactMethods(contactId: string): Promise<SQLiteContactMethod[]> { |
|||
const { db } = await initDatabase(); |
|||
|
|||
return db.selectAll<SQLiteContactMethod>( |
|||
'SELECT * FROM contact_methods WHERE contact_id = ? ORDER BY created_at DESC', |
|||
[contactId] |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Create or update contact with methods |
|||
*/ |
|||
export async function upsertContact( |
|||
contact: SQLiteContact, |
|||
methods: SQLiteContactMethod[] = [] |
|||
): Promise<void> { |
|||
if (!isSQLiteContact(contact)) { |
|||
throw new Error('Invalid contact data'); |
|||
} |
|||
|
|||
await withTransaction(async (db) => { |
|||
const existing = await db.selectOne<{ id: string }>( |
|||
'SELECT id FROM contacts WHERE id = ?', |
|||
[contact.id] |
|||
); |
|||
|
|||
if (existing) { |
|||
await db.exec(` |
|||
UPDATE contacts SET |
|||
did = ?, |
|||
name = ?, |
|||
notes = ?, |
|||
profile_image_url = ?, |
|||
public_key_base64 = ?, |
|||
next_pub_key_hash_b64 = ?, |
|||
sees_me = ?, |
|||
registered = ?, |
|||
updated_at = ? |
|||
WHERE id = ? |
|||
`, [
|
|||
contact.did, |
|||
contact.name || null, |
|||
contact.notes || null, |
|||
contact.profile_image_url || null, |
|||
contact.public_key_base64 || null, |
|||
contact.next_pub_key_hash_b64 || null, |
|||
contact.sees_me ? 1 : 0, |
|||
contact.registered ? 1 : 0, |
|||
Date.now(), |
|||
contact.id |
|||
]); |
|||
} else { |
|||
await db.exec(` |
|||
INSERT INTO contacts ( |
|||
id, did, name, notes, profile_image_url, |
|||
public_key_base64, next_pub_key_hash_b64, |
|||
sees_me, registered, created_at, updated_at |
|||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
|||
`, [
|
|||
contact.id, |
|||
contact.did, |
|||
contact.name || null, |
|||
contact.notes || null, |
|||
contact.profile_image_url || null, |
|||
contact.public_key_base64 || null, |
|||
contact.next_pub_key_hash_b64 || null, |
|||
contact.sees_me ? 1 : 0, |
|||
contact.registered ? 1 : 0, |
|||
contact.created_at, |
|||
contact.updated_at |
|||
]); |
|||
} |
|||
|
|||
// Update contact methods
|
|||
if (methods.length > 0) { |
|||
// Delete existing methods
|
|||
await db.exec( |
|||
'DELETE FROM contact_methods WHERE contact_id = ?', |
|||
[contact.id] |
|||
); |
|||
|
|||
// Insert new methods
|
|||
for (const method of methods) { |
|||
await db.exec(` |
|||
INSERT INTO contact_methods ( |
|||
id, contact_id, label, type, value, |
|||
created_at, updated_at |
|||
) VALUES (?, ?, ?, ?, ?, ?, ?) |
|||
`, [
|
|||
method.id, |
|||
contact.id, |
|||
method.label, |
|||
method.type, |
|||
method.value, |
|||
method.created_at, |
|||
method.updated_at |
|||
]); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
|
|||
// ============================================================================
|
|||
// Settings Operations
|
|||
// ============================================================================
|
|||
|
|||
/** |
|||
* Get setting by key |
|||
*/ |
|||
export async function getSetting(key: string): Promise<SQLiteSettings | null> { |
|||
const { db } = await initDatabase(); |
|||
|
|||
const settings = await db.selectAll<SQLiteSettings>( |
|||
'SELECT * FROM settings WHERE key = ?', |
|||
[key] |
|||
); |
|||
|
|||
return settings[0] || null; |
|||
} |
|||
|
|||
/** |
|||
* Get settings by account DID |
|||
*/ |
|||
export async function getSettingsByAccountDid(did: string): Promise<SQLiteSettings[]> { |
|||
const { db } = await initDatabase(); |
|||
|
|||
return db.selectAll<SQLiteSettings>( |
|||
'SELECT * FROM settings WHERE account_did = ? ORDER BY updated_at DESC', |
|||
[did] |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Set setting value |
|||
*/ |
|||
export async function setSetting(setting: SQLiteSettings): Promise<void> { |
|||
if (!isSQLiteSettings(setting)) { |
|||
throw new Error('Invalid settings data'); |
|||
} |
|||
|
|||
await withTransaction(async (db) => { |
|||
const existing = await db.selectOne<{ key: string }>( |
|||
'SELECT key FROM settings WHERE key = ?', |
|||
[setting.key] |
|||
); |
|||
|
|||
if (existing) { |
|||
await db.exec(` |
|||
UPDATE settings SET |
|||
account_did = ?, |
|||
value_json = ?, |
|||
updated_at = ? |
|||
WHERE key = ? |
|||
`, [
|
|||
setting.account_did || null, |
|||
setting.value_json, |
|||
Date.now(), |
|||
setting.key |
|||
]); |
|||
} else { |
|||
await db.exec(` |
|||
INSERT INTO settings ( |
|||
key, account_did, value_json, created_at, updated_at |
|||
) VALUES (?, ?, ?, ?, ?) |
|||
`, [
|
|||
setting.key, |
|||
setting.account_did || null, |
|||
setting.value_json, |
|||
setting.created_at, |
|||
setting.updated_at |
|||
]); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
// ============================================================================
|
|||
// Log Operations
|
|||
// ============================================================================
|
|||
|
|||
/** |
|||
* Add log entry |
|||
*/ |
|||
export async function addLog(log: SQLiteLog): Promise<void> { |
|||
await withTransaction(async (db) => { |
|||
await db.exec(` |
|||
INSERT INTO logs ( |
|||
id, level, message, metadata_json, created_at |
|||
) VALUES (?, ?, ?, ?, ?) |
|||
`, [
|
|||
log.id, |
|||
log.level, |
|||
log.message, |
|||
log.metadata_json || null, |
|||
log.created_at |
|||
]); |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Get logs by level |
|||
*/ |
|||
export async function getLogsByLevel( |
|||
level: string, |
|||
limit = 100, |
|||
offset = 0 |
|||
): Promise<SQLiteLog[]> { |
|||
const { db } = await initDatabase(); |
|||
|
|||
return db.selectAll<SQLiteLog>( |
|||
'SELECT * FROM logs WHERE level = ? ORDER BY created_at DESC LIMIT ? OFFSET ?', |
|||
[level, limit, offset] |
|||
); |
|||
} |
|||
|
|||
// ============================================================================
|
|||
// Secret Operations
|
|||
// ============================================================================
|
|||
|
|||
/** |
|||
* Get secret by key |
|||
*/ |
|||
export async function getSecret(key: string): Promise<SQLiteSecret | null> { |
|||
const { db } = await initDatabase(); |
|||
|
|||
const secrets = await db.selectAll<SQLiteSecret>( |
|||
'SELECT * FROM secrets WHERE key = ?', |
|||
[key] |
|||
); |
|||
|
|||
return secrets[0] || null; |
|||
} |
|||
|
|||
/** |
|||
* Set secret value |
|||
*/ |
|||
export async function setSecret(secret: SQLiteSecret): Promise<void> { |
|||
await withTransaction(async (db) => { |
|||
const existing = await db.selectOne<{ key: string }>( |
|||
'SELECT key FROM secrets WHERE key = ?', |
|||
[secret.key] |
|||
); |
|||
|
|||
if (existing) { |
|||
await db.exec(` |
|||
UPDATE secrets SET |
|||
value_encrypted = ?, |
|||
updated_at = ? |
|||
WHERE key = ? |
|||
`, [
|
|||
secret.value_encrypted, |
|||
Date.now(), |
|||
secret.key |
|||
]); |
|||
} else { |
|||
await db.exec(` |
|||
INSERT INTO secrets ( |
|||
key, value_encrypted, created_at, updated_at |
|||
) VALUES (?, ?, ?, ?) |
|||
`, [
|
|||
secret.key, |
|||
secret.value_encrypted, |
|||
secret.created_at, |
|||
secret.updated_at |
|||
]); |
|||
} |
|||
}); |
|||
} |
@ -0,0 +1,349 @@ |
|||
/** |
|||
* SQLite Type Definitions |
|||
* |
|||
* This file defines the type system for the SQLite implementation, |
|||
* mapping from the existing Dexie types to SQLite-compatible types. |
|||
* It includes both the database schema types and the runtime types. |
|||
*/ |
|||
|
|||
import { SQLiteCompatibleType } from '@jlongster/sql.js'; |
|||
|
|||
// ============================================================================
|
|||
// Base Types and Utilities
|
|||
// ============================================================================
|
|||
|
|||
/** |
|||
* SQLite column type mapping |
|||
*/ |
|||
export type SQLiteColumnType = |
|||
| 'INTEGER' // For numbers, booleans, dates
|
|||
| 'TEXT' // For strings, JSON
|
|||
| 'BLOB' // For binary data
|
|||
| 'REAL' // For floating point numbers
|
|||
| 'NULL'; // For null values
|
|||
|
|||
/** |
|||
* SQLite column definition |
|||
*/ |
|||
export interface SQLiteColumn { |
|||
name: string; |
|||
type: SQLiteColumnType; |
|||
nullable?: boolean; |
|||
primaryKey?: boolean; |
|||
unique?: boolean; |
|||
references?: { |
|||
table: string; |
|||
column: string; |
|||
}; |
|||
default?: SQLiteCompatibleType; |
|||
} |
|||
|
|||
/** |
|||
* SQLite table definition |
|||
*/ |
|||
export interface SQLiteTable { |
|||
name: string; |
|||
columns: SQLiteColumn[]; |
|||
indexes?: Array<{ |
|||
name: string; |
|||
columns: string[]; |
|||
unique?: boolean; |
|||
}>; |
|||
} |
|||
|
|||
// ============================================================================
|
|||
// Account Types
|
|||
// ============================================================================
|
|||
|
|||
/** |
|||
* SQLite-compatible Account type |
|||
* Maps from the Dexie Account type |
|||
*/ |
|||
export interface SQLiteAccount { |
|||
did: string; // TEXT PRIMARY KEY
|
|||
public_key_hex: string; // TEXT NOT NULL
|
|||
created_at: number; // INTEGER NOT NULL
|
|||
updated_at: number; // INTEGER NOT NULL
|
|||
identity_json?: string; // TEXT (encrypted JSON)
|
|||
mnemonic_encrypted?: string; // TEXT (encrypted)
|
|||
passkey_cred_id_hex?: string; // TEXT
|
|||
derivation_path?: string; // TEXT
|
|||
} |
|||
|
|||
export const ACCOUNTS_TABLE: SQLiteTable = { |
|||
name: 'accounts', |
|||
columns: [ |
|||
{ name: 'did', type: 'TEXT', primaryKey: true }, |
|||
{ name: 'public_key_hex', type: 'TEXT', nullable: false }, |
|||
{ name: 'created_at', type: 'INTEGER', nullable: false }, |
|||
{ name: 'updated_at', type: 'INTEGER', nullable: false }, |
|||
{ name: 'identity_json', type: 'TEXT' }, |
|||
{ name: 'mnemonic_encrypted', type: 'TEXT' }, |
|||
{ name: 'passkey_cred_id_hex', type: 'TEXT' }, |
|||
{ name: 'derivation_path', type: 'TEXT' } |
|||
], |
|||
indexes: [ |
|||
{ name: 'idx_accounts_created_at', columns: ['created_at'] }, |
|||
{ name: 'idx_accounts_updated_at', columns: ['updated_at'] } |
|||
] |
|||
}; |
|||
|
|||
// ============================================================================
|
|||
// Contact Types
|
|||
// ============================================================================
|
|||
|
|||
/** |
|||
* SQLite-compatible ContactMethod type |
|||
*/ |
|||
export interface SQLiteContactMethod { |
|||
id: string; // TEXT PRIMARY KEY
|
|||
contact_id: string; // TEXT NOT NULL
|
|||
label: string; // TEXT NOT NULL
|
|||
type: string; // TEXT NOT NULL
|
|||
value: string; // TEXT NOT NULL
|
|||
created_at: number; // INTEGER NOT NULL
|
|||
updated_at: number; // INTEGER NOT NULL
|
|||
} |
|||
|
|||
/** |
|||
* SQLite-compatible Contact type |
|||
*/ |
|||
export interface SQLiteContact { |
|||
id: string; // TEXT PRIMARY KEY
|
|||
did: string; // TEXT NOT NULL
|
|||
name?: string; // TEXT
|
|||
notes?: string; // TEXT
|
|||
profile_image_url?: string; // TEXT
|
|||
public_key_base64?: string; // TEXT
|
|||
next_pub_key_hash_b64?: string; // TEXT
|
|||
sees_me?: boolean; // INTEGER (0 or 1)
|
|||
registered?: boolean; // INTEGER (0 or 1)
|
|||
created_at: number; // INTEGER NOT NULL
|
|||
updated_at: number; // INTEGER NOT NULL
|
|||
} |
|||
|
|||
export const CONTACTS_TABLE: SQLiteTable = { |
|||
name: 'contacts', |
|||
columns: [ |
|||
{ name: 'id', type: 'TEXT', primaryKey: true }, |
|||
{ name: 'did', type: 'TEXT', nullable: false }, |
|||
{ name: 'name', type: 'TEXT' }, |
|||
{ name: 'notes', type: 'TEXT' }, |
|||
{ name: 'profile_image_url', type: 'TEXT' }, |
|||
{ name: 'public_key_base64', type: 'TEXT' }, |
|||
{ name: 'next_pub_key_hash_b64', type: 'TEXT' }, |
|||
{ name: 'sees_me', type: 'INTEGER' }, |
|||
{ name: 'registered', type: 'INTEGER' }, |
|||
{ name: 'created_at', type: 'INTEGER', nullable: false }, |
|||
{ name: 'updated_at', type: 'INTEGER', nullable: false } |
|||
], |
|||
indexes: [ |
|||
{ name: 'idx_contacts_did', columns: ['did'] }, |
|||
{ name: 'idx_contacts_created_at', columns: ['created_at'] } |
|||
] |
|||
}; |
|||
|
|||
export const CONTACT_METHODS_TABLE: SQLiteTable = { |
|||
name: 'contact_methods', |
|||
columns: [ |
|||
{ name: 'id', type: 'TEXT', primaryKey: true }, |
|||
{ name: 'contact_id', type: 'TEXT', nullable: false, |
|||
references: { table: 'contacts', column: 'id' } }, |
|||
{ name: 'label', type: 'TEXT', nullable: false }, |
|||
{ name: 'type', type: 'TEXT', nullable: false }, |
|||
{ name: 'value', type: 'TEXT', nullable: false }, |
|||
{ name: 'created_at', type: 'INTEGER', nullable: false }, |
|||
{ name: 'updated_at', type: 'INTEGER', nullable: false } |
|||
], |
|||
indexes: [ |
|||
{ name: 'idx_contact_methods_contact_id', columns: ['contact_id'] } |
|||
] |
|||
}; |
|||
|
|||
// ============================================================================
|
|||
// Settings Types
|
|||
// ============================================================================
|
|||
|
|||
/** |
|||
* SQLite-compatible Settings type |
|||
*/ |
|||
export interface SQLiteSettings { |
|||
key: string; // TEXT PRIMARY KEY
|
|||
account_did?: string; // TEXT
|
|||
value_json: string; // TEXT NOT NULL (JSON stringified)
|
|||
created_at: number; // INTEGER NOT NULL
|
|||
updated_at: number; // INTEGER NOT NULL
|
|||
} |
|||
|
|||
export const SETTINGS_TABLE: SQLiteTable = { |
|||
name: 'settings', |
|||
columns: [ |
|||
{ name: 'key', type: 'TEXT', primaryKey: true }, |
|||
{ name: 'account_did', type: 'TEXT' }, |
|||
{ name: 'value_json', type: 'TEXT', nullable: false }, |
|||
{ name: 'created_at', type: 'INTEGER', nullable: false }, |
|||
{ name: 'updated_at', type: 'INTEGER', nullable: false } |
|||
], |
|||
indexes: [ |
|||
{ name: 'idx_settings_account_did', columns: ['account_did'] }, |
|||
{ name: 'idx_settings_updated_at', columns: ['updated_at'] } |
|||
] |
|||
}; |
|||
|
|||
// ============================================================================
|
|||
// Log Types
|
|||
// ============================================================================
|
|||
|
|||
/** |
|||
* SQLite-compatible Log type |
|||
*/ |
|||
export interface SQLiteLog { |
|||
id: string; // TEXT PRIMARY KEY
|
|||
level: string; // TEXT NOT NULL
|
|||
message: string; // TEXT NOT NULL
|
|||
metadata_json?: string; // TEXT (JSON stringified)
|
|||
created_at: number; // INTEGER NOT NULL
|
|||
} |
|||
|
|||
export const LOGS_TABLE: SQLiteTable = { |
|||
name: 'logs', |
|||
columns: [ |
|||
{ name: 'id', type: 'TEXT', primaryKey: true }, |
|||
{ name: 'level', type: 'TEXT', nullable: false }, |
|||
{ name: 'message', type: 'TEXT', nullable: false }, |
|||
{ name: 'metadata_json', type: 'TEXT' }, |
|||
{ name: 'created_at', type: 'INTEGER', nullable: false } |
|||
], |
|||
indexes: [ |
|||
{ name: 'idx_logs_level', columns: ['level'] }, |
|||
{ name: 'idx_logs_created_at', columns: ['created_at'] } |
|||
] |
|||
}; |
|||
|
|||
// ============================================================================
|
|||
// Secret Types
|
|||
// ============================================================================
|
|||
|
|||
/** |
|||
* SQLite-compatible Secret type |
|||
* Note: This table should be encrypted at the database level |
|||
*/ |
|||
export interface SQLiteSecret { |
|||
key: string; // TEXT PRIMARY KEY
|
|||
value_encrypted: string; // TEXT NOT NULL (encrypted)
|
|||
created_at: number; // INTEGER NOT NULL
|
|||
updated_at: number; // INTEGER NOT NULL
|
|||
} |
|||
|
|||
export const SECRETS_TABLE: SQLiteTable = { |
|||
name: 'secrets', |
|||
columns: [ |
|||
{ name: 'key', type: 'TEXT', primaryKey: true }, |
|||
{ name: 'value_encrypted', type: 'TEXT', nullable: false }, |
|||
{ name: 'created_at', type: 'INTEGER', nullable: false }, |
|||
{ name: 'updated_at', type: 'INTEGER', nullable: false } |
|||
], |
|||
indexes: [ |
|||
{ name: 'idx_secrets_updated_at', columns: ['updated_at'] } |
|||
] |
|||
}; |
|||
|
|||
// ============================================================================
|
|||
// Database Schema
|
|||
// ============================================================================
|
|||
|
|||
/** |
|||
* Complete database schema definition |
|||
*/ |
|||
export const DATABASE_SCHEMA: SQLiteTable[] = [ |
|||
ACCOUNTS_TABLE, |
|||
CONTACTS_TABLE, |
|||
CONTACT_METHODS_TABLE, |
|||
SETTINGS_TABLE, |
|||
LOGS_TABLE, |
|||
SECRETS_TABLE |
|||
]; |
|||
|
|||
// ============================================================================
|
|||
// Type Guards and Validators
|
|||
// ============================================================================
|
|||
|
|||
/** |
|||
* Type guard for SQLiteAccount |
|||
*/ |
|||
export function isSQLiteAccount(value: unknown): value is SQLiteAccount { |
|||
return ( |
|||
typeof value === 'object' && |
|||
value !== null && |
|||
typeof (value as SQLiteAccount).did === 'string' && |
|||
typeof (value as SQLiteAccount).public_key_hex === 'string' && |
|||
typeof (value as SQLiteAccount).created_at === 'number' && |
|||
typeof (value as SQLiteAccount).updated_at === 'number' |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Type guard for SQLiteContact |
|||
*/ |
|||
export function isSQLiteContact(value: unknown): value is SQLiteContact { |
|||
return ( |
|||
typeof value === 'object' && |
|||
value !== null && |
|||
typeof (value as SQLiteContact).id === 'string' && |
|||
typeof (value as SQLiteContact).did === 'string' && |
|||
typeof (value as SQLiteContact).created_at === 'number' && |
|||
typeof (value as SQLiteContact).updated_at === 'number' |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Type guard for SQLiteSettings |
|||
*/ |
|||
export function isSQLiteSettings(value: unknown): value is SQLiteSettings { |
|||
return ( |
|||
typeof value === 'object' && |
|||
value !== null && |
|||
typeof (value as SQLiteSettings).key === 'string' && |
|||
typeof (value as SQLiteSettings).value_json === 'string' && |
|||
typeof (value as SQLiteSettings).created_at === 'number' && |
|||
typeof (value as SQLiteSettings).updated_at === 'number' |
|||
); |
|||
} |
|||
|
|||
// ============================================================================
|
|||
// Migration Types
|
|||
// ============================================================================
|
|||
|
|||
/** |
|||
* Type for migration data from Dexie to SQLite |
|||
*/ |
|||
export interface MigrationData { |
|||
accounts: SQLiteAccount[]; |
|||
contacts: SQLiteContact[]; |
|||
contactMethods: SQLiteContactMethod[]; |
|||
settings: SQLiteSettings[]; |
|||
logs: SQLiteLog[]; |
|||
secrets: SQLiteSecret[]; |
|||
metadata: { |
|||
version: string; |
|||
timestamp: number; |
|||
source: 'dexie'; |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Migration result type |
|||
*/ |
|||
export interface MigrationResult { |
|||
success: boolean; |
|||
error?: Error; |
|||
stats: { |
|||
accounts: number; |
|||
contacts: number; |
|||
contactMethods: number; |
|||
settings: number; |
|||
logs: number; |
|||
secrets: number; |
|||
}; |
|||
duration: number; |
|||
} |
@ -1,215 +0,0 @@ |
|||
import { createPinia } from "pinia"; |
|||
import { App as VueApp, ComponentPublicInstance, createApp } from "vue"; |
|||
import App from "./App.vue"; |
|||
import "./registerServiceWorker"; |
|||
import router from "./router"; |
|||
import axios from "axios"; |
|||
import VueAxios from "vue-axios"; |
|||
import Notifications from "notiwind"; |
|||
import "./assets/styles/tailwind.css"; |
|||
|
|||
import { library } from "@fortawesome/fontawesome-svg-core"; |
|||
import { |
|||
faArrowDown, |
|||
faArrowLeft, |
|||
faArrowRight, |
|||
faArrowRotateBackward, |
|||
faArrowUpRightFromSquare, |
|||
faArrowUp, |
|||
faBan, |
|||
faBitcoinSign, |
|||
faBurst, |
|||
faCalendar, |
|||
faCamera, |
|||
faCameraRotate, |
|||
faCaretDown, |
|||
faChair, |
|||
faCheck, |
|||
faChevronDown, |
|||
faChevronLeft, |
|||
faChevronRight, |
|||
faChevronUp, |
|||
faCircle, |
|||
faCircleCheck, |
|||
faCircleInfo, |
|||
faCircleQuestion, |
|||
faCircleUser, |
|||
faClock, |
|||
faCoins, |
|||
faComment, |
|||
faCopy, |
|||
faDollar, |
|||
faEllipsis, |
|||
faEllipsisVertical, |
|||
faEnvelopeOpenText, |
|||
faEraser, |
|||
faEye, |
|||
faEyeSlash, |
|||
faFileContract, |
|||
faFileLines, |
|||
faFilter, |
|||
faFloppyDisk, |
|||
faFolderOpen, |
|||
faForward, |
|||
faGift, |
|||
faGlobe, |
|||
faHammer, |
|||
faHand, |
|||
faHandHoldingDollar, |
|||
faHandHoldingHeart, |
|||
faHouseChimney, |
|||
faImage, |
|||
faImagePortrait, |
|||
faLeftRight, |
|||
faLightbulb, |
|||
faLink, |
|||
faLocationDot, |
|||
faLongArrowAltLeft, |
|||
faLongArrowAltRight, |
|||
faMagnifyingGlass, |
|||
faMessage, |
|||
faMinus, |
|||
faPen, |
|||
faPersonCircleCheck, |
|||
faPersonCircleQuestion, |
|||
faPlus, |
|||
faQuestion, |
|||
faQrcode, |
|||
faRightFromBracket, |
|||
faRotate, |
|||
faShareNodes, |
|||
faSpinner, |
|||
faSquare, |
|||
faSquareCaretDown, |
|||
faSquareCaretUp, |
|||
faSquarePlus, |
|||
faTrashCan, |
|||
faTriangleExclamation, |
|||
faUser, |
|||
faUsers, |
|||
faXmark, |
|||
} from "@fortawesome/free-solid-svg-icons"; |
|||
|
|||
library.add( |
|||
faArrowDown, |
|||
faArrowLeft, |
|||
faArrowRight, |
|||
faArrowRotateBackward, |
|||
faArrowUpRightFromSquare, |
|||
faArrowUp, |
|||
faBan, |
|||
faBitcoinSign, |
|||
faBurst, |
|||
faCalendar, |
|||
faCamera, |
|||
faCameraRotate, |
|||
faCaretDown, |
|||
faChair, |
|||
faCheck, |
|||
faChevronDown, |
|||
faChevronLeft, |
|||
faChevronRight, |
|||
faChevronUp, |
|||
faCircle, |
|||
faCircleCheck, |
|||
faCircleInfo, |
|||
faCircleQuestion, |
|||
faCircleUser, |
|||
faClock, |
|||
faCoins, |
|||
faComment, |
|||
faCopy, |
|||
faDollar, |
|||
faEllipsis, |
|||
faEllipsisVertical, |
|||
faEnvelopeOpenText, |
|||
faEraser, |
|||
faEye, |
|||
faEyeSlash, |
|||
faFileContract, |
|||
faFileLines, |
|||
faFilter, |
|||
faFloppyDisk, |
|||
faFolderOpen, |
|||
faForward, |
|||
faGift, |
|||
faGlobe, |
|||
faHammer, |
|||
faHand, |
|||
faHandHoldingDollar, |
|||
faHandHoldingHeart, |
|||
faHouseChimney, |
|||
faImage, |
|||
faImagePortrait, |
|||
faLeftRight, |
|||
faLightbulb, |
|||
faLink, |
|||
faLocationDot, |
|||
faLongArrowAltLeft, |
|||
faLongArrowAltRight, |
|||
faMagnifyingGlass, |
|||
faMessage, |
|||
faMinus, |
|||
faPen, |
|||
faPersonCircleCheck, |
|||
faPersonCircleQuestion, |
|||
faPlus, |
|||
faQrcode, |
|||
faQuestion, |
|||
faRotate, |
|||
faRightFromBracket, |
|||
faShareNodes, |
|||
faSpinner, |
|||
faSquare, |
|||
faSquareCaretDown, |
|||
faSquareCaretUp, |
|||
faSquarePlus, |
|||
faTrashCan, |
|||
faTriangleExclamation, |
|||
faUser, |
|||
faUsers, |
|||
faXmark, |
|||
); |
|||
|
|||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; |
|||
import Camera from "simple-vue-camera"; |
|||
import { logger } from "./utils/logger"; |
|||
|
|||
// Can trigger this with a 'throw' inside some top-level function, eg. on the HomeView
|
|||
function setupGlobalErrorHandler(app: VueApp) { |
|||
// @ts-expect-error 'cause we cannot see why config is not defined
|
|||
app.config.errorHandler = ( |
|||
err: Error, |
|||
instance: ComponentPublicInstance | null, |
|||
info: string, |
|||
) => { |
|||
logger.error( |
|||
"Ouch! Global Error Handler.", |
|||
"Error:", |
|||
err, |
|||
"- Error toString:", |
|||
err.toString(), |
|||
"- Info:", |
|||
info, |
|||
"- Instance:", |
|||
instance, |
|||
); |
|||
// Want to show a nice notiwind notification but can't figure out how.
|
|||
alert( |
|||
(err.message || "Something bad happened") + |
|||
" - Try reloading or restarting the app.", |
|||
); |
|||
}; |
|||
} |
|||
|
|||
const app = createApp(App) |
|||
.component("fa", FontAwesomeIcon) |
|||
.component("camera", Camera) |
|||
.use(createPinia()) |
|||
.use(VueAxios, axios) |
|||
.use(router) |
|||
.use(Notifications); |
|||
|
|||
setupGlobalErrorHandler(app); |
|||
|
|||
app.mount("#app"); |
Loading…
Reference in new issue