/** * 플로우 조건 파서 * JSON 조건을 SQL WHERE 절로 변환 */ import { FlowCondition, FlowConditionGroup, SqlWhereResult, } from "../types/flow"; export class FlowConditionParser { /** * 조건 JSON을 SQL WHERE 절로 변환 */ static toSqlWhere( conditionGroup: FlowConditionGroup | null | undefined ): SqlWhereResult { if ( !conditionGroup || !conditionGroup.conditions || conditionGroup.conditions.length === 0 ) { return { where: "1=1", params: [] }; } const conditions: string[] = []; const params: any[] = []; let paramIndex = 1; for (const condition of conditionGroup.conditions) { const column = this.sanitizeColumnName(condition.column); switch (condition.operator) { case "equals": conditions.push(`${column} = $${paramIndex}`); params.push(condition.value); paramIndex++; break; case "not_equals": conditions.push(`${column} != $${paramIndex}`); params.push(condition.value); paramIndex++; break; case "in": if (Array.isArray(condition.value) && condition.value.length > 0) { const placeholders = condition.value .map(() => `$${paramIndex++}`) .join(", "); conditions.push(`${column} IN (${placeholders})`); params.push(...condition.value); } break; case "not_in": if (Array.isArray(condition.value) && condition.value.length > 0) { const placeholders = condition.value .map(() => `$${paramIndex++}`) .join(", "); conditions.push(`${column} NOT IN (${placeholders})`); params.push(...condition.value); } break; case "greater_than": conditions.push(`${column} > $${paramIndex}`); params.push(condition.value); paramIndex++; break; case "less_than": conditions.push(`${column} < $${paramIndex}`); params.push(condition.value); paramIndex++; break; case "greater_than_or_equal": conditions.push(`${column} >= $${paramIndex}`); params.push(condition.value); paramIndex++; break; case "less_than_or_equal": conditions.push(`${column} <= $${paramIndex}`); params.push(condition.value); paramIndex++; break; case "is_null": conditions.push(`${column} IS NULL`); break; case "is_not_null": conditions.push(`${column} IS NOT NULL`); break; case "like": conditions.push(`${column} LIKE $${paramIndex}`); params.push(`%${condition.value}%`); paramIndex++; break; case "not_like": conditions.push(`${column} NOT LIKE $${paramIndex}`); params.push(`%${condition.value}%`); paramIndex++; break; default: throw new Error(`Unsupported operator: ${condition.operator}`); } } if (conditions.length === 0) { return { where: "1=1", params: [] }; } const joinOperator = conditionGroup.type === "OR" ? " OR " : " AND "; const where = conditions.join(joinOperator); return { where, params }; } /** * SQL 인젝션 방지를 위한 컬럼명 검증 */ private static sanitizeColumnName(columnName: string): string { // 알파벳, 숫자, 언더스코어, 점(.)만 허용 (테이블명.컬럼명 형태 지원) if (!/^[a-zA-Z0-9_.]+$/.test(columnName)) { throw new Error(`Invalid column name: ${columnName}`); } return columnName; } /** * 조건 검증 */ static validateConditionGroup(conditionGroup: FlowConditionGroup): void { if (!conditionGroup) { throw new Error("Condition group is required"); } if (!["AND", "OR"].includes(conditionGroup.type)) { throw new Error("Condition group type must be AND or OR"); } if (!Array.isArray(conditionGroup.conditions)) { throw new Error("Conditions must be an array"); } for (const condition of conditionGroup.conditions) { this.validateCondition(condition); } } /** * 개별 조건 검증 */ private static validateCondition(condition: FlowCondition): void { if (!condition.column) { throw new Error("Column name is required"); } const validOperators = [ "equals", "not_equals", "in", "not_in", "greater_than", "less_than", "greater_than_or_equal", "less_than_or_equal", "is_null", "is_not_null", "like", "not_like", ]; if (!validOperators.includes(condition.operator)) { throw new Error(`Invalid operator: ${condition.operator}`); } // is_null, is_not_null은 value가 필요 없음 if (!["is_null", "is_not_null"].includes(condition.operator)) { if (condition.value === undefined || condition.value === null) { throw new Error( `Value is required for operator: ${condition.operator}` ); } } // in, not_in은 배열이어야 함 if (["in", "not_in"].includes(condition.operator)) { if (!Array.isArray(condition.value) || condition.value.length === 0) { throw new Error( `Operator ${condition.operator} requires a non-empty array value` ); } } } }