@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
text/typescript
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