Browse Source
- Replace any[] with SqlValue[] type for SQL parameters in runMigrations - Update import to use QueryExecResult from interfaces/database - Add proper typing for SQL parameter values (string | number | null | Uint8Array) This change improves type safety and helps catch potential SQL parameter type mismatches at compile time, reducing the risk of runtime errors or data corruption.
14 changed files with 56 additions and 1510 deletions
@ -1,293 +0,0 @@ |
|||
/** |
|||
* 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'); |
|||
} |
|||
} |
@ -1,374 +0,0 @@ |
|||
/** |
|||
* 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'); |
|||
} |
|||
} |
@ -1,449 +0,0 @@ |
|||
/** |
|||
* 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 |
|||
]); |
|||
} |
|||
}); |
|||
} |
@ -1,349 +0,0 @@ |
|||
/** |
|||
* 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; |
|||
} |
Loading…
Reference in new issue