UNPKG

@kanadi/core

Version:

Multi-Layer CAPTCHA Framework with customizable validators and challenge bundles

157 lines (135 loc) 4.01 kB
import { sql } from "bun"; import Redis from "ioredis"; import { createLogger } from "../utils/logger"; export class DatabaseService { private static instance: DatabaseService; private redis: Redis | null = null; private pgConnected = false; private readonly logger = createLogger(DatabaseService.name); private constructor() {} static getInstance(): DatabaseService { if (!DatabaseService.instance) { DatabaseService.instance = new DatabaseService(); } return DatabaseService.instance; } async connectPostgreSQL(url?: string): Promise<void> { try { const result = await sql`SELECT 1 as connected`; if (result && result.length > 0) { this.pgConnected = true; this.logger.log("PostgreSQL connected successfully"); } } catch (error) { this.logger.error("PostgreSQL connection failed", error); throw error; } } async connectRedis(config: { host: string; port: number; password?: string; db?: number; }): Promise<void> { try { this.redis = new Redis({ host: config.host, port: config.port, password: config.password, db: config.db || 0, retryStrategy: (times) => { const delay = Math.min(times * 50, 2000); return delay; }, }); this.redis.on("error", (err) => { this.logger.error("Redis error", err); }); this.redis.on("connect", () => { this.logger.log("Redis connected successfully"); }); await this.redis.ping(); } catch (error) { this.logger.error("Redis connection failed", error); throw error; } } async runMigrations(migrationDir?: string): Promise<void> { try { const defaultMigrationDir = new URL("../database/migrations", import.meta.url).pathname; const actualMigrationDir = migrationDir || defaultMigrationDir; const migrations = [ `${actualMigrationDir}/001_initial_schema.sql`, `${actualMigrationDir}/002_fix_bundle_id_type.sql`, `${actualMigrationDir}/003_add_fingerprint_columns.sql`, `${actualMigrationDir}/004_enhance_validator_sessions.sql`, `${actualMigrationDir}/005_improve_bundle_sessions.sql`, `${actualMigrationDir}/006_add_ban_system.sql`, ]; this.logger.log(`Running ${migrations.length} migrations...`); for (const migrationPath of migrations) { const migrationName = migrationPath.split("/").pop(); this.logger.log(`Processing: ${migrationName}`); try { const migrationFile = Bun.file(migrationPath); const migrationSQL = await migrationFile.text(); try { await sql.unsafe(migrationSQL); this.logger.log(`${migrationName}: Migration completed`); } catch (error: any) { const skipCodes = ["42P07", "42701", "42710", "42P16"]; if (skipCodes.includes(error.errno)) { this.logger.log(`${migrationName}: Already exists (skipped)`); } else { this.logger.error(`Migration failed: ${error.message} (Code: ${error.errno})`); throw error; } } } catch (error: any) { this.logger.error(`Migration failed: ${migrationName}`); throw error; } } this.logger.log("All migrations completed successfully"); } catch (error) { this.logger.error("Migration process failed", error); throw error; } } getRedis(): Redis { if (!this.redis) { throw new Error("Redis not connected"); } return this.redis; } async healthCheck(): Promise<{ postgresql: boolean; redis: boolean; }> { const health = { postgresql: false, redis: false, }; try { const result = await sql`SELECT 1 as health`; health.postgresql = result.length > 0; } catch (error) { this.logger.error("PostgreSQL health check failed", error); } try { if (this.redis) { await this.redis.ping(); health.redis = true; } } catch (error) { this.logger.error("Redis health check failed", error); } return health; } async close(): Promise<void> { if (this.redis) { await this.redis.quit(); } } } export const dbService = DatabaseService.getInstance();