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

216 lines
5.6 KiB
TypeScript

/**
* 플로우 조건 파서
* 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":
case "=":
conditions.push(`${column} = $${paramIndex}`);
params.push(condition.value);
paramIndex++;
break;
case "not_equals":
case "!=":
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":
case ">":
conditions.push(`${column} > $${paramIndex}`);
params.push(condition.value);
paramIndex++;
break;
case "less_than":
case "<":
conditions.push(`${column} < $${paramIndex}`);
params.push(condition.value);
paramIndex++;
break;
case "greater_than_or_equal":
case ">=":
conditions.push(`${column} >= $${paramIndex}`);
params.push(condition.value);
paramIndex++;
break;
case "less_than_or_equal":
case "<=":
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`
);
}
}
}
}