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

354 lines
9.9 KiB
TypeScript

import {
FlowExternalDbIntegrationConfig,
FlowIntegrationContext,
FlowIntegrationResult,
} from "../types/flow";
import { FlowExternalDbConnectionService } from "./flowExternalDbConnectionService";
import { Pool } from "pg";
/**
* 플로우 외부 DB 연동 실행 서비스
* 외부 데이터베이스에 대한 작업(INSERT, UPDATE, DELETE, CUSTOM QUERY) 수행
*/
export class FlowExternalDbIntegrationService {
private connectionService: FlowExternalDbConnectionService;
constructor() {
this.connectionService = new FlowExternalDbConnectionService();
}
/**
* 외부 DB 연동 실행
*/
async execute(
context: FlowIntegrationContext,
config: FlowExternalDbIntegrationConfig
): Promise<FlowIntegrationResult> {
const startTime = Date.now();
try {
// 1. 연결 정보 조회
const connection = await this.connectionService.findById(
config.connectionId
);
if (!connection) {
return {
success: false,
error: {
code: "CONNECTION_NOT_FOUND",
message: `외부 DB 연결 정보를 찾을 수 없습니다 (ID: ${config.connectionId})`,
},
};
}
if (!connection.isActive) {
return {
success: false,
error: {
code: "CONNECTION_INACTIVE",
message: `외부 DB 연결이 비활성화 상태입니다 (${connection.name})`,
},
};
}
// 2. 쿼리 생성 (템플릿 변수 치환)
const query = this.buildQuery(config, context);
// 3. 실행
const pool = await this.connectionService.getConnectionPool(connection);
const result = await this.executeQuery(pool, query);
const executionTime = Date.now() - startTime;
return {
success: true,
message: `외부 DB 작업 성공 (${config.operation}, ${executionTime}ms)`,
data: result,
rollbackInfo: {
query: this.buildRollbackQuery(config, context, result),
connectionId: config.connectionId,
},
};
} catch (error: any) {
const executionTime = Date.now() - startTime;
return {
success: false,
error: {
code: "EXTERNAL_DB_ERROR",
message: error.message || "외부 DB 작업 실패",
details: {
operation: config.operation,
tableName: config.tableName,
executionTime,
originalError: error,
},
},
};
}
}
/**
* 쿼리 실행
*/
private async executeQuery(
pool: Pool,
query: { sql: string; params: any[] }
): Promise<any> {
const client = await pool.connect();
try {
const result = await client.query(query.sql, query.params);
return result.rows;
} finally {
client.release();
}
}
/**
* 쿼리 빌드 (템플릿 변수 치환 포함)
*/
private buildQuery(
config: FlowExternalDbIntegrationConfig,
context: FlowIntegrationContext
): { sql: string; params: any[] } {
let sql = "";
const params: any[] = [];
let paramIndex = 1;
switch (config.operation) {
case "update":
return this.buildUpdateQuery(config, context, paramIndex);
case "insert":
return this.buildInsertQuery(config, context, paramIndex);
case "delete":
return this.buildDeleteQuery(config, context, paramIndex);
case "custom":
return this.buildCustomQuery(config, context);
default:
throw new Error(`지원하지 않는 작업: ${config.operation}`);
}
}
/**
* UPDATE 쿼리 빌드
*/
private buildUpdateQuery(
config: FlowExternalDbIntegrationConfig,
context: FlowIntegrationContext,
startIndex: number
): { sql: string; params: any[] } {
if (!config.updateFields || Object.keys(config.updateFields).length === 0) {
throw new Error("UPDATE 작업에는 updateFields가 필요합니다");
}
if (
!config.whereCondition ||
Object.keys(config.whereCondition).length === 0
) {
throw new Error("UPDATE 작업에는 whereCondition이 필요합니다");
}
const setClauses: string[] = [];
const params: any[] = [];
let paramIndex = startIndex;
// SET 절 생성
for (const [key, value] of Object.entries(config.updateFields)) {
setClauses.push(`${key} = $${paramIndex}`);
params.push(this.replaceVariables(value, context));
paramIndex++;
}
// WHERE 절 생성
const whereClauses: string[] = [];
for (const [key, value] of Object.entries(config.whereCondition)) {
whereClauses.push(`${key} = $${paramIndex}`);
params.push(this.replaceVariables(value, context));
paramIndex++;
}
const sql = `UPDATE ${config.tableName} SET ${setClauses.join(", ")} WHERE ${whereClauses.join(" AND ")}`;
return { sql, params };
}
/**
* INSERT 쿼리 빌드
*/
private buildInsertQuery(
config: FlowExternalDbIntegrationConfig,
context: FlowIntegrationContext,
startIndex: number
): { sql: string; params: any[] } {
if (!config.updateFields || Object.keys(config.updateFields).length === 0) {
throw new Error("INSERT 작업에는 updateFields가 필요합니다");
}
const columns: string[] = [];
const placeholders: string[] = [];
const params: any[] = [];
let paramIndex = startIndex;
for (const [key, value] of Object.entries(config.updateFields)) {
columns.push(key);
placeholders.push(`$${paramIndex}`);
params.push(this.replaceVariables(value, context));
paramIndex++;
}
const sql = `INSERT INTO ${config.tableName} (${columns.join(", ")}) VALUES (${placeholders.join(", ")}) RETURNING *`;
return { sql, params };
}
/**
* DELETE 쿼리 빌드
*/
private buildDeleteQuery(
config: FlowExternalDbIntegrationConfig,
context: FlowIntegrationContext,
startIndex: number
): { sql: string; params: any[] } {
if (
!config.whereCondition ||
Object.keys(config.whereCondition).length === 0
) {
throw new Error("DELETE 작업에는 whereCondition이 필요합니다");
}
const whereClauses: string[] = [];
const params: any[] = [];
let paramIndex = startIndex;
for (const [key, value] of Object.entries(config.whereCondition)) {
whereClauses.push(`${key} = $${paramIndex}`);
params.push(this.replaceVariables(value, context));
paramIndex++;
}
const sql = `DELETE FROM ${config.tableName} WHERE ${whereClauses.join(" AND ")}`;
return { sql, params };
}
/**
* CUSTOM 쿼리 빌드
*/
private buildCustomQuery(
config: FlowExternalDbIntegrationConfig,
context: FlowIntegrationContext
): { sql: string; params: any[] } {
if (!config.customQuery) {
throw new Error("CUSTOM 작업에는 customQuery가 필요합니다");
}
// 템플릿 변수 치환
const sql = this.replaceVariables(config.customQuery, context);
// 커스텀 쿼리는 파라미터를 직접 관리
// 보안을 위해 가능하면 파라미터 바인딩 사용 권장
return { sql, params: [] };
}
/**
* 템플릿 변수 치환
*/
private replaceVariables(value: any, context: FlowIntegrationContext): any {
if (typeof value !== "string") {
return value;
}
let result = value;
// {{dataId}} 치환
result = result.replace(/\{\{dataId\}\}/g, String(context.dataId));
// {{currentUser}} 치환
result = result.replace(/\{\{currentUser\}\}/g, context.currentUser);
// {{currentTimestamp}} 치환
result = result.replace(
/\{\{currentTimestamp\}\}/g,
new Date().toISOString()
);
// {{flowId}} 치환
result = result.replace(/\{\{flowId\}\}/g, String(context.flowId));
// {{stepId}} 치환
result = result.replace(/\{\{stepId\}\}/g, String(context.stepId));
// {{tableName}} 치환
if (context.tableName) {
result = result.replace(/\{\{tableName\}\}/g, context.tableName);
}
// context.variables의 커스텀 변수 치환
for (const [key, val] of Object.entries(context.variables)) {
const regex = new RegExp(`\\{\\{${key}\\}\\}`, "g");
result = result.replace(regex, String(val));
}
// NOW() 같은 SQL 함수는 그대로 반환
if (result === "NOW()" || result.startsWith("CURRENT_")) {
return result;
}
return result;
}
/**
* 롤백 쿼리 생성
*/
private buildRollbackQuery(
config: FlowExternalDbIntegrationConfig,
context: FlowIntegrationContext,
result: any
): { sql: string; params: any[] } | null {
// 롤백 쿼리 생성 로직 (복잡하므로 실제 구현 시 상세 설계 필요)
// 예: INSERT -> DELETE, UPDATE -> 이전 값으로 UPDATE
switch (config.operation) {
case "insert":
// INSERT를 롤백하려면 삽입된 레코드를 DELETE
if (result && result[0] && result[0].id) {
return {
sql: `DELETE FROM ${config.tableName} WHERE id = $1`,
params: [result[0].id],
};
}
break;
case "delete":
// DELETE 롤백은 매우 어려움 (원본 데이터 필요)
console.warn("DELETE 작업의 롤백은 지원하지 않습니다");
break;
case "update":
// UPDATE 롤백을 위해서는 이전 값을 저장해야 함
console.warn("UPDATE 작업의 롤백은 현재 구현되지 않았습니다");
break;
default:
break;
}
return null;
}
/**
* 롤백 실행
*/
async rollback(
connectionId: number,
rollbackQuery: { sql: string; params: any[] }
): Promise<void> {
const connection = await this.connectionService.findById(connectionId);
if (!connection) {
throw new Error(
`롤백 실패: 연결 정보를 찾을 수 없습니다 (ID: ${connectionId})`
);
}
const pool = await this.connectionService.getConnectionPool(connection);
await this.executeQuery(pool, rollbackQuery);
}
}