rest api 액션노드 기능변경

This commit is contained in:
kjs
2025-10-13 12:00:41 +09:00
parent 68308efd22
commit 1274f58c3c
10 changed files with 617 additions and 174 deletions

View File

@@ -10,6 +10,7 @@
import { query, queryOne, transaction } from "../database/db";
import { logger } from "../utils/logger";
import axios from "axios";
// ===== 타입 정의 =====
@@ -410,6 +411,9 @@ export class NodeFlowExecutionService {
case "tableSource":
return this.executeTableSource(node, context);
case "restAPISource":
return this.executeRestAPISource(node, context);
case "dataTransform":
return this.executeDataTransform(node, inputData, context);
@@ -440,6 +444,123 @@ export class NodeFlowExecutionService {
}
}
/**
* REST API 소스 노드 실행
*/
private static async executeRestAPISource(
node: FlowNode,
context: ExecutionContext
): Promise<any[]> {
const {
url,
method = "GET",
headers = {},
body,
timeout = 30000,
responseMapping,
authentication,
} = node.data;
if (!url) {
throw new Error("REST API URL이 설정되지 않았습니다.");
}
logger.info(`🌐 REST API 호출: ${method} ${url}`);
try {
// 헤더 설정
const requestHeaders: any = { ...headers };
// 인증 헤더 추가
if (authentication) {
if (authentication.type === "bearer" && authentication.token) {
requestHeaders["Authorization"] = `Bearer ${authentication.token}`;
} else if (
authentication.type === "basic" &&
authentication.username &&
authentication.password
) {
const credentials = Buffer.from(
`${authentication.username}:${authentication.password}`
).toString("base64");
requestHeaders["Authorization"] = `Basic ${credentials}`;
} else if (authentication.type === "apikey" && authentication.token) {
const headerName = authentication.apiKeyHeader || "X-API-Key";
requestHeaders[headerName] = authentication.token;
}
}
if (!requestHeaders["Content-Type"]) {
requestHeaders["Content-Type"] = "application/json";
}
// API 호출
const response = await axios({
method: method.toLowerCase(),
url,
headers: requestHeaders,
data: body,
timeout,
});
logger.info(`✅ REST API 응답 수신: ${response.status}`);
let responseData = response.data;
// 🔥 표준 API 응답 형식 자동 감지 { success, message, data }
if (
!responseMapping &&
responseData &&
typeof responseData === "object" &&
"success" in responseData &&
"data" in responseData
) {
logger.info("🔍 표준 API 응답 형식 감지, data 속성 자동 추출");
responseData = responseData.data;
}
// responseMapping이 있으면 해당 경로의 데이터 추출
if (responseMapping && responseData) {
logger.info(`🔍 응답 매핑 적용: ${responseMapping}`);
const path = responseMapping.split(".");
for (const key of path) {
if (
responseData &&
typeof responseData === "object" &&
key in responseData
) {
responseData = responseData[key];
} else {
logger.warn(
`⚠️ 응답 매핑 경로를 찾을 수 없습니다: ${responseMapping}`
);
break;
}
}
}
// 배열이 아니면 배열로 변환
if (!Array.isArray(responseData)) {
logger.info("🔄 단일 객체를 배열로 변환");
responseData = [responseData];
}
logger.info(`📦 REST API 데이터 ${responseData.length}건 반환`);
// 첫 번째 데이터 샘플 상세 로깅
if (responseData.length > 0) {
console.log("🔍 REST API 응답 데이터 샘플 (첫 번째 항목):");
console.log(JSON.stringify(responseData[0], null, 2));
console.log("🔑 사용 가능한 필드명:", Object.keys(responseData[0]));
}
return responseData;
} catch (error: any) {
logger.error(`❌ REST API 호출 실패:`, error.message);
throw new Error(`REST API 호출 실패: ${error.message}`);
}
}
/**
* 테이블 소스 노드 실행
*/
@@ -521,6 +642,19 @@ export class NodeFlowExecutionService {
): Promise<any> {
const { targetTable, fieldMappings } = node.data;
logger.info(`💾 INSERT 노드 실행: ${targetTable}`);
console.log(
"📥 입력 데이터 타입:",
typeof inputData,
Array.isArray(inputData) ? `배열(${inputData.length}건)` : "단일 객체"
);
if (inputData && inputData.length > 0) {
console.log("📄 첫 번째 입력 데이터:");
console.log(JSON.stringify(inputData[0], null, 2));
console.log("🔑 입력 데이터 필드명:", Object.keys(inputData[0]));
}
return transaction(async (client) => {
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
let insertedCount = 0;
@@ -529,12 +663,17 @@ export class NodeFlowExecutionService {
const fields: string[] = [];
const values: any[] = [];
console.log("🗺️ 필드 매핑 처리 중...");
fieldMappings.forEach((mapping: any) => {
fields.push(mapping.targetField);
const value =
mapping.staticValue !== undefined
? mapping.staticValue
: data[mapping.sourceField];
console.log(
` ${mapping.sourceField}${mapping.targetField}: ${value === undefined ? "❌ undefined" : "✅ " + value}`
);
values.push(value);
});
@@ -543,6 +682,9 @@ export class NodeFlowExecutionService {
VALUES (${fields.map((_, i) => `$${i + 1}`).join(", ")})
`;
console.log("📝 실행할 SQL:", sql);
console.log("📊 바인딩 값:", values);
await client.query(sql, values);
insertedCount++;
}
@@ -682,7 +824,6 @@ export class NodeFlowExecutionService {
logger.info(`🌐 REST API INSERT 시작: ${apiMethod} ${apiEndpoint}`);
const axios = require("axios");
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
const results: any[] = [];
@@ -895,6 +1036,19 @@ export class NodeFlowExecutionService {
): Promise<any> {
const { targetTable, fieldMappings, whereConditions } = node.data;
logger.info(`🔄 UPDATE 노드 실행: ${targetTable}`);
console.log(
"📥 입력 데이터 타입:",
typeof inputData,
Array.isArray(inputData) ? `배열(${inputData.length}건)` : "단일 객체"
);
if (inputData && inputData.length > 0) {
console.log("📄 첫 번째 입력 데이터:");
console.log(JSON.stringify(inputData[0], null, 2));
console.log("🔑 입력 데이터 필드명:", Object.keys(inputData[0]));
}
return transaction(async (client) => {
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
let updatedCount = 0;
@@ -904,11 +1058,16 @@ export class NodeFlowExecutionService {
const values: any[] = [];
let paramIndex = 1;
console.log("🗺️ 필드 매핑 처리 중...");
fieldMappings.forEach((mapping: any) => {
const value =
mapping.staticValue !== undefined
? mapping.staticValue
: data[mapping.sourceField];
console.log(
` ${mapping.sourceField}${mapping.targetField}: ${value === undefined ? "❌ undefined" : "✅ " + value}`
);
setClauses.push(`${mapping.targetField} = $${paramIndex}`);
values.push(value);
paramIndex++;
@@ -926,6 +1085,9 @@ export class NodeFlowExecutionService {
${whereClause}
`;
console.log("📝 실행할 SQL:", sql);
console.log("📊 바인딩 값:", values);
const result = await client.query(sql, values);
updatedCount += result.rowCount || 0;
}
@@ -1086,7 +1248,6 @@ export class NodeFlowExecutionService {
logger.info(`🌐 REST API UPDATE 시작: ${apiMethod} ${apiEndpoint}`);
const axios = require("axios");
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
const results: any[] = [];
@@ -1197,15 +1358,31 @@ export class NodeFlowExecutionService {
): Promise<any> {
const { targetTable, whereConditions } = node.data;
logger.info(`🗑️ DELETE 노드 실행: ${targetTable}`);
console.log(
"📥 입력 데이터 타입:",
typeof inputData,
Array.isArray(inputData) ? `배열(${inputData.length}건)` : "단일 객체"
);
if (inputData && inputData.length > 0) {
console.log("📄 첫 번째 입력 데이터:");
console.log(JSON.stringify(inputData[0], null, 2));
console.log("🔑 입력 데이터 필드명:", Object.keys(inputData[0]));
}
return transaction(async (client) => {
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
let deletedCount = 0;
for (const data of dataArray) {
console.log("🔍 WHERE 조건 처리 중...");
const whereClause = this.buildWhereClause(whereConditions, data, 1);
const sql = `DELETE FROM ${targetTable} ${whereClause}`;
console.log("📝 실행할 SQL:", sql);
const result = await client.query(sql, []);
deletedCount += result.rowCount || 0;
}
@@ -1339,7 +1516,6 @@ export class NodeFlowExecutionService {
logger.info(`🌐 REST API DELETE 시작: ${apiEndpoint}`);
const axios = require("axios");
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
const results: any[] = [];
@@ -1440,6 +1616,20 @@ export class NodeFlowExecutionService {
throw new Error("UPSERT 액션에 충돌 키(Conflict Keys)가 필요합니다.");
}
logger.info(`🔀 UPSERT 노드 실행: ${targetTable}`);
console.log(
"📥 입력 데이터 타입:",
typeof inputData,
Array.isArray(inputData) ? `배열(${inputData.length}건)` : "단일 객체"
);
if (inputData && inputData.length > 0) {
console.log("📄 첫 번째 입력 데이터:");
console.log(JSON.stringify(inputData[0], null, 2));
console.log("🔑 입력 데이터 필드명:", Object.keys(inputData[0]));
}
console.log("🔑 충돌 키:", conflictKeys);
return transaction(async (client) => {
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
let insertedCount = 0;
@@ -1466,7 +1656,10 @@ export class NodeFlowExecutionService {
(key: string) => conflictKeyValues[key]
);
const checkSql = `SELECT id FROM ${targetTable} WHERE ${whereConditions} LIMIT 1`;
console.log("🔍 존재 여부 확인 - WHERE 조건:", whereConditions);
console.log("🔍 존재 여부 확인 - 바인딩 값:", whereValues);
const checkSql = `SELECT 1 FROM ${targetTable} WHERE ${whereConditions} LIMIT 1`;
const existingRow = await client.query(checkSql, whereValues);
if (existingRow.rows.length > 0) {
@@ -1780,7 +1973,6 @@ export class NodeFlowExecutionService {
logger.info(`🌐 REST API UPSERT 시작: ${apiMethod} ${apiEndpoint}`);
const axios = require("axios");
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
const results: any[] = [];
@@ -1977,6 +2169,20 @@ export class NodeFlowExecutionService {
const success = summary.failed === 0;
// 실패한 노드 상세 로깅
if (!success) {
const failedNodes = nodeSummaries.filter((n) => n.status === "failed");
logger.error(
`❌ 실패한 노드들:`,
failedNodes.map((n) => ({
nodeId: n.nodeId,
nodeName: n.nodeName,
nodeType: n.nodeType,
error: n.error,
}))
);
}
return {
success,
message: success