/** * 플로우 연결 서비스 */ import db from "../database/db"; import { FlowStepConnection, CreateFlowConnectionRequest } from "../types/flow"; export class FlowConnectionService { /** * 플로우 단계 연결 생성 */ async create( request: CreateFlowConnectionRequest ): Promise { // 순환 참조 체크 if ( await this.wouldCreateCycle( request.flowDefinitionId, request.fromStepId, request.toStepId ) ) { throw new Error( "Creating this connection would create a cycle in the flow" ); } const query = ` INSERT INTO flow_step_connection ( flow_definition_id, from_step_id, to_step_id, label ) VALUES ($1, $2, $3, $4) RETURNING * `; const result = await db.query(query, [ request.flowDefinitionId, request.fromStepId, request.toStepId, request.label || null, ]); return this.mapToFlowConnection(result[0]); } /** * 특정 플로우의 모든 연결 조회 */ async findByFlowId(flowDefinitionId: number): Promise { const query = ` SELECT * FROM flow_step_connection WHERE flow_definition_id = $1 ORDER BY id ASC `; const result = await db.query(query, [flowDefinitionId]); return result.map(this.mapToFlowConnection); } /** * 플로우 연결 단일 조회 */ async findById(id: number): Promise { const query = "SELECT * FROM flow_step_connection WHERE id = $1"; const result = await db.query(query, [id]); if (result.length === 0) { return null; } return this.mapToFlowConnection(result[0]); } /** * 플로우 연결 삭제 */ async delete(id: number): Promise { const query = "DELETE FROM flow_step_connection WHERE id = $1 RETURNING id"; const result = await db.query(query, [id]); return result.length > 0; } /** * 특정 단계에서 나가는 연결 조회 */ async findOutgoingConnections(stepId: number): Promise { const query = ` SELECT * FROM flow_step_connection WHERE from_step_id = $1 ORDER BY id ASC `; const result = await db.query(query, [stepId]); return result.map(this.mapToFlowConnection); } /** * 특정 단계로 들어오는 연결 조회 */ async findIncomingConnections(stepId: number): Promise { const query = ` SELECT * FROM flow_step_connection WHERE to_step_id = $1 ORDER BY id ASC `; const result = await db.query(query, [stepId]); return result.map(this.mapToFlowConnection); } /** * 순환 참조 체크 (DFS) */ private async wouldCreateCycle( flowDefinitionId: number, fromStepId: number, toStepId: number ): Promise { // toStepId에서 출발해서 fromStepId에 도달할 수 있는지 확인 const visited = new Set(); const stack = [toStepId]; while (stack.length > 0) { const current = stack.pop()!; if (current === fromStepId) { return true; // 순환 발견 } if (visited.has(current)) { continue; } visited.add(current); // 현재 노드에서 나가는 모든 연결 조회 const query = ` SELECT to_step_id FROM flow_step_connection WHERE flow_definition_id = $1 AND from_step_id = $2 `; const result = await db.query(query, [flowDefinitionId, current]); for (const row of result) { stack.push(row.to_step_id); } } return false; // 순환 없음 } /** * DB 행을 FlowStepConnection 객체로 변환 */ private mapToFlowConnection(row: any): FlowStepConnection { return { id: row.id, flowDefinitionId: row.flow_definition_id, fromStepId: row.from_step_id, toStepId: row.to_step_id, label: row.label, createdAt: row.created_at, }; } }