@kanadi/core
Version:
Multi-Layer CAPTCHA Framework with customizable validators and challenge bundles
157 lines (135 loc) • 4.01 kB
text/typescript
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();