ERP-node/backend-node/src/services/flowExternalDbConnectionSer...

437 lines
12 KiB
TypeScript

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,
};
}
}