import db from "../database/db"; import { FlowExternalDbConnection, CreateFlowExternalDbConnectionRequest, UpdateFlowExternalDbConnectionRequest, } from "../types/flow"; import { CredentialEncryption } from "../utils/credentialEncryption"; import { Pool } from "pg"; // import mysql from 'mysql2/promise'; // MySQL용 (추후) // import { ConnectionPool } from 'mssql'; // MSSQL용 (추후) /** * 플로우 전용 외부 DB 연결 관리 서비스 * (기존 제어관리 외부 DB 연결과 별도) */ export class FlowExternalDbConnectionService { private encryption: CredentialEncryption; private connectionPools: Map = new Map(); constructor() { // 환경 변수에서 SECRET_KEY를 가져오거나 기본값 설정 const secretKey = process.env.SECRET_KEY || "flow-external-db-secret-key-2025"; this.encryption = new CredentialEncryption(secretKey); } /** * 외부 DB 연결 생성 */ async create( request: CreateFlowExternalDbConnectionRequest, userId: string = "system" ): Promise { // 비밀번호 암호화 const encryptedPassword = this.encryption.encrypt(request.password); const query = ` INSERT INTO flow_external_db_connection ( name, description, db_type, host, port, database_name, username, password_encrypted, ssl_enabled, connection_options, created_by, updated_by ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING * `; const result = await db.query(query, [ request.name, request.description || null, request.dbType, request.host, request.port, request.databaseName, request.username, encryptedPassword, request.sslEnabled || false, request.connectionOptions ? JSON.stringify(request.connectionOptions) : null, userId, userId, ]); return this.mapToFlowExternalDbConnection(result[0]); } /** * ID로 외부 DB 연결 조회 */ async findById(id: number): Promise { const query = "SELECT * FROM flow_external_db_connection WHERE id = $1"; const result = await db.query(query, [id]); if (result.length === 0) { return null; } return this.mapToFlowExternalDbConnection(result[0]); } /** * 모든 외부 DB 연결 조회 */ async findAll( activeOnly: boolean = false ): Promise { let query = "SELECT * FROM flow_external_db_connection"; if (activeOnly) { query += " WHERE is_active = true"; } query += " ORDER BY name ASC"; const result = await db.query(query); return result.map((row) => this.mapToFlowExternalDbConnection(row)); } /** * 외부 DB 연결 수정 */ async update( id: number, request: UpdateFlowExternalDbConnectionRequest, userId: string = "system" ): Promise { const fields: string[] = []; const params: any[] = []; let paramIndex = 1; if (request.name !== undefined) { fields.push(`name = $${paramIndex}`); params.push(request.name); paramIndex++; } if (request.description !== undefined) { fields.push(`description = $${paramIndex}`); params.push(request.description); paramIndex++; } if (request.host !== undefined) { fields.push(`host = $${paramIndex}`); params.push(request.host); paramIndex++; } if (request.port !== undefined) { fields.push(`port = $${paramIndex}`); params.push(request.port); paramIndex++; } if (request.databaseName !== undefined) { fields.push(`database_name = $${paramIndex}`); params.push(request.databaseName); paramIndex++; } if (request.username !== undefined) { fields.push(`username = $${paramIndex}`); params.push(request.username); paramIndex++; } if (request.password !== undefined) { const encryptedPassword = this.encryption.encrypt(request.password); fields.push(`password_encrypted = $${paramIndex}`); params.push(encryptedPassword); paramIndex++; } if (request.sslEnabled !== undefined) { fields.push(`ssl_enabled = $${paramIndex}`); params.push(request.sslEnabled); paramIndex++; } if (request.connectionOptions !== undefined) { fields.push(`connection_options = $${paramIndex}`); params.push( request.connectionOptions ? JSON.stringify(request.connectionOptions) : null ); paramIndex++; } if (request.isActive !== undefined) { fields.push(`is_active = $${paramIndex}`); params.push(request.isActive); paramIndex++; } if (fields.length === 0) { return this.findById(id); } fields.push(`updated_by = $${paramIndex}`); params.push(userId); paramIndex++; fields.push(`updated_at = NOW()`); const query = ` UPDATE flow_external_db_connection SET ${fields.join(", ")} WHERE id = $${paramIndex} RETURNING * `; params.push(id); const result = await db.query(query, params); if (result.length === 0) { return null; } // 연결 풀 갱신 (비밀번호 변경 시) if (request.password !== undefined || request.host !== undefined) { this.closeConnection(id); } return this.mapToFlowExternalDbConnection(result[0]); } /** * 외부 DB 연결 삭제 */ async delete(id: number): Promise { // 연결 풀 정리 this.closeConnection(id); const query = "DELETE FROM flow_external_db_connection WHERE id = $1 RETURNING id"; const result = await db.query(query, [id]); return result.length > 0; } /** * 연결 테스트 */ async testConnection( id: number ): Promise<{ success: boolean; message: string }> { try { const connection = await this.findById(id); if (!connection) { return { success: false, message: "연결 정보를 찾을 수 없습니다." }; } const pool = await this.getConnectionPool(connection); // 간단한 쿼리로 연결 테스트 const client = await pool.connect(); try { await client.query("SELECT 1"); return { success: true, message: "연결 성공" }; } finally { client.release(); } } catch (error: any) { return { success: false, message: error.message }; } } /** * 외부 DB의 테이블 목록 조회 */ async getTables( id: number ): Promise<{ success: boolean; data?: string[]; message?: string }> { try { const connection = await this.findById(id); if (!connection) { return { success: false, message: "연결 정보를 찾을 수 없습니다." }; } const pool = await this.getConnectionPool(connection); const client = await pool.connect(); try { let query: string; switch (connection.dbType) { case "postgresql": query = "SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename"; break; case "mysql": query = `SELECT table_name as tablename FROM information_schema.tables WHERE table_schema = '${connection.databaseName}' ORDER BY table_name`; break; default: return { success: false, message: `지원하지 않는 DB 타입: ${connection.dbType}`, }; } const result = await client.query(query); const tables = result.rows.map((row: any) => row.tablename); return { success: true, data: tables }; } finally { client.release(); } } catch (error: any) { return { success: false, message: error.message }; } } /** * 외부 DB의 특정 테이블 컬럼 목록 조회 */ async getTableColumns( id: number, tableName: string ): Promise<{ success: boolean; data?: { column_name: string; data_type: string }[]; message?: string; }> { try { const connection = await this.findById(id); if (!connection) { return { success: false, message: "연결 정보를 찾을 수 없습니다." }; } const pool = await this.getConnectionPool(connection); const client = await pool.connect(); try { let query: string; switch (connection.dbType) { case "postgresql": query = `SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1 ORDER BY ordinal_position`; break; case "mysql": query = `SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = '${connection.databaseName}' AND table_name = ? ORDER BY ordinal_position`; break; default: return { success: false, message: `지원하지 않는 DB 타입: ${connection.dbType}`, }; } const result = await client.query(query, [tableName]); return { success: true, data: result.rows }; } finally { client.release(); } } catch (error: any) { return { success: false, message: error.message }; } } /** * 연결 풀 가져오기 (캐싱) */ async getConnectionPool(connection: FlowExternalDbConnection): Promise { if (this.connectionPools.has(connection.id)) { return this.connectionPools.get(connection.id)!; } // 비밀번호 복호화 const decryptedPassword = this.encryption.decrypt( connection.passwordEncrypted ); let pool: Pool; switch (connection.dbType) { case "postgresql": pool = new Pool({ host: connection.host, port: connection.port, database: connection.databaseName, user: connection.username, password: decryptedPassword, ssl: connection.sslEnabled, // 연결 풀 설정 (고갈 방지) max: 10, // 최대 연결 수 min: 2, // 최소 연결 수 idleTimeoutMillis: 30000, // 30초 유휴 시간 후 연결 해제 connectionTimeoutMillis: 10000, // 10초 연결 타임아웃 ...(connection.connectionOptions || {}), }); // 에러 핸들러 등록 pool.on("error", (err) => { console.error(`외부 DB 연결 풀 오류 (ID: ${connection.id}):`, err); }); break; // case "mysql": // pool = mysql.createPool({ ... }); // break; // case "mssql": // pool = new ConnectionPool({ ... }); // break; default: throw new Error(`지원하지 않는 DB 타입: ${connection.dbType}`); } this.connectionPools.set(connection.id, pool); return pool; } /** * 연결 풀 정리 */ closeConnection(id: number): void { const pool = this.connectionPools.get(id); if (pool) { pool.end(); this.connectionPools.delete(id); } } /** * 모든 연결 풀 정리 */ closeAllConnections(): void { for (const [id, pool] of this.connectionPools.entries()) { pool.end(); } this.connectionPools.clear(); } /** * DB row를 FlowExternalDbConnection으로 매핑 */ private mapToFlowExternalDbConnection(row: any): FlowExternalDbConnection { return { id: row.id, name: row.name, description: row.description || undefined, dbType: row.db_type, host: row.host, port: row.port, databaseName: row.database_name, username: row.username, passwordEncrypted: row.password_encrypted, sslEnabled: row.ssl_enabled, connectionOptions: row.connection_options || undefined, isActive: row.is_active, createdBy: row.created_by || undefined, updatedBy: row.updated_by || undefined, createdAt: row.created_at, updatedAt: row.updated_at, }; } }