Browse Source
Add SQLite database implementation with comprehensive features: - Core database functionality: - Connection management and pooling - Schema creation and validation - Transaction support with rollback - Backup and restore capabilities - Health checks and integrity verification - Data migration: - Migration utilities from Dexie to SQLite - Data transformation and validation - Migration verification and rollback - Backup before migration - CRUD operations for all entities: - Accounts, contacts, and contact methods - Settings and secrets - Logging and audit trails - Type safety and error handling: - Full TypeScript type definitions - Runtime data validation - Comprehensive error handling - Transaction safety Note: Requires @wa-sqlite/sql.js package to be installednew-storage
8 changed files with 2160 additions and 244 deletions
@ -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,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