UNPKG

@smartsamurai/krapi-sdk

Version:

KRAPI TypeScript SDK - Easy-to-use client SDK for connecting to self-hosted KRAPI servers (like Appwrite SDK)

670 lines (598 loc) 19.9 kB
import { KrapiError } from "./core/krapi-error"; import { DatabaseHealthStatus, SchemaValidationResult, MigrationResult, AutoFixResult, SchemaMismatch, FieldMismatch, DatabaseIssue, } from "./types"; import { normalizeError } from "./utils/error-handler"; /** * Database Health Management System * * Provides auto-fixers and auto-migration capabilities for SQLite. * Ensures database consistency during development without manual intervention. * * @class DatabaseHealthManager * @example * const manager = new DatabaseHealthManager(dbConnection, console); * const health = await manager.healthCheck(); * if (!health.isHealthy) { * await manager.autoFix(); * } */ export class DatabaseHealthManager { // @ts-expect-error - Schema version reserved for future use private _schemaVersion = "1.0.0"; // private _migrationHistory: MigrationRecord[] = []; private expectedSchema: ExpectedSchema = { tables: {}, }; constructor( private dbConnection: { query: (sql: string, params?: unknown[]) => Promise<{ rows?: unknown[] }>; }, private logger: Logger = console ) { // Don't initialize schema tracking during health checks to avoid conflicts // this.initializeSchemaTracking(); } /** * Simple database health check - just test connection and basic query * This is more reliable than complex schema validation */ async healthCheck(): Promise<DatabaseHealthStatus> { const startTime = Date.now(); this.logger.info("Starting simple database health check..."); try { // Simple connection test - just try to execute a basic query const result = await this.dbConnection.query("SELECT 1 as health_check"); if (result && result.rows && result.rows.length > 0) { const healthStatus: DatabaseHealthStatus = { connected: true, isHealthy: true, issues: [], warnings: [], checkDuration: Date.now() - startTime, response_time: Date.now() - startTime, }; this.logger.info( `Health check completed in ${healthStatus.checkDuration}ms. Healthy: ${healthStatus.isHealthy}` ); return healthStatus; } else { throw KrapiError.validationError( "Database query returned no results", "query", "health check query" ); } } catch (error) { this.logger.error("Health check failed:", error); const healthStatus: DatabaseHealthStatus = { connected: false, isHealthy: false, issues: [ { type: "connection_failed", severity: "error", description: `Database connection failed: ${ error instanceof Error ? error.message : "Unknown error" }`, }, ], warnings: [], checkDuration: Date.now() - startTime, response_time: Date.now() - startTime, }; return healthStatus; } } /** * Automatically fix all detected database issues * Non-destructive approach that preserves existing data */ async autoFix(): Promise<AutoFixResult> { const startTime = Date.now(); this.logger.info("Starting automatic database fixes..."); try { // First run health check to identify issues const healthStatus = await this.healthCheck(); if (healthStatus.isHealthy) { this.logger.info("Database is healthy, no fixes needed"); return { success: true, fixesApplied: 0, duration: Date.now() - startTime, details: "No issues detected", }; } const fixesApplied: string[] = []; let totalFixes = 0; // Apply fixes for each type of issue for (const issue of healthStatus.issues || []) { try { const fixResult = await this.applyFix(issue); if (fixResult.success) { fixesApplied.push(`${issue.type}: ${issue.description}`); totalFixes++; } } catch (error) { this.logger.warn(`Failed to apply fix for ${issue.type}:`, error); } } const result: AutoFixResult = { success: totalFixes > 0, fixesApplied: totalFixes, duration: Date.now() - startTime, details: `Applied ${totalFixes} fixes`, appliedFixes: fixesApplied, }; this.logger.info( `Auto-fix completed. Applied ${totalFixes} fixes in ${result.duration}ms` ); return result; } catch (error) { this.logger.error("Auto-fix failed:", error); throw normalizeError(error, "INTERNAL_ERROR", { operation: "autoFix", }); } } /** * Validate current database schema against expected schema */ async validateSchema(): Promise<SchemaValidationResult> { this.logger.info("Validating database schema..."); try { const currentSchema = await this.getCurrentSchema(); const validationResult = this.compareSchemas( currentSchema, this.expectedSchema ); const isValid = validationResult.mismatches.length === 0; return { valid: isValid, isValid, errors: [], warnings: [], missing_tables: validationResult.missingTables, extra_tables: validationResult.extraTables, field_mismatches: validationResult.fieldMismatches, mismatches: validationResult.mismatches, missingTables: validationResult.missingTables, extraTables: validationResult.extraTables, fieldMismatches: validationResult.fieldMismatches, timestamp: new Date().toISOString(), }; } catch (error) { this.logger.error("Schema validation failed:", error); throw normalizeError(error, "INTERNAL_ERROR", { operation: "validateSchema", }); } } /** * Run pending migrations to update database schema */ async migrate(): Promise<MigrationResult> { const startTime = Date.now(); this.logger.info("Starting database migration..."); try { const pendingMigrations = await this.getPendingMigrations(); if (pendingMigrations.length === 0) { this.logger.info("No pending migrations"); return { success: true, migrations_applied: 0, errors: [], duration: Date.now() - startTime, details: "No pending migrations", }; } const appliedMigrations: string[] = []; let successCount = 0; for (const migration of pendingMigrations) { try { await this.applyMigration(migration); appliedMigrations.push(migration.name); successCount++; this.logger.info(`Applied migration: ${migration.name}`); } catch (error) { this.logger.error( `Failed to apply migration ${migration.name}:`, error ); throw error; // Stop migration process on first failure } } const result: MigrationResult = { success: successCount === pendingMigrations.length, migrations_applied: successCount, errors: [], duration: Date.now() - startTime, details: `Applied ${successCount} migrations`, }; this.logger.info( `Migration completed. Applied ${successCount} migrations in ${result.duration}ms` ); return result; } catch (error) { this.logger.error("Migration failed:", error); throw normalizeError(error, "INTERNAL_ERROR", { operation: "migrate", }); } } /** * Rollback to a previous schema version */ async rollback(targetVersion: string): Promise<MigrationResult> { this.logger.info(`Rolling back to version ${targetVersion}...`); try { const rollbackMigrations = await this.getRollbackMigrations( targetVersion ); if (rollbackMigrations.length === 0) { this.logger.info("No rollback migrations needed"); return { success: true, migrations_applied: 0, errors: [], duration: 0, details: "No rollback needed", }; } // Apply rollback migrations in reverse order const reversedMigrations = rollbackMigrations.reverse(); const appliedRollbacks: string[] = []; let successCount = 0; for (const migration of reversedMigrations) { try { await this.applyRollbackMigration(migration); appliedRollbacks.push(migration.name); successCount++; } catch (error) { this.logger.error( `Failed to rollback migration ${migration.name}:`, error ); throw error; } } return { success: successCount === rollbackMigrations.length, migrations_applied: successCount, errors: [], duration: 0, details: `Rolled back ${successCount} migrations`, }; } catch (error) { this.logger.error("Rollback failed:", error); throw normalizeError(error, "INTERNAL_ERROR", { operation: "rollback", }); } } // Private helper methods // @ts-expect-error - Method reserved for future use private async _initializeSchemaTracking(): Promise<void> { try { // Create migration tracking table if it doesn't exist await this.createMigrationTable(); // Load expected schema from configuration or generate from current code this.expectedSchema = await this.loadExpectedSchema(); this.logger.info("Database health manager initialized"); } catch (error) { this.logger.error("Failed to initialize database health manager:", error); throw error; } } private async createMigrationTable(): Promise<void> { try { const createTableSQL = ` CREATE TABLE IF NOT EXISTS schema_migrations ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(255) NOT NULL, version VARCHAR(50) NOT NULL, applied_at TEXT DEFAULT CURRENT_TIMESTAMP, checksum VARCHAR(64) NOT NULL, execution_time_ms INTEGER, status VARCHAR(20) DEFAULT 'success' ); `; await this.dbConnection.query(createTableSQL); // Create indexes separately with error handling try { await this.dbConnection.query( "CREATE INDEX IF NOT EXISTS idx_migrations_version ON schema_migrations(version)" ); } catch { // Index might already exist, ignore error this.logger.info( "Migration version index already exists or failed to create" ); } try { await this.dbConnection.query( "CREATE INDEX IF NOT EXISTS idx_migrations_applied_at ON schema_migrations(applied_at)" ); } catch { // Index might already exist, ignore error this.logger.info( "Migration applied_at index already exists or failed to create" ); } } catch { // Table might already exist, ignore error this.logger.info("Migration table already exists or failed to create"); } } private async loadExpectedSchema(): Promise<ExpectedSchema> { // This would load from a schema definition file or generate from TypeScript interfaces // For now, return a basic structure return { tables: { users: { fields: { id: { type: "uuid", nullable: false, primary: true }, username: { type: "varchar(255)", nullable: false, unique: true }, email: { type: "varchar(255)", nullable: false, unique: true }, created_at: { type: "timestamp", nullable: false, default: "CURRENT_TIMESTAMP", }, }, indexes: ["idx_users_username", "idx_users_email"], constraints: ["users_email_check"], }, // Add more table definitions as needed }, }; } // @ts-expect-error - Method reserved for future use private async _checkTableStructure(): Promise<HealthCheckResult> { try { const issues: DatabaseIssue[] = []; const warnings: DatabaseIssue[] = []; const recommendations: string[] = []; // Get expected tables from schema const expectedTables = Object.keys(this.expectedSchema.tables); // Check if all expected tables exist for (const tableName of expectedTables) { const tableExists = await this.checkTableExists(tableName); if (!tableExists) { issues.push({ type: "missing_table", severity: "error", table: tableName, description: `Table '${tableName}' is missing. Suggestion: Create the missing table`, }); recommendations.push(`Create table '${tableName}'`); } } return { isHealthy: issues.length === 0, issues, warnings, recommendations, }; } catch (error) { return { isHealthy: false, issues: [ { type: "check_failed", severity: "error", description: `Failed to check table structure: ${ error instanceof Error ? error.message : "Unknown error" }`, }, ], warnings: [], recommendations: [], }; } } // @ts-expect-error - Method reserved for future use private async _checkFieldDefinitions(): Promise<HealthCheckResult> { try { const issues: DatabaseIssue[] = []; const warnings: DatabaseIssue[] = []; const recommendations: string[] = []; // Check each table's field definitions for (const [tableName, tableDef] of Object.entries( this.expectedSchema.tables )) { const tableExists = await this.checkTableExists(tableName); if (!tableExists) continue; const actualFields = await this.getTableFields(tableName); // Check for missing fields for (const [fieldName] of Object.entries(tableDef.fields)) { const fieldExists = actualFields.some((f) => f.name === fieldName); if (!fieldExists) { issues.push({ type: "missing_field", severity: "error", table: tableName, field: fieldName, description: `Field '${fieldName}' is missing from table '${tableName}'. Suggestion: Add the missing field to the table`, }); recommendations.push( `Add field '${fieldName}' to table '${tableName}'` ); } } // Check for extra fields for (const actualField of actualFields) { if (!(actualField.name in tableDef.fields)) { warnings.push({ type: "extra_field", severity: "warning", table: tableName, field: actualField.name, description: `Extra field '${actualField.name}' found in table '${tableName}'. Suggestion: Consider removing unused fields`, }); } } } return { isHealthy: issues.length === 0, issues, warnings, recommendations, }; } catch (error) { return { isHealthy: false, issues: [ { type: "check_failed", severity: "error", description: `Failed to check field definitions: ${ error instanceof Error ? error.message : "Unknown error" }`, }, ], warnings: [], recommendations: [], }; } } // @ts-expect-error - Method reserved for future use private async _checkIndexes(): Promise<HealthCheckResult> { // Implementation for checking indexes return { isHealthy: true, issues: [], warnings: [], recommendations: [] }; } // @ts-expect-error - Method reserved for future use private async _checkConstraints(): Promise<HealthCheckResult> { // Implementation for checking constraints return { isHealthy: true, issues: [], warnings: [], recommendations: [] }; } // @ts-expect-error - Method reserved for future use private async _checkForeignKeys(): Promise<HealthCheckResult> { // Implementation for checking foreign keys return { isHealthy: true, issues: [], warnings: [], recommendations: [] }; } // @ts-expect-error - Method reserved for future use private async _checkDataIntegrity(): Promise<HealthCheckResult> { // Implementation for checking data integrity return { isHealthy: true, issues: [], warnings: [], recommendations: [] }; } private async applyFix(_issue: DatabaseIssue): Promise<{ success: boolean }> { // Implementation for applying specific fixes return { success: true }; } private async getCurrentSchema(): Promise<Record<string, unknown>> { // Implementation for getting current database schema return {}; } private compareSchemas( _current: unknown, _expected: unknown ): { mismatches: SchemaMismatch[]; missingTables: string[]; extraTables: string[]; fieldMismatches: FieldMismatch[]; } { // Implementation for comparing schemas return { mismatches: [], missingTables: [], extraTables: [], fieldMismatches: [], }; } private async getPendingMigrations(): Promise<Migration[]> { // Implementation for getting pending migrations return []; } private async applyMigration(_migration: Migration): Promise<void> { // Implementation for applying a migration } private async getRollbackMigrations( _targetVersion: string ): Promise<Migration[]> { // Implementation for getting rollback migrations return []; } private async applyRollbackMigration(_migration: Migration): Promise<void> { // Implementation for applying a rollback migration } // Helper methods for health checks private async checkTableExists(tableName: string): Promise<boolean> { try { const result = await this.dbConnection.query( "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = $1)", [tableName] ); return (result.rows?.[0] as { exists: boolean })?.exists || false; } catch { return false; } } private async getTableFields( tableName: string ): Promise<Array<{ name: string; type: string }>> { try { const result = await this.dbConnection.query( `SELECT column_name as name, data_type as type FROM information_schema.columns WHERE table_name = $1 ORDER BY ordinal_position`, [tableName] ); return (result.rows || []) as Array<{ name: string; type: string }>; } catch { return []; } } } // Type definitions for the database health system interface Logger { info(message: string, ...args: unknown[]): void; warn(message: string, ...args: unknown[]): void; error(message: string, ...args: unknown[]): void; } // interface MigrationRecord { // id: number; // name: string; // version: string; // appliedAt: Date; // checksum: string; // executionTimeMs: number; // status: string; // } interface ExpectedSchema { tables: Record<string, TableDefinition>; } interface TableDefinition { fields: Record<string, FieldDefinition>; indexes: string[]; constraints: string[]; } interface FieldDefinition { type: string; nullable: boolean; primary?: boolean; unique?: boolean; default?: string; } interface Migration { name: string; version: string; up: string; down: string; checksum: string; } interface HealthCheckResult { isHealthy: boolean; issues: DatabaseIssue[]; warnings: DatabaseIssue[]; recommendations: string[]; } // Use DatabaseIssue from types.ts