@smartsamurai/krapi-sdk
Version:
KRAPI TypeScript SDK - Easy-to-use client SDK for connecting to self-hosted KRAPI servers (like Appwrite SDK)
441 lines (416 loc) • 13.5 kB
text/typescript
/**
* Health Adapter
*
* Unifies HealthHttpClient and HealthService behind a common interface.
* Implements all methods required by KrapiSocketInterface.
*/
import { DatabaseHealthManager } from "../../database-health";
import { HealthService , SystemHealth, DatabaseHealth } from "../../health-service";
import { HealthHttpClient } from "../../http-clients/health-http-client";
import { createAdapterInitError } from "./error-handler";
type Mode = "client" | "server";
export class HealthAdapter {
private mode: Mode;
private httpClient: HealthHttpClient | undefined;
private service: HealthService | undefined;
private databaseHealthManager: DatabaseHealthManager | undefined;
constructor(
mode: Mode,
httpClient?: HealthHttpClient,
service?: HealthService,
databaseHealthManager?: DatabaseHealthManager
) {
this.mode = mode;
this.httpClient = httpClient;
this.service = service;
this.databaseHealthManager = databaseHealthManager;
}
async check(): Promise<{
healthy: boolean;
message: string;
details?: Record<string, unknown>;
version: string;
}> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode);
}
const response = await this.httpClient.check();
const data = response.data as SystemHealth;
const result: {
healthy: boolean;
message: string;
details?: Record<string, unknown>;
version: string;
} = {
healthy: data?.healthy || false,
message: data?.message || "Health check completed",
version: data?.version || "unknown",
};
if (data?.details) {
result.details = data.details;
}
return result;
} else {
if (!this.service) {
throw createAdapterInitError("Health service", this.mode);
}
// Server mode: use runDiagnostics to get health status
const diagnostics = await this.service.runDiagnostics();
return {
healthy: diagnostics.success,
message: diagnostics.message,
details: diagnostics.details as Record<string, unknown>,
version: "1.0.0", // Server version - could be from package.json
};
}
}
async checkDatabase(): Promise<{
healthy: boolean;
message: string;
details?: Record<string, unknown>;
}> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode);
}
const response = await this.httpClient.checkDatabase();
const data = response.data as DatabaseHealth;
const result: {
healthy: boolean;
message: string;
details?: Record<string, unknown>;
} = {
healthy: data?.healthy || false,
message: data?.message || "Database check completed",
};
if (data?.details) {
result.details = data.details;
}
return result;
} else {
if (!this.service) {
throw createAdapterInitError("Health service", this.mode);
}
// Server mode: use runDiagnostics to get database health
const diagnostics = await this.service.runDiagnostics();
const dbHealth = diagnostics.details.database;
return {
healthy: dbHealth.status === "healthy",
message: dbHealth.message,
details: {
connection: dbHealth.connection,
tables: dbHealth.tables,
performance: dbHealth.performance,
},
};
}
}
async runDiagnostics(): Promise<{
tests: Array<{
name: string;
passed: boolean;
message: string;
duration: number;
}>;
summary: {
total: number;
passed: number;
failed: number;
duration: number;
};
}> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode);
}
const response = await this.httpClient.runDiagnostics();
const data = response.data;
if (!data) {
throw createAdapterInitError("Diagnostics data", this.mode, "No diagnostics data returned");
}
return {
tests: data.tests || [],
summary: data.summary || {
total: 0,
passed: 0,
failed: 0,
duration: 0,
},
};
} else {
if (!this.service) {
throw createAdapterInitError("Health service", this.mode);
}
const diagnostics = await this.service.runDiagnostics();
// Transform HealthDiagnostics to expected format
const tests: Array<{
name: string;
passed: boolean;
message: string;
duration: number;
}> = [];
// Database test
tests.push({
name: "Database",
passed: diagnostics.details.database.status === "healthy",
message: diagnostics.details.database.message,
duration: 0,
});
// System test
tests.push({
name: "System",
passed: diagnostics.details.system.status === "healthy",
message: `Memory: ${diagnostics.details.system.memory.percentage}%, CPU: ${diagnostics.details.system.cpu.usage}%`,
duration: 0,
});
// Service tests
diagnostics.details.services.forEach((service) => {
tests.push({
name: service.name,
passed: service.status === "healthy",
message: service.message,
duration: 0,
});
});
const passed = tests.filter((t) => t.passed).length;
const failed = tests.filter((t) => !t.passed).length;
return {
tests,
summary: {
total: tests.length,
passed,
failed,
duration: 0,
},
};
}
}
async validateSchema(): Promise<{
valid: boolean;
issues: Array<{
type: string;
table?: string;
field?: string;
message: string;
severity: "error" | "warning" | "info";
}>;
}> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode);
}
const response = await this.httpClient.validateSchema();
const data = response.data;
if (!data) {
throw createAdapterInitError("Schema validation data", this.mode, "No schema validation data returned");
}
return {
valid: data.valid,
issues: data.issues || [],
};
} else {
if (!this.databaseHealthManager) {
throw createAdapterInitError("Database health manager", this.mode);
}
const result = await this.databaseHealthManager.validateSchema();
// Transform the result to match the expected interface
const issues: Array<{
type: string;
table?: string;
field?: string;
message: string;
severity: "error" | "warning" | "info";
}> = [];
// Add missing tables as issues
for (const table of result.missing_tables || []) {
issues.push({
type: "missing_table",
table,
message: `Table '${table}' is missing`,
severity: "error",
});
}
// Add extra tables as issues
for (const table of result.extra_tables || []) {
issues.push({
type: "extra_table",
table,
message: `Table '${table}' exists but is not expected`,
severity: "warning",
});
}
// Add field mismatches as issues
for (const mismatch of result.field_mismatches || []) {
issues.push({
type: "field_mismatch",
table: mismatch.table,
field: mismatch.field,
message: `Field '${mismatch.field}' in table '${mismatch.table}': expected ${mismatch.expected_type}, got ${mismatch.actual_type}`,
severity: "error",
});
}
return {
valid: result.valid,
issues,
};
}
}
async autoFix(): Promise<{
success: boolean;
fixes_applied: number;
details: string[];
remaining_issues: number;
}> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode);
}
const response = await this.httpClient.autoFix();
const data = response.data;
if (!data) {
throw createAdapterInitError("Auto-fix data", this.mode, "No auto-fix data returned");
}
return {
success: data.success,
fixes_applied: data.fixed_issues?.length || 0,
details: data.fixed_issues || [],
remaining_issues: data.issues_remaining?.length || 0,
};
} else {
if (!this.databaseHealthManager) {
throw createAdapterInitError("Database health manager", this.mode);
}
const result = await this.databaseHealthManager.autoFix();
return {
success: result.success,
fixes_applied: result.fixesApplied || 0,
details: result.appliedFixes || [],
remaining_issues: 0,
};
}
}
async migrate(): Promise<{
success: boolean;
migrations_applied: number;
details: string[];
}> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode);
}
const response = await this.httpClient.runMigrations();
const data = response.data;
if (!data) {
throw createAdapterInitError("Migration data", this.mode, "No migration data returned");
}
return {
success: data.success,
migrations_applied: data.applied_migrations?.length || 0,
details: data.applied_migrations || [],
};
} else {
// Migrations are typically run through external tools (e.g., knex, prisma)
// In server mode, use autoFix which applies schema fixes
if (!this.databaseHealthManager) {
throw createAdapterInitError("Database health manager", this.mode);
}
const result = await this.databaseHealthManager.autoFix();
return {
success: result.success,
migrations_applied: result.fixesApplied || 0,
details: result.appliedFixes || [],
};
}
}
async getStats(): Promise<{
database: {
size_bytes: number;
tables_count: number;
connections: number;
uptime: number;
};
system: {
memory_usage: number;
cpu_usage: number;
disk_usage: number;
};
}> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode);
}
// Use getPerformanceMetrics or getSystemInfo to get stats
const [systemInfo, performanceMetrics] = await Promise.all([
this.httpClient.getSystemInfo().catch(() => null),
this.httpClient.getPerformanceMetrics().catch(() => null),
]);
const systemData = systemInfo?.data;
const perfData = performanceMetrics?.data;
return {
database: {
size_bytes: 0, // Not available in performance metrics
tables_count: 0, // Not available in performance metrics
connections: perfData?.database?.connection_count || 0,
uptime: systemData?.uptime || 0,
},
system: {
memory_usage: systemData?.memory?.percentage || 0,
cpu_usage: systemData?.cpu?.usage || 0,
disk_usage: systemData?.disk?.percentage || 0,
},
};
} else {
if (!this.service) {
throw createAdapterInitError("Health service", this.mode);
}
const diagnostics = await this.service.runDiagnostics();
return {
database: {
size_bytes: 0, // Not available in HealthService
tables_count: diagnostics.details.database.tables.length,
connections: diagnostics.details.database.performance.connectionPool.total,
uptime: diagnostics.details.system.uptime,
},
system: {
memory_usage: diagnostics.details.system.memory.percentage,
cpu_usage: diagnostics.details.system.cpu.usage,
disk_usage: diagnostics.details.system.disk.percentage,
},
};
}
}
async repairDatabase(): Promise<{
success: boolean;
actions: string[];
}> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode);
}
const response = await this.httpClient.repairDatabase();
const data = response.data;
if (!data) {
throw createAdapterInitError("Repair data", this.mode, "No repair data returned");
}
const actions: string[] = [];
if (data.repaired_tables) actions.push(...data.repaired_tables.map((t) => `Repaired table: ${t}`));
if (data.repaired_indexes) actions.push(...data.repaired_indexes.map((i) => `Repaired index: ${i}`));
if (data.repaired_constraints) actions.push(...data.repaired_constraints.map((c) => `Repaired constraint: ${c}`));
return {
success: data.success,
actions,
};
} else {
if (!this.databaseHealthManager) {
throw createAdapterInitError("Database health manager", this.mode);
}
// Use autoFix as the repair mechanism
const result = await this.databaseHealthManager.autoFix();
return {
success: result.success,
actions: result.appliedFixes || [],
};
}
}
}