204 lines
5.4 KiB
TypeScript
204 lines
5.4 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":
|
|
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`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|