feat(db): Implement SQLite database layer with migration support
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 installed
This commit is contained in:
389
docs/dexie-to-sqlite-mapping.md
Normal file
389
docs/dexie-to-sqlite-mapping.md
Normal file
@@ -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
|
||||||
306
docs/storage-implementation-checklist.md
Normal file
306
docs/storage-implementation-checklist.md
Normal file
@@ -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
|
||||||
29
main.js
29
main.js
@@ -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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
293
src/db/sqlite/init.ts
Normal file
293
src/db/sqlite/init.ts
Normal file
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
374
src/db/sqlite/migration.ts
Normal file
374
src/db/sqlite/migration.ts
Normal file
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
449
src/db/sqlite/operations.ts
Normal file
449
src/db/sqlite/operations.ts
Normal file
@@ -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
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
349
src/db/sqlite/types.ts
Normal file
349
src/db/sqlite/types.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
215
src/main.ts
215
src/main.ts
@@ -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");
|
|
||||||
Reference in New Issue
Block a user