플로우 외부db연결
This commit is contained in:
436
backend-node/src/services/flowExternalDbConnectionService.ts
Normal file
436
backend-node/src/services/flowExternalDbConnectionService.ts
Normal file
@@ -0,0 +1,436 @@
|
||||
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<number, Pool> = 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<FlowExternalDbConnection> {
|
||||
// 비밀번호 암호화
|
||||
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<FlowExternalDbConnection | null> {
|
||||
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<FlowExternalDbConnection[]> {
|
||||
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<FlowExternalDbConnection | null> {
|
||||
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<boolean> {
|
||||
// 연결 풀 정리
|
||||
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<Pool> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user