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.sql-absurd-sql
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