ERP-node/frontend/lib/report/queryUtils.ts

95 lines
2.6 KiB
TypeScript
Raw Normal View History

/**
* queryUtils.ts
*
* SQL .
*
* []
* - QueryManager.tsx : 쿼리 +
* - modals/QuerySettingsTab.tsx : 인캔버스
*
* Phase 5-A에서 QueryManager의 .
* .
*/
/** SQL 안전성 검증 결과 */
export interface QueryValidationResult {
isValid: boolean;
errorMessage: string | null;
}
/**
* SELECT SQL .
* DML/DDL .
*/
export const validateQuerySafety = (sql: string): QueryValidationResult => {
if (!sql || sql.trim() === "") {
return { isValid: false, errorMessage: "쿼리를 입력해주세요." };
}
const dangerousKeywords = [
"DELETE",
"DROP",
"TRUNCATE",
"INSERT",
"UPDATE",
"ALTER",
"CREATE",
"REPLACE",
"MERGE",
"GRANT",
"REVOKE",
"EXECUTE",
"EXEC",
"CALL",
];
const upperSql = sql.toUpperCase().trim();
for (const keyword of dangerousKeywords) {
const regex = new RegExp(`\\b${keyword}\\b`, "i");
if (regex.test(upperSql)) {
return {
isValid: false,
errorMessage: `보안상의 이유로 ${keyword} 명령어는 사용할 수 없습니다. SELECT 쿼리만 허용됩니다.`,
};
}
}
if (!upperSql.startsWith("SELECT") && !upperSql.startsWith("WITH")) {
return {
isValid: false,
errorMessage: "SELECT 쿼리만 허용됩니다. 데이터 조회 용도로만 사용할 수 있습니다.",
};
}
const semicolonCount = (sql.match(/;/g) || []).length;
if (semicolonCount > 1 || (semicolonCount === 1 && !sql.trim().endsWith(";"))) {
return {
isValid: false,
errorMessage: "보안상의 이유로 여러 개의 쿼리를 동시에 실행할 수 없습니다.",
};
}
return { isValid: true, errorMessage: null };
};
/**
* SQL에서 ($1, $2 ) .
* () , .
*/
export const detectParameters = (sql: string): string[] => {
const withoutStrings = sql.replace(/'[^']*'/g, "");
const matches = withoutStrings.match(/\$\d+/g);
if (!matches) return [];
const seen = new Set<string>();
const result: string[] = [];
for (const match of matches) {
if (!seen.has(match)) {
seen.add(match);
result.push(match);
}
}
return result;
};