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