ERP-node/backend-node/src/services/eventTriggerService.ts

628 lines
17 KiB
TypeScript
Raw Normal View History

2025-09-12 10:05:25 +09:00
import { PrismaClient } from "@prisma/client";
2025-09-12 11:33:54 +09:00
import { logger } from "../utils/logger";
2025-09-12 10:05:25 +09:00
const prisma = new PrismaClient();
// 조건 노드 타입 정의
interface ConditionNode {
type: "group" | "condition";
operator?: "AND" | "OR";
children?: ConditionNode[];
field?: string;
2025-09-12 11:33:54 +09:00
operator_type?: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE";
2025-09-12 10:05:25 +09:00
value?: any;
dataType?: string;
}
// 조건 제어 정보
interface ConditionControl {
triggerType: "insert" | "update" | "delete" | "insert_update";
conditionTree: ConditionNode | null;
}
// 연결 카테고리 정보
interface ConnectionCategory {
type: "simple-key" | "data-save" | "external-call" | "conditional-link";
rollbackOnError?: boolean;
enableLogging?: boolean;
maxRetryCount?: number;
}
// 대상 액션
interface TargetAction {
id: string;
actionType: "insert" | "update" | "delete" | "upsert";
targetTable: string;
enabled: boolean;
fieldMappings: FieldMapping[];
2025-09-15 10:11:22 +09:00
conditions?: Array<{
field: string;
operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE";
value: string;
logicalOperator?: "AND" | "OR";
}>;
splitConfig?: {
sourceField: string;
delimiter: string;
targetField: string;
};
2025-09-12 10:05:25 +09:00
description?: string;
}
// 필드 매핑
interface FieldMapping {
sourceField: string;
targetField: string;
transformFunction?: string;
defaultValue?: string;
}
// 실행 계획
interface ExecutionPlan {
sourceTable: string;
targetActions: TargetAction[];
}
// 실행 결과
interface ExecutionResult {
success: boolean;
executedActions: number;
failedActions: number;
errors: string[];
executionTime: number;
}
/**
*
*/
export class EventTriggerService {
/**
*
*/
static async executeEventTriggers(
triggerType: "insert" | "update" | "delete",
tableName: string,
data: Record<string, any>,
companyCode: string
): Promise<ExecutionResult[]> {
const startTime = Date.now();
const results: ExecutionResult[] = [];
try {
// 해당 테이블과 트리거 타입에 맞는 조건부 연결들 조회
const diagrams = await prisma.dataflow_diagrams.findMany({
where: {
company_code: companyCode,
control: {
path: ["triggerType"],
equals:
triggerType === "insert"
? "insert"
: triggerType === "update"
? ["update", "insert_update"]
: triggerType === "delete"
? "delete"
: triggerType,
},
plan: {
path: ["sourceTable"],
equals: tableName,
},
},
});
logger.info(
`Found ${diagrams.length} conditional connections for table ${tableName} with trigger ${triggerType}`
);
// 각 다이어그램에 대해 조건부 연결 실행
for (const diagram of diagrams) {
try {
const result = await this.executeDiagramTrigger(
diagram,
data,
companyCode
);
results.push(result);
} catch (error) {
logger.error(`Error executing diagram ${diagram.diagram_id}:`, error);
results.push({
success: false,
executedActions: 0,
failedActions: 1,
errors: [error instanceof Error ? error.message : "Unknown error"],
executionTime: Date.now() - startTime,
});
}
}
return results;
} catch (error) {
logger.error("Error in executeEventTriggers:", error);
throw error;
}
}
/**
*
*/
private static async executeDiagramTrigger(
diagram: any,
data: Record<string, any>,
companyCode: string
): Promise<ExecutionResult> {
const startTime = Date.now();
let executedActions = 0;
let failedActions = 0;
const errors: string[] = [];
try {
2025-09-12 11:33:54 +09:00
const control = diagram.control as unknown as ConditionControl;
const category = diagram.category as unknown as ConnectionCategory;
const plan = diagram.plan as unknown as ExecutionPlan;
2025-09-12 10:05:25 +09:00
logger.info(
`Executing diagram ${diagram.diagram_id} (${diagram.diagram_name})`
);
// 조건 평가
if (control.conditionTree) {
const conditionMet = await this.evaluateCondition(
control.conditionTree,
data
);
if (!conditionMet) {
logger.info(
`Conditions not met for diagram ${diagram.diagram_id}, skipping execution`
);
return {
success: true,
executedActions: 0,
failedActions: 0,
errors: [],
executionTime: Date.now() - startTime,
};
}
}
// 대상 액션들 실행
for (const action of plan.targetActions) {
if (!action.enabled) {
continue;
}
try {
await this.executeTargetAction(action, data, companyCode);
executedActions++;
if (category.enableLogging) {
logger.info(
`Successfully executed action ${action.id} on table ${action.targetTable}`
);
}
} catch (error) {
failedActions++;
const errorMsg =
error instanceof Error ? error.message : "Unknown error";
errors.push(`Action ${action.id}: ${errorMsg}`);
logger.error(`Failed to execute action ${action.id}:`, error);
// 오류 시 롤백 처리
if (category.rollbackOnError) {
logger.warn(`Rolling back due to error in action ${action.id}`);
// TODO: 롤백 로직 구현
break;
}
}
}
return {
success: failedActions === 0,
executedActions,
failedActions,
errors,
executionTime: Date.now() - startTime,
};
} catch (error) {
logger.error(`Error executing diagram ${diagram.diagram_id}:`, error);
return {
success: false,
executedActions: 0,
failedActions: 1,
errors: [error instanceof Error ? error.message : "Unknown error"],
executionTime: Date.now() - startTime,
};
}
}
/**
*
*/
private static async evaluateCondition(
condition: ConditionNode,
data: Record<string, any>
): Promise<boolean> {
if (condition.type === "group") {
if (!condition.children || condition.children.length === 0) {
return true;
}
const results = await Promise.all(
condition.children.map((child) => this.evaluateCondition(child, data))
);
if (condition.operator === "OR") {
return results.some((result) => result);
} else {
// AND
return results.every((result) => result);
}
} else if (condition.type === "condition") {
return this.evaluateSingleCondition(condition, data);
}
return false;
}
2025-09-15 10:11:22 +09:00
/**
* (AND/OR )
*/
private static async evaluateActionConditions(
conditions: Array<{
field: string;
operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE";
value: string;
logicalOperator?: "AND" | "OR";
}>,
data: Record<string, any>
): Promise<boolean> {
if (conditions.length === 0) {
return true;
}
let result = await this.evaluateActionCondition(conditions[0], data);
for (let i = 1; i < conditions.length; i++) {
const prevCondition = conditions[i - 1];
const currentCondition = conditions[i];
const currentResult = await this.evaluateActionCondition(
currentCondition,
data
);
if (prevCondition.logicalOperator === "OR") {
result = result || currentResult;
} else {
// 기본값은 AND
result = result && currentResult;
}
}
return result;
}
/**
*
*/
private static async evaluateActionCondition(
condition: {
field: string;
operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE";
value: string;
},
data: Record<string, any>
): Promise<boolean> {
const fieldValue = data[condition.field];
const conditionValue = condition.value;
switch (condition.operator) {
case "=":
return fieldValue == conditionValue;
case "!=":
return fieldValue != conditionValue;
case ">":
return Number(fieldValue) > Number(conditionValue);
case "<":
return Number(fieldValue) < Number(conditionValue);
case ">=":
return Number(fieldValue) >= Number(conditionValue);
case "<=":
return Number(fieldValue) <= Number(conditionValue);
case "LIKE":
return String(fieldValue).includes(String(conditionValue));
default:
return false;
}
}
2025-09-12 10:05:25 +09:00
/**
*
*/
private static evaluateSingleCondition(
condition: ConditionNode,
data: Record<string, any>
): boolean {
const { field, operator_type, value } = condition;
if (!field || !operator_type) {
return false;
}
const fieldValue = data[field];
switch (operator_type) {
case "=":
return fieldValue == value;
case "!=":
return fieldValue != value;
case ">":
return Number(fieldValue) > Number(value);
case "<":
return Number(fieldValue) < Number(value);
case ">=":
return Number(fieldValue) >= Number(value);
case "<=":
return Number(fieldValue) <= Number(value);
case "LIKE":
return String(fieldValue).includes(String(value));
default:
return false;
}
}
/**
*
*/
private static async executeTargetAction(
action: TargetAction,
sourceData: Record<string, any>,
companyCode: string
): Promise<void> {
2025-09-15 10:11:22 +09:00
// 액션별 조건 평가
if (action.conditions && action.conditions.length > 0) {
const conditionMet = await this.evaluateActionConditions(
action.conditions,
sourceData
);
if (!conditionMet) {
logger.info(
`Action conditions not met for action ${action.id}, skipping execution`
);
return;
}
}
2025-09-12 10:05:25 +09:00
// 필드 매핑을 통해 대상 데이터 생성
const targetData: Record<string, any> = {};
for (const mapping of action.fieldMappings) {
let value = sourceData[mapping.sourceField];
// 변환 함수 적용
if (mapping.transformFunction) {
value = this.applyTransformFunction(value, mapping.transformFunction);
}
// 기본값 설정
if (value === undefined || value === null) {
value = mapping.defaultValue;
}
targetData[mapping.targetField] = value;
}
// 회사 코드 추가
targetData.company_code = companyCode;
// 액션 타입별 실행
switch (action.actionType) {
case "insert":
await this.executeInsertAction(action.targetTable, targetData);
break;
case "update":
await this.executeUpdateAction(
action.targetTable,
targetData,
2025-09-15 10:11:22 +09:00
null // 액션별 조건은 이미 평가했으므로 WHERE 조건은 null
2025-09-12 10:05:25 +09:00
);
break;
case "delete":
await this.executeDeleteAction(
action.targetTable,
targetData,
2025-09-15 10:11:22 +09:00
null // 액션별 조건은 이미 평가했으므로 WHERE 조건은 null
2025-09-12 10:05:25 +09:00
);
break;
case "upsert":
await this.executeUpsertAction(action.targetTable, targetData);
break;
default:
throw new Error(`Unsupported action type: ${action.actionType}`);
}
}
/**
* INSERT
*/
private static async executeInsertAction(
tableName: string,
data: Record<string, any>
): Promise<void> {
// 동적 테이블 INSERT 실행
const sql = `INSERT INTO ${tableName} (${Object.keys(data).join(", ")}) VALUES (${Object.keys(
data
)
.map(() => "?")
.join(", ")})`;
await prisma.$executeRawUnsafe(sql, ...Object.values(data));
logger.info(`Inserted data into ${tableName}:`, data);
}
/**
* UPDATE
*/
private static async executeUpdateAction(
tableName: string,
data: Record<string, any>,
conditions?: ConditionNode
): Promise<void> {
// 조건이 없으면 실행하지 않음 (안전장치)
if (!conditions) {
throw new Error(
"UPDATE action requires conditions to prevent accidental mass updates"
);
}
// 동적 테이블 UPDATE 실행
const setClause = Object.keys(data)
.map((key) => `${key} = ?`)
.join(", ");
const whereClause = this.buildWhereClause(conditions);
const sql = `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause}`;
await prisma.$executeRawUnsafe(sql, ...Object.values(data));
logger.info(`Updated data in ${tableName}:`, data);
}
/**
* DELETE
*/
private static async executeDeleteAction(
tableName: string,
data: Record<string, any>,
conditions?: ConditionNode
): Promise<void> {
// 조건이 없으면 실행하지 않음 (안전장치)
if (!conditions) {
throw new Error(
"DELETE action requires conditions to prevent accidental mass deletions"
);
}
// 동적 테이블 DELETE 실행
const whereClause = this.buildWhereClause(conditions);
const sql = `DELETE FROM ${tableName} WHERE ${whereClause}`;
await prisma.$executeRawUnsafe(sql);
logger.info(`Deleted data from ${tableName} with conditions`);
}
/**
* UPSERT
*/
private static async executeUpsertAction(
tableName: string,
data: Record<string, any>
): Promise<void> {
// PostgreSQL UPSERT 구현
const columns = Object.keys(data);
const values = Object.values(data);
const conflictColumns = ["id", "company_code"]; // 기본 충돌 컬럼
const sql = `
INSERT INTO ${tableName} (${columns.join(", ")})
VALUES (${columns.map(() => "?").join(", ")})
ON CONFLICT (${conflictColumns.join(", ")})
DO UPDATE SET ${columns.map((col) => `${col} = EXCLUDED.${col}`).join(", ")}
`;
await prisma.$executeRawUnsafe(sql, ...values);
logger.info(`Upserted data into ${tableName}:`, data);
}
/**
* WHERE
*/
private static buildWhereClause(conditions: ConditionNode): string {
// 간단한 WHERE 절 구성 (실제 구현에서는 더 복잡한 로직 필요)
if (
conditions.type === "condition" &&
conditions.field &&
conditions.operator_type
) {
return `${conditions.field} ${conditions.operator_type} '${conditions.value}'`;
}
return "1=1"; // 기본값
}
/**
*
*/
private static applyTransformFunction(
value: any,
transformFunction: string
): any {
try {
// 안전한 변환 함수들만 허용
switch (transformFunction) {
case "UPPER":
return String(value).toUpperCase();
case "LOWER":
return String(value).toLowerCase();
case "TRIM":
return String(value).trim();
case "NOW":
return new Date();
case "UUID":
return require("crypto").randomUUID();
default:
logger.warn(`Unknown transform function: ${transformFunction}`);
return value;
}
} catch (error) {
logger.error(
`Error applying transform function ${transformFunction}:`,
error
);
return value;
}
}
/**
* (/)
*/
static async testConditionalConnection(
diagramId: number,
testData: Record<string, any>,
companyCode: string
): Promise<{ conditionMet: boolean; result?: ExecutionResult }> {
try {
const diagram = await prisma.dataflow_diagrams.findUnique({
where: { diagram_id: diagramId },
});
if (!diagram) {
throw new Error(`Diagram ${diagramId} not found`);
}
2025-09-12 11:33:54 +09:00
const control = diagram.control as unknown as ConditionControl;
2025-09-12 10:05:25 +09:00
// 조건 평가만 수행
const conditionMet = control.conditionTree
? await this.evaluateCondition(control.conditionTree, testData)
: true;
if (conditionMet) {
// 실제 실행 (테스트 모드)
const result = await this.executeDiagramTrigger(
diagram,
testData,
companyCode
);
return { conditionMet: true, result };
}
return { conditionMet: false };
} catch (error) {
logger.error("Error testing conditional connection:", error);
throw error;
}
}
}
export default EventTriggerService;